Compare commits

..

No commits in common. "develop" and "feature/room_members_perf" have entirely different histories.

527 changed files with 4676 additions and 15189 deletions

View File

@ -1,106 +1,11 @@
Changes in RiotX 0.5.0 (2019-XX-XX)
===================================================

Features:
-

Improvements:
- Reduce default release build log level, and lab option to enable more logs.

Other changes:
-

Bugfix:
- Fix crash due to missing informationData (#535)
- Progress in initial sync dialog is decreasing for a step and should not (#532)

Translations:
-

Build:
- Fix issue with version name (#533)
- Fix rendering issue of accepted third party invitation event

Changes in RiotX 0.4.0 (2019-XX-XX)
===================================================

Features:
- Display read receipts in timeline (#81)

Improvements:
- Reactions: Reinstate the ability to react with non-unicode keys (#307)

Bugfix:
- Fix text diff linebreak display (#441)
- Date change message repeats for each redaction until a normal message (#358)
- Slide-in reply icon is distorted (#423)
- Regression / e2e replies not encrypted
- Some video won't play
- Privacy: remove log of notifiable event (#519)
- Fix crash with EmojiCompat (#530)

Changes in RiotX 0.3.0 (2019-08-08)
===================================================

Features:
- Create Direct Room flow
- Handle `/markdown` command

Improvements:
- UI for pending edits (#193)
- UX image preview screen transition (#393)
- Basic support for resending failed messages (retry/remove)
- Enable proper cancellation of suspending functions (including db transaction)
- Enhances network connectivity checks in SDK
- Add "View Edit History" item in the message bottom sheet (#401)
- Cancel sync request on pause and timeout to 0 after pause (#404)

Other changes:
- Show sync progress also in room detail screen (#403)

Bugfix:
- Edited message: link confusion when (edited) appears in body (#398)
- Close detail room screen when the room is left with another client (#256)
- Clear notification for a room left on another client
- Fix messages with empty `in_reply_to` not rendering (#447)
- Fix clear cache (#408) and Logout (#205)
- Fix `(edited)` link can be copied to clipboard (#402)

Build:
- Split APK: generate one APK per arch, to reduce APK size of about 30%


Changes in RiotX 0.2.0 (2019-07-18)
===================================================

Features:
- Message Editing: View edit history (#121)
- Rooms filtering (#304)
- Edit in encrypted room

Improvements:
- Handle click on redacted events: view source and create permalink
- Improve long tap menu: reply on top, more compact (#368)
- Quick reply in timeline with swipe gesture (#167)
- Improve edit of replies
- Improve performance on Room Members and Users management (#381)

Other changes:
- migrate from rxbinding 2 to rxbinding 3

Bugfix:
- Fix regression on permalink click
- Fix crash reported by the PlayStore (#341)
- Fix Chat composer separator color in dark/black theme
- Fix bad layout for room directory filter (#349)
- Fix Copying link from a message shouldn't open context menu (#364)

Changes in RiotX 0.1.0 (2019-07-11) Changes in RiotX 0.1.0 (2019-07-11)
=================================================== ===================================================


First release! First release!


Mode details here: https://medium.com/@RiotChat/introducing-the-riotx-beta-for-android-b17952e8f771






======================================================= =======================================================
@ -108,7 +13,7 @@ Mode details here: https://medium.com/@RiotChat/introducing-the-riotx-beta-for-a
======================================================= =======================================================




Changes in RiotX 0.0.0 (2019-XX-XX) Changes in RiotX 0.XX (2019-XX-XX)
=================================================== ===================================================


Features: Features:

View File

@ -1,4 +1,4 @@
[![Buildkite](https://badge.buildkite.com/657d3db27364448d69d54f66c690f7788bc6aa80a7628e37f3.svg?branch=develop)](https://buildkite.com/matrix-dot-org/riotx-android/builds?branch=develop) [![Buildkite](https://badge.buildkite.com/657d3db27364448d69d54f66c690f7788bc6aa80a7628e37f3.svg?branch=develop)](https://buildkite.com/matrix-dot-org/riotx-android)
[![Weblate](https://translate.riot.im/widgets/riot-android/-/svg-badge.svg)](https://translate.riot.im/engage/riot-android/?utm_source=widget) [![Weblate](https://translate.riot.im/widgets/riot-android/-/svg-badge.svg)](https://translate.riot.im/engage/riot-android/?utm_source=widget)
[![RiotX Android Matrix room #riot-android:matrix.org](https://img.shields.io/matrix/riotx:matrix.org.svg?label=%23RiotX:matrix.org&logo=matrix&server_fqdn=matrix.org)](https://matrix.to/#/#riotx:matrix.org) [![RiotX Android Matrix room #riot-android:matrix.org](https://img.shields.io/matrix/riotx:matrix.org.svg?label=%23RiotX:matrix.org&logo=matrix&server_fqdn=matrix.org)](https://matrix.to/#/#riotx:matrix.org)
[![Quality Gate](https://sonarcloud.io/api/project_badges/measure?project=vector.android.riotx&metric=alert_status)](https://sonarcloud.io/dashboard?id=vector.android.riotx) [![Quality Gate](https://sonarcloud.io/api/project_badges/measure?project=vector.android.riotx&metric=alert_status)](https://sonarcloud.io/dashboard?id=vector.android.riotx)
@ -7,31 +7,14 @@


# RiotX Android # RiotX Android


RiotX is an Android Matrix Client currently in beta but in active development. RiotX is an Android Matrix Client currently in development. The application is not yet available on the PlayStore.


It is a total rewrite of [Riot-Android](https://github.com/vector-im/riot-android) with a new user experience. RiotX will become the official replacement as soon as all features are implemented. It's based on a new Matrix SDK, written in Kotlin.


[<img src="https://play.google.com/intl/en_us/badges/images/generic/en_badge_web_generic.png" alt="Get it on Google Play" height="60">](https://play.google.com/store/apps/details?id=im.vector.riotx) Download nightly build here: [![Buildkite](https://badge.buildkite.com/657d3db27364448d69d54f66c690f7788bc6aa80a7628e37f3.svg?branch=develop)](https://buildkite.com/matrix-dot-org/riotx-android/builds?branch=develop)

Nightly build: [![Buildkite](https://badge.buildkite.com/657d3db27364448d69d54f66c690f7788bc6aa80a7628e37f3.svg?branch=develop)](https://buildkite.com/matrix-dot-org/riotx-android/builds?branch=develop)

# New Android SDK

RiotX is based on a new Android SDK fully written in Kotlin (like RiotX). In order to make the early development as fast as possible, RiotX and the new SDK currently share the same git repository. We will make separate repos once the API is stable enough.


# Roadmap

The current target is to release an application out of beta with the same level of features (and even more) as Riot.
The roadmap has 3 phases:

- [phase 0](https://github.com/vector-im/riotX-android/labels/phase0): Prototyping / Project setup
- [phase 1](https://github.com/vector-im/riotX-android/labels/phase1): Beta release to the Play Store
- [phase 2](https://github.com/vector-im/riotX-android/labels/phase2): Out of beta


Matrix Room: [![RiotX Android Matrix room #riot-android:matrix.org](https://img.shields.io/matrix/riotx:matrix.org.svg?label=%23RiotX:matrix.org&logo=matrix&server_fqdn=matrix.org)](https://matrix.to/#/#riotx:matrix.org)


## Contributing ## Contributing


Please refer to [CONTRIBUTING.md](https://github.com/vector-im/riotX-android/blob/develop/CONTRIBUTING.md) if you want to contribute on Matrix Android projects! Please refer to [CONTRIBUTING.md](https://github.com/vector-im/riotX-android/blob/develop/CONTRIBUTING.md) if you want to contribute the Matrix on Android projects!

Come chat with the community in the dedicated Matrix [room](https://matrix.to/#/#riotx:matrix.org).

View File

@ -1,5 +1,3 @@
import javax.tools.JavaCompiler

// Top-level build file where you can add configuration options common to all sub-projects/modules. // Top-level build file where you can add configuration options common to all sub-projects/modules.


buildscript { buildscript {
@ -47,26 +45,7 @@ allprojects {
maven { url 'https://oss.sonatype.org/content/repositories/snapshots/' } maven { url 'https://oss.sonatype.org/content/repositories/snapshots/' }
google() google()
jcenter() jcenter()
maven {
url 'https://repo.adobe.com/nexus/content/repositories/public/'
content {
includeGroupByRegex "diff_match_patch"
}
}
} }

tasks.withType(JavaCompile).all {
options.compilerArgs += [
'-Adagger.gradle.incremental=enabled'
]
}

afterEvaluate {
extensions.findByName("kapt")?.arguments {
arg("dagger.gradle.incremental", "enabled")
}
}

} }


task clean(type: Delete) { task clean(type: Delete) {

View File

@ -1,4 +1,4 @@
This document aims to describe how RiotX android displays notifications to the end user. It also clarifies notifications and background settings in the app. This document aims to describe how Riot X android displays notifications to the end user. It also clarifies notifications and background settings in the app.


# Table of Contents # Table of Contents
1. [Prerequisites Knowledge](#prerequisites-knowledge) 1. [Prerequisites Knowledge](#prerequisites-knowledge)
@ -50,7 +50,7 @@ By default, this is 0, so the server will return immediately even if the respons


**delay** is a client preference. When the server responds to a sync request, the client waits for `delay`before calling a new sync. **delay** is a client preference. When the server responds to a sync request, the client waits for `delay`before calling a new sync.


When the RiotX Android app is open (i.e in foreground state), the default timeout is 30 seconds, and delay is 0. When the Riot X Android app is open (i.e in foreground state), the default timeout is 30 seconds, and delay is 0.


## How does a mobile app receives push notification ## How does a mobile app receives push notification


@ -86,7 +86,7 @@ This need some disambiguation, because it is the source of common confusion:
In order to send a push to a mobile, App developers need to have a server that will use the FCM APIs, and these APIs requires authentication! In order to send a push to a mobile, App developers need to have a server that will use the FCM APIs, and these APIs requires authentication!
This server is called a **Push Gateway** in the matrix world This server is called a **Push Gateway** in the matrix world


That means that RiotX Android, a matrix client created by New Vector, is using a **Push Gateway** with the needed credentials (FCM API secret Key) in order to send push to the New Vector client. That means that Riot X Android, a matrix client created by New Vector, is using a **Push Gateway** with the needed credentials (FCM API secret Key) in order to send push to the New Vector client.


If you create your own matrix client, you will also need to deploy an instance of a **Push Gateway** with the credentials needed to use FCM for your app. If you create your own matrix client, you will also need to deploy an instance of a **Push Gateway** with the credentials needed to use FCM for your app.


@ -223,7 +223,7 @@ Upon reception of the FCM push, RiotX will perform a sync call to the Home Serve
* The sync generates additional notifications (e.g an encrypted message where the user is mentioned detected locally) * The sync generates additional notifications (e.g an encrypted message where the user is mentioned detected locally)
* The sync takes too long and the process is killed before completion, or network is not reliable and the sync fails. * The sync takes too long and the process is killed before completion, or network is not reliable and the sync fails.


RiotX implements several strategies in these cases (TODO document) Riot X implements several strategies in these cases (TODO document)


## FCM Fallback mode ## FCM Fallback mode



View File

@ -33,12 +33,11 @@ android {
} }


dependencies { dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation project(":matrix-sdk-android") implementation project(":matrix-sdk-android")
implementation 'androidx.appcompat:appcompat:1.1.0-beta01' implementation 'androidx.appcompat:appcompat:1.1.0-beta01'
implementation 'io.reactivex.rxjava2:rxkotlin:2.3.0' implementation 'io.reactivex.rxjava2:rxkotlin:2.3.0'
implementation 'io.reactivex.rxjava2:rxandroid:2.1.1' implementation 'io.reactivex.rxjava2:rxandroid:2.1.1'
// Paging
implementation "androidx.paging:paging-runtime-ktx:2.1.0"


testImplementation 'junit:junit:4.12' testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test:runner:1.2.0' androidTestImplementation 'androidx.test:runner:1.2.0'

View File

@ -20,8 +20,6 @@ import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import io.reactivex.Observable import io.reactivex.Observable
import io.reactivex.android.MainThreadDisposable import io.reactivex.android.MainThreadDisposable
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.schedulers.Schedulers


private class LiveDataObservable<T>( private class LiveDataObservable<T>(
private val liveData: LiveData<T>, private val liveData: LiveData<T>,
@ -59,5 +57,5 @@ private class LiveDataObservable<T>(
} }


fun <T> LiveData<T>.asObservable(): Observable<T> { fun <T> LiveData<T>.asObservable(): Observable<T> {
return LiveDataObservable(this).observeOn(Schedulers.computation()) return LiveDataObservable(this)
} }

View File

@ -1,39 +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.rx

import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.util.Cancelable
import io.reactivex.CompletableEmitter
import io.reactivex.SingleEmitter

internal class MatrixCallbackCompletable<T>(private val completableEmitter: CompletableEmitter) : MatrixCallback<T> {

override fun onSuccess(data: T) {
completableEmitter.onComplete()
}

override fun onFailure(failure: Throwable) {
completableEmitter.tryOnError(failure)
}
}

fun Cancelable.toCompletable(completableEmitter: CompletableEmitter) {
completableEmitter.setCancellable {
this.cancel()
}
}

View File

@ -1,38 +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.rx

import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.util.Cancelable
import io.reactivex.SingleEmitter

internal class MatrixCallbackSingle<T>(private val singleEmitter: SingleEmitter<T>) : MatrixCallback<T> {

override fun onSuccess(data: T) {
singleEmitter.onSuccess(data)
}

override fun onFailure(failure: Throwable) {
singleEmitter.tryOnError(failure)
}
}

fun <T> Cancelable.toSingle(singleEmitter: SingleEmitter<T>) {
singleEmitter.setCancellable {
this.cancel()
}
}

View File

@ -18,40 +18,27 @@ package im.vector.matrix.rx


import im.vector.matrix.android.api.session.room.Room import im.vector.matrix.android.api.session.room.Room
import im.vector.matrix.android.api.session.room.model.EventAnnotationsSummary import im.vector.matrix.android.api.session.room.model.EventAnnotationsSummary
import im.vector.matrix.android.api.session.room.model.ReadReceipt
import im.vector.matrix.android.api.session.room.model.RoomSummary import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import io.reactivex.Observable import io.reactivex.Observable
import io.reactivex.Single import io.reactivex.schedulers.Schedulers


class RxRoom(private val room: Room) { class RxRoom(private val room: Room) {


fun liveRoomSummary(): Observable<RoomSummary> { fun liveRoomSummary(): Observable<RoomSummary> {
return room.liveRoomSummary().asObservable() return room.liveRoomSummary().asObservable().observeOn(Schedulers.computation())
} }


fun liveRoomMemberIds(): Observable<List<String>> { fun liveRoomMemberIds(): Observable<List<String>> {
return room.getRoomMemberIdsLive().asObservable() return room.getRoomMemberIdsLive().asObservable().observeOn(Schedulers.computation())
} }


fun liveAnnotationSummary(eventId: String): Observable<EventAnnotationsSummary> { fun liveAnnotationSummary(eventId: String): Observable<EventAnnotationsSummary> {
return room.getEventSummaryLive(eventId).asObservable() return room.getEventSummaryLive(eventId).asObservable().observeOn(Schedulers.computation())
} }


fun liveTimelineEvent(eventId: String): Observable<TimelineEvent> { fun liveTimelineEvent(eventId: String): Observable<TimelineEvent> {
return room.liveTimeLineEvent(eventId).asObservable() return room.liveTimeLineEvent(eventId).asObservable().observeOn(Schedulers.computation())
}

fun loadRoomMembersIfNeeded(): Single<Unit> = Single.create {
room.loadRoomMembersIfNeeded(MatrixCallbackSingle(it)).toSingle(it)
}

fun joinRoom(viaServers: List<String> = emptyList()): Single<Unit> = Single.create {
room.join(viaServers, MatrixCallbackSingle(it)).toSingle(it)
}

fun liveEventReadReceipts(eventId: String): Observable<List<ReadReceipt>> {
return room.getEventReadReceiptsLive(eventId).asObservable()
} }


} }

View File

@ -16,55 +16,30 @@


package im.vector.matrix.rx package im.vector.matrix.rx


import androidx.paging.PagedList
import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.group.model.GroupSummary 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.pushers.Pusher
import im.vector.matrix.android.api.session.room.model.RoomSummary 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 import im.vector.matrix.android.api.session.sync.SyncState
import im.vector.matrix.android.api.session.user.model.User
import io.reactivex.Observable import io.reactivex.Observable
import io.reactivex.Single import io.reactivex.schedulers.Schedulers


class RxSession(private val session: Session) { class RxSession(private val session: Session) {


fun liveRoomSummaries(): Observable<List<RoomSummary>> { fun liveRoomSummaries(): Observable<List<RoomSummary>> {
return session.liveRoomSummaries().asObservable() return session.liveRoomSummaries().asObservable().observeOn(Schedulers.computation())
} }


fun liveGroupSummaries(): Observable<List<GroupSummary>> { fun liveGroupSummaries(): Observable<List<GroupSummary>> {
return session.liveGroupSummaries().asObservable() return session.liveGroupSummaries().asObservable().observeOn(Schedulers.computation())
} }


fun liveSyncState(): Observable<SyncState> { fun liveSyncState(): Observable<SyncState> {
return session.syncState().asObservable() return session.syncState().asObservable().observeOn(Schedulers.computation())
} }


fun livePushers(): Observable<List<Pusher>> { fun livePushers(): Observable<List<Pusher>> {
return session.livePushers().asObservable() return session.livePushers().asObservable().observeOn(Schedulers.computation())
}

fun liveUsers(): Observable<List<User>> {
return session.liveUsers().asObservable()
}

fun livePagedUsers(filter: String? = null): Observable<PagedList<User>> {
return session.livePagedUsers(filter).asObservable()
}

fun createRoom(roomParams: CreateRoomParams): Single<String> = Single.create {
session.createRoom(roomParams, MatrixCallbackSingle(it)).toSingle(it)
}

fun searchUsersDirectory(search: String,
limit: Int,
excludedUserIds: Set<String>): Single<List<User>> = Single.create {
session.searchUsersDirectory(search, limit, excludedUserIds, MatrixCallbackSingle(it)).toSingle(it)
}

fun joinRoom(roomId: String, viaServers: List<String> = emptyList()): Single<Unit> = Single.create {
session.joinRoom(roomId, viaServers, MatrixCallbackSingle(it)).toSingle(it)
} }


} }

View File

@ -94,6 +94,7 @@ dependencies {
def markwon_version = '3.0.0' def markwon_version = '3.0.0'
def daggerVersion = '2.23.1' def daggerVersion = '2.23.1'


implementation fileTree(dir: 'libs', include: ['*.aar'])
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"
@ -109,7 +110,7 @@ dependencies {
implementation 'com.squareup.retrofit2:converter-moshi:2.4.0' implementation 'com.squareup.retrofit2:converter-moshi:2.4.0'
implementation 'com.squareup.okhttp3:okhttp:3.14.1' implementation 'com.squareup.okhttp3:okhttp:3.14.1'
implementation 'com.squareup.okhttp3:logging-interceptor:3.10.0' implementation 'com.squareup.okhttp3:logging-interceptor:3.10.0'
implementation 'com.novoda:merlin:1.2.0' implementation 'com.novoda:merlin:1.1.6'
implementation "com.squareup.moshi:moshi-adapters:$moshi_version" implementation "com.squareup.moshi:moshi-adapters:$moshi_version"
kapt "com.squareup.moshi:moshi-kotlin-codegen:$moshi_version" kapt "com.squareup.moshi:moshi-kotlin-codegen:$moshi_version"


@ -125,6 +126,9 @@ dependencies {
// FP // FP
implementation "io.arrow-kt:arrow-core:$arrow_version" implementation "io.arrow-kt:arrow-core:$arrow_version"
implementation "io.arrow-kt:arrow-instances-core:$arrow_version" implementation "io.arrow-kt:arrow-instances-core:$arrow_version"
implementation "io.arrow-kt:arrow-effects:$arrow_version"
implementation "io.arrow-kt:arrow-effects-instances:$arrow_version"
implementation "io.arrow-kt:arrow-integration-retrofit-adapter:$arrow_version"


// olm lib is now hosted by jitpack: https://jitpack.io/#org.matrix.gitlab.matrix-org/olm // olm lib is now hosted by jitpack: https://jitpack.io/#org.matrix.gitlab.matrix-org/olm
implementation 'org.matrix.gitlab.matrix-org:olm:3.1.2' implementation 'org.matrix.gitlab.matrix-org:olm:3.1.2'

Binary file not shown.

View File

@ -16,10 +16,10 @@


package im.vector.matrix.android; package im.vector.matrix.android;


import androidx.annotation.Nullable;
import androidx.lifecycle.LiveData; import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData; import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.Observer; import androidx.lifecycle.Observer;
import androidx.annotation.Nullable;


import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;

View File

@ -21,7 +21,7 @@ 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.RealmCryptoStore
import im.vector.matrix.android.internal.crypto.store.db.RealmCryptoStoreModule import im.vector.matrix.android.internal.crypto.store.db.RealmCryptoStoreModule
import io.realm.RealmConfiguration import io.realm.RealmConfiguration
import kotlin.random.Random import java.util.*


internal class CryptoStoreHelper { internal class CryptoStoreHelper {


@ -35,7 +35,7 @@ internal class CryptoStoreHelper {
} }


fun createCredential() = Credentials( fun createCredential() = Credentials(
userId = "userId_" + Random.nextInt(), userId = "userId_" + Random().nextInt(),
homeServer = "http://matrix.org", homeServer = "http://matrix.org",
accessToken = "access_token", accessToken = "access_token",
refreshToken = null, refreshToken = null,

View File

@ -19,7 +19,11 @@ package im.vector.matrix.android.session.room.timeline
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import com.zhuinden.monarchy.Monarchy import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.InstrumentedTest import im.vector.matrix.android.InstrumentedTest
import im.vector.matrix.android.internal.database.helper.* import im.vector.matrix.android.internal.database.helper.add
import im.vector.matrix.android.internal.database.helper.addAll
import im.vector.matrix.android.internal.database.helper.isUnlinked
import im.vector.matrix.android.internal.database.helper.lastStateIndex
import im.vector.matrix.android.internal.database.helper.merge
import im.vector.matrix.android.internal.database.model.ChunkEntity import im.vector.matrix.android.internal.database.model.ChunkEntity
import im.vector.matrix.android.internal.session.room.timeline.PaginationDirection import im.vector.matrix.android.internal.session.room.timeline.PaginationDirection
import im.vector.matrix.android.session.room.timeline.RoomDataHelper.createFakeListOfEvents import im.vector.matrix.android.session.room.timeline.RoomDataHelper.createFakeListOfEvents

View File

@ -16,6 +16,7 @@


package im.vector.matrix.android.session.room.timeline package im.vector.matrix.android.session.room.timeline


import arrow.core.Try
import im.vector.matrix.android.internal.session.room.timeline.GetContextOfEventTask import im.vector.matrix.android.internal.session.room.timeline.GetContextOfEventTask
import im.vector.matrix.android.internal.session.room.timeline.PaginationDirection import im.vector.matrix.android.internal.session.room.timeline.PaginationDirection
import im.vector.matrix.android.internal.session.room.timeline.TokenChunkEventPersistor import im.vector.matrix.android.internal.session.room.timeline.TokenChunkEventPersistor
@ -23,7 +24,7 @@ import kotlin.random.Random


internal class FakeGetContextOfEventTask constructor(private val tokenChunkEventPersistor: TokenChunkEventPersistor) : GetContextOfEventTask { internal class FakeGetContextOfEventTask constructor(private val tokenChunkEventPersistor: TokenChunkEventPersistor) : GetContextOfEventTask {


override suspend fun execute(params: GetContextOfEventTask.Params): TokenChunkEventPersistor.Result { override suspend fun execute(params: GetContextOfEventTask.Params): Try<TokenChunkEventPersistor.Result> {
val fakeEvents = RoomDataHelper.createFakeListOfEvents(30) val fakeEvents = RoomDataHelper.createFakeListOfEvents(30)
val tokenChunkEvent = FakeTokenChunkEvent( val tokenChunkEvent = FakeTokenChunkEvent(
Random.nextLong(System.currentTimeMillis()).toString(), Random.nextLong(System.currentTimeMillis()).toString(),

View File

@ -16,6 +16,7 @@


package im.vector.matrix.android.session.room.timeline package im.vector.matrix.android.session.room.timeline


import arrow.core.Try
import im.vector.matrix.android.internal.session.room.timeline.PaginationTask import im.vector.matrix.android.internal.session.room.timeline.PaginationTask
import im.vector.matrix.android.internal.session.room.timeline.TokenChunkEventPersistor import im.vector.matrix.android.internal.session.room.timeline.TokenChunkEventPersistor
import javax.inject.Inject import javax.inject.Inject
@ -23,7 +24,7 @@ import kotlin.random.Random


internal class FakePaginationTask @Inject constructor(private val tokenChunkEventPersistor: TokenChunkEventPersistor) : PaginationTask { internal class FakePaginationTask @Inject constructor(private val tokenChunkEventPersistor: TokenChunkEventPersistor) : PaginationTask {


override suspend fun execute(params: PaginationTask.Params): TokenChunkEventPersistor.Result { override suspend fun execute(params: PaginationTask.Params): Try<TokenChunkEventPersistor.Result> {
val fakeEvents = RoomDataHelper.createFakeListOfEvents(30) val fakeEvents = RoomDataHelper.createFakeListOfEvents(30)
val tokenChunkEvent = FakeTokenChunkEvent(params.from, Random.nextLong(System.currentTimeMillis()).toString(), fakeEvents) val tokenChunkEvent = FakeTokenChunkEvent(params.from, Random.nextLong(System.currentTimeMillis()).toString(), fakeEvents)
return tokenChunkEventPersistor.insertInDb(tokenChunkEvent, params.roomId, params.direction) return tokenChunkEventPersistor.insertInDb(tokenChunkEvent, params.roomId, params.direction)

View File

@ -28,7 +28,7 @@ object MatrixPatterns {
// regex pattern to find matrix user ids in a string. // regex pattern to find matrix user ids in a string.
// See https://matrix.org/speculator/spec/HEAD/appendices.html#historical-user-ids // See https://matrix.org/speculator/spec/HEAD/appendices.html#historical-user-ids
private const val MATRIX_USER_IDENTIFIER_REGEX = "@[A-Z0-9\\x21-\\x39\\x3B-\\x7F]+$DOMAIN_REGEX" private const val MATRIX_USER_IDENTIFIER_REGEX = "@[A-Z0-9\\x21-\\x39\\x3B-\\x7F]+$DOMAIN_REGEX"
val PATTERN_CONTAIN_MATRIX_USER_IDENTIFIER = MATRIX_USER_IDENTIFIER_REGEX.toRegex(RegexOption.IGNORE_CASE) private val PATTERN_CONTAIN_MATRIX_USER_IDENTIFIER = MATRIX_USER_IDENTIFIER_REGEX.toRegex(RegexOption.IGNORE_CASE)


// regex pattern to find room ids in a string. // regex pattern to find room ids in a string.
private const val MATRIX_ROOM_IDENTIFIER_REGEX = "![A-Z0-9]+$DOMAIN_REGEX" private const val MATRIX_ROOM_IDENTIFIER_REGEX = "![A-Z0-9]+$DOMAIN_REGEX"
@ -123,9 +123,9 @@ object MatrixPatterns {
*/ */
fun isEventId(str: String?): Boolean { fun isEventId(str: String?): Boolean {
return str != null return str != null
&& (str matches PATTERN_CONTAIN_MATRIX_EVENT_IDENTIFIER && (str matches PATTERN_CONTAIN_MATRIX_EVENT_IDENTIFIER
|| str matches PATTERN_CONTAIN_MATRIX_EVENT_IDENTIFIER_V3 || str matches PATTERN_CONTAIN_MATRIX_EVENT_IDENTIFIER_V3
|| str matches PATTERN_CONTAIN_MATRIX_EVENT_IDENTIFIER_V4) || str matches PATTERN_CONTAIN_MATRIX_EVENT_IDENTIFIER_V4)
} }


/** /**
@ -137,23 +137,4 @@ object MatrixPatterns {
fun isGroupId(str: String?): Boolean { fun isGroupId(str: String?): Boolean {
return str != null && str matches PATTERN_CONTAIN_MATRIX_GROUP_IDENTIFIER return str != null && str matches PATTERN_CONTAIN_MATRIX_GROUP_IDENTIFIER
} }

/**
* Extract server name from a matrix id
*
* @param matrixId
* @return null if not found or if matrixId is null
*/
fun extractServerNameFromId(matrixId: String?): String? {
if (matrixId == null) {
return null
}

val index = matrixId.indexOf(":")

return if (index == -1) {
null
} else matrixId.substring(index + 1)

}
} }

View File

@ -31,7 +31,7 @@ import okhttp3.TlsVersion
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class HomeServerConnectionConfig( data class HomeServerConnectionConfig(
val homeServerUri: Uri, val homeServerUri: Uri,
val identityServerUri: Uri? = null, val identityServerUri: Uri,
val antiVirusServerUri: Uri? = null, val antiVirusServerUri: Uri? = null,
val allowedFingerprints: MutableList<Fingerprint> = ArrayList(), val allowedFingerprints: MutableList<Fingerprint> = ArrayList(),
val shouldPin: Boolean = false, val shouldPin: Boolean = false,
@ -48,7 +48,7 @@ data class HomeServerConnectionConfig(
class Builder { class Builder {


private lateinit var homeServerUri: Uri private lateinit var homeServerUri: Uri
private var identityServerUri: Uri? = null private lateinit var identityServerUri: Uri
private var antiVirusServerUri: Uri? = null private var antiVirusServerUri: Uri? = null
private val allowedFingerprints: MutableList<Fingerprint> = ArrayList() private val allowedFingerprints: MutableList<Fingerprint> = ArrayList()
private var shouldPin: Boolean = false private var shouldPin: Boolean = false

View File

@ -17,6 +17,7 @@
package im.vector.matrix.android.api.comparators package im.vector.matrix.android.api.comparators


import im.vector.matrix.android.api.interfaces.DatedObject import im.vector.matrix.android.api.interfaces.DatedObject
import java.util.*


object DatedObjectComparators { object DatedObjectComparators {



View File

@ -19,7 +19,7 @@ package im.vector.matrix.android.api.extensions
import im.vector.matrix.android.api.comparators.DatedObjectComparators import im.vector.matrix.android.api.comparators.DatedObjectComparators
import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo
import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
import java.util.Collections import java.util.*


/* ========================================================================================== /* ==========================================================================================
* MXDeviceInfo * MXDeviceInfo

View File

@ -26,13 +26,8 @@ import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class MatrixError( data class MatrixError(
@Json(name = "errcode") val code: String, @Json(name = "errcode") val code: String,
@Json(name = "error") val message: String, @Json(name = "error") val message: String

) {
@Json(name = "consent_uri") val consentUri: String? = null,
// RESOURCE_LIMIT_EXCEEDED data
@Json(name = "limit_type") val limitType: String? = null,
@Json(name = "admin_contact") val adminUri: String? = null) {



companion object { companion object {
const val FORBIDDEN = "M_FORBIDDEN" const val FORBIDDEN = "M_FORBIDDEN"
@ -60,8 +55,5 @@ data class MatrixError(
const val M_CONSENT_NOT_GIVEN = "M_CONSENT_NOT_GIVEN" const val M_CONSENT_NOT_GIVEN = "M_CONSENT_NOT_GIVEN"
const val RESOURCE_LIMIT_EXCEEDED = "M_RESOURCE_LIMIT_EXCEEDED" const val RESOURCE_LIMIT_EXCEEDED = "M_RESOURCE_LIMIT_EXCEEDED"
const val WRONG_ROOM_KEYS_VERSION = "M_WRONG_ROOM_KEYS_VERSION" const val WRONG_ROOM_KEYS_VERSION = "M_WRONG_ROOM_KEYS_VERSION"

// Possible value for "limit_type"
const val LIMIT_TYPE_MAU = "monthly_active_user"
} }
} }

View File

@ -42,7 +42,7 @@ object MatrixLinkify {
hasMatch = true hasMatch = true
val startPos = match.range.first val startPos = match.range.first
if (startPos == 0 || text[startPos - 1] != '/') { if (startPos == 0 || text[startPos - 1] != '/') {
val endPos = match.range.last + 1 val endPos = match.range.last
val url = text.substring(match.range) val url = text.substring(match.range)
val span = MatrixPermalinkSpan(url, callback) val span = MatrixPermalinkSpan(url, callback)
spannable.setSpan(span, startPos, endPos, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) spannable.setSpan(span, startPos, endPos, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)

View File

@ -18,7 +18,6 @@ package im.vector.matrix.android.api.permalinks


import android.text.style.ClickableSpan import android.text.style.ClickableSpan
import android.view.View import android.view.View
import im.vector.matrix.android.api.permalinks.MatrixPermalinkSpan.Callback


/** /**
* This MatrixPermalinkSpan is a clickable span which use a [Callback] to communicate back. * This MatrixPermalinkSpan is a clickable span which use a [Callback] to communicate back.

View File

@ -18,7 +18,6 @@ package im.vector.matrix.android.api.pushrules
import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.pushrules.rest.PushRule import im.vector.matrix.android.api.pushrules.rest.PushRule
import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.util.Cancelable


interface PushRuleService { interface PushRuleService {


@ -32,7 +31,7 @@ interface PushRuleService {


//TODO update rule //TODO update rule


fun updatePushRuleEnableStatus(kind: String, pushRule: PushRule, enabled: Boolean, callback: MatrixCallback<Unit>): Cancelable fun updatePushRuleEnableStatus(kind: String, pushRule: PushRule, enabled: Boolean, callback: MatrixCallback<Unit>)


fun addPushRuleListener(listener: PushRuleListener) fun addPushRuleListener(listener: PushRuleListener)


@ -42,7 +41,6 @@ interface PushRuleService {


interface PushRuleListener { interface PushRuleListener {
fun onMatchRule(event: Event, actions: List<Action>) fun onMatchRule(event: Event, actions: List<Action>)
fun onRoomLeft(roomId: String)
fun batchFinish() fun batchFinish()
} }
} }

View File

@ -18,17 +18,18 @@ package im.vector.matrix.android.api.pushrules
import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.room.RoomService import im.vector.matrix.android.api.session.room.RoomService
import timber.log.Timber import timber.log.Timber
import java.util.regex.Pattern


private val regex = Regex("^(==|<=|>=|<|>)?(\\d*)$") private val regex = Pattern.compile("^(==|<=|>=|<|>)?(\\d*)$")


class RoomMemberCountCondition(val iz: String) : Condition(Kind.room_member_count) { class RoomMemberCountCondition(val `is`: String) : Condition(Kind.room_member_count) {


override fun isSatisfied(conditionResolver: ConditionResolver): Boolean { override fun isSatisfied(conditionResolver: ConditionResolver): Boolean {
return conditionResolver.resolveRoomMemberCountCondition(this) return conditionResolver.resolveRoomMemberCountCondition(this)
} }


override fun technicalDescription(): String { override fun technicalDescription(): String {
return "Room member count is $iz" return "Room member count is $`is`"
} }


fun isSatisfied(event: Event, session: RoomService?): Boolean { fun isSatisfied(event: Event, session: RoomService?): Boolean {
@ -55,9 +56,12 @@ class RoomMemberCountCondition(val iz: String) : Condition(Kind.room_member_coun
*/ */
private fun parseIsField(): Pair<String?, Int>? { private fun parseIsField(): Pair<String?, Int>? {
try { try {
val match = regex.find(iz) ?: return null val match = regex.matcher(`is`)
val (prefix, count) = match.destructured if (match.find()) {
return prefix to count.toInt() val prefix = match.group(1)
val count = match.group(2).toInt()
return prefix to count
}
} catch (t: Throwable) { } catch (t: Throwable) {
Timber.d(t) Timber.d(t)
} }

View File

@ -20,10 +20,10 @@ import androidx.lifecycle.LiveData


interface InitialSyncProgressService { interface InitialSyncProgressService {


fun getInitialSyncProgressStatus() : LiveData<Status?> fun getLiveStatus() : LiveData<Status?>


data class Status( data class Status(
@StringRes val statusText: Int, @StringRes val statusText: Int?,
val percentProgress: Int = 0 val percentProgress: Int = 0
) )
} }

View File

@ -57,9 +57,6 @@ interface Session :
*/ */
val sessionParams: SessionParams val sessionParams: SessionParams


/**
* Useful shortcut to get access to the userId
*/
val myUserId: String val myUserId: String
get() = sessionParams.credentials.userId get() = sessionParams.credentials.userId


@ -87,7 +84,7 @@ interface Session :
/** /**
* This method start the sync thread. * This method start the sync thread.
*/ */
fun startSync(fromForeground : Boolean) fun startSync()


/** /**
* This method stop the sync thread. * This method stop the sync thread.

View File

@ -26,12 +26,14 @@ import im.vector.matrix.android.api.session.events.model.Content
import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.internal.crypto.MXEventDecryptionResult import im.vector.matrix.android.internal.crypto.MXEventDecryptionResult
import im.vector.matrix.android.internal.crypto.NewSessionListener import im.vector.matrix.android.internal.crypto.NewSessionListener
import im.vector.matrix.android.internal.crypto.attachments.ElementToDecrypt
import im.vector.matrix.android.internal.crypto.model.ImportRoomKeysResult import im.vector.matrix.android.internal.crypto.model.ImportRoomKeysResult
import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo 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.MXEncryptEventContentResult
import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap
import im.vector.matrix.android.internal.crypto.model.rest.DevicesListResponse import im.vector.matrix.android.internal.crypto.model.rest.DevicesListResponse
import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody
import java.io.File


interface CryptoService { interface CryptoService {



View File

@ -20,9 +20,6 @@ import android.text.TextUtils
import com.squareup.moshi.Json import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
import im.vector.matrix.android.api.session.crypto.MXCryptoError import im.vector.matrix.android.api.session.crypto.MXCryptoError
import im.vector.matrix.android.api.session.room.model.message.MessageContent
import im.vector.matrix.android.api.session.room.model.message.MessageType
import im.vector.matrix.android.api.session.room.send.SendState
import im.vector.matrix.android.api.util.JsonDict import im.vector.matrix.android.api.util.JsonDict
import im.vector.matrix.android.internal.crypto.algorithms.olm.OlmDecryptionResult import im.vector.matrix.android.internal.crypto.algorithms.olm.OlmDecryptionResult
import im.vector.matrix.android.internal.di.MoshiProvider import im.vector.matrix.android.internal.di.MoshiProvider
@ -82,15 +79,9 @@ data class Event(
) { ) {




@Transient
var mxDecryptionResult: OlmDecryptionResult? = null var mxDecryptionResult: OlmDecryptionResult? = null

@Transient
var mCryptoError: MXCryptoError.ErrorType? = null var mCryptoError: MXCryptoError.ErrorType? = null


@Transient
var sendState: SendState = SendState.UNKNOWN



/** /**
* Check if event is a state event. * Check if event is a state event.
@ -104,6 +95,42 @@ data class Event(
// Crypto // Crypto
//============================================================================================================== //==============================================================================================================


// /**
// * For encrypted events, the plaintext payload for the event.
// * This is a small MXEvent instance with typically value for `type` and 'content' fields.
// */
// @Transient
// var mClearEvent: Event? = null
// private set
//
// /**
// * Curve25519 key which we believe belongs to the sender of the event.
// * See `senderKey` property.
// */
// @Transient
// private var mSenderCurve25519Key: String? = null
//
// /**
// * Ed25519 key which the sender of this event (for olm) or the creator of the megolm session (for megolm) claims to own.
// * See `claimedEd25519Key` property.
// */
// @Transient
// private var mClaimedEd25519Key: String? = null
//
// /**
// * Curve25519 keys of devices involved in telling us about the senderCurve25519Key and claimedEd25519Key.
// * See `forwardingCurve25519KeyChain` property.
// */
// @Transient
// private var mForwardingCurve25519KeyChain: List<String> = ArrayList()
//
// /**
// * Decryption error
// */
// @Transient
// var mCryptoError: MXCryptoError? = null
// private set

/** /**
* @return true if this event is encrypted. * @return true if this event is encrypted.
*/ */
@ -111,11 +138,51 @@ data class Event(
return TextUtils.equals(type, EventType.ENCRYPTED) return TextUtils.equals(type, EventType.ENCRYPTED)
} }


/**
* Update the clear data on this event.
* This is used after decrypting an event; it should not be used by applications.
*
* @param decryptionResult the decryption result, including the plaintext and some key info.
*/
// internal fun setClearData(decryptionResult: MXEventDecryptionResult?) {
// mClearEvent = null
// if (decryptionResult != null) {
// if (decryptionResult.clearEvent != null) {
// val adapter = MoshiProvider.providesMoshi().adapter(Event::class.java)
// mClearEvent = adapter.fromJsonValue(decryptionResult.clearEvent)
//
// if (mClearEvent != null) {
// mSenderCurve25519Key = decryptionResult.senderCurve25519Key
// mClaimedEd25519Key = decryptionResult.claimedEd25519Key
// mForwardingCurve25519KeyChain = decryptionResult.forwardingCurve25519KeyChain
//
// // For encrypted events with relation, the m.relates_to is kept in clear, so we need to put it back
// // in the clear event
// try {
// content?.get("m.relates_to")?.let { clearRelates ->
// mClearEvent = mClearEvent?.copy(
// content = HashMap(mClearEvent!!.content).apply {
// this["m.relates_to"] = clearRelates
// }
// )
// }
// } catch (e: Exception) {
// Timber.e(e, "Unable to restore 'm.relates_to' the clear event")
// }
// }
//
//
// }
// }
// mCryptoError = null
// }

/** /**
* @return The curve25519 key that sent this event. * @return The curve25519 key that sent this event.
*/ */
fun getSenderKey(): String? { fun getSenderKey(): String? {
return mxDecryptionResult?.senderKey return mxDecryptionResult?.senderKey
// return mClearEvent?.mSenderCurve25519Key ?: mSenderCurve25519Key
} }


/** /**
@ -123,13 +190,23 @@ data class Event(
*/ */
fun getKeysClaimed(): Map<String, String> { fun getKeysClaimed(): Map<String, String> {
return mxDecryptionResult?.keysClaimed ?: HashMap() return mxDecryptionResult?.keysClaimed ?: HashMap()
// val res = HashMap<String, String>()
//
// val claimedEd25519Key = if (null != mClearEvent) mClearEvent!!.mClaimedEd25519Key else mClaimedEd25519Key
//
// if (null != claimedEd25519Key) {
// res["ed25519"] = claimedEd25519Key
// }
//
// return res
} }

//
/** /**
* @return the event type * @return the event type
*/ */
fun getClearType(): String { fun getClearType(): String {
return mxDecryptionResult?.payload?.get("type")?.toString() ?: type return mxDecryptionResult?.payload?.get("type")?.toString()
?: type//get("type")?.toString() ?: type
} }


/** /**
@ -139,8 +216,30 @@ data class Event(
return mxDecryptionResult?.payload?.get("content") as? Content ?: content return mxDecryptionResult?.payload?.get("content") as? Content ?: content
} }


// /**
// * @return the linked crypto error
// */
// fun getCryptoError(): MXCryptoError? {
// return mCryptoError
// }
//
// /**
// * Update the linked crypto error
// *
// * @param error the new crypto error.
// */
// fun setCryptoError(error: MXCryptoError?) {
// mCryptoError = error
// if (null != error) {
// mClearEvent = null
// }
// }


fun toContentStringWithIndent(): String { fun toContentStringWithIndent(): String {
val contentMap = toContent()?.toMutableMap() ?: HashMap() val contentMap = this.toContent()?.toMutableMap() ?: HashMap()
contentMap.remove("mxDecryptionResult")
contentMap.remove("mCryptoError")
return JSONObject(contentMap).toString(4) return JSONObject(contentMap).toString(4)
} }


@ -173,7 +272,6 @@ data class Event(
if (redacts != other.redacts) return false if (redacts != other.redacts) return false
if (mxDecryptionResult != other.mxDecryptionResult) return false if (mxDecryptionResult != other.mxDecryptionResult) return false
if (mCryptoError != other.mCryptoError) return false if (mCryptoError != other.mCryptoError) return false
if (sendState != other.sendState) return false


return true return true
} }
@ -191,27 +289,6 @@ data class Event(
result = 31 * result + (redacts?.hashCode() ?: 0) result = 31 * result + (redacts?.hashCode() ?: 0)
result = 31 * result + (mxDecryptionResult?.hashCode() ?: 0) result = 31 * result + (mxDecryptionResult?.hashCode() ?: 0)
result = 31 * result + (mCryptoError?.hashCode() ?: 0) result = 31 * result + (mCryptoError?.hashCode() ?: 0)
result = 31 * result + sendState.hashCode()
return result return result
} }

}


fun Event.isTextMessage(): Boolean {
return getClearType() == EventType.MESSAGE
&& when (getClearContent()?.toModel<MessageContent>()?.type) {
MessageType.MSGTYPE_TEXT,
MessageType.MSGTYPE_EMOTE,
MessageType.MSGTYPE_NOTICE -> true
else -> false
}
}

fun Event.isImageMessage(): Boolean {
return getClearType() == EventType.MESSAGE
&& when (getClearContent()?.toModel<MessageContent>()?.type) {
MessageType.MSGTYPE_IMAGE -> true
else -> false
}
} }

View File

@ -17,7 +17,7 @@ package im.vector.matrix.android.api.session.events.model




/** /**
* Constants defining known event relation types from Matrix specifications * Constants defining known event relation types from Matrix specifications.
*/ */
object RelationType { object RelationType {


@ -25,7 +25,7 @@ object RelationType {
const val ANNOTATION = "m.annotation" const val ANNOTATION = "m.annotation"
/** Lets you define an event which replaces an existing event.*/ /** Lets you define an event which replaces an existing event.*/
const val REPLACE = "m.replace" const val REPLACE = "m.replace"
/** Lets you define an event which references an existing event.*/ /** ets you define an event which references an existing event.*/
const val REFERENCE = "m.reference" const val REFERENCE = "m.reference"


} }

View File

@ -17,7 +17,7 @@ package im.vector.matrix.android.api.session.pushers


import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.MatrixCallback
import java.util.UUID import java.util.*




interface PushersService { interface PushersService {

View File

@ -30,17 +30,20 @@ interface RoomDirectoryService {
/** /**
* Get rooms from directory * Get rooms from directory
*/ */
fun getPublicRooms(server: String?, publicRoomsParams: PublicRoomsParams, callback: MatrixCallback<PublicRoomsResponse>): Cancelable fun getPublicRooms(server: String?,
publicRoomsParams: PublicRoomsParams,
callback: MatrixCallback<PublicRoomsResponse>): Cancelable


/** /**
* Join a room by id * Join a room by id
*/ */
fun joinRoom(roomId: String, callback: MatrixCallback<Unit>): Cancelable fun joinRoom(roomId: String,
callback: MatrixCallback<Unit>)


/** /**
* Fetches the overall metadata about protocols supported by the homeserver. * Fetches the overall metadata about protocols supported by the homeserver.
* Includes both the available protocols and all fields required for queries against each protocol. * Includes both the available protocols and all fields required for queries against each protocol.
*/ */
fun getThirdPartyProtocol(callback: MatrixCallback<Map<String, ThirdPartyProtocol>>): Cancelable fun getThirdPartyProtocol(callback: MatrixCallback<Map<String, ThirdPartyProtocol>>)


} }

View File

@ -20,7 +20,6 @@ import androidx.lifecycle.LiveData
import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.session.room.model.RoomSummary 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.room.model.create.CreateRoomParams
import im.vector.matrix.android.api.util.Cancelable


/** /**
* This interface defines methods to get rooms. It's implemented at the session level. * This interface defines methods to get rooms. It's implemented at the session level.
@ -28,18 +27,10 @@ import im.vector.matrix.android.api.util.Cancelable
interface RoomService { interface RoomService {


/** /**
* Create a room asynchronously * Create a room
*/ */
fun createRoom(createRoomParams: CreateRoomParams, callback: MatrixCallback<String>): Cancelable fun createRoom(createRoomParams: CreateRoomParams,

callback: MatrixCallback<String>)
/**
* Join a room by id
* @param roomId the roomId of the room to join
* @param viaServers the servers to attempt to join the room through. One of the servers must be participating in the room.
*/
fun joinRoom(roomId: String,
viaServers: List<String> = emptyList(),
callback: MatrixCallback<Unit>): Cancelable


/** /**
* Get a room from a roomId * Get a room from a roomId

View File

@ -1,25 +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.api.session.room.failure

import im.vector.matrix.android.api.failure.Failure

sealed class CreateRoomFailure : Failure.FeatureFailure() {

object CreatedWithTimeout: CreateRoomFailure()

}

View File

@ -1,25 +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.api.session.room.failure

import im.vector.matrix.android.api.failure.Failure

sealed class JoinRoomFailure : Failure.FeatureFailure() {

object JoinedWithTimeout : JoinRoomFailure()

}

View File

@ -30,7 +30,7 @@ interface MembershipService {
* This methods load all room members if it was done yet. * This methods load all room members if it was done yet.
* @return a [Cancelable] * @return a [Cancelable]
*/ */
fun loadRoomMembersIfNeeded(matrixCallback: MatrixCallback<Unit>): Cancelable fun loadRoomMembersIfNeeded(): Cancelable


/** /**
* Return the roomMember with userId or null. * Return the roomMember with userId or null.
@ -52,17 +52,16 @@ interface MembershipService {
/** /**
* Invite a user in the room * Invite a user in the room
*/ */
fun invite(userId: String, callback: MatrixCallback<Unit>): Cancelable fun invite(userId: String, callback: MatrixCallback<Unit>)


/** /**
* Join the room, or accept an invitation. * Join the room, or accept an invitation.
*/ */

fun join(callback: MatrixCallback<Unit>)
fun join(viaServers: List<String> = emptyList(), callback: MatrixCallback<Unit>): Cancelable


/** /**
* Leave the room, or reject an invitation. * Leave the room, or reject an invitation.
*/ */
fun leave(callback: MatrixCallback<Unit>): Cancelable fun leave(callback: MatrixCallback<Unit>)


} }

View File

@ -16,9 +16,8 @@


package im.vector.matrix.android.api.session.room.model package im.vector.matrix.android.api.session.room.model


import im.vector.matrix.android.api.session.user.model.User

data class ReadReceipt( data class ReadReceipt(
val user: User, val userId: String,
val eventId: String,
val originServerTs: Long val originServerTs: Long
) )

View File

@ -34,10 +34,5 @@ data class RoomSummary(
val notificationCount: Int = 0, val notificationCount: Int = 0,
val highlightCount: Int = 0, val highlightCount: Int = 0,
val tags: List<RoomTag> = emptyList(), val tags: List<RoomTag> = emptyList(),
val membership: Membership = Membership.NONE, val membership: Membership = Membership.NONE
val versioningState: VersioningState = VersioningState.NONE )
) {

val isVersioned: Boolean
get() = versioningState != VersioningState.NONE
}

View File

@ -29,6 +29,7 @@ import im.vector.matrix.android.api.session.room.model.PowerLevels
import im.vector.matrix.android.api.session.room.model.RoomDirectoryVisibility import im.vector.matrix.android.api.session.room.model.RoomDirectoryVisibility
import im.vector.matrix.android.api.session.room.model.RoomHistoryVisibility import im.vector.matrix.android.api.session.room.model.RoomHistoryVisibility
import im.vector.matrix.android.internal.auth.data.ThreePidMedium import im.vector.matrix.android.internal.auth.data.ThreePidMedium
import java.util.*


/** /**
* Parameter to create a room, with facilities functions to configure it * Parameter to create a room, with facilities functions to configure it
@ -127,12 +128,12 @@ class CreateRoomParams {
contentMap["algorithm"] = algorithm contentMap["algorithm"] = algorithm


val algoEvent = Event(type = EventType.ENCRYPTION, val algoEvent = Event(type = EventType.ENCRYPTION,
stateKey = "", stateKey = "",
content = contentMap.toContent() content = contentMap.toContent()
) )


if (null == initialStates) { if (null == initialStates) {
initialStates = mutableListOf(algoEvent) initialStates = Arrays.asList<Event>(algoEvent)
} else { } else {
initialStates!!.add(algoEvent) initialStates!!.add(algoEvent)
} }
@ -161,11 +162,11 @@ class CreateRoomParams {
contentMap["history_visibility"] = historyVisibility contentMap["history_visibility"] = historyVisibility


val historyVisibilityEvent = Event(type = EventType.STATE_HISTORY_VISIBILITY, val historyVisibilityEvent = Event(type = EventType.STATE_HISTORY_VISIBILITY,
stateKey = "", stateKey = "",
content = contentMap.toContent()) content = contentMap.toContent())


if (null == initialStates) { if (null == initialStates) {
initialStates = mutableListOf(historyVisibilityEvent) initialStates = Arrays.asList<Event>(historyVisibilityEvent)
} else { } else {
initialStates!!.add(historyVisibilityEvent) initialStates!!.add(historyVisibilityEvent)
} }
@ -201,8 +202,8 @@ class CreateRoomParams {
*/ */
fun isDirect(): Boolean { fun isDirect(): Boolean {
return preset == CreateRoomPreset.PRESET_TRUSTED_PRIVATE_CHAT return preset == CreateRoomPreset.PRESET_TRUSTED_PRIVATE_CHAT
&& isDirect == true && isDirect == true
&& (1 == getInviteCount() || 1 == getInvite3PidCount()) && (1 == getInviteCount() || 1 == getInvite3PidCount())
} }


/** /**
@ -222,13 +223,14 @@ class CreateRoomParams {
credentials: Credentials, credentials: Credentials,
ids: List<String>) { ids: List<String>) {
for (id in ids) { for (id in ids) {
if (Patterns.EMAIL_ADDRESS.matcher(id).matches() && hsConfig.identityServerUri != null) { if (Patterns.EMAIL_ADDRESS.matcher(id).matches()) {
if (null == invite3pids) { if (null == invite3pids) {
invite3pids = ArrayList() invite3pids = ArrayList()
} }

val pid = Invite3Pid(idServer = hsConfig.identityServerUri.host!!, val pid = Invite3Pid(idServer = hsConfig.identityServerUri.host!!,
medium = ThreePidMedium.EMAIL, medium = ThreePidMedium.EMAIL,
address = id) address = id)


invite3pids!!.add(pid) invite3pids!!.add(pid)
} else if (isUserId(id)) { } else if (isUserId(id)) {

View File

@ -1,28 +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.api.session.room.model.create

import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass

/**
* A link to an old room in case of room versioning
*/
@JsonClass(generateAdapter = true)
data class Predecessor(
@Json(name = "room_id") val roomId: String? = null,
@Json(name = "event_id") val eventId: String? = null
)

View File

@ -1,32 +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.api.session.room.model.create

import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass

/**
* Content of a m.room.create type event
*/
@JsonClass(generateAdapter = true)
data class RoomCreateContent(
@Json(name = "creator") val creator: String? = null,
@Json(name = "room_version") val roomVersion: String? = null,
@Json(name = "predecessor") val predecessor: Predecessor? = null
)


View File

@ -26,8 +26,3 @@ interface MessageContent {
val relatesTo: RelationDefaultContent? val relatesTo: RelationDefaultContent?
val newContent: Content? val newContent: Content?
} }


fun MessageContent?.isReply(): Boolean {
return this?.relatesTo?.inReplyTo?.eventId != null
}

View File

@ -16,10 +16,7 @@


package im.vector.matrix.android.api.session.room.model.relation package im.vector.matrix.android.api.session.room.model.relation


import im.vector.matrix.android.api.session.events.model.RelationType

interface RelationContent { interface RelationContent {
/** See [RelationType] for known possible values */
val type: String? val type: String?
val eventId: String? val eventId: String?
val inReplyTo: ReplyToContent? val inReplyTo: ReplyToContent?

View File

@ -16,8 +16,6 @@
package im.vector.matrix.android.api.session.room.model.relation package im.vector.matrix.android.api.session.room.model.relation


import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.room.model.EventAnnotationsSummary import im.vector.matrix.android.api.session.room.model.EventAnnotationsSummary
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent 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.Cancelable
@ -81,25 +79,6 @@ interface RelationService {
compatibilityBodyText: String = "* $newBodyText"): Cancelable compatibilityBodyText: String = "* $newBodyText"): Cancelable




/**
* Edit a reply. This is a special case because replies contains fallback text as a prefix.
* This method will take the new body (stripped from fallbacks) and re-add them before sending.
* @param replyToEdit The event to edit
* @param originalTimelineEvent the message that this reply (being edited) is relating to
* @param newBodyText The edited body (stripped from in reply to content)
* @param compatibilityBodyText The text that will appear on clients that don't support yet edition
*/
fun editReply(replyToEdit: TimelineEvent,
originalTimelineEvent: TimelineEvent,
newBodyText: String,
compatibilityBodyText: String = "* $newBodyText"): Cancelable

/**
* Get's the edit history of the given event
*/
fun fetchEditHistory(eventId: String, callback: MatrixCallback<List<Event>>)


/** /**
* Reply to an event in the timeline (must be in same room) * Reply to an event in the timeline (must be in same room)
* https://matrix.org/docs/spec/client_server/r0.4.0.html#id350 * https://matrix.org/docs/spec/client_server/r0.4.0.html#id350
@ -112,6 +91,4 @@ interface RelationService {
autoMarkdown: Boolean = false): Cancelable? autoMarkdown: Boolean = false): Cancelable?


fun getEventSummaryLive(eventId: String): LiveData<EventAnnotationsSummary> fun getEventSummaryLive(eventId: String): LiveData<EventAnnotationsSummary>


} }

View File

@ -21,5 +21,5 @@ import com.squareup.moshi.JsonClass


@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class ReplyToContent( data class ReplyToContent(
@Json(name = "event_id") val eventId: String? = null @Json(name = "event_id") val eventId: String
) )

View File

@ -1,28 +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.api.session.room.model.tombstone

import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass

/**
* Class to contains Tombstone information
*/
@JsonClass(generateAdapter = true)
data class RoomTombstoneContent(
@Json(name = "body") val body: String? = null,
@Json(name = "replacement_room") val replacementRoom: String?
)

View File

@ -16,9 +16,7 @@


package im.vector.matrix.android.api.session.room.read package im.vector.matrix.android.api.session.room.read


import androidx.lifecycle.LiveData
import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.session.room.model.ReadReceipt


/** /**
* This interface defines methods to handle read receipts and read marker in a room. It's implemented at the room level. * This interface defines methods to handle read receipts and read marker in a room. It's implemented at the room level.
@ -41,6 +39,4 @@ interface ReadService {
fun setReadMarker(fullyReadEventId: String, callback: MatrixCallback<Unit>) fun setReadMarker(fullyReadEventId: String, callback: MatrixCallback<Unit>)


fun isEventRead(eventId: String): Boolean fun isEventRead(eventId: String): Boolean

fun getEventReadReceiptsLive(eventId: String): LiveData<List<ReadReceipt>>
} }

View File

@ -19,7 +19,6 @@ package im.vector.matrix.android.api.session.room.send
import im.vector.matrix.android.api.session.content.ContentAttachmentData import im.vector.matrix.android.api.session.content.ContentAttachmentData
import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.room.model.message.MessageType import im.vector.matrix.android.api.session.room.model.message.MessageType
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.Cancelable




@ -66,31 +65,4 @@ interface SendService {
*/ */
fun redactEvent(event: Event, reason: String?): Cancelable fun redactEvent(event: Event, reason: String?): Cancelable



/**
* Schedule this message to be resent
* @param localEcho the unsent local echo
*/
fun resendTextMessage(localEcho: TimelineEvent): Cancelable?

/**
* Schedule this message to be resent
* @param localEcho the unsent local echo
*/
fun resendMediaMessage(localEcho: TimelineEvent): Cancelable?


/**
* Remove this failed message from the timeline
* @param localEcho the unsent local echo
*/
fun deleteFailedEcho(localEcho: TimelineEvent)

fun clearSendingQueue()

/**
* Resend all failed messages one by one (and keep order)
*/
fun resendAllFailedMessages()

} }

View File

@ -16,7 +16,6 @@


package im.vector.matrix.android.api.session.room.send package im.vector.matrix.android.api.session.room.send



enum class SendState { enum class SendState {
UNKNOWN, UNKNOWN,
// the event has not been sent // the event has not been sent
@ -34,19 +33,12 @@ enum class SendState {
// the event failed to be sent because some unknown devices have been found while encrypting it // the event failed to be sent because some unknown devices have been found while encrypting it
FAILED_UNKNOWN_DEVICES; FAILED_UNKNOWN_DEVICES;


internal companion object { fun isSent(): Boolean {
val HAS_FAILED_STATES = listOf(UNDELIVERED, FAILED_UNKNOWN_DEVICES) return this == SENT || this == SYNCED
val IS_SENT_STATES = listOf(SENT, SYNCED)
val IS_SENDING_STATES = listOf(UNSENT, ENCRYPTING, SENDING)
val PENDING_STATES = IS_SENDING_STATES + HAS_FAILED_STATES
} }


fun isSent() = IS_SENT_STATES.contains(this) fun hasFailed(): Boolean {

return this == UNDELIVERED || this == FAILED_UNKNOWN_DEVICES
fun hasFailed() = HAS_FAILED_STATES.contains(this) }

fun isSending() = IS_SENDING_STATES.contains(this)


} }



View File

@ -17,7 +17,6 @@
package im.vector.matrix.android.api.session.room.state package im.vector.matrix.android.api.session.room.state


import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.session.events.model.Event


interface StateService { interface StateService {


@ -26,6 +25,4 @@ interface StateService {
*/ */
fun updateTopic(topic: String, callback: MatrixCallback<Unit>) fun updateTopic(topic: String, callback: MatrixCallback<Unit>)


fun getStateEvent(eventType: String): Event?

} }

View File

@ -56,9 +56,6 @@ interface Timeline {
*/ */
fun paginate(direction: Direction, count: Int) fun paginate(direction: Direction, count: Int)


fun pendingEventCount() : Int

fun failedToDeliverEventCount() : Int


interface Listener { interface Listener {
/** /**

View File

@ -20,11 +20,8 @@ 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.EventType
import im.vector.matrix.android.api.session.events.model.toModel import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.model.EventAnnotationsSummary import im.vector.matrix.android.api.session.room.model.EventAnnotationsSummary
import im.vector.matrix.android.api.session.room.model.ReadReceipt
import im.vector.matrix.android.api.session.room.model.message.MessageContent import im.vector.matrix.android.api.session.room.model.message.MessageContent
import im.vector.matrix.android.api.session.room.model.message.isReply import im.vector.matrix.android.api.session.room.send.SendState
import im.vector.matrix.android.api.util.ContentUtils.extractUsefulTextFromReply
import im.vector.matrix.android.internal.crypto.model.event.EncryptedEventContent


/** /**
* This data class is a wrapper around an Event. It allows to get useful data in the context of a timeline. * This data class is a wrapper around an Event. It allows to get useful data in the context of a timeline.
@ -38,8 +35,8 @@ data class TimelineEvent(
val senderName: String?, val senderName: String?,
val isUniqueDisplayName: Boolean, val isUniqueDisplayName: Boolean,
val senderAvatar: String?, val senderAvatar: String?,
val annotations: EventAnnotationsSummary? = null, val sendState: SendState,
val readReceipts: List<ReadReceipt> = emptyList() val annotations: EventAnnotationsSummary? = null
) { ) {


val metadata = HashMap<String, Any>() val metadata = HashMap<String, Any>()
@ -67,8 +64,8 @@ data class TimelineEvent(
"$name (${root.senderId})" "$name (${root.senderId})"
} }
} }
?: root.senderId ?: root.senderId
?: "" ?: ""
} }


/** /**
@ -86,26 +83,8 @@ data class TimelineEvent(
} }
} }



/**
* Tells if the event has been edited
*/
fun TimelineEvent.hasBeenEdited() = annotations?.editSummary != null

/** /**
* Get last MessageContent, after a possible edition * Get last MessageContent, after a possible edition
*/ */
fun TimelineEvent.getLastMessageContent(): MessageContent? = annotations?.editSummary?.aggregatedContent?.toModel() fun TimelineEvent.getLastMessageContent(): MessageContent? = annotations?.editSummary?.aggregatedContent?.toModel()
?: root.getClearContent().toModel() ?: root.getClearContent().toModel()


fun TimelineEvent.getTextEditableContent(): String? {
val originalContent = root.getClearContent().toModel<MessageContent>() ?: return null
val isReply = originalContent.isReply() || root.content.toModel<EncryptedEventContent>()?.relatesTo?.inReplyTo?.eventId != null
val lastContent = getLastMessageContent()
return if (isReply) {
return extractUsefulTextFromReply(lastContent?.body ?: "")
} else {
lastContent?.body ?: ""
}
}

View File

@ -25,12 +25,12 @@ interface TimelineService {


/** /**
* Instantiate a [Timeline] with an optional initial eventId, to be used with permalink. * Instantiate a [Timeline] with an optional initial eventId, to be used with permalink.
* You can also configure some settings with the [settings] param. * You can filter the type you want to grab with the allowedTypes param.
* @param eventId the optional initial eventId. * @param eventId the optional initial eventId.
* @param settings settings to configure the timeline. * @param allowedTypes the optional filter types
* @return the instantiated timeline * @return the instantiated timeline
*/ */
fun createTimeline(eventId: String?, settings: TimelineSettings): Timeline fun createTimeline(eventId: String?, allowedTypes: List<String>? = null): Timeline




fun getTimeLineEvent(eventId: String): TimelineEvent? fun getTimeLineEvent(eventId: String): TimelineEvent?

View File

@ -1,44 +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.api.session.room.timeline

/**
* Data class holding setting values for a [Timeline] instance.
*/
data class TimelineSettings(
/**
* The initial number of events to retrieve from cache. You might get less events if you don't have loaded enough yet.
*/
val initialSize: Int,
/**
* A flag to filter edit events
*/
val filterEdits: Boolean = false,
/**
* A flag to filter by types. It should be used with [allowedTypes] field
*/
val filterTypes: Boolean = false,
/**
* If [filterTypes] is true, the list of types allowed by the list.
*/
val allowedTypes: List<String> = emptyList(),
/**
* If true, will build read receipts for each event.
*/
val buildReadReceipts: Boolean = true

)

View File

@ -18,7 +18,7 @@ package im.vector.matrix.android.api.session.sync


sealed class SyncState { sealed class SyncState {
object IDLE : SyncState() object IDLE : SyncState()
data class RUNNING(val afterPause: Boolean) : SyncState() data class RUNNING(val catchingUp: Boolean) : SyncState()
object PAUSED : SyncState() object PAUSED : SyncState()
object KILLING : SyncState() object KILLING : SyncState()
object KILLED : SyncState() object KILLED : SyncState()

View File

@ -17,10 +17,7 @@
package im.vector.matrix.android.api.session.user package im.vector.matrix.android.api.session.user


import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.paging.PagedList
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.session.user.model.User import im.vector.matrix.android.api.session.user.model.User
import im.vector.matrix.android.api.util.Cancelable


/** /**
* This interface defines methods to get users. It's implemented at the session level. * This interface defines methods to get users. It's implemented at the session level.
@ -34,34 +31,11 @@ interface UserService {
*/ */
fun getUser(userId: String): User? fun getUser(userId: String): User?


/**
* Search list of users on server directory.
* @param search the searched term
* @param limit the max number of users to return
* @param excludedUserIds the user ids to filter from the search
* @param callback the async callback
* @return Cancelable
*/
fun searchUsersDirectory(search: String, limit: Int, excludedUserIds: Set<String>, callback: MatrixCallback<List<User>>): Cancelable

/** /**
* Observe a live user from a userId * Observe a live user from a userId
* @param userId the userId to look for. * @param userId the userId to look for.
* @return a Livedata of user with userId * @return a Livedata of user with userId
*/ */
fun liveUser(userId: String): LiveData<User?> fun observeUser(userId: String): LiveData<User?>

/**
* Observe a live list of users sorted alphabetically
* @return a Livedata of users
*/
fun liveUsers(): LiveData<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
*/
fun livePagedUsers(filter: String? = null): LiveData<PagedList<User>>


} }

View File

@ -19,6 +19,5 @@ package im.vector.matrix.android.api.util
class CancelableBag : Cancelable, MutableList<Cancelable> by ArrayList() { class CancelableBag : Cancelable, MutableList<Cancelable> by ArrayList() {
override fun cancel() { override fun cancel() {
forEach { it.cancel() } forEach { it.cancel() }
clear()
} }
} }

View File

@ -1,47 +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.api.util


object ContentUtils {
fun extractUsefulTextFromReply(repliedBody: String): String {
val lines = repliedBody.lines()
var wellFormed = repliedBody.startsWith(">")
var endOfPreviousFound = false
val usefullines = ArrayList<String>()
lines.forEach {
if (it == "") {
endOfPreviousFound = true
return@forEach
}
if (!endOfPreviousFound) {
wellFormed = wellFormed && it.startsWith(">")
} else {
usefullines.add(it)
}
}
return usefullines.joinToString("\n").takeIf { wellFormed } ?: repliedBody
}

fun extractUsefulTextFromHtmlReply(repliedBody: String): String {
if (repliedBody.startsWith("<mx-reply>")) {
val closingTagIndex = repliedBody.lastIndexOf("</mx-reply>")
if (closingTagIndex != -1)
return repliedBody.substring(closingTagIndex + "</mx-reply>".length).trim()
}
return repliedBody
}
}

View File

@ -27,7 +27,7 @@ import java.math.BigInteger
import java.security.KeyPairGenerator import java.security.KeyPairGenerator
import java.security.KeyStore import java.security.KeyStore
import java.security.SecureRandom import java.security.SecureRandom
import java.util.Calendar import java.util.*
import javax.crypto.* import javax.crypto.*
import javax.crypto.spec.GCMParameterSpec import javax.crypto.spec.GCMParameterSpec
import javax.crypto.spec.IvParameterSpec import javax.crypto.spec.IvParameterSpec
@ -479,7 +479,12 @@ object SecretStoringUtils {
val output = Cipher.getInstance(RSA_MODE) val output = Cipher.getInstance(RSA_MODE)
output.init(Cipher.DECRYPT_MODE, privateKeyEntry.privateKey) output.init(Cipher.DECRYPT_MODE, privateKeyEntry.privateKey)


return CipherInputStream(encrypted, output).use { it.readBytes() } val bos = ByteArrayOutputStream()
CipherInputStream(encrypted, output).use {
it.copyTo(bos)
}

return bos.toByteArray()
} }


private fun formatMExtract(bis: InputStream): Pair<ByteArray, ByteArray> { private fun formatMExtract(bis: InputStream): Pair<ByteArray, ByteArray> {
@ -490,7 +495,14 @@ object SecretStoringUtils {
val iv = ByteArray(ivSize) val iv = ByteArray(ivSize)
bis.read(iv, 0, ivSize) bis.read(iv, 0, ivSize)


val encrypted = bis.readBytes()
val bos = ByteArrayOutputStream()
var next = bis.read()
while (next != -1) {
bos.write(next)
next = bis.read()
}
val encrypted = bos.toByteArray()
return Pair(iv, encrypted) return Pair(iv, encrypted)
} }


@ -518,7 +530,14 @@ object SecretStoringUtils {
val iv = ByteArray(ivSize) val iv = ByteArray(ivSize)
bis.read(iv) bis.read(iv)


val encrypted = bis.readBytes() val bos = ByteArrayOutputStream()

var next = bis.read()
while (next != -1) {
bos.write(next)
next = bis.read()
}
val encrypted = bos.toByteArray()
return Triple(encryptedKey, iv, encrypted) return Triple(encryptedKey, iv, encrypted)
} }


@ -560,7 +579,14 @@ object SecretStoringUtils {
val iv = ByteArray(ivSize) val iv = ByteArray(ivSize)
bis.read(iv) bis.read(iv)


val encrypted = bis.readBytes() val bos = ByteArrayOutputStream()

var next = bis.read()
while (next != -1) {
bos.write(next)
next = bis.read()
}
val encrypted = bos.toByteArray()
return Triple(salt, iv, encrypted) return Triple(salt, iv, encrypted)
} }
} }

View File

@ -50,10 +50,16 @@ internal class SessionManager @Inject constructor(private val matrixComponent: M
} }


private fun getOrCreateSessionComponent(sessionParams: SessionParams): SessionComponent { private fun getOrCreateSessionComponent(sessionParams: SessionParams): SessionComponent {
return sessionComponents.getOrPut(sessionParams.credentials.userId) { val userId = sessionParams.credentials.userId
DaggerSessionComponent if (sessionComponents.containsKey(userId)) {
.factory() return sessionComponents[userId]!!
.create(matrixComponent, sessionParams)
} }
return DaggerSessionComponent
.factory()
.create(matrixComponent, sessionParams)
.also {
sessionComponents[sessionParams.credentials.userId] = it
}
} }

} }

View File

@ -68,9 +68,7 @@ internal class DefaultAuthenticator @Inject constructor(@Unauthenticated
callback: MatrixCallback<Session>): Cancelable { callback: MatrixCallback<Session>): Cancelable {


val job = GlobalScope.launch(coroutineDispatchers.main) { val job = GlobalScope.launch(coroutineDispatchers.main) {
val sessionOrFailure = runCatching { val sessionOrFailure = authenticate(homeServerConnectionConfig, login, password)
authenticate(homeServerConnectionConfig, login, password)
}
sessionOrFailure.foldToCallback(callback) sessionOrFailure.foldToCallback(callback)
} }
return CancelableCoroutine(job) return CancelableCoroutine(job)
@ -87,12 +85,16 @@ internal class DefaultAuthenticator @Inject constructor(@Unauthenticated
} else { } else {
PasswordLoginParams.userIdentifier(login, password, "Mobile") PasswordLoginParams.userIdentifier(login, password, "Mobile")
} }
val credentials = executeRequest<Credentials> { executeRequest<Credentials> {
apiCall = authAPI.login(loginParams) apiCall = authAPI.login(loginParams)
}.map {
val sessionParams = SessionParams(it, homeServerConnectionConfig)
sessionParamsStore.save(sessionParams)
sessionParams
}.map {
sessionManager.getOrCreateSession(it)
} }
val sessionParams = SessionParams(credentials, homeServerConnectionConfig)
sessionParamsStore.save(sessionParams)
sessionManager.getOrCreateSession(sessionParams)
} }


private fun buildAuthAPI(homeServerConnectionConfig: HomeServerConnectionConfig): AuthAPI { private fun buildAuthAPI(homeServerConnectionConfig: HomeServerConnectionConfig): AuthAPI {

View File

@ -27,7 +27,7 @@ internal interface SessionParamsStore {


fun getAll(): List<SessionParams> fun getAll(): List<SessionParams>


fun save(sessionParams: SessionParams): Try<Unit> fun save(sessionParams: SessionParams): Try<SessionParams>


fun delete(userId: String): Try<Unit> fun delete(userId: String): Try<Unit>



View File

@ -62,7 +62,7 @@ internal class RealmSessionParamsStore @Inject constructor(private val mapper: S
return sessionParams return sessionParams
} }


override fun save(sessionParams: SessionParams): Try<Unit> { override fun save(sessionParams: SessionParams): Try<SessionParams> {
return Try { return Try {
val entity = mapper.map(sessionParams) val entity = mapper.map(sessionParams)
if (entity != null) { if (entity != null) {
@ -72,6 +72,7 @@ internal class RealmSessionParamsStore @Inject constructor(private val mapper: S
} }
realm.close() realm.close()
} }
sessionParams
} }
} }



View File

@ -20,6 +20,7 @@ import com.squareup.moshi.Moshi
import im.vector.matrix.android.api.auth.data.Credentials 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.HomeServerConnectionConfig
import im.vector.matrix.android.api.auth.data.SessionParams import im.vector.matrix.android.api.auth.data.SessionParams
import im.vector.matrix.android.internal.di.MatrixScope
import javax.inject.Inject import javax.inject.Inject


internal class SessionParamsMapper @Inject constructor(moshi: Moshi) { internal class SessionParamsMapper @Inject constructor(moshi: Moshi) {

View File

@ -21,6 +21,7 @@ package im.vector.matrix.android.internal.crypto
import android.content.Context import android.content.Context
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import android.text.TextUtils
import arrow.core.Try import arrow.core.Try
import com.squareup.moshi.Types import com.squareup.moshi.Types
import com.zhuinden.monarchy.Monarchy import com.zhuinden.monarchy.Monarchy
@ -72,15 +73,17 @@ import im.vector.matrix.android.internal.session.room.membership.RoomMembers
import im.vector.matrix.android.internal.session.sync.model.SyncResponse import im.vector.matrix.android.internal.session.sync.model.SyncResponse
import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.internal.task.configureWith import im.vector.matrix.android.internal.task.configureWith
import im.vector.matrix.android.internal.task.toConfigurableTask
import im.vector.matrix.android.internal.util.JsonCanonicalizer import im.vector.matrix.android.internal.util.JsonCanonicalizer
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
import im.vector.matrix.android.internal.util.fetchCopied import im.vector.matrix.android.internal.util.fetchCopied
import kotlinx.coroutines.* import kotlinx.coroutines.*
import org.matrix.olm.OlmManager import org.matrix.olm.OlmManager
import timber.log.Timber import timber.log.Timber
import java.util.*
import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicBoolean
import javax.inject.Inject import javax.inject.Inject
import kotlin.math.max import kotlin.coroutines.EmptyCoroutineContext


/** /**
* A `CryptoService` class instance manages the end-to-end crypto for a session. * A `CryptoService` class instance manages the end-to-end crypto for a session.
@ -93,7 +96,7 @@ import kotlin.math.max
* Specially, it tracks all room membership changes events in order to do keys updates. * Specially, it tracks all room membership changes events in order to do keys updates.
*/ */
@SessionScope @SessionScope
internal class DefaultCryptoService @Inject constructor( internal class CryptoManager @Inject constructor(
// Olm Manager // Olm Manager
private val olmManager: OlmManager, private val olmManager: OlmManager,
// The credentials, // The credentials,
@ -166,25 +169,22 @@ internal class DefaultCryptoService @Inject constructor(


override fun setDeviceName(deviceId: String, deviceName: String, callback: MatrixCallback<Unit>) { override fun setDeviceName(deviceId: String, deviceName: String, callback: MatrixCallback<Unit>) {
setDeviceNameTask setDeviceNameTask
.configureWith(SetDeviceNameTask.Params(deviceId, deviceName)) { .configureWith(SetDeviceNameTask.Params(deviceId, deviceName))
this.callback = callback .dispatchTo(callback)
}
.executeBy(taskExecutor) .executeBy(taskExecutor)
} }


override fun deleteDevice(deviceId: String, callback: MatrixCallback<Unit>) { override fun deleteDevice(deviceId: String, callback: MatrixCallback<Unit>) {
deleteDeviceTask deleteDeviceTask
.configureWith(DeleteDeviceTask.Params(deviceId)) { .configureWith(DeleteDeviceTask.Params(deviceId))
this.callback = callback .dispatchTo(callback)
}
.executeBy(taskExecutor) .executeBy(taskExecutor)
} }


override fun deleteDeviceWithUserPassword(deviceId: String, authSession: String?, password: String, callback: MatrixCallback<Unit>) { override fun deleteDeviceWithUserPassword(deviceId: String, authSession: String?, password: String, callback: MatrixCallback<Unit>) {
deleteDeviceWithUserPasswordTask deleteDeviceWithUserPasswordTask
.configureWith(DeleteDeviceWithUserPasswordTask.Params(deviceId, authSession, password)) { .configureWith(DeleteDeviceWithUserPasswordTask.Params(deviceId, authSession, password))
this.callback = callback .dispatchTo(callback)
}
.executeBy(taskExecutor) .executeBy(taskExecutor)
} }


@ -198,9 +198,8 @@ internal class DefaultCryptoService @Inject constructor(


override fun getDevicesList(callback: MatrixCallback<DevicesListResponse>) { override fun getDevicesList(callback: MatrixCallback<DevicesListResponse>) {
getDevicesTask getDevicesTask
.configureWith { .toConfigurableTask()
this.callback = callback .dispatchTo(callback)
}
.executeBy(taskExecutor) .executeBy(taskExecutor)
} }


@ -249,7 +248,7 @@ internal class DefaultCryptoService @Inject constructor(
return return
} }
isStarting.set(true) isStarting.set(true)
GlobalScope.launch(coroutineDispatchers.crypto) { CoroutineScope(coroutineDispatchers.crypto).launch {
internalStart(isInitialSync) internalStart(isInitialSync)
} }
} }
@ -257,36 +256,35 @@ internal class DefaultCryptoService @Inject constructor(
private suspend fun internalStart(isInitialSync: Boolean) { private suspend fun internalStart(isInitialSync: Boolean) {
// Open the store // Open the store
cryptoStore.open() cryptoStore.open()
runCatching { uploadDeviceKeys()
uploadDeviceKeys() .flatMap { oneTimeKeysUploader.maybeUploadOneTimeKeys() }
oneTimeKeysUploader.maybeUploadOneTimeKeys() .fold(
outgoingRoomKeyRequestManager.start() {
keysBackup.checkAndStartKeysBackup() Timber.e("Start failed: $it")
if (isInitialSync) { delay(1000)
// refresh the devices list for each known room members isStarting.set(false)
deviceListManager.invalidateAllDeviceLists() internalStart(isInitialSync)
deviceListManager.refreshOutdatedDeviceLists() },
} else { {
incomingRoomKeyRequestManager.processReceivedRoomKeyRequests() outgoingRoomKeyRequestManager.start()
} keysBackup.checkAndStartKeysBackup()
}.fold( if (isInitialSync) {
{ // refresh the devices list for each known room members
isStarting.set(false) deviceListManager.invalidateAllDeviceLists()
isStarted.set(true) deviceListManager.refreshOutdatedDeviceLists()
}, } else {
{ incomingRoomKeyRequestManager.processReceivedRoomKeyRequests()
Timber.e("Start failed: $it") }
delay(1000) isStarting.set(false)
isStarting.set(false) isStarted.set(true)
internalStart(isInitialSync) }
} )
)
} }


/** /**
* Close the crypto * Close the crypto
*/ */
fun close() = runBlocking(coroutineDispatchers.crypto) { fun close() {
olmDevice.release() olmDevice.release()
cryptoStore.close() cryptoStore.close()
outgoingRoomKeyRequestManager.stop() outgoingRoomKeyRequestManager.stop()
@ -317,7 +315,7 @@ internal class DefaultCryptoService @Inject constructor(
* @param syncResponse the syncResponse * @param syncResponse the syncResponse
*/ */
fun onSyncCompleted(syncResponse: SyncResponse) { fun onSyncCompleted(syncResponse: SyncResponse) {
GlobalScope.launch(coroutineDispatchers.crypto) { CoroutineScope(coroutineDispatchers.crypto).launch {
if (syncResponse.deviceLists != null) { if (syncResponse.deviceLists != null) {
deviceListManager.handleDeviceListsChanges(syncResponse.deviceLists.changed, syncResponse.deviceLists.left) deviceListManager.handleDeviceListsChanges(syncResponse.deviceLists.changed, syncResponse.deviceLists.left)
} }
@ -342,7 +340,7 @@ internal class DefaultCryptoService @Inject constructor(
* @return the device info, or null if not found / unsupported algorithm / crypto released * @return the device info, or null if not found / unsupported algorithm / crypto released
*/ */
override fun deviceWithIdentityKey(senderKey: String, algorithm: String): MXDeviceInfo? { override fun deviceWithIdentityKey(senderKey: String, algorithm: String): MXDeviceInfo? {
return if (algorithm != MXCRYPTO_ALGORITHM_MEGOLM && algorithm != MXCRYPTO_ALGORITHM_OLM) { return if (!TextUtils.equals(algorithm, MXCRYPTO_ALGORITHM_MEGOLM) && !TextUtils.equals(algorithm, MXCRYPTO_ALGORITHM_OLM)) {
// We only deal in olm keys // We only deal in olm keys
null null
} else cryptoStore.deviceWithIdentityKey(senderKey) } else cryptoStore.deviceWithIdentityKey(senderKey)
@ -355,8 +353,8 @@ internal class DefaultCryptoService @Inject constructor(
* @param deviceId the device id * @param deviceId the device id
*/ */
override fun getDeviceInfo(userId: String, deviceId: String?): MXDeviceInfo? { override fun getDeviceInfo(userId: String, deviceId: String?): MXDeviceInfo? {
return if (userId.isNotEmpty() && !deviceId.isNullOrEmpty()) { return if (!TextUtils.isEmpty(userId) && !TextUtils.isEmpty(deviceId)) {
cryptoStore.getUserDevice(deviceId, userId) cryptoStore.getUserDevice(deviceId!!, userId)
} else { } else {
null null
} }
@ -441,7 +439,7 @@ internal class DefaultCryptoService @Inject constructor(
// (for now at least. Maybe we should alert the user somehow?) // (for now at least. Maybe we should alert the user somehow?)
val existingAlgorithm = cryptoStore.getRoomAlgorithm(roomId) val existingAlgorithm = cryptoStore.getRoomAlgorithm(roomId)


if (!existingAlgorithm.isNullOrEmpty() && existingAlgorithm != algorithm) { if (!TextUtils.isEmpty(existingAlgorithm) && !TextUtils.equals(existingAlgorithm, algorithm)) {
Timber.e("## setEncryptionInRoom() : Ignoring m.room.encryption event which requests a change of config in $roomId") Timber.e("## setEncryptionInRoom() : Ignoring m.room.encryption event which requests a change of config in $roomId")
return false return false
} }
@ -537,7 +535,7 @@ internal class DefaultCryptoService @Inject constructor(
eventType: String, eventType: String,
roomId: String, roomId: String,
callback: MatrixCallback<MXEncryptEventContentResult>) { callback: MatrixCallback<MXEncryptEventContentResult>) {
GlobalScope.launch(coroutineDispatchers.crypto) { CoroutineScope(coroutineDispatchers.crypto).launch {
if (!isStarted()) { if (!isStarted()) {
Timber.v("## encryptEventContent() : wait after e2e init") Timber.v("## encryptEventContent() : wait after e2e init")
internalStart(false) internalStart(false)
@ -560,16 +558,13 @@ internal class DefaultCryptoService @Inject constructor(
if (safeAlgorithm != null) { if (safeAlgorithm != null) {
val t0 = System.currentTimeMillis() val t0 = System.currentTimeMillis()
Timber.v("## encryptEventContent() starts") Timber.v("## encryptEventContent() starts")
runCatching { safeAlgorithm.encryptEventContent(eventContent, eventType, userIds)
safeAlgorithm.encryptEventContent(eventContent, eventType, userIds)
}
.fold( .fold(
{ callback.onFailure(it) },
{ {
Timber.v("## encryptEventContent() : succeeds after ${System.currentTimeMillis() - t0} ms") Timber.v("## encryptEventContent() : succeeds after ${System.currentTimeMillis() - t0} ms")
callback.onSuccess(MXEncryptEventContentResult(it, EventType.ENCRYPTED)) callback.onSuccess(MXEncryptEventContentResult(it, EventType.ENCRYPTED))
}, }
{ callback.onFailure(it) }

) )
} else { } else {
val algorithm = getEncryptionAlgorithm(roomId) val algorithm = getEncryptionAlgorithm(roomId)
@ -591,7 +586,10 @@ internal class DefaultCryptoService @Inject constructor(
@Throws(MXCryptoError::class) @Throws(MXCryptoError::class)
override fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult { override fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult {
return runBlocking { return runBlocking {
internalDecryptEvent(event, timeline) internalDecryptEvent(event, timeline).fold(
{ throw it },
{ it }
)
} }
} }


@ -603,11 +601,9 @@ internal class DefaultCryptoService @Inject constructor(
* @param callback the callback to return data or null * @param callback the callback to return data or null
*/ */
override fun decryptEventAsync(event: Event, timeline: String, callback: MatrixCallback<MXEventDecryptionResult>) { override fun decryptEventAsync(event: Event, timeline: String, callback: MatrixCallback<MXEventDecryptionResult>) {
GlobalScope.launch { GlobalScope.launch(EmptyCoroutineContext) {
val result = runCatching { val result = withContext(coroutineDispatchers.crypto) {
withContext(coroutineDispatchers.crypto) { internalDecryptEvent(event, timeline)
internalDecryptEvent(event, timeline)
}
} }
result.foldToCallback(callback) result.foldToCallback(callback)
} }
@ -618,22 +614,22 @@ internal class DefaultCryptoService @Inject constructor(
* *
* @param event the raw event. * @param event the raw event.
* @param timeline the id of the timeline where the event is decrypted. It is used to prevent replay attack. * @param timeline the id of the timeline where the event is decrypted. It is used to prevent replay attack.
* @return the MXEventDecryptionResult data, or null in case of error * @return the MXEventDecryptionResult data, or null in case of error wrapped into [Try]
*/ */
private suspend fun internalDecryptEvent(event: Event, timeline: String): MXEventDecryptionResult { private suspend fun internalDecryptEvent(event: Event, timeline: String): Try<MXEventDecryptionResult> {
val eventContent = event.content val eventContent = event.content
if (eventContent == null) { return if (eventContent == null) {
Timber.e("## decryptEvent : empty event content") Timber.e("## decryptEvent : empty event content")
throw MXCryptoError.Base(MXCryptoError.ErrorType.BAD_ENCRYPTED_MESSAGE, MXCryptoError.BAD_ENCRYPTED_MESSAGE_REASON) Try.Failure(MXCryptoError.Base(MXCryptoError.ErrorType.BAD_ENCRYPTED_MESSAGE, MXCryptoError.BAD_ENCRYPTED_MESSAGE_REASON))
} else { } else {
val algorithm = eventContent["algorithm"]?.toString() val algorithm = eventContent["algorithm"]?.toString()
val alg = roomDecryptorProvider.getOrCreateRoomDecryptor(event.roomId, algorithm) val alg = roomDecryptorProvider.getOrCreateRoomDecryptor(event.roomId, algorithm)
if (alg == null) { if (alg == null) {
val reason = String.format(MXCryptoError.UNABLE_TO_DECRYPT_REASON, event.eventId, algorithm) val reason = String.format(MXCryptoError.UNABLE_TO_DECRYPT_REASON, event.eventId, algorithm)
Timber.e("## decryptEvent() : $reason") Timber.e("## decryptEvent() : $reason")
throw MXCryptoError.Base(MXCryptoError.ErrorType.UNABLE_TO_DECRYPT, reason) Try.Failure(MXCryptoError.Base(MXCryptoError.ErrorType.UNABLE_TO_DECRYPT, reason))
} else { } else {
return alg.decryptEvent(event, timeline) alg.decryptEvent(event, timeline)
} }
} }
} }
@ -653,7 +649,7 @@ internal class DefaultCryptoService @Inject constructor(
* @param event the event * @param event the event
*/ */
fun onToDeviceEvent(event: Event) { fun onToDeviceEvent(event: Event) {
GlobalScope.launch(coroutineDispatchers.crypto) { CoroutineScope(coroutineDispatchers.crypto).launch {
when (event.getClearType()) { when (event.getClearType()) {
EventType.ROOM_KEY, EventType.FORWARDED_ROOM_KEY -> { EventType.ROOM_KEY, EventType.FORWARDED_ROOM_KEY -> {
onRoomKeyEvent(event) onRoomKeyEvent(event)
@ -675,7 +671,7 @@ internal class DefaultCryptoService @Inject constructor(
*/ */
private fun onRoomKeyEvent(event: Event) { private fun onRoomKeyEvent(event: Event) {
val roomKeyContent = event.getClearContent().toModel<RoomKeyContent>() ?: return val roomKeyContent = event.getClearContent().toModel<RoomKeyContent>() ?: return
if (roomKeyContent.roomId.isNullOrEmpty() || roomKeyContent.algorithm.isNullOrEmpty()) { if (TextUtils.isEmpty(roomKeyContent.roomId) || TextUtils.isEmpty(roomKeyContent.algorithm)) {
Timber.e("## onRoomKeyEvent() : missing fields") Timber.e("## onRoomKeyEvent() : missing fields")
return return
} }
@ -693,15 +689,14 @@ internal class DefaultCryptoService @Inject constructor(
* @param event the encryption event. * @param event the encryption event.
*/ */
private fun onRoomEncryptionEvent(roomId: String, event: Event) { private fun onRoomEncryptionEvent(roomId: String, event: Event) {
GlobalScope.launch(coroutineDispatchers.crypto) { CoroutineScope(coroutineDispatchers.crypto).launch {
val params = LoadRoomMembersTask.Params(roomId) val params = LoadRoomMembersTask.Params(roomId)
try { loadRoomMembersTask
loadRoomMembersTask.execute(params) .execute(params)
val userIds = getRoomUserIds(roomId) .map { _ ->
setEncryptionInRoom(roomId, event.content?.get("algorithm")?.toString(), true, userIds) val userIds = getRoomUserIds(roomId)
} catch (throwable: Throwable) { setEncryptionInRoom(roomId, event.content?.get("algorithm")?.toString(), true, userIds)
Timber.e(throwable) }
}
} }
} }


@ -743,7 +738,7 @@ internal class DefaultCryptoService @Inject constructor(
val membership = roomMember?.membership val membership = roomMember?.membership
if (membership == Membership.JOIN) { if (membership == Membership.JOIN) {
// make sure we are tracking the deviceList for this user. // make sure we are tracking the deviceList for this user.
deviceListManager.startTrackingDeviceList(listOf(userId)) deviceListManager.startTrackingDeviceList(Arrays.asList(userId))
} else if (membership == Membership.INVITE } else if (membership == Membership.INVITE
&& shouldEncryptForInvitedMembers(roomId) && shouldEncryptForInvitedMembers(roomId)
&& cryptoConfig.enableEncryptionForInvitedMembers) { && cryptoConfig.enableEncryptionForInvitedMembers) {
@ -752,7 +747,7 @@ internal class DefaultCryptoService @Inject constructor(
// know what other servers are in the room at the time they've been invited. // know what other servers are in the room at the time they've been invited.
// They therefore will not send device updates if a user logs in whilst // They therefore will not send device updates if a user logs in whilst
// their state is invite. // their state is invite.
deviceListManager.startTrackingDeviceList(listOf(userId)) deviceListManager.startTrackingDeviceList(Arrays.asList(userId))
} }
} }
} }
@ -768,7 +763,7 @@ internal class DefaultCryptoService @Inject constructor(
/** /**
* Upload my user's device keys. * Upload my user's device keys.
*/ */
private suspend fun uploadDeviceKeys(): KeysUploadResponse { private suspend fun uploadDeviceKeys(): Try<KeysUploadResponse> {
// Prepare the device keys data to send // Prepare the device keys data to send
// Sign it // Sign it
val canonicalJson = JsonCanonicalizer.getCanonicalJson(Map::class.java, getMyDevice().signalableJSONDictionary()) val canonicalJson = JsonCanonicalizer.getCanonicalJson(Map::class.java, getMyDevice().signalableJSONDictionary())
@ -787,11 +782,7 @@ internal class DefaultCryptoService @Inject constructor(
* @param callback the exported keys * @param callback the exported keys
*/ */
override fun exportRoomKeys(password: String, callback: MatrixCallback<ByteArray>) { override fun exportRoomKeys(password: String, callback: MatrixCallback<ByteArray>) {
GlobalScope.launch(coroutineDispatchers.main) { exportRoomKeys(password, MXMegolmExportEncryption.DEFAULT_ITERATION_COUNT, callback)
runCatching {
exportRoomKeys(password, MXMegolmExportEncryption.DEFAULT_ITERATION_COUNT)
}.fold(callback::onSuccess, callback::onFailure)
}
} }


/** /**
@ -801,16 +792,30 @@ internal class DefaultCryptoService @Inject constructor(
* @param anIterationCount the encryption iteration count (0 means no encryption) * @param anIterationCount the encryption iteration count (0 means no encryption)
* @param callback the exported keys * @param callback the exported keys
*/ */
private suspend fun exportRoomKeys(password: String, anIterationCount: Int): ByteArray { private fun exportRoomKeys(password: String, anIterationCount: Int, callback: MatrixCallback<ByteArray>) {
return withContext(coroutineDispatchers.crypto) { GlobalScope.launch(coroutineDispatchers.main) {
val iterationCount = max(0, anIterationCount) withContext(coroutineDispatchers.crypto) {
Try {
val iterationCount = Math.max(0, anIterationCount)


val exportedSessions = cryptoStore.getInboundGroupSessions().mapNotNull { it.exportKeys() } val exportedSessions = ArrayList<MegolmSessionData>()


val adapter = MoshiProvider.providesMoshi() val inboundGroupSessions = cryptoStore.getInboundGroupSessions()
.adapter(List::class.java)


MXMegolmExportEncryption.encryptMegolmKeyFile(adapter.toJson(exportedSessions), password, iterationCount) for (session in inboundGroupSessions) {
val megolmSessionData = session.exportKeys()

if (null != megolmSessionData) {
exportedSessions.add(megolmSessionData)
}
}

val adapter = MoshiProvider.providesMoshi()
.adapter(List::class.java)

MXMegolmExportEncryption.encryptMegolmKeyFile(adapter.toJson(exportedSessions), password, iterationCount)
}
}.foldToCallback(callback)
} }
} }


@ -874,9 +879,11 @@ internal class DefaultCryptoService @Inject constructor(
*/ */
fun checkUnknownDevices(userIds: List<String>, callback: MatrixCallback<Unit>) { fun checkUnknownDevices(userIds: List<String>, callback: MatrixCallback<Unit>) {
// force the refresh to ensure that the devices list is up-to-date // force the refresh to ensure that the devices list is up-to-date
GlobalScope.launch(coroutineDispatchers.crypto) { CoroutineScope(coroutineDispatchers.crypto).launch {
runCatching { deviceListManager.downloadKeys(userIds, true) } deviceListManager
.downloadKeys(userIds, true)
.fold( .fold(
{ callback.onFailure(it) },
{ {
val unknownDevices = getUnknownDevices(it) val unknownDevices = getUnknownDevices(it)
if (unknownDevices.map.isEmpty()) { if (unknownDevices.map.isEmpty()) {
@ -885,8 +892,7 @@ internal class DefaultCryptoService @Inject constructor(
// trigger an an unknown devices exception // trigger an an unknown devices exception
callback.onFailure(Failure.CryptoError(MXCryptoError.UnknownDevice(unknownDevices))) callback.onFailure(Failure.CryptoError(MXCryptoError.UnknownDevice(unknownDevices)))
} }
}, }
{ callback.onFailure(it) }
) )
} }
} }
@ -938,7 +944,7 @@ internal class DefaultCryptoService @Inject constructor(
val roomIds = cryptoStore.getRoomsListBlacklistUnverifiedDevices().toMutableList() val roomIds = cryptoStore.getRoomsListBlacklistUnverifiedDevices().toMutableList()


if (add) { if (add) {
if (roomId !in roomIds) { if (!roomIds.contains(roomId)) {
roomIds.add(roomId) roomIds.add(roomId)
} }
} else { } else {
@ -1027,7 +1033,8 @@ internal class DefaultCryptoService @Inject constructor(
val unknownDevices = MXUsersDevicesMap<MXDeviceInfo>() val unknownDevices = MXUsersDevicesMap<MXDeviceInfo>()
val userIds = devicesInRoom.userIds val userIds = devicesInRoom.userIds
for (userId in userIds) { for (userId in userIds) {
devicesInRoom.getUserDeviceIds(userId)?.forEach { deviceId -> val deviceIds = devicesInRoom.getUserDeviceIds(userId)
deviceIds?.forEach { deviceId ->
devicesInRoom.getObject(userId, deviceId) devicesInRoom.getObject(userId, deviceId)
?.takeIf { it.isUnknown } ?.takeIf { it.isUnknown }
?.let { ?.let {
@ -1040,18 +1047,17 @@ internal class DefaultCryptoService @Inject constructor(
} }


override fun downloadKeys(userIds: List<String>, forceDownload: Boolean, callback: MatrixCallback<MXUsersDevicesMap<MXDeviceInfo>>) { override fun downloadKeys(userIds: List<String>, forceDownload: Boolean, callback: MatrixCallback<MXUsersDevicesMap<MXDeviceInfo>>) {
GlobalScope.launch(coroutineDispatchers.crypto) { CoroutineScope(coroutineDispatchers.crypto).launch {
runCatching { deviceListManager
deviceListManager.downloadKeys(userIds, forceDownload) .downloadKeys(userIds, forceDownload)
}.foldToCallback(callback) .foldToCallback(callback)
} }
} }


override fun clearCryptoCache(callback: MatrixCallback<Unit>) { override fun clearCryptoCache(callback: MatrixCallback<Unit>) {
clearCryptoDataTask clearCryptoDataTask
.configureWith { .toConfigurableTask()
this.callback = callback .dispatchTo(callback)
}
.executeBy(taskExecutor) .executeBy(taskExecutor)
} }


@ -1067,6 +1073,6 @@ internal class DefaultCryptoService @Inject constructor(
* ========================================================================================== */ * ========================================================================================== */


override fun toString(): String { override fun toString(): String {
return "DefaultCryptoService of " + credentials.userId + " (" + credentials.deviceId + ")" return "CryptoManager of " + credentials.userId + " (" + credentials.deviceId + ")"
} }
} }

View File

@ -105,7 +105,7 @@ internal abstract class CryptoModule {
} }


@Binds @Binds
abstract fun bindCryptoService(cryptoService: DefaultCryptoService): CryptoService abstract fun bindCryptoService(cryptoManager: CryptoManager): CryptoService


@Binds @Binds
abstract fun bindDeleteDeviceTask(deleteDeviceTask: DefaultDeleteDeviceTask): DeleteDeviceTask abstract fun bindDeleteDeviceTask(deleteDeviceTask: DefaultDeleteDeviceTask): DeleteDeviceTask

View File

@ -18,12 +18,14 @@
package im.vector.matrix.android.internal.crypto package im.vector.matrix.android.internal.crypto


import android.text.TextUtils import android.text.TextUtils
import arrow.core.Try
import im.vector.matrix.android.api.MatrixPatterns import im.vector.matrix.android.api.MatrixPatterns
import im.vector.matrix.android.api.auth.data.Credentials import im.vector.matrix.android.api.auth.data.Credentials
import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo
import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
import im.vector.matrix.android.internal.crypto.tasks.DownloadKeysForUsersTask import im.vector.matrix.android.internal.crypto.tasks.DownloadKeysForUsersTask
import im.vector.matrix.android.internal.extensions.onError
import im.vector.matrix.android.internal.session.SessionScope import im.vector.matrix.android.internal.session.SessionScope
import im.vector.matrix.android.internal.session.sync.SyncTokenStore import im.vector.matrix.android.internal.session.sync.SyncTokenStore
import timber.log.Timber import timber.log.Timber
@ -235,7 +237,7 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
* @param forceDownload Always download the keys even if cached. * @param forceDownload Always download the keys even if cached.
* @param callback the asynchronous callback * @param callback the asynchronous callback
*/ */
suspend fun downloadKeys(userIds: List<String>?, forceDownload: Boolean): MXUsersDevicesMap<MXDeviceInfo> { suspend fun downloadKeys(userIds: List<String>?, forceDownload: Boolean): Try<MXUsersDevicesMap<MXDeviceInfo>> {
Timber.v("## downloadKeys() : forceDownload $forceDownload : $userIds") Timber.v("## downloadKeys() : forceDownload $forceDownload : $userIds")
// Map from userId -> deviceId -> MXDeviceInfo // Map from userId -> deviceId -> MXDeviceInfo
val stored = MXUsersDevicesMap<MXDeviceInfo>() val stored = MXUsersDevicesMap<MXDeviceInfo>()
@ -266,15 +268,16 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
} }
return if (downloadUsers.isEmpty()) { return if (downloadUsers.isEmpty()) {
Timber.v("## downloadKeys() : no new user device") Timber.v("## downloadKeys() : no new user device")
stored Try.just(stored)
} else { } else {
Timber.v("## downloadKeys() : starts") Timber.v("## downloadKeys() : starts")
val t0 = System.currentTimeMillis() val t0 = System.currentTimeMillis()
val result = doKeyDownloadForUsers(downloadUsers) doKeyDownloadForUsers(downloadUsers)
Timber.v("## downloadKeys() : doKeyDownloadForUsers succeeds after " + (System.currentTimeMillis() - t0) + " ms") .map {
result.also { Timber.v("## downloadKeys() : doKeyDownloadForUsers succeeds after " + (System.currentTimeMillis() - t0) + " ms")
it.addEntriesFromMap(stored) it.addEntriesFromMap(stored)
} it
}
} }
} }


@ -283,60 +286,60 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
* *
* @param downloadUsers the user ids list * @param downloadUsers the user ids list
*/ */
private suspend fun doKeyDownloadForUsers(downloadUsers: MutableList<String>): MXUsersDevicesMap<MXDeviceInfo> { private suspend fun doKeyDownloadForUsers(downloadUsers: MutableList<String>): Try<MXUsersDevicesMap<MXDeviceInfo>> {
Timber.v("## doKeyDownloadForUsers() : doKeyDownloadForUsers $downloadUsers") Timber.v("## doKeyDownloadForUsers() : doKeyDownloadForUsers $downloadUsers")
// get the user ids which did not already trigger a keys download // get the user ids which did not already trigger a keys download
val filteredUsers = downloadUsers.filter { MatrixPatterns.isUserId(it) } val filteredUsers = downloadUsers.filter { MatrixPatterns.isUserId(it) }
if (filteredUsers.isEmpty()) { if (filteredUsers.isEmpty()) {
// trigger nothing // trigger nothing
return MXUsersDevicesMap() return Try.just(MXUsersDevicesMap())
} }
val params = DownloadKeysForUsersTask.Params(filteredUsers, syncTokenStore.getLastToken()) val params = DownloadKeysForUsersTask.Params(filteredUsers, syncTokenStore.getLastToken())
val response = try { return downloadKeysForUsersTask.execute(params)
downloadKeysForUsersTask.execute(params) .map { response ->
} catch (throwable: Throwable) { Timber.v("## doKeyDownloadForUsers() : Got keys for " + filteredUsers.size + " users")
Timber.e(throwable, "##doKeyDownloadForUsers(): error") for (userId in filteredUsers) {
onKeysDownloadFailed(filteredUsers) val devices = response.deviceKeys?.get(userId)
throw throwable Timber.v("## doKeyDownloadForUsers() : Got keys for $userId : $devices")
} if (devices != null) {
Timber.v("## doKeyDownloadForUsers() : Got keys for " + filteredUsers.size + " users") val mutableDevices = HashMap(devices)
for (userId in filteredUsers) { val deviceIds = ArrayList(mutableDevices.keys)
val devices = response.deviceKeys?.get(userId) for (deviceId in deviceIds) {
Timber.v("## doKeyDownloadForUsers() : Got keys for $userId : $devices") // Get the potential previously store device keys for this device
if (devices != null) { val previouslyStoredDeviceKeys = cryptoStore.getUserDevice(deviceId, userId)
val mutableDevices = HashMap(devices) val deviceInfo = mutableDevices[deviceId]
val deviceIds = ArrayList(mutableDevices.keys)
for (deviceId in deviceIds) {
// Get the potential previously store device keys for this device
val previouslyStoredDeviceKeys = cryptoStore.getUserDevice(deviceId, userId)
val deviceInfo = mutableDevices[deviceId]


// in some race conditions (like unit tests) // in some race conditions (like unit tests)
// the self device must be seen as verified // the self device must be seen as verified
if (TextUtils.equals(deviceInfo!!.deviceId, credentials.deviceId) && TextUtils.equals(userId, credentials.userId)) { if (TextUtils.equals(deviceInfo!!.deviceId, credentials.deviceId) && TextUtils.equals(userId, credentials.userId)) {
deviceInfo.verified = MXDeviceInfo.DEVICE_VERIFICATION_VERIFIED deviceInfo.verified = MXDeviceInfo.DEVICE_VERIFICATION_VERIFIED
} }
// Validate received keys // Validate received keys
if (!validateDeviceKeys(deviceInfo, userId, deviceId, previouslyStoredDeviceKeys)) { if (!validateDeviceKeys(deviceInfo, userId, deviceId, previouslyStoredDeviceKeys)) {
// New device keys are not valid. Do not store them // New device keys are not valid. Do not store them
mutableDevices.remove(deviceId) mutableDevices.remove(deviceId)
if (null != previouslyStoredDeviceKeys) { if (null != previouslyStoredDeviceKeys) {
// But keep old validated ones if any // But keep old validated ones if any
mutableDevices[deviceId] = previouslyStoredDeviceKeys mutableDevices[deviceId] = previouslyStoredDeviceKeys
}
} else if (null != previouslyStoredDeviceKeys) {
// The verified status is not sync'ed with hs.
// This is a client side information, valid only for this client.
// So, transfer its previous value
mutableDevices[deviceId]!!.verified = previouslyStoredDeviceKeys.verified
}
}
// Update the store
// Note that devices which aren't in the response will be removed from the stores
cryptoStore.storeUserDevices(userId, mutableDevices)
} }
} else if (null != previouslyStoredDeviceKeys) {
// The verified status is not sync'ed with hs.
// This is a client side information, valid only for this client.
// So, transfer its previous value
mutableDevices[deviceId]!!.verified = previouslyStoredDeviceKeys.verified
} }
onKeysDownloadSucceed(filteredUsers, response.failures)
}
.onError {
Timber.e(it, "##doKeyDownloadForUsers(): error")
onKeysDownloadFailed(filteredUsers)
} }
// Update the store
// Note that devices which aren't in the response will be removed from the stores
cryptoStore.storeUserDevices(userId, mutableDevices)
}
}
return onKeysDownloadSucceed(filteredUsers, response.failures)
} }


/** /**
@ -462,16 +465,15 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
} }


cryptoStore.saveDeviceTrackingStatuses(deviceTrackingStatuses) cryptoStore.saveDeviceTrackingStatuses(deviceTrackingStatuses)
runCatching { doKeyDownloadForUsers(users)
doKeyDownloadForUsers(users) .fold(
}.fold( {
{ Timber.e(it, "## refreshOutdatedDeviceLists() : ERROR updating device keys for users $users")
Timber.v("## refreshOutdatedDeviceLists() : done") },
}, {
{ Timber.v("## refreshOutdatedDeviceLists() : done")
Timber.e(it, "## refreshOutdatedDeviceLists() : ERROR updating device keys for users $users") }
} )
)
} }


companion object { companion object {

View File

@ -18,6 +18,7 @@
package im.vector.matrix.android.internal.crypto package im.vector.matrix.android.internal.crypto


import android.text.TextUtils import android.text.TextUtils
import arrow.core.Try
import im.vector.matrix.android.api.session.crypto.MXCryptoError import im.vector.matrix.android.api.session.crypto.MXCryptoError
import im.vector.matrix.android.api.util.JSON_DICT_PARAMETERIZED_TYPE import im.vector.matrix.android.api.util.JSON_DICT_PARAMETERIZED_TYPE
import im.vector.matrix.android.api.util.JsonDict import im.vector.matrix.android.api.util.JsonDict
@ -79,7 +80,8 @@ internal class MXOlmDevice @Inject constructor(
// //
// The first level keys are timeline ids. // The first level keys are timeline ids.
// The second level keys are strings of form "<senderKey>|<session_id>|<message_index>" // The second level keys are strings of form "<senderKey>|<session_id>|<message_index>"
private val inboundGroupSessionMessageIndexes: MutableMap<String, MutableSet<String>> = HashMap() // Values are true.
private val inboundGroupSessionMessageIndexes: MutableMap<String, MutableMap<String, Boolean>> = HashMap()


init { init {
// Retrieve the account from the store // Retrieve the account from the store
@ -504,25 +506,25 @@ internal class MXOlmDevice @Inject constructor(
keysClaimed: Map<String, String>, keysClaimed: Map<String, String>,
exportFormat: Boolean): Boolean { exportFormat: Boolean): Boolean {
val session = OlmInboundGroupSessionWrapper(sessionKey, exportFormat) val session = OlmInboundGroupSessionWrapper(sessionKey, exportFormat)
runCatching { getInboundGroupSession(sessionId, senderKey, roomId) }
.fold(
{
// If we already have this session, consider updating it
Timber.e("## addInboundGroupSession() : Update for megolm session $senderKey/$sessionId")


val existingFirstKnown = it.firstKnownIndex!! getInboundGroupSession(sessionId, senderKey, roomId).fold(
val newKnownFirstIndex = session.firstKnownIndex {
// Nothing to do in case of error
},
{
// If we already have this session, consider updating it
Timber.e("## addInboundGroupSession() : Update for megolm session $senderKey/$sessionId")


//If our existing session is better we keep it val existingFirstKnown = it.firstKnownIndex!!
if (newKnownFirstIndex != null && existingFirstKnown <= newKnownFirstIndex) { val newKnownFirstIndex = session.firstKnownIndex
session.olmInboundGroupSession?.releaseSession()
return false //If our existing session is better we keep it
} if (newKnownFirstIndex != null && existingFirstKnown <= newKnownFirstIndex) {
}, session.olmInboundGroupSession?.releaseSession()
{ return false
// Nothing to do in case of error }
} }
) )


// sanity check // sanity check
if (null == session.olmInboundGroupSession) { if (null == session.olmInboundGroupSession) {
@ -593,8 +595,12 @@ internal class MXOlmDevice @Inject constructor(
continue continue
} }


runCatching { getInboundGroupSession(sessionId, senderKey, roomId) } getInboundGroupSession(sessionId, senderKey, roomId)
.fold( .fold(
{
// Session does not already exist, add it
sessions.add(session)
},
{ {
// If we already have this session, consider updating it // If we already have this session, consider updating it
Timber.e("## importInboundGroupSession() : Update for megolm session $senderKey/$sessionId") Timber.e("## importInboundGroupSession() : Update for megolm session $senderKey/$sessionId")
@ -607,12 +613,7 @@ internal class MXOlmDevice @Inject constructor(
sessions.add(session) sessions.add(session)
} }
Unit Unit
},
{
// Session does not already exist, add it
sessions.add(session)
} }

) )
} }


@ -647,55 +648,61 @@ internal class MXOlmDevice @Inject constructor(
roomId: String, roomId: String,
timeline: String?, timeline: String?,
sessionId: String, sessionId: String,
senderKey: String): OlmDecryptionResult { senderKey: String): Try<OlmDecryptionResult> {
val session = getInboundGroupSession(sessionId, senderKey, roomId) return getInboundGroupSession(sessionId, senderKey, roomId)
// Check that the room id matches the original one for the session. This stops .flatMap { session ->
// the HS pretending a message was targeting a different room. // Check that the room id matches the original one for the session. This stops
if (roomId == session.roomId) { // the HS pretending a message was targeting a different room.
var decryptResult: OlmInboundGroupSession.DecryptMessageResult? = null if (roomId == session.roomId) {
try { var decryptResult: OlmInboundGroupSession.DecryptMessageResult? = null
decryptResult = session.olmInboundGroupSession!!.decryptMessage(body) try {
} catch (e: OlmException) { decryptResult = session.olmInboundGroupSession!!.decryptMessage(body)
Timber.e(e, "## decryptGroupMessage () : decryptMessage failed") } catch (e: OlmException) {
throw MXCryptoError.OlmError(e) Timber.e(e, "## decryptGroupMessage () : decryptMessage failed")
} return@flatMap Try.Failure(MXCryptoError.OlmError(e))
}


if (null != timeline) { if (null != timeline) {
val timelineSet = inboundGroupSessionMessageIndexes.getOrPut(timeline) { mutableSetOf() } if (!inboundGroupSessionMessageIndexes.containsKey(timeline)) {
inboundGroupSessionMessageIndexes[timeline] = HashMap()
}


val messageIndexKey = senderKey + "|" + sessionId + "|" + decryptResult.mIndex val messageIndexKey = senderKey + "|" + sessionId + "|" + decryptResult.mIndex


if (timelineSet.contains(messageIndexKey)) { if (inboundGroupSessionMessageIndexes[timeline]?.get(messageIndexKey) != null) {
val reason = String.format(MXCryptoError.DUPLICATE_MESSAGE_INDEX_REASON, decryptResult.mIndex) val reason = String.format(MXCryptoError.DUPLICATE_MESSAGE_INDEX_REASON, decryptResult.mIndex)
Timber.e("## decryptGroupMessage() : $reason") Timber.e("## decryptGroupMessage() : $reason")
throw MXCryptoError.Base(MXCryptoError.ErrorType.DUPLICATED_MESSAGE_INDEX, reason) return@flatMap Try.Failure(MXCryptoError.Base(MXCryptoError.ErrorType.DUPLICATED_MESSAGE_INDEX, reason))
}

inboundGroupSessionMessageIndexes[timeline]!!.put(messageIndexKey, true)
}

store.storeInboundGroupSessions(listOf(session))
val payload = try {
val adapter = MoshiProvider.providesMoshi().adapter<JsonDict>(JSON_DICT_PARAMETERIZED_TYPE)
val payloadString = convertFromUTF8(decryptResult.mDecryptedMessage)
adapter.fromJson(payloadString)
} catch (e: Exception) {
Timber.e("## decryptGroupMessage() : fails to parse the payload")
return@flatMap Try.Failure(
MXCryptoError.Base(MXCryptoError.ErrorType.BAD_DECRYPTED_FORMAT, MXCryptoError.BAD_DECRYPTED_FORMAT_TEXT_REASON))
}

return@flatMap Try.just(
OlmDecryptionResult(
payload,
session.keysClaimed,
senderKey,
session.forwardingCurve25519KeyChain
)
)
} else {
val reason = String.format(MXCryptoError.INBOUND_SESSION_MISMATCH_ROOM_ID_REASON, roomId, session.roomId)
Timber.e("## decryptGroupMessage() : $reason")
return@flatMap Try.Failure(MXCryptoError.Base(MXCryptoError.ErrorType.INBOUND_SESSION_MISMATCH_ROOM_ID, reason))
}
} }

timelineSet.add(messageIndexKey)
}

store.storeInboundGroupSessions(listOf(session))
val payload = try {
val adapter = MoshiProvider.providesMoshi().adapter<JsonDict>(JSON_DICT_PARAMETERIZED_TYPE)
val payloadString = convertFromUTF8(decryptResult.mDecryptedMessage)
adapter.fromJson(payloadString)
} catch (e: Exception) {
Timber.e("## decryptGroupMessage() : fails to parse the payload")
throw
MXCryptoError.Base(MXCryptoError.ErrorType.BAD_DECRYPTED_FORMAT, MXCryptoError.BAD_DECRYPTED_FORMAT_TEXT_REASON)
}

return OlmDecryptionResult(
payload,
session.keysClaimed,
senderKey,
session.forwardingCurve25519KeyChain
)
} else {
val reason = String.format(MXCryptoError.INBOUND_SESSION_MISMATCH_ROOM_ID_REASON, roomId, session.roomId)
Timber.e("## decryptGroupMessage() : $reason")
throw MXCryptoError.Base(MXCryptoError.ErrorType.INBOUND_SESSION_MISMATCH_ROOM_ID, reason)
}
} }


/** /**
@ -759,26 +766,26 @@ internal class MXOlmDevice @Inject constructor(
* @param senderKey the base64-encoded curve25519 key of the sender. * @param senderKey the base64-encoded curve25519 key of the sender.
* @return the inbound group session. * @return the inbound group session.
*/ */
fun getInboundGroupSession(sessionId: String?, senderKey: String?, roomId: String?): OlmInboundGroupSessionWrapper { fun getInboundGroupSession(sessionId: String?, senderKey: String?, roomId: String?): Try<OlmInboundGroupSessionWrapper> {
if (sessionId.isNullOrBlank() || senderKey.isNullOrBlank()) { if (sessionId.isNullOrBlank() || senderKey.isNullOrBlank()) {
throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_SENDER_KEY, MXCryptoError.ERROR_MISSING_PROPERTY_REASON) return Try.Failure(MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_SENDER_KEY, MXCryptoError.ERROR_MISSING_PROPERTY_REASON))
} }


val session = store.getInboundGroupSession(sessionId, senderKey) val session = store.getInboundGroupSession(sessionId, senderKey)


if (session != null) { return if (null != session) {
// Check that the room id matches the original one for the session. This stops // Check that the room id matches the original one for the session. This stops
// the HS pretending a message was targeting a different room. // the HS pretending a message was targeting a different room.
if (!TextUtils.equals(roomId, session.roomId)) { if (!TextUtils.equals(roomId, session.roomId)) {
val errorDescription = String.format(MXCryptoError.INBOUND_SESSION_MISMATCH_ROOM_ID_REASON, roomId, session.roomId) val errorDescription = String.format(MXCryptoError.INBOUND_SESSION_MISMATCH_ROOM_ID_REASON, roomId, session.roomId)
Timber.e("## getInboundGroupSession() : $errorDescription") Timber.e("## getInboundGroupSession() : $errorDescription")
throw MXCryptoError.Base(MXCryptoError.ErrorType.INBOUND_SESSION_MISMATCH_ROOM_ID, errorDescription) Try.Failure(MXCryptoError.Base(MXCryptoError.ErrorType.INBOUND_SESSION_MISMATCH_ROOM_ID, errorDescription))
} else { } else {
return session Try.just(session)
} }
} else { } else {
Timber.e("## getInboundGroupSession() : Cannot retrieve inbound group session $sessionId") Timber.e("## getInboundGroupSession() : Cannot retrieve inbound group session $sessionId")
throw MXCryptoError.Base(MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID, MXCryptoError.UNKNOWN_INBOUND_SESSION_ID_REASON) Try.Failure(MXCryptoError.Base(MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID, MXCryptoError.UNKNOWN_INBOUND_SESSION_ID_REASON))
} }
} }


@ -791,6 +798,6 @@ internal class MXOlmDevice @Inject constructor(
* @return true if the unbound session keys are known. * @return true if the unbound session keys are known.
*/ */
fun hasInboundSessionKeys(roomId: String, senderKey: String, sessionId: String): Boolean { fun hasInboundSessionKeys(roomId: String, senderKey: String, sessionId: String): Boolean {
return runCatching { getInboundGroupSession(sessionId, senderKey, roomId) }.isSuccess return getInboundGroupSession(sessionId, senderKey, roomId).isSuccess()
} }
} }

View File

@ -17,6 +17,8 @@
package im.vector.matrix.android.internal.crypto package im.vector.matrix.android.internal.crypto


import im.vector.matrix.android.api.auth.data.Credentials import im.vector.matrix.android.api.auth.data.Credentials
import im.vector.matrix.android.internal.session.SessionScope
import java.util.*
import javax.inject.Inject import javax.inject.Inject


internal class ObjectSigner @Inject constructor(private val credentials: Credentials, internal class ObjectSigner @Inject constructor(private val credentials: Credentials,

View File

@ -16,6 +16,8 @@


package im.vector.matrix.android.internal.crypto package im.vector.matrix.android.internal.crypto


import arrow.core.Try
import arrow.instances.`try`.applicativeError.handleError
import im.vector.matrix.android.api.auth.data.Credentials import im.vector.matrix.android.api.auth.data.Credentials
import im.vector.matrix.android.internal.crypto.model.MXKey import im.vector.matrix.android.internal.crypto.model.MXKey
import im.vector.matrix.android.internal.crypto.model.rest.KeysUploadResponse import im.vector.matrix.android.internal.crypto.model.rest.KeysUploadResponse
@ -57,13 +59,13 @@ internal class OneTimeKeysUploader @Inject constructor(
/** /**
* Check if the OTK must be uploaded. * Check if the OTK must be uploaded.
*/ */
suspend fun maybeUploadOneTimeKeys() { suspend fun maybeUploadOneTimeKeys(): Try<Unit> {
if (oneTimeKeyCheckInProgress) { if (oneTimeKeyCheckInProgress) {
return return Try.just(Unit)
} }
if (System.currentTimeMillis() - lastOneTimeKeyCheck < ONE_TIME_KEY_UPLOAD_PERIOD) { if (System.currentTimeMillis() - lastOneTimeKeyCheck < ONE_TIME_KEY_UPLOAD_PERIOD) {
// we've done a key upload recently. // we've done a key upload recently.
return return Try.just(Unit)
} }


lastOneTimeKeyCheck = System.currentTimeMillis() lastOneTimeKeyCheck = System.currentTimeMillis()
@ -79,31 +81,41 @@ internal class OneTimeKeysUploader @Inject constructor(
// discard the oldest private keys first. This will eventually clean // discard the oldest private keys first. This will eventually clean
// out stale private keys that won't receive a message. // out stale private keys that won't receive a message.
val keyLimit = Math.floor(maxOneTimeKeys / 2.0).toInt() val keyLimit = Math.floor(maxOneTimeKeys / 2.0).toInt()
if (oneTimeKeyCount != null) { val result = if (oneTimeKeyCount != null) {
uploadOTK(oneTimeKeyCount!!, keyLimit) uploadOTK(oneTimeKeyCount!!, keyLimit)
} else { } else {
// ask the server how many keys we have // ask the server how many keys we have
val uploadKeysParams = UploadKeysTask.Params(null, null, credentials.deviceId!!) val uploadKeysParams = UploadKeysTask.Params(null, null, credentials.deviceId!!)
val response = uploadKeysTask.execute(uploadKeysParams) uploadKeysTask.execute(uploadKeysParams)
// We need to keep a pool of one time public keys on the server so that .flatMap {
// other devices can start conversations with us. But we can only store // We need to keep a pool of one time public keys on the server so that
// a finite number of private keys in the olm Account object. // other devices can start conversations with us. But we can only store
// To complicate things further then can be a delay between a device // a finite number of private keys in the olm Account object.
// claiming a public one time key from the server and it sending us a // To complicate things further then can be a delay between a device
// message. We need to keep the corresponding private key locally until // claiming a public one time key from the server and it sending us a
// we receive the message. // message. We need to keep the corresponding private key locally until
// But that message might never arrive leaving us stuck with duff // we receive the message.
// private keys clogging up our local storage. // But that message might never arrive leaving us stuck with duff
// So we need some kind of engineering compromise to balance all of // private keys clogging up our local storage.
// these factors. // So we need some kind of engineering compromise to balance all of
// TODO Why we do not set oneTimeKeyCount here? // these factors.
// TODO This is not needed anymore, see https://github.com/matrix-org/matrix-js-sdk/pull/493 (TODO on iOS also) // TODO Why we do not set oneTimeKeyCount here?
val keyCount = response.oneTimeKeyCountsForAlgorithm(MXKey.KEY_SIGNED_CURVE_25519_TYPE) // TODO This is not needed anymore, see https://github.com/matrix-org/matrix-js-sdk/pull/493 (TODO on iOS also)
uploadOTK(keyCount, keyLimit) val keyCount = it.oneTimeKeyCountsForAlgorithm(MXKey.KEY_SIGNED_CURVE_25519_TYPE)
uploadOTK(keyCount, keyLimit)
}
} }
Timber.v("## uploadKeys() : success") return result
oneTimeKeyCount = null .map {
oneTimeKeyCheckInProgress = false Timber.v("## uploadKeys() : success")
oneTimeKeyCount = null
oneTimeKeyCheckInProgress = false
}
.handleError {
Timber.e(it, "## uploadKeys() : failed")
oneTimeKeyCount = null
oneTimeKeyCheckInProgress = false
}
} }


/** /**
@ -112,26 +124,29 @@ internal class OneTimeKeysUploader @Inject constructor(
* @param keyCount the key count * @param keyCount the key count
* @param keyLimit the limit * @param keyLimit the limit
*/ */
private suspend fun uploadOTK(keyCount: Int, keyLimit: Int) { private suspend fun uploadOTK(keyCount: Int, keyLimit: Int): Try<Unit> {
if (keyLimit <= keyCount) { if (keyLimit <= keyCount) {
// If we don't need to generate any more keys then we are done. // If we don't need to generate any more keys then we are done.
return return Try.just(Unit)
} }

val keysThisLoop = Math.min(keyLimit - keyCount, ONE_TIME_KEY_GENERATION_MAX_NUMBER) val keysThisLoop = Math.min(keyLimit - keyCount, ONE_TIME_KEY_GENERATION_MAX_NUMBER)
olmDevice.generateOneTimeKeys(keysThisLoop) olmDevice.generateOneTimeKeys(keysThisLoop)
val response = uploadOneTimeKeys() return uploadOneTimeKeys()
if (response.hasOneTimeKeyCountsForAlgorithm(MXKey.KEY_SIGNED_CURVE_25519_TYPE)) { .flatMap {
uploadOTK(response.oneTimeKeyCountsForAlgorithm(MXKey.KEY_SIGNED_CURVE_25519_TYPE), keyLimit) if (it.hasOneTimeKeyCountsForAlgorithm(MXKey.KEY_SIGNED_CURVE_25519_TYPE)) {
} else { uploadOTK(it.oneTimeKeyCountsForAlgorithm(MXKey.KEY_SIGNED_CURVE_25519_TYPE), keyLimit)
Timber.e("## uploadLoop() : response for uploading keys does not contain one_time_key_counts.signed_curve25519") } else {
throw Exception("response for uploading keys does not contain one_time_key_counts.signed_curve25519") Timber.e("## uploadLoop() : response for uploading keys does not contain one_time_key_counts.signed_curve25519")
} Try.raise(Exception("response for uploading keys does not contain one_time_key_counts.signed_curve25519"))
}
}
} }


/** /**
* Upload my user's one time keys. * Upload my user's one time keys.
*/ */
private suspend fun uploadOneTimeKeys(): KeysUploadResponse { private suspend fun uploadOneTimeKeys(): Try<KeysUploadResponse> {
val oneTimeKeys = olmDevice.getOneTimeKeys() val oneTimeKeys = olmDevice.getOneTimeKeys()
val oneTimeJson = HashMap<String, Any>() val oneTimeJson = HashMap<String, Any>()


@ -154,10 +169,13 @@ internal class OneTimeKeysUploader @Inject constructor(
// For now, we set the device id explicitly, as we may not be using the // For now, we set the device id explicitly, as we may not be using the
// same one as used in login. // same one as used in login.
val uploadParams = UploadKeysTask.Params(null, oneTimeJson, credentials.deviceId!!) val uploadParams = UploadKeysTask.Params(null, oneTimeJson, credentials.deviceId!!)
val response = uploadKeysTask.execute(uploadParams) return uploadKeysTask
lastPublishedOneTimeKeys = oneTimeKeys .execute(uploadParams)
olmDevice.markKeysAsPublished() .map {
return response lastPublishedOneTimeKeys = oneTimeKeys
olmDevice.markKeysAsPublished()
it
}
} }


companion object { companion object {

View File

@ -299,12 +299,10 @@ internal class OutgoingRoomKeyRequestManager @Inject constructor(
// TODO Change this two hard coded key to something better // TODO Change this two hard coded key to something better
contentMap.setObject(recipient["userId"], recipient["deviceId"], message) contentMap.setObject(recipient["userId"], recipient["deviceId"], message)
} }
sendToDeviceTask sendToDeviceTask.configureWith(SendToDeviceTask.Params(EventType.ROOM_KEY_REQUEST, contentMap, transactionId))
.configureWith(SendToDeviceTask.Params(EventType.ROOM_KEY_REQUEST, contentMap, transactionId)) { .dispatchTo(callback)
this.callback = callback .executeOn(TaskThread.CALLER)
this.callbackThread = TaskThread.CALLER .callbackOn(TaskThread.CALLER)
this.executionThread = TaskThread.CALLER
}
.executeBy(taskExecutor) .executeBy(taskExecutor)
} }



View File

@ -17,6 +17,7 @@
package im.vector.matrix.android.internal.crypto.actions package im.vector.matrix.android.internal.crypto.actions


import android.text.TextUtils import android.text.TextUtils
import arrow.core.Try
import im.vector.matrix.android.internal.crypto.MXOlmDevice import im.vector.matrix.android.internal.crypto.MXOlmDevice
import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo
import im.vector.matrix.android.internal.crypto.model.MXKey import im.vector.matrix.android.internal.crypto.model.MXKey
@ -31,7 +32,7 @@ internal class EnsureOlmSessionsForDevicesAction @Inject constructor(private val
private val oneTimeKeysForUsersDeviceTask: ClaimOneTimeKeysForUsersDeviceTask) { private val oneTimeKeysForUsersDeviceTask: ClaimOneTimeKeysForUsersDeviceTask) {




suspend fun handle(devicesByUser: Map<String, List<MXDeviceInfo>>): MXUsersDevicesMap<MXOlmSessionResult> { suspend fun handle(devicesByUser: Map<String, List<MXDeviceInfo>>): Try<MXUsersDevicesMap<MXOlmSessionResult>> {
val devicesWithoutSession = ArrayList<MXDeviceInfo>() val devicesWithoutSession = ArrayList<MXDeviceInfo>()


val results = MXUsersDevicesMap<MXOlmSessionResult>() val results = MXUsersDevicesMap<MXOlmSessionResult>()
@ -57,7 +58,7 @@ internal class EnsureOlmSessionsForDevicesAction @Inject constructor(private val
} }


if (devicesWithoutSession.size == 0) { if (devicesWithoutSession.size == 0) {
return results return Try.just(results)
} }


// Prepare the request for claiming one-time keys // Prepare the request for claiming one-time keys
@ -78,36 +79,39 @@ internal class EnsureOlmSessionsForDevicesAction @Inject constructor(private val
Timber.v("## claimOneTimeKeysForUsersDevices() : $usersDevicesToClaim") Timber.v("## claimOneTimeKeysForUsersDevices() : $usersDevicesToClaim")


val claimParams = ClaimOneTimeKeysForUsersDeviceTask.Params(usersDevicesToClaim) val claimParams = ClaimOneTimeKeysForUsersDeviceTask.Params(usersDevicesToClaim)
val oneTimeKeys = oneTimeKeysForUsersDeviceTask.execute(claimParams) return oneTimeKeysForUsersDeviceTask
Timber.v("## claimOneTimeKeysForUsersDevices() : keysClaimResponse.oneTimeKeys: $oneTimeKeys") .execute(claimParams)
for (userId in userIds) { .map {
val deviceInfos = devicesByUser[userId] Timber.v("## claimOneTimeKeysForUsersDevices() : keysClaimResponse.oneTimeKeys: $it")
for (deviceInfo in deviceInfos!!) { for (userId in userIds) {
var oneTimeKey: MXKey? = null val deviceInfos = devicesByUser[userId]
val deviceIds = oneTimeKeys.getUserDeviceIds(userId) for (deviceInfo in deviceInfos!!) {
if (null != deviceIds) { var oneTimeKey: MXKey? = null
for (deviceId in deviceIds) { val deviceIds = it.getUserDeviceIds(userId)
val olmSessionResult = results.getObject(userId, deviceId) if (null != deviceIds) {
if (olmSessionResult!!.sessionId != null) { for (deviceId in deviceIds) {
// We already have a result for this device val olmSessionResult = results.getObject(userId, deviceId)
continue if (olmSessionResult!!.sessionId != null) {
// We already have a result for this device
continue
}
val key = it.getObject(userId, deviceId)
if (key?.type == oneTimeKeyAlgorithm) {
oneTimeKey = key
}
if (oneTimeKey == null) {
Timber.v("## ensureOlmSessionsForDevices() : No one-time keys " + oneTimeKeyAlgorithm
+ " for device " + userId + " : " + deviceId)
continue
}
// Update the result for this device in results
olmSessionResult.sessionId = verifyKeyAndStartSession(oneTimeKey, userId, deviceInfo)
}
}
} }
val key = oneTimeKeys.getObject(userId, deviceId)
if (key?.type == oneTimeKeyAlgorithm) {
oneTimeKey = key
}
if (oneTimeKey == null) {
Timber.v("## ensureOlmSessionsForDevices() : No one-time keys " + oneTimeKeyAlgorithm
+ " for device " + userId + " : " + deviceId)
continue
}
// Update the result for this device in results
olmSessionResult.sessionId = verifyKeyAndStartSession(oneTimeKey, userId, deviceInfo)
} }
results
} }
}
}
return results
} }


private fun verifyKeyAndStartSession(oneTimeKey: MXKey, userId: String, deviceInfo: MXDeviceInfo): String? { private fun verifyKeyAndStartSession(oneTimeKey: MXKey, userId: String, deviceInfo: MXDeviceInfo): String? {

View File

@ -17,11 +17,14 @@
package im.vector.matrix.android.internal.crypto.actions package im.vector.matrix.android.internal.crypto.actions


import android.text.TextUtils import android.text.TextUtils
import arrow.core.Try
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.internal.crypto.MXOlmDevice import im.vector.matrix.android.internal.crypto.MXOlmDevice
import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo
import im.vector.matrix.android.internal.crypto.model.MXOlmSessionResult import im.vector.matrix.android.internal.crypto.model.MXOlmSessionResult
import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
import im.vector.matrix.android.internal.session.SessionScope
import timber.log.Timber import timber.log.Timber
import java.util.* import java.util.*
import javax.inject.Inject import javax.inject.Inject
@ -34,7 +37,7 @@ internal class EnsureOlmSessionsForUsersAction @Inject constructor(private val o
* Try to make sure we have established olm sessions for the given users. * Try to make sure we have established olm sessions for the given users.
* @param users a list of user ids. * @param users a list of user ids.
*/ */
suspend fun handle(users: List<String>) : MXUsersDevicesMap<MXOlmSessionResult> { suspend fun handle(users: List<String>) : Try<MXUsersDevicesMap<MXOlmSessionResult>> {
Timber.v("## ensureOlmSessionsForUsers() : ensureOlmSessionsForUsers $users") Timber.v("## ensureOlmSessionsForUsers() : ensureOlmSessionsForUsers $users")
val devicesByUser = HashMap<String /* userId */, MutableList<MXDeviceInfo>>() val devicesByUser = HashMap<String /* userId */, MutableList<MXDeviceInfo>>()



View File

@ -26,6 +26,7 @@ import im.vector.matrix.android.internal.crypto.RoomDecryptorProvider
import im.vector.matrix.android.internal.crypto.model.ImportRoomKeysResult import im.vector.matrix.android.internal.crypto.model.ImportRoomKeysResult
import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
import im.vector.matrix.android.internal.session.SessionScope
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject



View File

@ -19,6 +19,7 @@ package im.vector.matrix.android.internal.crypto.actions
import im.vector.matrix.android.api.auth.data.Credentials import im.vector.matrix.android.api.auth.data.Credentials
import im.vector.matrix.android.internal.crypto.keysbackup.KeysBackup import im.vector.matrix.android.internal.crypto.keysbackup.KeysBackup
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
import im.vector.matrix.android.internal.session.SessionScope
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject



View File

@ -17,6 +17,7 @@


package im.vector.matrix.android.internal.crypto.algorithms package im.vector.matrix.android.internal.crypto.algorithms


import arrow.core.Try
import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.internal.crypto.IncomingRoomKeyRequest import im.vector.matrix.android.internal.crypto.IncomingRoomKeyRequest
import im.vector.matrix.android.internal.crypto.MXEventDecryptionResult import im.vector.matrix.android.internal.crypto.MXEventDecryptionResult
@ -34,7 +35,7 @@ internal interface IMXDecrypting {
* @param timeline the id of the timeline where the event is decrypted. It is used to prevent replay attack. * @param timeline the id of the timeline where the event is decrypted. It is used to prevent replay attack.
* @return the decryption information, or an error * @return the decryption information, or an error
*/ */
suspend fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult suspend fun decryptEvent(event: Event, timeline: String): Try<MXEventDecryptionResult>


/** /**
* Handle a key event. * Handle a key event.

View File

@ -17,6 +17,7 @@


package im.vector.matrix.android.internal.crypto.algorithms package im.vector.matrix.android.internal.crypto.algorithms


import arrow.core.Try
import im.vector.matrix.android.api.session.events.model.Content import im.vector.matrix.android.api.session.events.model.Content


/** /**
@ -30,7 +31,7 @@ internal interface IMXEncrypting {
* @param eventContent the content of the event. * @param eventContent the content of the event.
* @param eventType the type of the event. * @param eventType the type of the event.
* @param userIds the room members the event will be sent to. * @param userIds the room members the event will be sent to.
* @return the encrypted content * @return the encrypted content wrapped by [Try]
*/ */
suspend fun encryptEventContent(eventContent: Content, eventType: String, userIds: List<String>): Content suspend fun encryptEventContent(eventContent: Content, eventType: String, userIds: List<String>): Try<Content>
} }

View File

@ -18,6 +18,7 @@
package im.vector.matrix.android.internal.crypto.algorithms.megolm package im.vector.matrix.android.internal.crypto.algorithms.megolm


import android.text.TextUtils import android.text.TextUtils
import arrow.core.Try
import im.vector.matrix.android.api.auth.data.Credentials import im.vector.matrix.android.api.auth.data.Credentials
import im.vector.matrix.android.api.session.crypto.MXCryptoError import im.vector.matrix.android.api.session.crypto.MXCryptoError
import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.events.model.Event
@ -28,6 +29,7 @@ import im.vector.matrix.android.internal.crypto.actions.EnsureOlmSessionsForDevi
import im.vector.matrix.android.internal.crypto.actions.MessageEncrypter import im.vector.matrix.android.internal.crypto.actions.MessageEncrypter
import im.vector.matrix.android.internal.crypto.algorithms.IMXDecrypting import im.vector.matrix.android.internal.crypto.algorithms.IMXDecrypting
import im.vector.matrix.android.internal.crypto.keysbackup.KeysBackup import im.vector.matrix.android.internal.crypto.keysbackup.KeysBackup
import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo
import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap
import im.vector.matrix.android.internal.crypto.model.event.EncryptedEventContent 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.RoomKeyContent
@ -36,9 +38,11 @@ import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
import im.vector.matrix.android.internal.crypto.tasks.SendToDeviceTask import im.vector.matrix.android.internal.crypto.tasks.SendToDeviceTask
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import timber.log.Timber import timber.log.Timber
import java.util.*
import kotlin.collections.HashMap


internal class MXMegolmDecryption(private val credentials: Credentials, internal class MXMegolmDecryption(private val credentials: Credentials,
private val olmDevice: MXOlmDevice, private val olmDevice: MXOlmDevice,
@ -59,46 +63,30 @@ internal class MXMegolmDecryption(private val credentials: Credentials,
*/ */
private var pendingEvents: MutableMap<String /* senderKey|sessionId */, MutableMap<String /* timelineId */, MutableList<Event>>> = HashMap() private var pendingEvents: MutableMap<String /* senderKey|sessionId */, MutableMap<String /* timelineId */, MutableList<Event>>> = HashMap()


override suspend fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult { override suspend fun decryptEvent(event: Event, timeline: String): Try<MXEventDecryptionResult> {
return decryptEvent(event, timeline, true) return decryptEvent(event, timeline, true)
} }


private suspend fun decryptEvent(event: Event, timeline: String, requestKeysOnFail: Boolean): MXEventDecryptionResult { private fun decryptEvent(event: Event, timeline: String, requestKeysOnFail: Boolean): Try<MXEventDecryptionResult> {
if (event.roomId.isNullOrBlank()) { if (event.roomId.isNullOrBlank()) {
throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_FIELDS, MXCryptoError.MISSING_FIELDS_REASON) return Try.Failure(MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_FIELDS, MXCryptoError.MISSING_FIELDS_REASON))
} }


val encryptedEventContent = event.content.toModel<EncryptedEventContent>() val encryptedEventContent = event.content.toModel<EncryptedEventContent>()
?: throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_FIELDS, MXCryptoError.MISSING_FIELDS_REASON) ?: return Try.Failure(MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_FIELDS, MXCryptoError.MISSING_FIELDS_REASON))


if (encryptedEventContent.senderKey.isNullOrBlank() if (encryptedEventContent.senderKey.isNullOrBlank()
|| encryptedEventContent.sessionId.isNullOrBlank() || encryptedEventContent.sessionId.isNullOrBlank()
|| encryptedEventContent.ciphertext.isNullOrBlank()) { || encryptedEventContent.ciphertext.isNullOrBlank()) {
throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_FIELDS, MXCryptoError.MISSING_FIELDS_REASON) return Try.Failure(MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_FIELDS, MXCryptoError.MISSING_FIELDS_REASON))
} }


return runCatching { return olmDevice.decryptGroupMessage(encryptedEventContent.ciphertext,
olmDevice.decryptGroupMessage(encryptedEventContent.ciphertext, event.roomId,
event.roomId, timeline,
timeline, encryptedEventContent.sessionId,
encryptedEventContent.sessionId, encryptedEventContent.senderKey)
encryptedEventContent.senderKey)
}
.fold( .fold(
{ olmDecryptionResult ->
// the decryption succeeds
if (olmDecryptionResult.payload != null) {
MXEventDecryptionResult(
clearEvent = olmDecryptionResult.payload,
senderCurve25519Key = olmDecryptionResult.senderKey,
claimedEd25519Key = olmDecryptionResult.keysClaimed?.get("ed25519"),
forwardingCurve25519KeyChain = olmDecryptionResult.forwardingCurve25519KeyChain
?: emptyList()
)
} else {
throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_FIELDS, MXCryptoError.MISSING_FIELDS_REASON)
}
},
{ throwable -> { throwable ->
if (throwable is MXCryptoError.OlmError) { if (throwable is MXCryptoError.OlmError) {
// TODO Check the value of .message // TODO Check the value of .message
@ -112,10 +100,10 @@ internal class MXMegolmDecryption(private val credentials: Credentials,
val reason = String.format(MXCryptoError.OLM_REASON, throwable.olmException.message) val reason = String.format(MXCryptoError.OLM_REASON, throwable.olmException.message)
val detailedReason = String.format(MXCryptoError.DETAILED_OLM_REASON, encryptedEventContent.ciphertext, reason) val detailedReason = String.format(MXCryptoError.DETAILED_OLM_REASON, encryptedEventContent.ciphertext, reason)


throw MXCryptoError.Base( Try.Failure(MXCryptoError.Base(
MXCryptoError.ErrorType.OLM, MXCryptoError.ErrorType.OLM,
reason, reason,
detailedReason) detailedReason))
} }
if (throwable is MXCryptoError.Base) { if (throwable is MXCryptoError.Base) {
if (throwable.errorType == MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID) { if (throwable.errorType == MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID) {
@ -125,7 +113,23 @@ internal class MXMegolmDecryption(private val credentials: Credentials,
} }
} }
} }
throw throwable
Try.Failure(throwable)
},
{ olmDecryptionResult ->
// the decryption succeeds
if (olmDecryptionResult.payload != null) {
Try.just(
MXEventDecryptionResult(
clearEvent = olmDecryptionResult.payload,
senderCurve25519Key = olmDecryptionResult.senderKey,
claimedEd25519Key = olmDecryptionResult.keysClaimed?.get("ed25519"),
forwardingCurve25519KeyChain = olmDecryptionResult.forwardingCurve25519KeyChain ?: emptyList()
)
)
} else {
Try.Failure(MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_FIELDS, MXCryptoError.MISSING_FIELDS_REASON))
}
} }
) )
} }
@ -308,49 +312,54 @@ internal class MXMegolmDecryption(private val credentials: Credentials,
return return
} }
val userId = request.userId ?: return val userId = request.userId ?: return
GlobalScope.launch(coroutineDispatchers.crypto) { CoroutineScope(coroutineDispatchers.crypto).launch {
runCatching { deviceListManager.downloadKeys(listOf(userId), false) } deviceListManager
.mapCatching { .downloadKeys(listOf(userId), false)
.flatMap {
val deviceId = request.deviceId val deviceId = request.deviceId
val deviceInfo = cryptoStore.getUserDevice(deviceId ?: "", userId) val deviceInfo = cryptoStore.getUserDevice(deviceId ?: "", userId)
if (deviceInfo == null) { if (deviceInfo == null) {
throw RuntimeException() throw RuntimeException()
} else { } else {
val devicesByUser = mapOf(userId to listOf(deviceInfo)) val devicesByUser = HashMap<String, List<MXDeviceInfo>>()
val usersDeviceMap = ensureOlmSessionsForDevicesAction.handle(devicesByUser) devicesByUser[userId] = ArrayList(Arrays.asList(deviceInfo))
val body = request.requestBody ensureOlmSessionsForDevicesAction
val olmSessionResult = usersDeviceMap.getObject(userId, deviceId) .handle(devicesByUser)
if (olmSessionResult?.sessionId == null) { .flatMap {
// no session with this device, probably because there val body = request.requestBody
// were no one-time keys. val olmSessionResult = it.getObject(userId, deviceId)
return@mapCatching if (olmSessionResult?.sessionId == null) {
} // no session with this device, probably because there
Timber.v("## shareKeysWithDevice() : sharing keys for session" + // were no one-time keys.
" ${body?.senderKey}|${body?.sessionId} with device $userId:$deviceId") Try.just(Unit)
}
Timber.v("## shareKeysWithDevice() : sharing keys for session" +
" ${body?.senderKey}|${body?.sessionId} with device $userId:$deviceId")


val payloadJson = mutableMapOf<String, Any>("type" to EventType.FORWARDED_ROOM_KEY) val payloadJson = HashMap<String, Any>()
runCatching { olmDevice.getInboundGroupSession(body?.sessionId, body?.senderKey, body?.roomId) } payloadJson["type"] = EventType.FORWARDED_ROOM_KEY
.fold(
{
// TODO
payloadJson["content"] = it.exportKeys()
?: ""
},
{
// TODO
}


) olmDevice.getInboundGroupSession(body?.sessionId, body?.senderKey, body?.roomId)
.fold(
{
// TODO
},
{
// TODO
payloadJson["content"] = it.exportKeys() ?: ""
}
)


val encodedPayload = messageEncrypter.encryptMessage(payloadJson, listOf(deviceInfo)) val encodedPayload = messageEncrypter.encryptMessage(payloadJson, Arrays.asList(deviceInfo))
val sendToDeviceMap = MXUsersDevicesMap<Any>() val sendToDeviceMap = MXUsersDevicesMap<Any>()
sendToDeviceMap.setObject(userId, deviceId, encodedPayload) sendToDeviceMap.setObject(userId, deviceId, encodedPayload)
Timber.v("## shareKeysWithDevice() : sending to $userId:$deviceId") Timber.v("## shareKeysWithDevice() : sending to $userId:$deviceId")
val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, sendToDeviceMap) val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, sendToDeviceMap)
sendToDeviceTask.execute(sendToDeviceParams) sendToDeviceTask.execute(sendToDeviceParams)
}
} }
} }
} }
} }
}


}

View File

@ -24,6 +24,7 @@ import im.vector.matrix.android.internal.crypto.actions.EnsureOlmSessionsForDevi
import im.vector.matrix.android.internal.crypto.actions.MessageEncrypter import im.vector.matrix.android.internal.crypto.actions.MessageEncrypter
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
import im.vector.matrix.android.internal.crypto.tasks.SendToDeviceTask import im.vector.matrix.android.internal.crypto.tasks.SendToDeviceTask
import im.vector.matrix.android.internal.session.SessionScope
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
import javax.inject.Inject import javax.inject.Inject



View File

@ -19,6 +19,7 @@
package im.vector.matrix.android.internal.crypto.algorithms.megolm package im.vector.matrix.android.internal.crypto.algorithms.megolm


import android.text.TextUtils import android.text.TextUtils
import arrow.core.Try
import im.vector.matrix.android.api.auth.data.Credentials import im.vector.matrix.android.api.auth.data.Credentials
import im.vector.matrix.android.api.session.crypto.MXCryptoError import im.vector.matrix.android.api.session.crypto.MXCryptoError
import im.vector.matrix.android.api.session.events.model.Content import im.vector.matrix.android.api.session.events.model.Content
@ -68,10 +69,12 @@ internal class MXMegolmEncryption(


override suspend fun encryptEventContent(eventContent: Content, override suspend fun encryptEventContent(eventContent: Content,
eventType: String, eventType: String,
userIds: List<String>): Content { userIds: List<String>): Try<Content> {
val devices = getDevicesInRoom(userIds) return getDevicesInRoom(userIds)
val outboundSession = ensureOutboundSession(devices) .flatMap { ensureOutboundSession(it) }
return encryptContent(outboundSession, eventType, eventContent) .flatMap {
encryptContent(it, eventType, eventContent)
}
} }


/** /**
@ -98,7 +101,7 @@ internal class MXMegolmEncryption(
* *
* @param devicesInRoom the devices list * @param devicesInRoom the devices list
*/ */
private suspend fun ensureOutboundSession(devicesInRoom: MXUsersDevicesMap<MXDeviceInfo>): MXOutboundSessionInfo { private suspend fun ensureOutboundSession(devicesInRoom: MXUsersDevicesMap<MXDeviceInfo>): Try<MXOutboundSessionInfo> {
var session = outboundSession var session = outboundSession
if (session == null if (session == null
// Need to make a brand new session? // Need to make a brand new session?
@ -123,8 +126,7 @@ internal class MXMegolmEncryption(
} }
} }
} }
shareKey(safeSession, shareMap) return shareKey(safeSession, shareMap).map { safeSession!! }
return safeSession
} }


/** /**
@ -134,11 +136,11 @@ internal class MXMegolmEncryption(
* @param devicesByUsers the devices map * @param devicesByUsers the devices map
*/ */
private suspend fun shareKey(session: MXOutboundSessionInfo, private suspend fun shareKey(session: MXOutboundSessionInfo,
devicesByUsers: Map<String, List<MXDeviceInfo>>) { devicesByUsers: Map<String, List<MXDeviceInfo>>): Try<Unit> {
// nothing to send, the task is done // nothing to send, the task is done
if (devicesByUsers.isEmpty()) { if (devicesByUsers.isEmpty()) {
Timber.v("## shareKey() : nothing more to do") Timber.v("## shareKey() : nothing more to do")
return return Try.just(Unit)
} }
// reduce the map size to avoid request timeout when there are too many devices (Users size * devices per user) // reduce the map size to avoid request timeout when there are too many devices (Users size * devices per user)
val subMap = HashMap<String, List<MXDeviceInfo>>() val subMap = HashMap<String, List<MXDeviceInfo>>()
@ -155,9 +157,11 @@ internal class MXMegolmEncryption(
} }
} }
Timber.v("## shareKey() ; userId $userIds") Timber.v("## shareKey() ; userId $userIds")
shareUserDevicesKey(session, subMap) return shareUserDevicesKey(session, subMap)
val remainingDevices = devicesByUsers.filterKeys { userIds.contains(it).not() } .flatMap {
shareKey(session, remainingDevices) val remainingDevices = devicesByUsers.filterKeys { userIds.contains(it).not() }
shareKey(session, remainingDevices)
}
} }


/** /**
@ -168,7 +172,7 @@ internal class MXMegolmEncryption(
* @param callback the asynchronous callback * @param callback the asynchronous callback
*/ */
private suspend fun shareUserDevicesKey(session: MXOutboundSessionInfo, private suspend fun shareUserDevicesKey(session: MXOutboundSessionInfo,
devicesByUser: Map<String, List<MXDeviceInfo>>) { devicesByUser: Map<String, List<MXDeviceInfo>>): Try<Unit> {
val sessionKey = olmDevice.getSessionKey(session.sessionId) val sessionKey = olmDevice.getSessionKey(session.sessionId)
val chainIndex = olmDevice.getMessageIndex(session.sessionId) val chainIndex = olmDevice.getMessageIndex(session.sessionId)


@ -186,86 +190,94 @@ internal class MXMegolmEncryption(
var t0 = System.currentTimeMillis() var t0 = System.currentTimeMillis()
Timber.v("## shareUserDevicesKey() : starts") Timber.v("## shareUserDevicesKey() : starts")


val results = ensureOlmSessionsForDevicesAction.handle(devicesByUser) return ensureOlmSessionsForDevicesAction.handle(devicesByUser)
Timber.v("## shareUserDevicesKey() : ensureOlmSessionsForDevices succeeds after " .flatMap {
+ (System.currentTimeMillis() - t0) + " ms") Timber.v("## shareUserDevicesKey() : ensureOlmSessionsForDevices succeeds after "
val contentMap = MXUsersDevicesMap<Any>() + (System.currentTimeMillis() - t0) + " ms")
var haveTargets = false val contentMap = MXUsersDevicesMap<Any>()
val userIds = results.userIds var haveTargets = false
for (userId in userIds) { val userIds = it.userIds
val devicesToShareWith = devicesByUser[userId] for (userId in userIds) {
for ((deviceID) in devicesToShareWith!!) { val devicesToShareWith = devicesByUser[userId]
val sessionResult = results.getObject(userId, deviceID) for ((deviceID) in devicesToShareWith!!) {
if (sessionResult?.sessionId == null) { val sessionResult = it.getObject(userId, deviceID)
// no session with this device, probably because there if (sessionResult?.sessionId == null) {
// were no one-time keys. // no session with this device, probably because there
// // were no one-time keys.
// we could send them a to_device message anyway, as a //
// signal that they have missed out on the key sharing // we could send them a to_device message anyway, as a
// message because of the lack of keys, but there's not // signal that they have missed out on the key sharing
// much point in that really; it will mostly serve to clog // message because of the lack of keys, but there's not
// up to_device inboxes. // much point in that really; it will mostly serve to clog
// // up to_device inboxes.
// ensureOlmSessionsForUsers has already done the logging, //
// so just skip it. // ensureOlmSessionsForUsers has already done the logging,
continue // so just skip it.
} continue
Timber.v("## shareUserDevicesKey() : Sharing keys with device $userId:$deviceID") }
//noinspection ArraysAsListWithZeroOrOneArgument,ArraysAsListWithZeroOrOneArgument Timber.v("## shareUserDevicesKey() : Sharing keys with device $userId:$deviceID")
contentMap.setObject(userId, deviceID, messageEncrypter.encryptMessage(payload, Arrays.asList(sessionResult.deviceInfo))) //noinspection ArraysAsListWithZeroOrOneArgument,ArraysAsListWithZeroOrOneArgument
haveTargets = true contentMap.setObject(userId, deviceID, messageEncrypter.encryptMessage(payload, Arrays.asList(sessionResult.deviceInfo)))
} haveTargets = true
} }
if (haveTargets) { }
t0 = System.currentTimeMillis() if (haveTargets) {
Timber.v("## shareUserDevicesKey() : has target") t0 = System.currentTimeMillis()
val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, contentMap) Timber.v("## shareUserDevicesKey() : has target")
sendToDeviceTask.execute(sendToDeviceParams) val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, contentMap)
Timber.v("## shareUserDevicesKey() : sendToDevice succeeds after " sendToDeviceTask.execute(sendToDeviceParams)
+ (System.currentTimeMillis() - t0) + " ms") .map {
Timber.v("## shareUserDevicesKey() : sendToDevice succeeds after "
+ (System.currentTimeMillis() - t0) + " ms")


// Add the devices we have shared with to session.sharedWithDevices. // Add the devices we have shared with to session.sharedWithDevices.
// we deliberately iterate over devicesByUser (ie, the devices we // we deliberately iterate over devicesByUser (ie, the devices we
// attempted to share with) rather than the contentMap (those we did // attempted to share with) rather than the contentMap (those we did
// share with), because we don't want to try to claim a one-time-key // share with), because we don't want to try to claim a one-time-key
// for dead devices on every message. // for dead devices on every message.
for (userId in devicesByUser.keys) { for (userId in devicesByUser.keys) {
val devicesToShareWith = devicesByUser[userId] val devicesToShareWith = devicesByUser[userId]
for ((deviceId) in devicesToShareWith!!) { for ((deviceId) in devicesToShareWith!!) {
session.sharedWithDevices.setObject(userId, deviceId, chainIndex) session.sharedWithDevices.setObject(userId, deviceId, chainIndex)
}
}
Unit
}
} else {
Timber.v("## shareUserDevicesKey() : no need to sharekey")
Try.just(Unit)
}
} }
}
} else {
Timber.v("## shareUserDevicesKey() : no need to sharekey")
}
} }


/** /**
* process the pending encryptions * process the pending encryptions
*/ */
private fun encryptContent(session: MXOutboundSessionInfo, eventType: String, eventContent: Content): Content { private fun encryptContent(session: MXOutboundSessionInfo, eventType: String, eventContent: Content): Try<Content> {
// Everything is in place, encrypt all pending events return Try<Content> {
val payloadJson = HashMap<String, Any>() // Everything is in place, encrypt all pending events
payloadJson["room_id"] = roomId val payloadJson = HashMap<String, Any>()
payloadJson["type"] = eventType payloadJson["room_id"] = roomId
payloadJson["content"] = eventContent payloadJson["type"] = eventType
payloadJson["content"] = eventContent


// Get canonical Json from // Get canonical Json from


val payloadString = convertToUTF8(JsonCanonicalizer.getCanonicalJson(Map::class.java, payloadJson)) val payloadString = convertToUTF8(JsonCanonicalizer.getCanonicalJson(Map::class.java, payloadJson))
val ciphertext = olmDevice.encryptGroupMessage(session.sessionId, payloadString!!) val ciphertext = olmDevice.encryptGroupMessage(session.sessionId, payloadString!!)


val map = HashMap<String, Any>() val map = HashMap<String, Any>()
map["algorithm"] = MXCRYPTO_ALGORITHM_MEGOLM map["algorithm"] = MXCRYPTO_ALGORITHM_MEGOLM
map["sender_key"] = olmDevice.deviceCurve25519Key!! map["sender_key"] = olmDevice.deviceCurve25519Key!!
map["ciphertext"] = ciphertext!! map["ciphertext"] = ciphertext!!
map["session_id"] = session.sessionId map["session_id"] = session.sessionId


// Include our device ID so that recipients can send us a // Include our device ID so that recipients can send us a
// m.new_device message if they don't have our session key. // m.new_device message if they don't have our session key.
map["device_id"] = credentials.deviceId!! map["device_id"] = credentials.deviceId!!
session.useCount++ session.useCount++
return map map
}
} }


/** /**
@ -275,47 +287,50 @@ internal class MXMegolmEncryption(
* @param userIds the user ids whose devices must be checked. * @param userIds the user ids whose devices must be checked.
* @param callback the asynchronous callback * @param callback the asynchronous callback
*/ */
private suspend fun getDevicesInRoom(userIds: List<String>): MXUsersDevicesMap<MXDeviceInfo> { private suspend fun getDevicesInRoom(userIds: List<String>): Try<MXUsersDevicesMap<MXDeviceInfo>> {
// We are happy to use a cached version here: we assume that if we already // We are happy to use a cached version here: we assume that if we already
// have a list of the user's devices, then we already share an e2e room // have a list of the user's devices, then we already share an e2e room
// with them, which means that they will have announced any new devices via // with them, which means that they will have announced any new devices via
// an m.new_device. // an m.new_device.
val keys = deviceListManager.downloadKeys(userIds, false) return deviceListManager
val encryptToVerifiedDevicesOnly = cryptoStore.getGlobalBlacklistUnverifiedDevices() .downloadKeys(userIds, false)
|| cryptoStore.getRoomsListBlacklistUnverifiedDevices().contains(roomId) .flatMap {
val encryptToVerifiedDevicesOnly = cryptoStore.getGlobalBlacklistUnverifiedDevices()
|| cryptoStore.getRoomsListBlacklistUnverifiedDevices().contains(roomId)


val devicesInRoom = MXUsersDevicesMap<MXDeviceInfo>() val devicesInRoom = MXUsersDevicesMap<MXDeviceInfo>()
val unknownDevices = MXUsersDevicesMap<MXDeviceInfo>() val unknownDevices = MXUsersDevicesMap<MXDeviceInfo>()


for (userId in keys.userIds) { for (userId in it.userIds) {
val deviceIds = keys.getUserDeviceIds(userId) ?: continue val deviceIds = it.getUserDeviceIds(userId) ?: continue
for (deviceId in deviceIds) { for (deviceId in deviceIds) {
val deviceInfo = keys.getObject(userId, deviceId) ?: continue val deviceInfo = it.getObject(userId, deviceId) ?: continue
if (warnOnUnknownDevicesRepository.warnOnUnknownDevices() && deviceInfo.isUnknown) { if (warnOnUnknownDevicesRepository.warnOnUnknownDevices() && deviceInfo.isUnknown) {
// The device is not yet known by the user // The device is not yet known by the user
unknownDevices.setObject(userId, deviceId, deviceInfo) unknownDevices.setObject(userId, deviceId, deviceInfo)
continue continue
} }
if (deviceInfo.isBlocked) { if (deviceInfo.isBlocked) {
// Remove any blocked devices // Remove any blocked devices
continue continue
} }


if (!deviceInfo.isVerified && encryptToVerifiedDevicesOnly) { if (!deviceInfo.isVerified && encryptToVerifiedDevicesOnly) {
continue continue
} }


if (TextUtils.equals(deviceInfo.identityKey(), olmDevice.deviceCurve25519Key)) { if (TextUtils.equals(deviceInfo.identityKey(), olmDevice.deviceCurve25519Key)) {
// Don't bother sending to ourself // Don't bother sending to ourself
continue continue
}
devicesInRoom.setObject(userId, deviceId, deviceInfo)
}
}
if (unknownDevices.isEmpty) {
Try.just(devicesInRoom)
} else {
Try.Failure(MXCryptoError.UnknownDevice(unknownDevices))
}
} }
devicesInRoom.setObject(userId, deviceId, deviceInfo)
}
}
if (unknownDevices.isEmpty) {
return devicesInRoom
} else {
throw MXCryptoError.UnknownDevice(unknownDevices)
}
} }
} }

View File

@ -17,6 +17,7 @@


package im.vector.matrix.android.internal.crypto.algorithms.olm package im.vector.matrix.android.internal.crypto.algorithms.olm


import arrow.core.Try
import im.vector.matrix.android.api.auth.data.Credentials import im.vector.matrix.android.api.auth.data.Credentials
import im.vector.matrix.android.api.session.crypto.MXCryptoError import im.vector.matrix.android.api.session.crypto.MXCryptoError
import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.events.model.Event
@ -31,6 +32,7 @@ import im.vector.matrix.android.internal.crypto.model.event.OlmPayloadContent
import im.vector.matrix.android.internal.di.MoshiProvider import im.vector.matrix.android.internal.di.MoshiProvider
import im.vector.matrix.android.internal.util.convertFromUTF8 import im.vector.matrix.android.internal.util.convertFromUTF8
import timber.log.Timber import timber.log.Timber
import java.util.*


internal class MXOlmDecryption( internal class MXOlmDecryption(
// The olm device interface // The olm device interface
@ -39,28 +41,29 @@ internal class MXOlmDecryption(
private val credentials: Credentials) private val credentials: Credentials)
: IMXDecrypting { : IMXDecrypting {


override suspend fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult { override suspend fun decryptEvent(event: Event, timeline: String): Try<MXEventDecryptionResult> {
val olmEventContent = event.content.toModel<OlmEventContent>() ?: run { val olmEventContent = event.content.toModel<OlmEventContent>() ?: run {
Timber.e("## decryptEvent() : bad event format") Timber.e("## decryptEvent() : bad event format")
throw MXCryptoError.Base(MXCryptoError.ErrorType.BAD_EVENT_FORMAT, return Try.Failure(MXCryptoError.Base(MXCryptoError.ErrorType.BAD_EVENT_FORMAT,
MXCryptoError.BAD_EVENT_FORMAT_TEXT_REASON) MXCryptoError.BAD_EVENT_FORMAT_TEXT_REASON))
} }


val cipherText = olmEventContent.ciphertext ?: run { val cipherText = olmEventContent.ciphertext ?: run {
Timber.e("## decryptEvent() : missing cipher text") Timber.e("## decryptEvent() : missing cipher text")
throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_CIPHER_TEXT, return Try.Failure(MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_CIPHER_TEXT,
MXCryptoError.MISSING_CIPHER_TEXT_REASON) MXCryptoError.MISSING_CIPHER_TEXT_REASON))
} }


val senderKey = olmEventContent.senderKey ?: run { val senderKey = olmEventContent.senderKey ?: run {
Timber.e("## decryptEvent() : missing sender key") Timber.e("## decryptEvent() : missing sender key")
throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_SENDER_KEY, return Try.Failure(MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_SENDER_KEY,
MXCryptoError.MISSING_SENDER_KEY_TEXT_REASON) MXCryptoError.MISSING_SENDER_KEY_TEXT_REASON))
} }


val messageAny = cipherText[olmDevice.deviceCurve25519Key] ?: run { val messageAny = cipherText[olmDevice.deviceCurve25519Key] ?: run {
Timber.e("## decryptEvent() : our device ${olmDevice.deviceCurve25519Key} is not included in recipients") Timber.e("## decryptEvent() : our device ${olmDevice.deviceCurve25519Key} is not included in recipients")
throw MXCryptoError.Base(MXCryptoError.ErrorType.NOT_INCLUDE_IN_RECIPIENTS, MXCryptoError.NOT_INCLUDED_IN_RECIPIENT_REASON) return Try.Failure(MXCryptoError.Base(MXCryptoError.ErrorType.NOT_INCLUDE_IN_RECIPIENTS,
MXCryptoError.NOT_INCLUDED_IN_RECIPIENT_REASON))
} }


// The message for myUser // The message for myUser
@ -70,12 +73,14 @@ internal class MXOlmDecryption(


if (decryptedPayload == null) { if (decryptedPayload == null) {
Timber.e("## decryptEvent() Failed to decrypt Olm event (id= ${event.eventId} from $senderKey") Timber.e("## decryptEvent() Failed to decrypt Olm event (id= ${event.eventId} from $senderKey")
throw MXCryptoError.Base(MXCryptoError.ErrorType.BAD_ENCRYPTED_MESSAGE, MXCryptoError.BAD_ENCRYPTED_MESSAGE_REASON) return Try.Failure(MXCryptoError.Base(MXCryptoError.ErrorType.BAD_ENCRYPTED_MESSAGE,
MXCryptoError.BAD_ENCRYPTED_MESSAGE_REASON))
} }
val payloadString = convertFromUTF8(decryptedPayload) val payloadString = convertFromUTF8(decryptedPayload)
if (payloadString == null) { if (payloadString == null) {
Timber.e("## decryptEvent() Failed to decrypt Olm event (id= ${event.eventId} from $senderKey") Timber.e("## decryptEvent() Failed to decrypt Olm event (id= ${event.eventId} from $senderKey")
throw MXCryptoError.Base(MXCryptoError.ErrorType.BAD_ENCRYPTED_MESSAGE, MXCryptoError.BAD_ENCRYPTED_MESSAGE_REASON) return Try.Failure(MXCryptoError.Base(MXCryptoError.ErrorType.BAD_ENCRYPTED_MESSAGE,
MXCryptoError.BAD_ENCRYPTED_MESSAGE_REASON))
} }


val adapter = MoshiProvider.providesMoshi().adapter<JsonDict>(JSON_DICT_PARAMETERIZED_TYPE) val adapter = MoshiProvider.providesMoshi().adapter<JsonDict>(JSON_DICT_PARAMETERIZED_TYPE)
@ -83,70 +88,73 @@ internal class MXOlmDecryption(


if (payload == null) { if (payload == null) {
Timber.e("## decryptEvent failed : null payload") Timber.e("## decryptEvent failed : null payload")
throw MXCryptoError.Base(MXCryptoError.ErrorType.UNABLE_TO_DECRYPT, MXCryptoError.MISSING_CIPHER_TEXT_REASON) return Try.Failure(MXCryptoError.Base(MXCryptoError.ErrorType.UNABLE_TO_DECRYPT,
MXCryptoError.MISSING_CIPHER_TEXT_REASON))
} }


val olmPayloadContent = OlmPayloadContent.fromJsonString(payloadString) ?: run { val olmPayloadContent = OlmPayloadContent.fromJsonString(payloadString) ?: run {
Timber.e("## decryptEvent() : bad olmPayloadContent format") Timber.e("## decryptEvent() : bad olmPayloadContent format")
throw MXCryptoError.Base(MXCryptoError.ErrorType.BAD_DECRYPTED_FORMAT, MXCryptoError.BAD_DECRYPTED_FORMAT_TEXT_REASON) return Try.Failure(MXCryptoError.Base(MXCryptoError.ErrorType.BAD_DECRYPTED_FORMAT,
MXCryptoError.BAD_DECRYPTED_FORMAT_TEXT_REASON))
} }


if (olmPayloadContent.recipient.isNullOrBlank()) { if (olmPayloadContent.recipient.isNullOrBlank()) {
val reason = String.format(MXCryptoError.ERROR_MISSING_PROPERTY_REASON, "recipient") val reason = String.format(MXCryptoError.ERROR_MISSING_PROPERTY_REASON, "recipient")
Timber.e("## decryptEvent() : $reason") Timber.e("## decryptEvent() : $reason")
throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_PROPERTY, reason) return Try.Failure(MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_PROPERTY,
reason))
} }


if (olmPayloadContent.recipient != credentials.userId) { if (olmPayloadContent.recipient != credentials.userId) {
Timber.e("## decryptEvent() : Event ${event.eventId}:" + Timber.e("## decryptEvent() : Event ${event.eventId}:" +
" Intended recipient ${olmPayloadContent.recipient} does not match our id ${credentials.userId}") " Intended recipient ${olmPayloadContent.recipient} does not match our id ${credentials.userId}")
throw MXCryptoError.Base(MXCryptoError.ErrorType.BAD_RECIPIENT, return Try.Failure(MXCryptoError.Base(MXCryptoError.ErrorType.BAD_RECIPIENT,
String.format(MXCryptoError.BAD_RECIPIENT_REASON, olmPayloadContent.recipient)) String.format(MXCryptoError.BAD_RECIPIENT_REASON, olmPayloadContent.recipient)))
} }


val recipientKeys = olmPayloadContent.recipient_keys ?: run { val recipientKeys = olmPayloadContent.recipient_keys ?: run {
Timber.e("## decryptEvent() : Olm event (id=${event.eventId}) contains no 'recipient_keys' property; cannot prevent unknown-key attack") Timber.e("## decryptEvent() : Olm event (id=${event.eventId}) contains no 'recipient_keys' property; cannot prevent unknown-key attack")
throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_PROPERTY, return Try.Failure(MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_PROPERTY,
String.format(MXCryptoError.ERROR_MISSING_PROPERTY_REASON, "recipient_keys")) String.format(MXCryptoError.ERROR_MISSING_PROPERTY_REASON, "recipient_keys")))
} }


val ed25519 = recipientKeys["ed25519"] val ed25519 = recipientKeys["ed25519"]


if (ed25519 != olmDevice.deviceEd25519Key) { if (ed25519 != olmDevice.deviceEd25519Key) {
Timber.e("## decryptEvent() : Event ${event.eventId}: Intended recipient ed25519 key $ed25519 did not match ours") Timber.e("## decryptEvent() : Event ${event.eventId}: Intended recipient ed25519 key $ed25519 did not match ours")
throw MXCryptoError.Base(MXCryptoError.ErrorType.BAD_RECIPIENT_KEY, return Try.Failure(MXCryptoError.Base(MXCryptoError.ErrorType.BAD_RECIPIENT_KEY,
MXCryptoError.BAD_RECIPIENT_KEY_REASON) MXCryptoError.BAD_RECIPIENT_KEY_REASON))
} }


if (olmPayloadContent.sender.isNullOrBlank()) { if (olmPayloadContent.sender.isNullOrBlank()) {
Timber.e("## decryptEvent() : Olm event (id=${event.eventId}) contains no 'sender' property; cannot prevent unknown-key attack") Timber.e("## decryptEvent() : Olm event (id=${event.eventId}) contains no 'sender' property; cannot prevent unknown-key attack")
throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_PROPERTY, return Try.Failure(MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_PROPERTY,
String.format(MXCryptoError.ERROR_MISSING_PROPERTY_REASON, "sender")) String.format(MXCryptoError.ERROR_MISSING_PROPERTY_REASON, "sender")))
} }


if (olmPayloadContent.sender != event.senderId) { if (olmPayloadContent.sender != event.senderId) {
Timber.e("Event ${event.eventId}: original sender ${olmPayloadContent.sender} does not match reported sender ${event.senderId}") Timber.e("Event ${event.eventId}: original sender ${olmPayloadContent.sender} does not match reported sender ${event.senderId}")
throw MXCryptoError.Base(MXCryptoError.ErrorType.FORWARDED_MESSAGE, return Try.Failure(MXCryptoError.Base(MXCryptoError.ErrorType.FORWARDED_MESSAGE,
String.format(MXCryptoError.FORWARDED_MESSAGE_REASON, olmPayloadContent.sender)) String.format(MXCryptoError.FORWARDED_MESSAGE_REASON, olmPayloadContent.sender)))
} }


if (olmPayloadContent.room_id != event.roomId) { if (olmPayloadContent.room_id != event.roomId) {
Timber.e("## decryptEvent() : Event ${event.eventId}: original room ${olmPayloadContent.room_id} does not match reported room ${event.roomId}") Timber.e("## decryptEvent() : Event ${event.eventId}: original room ${olmPayloadContent.room_id} does not match reported room ${event.roomId}")
throw MXCryptoError.Base(MXCryptoError.ErrorType.BAD_ROOM, return Try.Failure(MXCryptoError.Base(MXCryptoError.ErrorType.BAD_ROOM,
String.format(MXCryptoError.BAD_ROOM_REASON, olmPayloadContent.room_id)) String.format(MXCryptoError.BAD_ROOM_REASON, olmPayloadContent.room_id)))
} }


val keys = olmPayloadContent.keys ?: run { val keys = olmPayloadContent.keys ?: run {
Timber.e("## decryptEvent failed : null keys") Timber.e("## decryptEvent failed : null keys")
throw MXCryptoError.Base(MXCryptoError.ErrorType.UNABLE_TO_DECRYPT, return Try.Failure(MXCryptoError.Base(MXCryptoError.ErrorType.UNABLE_TO_DECRYPT,
MXCryptoError.MISSING_CIPHER_TEXT_REASON) MXCryptoError.MISSING_CIPHER_TEXT_REASON))
} }


return MXEventDecryptionResult( return Try.just(MXEventDecryptionResult(
clearEvent = payload, clearEvent = payload,
senderCurve25519Key = senderKey, senderCurve25519Key = senderKey,
claimedEd25519Key = keys["ed25519"] claimedEd25519Key = keys["ed25519"]
) ))
} }


/** /**
@ -157,14 +165,33 @@ internal class MXOlmDecryption(
* @return payload, if decrypted successfully. * @return payload, if decrypted successfully.
*/ */
private fun decryptMessage(message: JsonDict, theirDeviceIdentityKey: String): String? { private fun decryptMessage(message: JsonDict, theirDeviceIdentityKey: String): String? {
val sessionIds = olmDevice.getSessionIds(theirDeviceIdentityKey) ?: emptySet() val sessionIdsSet = olmDevice.getSessionIds(theirDeviceIdentityKey)


val messageBody = message["body"] as? String ?: return null val sessionIds: List<String>
val messageType = when (val typeAsVoid = message["type"]) {
is Double -> typeAsVoid.toInt() if (null == sessionIdsSet) {
is Int -> typeAsVoid sessionIds = ArrayList()
is Long -> typeAsVoid.toInt() } else {
else -> return null sessionIds = ArrayList(sessionIdsSet)
}

val messageBody = message["body"] as? String
var messageType: Int? = null

val typeAsVoid = message["type"]

if (null != typeAsVoid) {
if (typeAsVoid is Double) {
messageType = typeAsVoid.toInt()
} else if (typeAsVoid is Int) {
messageType = typeAsVoid
} else if (typeAsVoid is Long) {
messageType = typeAsVoid.toInt()
}
}

if (null == messageBody || null == messageType) {
return null
} }


// Try each session in turn // Try each session in turn

View File

@ -18,6 +18,7 @@ package im.vector.matrix.android.internal.crypto.algorithms.olm


import im.vector.matrix.android.api.auth.data.Credentials import im.vector.matrix.android.api.auth.data.Credentials
import im.vector.matrix.android.internal.crypto.MXOlmDevice import im.vector.matrix.android.internal.crypto.MXOlmDevice
import im.vector.matrix.android.internal.session.SessionScope
import javax.inject.Inject import javax.inject.Inject


internal class MXOlmDecryptionFactory @Inject constructor(private val olmDevice: MXOlmDevice, internal class MXOlmDecryptionFactory @Inject constructor(private val olmDevice: MXOlmDevice,

View File

@ -19,6 +19,7 @@
package im.vector.matrix.android.internal.crypto.algorithms.olm package im.vector.matrix.android.internal.crypto.algorithms.olm


import android.text.TextUtils import android.text.TextUtils
import arrow.core.Try
import im.vector.matrix.android.api.session.events.model.Content import im.vector.matrix.android.api.session.events.model.Content
import im.vector.matrix.android.api.session.events.model.toContent import im.vector.matrix.android.api.session.events.model.toContent
import im.vector.matrix.android.internal.crypto.DeviceListManager import im.vector.matrix.android.internal.crypto.DeviceListManager
@ -39,35 +40,37 @@ internal class MXOlmEncryption(
private val ensureOlmSessionsForUsersAction: EnsureOlmSessionsForUsersAction) private val ensureOlmSessionsForUsersAction: EnsureOlmSessionsForUsersAction)
: IMXEncrypting { : IMXEncrypting {


override suspend fun encryptEventContent(eventContent: Content, eventType: String, userIds: List<String>): Content { override suspend fun encryptEventContent(eventContent: Content, eventType: String, userIds: List<String>): Try<Content> {
// pick the list of recipients based on the membership list. // pick the list of recipients based on the membership list.
// //
// TODO: there is a race condition here! What if a new user turns up // TODO: there is a race condition here! What if a new user turns up
ensureSession(userIds) return ensureSession(userIds)
val deviceInfos = ArrayList<MXDeviceInfo>() .map {
for (userId in userIds) { val deviceInfos = ArrayList<MXDeviceInfo>()
val devices = cryptoStore.getUserDevices(userId)?.values ?: emptyList() for (userId in userIds) {
for (device in devices) { val devices = cryptoStore.getUserDevices(userId)?.values ?: emptyList()
val key = device.identityKey() for (device in devices) {
if (TextUtils.equals(key, olmDevice.deviceCurve25519Key)) { val key = device.identityKey()
// Don't bother setting up session to ourself if (TextUtils.equals(key, olmDevice.deviceCurve25519Key)) {
continue // Don't bother setting up session to ourself
} continue
if (device.isBlocked) { }
// Don't bother setting up sessions with blocked users if (device.isBlocked) {
continue // Don't bother setting up sessions with blocked users
} continue
deviceInfos.add(device) }
} deviceInfos.add(device)
} }
}


val messageMap = HashMap<String, Any>() val messageMap = HashMap<String, Any>()
messageMap["room_id"] = roomId messageMap["room_id"] = roomId
messageMap["type"] = eventType messageMap["type"] = eventType
messageMap["content"] = eventContent messageMap["content"] = eventContent


messageEncrypter.encryptMessage(messageMap, deviceInfos) messageEncrypter.encryptMessage(messageMap, deviceInfos)
return messageMap.toContent()!! messageMap.toContent()!!
}
} }




@ -75,9 +78,13 @@ internal class MXOlmEncryption(
* Ensure that the session * Ensure that the session
* *
* @param users the user ids list * @param users the user ids list
* @param callback the asynchronous callback
*/ */
private suspend fun ensureSession(users: List<String>) { private suspend fun ensureSession(users: List<String>): Try<Unit> {
deviceListManager.downloadKeys(users, false) return deviceListManager
ensureOlmSessionsForUsersAction.handle(users) .downloadKeys(users, false)
.flatMap { ensureOlmSessionsForUsersAction.handle(users) }
.map { Unit }

} }
} }

View File

@ -21,6 +21,7 @@ import im.vector.matrix.android.internal.crypto.MXOlmDevice
import im.vector.matrix.android.internal.crypto.actions.EnsureOlmSessionsForUsersAction import im.vector.matrix.android.internal.crypto.actions.EnsureOlmSessionsForUsersAction
import im.vector.matrix.android.internal.crypto.actions.MessageEncrypter import im.vector.matrix.android.internal.crypto.actions.MessageEncrypter
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
import im.vector.matrix.android.internal.session.SessionScope
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
import javax.inject.Inject import javax.inject.Inject



View File

@ -18,6 +18,8 @@ package im.vector.matrix.android.internal.crypto.api




import im.vector.matrix.android.internal.crypto.model.rest.* import im.vector.matrix.android.internal.crypto.model.rest.*
import im.vector.matrix.android.internal.crypto.model.rest.KeysUploadBody
import im.vector.matrix.android.internal.crypto.model.rest.SendToDeviceBody
import im.vector.matrix.android.internal.network.NetworkConstants import im.vector.matrix.android.internal.network.NetworkConstants
import retrofit2.Call import retrofit2.Call
import retrofit2.http.* import retrofit2.http.*

View File

@ -26,6 +26,7 @@ import java.io.ByteArrayOutputStream
import java.io.InputStream import java.io.InputStream
import java.security.MessageDigest import java.security.MessageDigest
import java.security.SecureRandom import java.security.SecureRandom
import java.util.*
import javax.crypto.Cipher import javax.crypto.Cipher
import javax.crypto.spec.IvParameterSpec import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.SecretKeySpec import javax.crypto.spec.SecretKeySpec
@ -58,7 +59,8 @@ object MXEncryptedAttachments {
// Half of the IV is random, the lower order bits are zeroed // Half of the IV is random, the lower order bits are zeroed
// such that the counter never wraps. // such that the counter never wraps.
// See https://github.com/matrix-org/matrix-ios-kit/blob/3dc0d8e46b4deb6669ed44f72ad79be56471354c/MatrixKit/Models/Room/MXEncryptedAttachments.m#L75 // See https://github.com/matrix-org/matrix-ios-kit/blob/3dc0d8e46b4deb6669ed44f72ad79be56471354c/MatrixKit/Models/Room/MXEncryptedAttachments.m#L75
val initVectorBytes = ByteArray(16) { 0.toByte() } val initVectorBytes = ByteArray(16)
Arrays.fill(initVectorBytes, 0.toByte())


val ivRandomPart = ByteArray(8) val ivRandomPart = ByteArray(8)
secureRandom.nextBytes(ivRandomPart) secureRandom.nextBytes(ivRandomPart)
@ -113,7 +115,7 @@ object MXEncryptedAttachments {
encryptedByteArray = outStream.toByteArray() encryptedByteArray = outStream.toByteArray()
) )


Timber.v("Encrypt in ${System.currentTimeMillis() - t0} ms") Timber.v("Encrypt in " + (System.currentTimeMillis() - t0) + " ms")
return Try.just(result) return Try.just(result)
} catch (oom: OutOfMemoryError) { } catch (oom: OutOfMemoryError) {
Timber.e(oom, "## encryptAttachment failed") Timber.e(oom, "## encryptAttachment failed")
@ -204,13 +206,13 @@ object MXEncryptedAttachments {
val decryptedStream = ByteArrayInputStream(outStream.toByteArray()) val decryptedStream = ByteArrayInputStream(outStream.toByteArray())
outStream.close() outStream.close()


Timber.v("Decrypt in ${System.currentTimeMillis() - t0} ms") Timber.v("Decrypt in " + (System.currentTimeMillis() - t0) + " ms")


return decryptedStream return decryptedStream
} catch (oom: OutOfMemoryError) { } catch (oom: OutOfMemoryError) {
Timber.e(oom, "## decryptAttachment() : failed ${oom.message}") Timber.e(oom, "## decryptAttachment() : failed " + oom.message)
} catch (e: Exception) { } catch (e: Exception) {
Timber.e(e, "## decryptAttachment() : failed ${e.message}") Timber.e(e, "## decryptAttachment() : failed " + e.message)
} }


try { try {
@ -226,20 +228,34 @@ object MXEncryptedAttachments {
* Base64 URL conversion methods * Base64 URL conversion methods
*/ */


private fun base64UrlToBase64(base64Url: String): String { private fun base64UrlToBase64(base64Url: String?): String? {
return base64Url.replace('-', '+') var result = base64Url
.replace('_', '/') if (null != result) {
result = result.replace("-".toRegex(), "+")
result = result.replace("_".toRegex(), "/")
}

return result
} }


private fun base64ToBase64Url(base64: String): String { private fun base64ToBase64Url(base64: String?): String? {
return base64.replace("\n".toRegex(), "") var result = base64
.replace("\\+".toRegex(), "-") if (null != result) {
.replace('/', '_') result = result.replace("\n".toRegex(), "")
.replace("=", "") result = result.replace("\\+".toRegex(), "-")
result = result.replace("/".toRegex(), "_")
result = result.replace("=".toRegex(), "")
}
return result
} }


private fun base64ToUnpaddedBase64(base64: String): String { private fun base64ToUnpaddedBase64(base64: String?): String? {
return base64.replace("\n".toRegex(), "") var result = base64
.replace("=", "") if (null != result) {
result = result.replace("\n".toRegex(), "")
result = result.replace("=".toRegex(), "")
}

return result
} }
} }

View File

@ -51,6 +51,7 @@ import im.vector.matrix.android.internal.crypto.store.db.model.KeysBackupDataEnt
import im.vector.matrix.android.internal.di.MoshiProvider import im.vector.matrix.android.internal.di.MoshiProvider
import im.vector.matrix.android.internal.extensions.foldToCallback import im.vector.matrix.android.internal.extensions.foldToCallback
import im.vector.matrix.android.internal.session.SessionScope import im.vector.matrix.android.internal.session.SessionScope
import im.vector.matrix.android.internal.task.*
import im.vector.matrix.android.internal.task.Task import im.vector.matrix.android.internal.task.Task
import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.internal.task.TaskThread import im.vector.matrix.android.internal.task.TaskThread
@ -66,8 +67,9 @@ import org.matrix.olm.OlmPkEncryption
import org.matrix.olm.OlmPkMessage import org.matrix.olm.OlmPkMessage
import timber.log.Timber import timber.log.Timber
import java.security.InvalidParameterException import java.security.InvalidParameterException
import java.util.*
import javax.inject.Inject import javax.inject.Inject
import kotlin.random.Random import kotlin.collections.HashMap


/** /**
* A KeysBackup class instance manage incremental backup of e2e keys (megolm keys) * A KeysBackup class instance manage incremental backup of e2e keys (megolm keys)
@ -113,6 +115,8 @@ internal class KeysBackup @Inject constructor(
// The backup key being used. // The backup key being used.
private var backupOlmPkEncryption: OlmPkEncryption? = null private var backupOlmPkEncryption: OlmPkEncryption? = null


private val random = Random()

private var backupAllGroupSessionsCallback: MatrixCallback<Unit>? = null private var backupAllGroupSessionsCallback: MatrixCallback<Unit>? = null


private var keysBackupStateListener: KeysBackupStateListener? = null private var keysBackupStateListener: KeysBackupStateListener? = null
@ -200,32 +204,31 @@ internal class KeysBackup @Inject constructor(
keysBackupStateManager.state = KeysBackupState.Enabling keysBackupStateManager.state = KeysBackupState.Enabling


createKeysBackupVersionTask createKeysBackupVersionTask
.configureWith(createKeysBackupVersionBody) { .configureWith(createKeysBackupVersionBody)
this.callback = object : MatrixCallback<KeysVersion> { .dispatchTo(object : MatrixCallback<KeysVersion> {
override fun onSuccess(info: KeysVersion) { override fun onSuccess(info: KeysVersion) {
// Reset backup markers. // Reset backup markers.
cryptoStore.resetBackupMarkers() cryptoStore.resetBackupMarkers()


val keyBackupVersion = KeysVersionResult() val keyBackupVersion = KeysVersionResult()
keyBackupVersion.algorithm = createKeysBackupVersionBody.algorithm keyBackupVersion.algorithm = createKeysBackupVersionBody.algorithm
keyBackupVersion.authData = createKeysBackupVersionBody.authData keyBackupVersion.authData = createKeysBackupVersionBody.authData
keyBackupVersion.version = info.version keyBackupVersion.version = info.version


// We can consider that the server does not have keys yet // We can consider that the server does not have keys yet
keyBackupVersion.count = 0 keyBackupVersion.count = 0
keyBackupVersion.hash = null keyBackupVersion.hash = null


enableKeysBackup(keyBackupVersion) enableKeysBackup(keyBackupVersion)


callback.onSuccess(info) callback.onSuccess(info)
}

override fun onFailure(failure: Throwable) {
keysBackupStateManager.state = KeysBackupState.Disabled
callback.onFailure(failure)
}
} }
}
override fun onFailure(failure: Throwable) {
keysBackupStateManager.state = KeysBackupState.Disabled
callback.onFailure(failure)
}
})
.executeBy(taskExecutor) .executeBy(taskExecutor)
} }


@ -240,29 +243,27 @@ internal class KeysBackup @Inject constructor(
keysBackupStateManager.state = KeysBackupState.Unknown keysBackupStateManager.state = KeysBackupState.Unknown
} }


deleteBackupTask deleteBackupTask.configureWith(DeleteBackupTask.Params(version))
.configureWith(DeleteBackupTask.Params(version)) { .dispatchTo(object : MatrixCallback<Unit> {
this.callback = object : MatrixCallback<Unit> { private fun eventuallyRestartBackup() {
private fun eventuallyRestartBackup() { // Do not stay in KeysBackupState.Unknown but check what is available on the homeserver
// Do not stay in KeysBackupState.Unknown but check what is available on the homeserver if (state == KeysBackupState.Unknown) {
if (state == KeysBackupState.Unknown) { checkAndStartKeysBackup()
checkAndStartKeysBackup()
}
}

override fun onSuccess(data: Unit) {
eventuallyRestartBackup()

uiHandler.post { callback?.onSuccess(Unit) }
}

override fun onFailure(failure: Throwable) {
eventuallyRestartBackup()

uiHandler.post { callback?.onFailure(failure) }
} }
} }
}
override fun onSuccess(data: Unit) {
eventuallyRestartBackup()

uiHandler.post { callback?.onSuccess(Unit) }
}

override fun onFailure(failure: Throwable) {
eventuallyRestartBackup()

uiHandler.post { callback?.onFailure(failure) }
}
})
.executeBy(taskExecutor) .executeBy(taskExecutor)
} }
} }
@ -354,14 +355,15 @@ internal class KeysBackup @Inject constructor(
callback: MatrixCallback<KeysBackupVersionTrust>) { callback: MatrixCallback<KeysBackupVersionTrust>) {
// TODO Validate with François that this is correct // TODO Validate with François that this is correct
object : Task<KeysVersionResult, KeysBackupVersionTrust> { object : Task<KeysVersionResult, KeysBackupVersionTrust> {
override suspend fun execute(params: KeysVersionResult): KeysBackupVersionTrust { override suspend fun execute(params: KeysVersionResult): Try<KeysBackupVersionTrust> {
return getKeysBackupTrustBg(params) return Try {
getKeysBackupTrustBg(params)
}
} }
} }
.configureWith(keysBackupVersion) { .configureWith(keysBackupVersion)
this.callback = callback .dispatchTo(callback)
this.executionThread = TaskThread.COMPUTATION .executeOn(TaskThread.COMPUTATION)
}
.executeBy(taskExecutor) .executeBy(taskExecutor)
} }


@ -452,8 +454,7 @@ internal class KeysBackup @Inject constructor(
val myUserId = credentials.userId val myUserId = credentials.userId


// Get current signatures, or create an empty set // Get current signatures, or create an empty set
val myUserSignatures = authData.signatures?.get(myUserId)?.toMutableMap() val myUserSignatures = authData.signatures?.get(myUserId)?.toMutableMap() ?: HashMap()
?: HashMap()


if (trust) { if (trust) {
// Add current device signature // Add current device signature
@ -491,28 +492,27 @@ internal class KeysBackup @Inject constructor(


// And send it to the homeserver // And send it to the homeserver
updateKeysBackupVersionTask updateKeysBackupVersionTask
.configureWith(UpdateKeysBackupVersionTask.Params(keysBackupVersion.version!!, updateKeysBackupVersionBody)) { .configureWith(UpdateKeysBackupVersionTask.Params(keysBackupVersion.version!!, updateKeysBackupVersionBody))
this.callback = object : MatrixCallback<Unit> { .dispatchTo(object : MatrixCallback<Unit> {
override fun onSuccess(data: Unit) { override fun onSuccess(data: Unit) {
// Relaunch the state machine on this updated backup version // Relaunch the state machine on this updated backup version
val newKeysBackupVersion = KeysVersionResult() val newKeysBackupVersion = KeysVersionResult()


newKeysBackupVersion.version = keysBackupVersion.version newKeysBackupVersion.version = keysBackupVersion.version
newKeysBackupVersion.algorithm = keysBackupVersion.algorithm newKeysBackupVersion.algorithm = keysBackupVersion.algorithm
newKeysBackupVersion.count = keysBackupVersion.count newKeysBackupVersion.count = keysBackupVersion.count
newKeysBackupVersion.hash = keysBackupVersion.hash newKeysBackupVersion.hash = keysBackupVersion.hash
newKeysBackupVersion.authData = updateKeysBackupVersionBody.authData newKeysBackupVersion.authData = updateKeysBackupVersionBody.authData


checkAndStartWithKeysBackupVersion(newKeysBackupVersion) checkAndStartWithKeysBackupVersion(newKeysBackupVersion)


callback.onSuccess(data) callback.onSuccess(data)
}

override fun onFailure(failure: Throwable) {
callback.onFailure(failure)
}
} }
}
override fun onFailure(failure: Throwable) {
callback.onFailure(failure)
}
})
.executeBy(taskExecutor) .executeBy(taskExecutor)
} }
} }
@ -758,52 +758,49 @@ internal class KeysBackup @Inject constructor(
if (roomId != null && sessionId != null) { if (roomId != null && sessionId != null) {
// Get key for the room and for the session // Get key for the room and for the session
getRoomSessionDataTask getRoomSessionDataTask
.configureWith(GetRoomSessionDataTask.Params(roomId, sessionId, version)) { .configureWith(GetRoomSessionDataTask.Params(roomId, sessionId, version))
this.callback = object : MatrixCallback<KeyBackupData> { .dispatchTo(object : MatrixCallback<KeyBackupData> {
override fun onSuccess(data: KeyBackupData) { override fun onSuccess(data: KeyBackupData) {
// Convert to KeysBackupData // Convert to KeysBackupData
val keysBackupData = KeysBackupData() val keysBackupData = KeysBackupData()
keysBackupData.roomIdToRoomKeysBackupData = HashMap() keysBackupData.roomIdToRoomKeysBackupData = HashMap()
val roomKeysBackupData = RoomKeysBackupData() val roomKeysBackupData = RoomKeysBackupData()
roomKeysBackupData.sessionIdToKeyBackupData = HashMap() roomKeysBackupData.sessionIdToKeyBackupData = HashMap()
roomKeysBackupData.sessionIdToKeyBackupData[sessionId] = data roomKeysBackupData.sessionIdToKeyBackupData[sessionId] = data
keysBackupData.roomIdToRoomKeysBackupData[roomId] = roomKeysBackupData keysBackupData.roomIdToRoomKeysBackupData[roomId] = roomKeysBackupData


callback.onSuccess(keysBackupData) callback.onSuccess(keysBackupData)
}

override fun onFailure(failure: Throwable) {
callback.onFailure(failure)
}
} }
}
override fun onFailure(failure: Throwable) {
callback.onFailure(failure)
}
})
.executeBy(taskExecutor) .executeBy(taskExecutor)
} else if (roomId != null) { } else if (roomId != null) {
// Get all keys for the room // Get all keys for the room
getRoomSessionsDataTask getRoomSessionsDataTask
.configureWith(GetRoomSessionsDataTask.Params(roomId, version)) { .configureWith(GetRoomSessionsDataTask.Params(roomId, version))
this.callback = object : MatrixCallback<RoomKeysBackupData> { .dispatchTo(object : MatrixCallback<RoomKeysBackupData> {
override fun onSuccess(data: RoomKeysBackupData) { override fun onSuccess(data: RoomKeysBackupData) {
// Convert to KeysBackupData // Convert to KeysBackupData
val keysBackupData = KeysBackupData() val keysBackupData = KeysBackupData()
keysBackupData.roomIdToRoomKeysBackupData = HashMap() keysBackupData.roomIdToRoomKeysBackupData = HashMap()
keysBackupData.roomIdToRoomKeysBackupData[roomId] = data keysBackupData.roomIdToRoomKeysBackupData[roomId] = data


callback.onSuccess(keysBackupData) callback.onSuccess(keysBackupData)
}

override fun onFailure(failure: Throwable) {
callback.onFailure(failure)
}
} }
}
override fun onFailure(failure: Throwable) {
callback.onFailure(failure)
}
})
.executeBy(taskExecutor) .executeBy(taskExecutor)
} else { } else {
// Get all keys // Get all keys
getSessionsDataTask getSessionsDataTask
.configureWith(GetSessionsDataTask.Params(version)) { .configureWith(GetSessionsDataTask.Params(version))
this.callback = callback .dispatchTo(callback)
}
.executeBy(taskExecutor) .executeBy(taskExecutor)
} }
} }
@ -845,7 +842,7 @@ internal class KeysBackup @Inject constructor(
// Wait between 0 and 10 seconds, to avoid backup requests from // Wait between 0 and 10 seconds, to avoid backup requests from
// different clients hitting the server all at the same time when a // different clients hitting the server all at the same time when a
// new key is sent // new key is sent
val delayInMs = Random.nextLong(KEY_BACKUP_WAITING_TIME_TO_SEND_KEY_BACKUP_MILLIS) val delayInMs = random.nextInt(KEY_BACKUP_WAITING_TIME_TO_SEND_KEY_BACKUP_MILLIS).toLong()


uiHandler.postDelayed({ backupKeys() }, delayInMs) uiHandler.postDelayed({ backupKeys() }, delayInMs)
} }
@ -858,47 +855,45 @@ internal class KeysBackup @Inject constructor(
override fun getVersion(version: String, override fun getVersion(version: String,
callback: MatrixCallback<KeysVersionResult?>) { callback: MatrixCallback<KeysVersionResult?>) {
getKeysBackupVersionTask getKeysBackupVersionTask
.configureWith(version) { .configureWith(version)
this.callback = object : MatrixCallback<KeysVersionResult> { .dispatchTo(object : MatrixCallback<KeysVersionResult> {
override fun onSuccess(data: KeysVersionResult) { override fun onSuccess(data: KeysVersionResult) {
callback.onSuccess(data) callback.onSuccess(data)
} }


override fun onFailure(failure: Throwable) { override fun onFailure(failure: Throwable) {
if (failure is Failure.ServerError if (failure is Failure.ServerError
&& failure.error.code == MatrixError.NOT_FOUND) { && failure.error.code == MatrixError.NOT_FOUND) {
// Workaround because the homeserver currently returns M_NOT_FOUND when there is no key backup // Workaround because the homeserver currently returns M_NOT_FOUND when there is no key backup
callback.onSuccess(null) callback.onSuccess(null)
} else { } else {
// Transmit the error // Transmit the error
callback.onFailure(failure) callback.onFailure(failure)
}
} }
} }
} })
.executeBy(taskExecutor) .executeBy(taskExecutor)
} }


override fun getCurrentVersion(callback: MatrixCallback<KeysVersionResult?>) { override fun getCurrentVersion(callback: MatrixCallback<KeysVersionResult?>) {
getKeysBackupLastVersionTask getKeysBackupLastVersionTask
.configureWith { .toConfigurableTask()
this.callback = object : MatrixCallback<KeysVersionResult> { .dispatchTo(object : MatrixCallback<KeysVersionResult> {
override fun onSuccess(data: KeysVersionResult) { override fun onSuccess(data: KeysVersionResult) {
callback.onSuccess(data) callback.onSuccess(data)
} }


override fun onFailure(failure: Throwable) { override fun onFailure(failure: Throwable) {
if (failure is Failure.ServerError if (failure is Failure.ServerError
&& failure.error.code == MatrixError.NOT_FOUND) { && failure.error.code == MatrixError.NOT_FOUND) {
// Workaround because the homeserver currently returns M_NOT_FOUND when there is no key backup // Workaround because the homeserver currently returns M_NOT_FOUND when there is no key backup
callback.onSuccess(null) callback.onSuccess(null)
} else { } else {
// Transmit the error // Transmit the error
callback.onFailure(failure) callback.onFailure(failure)
}
} }
} }
} })
.executeBy(taskExecutor) .executeBy(taskExecutor)
} }


@ -1241,72 +1236,69 @@ internal class KeysBackup @Inject constructor(


Timber.v("backupKeys: 4 - Sending request") Timber.v("backupKeys: 4 - Sending request")


val sendingRequestCallback = object : MatrixCallback<BackupKeysResult> {
override fun onSuccess(data: BackupKeysResult) {
uiHandler.post {
Timber.v("backupKeys: 5a - Request complete")

// Mark keys as backed up
cryptoStore.markBackupDoneForInboundGroupSessions(olmInboundGroupSessionWrappers)

if (olmInboundGroupSessionWrappers.size < KEY_BACKUP_SEND_KEYS_MAX_COUNT) {
Timber.v("backupKeys: All keys have been backed up")
onServerDataRetrieved(data.count, data.hash)

// Note: Changing state will trigger the call to backupAllGroupSessionsCallback.onSuccess()
keysBackupStateManager.state = KeysBackupState.ReadyToBackUp
} else {
Timber.v("backupKeys: Continue to back up keys")
keysBackupStateManager.state = KeysBackupState.WillBackUp

backupKeys()
}
}
}

override fun onFailure(failure: Throwable) {
if (failure is Failure.ServerError) {
uiHandler.post {
Timber.e(failure, "backupKeys: backupKeys failed.")

when (failure.error.code) {
MatrixError.NOT_FOUND,
MatrixError.WRONG_ROOM_KEYS_VERSION -> {
// Backup has been deleted on the server, or we are not using the last backup version
keysBackupStateManager.state = KeysBackupState.WrongBackUpVersion
backupAllGroupSessionsCallback?.onFailure(failure)
resetBackupAllGroupSessionsListeners()
resetKeysBackupData()
keysBackupVersion = null

// Do not stay in KeysBackupState.WrongBackUpVersion but check what is available on the homeserver
checkAndStartKeysBackup()
}
else ->
// Come back to the ready state so that we will retry on the next received key
keysBackupStateManager.state = KeysBackupState.ReadyToBackUp
}
}
} else {
uiHandler.post {
backupAllGroupSessionsCallback?.onFailure(failure)
resetBackupAllGroupSessionsListeners()

Timber.e("backupKeys: backupKeys failed.")

// Retry a bit later
keysBackupStateManager.state = KeysBackupState.ReadyToBackUp
maybeBackupKeys()
}
}
}
}

// Make the request // Make the request
storeSessionDataTask storeSessionDataTask
.configureWith(StoreSessionsDataTask.Params(keysBackupVersion!!.version!!, keysBackupData)) { .configureWith(StoreSessionsDataTask.Params(keysBackupVersion!!.version!!, keysBackupData))
this.callback = sendingRequestCallback .dispatchTo(object : MatrixCallback<BackupKeysResult> {
} override fun onSuccess(data: BackupKeysResult) {
uiHandler.post {
Timber.v("backupKeys: 5a - Request complete")

// Mark keys as backed up
cryptoStore.markBackupDoneForInboundGroupSessions(olmInboundGroupSessionWrappers)

if (olmInboundGroupSessionWrappers.size < KEY_BACKUP_SEND_KEYS_MAX_COUNT) {
Timber.v("backupKeys: All keys have been backed up")
onServerDataRetrieved(data.count, data.hash)

// Note: Changing state will trigger the call to backupAllGroupSessionsCallback.onSuccess()
keysBackupStateManager.state = KeysBackupState.ReadyToBackUp
} else {
Timber.v("backupKeys: Continue to back up keys")
keysBackupStateManager.state = KeysBackupState.WillBackUp

backupKeys()
}
}
}

override fun onFailure(failure: Throwable) {
if (failure is Failure.ServerError) {
uiHandler.post {
Timber.e(failure, "backupKeys: backupKeys failed.")

when (failure.error.code) {
MatrixError.NOT_FOUND,
MatrixError.WRONG_ROOM_KEYS_VERSION -> {
// Backup has been deleted on the server, or we are not using the last backup version
keysBackupStateManager.state = KeysBackupState.WrongBackUpVersion
backupAllGroupSessionsCallback?.onFailure(failure)
resetBackupAllGroupSessionsListeners()
resetKeysBackupData()
keysBackupVersion = null

// Do not stay in KeysBackupState.WrongBackUpVersion but check what is available on the homeserver
checkAndStartKeysBackup()
}
else ->
// Come back to the ready state so that we will retry on the next received key
keysBackupStateManager.state = KeysBackupState.ReadyToBackUp
}
}
} else {
uiHandler.post {
backupAllGroupSessionsCallback?.onFailure(failure)
resetBackupAllGroupSessionsListeners()

Timber.e("backupKeys: backupKeys failed.")

// Retry a bit later
keysBackupStateManager.state = KeysBackupState.ReadyToBackUp
maybeBackupKeys()
}
}
}
})
.executeBy(taskExecutor) .executeBy(taskExecutor)
} }
} }
@ -1402,7 +1394,7 @@ internal class KeysBackup @Inject constructor(


companion object { companion object {
// Maximum delay in ms in {@link maybeBackupKeys} // Maximum delay in ms in {@link maybeBackupKeys}
private const val KEY_BACKUP_WAITING_TIME_TO_SEND_KEY_BACKUP_MILLIS = 10_000L private const val KEY_BACKUP_WAITING_TIME_TO_SEND_KEY_BACKUP_MILLIS = 10000


// Maximum number of keys to send at a time to the homeserver. // Maximum number of keys to send at a time to the homeserver.
private const val KEY_BACKUP_SEND_KEYS_MAX_COUNT = 100 private const val KEY_BACKUP_SEND_KEYS_MAX_COUNT = 100

View File

@ -22,7 +22,7 @@ package im.vector.matrix.android.internal.crypto.keysbackup
import androidx.annotation.WorkerThread import androidx.annotation.WorkerThread
import im.vector.matrix.android.api.listeners.ProgressListener import im.vector.matrix.android.api.listeners.ProgressListener
import timber.log.Timber import timber.log.Timber
import java.util.UUID import java.util.*
import javax.crypto.Mac import javax.crypto.Mac
import javax.crypto.spec.SecretKeySpec import javax.crypto.spec.SecretKeySpec
import kotlin.experimental.xor import kotlin.experimental.xor
@ -142,11 +142,12 @@ private fun deriveKey(password: String,
* Generate a 32 chars salt * Generate a 32 chars salt
*/ */
private fun generateSalt(): String { private fun generateSalt(): String {
val salt = buildString { var salt = ""
do {
append(UUID.randomUUID().toString()) do {
} while (length < SALT_LENGTH) salt += UUID.randomUUID().toString()
} } while (salt.length < SALT_LENGTH)



return salt.substring(0, SALT_LENGTH) return salt.substring(0, SALT_LENGTH)
} }

View File

@ -20,6 +20,7 @@ import android.os.Handler
import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupState import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupState
import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupStateListener import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupStateListener
import timber.log.Timber import timber.log.Timber
import java.util.*


internal class KeysBackupStateManager(private val uiHandler: Handler) { internal class KeysBackupStateManager(private val uiHandler: Handler) {



View File

@ -16,6 +16,7 @@


package im.vector.matrix.android.internal.crypto.keysbackup.tasks package im.vector.matrix.android.internal.crypto.keysbackup.tasks


import arrow.core.Try
import im.vector.matrix.android.internal.crypto.keysbackup.api.RoomKeysApi import im.vector.matrix.android.internal.crypto.keysbackup.api.RoomKeysApi
import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.CreateKeysBackupVersionBody import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.CreateKeysBackupVersionBody
import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.KeysVersion import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.KeysVersion
@ -29,7 +30,7 @@ internal class DefaultCreateKeysBackupVersionTask @Inject constructor(private va
: CreateKeysBackupVersionTask { : CreateKeysBackupVersionTask {




override suspend fun execute(params: CreateKeysBackupVersionBody): KeysVersion { override suspend fun execute(params: CreateKeysBackupVersionBody): Try<KeysVersion> {
return executeRequest { return executeRequest {
apiCall = roomKeysApi.createKeysBackupVersion(params) apiCall = roomKeysApi.createKeysBackupVersion(params)
} }

View File

@ -16,8 +16,10 @@


package im.vector.matrix.android.internal.crypto.keysbackup.tasks package im.vector.matrix.android.internal.crypto.keysbackup.tasks


import arrow.core.Try
import im.vector.matrix.android.internal.crypto.keysbackup.api.RoomKeysApi import im.vector.matrix.android.internal.crypto.keysbackup.api.RoomKeysApi
import im.vector.matrix.android.internal.network.executeRequest import im.vector.matrix.android.internal.network.executeRequest
import im.vector.matrix.android.internal.session.SessionScope
import im.vector.matrix.android.internal.task.Task import im.vector.matrix.android.internal.task.Task
import javax.inject.Inject import javax.inject.Inject


@ -31,9 +33,10 @@ internal interface DeleteBackupTask : Task<DeleteBackupTask.Params, Unit> {
internal class DefaultDeleteBackupTask @Inject constructor(private val roomKeysApi: RoomKeysApi) internal class DefaultDeleteBackupTask @Inject constructor(private val roomKeysApi: RoomKeysApi)
: DeleteBackupTask { : DeleteBackupTask {


override suspend fun execute(params: DeleteBackupTask.Params) { override suspend fun execute(params: DeleteBackupTask.Params): Try<Unit> {
return executeRequest { return executeRequest {
apiCall = roomKeysApi.deleteBackup(params.version) apiCall = roomKeysApi.deleteBackup(
params.version)
} }
} }
} }

View File

@ -16,8 +16,10 @@


package im.vector.matrix.android.internal.crypto.keysbackup.tasks package im.vector.matrix.android.internal.crypto.keysbackup.tasks


import arrow.core.Try
import im.vector.matrix.android.internal.crypto.keysbackup.api.RoomKeysApi import im.vector.matrix.android.internal.crypto.keysbackup.api.RoomKeysApi
import im.vector.matrix.android.internal.network.executeRequest import im.vector.matrix.android.internal.network.executeRequest
import im.vector.matrix.android.internal.session.SessionScope
import im.vector.matrix.android.internal.task.Task import im.vector.matrix.android.internal.task.Task
import javax.inject.Inject import javax.inject.Inject


@ -32,7 +34,7 @@ internal interface DeleteRoomSessionDataTask : Task<DeleteRoomSessionDataTask.Pa
internal class DefaultDeleteRoomSessionDataTask @Inject constructor(private val roomKeysApi: RoomKeysApi) internal class DefaultDeleteRoomSessionDataTask @Inject constructor(private val roomKeysApi: RoomKeysApi)
: DeleteRoomSessionDataTask { : DeleteRoomSessionDataTask {


override suspend fun execute(params: DeleteRoomSessionDataTask.Params) { override suspend fun execute(params: DeleteRoomSessionDataTask.Params): Try<Unit> {
return executeRequest { return executeRequest {
apiCall = roomKeysApi.deleteRoomSessionData( apiCall = roomKeysApi.deleteRoomSessionData(
params.roomId, params.roomId,

View File

@ -16,8 +16,10 @@


package im.vector.matrix.android.internal.crypto.keysbackup.tasks package im.vector.matrix.android.internal.crypto.keysbackup.tasks


import arrow.core.Try
import im.vector.matrix.android.internal.crypto.keysbackup.api.RoomKeysApi import im.vector.matrix.android.internal.crypto.keysbackup.api.RoomKeysApi
import im.vector.matrix.android.internal.network.executeRequest import im.vector.matrix.android.internal.network.executeRequest
import im.vector.matrix.android.internal.session.SessionScope
import im.vector.matrix.android.internal.task.Task import im.vector.matrix.android.internal.task.Task
import javax.inject.Inject import javax.inject.Inject


@ -31,7 +33,7 @@ internal interface DeleteRoomSessionsDataTask : Task<DeleteRoomSessionsDataTask.
internal class DefaultDeleteRoomSessionsDataTask @Inject constructor(private val roomKeysApi: RoomKeysApi) internal class DefaultDeleteRoomSessionsDataTask @Inject constructor(private val roomKeysApi: RoomKeysApi)
: DeleteRoomSessionsDataTask { : DeleteRoomSessionsDataTask {


override suspend fun execute(params: DeleteRoomSessionsDataTask.Params) { override suspend fun execute(params: DeleteRoomSessionsDataTask.Params): Try<Unit> {
return executeRequest { return executeRequest {
apiCall = roomKeysApi.deleteRoomSessionsData( apiCall = roomKeysApi.deleteRoomSessionsData(
params.roomId, params.roomId,

View File

@ -16,8 +16,10 @@


package im.vector.matrix.android.internal.crypto.keysbackup.tasks package im.vector.matrix.android.internal.crypto.keysbackup.tasks


import arrow.core.Try
import im.vector.matrix.android.internal.crypto.keysbackup.api.RoomKeysApi import im.vector.matrix.android.internal.crypto.keysbackup.api.RoomKeysApi
import im.vector.matrix.android.internal.network.executeRequest import im.vector.matrix.android.internal.network.executeRequest
import im.vector.matrix.android.internal.session.SessionScope
import im.vector.matrix.android.internal.task.Task import im.vector.matrix.android.internal.task.Task
import javax.inject.Inject import javax.inject.Inject


@ -30,9 +32,10 @@ internal interface DeleteSessionsDataTask : Task<DeleteSessionsDataTask.Params,
internal class DefaultDeleteSessionsDataTask @Inject constructor(private val roomKeysApi: RoomKeysApi) internal class DefaultDeleteSessionsDataTask @Inject constructor(private val roomKeysApi: RoomKeysApi)
: DeleteSessionsDataTask { : DeleteSessionsDataTask {


override suspend fun execute(params: DeleteSessionsDataTask.Params) { override suspend fun execute(params: DeleteSessionsDataTask.Params): Try<Unit> {
return executeRequest { return executeRequest {
apiCall = roomKeysApi.deleteSessionsData(params.version) apiCall = roomKeysApi.deleteSessionsData(
params.version)
} }
} }
} }

View File

@ -16,6 +16,7 @@


package im.vector.matrix.android.internal.crypto.keysbackup.tasks package im.vector.matrix.android.internal.crypto.keysbackup.tasks


import arrow.core.Try
import im.vector.matrix.android.internal.crypto.keysbackup.api.RoomKeysApi import im.vector.matrix.android.internal.crypto.keysbackup.api.RoomKeysApi
import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.KeysVersionResult import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.KeysVersionResult
import im.vector.matrix.android.internal.network.executeRequest import im.vector.matrix.android.internal.network.executeRequest
@ -28,7 +29,7 @@ internal class DefaultGetKeysBackupLastVersionTask @Inject constructor(private v
: GetKeysBackupLastVersionTask { : GetKeysBackupLastVersionTask {




override suspend fun execute(params: Unit): KeysVersionResult { override suspend fun execute(params: Unit): Try<KeysVersionResult> {
return executeRequest { return executeRequest {
apiCall = roomKeysApi.getKeysBackupLastVersion() apiCall = roomKeysApi.getKeysBackupLastVersion()
} }

View File

@ -16,6 +16,7 @@


package im.vector.matrix.android.internal.crypto.keysbackup.tasks package im.vector.matrix.android.internal.crypto.keysbackup.tasks


import arrow.core.Try
import im.vector.matrix.android.internal.crypto.keysbackup.api.RoomKeysApi import im.vector.matrix.android.internal.crypto.keysbackup.api.RoomKeysApi
import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.KeysVersionResult import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.KeysVersionResult
import im.vector.matrix.android.internal.network.executeRequest import im.vector.matrix.android.internal.network.executeRequest
@ -28,7 +29,7 @@ internal class DefaultGetKeysBackupVersionTask @Inject constructor(private val r
: GetKeysBackupVersionTask { : GetKeysBackupVersionTask {




override suspend fun execute(params: String): KeysVersionResult { override suspend fun execute(params: String): Try<KeysVersionResult> {
return executeRequest { return executeRequest {
apiCall = roomKeysApi.getKeysBackupVersion(params) apiCall = roomKeysApi.getKeysBackupVersion(params)
} }

View File

@ -16,9 +16,11 @@


package im.vector.matrix.android.internal.crypto.keysbackup.tasks package im.vector.matrix.android.internal.crypto.keysbackup.tasks


import arrow.core.Try
import im.vector.matrix.android.internal.crypto.keysbackup.api.RoomKeysApi import im.vector.matrix.android.internal.crypto.keysbackup.api.RoomKeysApi
import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.KeyBackupData import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.KeyBackupData
import im.vector.matrix.android.internal.network.executeRequest import im.vector.matrix.android.internal.network.executeRequest
import im.vector.matrix.android.internal.session.SessionScope
import im.vector.matrix.android.internal.task.Task import im.vector.matrix.android.internal.task.Task
import javax.inject.Inject import javax.inject.Inject


@ -33,7 +35,7 @@ internal interface GetRoomSessionDataTask : Task<GetRoomSessionDataTask.Params,
internal class DefaultGetRoomSessionDataTask @Inject constructor(private val roomKeysApi: RoomKeysApi) internal class DefaultGetRoomSessionDataTask @Inject constructor(private val roomKeysApi: RoomKeysApi)
: GetRoomSessionDataTask { : GetRoomSessionDataTask {


override suspend fun execute(params: GetRoomSessionDataTask.Params): KeyBackupData { override suspend fun execute(params: GetRoomSessionDataTask.Params): Try<KeyBackupData> {
return executeRequest { return executeRequest {
apiCall = roomKeysApi.getRoomSessionData( apiCall = roomKeysApi.getRoomSessionData(
params.roomId, params.roomId,

View File

@ -16,9 +16,11 @@


package im.vector.matrix.android.internal.crypto.keysbackup.tasks package im.vector.matrix.android.internal.crypto.keysbackup.tasks


import arrow.core.Try
import im.vector.matrix.android.internal.crypto.keysbackup.api.RoomKeysApi import im.vector.matrix.android.internal.crypto.keysbackup.api.RoomKeysApi
import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.RoomKeysBackupData import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.RoomKeysBackupData
import im.vector.matrix.android.internal.network.executeRequest import im.vector.matrix.android.internal.network.executeRequest
import im.vector.matrix.android.internal.session.SessionScope
import im.vector.matrix.android.internal.task.Task import im.vector.matrix.android.internal.task.Task
import javax.inject.Inject import javax.inject.Inject


@ -33,7 +35,7 @@ internal interface GetRoomSessionsDataTask : Task<GetRoomSessionsDataTask.Params
internal class DefaultGetRoomSessionsDataTask @Inject constructor(private val roomKeysApi: RoomKeysApi) internal class DefaultGetRoomSessionsDataTask @Inject constructor(private val roomKeysApi: RoomKeysApi)
: GetRoomSessionsDataTask { : GetRoomSessionsDataTask {


override suspend fun execute(params: GetRoomSessionsDataTask.Params): RoomKeysBackupData { override suspend fun execute(params: GetRoomSessionsDataTask.Params): Try<RoomKeysBackupData> {
return executeRequest { return executeRequest {
apiCall = roomKeysApi.getRoomSessionsData( apiCall = roomKeysApi.getRoomSessionsData(
params.roomId, params.roomId,

View File

@ -16,9 +16,11 @@


package im.vector.matrix.android.internal.crypto.keysbackup.tasks package im.vector.matrix.android.internal.crypto.keysbackup.tasks


import arrow.core.Try
import im.vector.matrix.android.internal.crypto.keysbackup.api.RoomKeysApi import im.vector.matrix.android.internal.crypto.keysbackup.api.RoomKeysApi
import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.KeysBackupData import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.KeysBackupData
import im.vector.matrix.android.internal.network.executeRequest import im.vector.matrix.android.internal.network.executeRequest
import im.vector.matrix.android.internal.session.SessionScope
import im.vector.matrix.android.internal.task.Task import im.vector.matrix.android.internal.task.Task
import javax.inject.Inject import javax.inject.Inject


@ -31,9 +33,10 @@ internal interface GetSessionsDataTask : Task<GetSessionsDataTask.Params, KeysBa
internal class DefaultGetSessionsDataTask @Inject constructor(private val roomKeysApi: RoomKeysApi) internal class DefaultGetSessionsDataTask @Inject constructor(private val roomKeysApi: RoomKeysApi)
: GetSessionsDataTask { : GetSessionsDataTask {


override suspend fun execute(params: GetSessionsDataTask.Params): KeysBackupData { override suspend fun execute(params: GetSessionsDataTask.Params): Try<KeysBackupData> {
return executeRequest { return executeRequest {
apiCall = roomKeysApi.getSessionsData(params.version) apiCall = roomKeysApi.getSessionsData(
params.version)
} }
} }
} }

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