Merge branch 'release/1.0.10'

This commit is contained in:
Benoit Marty 2020-11-04 15:58:45 +01:00
commit bba167d4ea
255 changed files with 9595 additions and 6720 deletions

View File

@ -33,3 +33,8 @@ First of all, we thank all contributors who use Element and report problems on t
We do not forget all translators, for their work of translating Element into many languages. They are also the authors of Element. We do not forget all translators, for their work of translating Element into many languages. They are also the authors of Element.
Feel free to add your name below, when you contribute to the project! Feel free to add your name below, when you contribute to the project!
Name | Matrix ID | GitHub
--------|---------------------|--------------------------------------
gjpower | @gjpower:matrix.org | [gjpower](https://github.com/gjpower)

View File

@ -1,3 +1,30 @@
Changes in Element 1.0.10 (2020-11-04)
===================================================
Improvements 🙌:
- Rework sending Event management (#154)
- New room creation screen: set topic and avatar in the room creation form (#2078)
- Toggle Low priority tag (#1490)
- Add option to send with enter (#1195)
- Use Hardware keyboard enter to send message (use shift-enter for new line) (#1881, #1440)
- Edit and remove icons are now visible on image attachment preview screen (#2294)
- Room profile: BigImageViewerActivity now only display the image. Use the room setting to change or delete the room Avatar
- Better visibility of text reactions in dark theme (#1118)
- Room member profile: Add action to create (or open) a DM (#2310)
- Prepare changelog for F-Droid (#2296)
- Add graphic resources for F-Droid (#812, #2220)
- Highlight text in the body of the displayed result (#2200)
- Considerably faster QR-code bitmap generation (#2331)
Bugfix 🐛:
- Fixed ringtone handling (#2100 & #2246)
- Messages encrypted with no way to decrypt after SDK update from 0.18 to 1.0.0 (#2252)
- Incoming call continues to ring if call is answered on another device (#1921)
- Search Result | scroll jumps after pagination (#2238)
- Badly formatted mentions in body (#1506)
- KeysBackup: Avoid using `!!` (#2262)
- Two elements in the task switcher (#2299)
Changes in Element 1.0.9 (2020-10-16) Changes in Element 1.0.9 (2020-10-16)
=================================================== ===================================================

View File

@ -65,9 +65,8 @@ allprojects {
tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all { tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all {
// Warnings are potential errors, so stop ignoring them // Warnings are potential errors, so stop ignoring them
// You can override by passing `-PallWarningsAsErrors=false` in the command line // You can override by passing `-PallWarningsAsErrors=false` in the command line
kotlinOptions.allWarningsAsErrors = project.properties['allWarningsAsErrors']?.toBoolean() ?: true kotlinOptions.allWarningsAsErrors = project.getProperties().getOrDefault("allWarningsAsErrors", "true").toBoolean()
} }
} }
task clean(type: Delete) { task clean(type: Delete) {

View File

@ -0,0 +1,30 @@
Element е приложение от нов тип за съобщения и сътрудничество:
1. Дава Ви контрол, за да запазите поверителността си
2. Позволява ви да комуникирате с всеки в мрежата на Matrix и дори извън него, като се интегрира с приложения като Slack
3. Предпазва ви от реклами, изтичане на данни и търговско следене
4. Защитава ви чрез шифроване от край до край, с кръстосано подписване, за да проверите другите
Element е напълно различен от другите приложения за съобщения и сътрудничество, понеже е децентрализиран и с отворен код.
Element ви позволява да го хоствате самостоятелно - или да изберете хост - така че да имате поверителност, собственост и контрол върху Вашите данни и разговори. Дава ви достъп до отворена мрежа, така че комуникацията Ви не е ограничена до потребителите на Element. И е много сигурно.
Element е в състояние да направи всичко това, защото работи върху Matrix - стандартът за отворена, децентрализирана комуникация.
Element ви дава контрол, като ви позволява да изберете кой да хоства Вашите разговори. От приложението Element можете да изберете хостване по различни начини:
1. Вземете безплатен профил на публичния сървър на matrix.org, хостван от разработчиците на Matrix, или изберете от хиляди публични сървъри, хоствани от доброволци
2. Самостоятелно хоствайте профила си, като пуснете сървър на собствен хардуер
3. Регистрирайте се за профил на персонализиран сървър, като се абонирате за хостинг платформата Element Matrix Services
<b>Защо да изберете Element?</b>
<b>ПРИТЕЖАВАЙТЕ ДАННИТЕ СИ</b>: Вие решавате къде да съхранявате вашите данни и съобщения. Вие ги притежавате и контролирате, а не някаква МЕГАКОРПОРАЦИЯ, която складира вашите данни или дава достъп на трети страни.
<b>ОТВОРЕНИ СЪОБЩЕНИЯ И СЪТРУДНИЧЕСТВО</b>: Можете да разговаряте с всеки друг в мрежата на Matrix, независимо дали използва Element или друго приложение на Matrix и дори ако използва различна система за съобщения като Slack, IRC or XMPP.
<b>СВРЪХ СИГУРНО</b>: Реално шифроване от край до край (само тези в разговора могат да дешифрират съобщения) и кръстосано подписване за проверка на устройствата на участниците в разговора.
<b>ПЪЛНА КОМУНИКАЦИЯ</b>: Съобщения, гласови и видео разговори, споделяне на файлове, споделяне на екран и цял куп интеграции, ботове и джаджи. Изграждайте стаи, общности, поддържайте връзка и направете нещата завършени.
<b>НАВСЯКЪДЕ КЪДЕТО СТЕ</b>: Поддържайте връзка, където и да сте, с напълно синхронизирана история на съобщенията на всичките ви устройства и чрез web на https://app.element.io.

View File

@ -0,0 +1 @@
Сигурен децентрализиран чат и VoIP. Пазете данните си от външни лица.

View File

@ -0,0 +1 @@
Element (предишен Riot.im)

View File

@ -0,0 +1,2 @@
This new version mainly contains bug fixes and improvements. Sending a message is now much faster.
Full changelog: https://github.com/vector-im/element-android/releases/tag/v1.0.10

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 131 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 316 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 310 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 543 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 341 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 334 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 551 KiB

View File

@ -0,0 +1,30 @@
Element estas nova speco de mesaĝilo kaj kunlaborilo, kiu:
1. Lasas vin regi vian komunikadon por protekti vian privatecon
2. Lasas vin komuniki kun ĉiu ajn en la reto de Matrix, kaj eĉ pliaj, per interkompreno kun aplikaĵoj kiel ekzemple Slack
3. Protektas vin de reklamoj, kolektado de datumoj, kaj muritaj ĝardenoj
4. Sekurigas vian komunikadon per tutvoja ĉifrado, kun la eblo kontroli aliajn per delegaj subskriboj
Element malsamas al aliaj mesaĝiloj kaj kunlaboriloj, ĉar ĝi estas sencentra kaj malfermitkoda.
Element lasas vin gastigi vian propran servilon, aŭ elekti servilon, kiu plaĉas al vi, por ke vi ne perdu privatecon, kaj por ke vi daŭre regu kaj posedu viajn datumojn kaj interparolojn. Ĝi donas al vi aliron al malfermita reto; por ke via interparolado ne estu limigita nur al aliaj uzantoj de Element. Kaj ĝi estas tre sekura.
Element povas fari ĉi ĉion, ĉar ĝi funkcias per Matrix publika normo por malfermita, sencentra komunikado.
Element lasas vi elekti, kiu gastigos viajn interparolojn. Per la aplikaĵo Element, vi povas elekti diversajn specojn de gastigado:
1. Akiri senpagan konton ĉe la publika servilo matrix.org, gastigata de la programistoj de Matrix, aŭ elekti unu el miloj da publikaj serviloj, gastigataj de volontuloj
2. Memgastiĝi per via propra servilo ĉe via propra aparato
3. Registriĝi ĉe propra servilo per simpla pagaliĝo al la gastiga platformo «Element Matrix Services»
<b>Kial Element?</b>
<b>POSEDU VIAJN DATUMOJN</b>: Vi decidu, kie vi tenu viajn datumojn kaj mesaĝojn. Vi posedas kaj regas ilin, ne iu granda komerca firmao, kiu kolektas viajn datumojn aŭ donas aliron al aliuloj.
<b>MALFERMAJ MESAĜADO KAJ KUNLABORADO</b>: Vi povas babili kun ĉiu alia en la reto de Matrix, ĉu ĝi uzas Elementon aŭ alian aplikaĵon de Matrix, kaj eĉ se ĝi uzas tute alian mesaĝilon, kiel ekzemple Slack, IRC, aŭ XMPP.
<b>TRE SEKURA</b>: Vera tutvoja ĉifrado (nur la interparolantoj povas malĉifri siajn mesaĝojn), kaj delegaj subskriboj por kontroli la aparatojn de partoprenantoj.
<b>SENMANKA KOMUNIKADO</b>: Mesaĝoj, voĉvokoj kaj vidvokoj, havigado de dosieroj, ekrano, kaj multaj diversaj kunigoj, robotoj kaj fenestraĵoj. Kreu ĉambrojn, komunumojn, komuniku kaj kunlaboru.
<b>ĈIE KUN VI</b>: Tenu vin ĝisdata per historio de mesaĝoj plene spegulita trans ĉiuj viaj aparatoj, kaj sur la reto per https://app.element.io.

View File

@ -0,0 +1 @@
Sekura kaj sencentrigita vokado kaj babilado. Tenu viajn datumojn sekuraj.

View File

@ -0,0 +1 @@
Element (antaŭe Riot.im)

View File

@ -0,0 +1,30 @@
Element on uut tüüpi suhtlus- ja koostöörakendus, mis:
1. Võimaldab täielikku kontrolli privaatsuse üle
2. Võimaldab suhelda kõigiga Matrixi võrgus ja isegi väljaspool seda, olles integreeritud selliste rakendustega nagu Slack
3. Kaitseb sind reklaamide ja andmekogumise eest
4. Tagab turvalisuse läbiva krüptimise abil, kasutades risttunnustamist vestlejate tuvastamiseks
Element erineb täielikult teistest sõnumside- ja koostöörakendustest, kuna see on detsentraliseeritud ja avatud lähtekoodiga.
Element võimaldab ise hostida - või valida hosti -, et oleks tagatud privaatsus ja kontroll oma andmete ja vestluste üle. Element annab ka juurdepääsu avatud võrgule, nii et te ei pea vaid Elemendi kasutajatega rääkima. Ning kogu süsteem on väga turvaline.
Element töötab Matrixil - avatud, detsentraliseeritud suhtlus-standardil.
Võimaldades valida, kes vestlusi korraldab, annab Element annab kontrolli sinule. Rakendust Element saad kasutada mitmel viisil.
1. Tasuta konto Matrixi arendajate hostitud avalikus serveris matrix.org või vali tuhandete avalike serverite hulgast, mida haldavad vabatahtlikud
2. Hosti oma kontot ise, paigaldades serveri oma riistvarale
3. Registreeruge serveris olevale kontole, tellides Element Matrix Services teenuseplatvormi
<b> Miks valida element? </b>
<b> KONTROLL ANDMETE ÜLE</b>: otsustad ise, kus oma andmeid ja sõnumeid hoida. Need kuuluvad sulle ja sinu käes on kontroll, mitte mõne MEGAFIRMA käes, mis andmeid oma kasuks kaevandab või kolmandatele isikutele juurdepääsu annab.
<b> AVATUD SUHTLUS JA KOOSTÖÖ </b>: saad vestelda kõigi teistega Matrixi võrgus, olenemata sellest, kas nad kasutavad Elementi või mõnda muud Matrixi rakendust, ja isegi kui nad kasutavad teistsugust suhtlussüsteemi nagu Slack, IRC või XMPP.
<b> ÜLITURVALINE </b>: tõeline läbiv krüptimine (ainult vestluses osalejad saavad sõnumeid lugeda) ja risttunnustamine vestluses osalejate tuvastamiseks.
<b> KÕIK SUHTLUSVÕIMALUSED</b>: sõnumid, hääl- ja videokõned, failide jagamine, ekraani jagamine ja terve hulk lõiminguid, roboteid ja vidinaid. Loo tubasid, kogukondi, hoia ühendust ja saa asjad aetud.
<b> KÕIKJAL, KUS VIIBITE</b>: saad suhelda kõigis oma seadmetes ja ka veebis aadressil https://app.element.io ning sealjuures täielikult sünkroonitud sõnumite ajalooga.

View File

@ -0,0 +1,30 @@
المنت گونه‌ای جدید از کاره‌های پیام‌رسانی و همکاری است که:
۱. کنترل محرمانگیتان را در دست خودتان می‌گذارد
۲. می‌گذارد با هرکسی در شبکهٔ ماتریکس و حتا فراتر از آن، ارتباط برقرار کنید
۳. از شما در برابر تبلیغات، داده‌کاوری و دیوارهای پرداختی، محافظت می‌کند
۴. با رمزنگاری سرتاسری با ورود چندگانه، امنتان می‌کند
المنت به خاطر نامتمرکز و نرم‌افزار آزاد بودن، کاملاً با دیگر کاره‌های پیام‌رسانی و همکاری، فرق دارد.
المنت می‌گذارد خودمیزبانی کرده یا میزبانی برگزینید که امنیت، مالکیت و واپایش داده‌ها و گفت‌وگوهایتان را در اختیار داشته باشید. این کاره شما را به شبکه‌ای باز و شدیداً امن وصل کرده تا مجبور نباشید فقط با دیگر کاربران المنت صحبت کنید.
المنت می‌تواند همهٔ این کارها را بکند، چرا که روی ماتریکس، استانداردی برای گفت‌وگوی باز و نامتمرکز عمل می‌کند.
المنت با اجازه برای گزینش کسی که گفت‌وگوهایتان را میزبانی می‌کند، کنترل را به شما می‌دهد. با کارهٔ المنت، می‌توانید برگزینید که به روش‌های مختلفی میزبانی شوید:
۱. گرفتن حسابی رایگان روی کارساز عمومی matrix.org که به دست توسعه‌دهندگان ماتریکس میزبانی می‌شود، یا گرینش از میان هزاران کارساز عمومی میزبانی‌شده به دست داوطلبان
۲. خودمیزبانی حسابتان با اجرای کراسازی روی سخت‌افزار خودتان
۳. ثبت‌نام برای حسابی روی یک کارساز سفارشی با اشتراک در بن‌یازهٔ میزبانی خدمات ماتریکس المنت
<b>چرا المنت را برگزینیم؟</b>
<b>مالک داده‌هایتان باشید</b>: خوتان تصمیم می‌گیرید که داده‌ها و پیام‌هایتان را کجا نگه دارید. شما صاحبشان هستید و واپایششان می‌کنید، نه شرکت‌های بزرگی که داده‌هایتان را کاویده و به شرکت‌های دیگر دسترسی می‌دهند.
<b>پیام‌رسانی و همکاری باز</b>: می‌توانید با هرکسی در شبکهٔ ماتریکس گپ بزنید، چه از المنت استفاده کنند و چه از هر کارهٔ ماتریکس دیگری؛ و حتا اگر از سامانهٔ پیام‌رسانی متفاوتی مثل اسلک، آی‌آرسی یا جبر استفاده کنند.
<b>فوق امن</b>: رمزنگاری سرتاسری واقعی (فقط کسانی که در گفت‌وگو هستند،‌می‌توانند پیام‌ها را رمزگشایی کنند) و ورود چندگانه برای تأیید هویت افزاره‌های شرکت‌کنندگان در گفت‌وگو.
<b>ارتباط کامل</b>: پیام‌رسانی، تماس‌های صوتی و تصویری،‌هم‌رسانی پرونده، هم‌رسانی صفحه و یه عالمه یکپارچگی، بات و ابزارک. اتاق و اجتماع ساخته، در دسترس بوده و کارها را انجام دهید.
<b>هرجا که هستید</b>: هر کجا که هستید، با هم‌گام سازی کامل تاریخچهٔ پیام‌ها بین همهٔ افزاره‌هایتان و وب روی https://app.element.io در دسترس باشید.

View File

@ -0,0 +1 @@
گپ و تماس نامتمرکز امن. داده‌هایتان را از شرکت‌ها امن نگه دارید.

View File

@ -0,0 +1 @@
المنت (ریوت سابق)

View File

@ -0,0 +1,30 @@
Element est une nouvelle application de messagerie et de collaboration qui :
1) Vous place aux commandes de votre vie privée
2) Vous permet de communiquer avec n'importe qui du réseau Matrix, et plus encore par des intégrations d'autres applications comme Slack ou Discord
3) Vous protège de la publicité et de la collecte de données
4) Vous sécurise grâce à du chiffrement bout-à-bout, avec de la signature croisée pour authentifier les autres utilisateurs
Element est complètement différent des autres applications de messagerie et de collaboration puisque l'application est décentralisée et open-source.
Element vous permet d'héberger vous-même -ou de choisir un hôte- vous permettant d'assurer votre vie privée, la propriété et le contrôle de vos données et de vos conversations. Cela vous offre l'accès à un réseau ouvert, vous n'êtes donc pas condamné à parler à d'autres utilisateurs d'Element seulement. Et c'est très sécurisé.
Element peut faire tout ça car il est basé sur Matrix, le protocole standard pour la communication ouverte et décentralisée.
Element vous donne le contrôle en vous laissant choisir qui héberge vos conversations. Depuis l'application Element, vous pouvez choisir votre hôte de différentes manières :
1) Créer un compte gratuit sur le serveur public matrix.org hébergé par les développeurs de Matrix, ou choisir parler les milliers de serveurs public hébergés par des bénévoles
2) Héberger vous-même votre compte en installant un serveur sur votre propre machine
3) Créer un compte sur un serveur personnalisé en souscrivant sur la plateforme d'hébergement « Element Matrix Services » (EMS)
<b>Pourquoi choisir Element ?</b>
<b>POSSÉDEZ VOS DONNÉES</b> : Vous décidez où conserver vos données et vos messages. Vous les possédez et vous les contrôlez, et non une MEGACORP qui mine vos données ou les donnent à des tiers
<b>UNE MESSAGERIE OUVERTE ET COLLABORATIVE</b> : Vous pouvez discuter avec n'importe qui sur le réseau Matrix, qu'ils utilisent Element ou une autre application basée sur Matrix, et même s'ils utilisent un système de messagerie différent comment Slack, Discord, IRC ou XMPP.
<b>SUPER SÉCURISÉ</b> : Un réel chiffrement bout-à-bout (seulement ceux deux la conversation peuvent déchiffrer les messages), et une signature croisée pour vérifier les appareils des participants de la conversation.
<b>COMMUNICATION COMPLÈTE</b> : Messagerie, appels vocaux et vidéo, transfert de fichiers, partage d'écran et un tas d'intégrations, robots et widgets. Construisez des salons, des communautés, restez en contact et accomplissez de grandes choses.
<b>PARTOUT OÙ VOUS ÊTES</b> : Restez connectés peu import où vous êtes avec la synchronisation complète de l'historique des messages sur tous vos appareils et sur le web sur https://app.element.io.

View File

@ -0,0 +1 @@
Chat & VoIP sûr et décentralisé. Gardez vos données en sécurité.

View File

@ -1 +1 @@
Element (előzőleg Riot.im) Element (régebben Riot.im)

View File

@ -21,7 +21,7 @@ Element sätter dig i kontroll genom att låta dig välja att vara värd för di
<b>ÄG DIN DATA</b>: Du väljer var du vill ha din data och dina meddelanden. Du äger den och kontrollerar den, inte nåt stort företag som samlar in din data och ger den till tredje parter. <b>ÄG DIN DATA</b>: Du väljer var du vill ha din data och dina meddelanden. Du äger den och kontrollerar den, inte nåt stort företag som samlar in din data och ger den till tredje parter.
<b>ÖPPEN KOMMUNIKATION OCH ÖPPET SAMARBETE</b>: Du kan chatta med med vem som helst på Matrix-nätverket, oavsett om de använder Element eller en annan Matrix-app, och till och med om de använder ett annat meddelandesystem som Slack, IRC eller XMPP. <b>ÖPPEN KOMMUNIKATION OCH ÖPPET SAMARBETE</b>: Du kan chatta med vem som helst på Matrix-nätverket, oavsett om de använder Element eller en annan Matrix-app, och till och med om de använder ett annat meddelandesystem som Slack, IRC eller XMPP.
<b>SUPERSÄKER</b>: Riktig totalsträckskryptering (bara de in konversationen kan avkryptera meddelandena), och korssingering för att verifiera konversationsmedlemmars enheter. <b>SUPERSÄKER</b>: Riktig totalsträckskryptering (bara de in konversationen kan avkryptera meddelandena), och korssingering för att verifiera konversationsmedlemmars enheter.

View File

@ -142,6 +142,10 @@ class RxRoom(private val room: Room) {
fun updateAvatar(avatarUri: Uri, fileName: String): Completable = completableBuilder<Unit> { fun updateAvatar(avatarUri: Uri, fileName: String): Completable = completableBuilder<Unit> {
room.updateAvatar(avatarUri, fileName, it) room.updateAvatar(avatarUri, fileName, it)
} }
fun deleteAvatar(): Completable = completableBuilder<Unit> {
room.deleteAvatar(it)
}
} }
fun Room.rx(): RxRoom { fun Room.rx(): RxRoom {

View File

@ -88,7 +88,10 @@ class CommonTestHelper(context: Context) {
fun syncSession(session: Session) { fun syncSession(session: Session) {
val lock = CountDownLatch(1) val lock = CountDownLatch(1)
GlobalScope.launch(Dispatchers.Main) { session.open() } val job = GlobalScope.launch(Dispatchers.Main) {
session.open()
}
runBlocking { job.join() }
session.startSync(true) session.startSync(true)
@ -341,7 +344,7 @@ class CommonTestHelper(context: Context) {
} }
// Transform a method with a MatrixCallback to a synchronous method // Transform a method with a MatrixCallback to a synchronous method
inline fun <reified T> doSync(block: (MatrixCallback<T>) -> Unit): T { inline fun <reified T> doSync(timeout: Long? = TestConstants.timeOutMillis, block: (MatrixCallback<T>) -> Unit): T {
val lock = CountDownLatch(1) val lock = CountDownLatch(1)
var result: T? = null var result: T? = null
@ -354,7 +357,7 @@ class CommonTestHelper(context: Context) {
block.invoke(callback) block.invoke(callback)
await(lock) await(lock, timeout)
assertNotNull(result) assertNotNull(result)
return result!! return result!!
@ -366,8 +369,9 @@ class CommonTestHelper(context: Context) {
fun Iterable<Session>.signOutAndClose() = forEach { signOutAndClose(it) } fun Iterable<Session>.signOutAndClose() = forEach { signOutAndClose(it) }
fun signOutAndClose(session: Session) { fun signOutAndClose(session: Session) {
doSync<Unit> { session.signOut(true, it) } doSync<Unit>(60_000) { session.signOut(true, it) }
session.close() // no need signout will close
// session.close()
} }
} }

View File

@ -32,9 +32,6 @@ import org.matrix.android.sdk.api.session.room.model.Membership
import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.api.session.room.model.RoomSummary
import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams
import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams
import org.matrix.android.sdk.api.session.room.timeline.Timeline
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings
import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM_BACKUP import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM_BACKUP
import org.matrix.android.sdk.internal.crypto.keysbackup.model.MegolmBackupAuthData import org.matrix.android.sdk.internal.crypto.keysbackup.model.MegolmBackupAuthData
@ -197,47 +194,16 @@ class CryptoTestHelper(private val mTestHelper: CommonTestHelper) {
val roomFromBobPOV = bobSession.getRoom(aliceRoomId)!! val roomFromBobPOV = bobSession.getRoom(aliceRoomId)!!
val roomFromAlicePOV = aliceSession.getRoom(aliceRoomId)!! val roomFromAlicePOV = aliceSession.getRoom(aliceRoomId)!!
val lock = CountDownLatch(1)
val bobEventsListener = object : Timeline.Listener {
override fun onTimelineFailure(throwable: Throwable) {
// noop
}
override fun onNewTimelineEvents(eventIds: List<String>) {
// noop
}
override fun onTimelineUpdated(snapshot: List<TimelineEvent>) {
val messages = snapshot.filter { it.root.getClearType() == EventType.MESSAGE }
.groupBy { it.root.senderId!! }
// Alice has sent 2 messages and Bob has sent 3 messages
if (messages[aliceSession.myUserId]?.size == 2 && messages[bobSession.myUserId]?.size == 3) {
lock.countDown()
}
}
}
val bobTimeline = roomFromBobPOV.createTimeline(null, TimelineSettings(20))
bobTimeline.start()
bobTimeline.addListener(bobEventsListener)
// Alice sends a message // Alice sends a message
roomFromAlicePOV.sendTextMessage(messagesFromAlice[0]) mTestHelper.sendTextMessage(roomFromAlicePOV, messagesFromAlice[0], 1)
// roomFromAlicePOV.sendTextMessage(messagesFromAlice[0])
// Bob send 3 messages // Bob send 3 messages
roomFromBobPOV.sendTextMessage(messagesFromBob[0]) mTestHelper.sendTextMessage(roomFromBobPOV, messagesFromBob[0], 1)
roomFromBobPOV.sendTextMessage(messagesFromBob[1]) mTestHelper.sendTextMessage(roomFromBobPOV, messagesFromBob[1], 1)
roomFromBobPOV.sendTextMessage(messagesFromBob[2]) mTestHelper.sendTextMessage(roomFromBobPOV, messagesFromBob[2], 1)
// Alice sends a message // Alice sends a message
roomFromAlicePOV.sendTextMessage(messagesFromAlice[1]) mTestHelper.sendTextMessage(roomFromAlicePOV, messagesFromAlice[1], 1)
mTestHelper.await(lock)
bobTimeline.removeListener(bobEventsListener)
bobTimeline.dispose()
return cryptoTestData return cryptoTestData
} }
@ -279,20 +245,14 @@ class CryptoTestHelper(private val mTestHelper: CommonTestHelper) {
fun createFakeMegolmBackupCreationInfo(): MegolmBackupCreationInfo { fun createFakeMegolmBackupCreationInfo(): MegolmBackupCreationInfo {
return MegolmBackupCreationInfo( return MegolmBackupCreationInfo(
algorithm = MXCRYPTO_ALGORITHM_MEGOLM_BACKUP, algorithm = MXCRYPTO_ALGORITHM_MEGOLM_BACKUP,
authData = createFakeMegolmBackupAuthData() authData = createFakeMegolmBackupAuthData(),
recoveryKey = "fake"
) )
} }
fun createDM(alice: Session, bob: Session): String { fun createDM(alice: Session, bob: Session): String {
val roomId = mTestHelper.doSync<String> { val roomId = mTestHelper.doSync<String> {
alice.createRoom( alice.createDirectRoom(bob.myUserId, it)
CreateRoomParams().apply {
invitedUserIds.add(bob.myUserId)
setDirectMessage()
enableEncryptionIfInvitedUsersSupportIt = true
},
it
)
} }
mTestHelper.waitWithLatch { latch -> mTestHelper.waitWithLatch { latch ->

View File

@ -115,9 +115,8 @@ class KeysBackupTest : InstrumentedTest {
} }
assertEquals(MXCRYPTO_ALGORITHM_MEGOLM_BACKUP, megolmBackupCreationInfo.algorithm) assertEquals(MXCRYPTO_ALGORITHM_MEGOLM_BACKUP, megolmBackupCreationInfo.algorithm)
assertNotNull(megolmBackupCreationInfo.authData) assertNotNull(megolmBackupCreationInfo.authData.publicKey)
assertNotNull(megolmBackupCreationInfo.authData!!.publicKey) assertNotNull(megolmBackupCreationInfo.authData.signatures)
assertNotNull(megolmBackupCreationInfo.authData!!.signatures)
assertNotNull(megolmBackupCreationInfo.recoveryKey) assertNotNull(megolmBackupCreationInfo.recoveryKey)
stateObserver.stopAndCheckStates(null) stateObserver.stopAndCheckStates(null)
@ -258,14 +257,14 @@ class KeysBackupTest : InstrumentedTest {
// - Check encryptGroupSession() returns stg // - Check encryptGroupSession() returns stg
val keyBackupData = keysBackup.encryptGroupSession(session) val keyBackupData = keysBackup.encryptGroupSession(session)
assertNotNull(keyBackupData) assertNotNull(keyBackupData)
assertNotNull(keyBackupData.sessionData) assertNotNull(keyBackupData!!.sessionData)
// - Check pkDecryptionFromRecoveryKey() is able to create a OlmPkDecryption // - Check pkDecryptionFromRecoveryKey() is able to create a OlmPkDecryption
val decryption = keysBackup.pkDecryptionFromRecoveryKey(keyBackupCreationInfo.recoveryKey) val decryption = keysBackup.pkDecryptionFromRecoveryKey(keyBackupCreationInfo.recoveryKey)
assertNotNull(decryption) assertNotNull(decryption)
// - Check decryptKeyBackupData() returns stg // - Check decryptKeyBackupData() returns stg
val sessionData = keysBackup val sessionData = keysBackup
.decryptKeyBackupData(keyBackupData, .decryptKeyBackupData(keyBackupData!!,
session.olmInboundGroupSession!!.sessionIdentifier(), session.olmInboundGroupSession!!.sessionIdentifier(),
cryptoTestData.roomId, cryptoTestData.roomId,
decryption!!) decryption!!)

View File

@ -25,6 +25,8 @@ import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.junit.runners.MethodSorters import org.junit.runners.MethodSorters
import org.matrix.android.sdk.InstrumentedTest import org.matrix.android.sdk.InstrumentedTest
import org.matrix.android.sdk.internal.session.room.send.pills.MentionLinkSpecComparator
import org.matrix.android.sdk.internal.session.room.send.pills.TextPillsUtils
/** /**
* It will not be possible to test all combinations. For the moment I add a few tests, then, depending on the problem discovered in the wild, * It will not be possible to test all combinations. For the moment I add a few tests, then, depending on the problem discovered in the wild,
@ -45,7 +47,8 @@ class MarkdownParserTest : InstrumentedTest {
*/ */
private val markdownParser = MarkdownParser( private val markdownParser = MarkdownParser(
Parser.builder().build(), Parser.builder().build(),
HtmlRenderer.builder().build() HtmlRenderer.builder().softbreak("<br />").build(),
TextPillsUtils(MentionLinkSpecComparator())
) )
@Test @Test
@ -144,12 +147,14 @@ class MarkdownParserTest : InstrumentedTest {
) )
} }
// TODO. Improve testTypeNewLines function to cover <pre><code class="language-code">test</code></pre>
@Test @Test
fun parseCodeNewLines() { fun parseCodeNewLines_not_passing() {
testTypeNewLines( testTypeNewLines(
name = "code", name = "code",
markdownPattern = "`", markdownPattern = "```",
htmlExpectedTag = "code" htmlExpectedTag = "code",
softBreak = "\n"
) )
} }
@ -163,7 +168,7 @@ class MarkdownParserTest : InstrumentedTest {
} }
@Test @Test
fun parseCode2NewLines() { fun parseCode2NewLines_not_passing() {
testTypeNewLines( testTypeNewLines(
name = "code", name = "code",
markdownPattern = "``", markdownPattern = "``",
@ -181,7 +186,7 @@ class MarkdownParserTest : InstrumentedTest {
} }
@Test @Test
fun parseCode3NewLines() { fun parseCode3NewLines_not_passing() {
testTypeNewLines( testTypeNewLines(
name = "code", name = "code",
markdownPattern = "```", markdownPattern = "```",
@ -243,7 +248,7 @@ class MarkdownParserTest : InstrumentedTest {
} }
@Test @Test
fun parseBoldNewLines_not_passing() { fun parseBoldNewLines2() {
"**bold**\nline2".let { markdownParser.parse(it).expect(it, "<strong>bold</strong><br />line2") } "**bold**\nline2".let { markdownParser.parse(it).expect(it, "<strong>bold</strong><br />line2") }
} }
@ -334,13 +339,14 @@ class MarkdownParserTest : InstrumentedTest {
private fun testTypeNewLines(name: String, private fun testTypeNewLines(name: String,
markdownPattern: String, markdownPattern: String,
htmlExpectedTag: String) { htmlExpectedTag: String,
softBreak: String = "<br />") {
// With new line inside the block // With new line inside the block
"$markdownPattern$name\n$name$markdownPattern" "$markdownPattern$name\n$name$markdownPattern"
.let { .let {
markdownParser.parse(it) markdownParser.parse(it)
.expect(expectedText = it, .expect(expectedText = it,
expectedFormattedText = "<$htmlExpectedTag>$name<br />$name</$htmlExpectedTag>") expectedFormattedText = "<$htmlExpectedTag>$name$softBreak$name</$htmlExpectedTag>")
} }
// With new line between two blocks // With new line between two blocks
@ -348,7 +354,7 @@ class MarkdownParserTest : InstrumentedTest {
.let { .let {
markdownParser.parse(it) markdownParser.parse(it)
.expect(expectedText = it, .expect(expectedText = it,
expectedFormattedText = "<$htmlExpectedTag>$name</$htmlExpectedTag><$htmlExpectedTag>$name</$htmlExpectedTag>") expectedFormattedText = "<$htmlExpectedTag>$name</$htmlExpectedTag><br /><$htmlExpectedTag>$name</$htmlExpectedTag>")
} }
} }

View File

@ -1,68 +0,0 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.database
import org.matrix.android.sdk.internal.crypto.store.db.model.CrossSigningInfoEntity
import org.matrix.android.sdk.internal.crypto.store.db.model.CryptoMetadataEntity
import org.matrix.android.sdk.internal.crypto.store.db.model.CryptoRoomEntity
import org.matrix.android.sdk.internal.crypto.store.db.model.DeviceInfoEntity
import org.matrix.android.sdk.internal.crypto.store.db.model.GossipingEventEntity
import org.matrix.android.sdk.internal.crypto.store.db.model.IncomingGossipingRequestEntity
import org.matrix.android.sdk.internal.crypto.store.db.model.KeyInfoEntity
import org.matrix.android.sdk.internal.crypto.store.db.model.KeysBackupDataEntity
import org.matrix.android.sdk.internal.crypto.store.db.model.MyDeviceLastSeenInfoEntity
import org.matrix.android.sdk.internal.crypto.store.db.model.OlmInboundGroupSessionEntity
import org.matrix.android.sdk.internal.crypto.store.db.model.OlmSessionEntity
import org.matrix.android.sdk.internal.crypto.store.db.model.OutgoingGossipingRequestEntity
import org.matrix.android.sdk.internal.crypto.store.db.model.TrustLevelEntity
import org.matrix.android.sdk.internal.crypto.store.db.model.UserEntity
import io.realm.Realm
import io.realm.RealmConfiguration
import io.realm.kotlin.where
import timber.log.Timber
object RealmDebugTools {
/**
* Log info about the crypto DB
*/
fun dumpCryptoDb(realmConfiguration: RealmConfiguration) {
Realm.getInstance(realmConfiguration).use {
Timber.d("Realm located at : ${realmConfiguration.realmDirectory}/${realmConfiguration.realmFileName}")
val key = realmConfiguration.encryptionKey.joinToString("") { byte -> "%02x".format(byte) }
Timber.d("Realm encryption key : $key")
// Check if we have data
Timber.e("Realm is empty: ${it.isEmpty}")
Timber.d("Realm has CryptoMetadataEntity: ${it.where<CryptoMetadataEntity>().count()}")
Timber.d("Realm has CryptoRoomEntity: ${it.where<CryptoRoomEntity>().count()}")
Timber.d("Realm has DeviceInfoEntity: ${it.where<DeviceInfoEntity>().count()}")
Timber.d("Realm has KeysBackupDataEntity: ${it.where<KeysBackupDataEntity>().count()}")
Timber.d("Realm has OlmInboundGroupSessionEntity: ${it.where<OlmInboundGroupSessionEntity>().count()}")
Timber.d("Realm has OlmSessionEntity: ${it.where<OlmSessionEntity>().count()}")
Timber.d("Realm has UserEntity: ${it.where<UserEntity>().count()}")
Timber.d("Realm has KeyInfoEntity: ${it.where<KeyInfoEntity>().count()}")
Timber.d("Realm has CrossSigningInfoEntity: ${it.where<CrossSigningInfoEntity>().count()}")
Timber.d("Realm has TrustLevelEntity: ${it.where<TrustLevelEntity>().count()}")
Timber.d("Realm has GossipingEventEntity: ${it.where<GossipingEventEntity>().count()}")
Timber.d("Realm has IncomingGossipingRequestEntity: ${it.where<IncomingGossipingRequestEntity>().count()}")
Timber.d("Realm has OutgoingGossipingRequestEntity: ${it.where<OutgoingGossipingRequestEntity>().count()}")
Timber.d("Realm has MyDeviceLastSeenInfoEntity: ${it.where<MyDeviceLastSeenInfoEntity>().count()}")
}
}
}

View File

@ -238,4 +238,9 @@ interface Session :
} }
val sharedSecretStorageService: SharedSecretStorageService val sharedSecretStorageService: SharedSecretStorageService
/**
* Maintenance API, allows to print outs info on DB size to logcat
*/
fun logDbUsageInfo()
} }

View File

@ -18,6 +18,7 @@ package org.matrix.android.sdk.api.session.crypto
import android.content.Context import android.content.Context
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.paging.PagedList
import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.MatrixCallback
import org.matrix.android.sdk.api.listeners.ProgressListener import org.matrix.android.sdk.api.listeners.ProgressListener
import org.matrix.android.sdk.api.session.crypto.crosssigning.CrossSigningService import org.matrix.android.sdk.api.session.crypto.crosssigning.CrossSigningService
@ -40,6 +41,7 @@ import org.matrix.android.sdk.internal.crypto.model.event.RoomKeyWithHeldContent
import org.matrix.android.sdk.internal.crypto.model.rest.DeviceInfo import org.matrix.android.sdk.internal.crypto.model.rest.DeviceInfo
import org.matrix.android.sdk.internal.crypto.model.rest.DevicesListResponse import org.matrix.android.sdk.internal.crypto.model.rest.DevicesListResponse
import org.matrix.android.sdk.internal.crypto.model.rest.RoomKeyRequestBody import org.matrix.android.sdk.internal.crypto.model.rest.RoomKeyRequestBody
import kotlin.jvm.Throws
interface CryptoService { interface CryptoService {
@ -142,12 +144,17 @@ interface CryptoService {
fun removeSessionListener(listener: NewSessionListener) fun removeSessionListener(listener: NewSessionListener)
fun getOutgoingRoomKeyRequests(): List<OutgoingRoomKeyRequest> fun getOutgoingRoomKeyRequests(): List<OutgoingRoomKeyRequest>
fun getOutgoingRoomKeyRequestsPaged(): LiveData<PagedList<OutgoingRoomKeyRequest>>
fun getIncomingRoomKeyRequests(): List<IncomingRoomKeyRequest> fun getIncomingRoomKeyRequests(): List<IncomingRoomKeyRequest>
fun getIncomingRoomKeyRequestsPaged(): LiveData<PagedList<IncomingRoomKeyRequest>>
fun getGossipingEventsTrail(): List<Event> fun getGossipingEventsTrail(): LiveData<PagedList<Event>>
fun getGossipingEvents(): List<Event>
// For testing shared session // For testing shared session
fun getSharedWithInfo(roomId: String?, sessionId: String): MXUsersDevicesMap<Int> fun getSharedWithInfo(roomId: String?, sessionId: String): MXUsersDevicesMap<Int>
fun getWithHeldMegolmSession(roomId: String, sessionId: String): RoomKeyWithHeldContent? fun getWithHeldMegolmSession(roomId: String, sessionId: String): RoomKeyWithHeldContent?
fun logDbUsageInfo()
} }

View File

@ -35,6 +35,22 @@ interface RoomService {
fun createRoom(createRoomParams: CreateRoomParams, fun createRoom(createRoomParams: CreateRoomParams,
callback: MatrixCallback<String>): Cancelable callback: MatrixCallback<String>): Cancelable
/**
* Create a direct room asynchronously. This is a facility method to create a direct room with the necessary parameters
*/
fun createDirectRoom(otherUserId: String,
callback: MatrixCallback<String>): Cancelable {
return createRoom(
CreateRoomParams()
.apply {
invitedUserIds.add(otherUserId)
setDirectMessage()
enableEncryptionIfInvitedUsersSupportIt = true
},
callback
)
}
/** /**
* Join a room by id * Join a room by id
* @param roomIdOrAlias the roomId or the room alias of the room to join * @param roomIdOrAlias the roomId or the room alias of the room to join
@ -113,5 +129,16 @@ interface RoomService {
*/ */
fun getChangeMembershipsLive(): LiveData<Map<String, ChangeMembershipState>> fun getChangeMembershipsLive(): LiveData<Map<String, ChangeMembershipState>>
fun getExistingDirectRoomWithUser(otherUserId: String): Room? /**
* Return the roomId of an existing DM with the other user, or null if such room does not exist
* A room is a DM if:
* - it is listed in the `m.direct` account data
* - the current user has joined the room
* - the other user is invited or has joined the room
* - it has exactly 2 members
* Note:
* - the returning room can be encrypted or not
* - the power level of the users are not taken into account. Normally in a DM, the 2 members are admins of the room
*/
fun getExistingDirectRoomWithUser(otherUserId: String): String?
} }

View File

@ -63,8 +63,13 @@ data class RoomSummary constructor(
val hasNewMessages: Boolean val hasNewMessages: Boolean
get() = notificationCount != 0 get() = notificationCount != 0
val isLowPriority: Boolean
get() = hasTag(RoomTag.ROOM_TAG_LOW_PRIORITY)
val isFavorite: Boolean val isFavorite: Boolean
get() = tags.any { it.name == RoomTag.ROOM_TAG_FAVOURITE } get() = hasTag(RoomTag.ROOM_TAG_FAVOURITE)
fun hasTag(tag: String) = tags.any { it.name == tag }
val canStartCall: Boolean val canStartCall: Boolean
get() = joinedMembersCount == 2 get() = joinedMembersCount == 2

View File

@ -16,6 +16,7 @@
package org.matrix.android.sdk.api.session.room.model.create package org.matrix.android.sdk.api.session.room.model.create
import android.net.Uri
import org.matrix.android.sdk.api.session.identity.ThreePid import org.matrix.android.sdk.api.session.identity.ThreePid
import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent
import org.matrix.android.sdk.api.session.room.model.RoomDirectoryVisibility import org.matrix.android.sdk.api.session.room.model.RoomDirectoryVisibility
@ -51,6 +52,11 @@ class CreateRoomParams {
*/ */
var topic: String? = null var topic: String? = null
/**
* If this is not null, the image uri will be sent to the media server and will be set as a room avatar.
*/
var avatarUri: Uri? = null
/** /**
* A list of user IDs to invite to the room. * A list of user IDs to invite to the room.
* This will tell the server to invite everyone in the list to the newly created room. * This will tell the server to invite everyone in the list to the newly created room.

View File

@ -23,7 +23,7 @@ import com.squareup.moshi.JsonClass
data class ReactionInfo( data class ReactionInfo(
@Json(name = "rel_type") override val type: String?, @Json(name = "rel_type") override val type: String?,
@Json(name = "event_id") override val eventId: String, @Json(name = "event_id") override val eventId: String,
val key: String, @Json(name = "key") val key: String,
// always null for reaction // always null for reaction
@Json(name = "m.in_reply_to") override val inReplyTo: ReplyToContent? = null, @Json(name = "m.in_reply_to") override val inReplyTo: ReplyToContent? = null,
@Json(name = "option") override val option: Int? = null @Json(name = "option") override val option: Int? = null

View File

@ -123,11 +123,6 @@ interface SendService {
*/ */
fun deleteFailedEcho(localEcho: TimelineEvent) fun deleteFailedEcho(localEcho: TimelineEvent)
/**
* Delete all the events in one of the sending states
*/
fun clearSendingQueue()
/** /**
* Cancel sending a specific event. It has to be in one of the sending states * Cancel sending a specific event. It has to be in one of the sending states
*/ */

View File

@ -58,6 +58,11 @@ interface StateService {
*/ */
fun updateAvatar(avatarUri: Uri, fileName: String, callback: MatrixCallback<Unit>): Cancelable fun updateAvatar(avatarUri: Uri, fileName: String, callback: MatrixCallback<Unit>): Cancelable
/**
* Delete the avatar of the room
*/
fun deleteAvatar(callback: MatrixCallback<Unit>): Cancelable
fun sendStateEvent(eventType: String, stateKey: String?, body: JsonDict, callback: MatrixCallback<Unit>): Cancelable fun sendStateEvent(eventType: String, stateKey: String?, body: JsonDict, callback: MatrixCallback<Unit>): Cancelable
fun getStateEvent(eventType: String, stateKey: QueryStringValue = QueryStringValue.NoCondition): Event? fun getStateEvent(eventType: String, stateKey: QueryStringValue = QueryStringValue.NoCondition): Event?

View File

@ -0,0 +1,61 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.crypto
import com.zhuinden.monarchy.Monarchy
import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.internal.database.model.EventEntity
import org.matrix.android.sdk.internal.database.model.EventEntityFields
import org.matrix.android.sdk.internal.database.query.whereType
import org.matrix.android.sdk.internal.di.SessionDatabase
import org.matrix.android.sdk.internal.session.room.membership.RoomMemberHelper
import org.matrix.android.sdk.internal.util.fetchCopied
import javax.inject.Inject
/**
* The crypto module needs some information regarding rooms that are stored
* in the session DB, this class encapsulate this functionality
*/
internal class CryptoSessionInfoProvider @Inject constructor(
@SessionDatabase private val monarchy: Monarchy
) {
fun isRoomEncrypted(roomId: String): Boolean {
val encryptionEvent = monarchy.fetchCopied { realm ->
EventEntity.whereType(realm, roomId = roomId, type = EventType.STATE_ROOM_ENCRYPTION)
.contains(EventEntityFields.CONTENT, "\"algorithm\":\"$MXCRYPTO_ALGORITHM_MEGOLM\"")
.isNotNull(EventEntityFields.STATE_KEY) // should be an empty key
.findFirst()
}
return encryptionEvent != null
}
/**
* @param allActive if true return joined as well as invited, if false, only joined
*/
fun getRoomUserIds(roomId: String, allActive: Boolean): List<String> {
var userIds: List<String> = emptyList()
monarchy.doWithRealm { realm ->
userIds = if (allActive) {
RoomMemberHelper(realm, roomId).getActiveRoomMemberIds()
} else {
RoomMemberHelper(realm, roomId).getJoinedRoomMemberIds()
}
}
return userIds
}
}

View File

@ -17,12 +17,10 @@
package org.matrix.android.sdk.internal.crypto package org.matrix.android.sdk.internal.crypto
import android.content.Context import android.content.Context
import android.os.Handler
import android.os.Looper
import androidx.annotation.VisibleForTesting import androidx.annotation.VisibleForTesting
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.paging.PagedList
import com.squareup.moshi.Types import com.squareup.moshi.Types
import com.zhuinden.monarchy.Monarchy
import dagger.Lazy import dagger.Lazy
import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@ -51,9 +49,7 @@ import org.matrix.android.sdk.api.session.room.model.Membership
import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibility import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibility
import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibilityContent import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibilityContent
import org.matrix.android.sdk.api.session.room.model.RoomMemberContent import org.matrix.android.sdk.api.session.room.model.RoomMemberContent
import org.matrix.android.sdk.internal.crypto.actions.EnsureOlmSessionsForDevicesAction
import org.matrix.android.sdk.internal.crypto.actions.MegolmSessionDataImporter import org.matrix.android.sdk.internal.crypto.actions.MegolmSessionDataImporter
import org.matrix.android.sdk.internal.crypto.actions.MessageEncrypter
import org.matrix.android.sdk.internal.crypto.actions.SetDeviceVerificationAction import org.matrix.android.sdk.internal.crypto.actions.SetDeviceVerificationAction
import org.matrix.android.sdk.internal.crypto.algorithms.IMXEncrypting import org.matrix.android.sdk.internal.crypto.algorithms.IMXEncrypting
import org.matrix.android.sdk.internal.crypto.algorithms.IMXWithHeldExtension import org.matrix.android.sdk.internal.crypto.algorithms.IMXWithHeldExtension
@ -68,7 +64,6 @@ import org.matrix.android.sdk.internal.crypto.model.MXDeviceInfo
import org.matrix.android.sdk.internal.crypto.model.MXEncryptEventContentResult import org.matrix.android.sdk.internal.crypto.model.MXEncryptEventContentResult
import org.matrix.android.sdk.internal.crypto.model.MXUsersDevicesMap import org.matrix.android.sdk.internal.crypto.model.MXUsersDevicesMap
import org.matrix.android.sdk.internal.crypto.model.event.EncryptedEventContent import org.matrix.android.sdk.internal.crypto.model.event.EncryptedEventContent
import org.matrix.android.sdk.internal.crypto.model.event.OlmEventContent
import org.matrix.android.sdk.internal.crypto.model.event.RoomKeyContent import org.matrix.android.sdk.internal.crypto.model.event.RoomKeyContent
import org.matrix.android.sdk.internal.crypto.model.event.RoomKeyWithHeldContent import org.matrix.android.sdk.internal.crypto.model.event.RoomKeyWithHeldContent
import org.matrix.android.sdk.internal.crypto.model.event.SecretSendEventContent import org.matrix.android.sdk.internal.crypto.model.event.SecretSendEventContent
@ -82,21 +77,15 @@ import org.matrix.android.sdk.internal.crypto.tasks.DeleteDeviceTask
import org.matrix.android.sdk.internal.crypto.tasks.DeleteDeviceWithUserPasswordTask import org.matrix.android.sdk.internal.crypto.tasks.DeleteDeviceWithUserPasswordTask
import org.matrix.android.sdk.internal.crypto.tasks.GetDeviceInfoTask import org.matrix.android.sdk.internal.crypto.tasks.GetDeviceInfoTask
import org.matrix.android.sdk.internal.crypto.tasks.GetDevicesTask import org.matrix.android.sdk.internal.crypto.tasks.GetDevicesTask
import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask
import org.matrix.android.sdk.internal.crypto.tasks.SetDeviceNameTask import org.matrix.android.sdk.internal.crypto.tasks.SetDeviceNameTask
import org.matrix.android.sdk.internal.crypto.tasks.UploadKeysTask import org.matrix.android.sdk.internal.crypto.tasks.UploadKeysTask
import org.matrix.android.sdk.internal.crypto.verification.DefaultVerificationService import org.matrix.android.sdk.internal.crypto.verification.DefaultVerificationService
import org.matrix.android.sdk.internal.database.model.EventEntity
import org.matrix.android.sdk.internal.database.model.EventEntityFields
import org.matrix.android.sdk.internal.database.query.whereType
import org.matrix.android.sdk.internal.di.DeviceId import org.matrix.android.sdk.internal.di.DeviceId
import org.matrix.android.sdk.internal.di.MoshiProvider import org.matrix.android.sdk.internal.di.MoshiProvider
import org.matrix.android.sdk.internal.di.SessionDatabase
import org.matrix.android.sdk.internal.di.UserId import org.matrix.android.sdk.internal.di.UserId
import org.matrix.android.sdk.internal.extensions.foldToCallback import org.matrix.android.sdk.internal.extensions.foldToCallback
import org.matrix.android.sdk.internal.session.SessionScope import org.matrix.android.sdk.internal.session.SessionScope
import org.matrix.android.sdk.internal.session.room.membership.LoadRoomMembersTask import org.matrix.android.sdk.internal.session.room.membership.LoadRoomMembersTask
import org.matrix.android.sdk.internal.session.room.membership.RoomMemberHelper
import org.matrix.android.sdk.internal.session.sync.model.SyncResponse import org.matrix.android.sdk.internal.session.sync.model.SyncResponse
import org.matrix.android.sdk.internal.task.TaskExecutor import org.matrix.android.sdk.internal.task.TaskExecutor
import org.matrix.android.sdk.internal.task.TaskThread import org.matrix.android.sdk.internal.task.TaskThread
@ -104,11 +93,11 @@ import org.matrix.android.sdk.internal.task.configureWith
import org.matrix.android.sdk.internal.task.launchToCallback import org.matrix.android.sdk.internal.task.launchToCallback
import org.matrix.android.sdk.internal.util.JsonCanonicalizer import org.matrix.android.sdk.internal.util.JsonCanonicalizer
import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers
import org.matrix.android.sdk.internal.util.fetchCopied
import org.matrix.olm.OlmManager import org.matrix.olm.OlmManager
import timber.log.Timber import timber.log.Timber
import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicBoolean
import javax.inject.Inject import javax.inject.Inject
import kotlin.jvm.Throws
import kotlin.math.max import kotlin.math.max
/** /**
@ -171,28 +160,16 @@ internal class DefaultCryptoService @Inject constructor(
private val setDeviceNameTask: SetDeviceNameTask, private val setDeviceNameTask: SetDeviceNameTask,
private val uploadKeysTask: UploadKeysTask, private val uploadKeysTask: UploadKeysTask,
private val loadRoomMembersTask: LoadRoomMembersTask, private val loadRoomMembersTask: LoadRoomMembersTask,
@SessionDatabase private val monarchy: Monarchy, private val cryptoSessionInfoProvider: CryptoSessionInfoProvider,
private val coroutineDispatchers: MatrixCoroutineDispatchers, private val coroutineDispatchers: MatrixCoroutineDispatchers,
private val taskExecutor: TaskExecutor, private val taskExecutor: TaskExecutor,
private val cryptoCoroutineScope: CoroutineScope, private val cryptoCoroutineScope: CoroutineScope,
private val ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction, private val eventDecryptor: EventDecryptor
private val sendToDeviceTask: SendToDeviceTask,
private val messageEncrypter: MessageEncrypter
) : CryptoService { ) : CryptoService {
init {
verificationService.cryptoService = this
}
private val uiHandler = Handler(Looper.getMainLooper())
private val isStarting = AtomicBoolean(false) private val isStarting = AtomicBoolean(false)
private val isStarted = AtomicBoolean(false) private val isStarted = AtomicBoolean(false)
// The date of the last time we forced establishment
// of a new session for each user:device.
private val lastNewSessionForcedDates = MXUsersDevicesMap<Long>()
fun onStateEvent(roomId: String, event: Event) { fun onStateEvent(roomId: String, event: Event) {
when (event.getClearType()) { when (event.getClearType()) {
EventType.STATE_ROOM_ENCRYPTION -> onRoomEncryptionEvent(roomId, event) EventType.STATE_ROOM_ENCRYPTION -> onRoomEncryptionEvent(roomId, event)
@ -209,6 +186,8 @@ internal class DefaultCryptoService @Inject constructor(
} }
} }
val gossipingBuffer = mutableListOf<Event>()
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)) {
@ -335,6 +314,10 @@ internal class DefaultCryptoService @Inject constructor(
} }
// Just update // Just update
fetchDevicesList(NoOpMatrixCallback()) fetchDevicesList(NoOpMatrixCallback())
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
cryptoStore.tidyUpDataBase()
}
} }
fun ensureDevice() { fun ensureDevice() {
@ -410,7 +393,7 @@ internal class DefaultCryptoService @Inject constructor(
*/ */
fun close() = runBlocking(coroutineDispatchers.crypto) { fun close() = runBlocking(coroutineDispatchers.crypto) {
cryptoCoroutineScope.coroutineContext.cancelChildren(CancellationException("Closing crypto module")) cryptoCoroutineScope.coroutineContext.cancelChildren(CancellationException("Closing crypto module"))
incomingGossipingRequestManager.close()
olmDevice.release() olmDevice.release()
cryptoStore.close() cryptoStore.close()
} }
@ -452,6 +435,13 @@ internal class DefaultCryptoService @Inject constructor(
incomingGossipingRequestManager.processReceivedGossipingRequests() incomingGossipingRequestManager.processReceivedGossipingRequests()
} }
} }
tryOrNull {
gossipingBuffer.toList().let {
cryptoStore.saveGossipingEvents(it)
}
gossipingBuffer.clear()
}
} }
} }
@ -612,13 +602,7 @@ internal class DefaultCryptoService @Inject constructor(
* @return true if the room is encrypted with algorithm MXCRYPTO_ALGORITHM_MEGOLM * @return true if the room is encrypted with algorithm MXCRYPTO_ALGORITHM_MEGOLM
*/ */
override fun isRoomEncrypted(roomId: String): Boolean { override fun isRoomEncrypted(roomId: String): Boolean {
val encryptionEvent = monarchy.fetchCopied { realm -> return cryptoSessionInfoProvider.isRoomEncrypted(roomId)
EventEntity.whereType(realm, roomId = roomId, type = EventType.STATE_ROOM_ENCRYPTION)
.contains(EventEntityFields.CONTENT, "\"algorithm\":\"$MXCRYPTO_ALGORITHM_MEGOLM\"")
.isNotNull(EventEntityFields.STATE_KEY)
.findFirst()
}
return encryptionEvent != null
} }
/** /**
@ -660,11 +644,8 @@ internal class DefaultCryptoService @Inject constructor(
eventType: String, eventType: String,
roomId: String, roomId: String,
callback: MatrixCallback<MXEncryptEventContentResult>) { callback: MatrixCallback<MXEncryptEventContentResult>) {
// moved to crypto scope to have uptodate values
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
// if (!isStarted()) {
// Timber.v("## CRYPTO | encryptEventContent() : wait after e2e init")
// internalStart(false)
// }
val userIds = getRoomUserIds(roomId) val userIds = getRoomUserIds(roomId)
var alg = roomEncryptorsStore.get(roomId) var alg = roomEncryptorsStore.get(roomId)
if (alg == null) { if (alg == null) {
@ -720,14 +701,7 @@ 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>) {
cryptoCoroutineScope.launch { eventDecryptor.decryptEventAsync(event, timeline, callback)
val result = runCatching {
withContext(coroutineDispatchers.crypto) {
internalDecryptEvent(event, timeline)
}
}
result.foldToCallback(callback)
}
} }
/** /**
@ -739,42 +713,7 @@ internal class DefaultCryptoService @Inject constructor(
*/ */
@Throws(MXCryptoError::class) @Throws(MXCryptoError::class)
private fun internalDecryptEvent(event: Event, timeline: String): MXEventDecryptionResult { private fun internalDecryptEvent(event: Event, timeline: String): MXEventDecryptionResult {
val eventContent = event.content return eventDecryptor.decryptEvent(event, timeline)
if (eventContent == null) {
Timber.e("## CRYPTO | decryptEvent : empty event content")
throw MXCryptoError.Base(MXCryptoError.ErrorType.BAD_ENCRYPTED_MESSAGE, MXCryptoError.BAD_ENCRYPTED_MESSAGE_REASON)
} else {
val algorithm = eventContent["algorithm"]?.toString()
val alg = roomDecryptorProvider.getOrCreateRoomDecryptor(event.roomId, algorithm)
if (alg == null) {
val reason = String.format(MXCryptoError.UNABLE_TO_DECRYPT_REASON, event.eventId, algorithm)
Timber.e("## CRYPTO | decryptEvent() : $reason")
throw MXCryptoError.Base(MXCryptoError.ErrorType.UNABLE_TO_DECRYPT, reason)
} else {
try {
return alg.decryptEvent(event, timeline)
} catch (mxCryptoError: MXCryptoError) {
Timber.d("## CRYPTO | internalDecryptEvent : Failed to decrypt ${event.eventId} reason: $mxCryptoError")
if (algorithm == MXCRYPTO_ALGORITHM_OLM) {
if (mxCryptoError is MXCryptoError.Base
&& mxCryptoError.errorType == MXCryptoError.ErrorType.BAD_ENCRYPTED_MESSAGE) {
// need to find sending device
val olmContent = event.content.toModel<OlmEventContent>()
cryptoStore.getUserDevices(event.senderId ?: "")
?.values
?.firstOrNull { it.identityKey() == olmContent?.senderKey }
?.let {
markOlmSessionForUnwedging(event.senderId ?: "", it)
}
?: run {
Timber.v("## CRYPTO | markOlmSessionForUnwedging() : Failed to find sender crypto device")
}
}
}
throw mxCryptoError
}
}
}
} }
/** /**
@ -796,19 +735,19 @@ internal class DefaultCryptoService @Inject constructor(
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
when (event.getClearType()) { when (event.getClearType()) {
EventType.ROOM_KEY, EventType.FORWARDED_ROOM_KEY -> { EventType.ROOM_KEY, EventType.FORWARDED_ROOM_KEY -> {
cryptoStore.saveGossipingEvent(event) gossipingBuffer.add(event)
// Keys are imported directly, not waiting for end of sync // Keys are imported directly, not waiting for end of sync
onRoomKeyEvent(event) onRoomKeyEvent(event)
} }
EventType.REQUEST_SECRET, EventType.REQUEST_SECRET,
EventType.ROOM_KEY_REQUEST -> { EventType.ROOM_KEY_REQUEST -> {
// save audit trail // save audit trail
cryptoStore.saveGossipingEvent(event) gossipingBuffer.add(event)
// Requests are stacked, and will be handled one by one at the end of the sync (onSyncComplete) // Requests are stacked, and will be handled one by one at the end of the sync (onSyncComplete)
incomingGossipingRequestManager.onGossipingRequestEvent(event) incomingGossipingRequestManager.onGossipingRequestEvent(event)
} }
EventType.SEND_SECRET -> { EventType.SEND_SECRET -> {
cryptoStore.saveGossipingEvent(event) gossipingBuffer.add(event)
onSecretSendReceived(event) onSecretSendReceived(event)
} }
EventType.ROOM_KEY_WITHHELD -> { EventType.ROOM_KEY_WITHHELD -> {
@ -828,7 +767,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
Timber.v("## CRYPTO | GOSSIP onRoomKeyEvent() : type<${event.type}> , sessionId<${roomKeyContent.sessionId}>") Timber.v("## CRYPTO | GOSSIP onRoomKeyEvent() : type<${event.getClearType()}> , sessionId<${roomKeyContent.sessionId}>")
if (roomKeyContent.roomId.isNullOrEmpty() || roomKeyContent.algorithm.isNullOrEmpty()) { if (roomKeyContent.roomId.isNullOrEmpty() || roomKeyContent.algorithm.isNullOrEmpty()) {
Timber.e("## CRYPTO | GOSSIP onRoomKeyEvent() : missing fields") Timber.e("## CRYPTO | GOSSIP onRoomKeyEvent() : missing fields")
return return
@ -935,19 +874,9 @@ internal class DefaultCryptoService @Inject constructor(
} }
private fun getRoomUserIds(roomId: String): List<String> { private fun getRoomUserIds(roomId: String): List<String> {
var userIds: List<String> = emptyList()
monarchy.doWithRealm { realm ->
// Check whether the event content must be encrypted for the invited members.
val encryptForInvitedMembers = isEncryptionEnabledForInvitedUser() val encryptForInvitedMembers = isEncryptionEnabledForInvitedUser()
&& shouldEncryptForInvitedMembers(roomId) && shouldEncryptForInvitedMembers(roomId)
return cryptoSessionInfoProvider.getRoomUserIds(roomId, encryptForInvitedMembers)
userIds = if (encryptForInvitedMembers) {
RoomMemberHelper(realm, roomId).getActiveRoomMemberIds()
} else {
RoomMemberHelper(realm, roomId).getJoinedRoomMemberIds()
}
}
return userIds
} }
/** /**
@ -1257,38 +1186,38 @@ internal class DefaultCryptoService @Inject constructor(
incomingGossipingRequestManager.removeRoomKeysRequestListener(listener) incomingGossipingRequestManager.removeRoomKeysRequestListener(listener)
} }
private fun markOlmSessionForUnwedging(senderId: String, deviceInfo: CryptoDeviceInfo) { // private fun markOlmSessionForUnwedging(senderId: String, deviceInfo: CryptoDeviceInfo) {
val deviceKey = deviceInfo.identityKey() // val deviceKey = deviceInfo.identityKey()
//
val lastForcedDate = lastNewSessionForcedDates.getObject(senderId, deviceKey) ?: 0 // val lastForcedDate = lastNewSessionForcedDates.getObject(senderId, deviceKey) ?: 0
val now = System.currentTimeMillis() // val now = System.currentTimeMillis()
if (now - lastForcedDate < CRYPTO_MIN_FORCE_SESSION_PERIOD_MILLIS) { // if (now - lastForcedDate < CRYPTO_MIN_FORCE_SESSION_PERIOD_MILLIS) {
Timber.d("## CRYPTO | markOlmSessionForUnwedging: New session already forced with device at $lastForcedDate. Not forcing another") // Timber.d("## CRYPTO | markOlmSessionForUnwedging: New session already forced with device at $lastForcedDate. Not forcing another")
return // return
} // }
//
Timber.d("## CRYPTO | markOlmSessionForUnwedging from $senderId:${deviceInfo.deviceId}") // Timber.d("## CRYPTO | markOlmSessionForUnwedging from $senderId:${deviceInfo.deviceId}")
lastNewSessionForcedDates.setObject(senderId, deviceKey, now) // lastNewSessionForcedDates.setObject(senderId, deviceKey, now)
//
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { // cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
ensureOlmSessionsForDevicesAction.handle(mapOf(senderId to listOf(deviceInfo)), force = true) // ensureOlmSessionsForDevicesAction.handle(mapOf(senderId to listOf(deviceInfo)), force = true)
//
// Now send a blank message on that session so the other side knows about it. // // Now send a blank message on that session so the other side knows about it.
// (The keyshare request is sent in the clear so that won't do) // // (The keyshare request is sent in the clear so that won't do)
// We send this first such that, as long as the toDevice messages arrive in the // // We send this first such that, as long as the toDevice messages arrive in the
// same order we sent them, the other end will get this first, set up the new session, // // same order we sent them, the other end will get this first, set up the new session,
// then get the keyshare request and send the key over this new session (because it // // then get the keyshare request and send the key over this new session (because it
// is the session it has most recently received a message on). // // is the session it has most recently received a message on).
val payloadJson = mapOf<String, Any>("type" to EventType.DUMMY) // val payloadJson = mapOf<String, Any>("type" to EventType.DUMMY)
//
val encodedPayload = messageEncrypter.encryptMessage(payloadJson, listOf(deviceInfo)) // val encodedPayload = messageEncrypter.encryptMessage(payloadJson, listOf(deviceInfo))
val sendToDeviceMap = MXUsersDevicesMap<Any>() // val sendToDeviceMap = MXUsersDevicesMap<Any>()
sendToDeviceMap.setObject(senderId, deviceInfo.deviceId, encodedPayload) // sendToDeviceMap.setObject(senderId, deviceInfo.deviceId, encodedPayload)
Timber.v("## CRYPTO | markOlmSessionForUnwedging() : sending to $senderId:${deviceInfo.deviceId}") // Timber.v("## CRYPTO | markOlmSessionForUnwedging() : sending to $senderId:${deviceInfo.deviceId}")
val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, sendToDeviceMap) // val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, sendToDeviceMap)
sendToDeviceTask.execute(sendToDeviceParams) // sendToDeviceTask.execute(sendToDeviceParams)
} // }
} // }
/** /**
* Provides the list of unknown devices * Provides the list of unknown devices
@ -1339,14 +1268,26 @@ internal class DefaultCryptoService @Inject constructor(
return cryptoStore.getOutgoingRoomKeyRequests() return cryptoStore.getOutgoingRoomKeyRequests()
} }
override fun getOutgoingRoomKeyRequestsPaged(): LiveData<PagedList<OutgoingRoomKeyRequest>> {
return cryptoStore.getOutgoingRoomKeyRequestsPaged()
}
override fun getIncomingRoomKeyRequestsPaged(): LiveData<PagedList<IncomingRoomKeyRequest>> {
return cryptoStore.getIncomingRoomKeyRequestsPaged()
}
override fun getIncomingRoomKeyRequests(): List<IncomingRoomKeyRequest> { override fun getIncomingRoomKeyRequests(): List<IncomingRoomKeyRequest> {
return cryptoStore.getIncomingRoomKeyRequests() return cryptoStore.getIncomingRoomKeyRequests()
} }
override fun getGossipingEventsTrail(): List<Event> { override fun getGossipingEventsTrail(): LiveData<PagedList<Event>> {
return cryptoStore.getGossipingEventsTrail() return cryptoStore.getGossipingEventsTrail()
} }
override fun getGossipingEvents(): List<Event> {
return cryptoStore.getGossipingEvents()
}
override fun getSharedWithInfo(roomId: String?, sessionId: String): MXUsersDevicesMap<Int> { override fun getSharedWithInfo(roomId: String?, sessionId: String): MXUsersDevicesMap<Int> {
return cryptoStore.getSharedWithInfo(roomId, sessionId) return cryptoStore.getSharedWithInfo(roomId, sessionId)
} }
@ -1354,6 +1295,11 @@ internal class DefaultCryptoService @Inject constructor(
override fun getWithHeldMegolmSession(roomId: String, sessionId: String): RoomKeyWithHeldContent? { override fun getWithHeldMegolmSession(roomId: String, sessionId: String): RoomKeyWithHeldContent? {
return cryptoStore.getWithHeldMegolmSession(roomId, sessionId) return cryptoStore.getWithHeldMegolmSession(roomId, sessionId)
} }
override fun logDbUsageInfo() {
cryptoStore.logDbUsageInfo()
}
/* ========================================================================================== /* ==========================================================================================
* For test only * For test only
* ========================================================================================== */ * ========================================================================================== */

View File

@ -377,7 +377,7 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
} }
// Update devices trust for these users // Update devices trust for these users
dispatchDeviceChange(downloadUsers) // dispatchDeviceChange(downloadUsers)
return onKeysDownloadSucceed(filteredUsers, response.failures) return onKeysDownloadSucceed(filteredUsers, response.failures)
} }

View File

@ -0,0 +1,169 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.crypto
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.matrix.android.sdk.api.MatrixCallback
import org.matrix.android.sdk.api.session.crypto.MXCryptoError
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.internal.crypto.actions.EnsureOlmSessionsForDevicesAction
import org.matrix.android.sdk.internal.crypto.actions.MessageEncrypter
import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo
import org.matrix.android.sdk.internal.crypto.model.MXUsersDevicesMap
import org.matrix.android.sdk.internal.crypto.model.event.OlmEventContent
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask
import org.matrix.android.sdk.internal.extensions.foldToCallback
import org.matrix.android.sdk.internal.session.SessionScope
import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers
import timber.log.Timber
import javax.inject.Inject
import kotlin.jvm.Throws
@SessionScope
internal class EventDecryptor @Inject constructor(
private val cryptoCoroutineScope: CoroutineScope,
private val coroutineDispatchers: MatrixCoroutineDispatchers,
private val roomDecryptorProvider: RoomDecryptorProvider,
private val messageEncrypter: MessageEncrypter,
private val sendToDeviceTask: SendToDeviceTask,
private val ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction,
private val cryptoStore: IMXCryptoStore
) {
// The date of the last time we forced establishment
// of a new session for each user:device.
private val lastNewSessionForcedDates = MXUsersDevicesMap<Long>()
/**
* Decrypt an event
*
* @param event the raw event.
* @param timeline the id of the timeline where the event is decrypted. It is used to prevent replay attack.
* @return the MXEventDecryptionResult data, or throw in case of error
*/
@Throws(MXCryptoError::class)
fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult {
return internalDecryptEvent(event, timeline)
}
/**
* Decrypt an event asynchronously
*
* @param event the raw event.
* @param timeline the id of the timeline where the event is decrypted. It is used to prevent replay attack.
* @param callback the callback to return data or null
*/
fun decryptEventAsync(event: Event, timeline: String, callback: MatrixCallback<MXEventDecryptionResult>) {
// is it needed to do that on the crypto scope??
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
runCatching {
internalDecryptEvent(event, timeline)
}.foldToCallback(callback)
}
}
/**
* Decrypt an event
*
* @param event the raw event.
* @param timeline the id of the timeline where the event is decrypted. It is used to prevent replay attack.
* @return the MXEventDecryptionResult data, or null in case of error
*/
@Throws(MXCryptoError::class)
private fun internalDecryptEvent(event: Event, timeline: String): MXEventDecryptionResult {
val eventContent = event.content
if (eventContent == null) {
Timber.e("## CRYPTO | decryptEvent : empty event content")
throw MXCryptoError.Base(MXCryptoError.ErrorType.BAD_ENCRYPTED_MESSAGE, MXCryptoError.BAD_ENCRYPTED_MESSAGE_REASON)
} else {
val algorithm = eventContent["algorithm"]?.toString()
val alg = roomDecryptorProvider.getOrCreateRoomDecryptor(event.roomId, algorithm)
if (alg == null) {
val reason = String.format(MXCryptoError.UNABLE_TO_DECRYPT_REASON, event.eventId, algorithm)
Timber.e("## CRYPTO | decryptEvent() : $reason")
throw MXCryptoError.Base(MXCryptoError.ErrorType.UNABLE_TO_DECRYPT, reason)
} else {
try {
return alg.decryptEvent(event, timeline)
} catch (mxCryptoError: MXCryptoError) {
Timber.d("## CRYPTO | internalDecryptEvent : Failed to decrypt ${event.eventId} reason: $mxCryptoError")
if (algorithm == MXCRYPTO_ALGORITHM_OLM) {
if (mxCryptoError is MXCryptoError.Base
&& mxCryptoError.errorType == MXCryptoError.ErrorType.BAD_ENCRYPTED_MESSAGE) {
// need to find sending device
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
val olmContent = event.content.toModel<OlmEventContent>()
cryptoStore.getUserDevices(event.senderId ?: "")
?.values
?.firstOrNull { it.identityKey() == olmContent?.senderKey }
?.let {
markOlmSessionForUnwedging(event.senderId ?: "", it)
}
?: run {
Timber.v("## CRYPTO | markOlmSessionForUnwedging() : Failed to find sender crypto device")
}
}
}
}
throw mxCryptoError
}
}
}
}
// coroutineDispatchers.crypto scope
private fun markOlmSessionForUnwedging(senderId: String, deviceInfo: CryptoDeviceInfo) {
val deviceKey = deviceInfo.identityKey()
val lastForcedDate = lastNewSessionForcedDates.getObject(senderId, deviceKey) ?: 0
val now = System.currentTimeMillis()
if (now - lastForcedDate < DefaultCryptoService.CRYPTO_MIN_FORCE_SESSION_PERIOD_MILLIS) {
Timber.d("## CRYPTO | markOlmSessionForUnwedging: New session already forced with device at $lastForcedDate. Not forcing another")
return
}
Timber.d("## CRYPTO | markOlmSessionForUnwedging from $senderId:${deviceInfo.deviceId}")
lastNewSessionForcedDates.setObject(senderId, deviceKey, now)
// offload this from crypto thread (?)
cryptoCoroutineScope.launch(coroutineDispatchers.computation) {
ensureOlmSessionsForDevicesAction.handle(mapOf(senderId to listOf(deviceInfo)), force = true)
// Now send a blank message on that session so the other side knows about it.
// (The keyshare request is sent in the clear so that won't do)
// We send this first such that, as long as the toDevice messages arrive in the
// same order we sent them, the other end will get this first, set up the new session,
// then get the keyshare request and send the key over this new session (because it
// is the session it has most recently received a message on).
val payloadJson = mapOf<String, Any>("type" to EventType.DUMMY)
val encodedPayload = messageEncrypter.encryptMessage(payloadJson, listOf(deviceInfo))
val sendToDeviceMap = MXUsersDevicesMap<Any>()
sendToDeviceMap.setObject(senderId, deviceInfo.deviceId, encodedPayload)
Timber.v("## CRYPTO | markOlmSessionForUnwedging() : sending to $senderId:${deviceInfo.deviceId}")
withContext(coroutineDispatchers.io) {
val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, sendToDeviceMap)
sendToDeviceTask.execute(sendToDeviceParams)
}
}
}
}

View File

@ -0,0 +1,99 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.crypto
import android.util.LruCache
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.internal.crypto.model.OlmInboundGroupSessionWrapper2
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers
import timber.log.Timber
import java.util.Timer
import java.util.TimerTask
import javax.inject.Inject
/**
* Allows to cache and batch store operations on inbound group session store.
* Because it is used in the decrypt flow, that can be called quite rapidly
*/
internal class InboundGroupSessionStore @Inject constructor(
private val store: IMXCryptoStore,
private val cryptoCoroutineScope: CoroutineScope,
private val coroutineDispatchers: MatrixCoroutineDispatchers) {
private data class CacheKey(
val sessionId: String,
val senderKey: String
)
private val sessionCache = object : LruCache<CacheKey, OlmInboundGroupSessionWrapper2>(30) {
override fun entryRemoved(evicted: Boolean, key: CacheKey?, oldValue: OlmInboundGroupSessionWrapper2?, newValue: OlmInboundGroupSessionWrapper2?) {
if (evicted && oldValue != null) {
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
Timber.v("## Inbound: entryRemoved ${oldValue.roomId}-${oldValue.senderKey}")
store.storeInboundGroupSessions(listOf(oldValue))
}
}
}
}
private val timer = Timer()
private var timerTask: TimerTask? = null
private val dirtySession = mutableListOf<OlmInboundGroupSessionWrapper2>()
@Synchronized
fun getInboundGroupSession(sessionId: String, senderKey: String): OlmInboundGroupSessionWrapper2? {
synchronized(sessionCache) {
val known = sessionCache[CacheKey(sessionId, senderKey)]
Timber.v("## Inbound: getInboundGroupSession in cache ${known != null}")
return known ?: store.getInboundGroupSession(sessionId, senderKey)?.also {
Timber.v("## Inbound: getInboundGroupSession cache populate ${it.roomId}")
sessionCache.put(CacheKey(sessionId, senderKey), it)
}
}
}
@Synchronized
fun storeInBoundGroupSession(wrapper: OlmInboundGroupSessionWrapper2) {
Timber.v("## Inbound: getInboundGroupSession mark as dirty ${wrapper.roomId}-${wrapper.senderKey}")
// We want to batch this a bit for performances
dirtySession.add(wrapper)
timerTask?.cancel()
timerTask = object : TimerTask() {
override fun run() {
batchSave()
}
}
timer.schedule(timerTask!!, 2_000)
}
@Synchronized
private fun batchSave() {
val toSave = mutableListOf<OlmInboundGroupSessionWrapper2>().apply { addAll(dirtySession) }
dirtySession.clear()
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
Timber.v("## Inbound: getInboundGroupSession batching save of ${dirtySession.size}")
tryOrNull {
store.storeInboundGroupSessions(toSave)
}
}
}
}

View File

@ -38,6 +38,7 @@ import org.matrix.android.sdk.internal.session.SessionScope
import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers
import org.matrix.android.sdk.internal.worker.WorkerParamsFactory import org.matrix.android.sdk.internal.worker.WorkerParamsFactory
import timber.log.Timber import timber.log.Timber
import java.util.concurrent.Executors
import javax.inject.Inject import javax.inject.Inject
@SessionScope @SessionScope
@ -52,6 +53,7 @@ internal class IncomingGossipingRequestManager @Inject constructor(
private val coroutineDispatchers: MatrixCoroutineDispatchers, private val coroutineDispatchers: MatrixCoroutineDispatchers,
private val cryptoCoroutineScope: CoroutineScope) { private val cryptoCoroutineScope: CoroutineScope) {
private val executor = Executors.newSingleThreadExecutor()
// list of IncomingRoomKeyRequests/IncomingRoomKeyRequestCancellations // list of IncomingRoomKeyRequests/IncomingRoomKeyRequestCancellations
// we received in the current sync. // we received in the current sync.
private val receivedGossipingRequests = ArrayList<IncomingShareRequestCommon>() private val receivedGossipingRequests = ArrayList<IncomingShareRequestCommon>()
@ -64,6 +66,10 @@ internal class IncomingGossipingRequestManager @Inject constructor(
receivedGossipingRequests.addAll(cryptoStore.getPendingIncomingGossipingRequests()) receivedGossipingRequests.addAll(cryptoStore.getPendingIncomingGossipingRequests())
} }
fun close() {
executor.shutdownNow()
}
// Recently verified devices (map of deviceId and timestamp) // Recently verified devices (map of deviceId and timestamp)
private val recentlyVerifiedDevices = HashMap<String, Long>() private val recentlyVerifiedDevices = HashMap<String, Long>()
@ -99,7 +105,7 @@ internal class IncomingGossipingRequestManager @Inject constructor(
fun onGossipingRequestEvent(event: Event) { fun onGossipingRequestEvent(event: Event) {
Timber.v("## CRYPTO | GOSSIP onGossipingRequestEvent type ${event.type} from user ${event.senderId}") Timber.v("## CRYPTO | GOSSIP onGossipingRequestEvent type ${event.type} from user ${event.senderId}")
val roomKeyShare = event.getClearContent().toModel<GossipingDefaultContent>() val roomKeyShare = event.getClearContent().toModel<GossipingDefaultContent>()
val ageLocalTs = event.unsignedData?.age?.let { System.currentTimeMillis() - it } // val ageLocalTs = event.unsignedData?.age?.let { System.currentTimeMillis() - it }
when (roomKeyShare?.action) { when (roomKeyShare?.action) {
GossipingToDeviceObject.ACTION_SHARE_REQUEST -> { GossipingToDeviceObject.ACTION_SHARE_REQUEST -> {
if (event.getClearType() == EventType.REQUEST_SECRET) { if (event.getClearType() == EventType.REQUEST_SECRET) {
@ -108,8 +114,8 @@ internal class IncomingGossipingRequestManager @Inject constructor(
// ignore, it was sent by me as * // ignore, it was sent by me as *
Timber.v("## GOSSIP onGossipingRequestEvent type ${event.type} ignore remote echo") Timber.v("## GOSSIP onGossipingRequestEvent type ${event.type} ignore remote echo")
} else { } else {
// save in DB // // save in DB
cryptoStore.storeIncomingGossipingRequest(it, ageLocalTs) // cryptoStore.storeIncomingGossipingRequest(it, ageLocalTs)
receivedGossipingRequests.add(it) receivedGossipingRequests.add(it)
} }
} }
@ -119,7 +125,7 @@ internal class IncomingGossipingRequestManager @Inject constructor(
// ignore, it was sent by me as * // ignore, it was sent by me as *
Timber.v("## GOSSIP onGossipingRequestEvent type ${event.type} ignore remote echo") Timber.v("## GOSSIP onGossipingRequestEvent type ${event.type} ignore remote echo")
} else { } else {
cryptoStore.storeIncomingGossipingRequest(it, ageLocalTs) // cryptoStore.storeIncomingGossipingRequest(it, ageLocalTs)
receivedGossipingRequests.add(it) receivedGossipingRequests.add(it)
} }
} }
@ -144,13 +150,8 @@ internal class IncomingGossipingRequestManager @Inject constructor(
fun processReceivedGossipingRequests() { fun processReceivedGossipingRequests() {
val roomKeyRequestsToProcess = receivedGossipingRequests.toList() val roomKeyRequestsToProcess = receivedGossipingRequests.toList()
receivedGossipingRequests.clear() receivedGossipingRequests.clear()
for (request in roomKeyRequestsToProcess) {
if (request is IncomingRoomKeyRequest) { Timber.v("## CRYPTO | GOSSIP processReceivedGossipingRequests() : ${roomKeyRequestsToProcess.size} request to process")
processIncomingRoomKeyRequest(request)
} else if (request is IncomingSecretShareRequest) {
processIncomingSecretShareRequest(request)
}
}
var receivedRequestCancellations: List<IncomingRequestCancellation>? = null var receivedRequestCancellations: List<IncomingRequestCancellation>? = null
@ -161,6 +162,16 @@ internal class IncomingGossipingRequestManager @Inject constructor(
} }
} }
executor.execute {
cryptoStore.storeIncomingGossipingRequests(roomKeyRequestsToProcess)
for (request in roomKeyRequestsToProcess) {
if (request is IncomingRoomKeyRequest) {
processIncomingRoomKeyRequest(request)
} else if (request is IncomingSecretShareRequest) {
processIncomingSecretShareRequest(request)
}
}
receivedRequestCancellations?.forEach { request -> receivedRequestCancellations?.forEach { request ->
Timber.v("## CRYPTO | GOSSIP processReceivedGossipingRequests() : m.room_key_request cancellation $request") Timber.v("## CRYPTO | GOSSIP processReceivedGossipingRequests() : m.room_key_request cancellation $request")
// we should probably only notify the app of cancellations we told it // we should probably only notify the app of cancellations we told it
@ -183,6 +194,7 @@ internal class IncomingGossipingRequestManager @Inject constructor(
} }
} }
} }
}
private fun processIncomingRoomKeyRequest(request: IncomingRoomKeyRequest) { private fun processIncomingRoomKeyRequest(request: IncomingRoomKeyRequest) {
val userId = request.userId ?: return val userId = request.userId ?: return

View File

@ -44,7 +44,9 @@ internal class MXOlmDevice @Inject constructor(
/** /**
* The store where crypto data is saved. * The store where crypto data is saved.
*/ */
private val store: IMXCryptoStore) { private val store: IMXCryptoStore,
private val inboundGroupSessionStore: InboundGroupSessionStore
) {
/** /**
* @return the Curve25519 key for the account. * @return the Curve25519 key for the account.
@ -657,7 +659,7 @@ internal class MXOlmDevice @Inject constructor(
timelineSet.add(messageIndexKey) timelineSet.add(messageIndexKey)
} }
store.storeInboundGroupSessions(listOf(session)) inboundGroupSessionStore.storeInBoundGroupSession(session)
val payload = try { val payload = try {
val adapter = MoshiProvider.providesMoshi().adapter<JsonDict>(JSON_DICT_PARAMETERIZED_TYPE) val adapter = MoshiProvider.providesMoshi().adapter<JsonDict>(JSON_DICT_PARAMETERIZED_TYPE)
val payloadString = convertFromUTF8(decryptResult.mDecryptedMessage) val payloadString = convertFromUTF8(decryptResult.mDecryptedMessage)
@ -745,7 +747,7 @@ internal class MXOlmDevice @Inject constructor(
throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_SENDER_KEY, MXCryptoError.ERROR_MISSING_PROPERTY_REASON) throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_SENDER_KEY, MXCryptoError.ERROR_MISSING_PROPERTY_REASON)
} }
val session = store.getInboundGroupSession(sessionId, senderKey) val session = inboundGroupSessionStore.getInboundGroupSession(sessionId, senderKey)
if (session != null) { if (session != null) {
// 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

View File

@ -88,7 +88,7 @@ internal class OutgoingGossipingRequestManager @Inject constructor(
* @param requestBody requestBody * @param requestBody requestBody
*/ */
fun cancelRoomKeyRequest(requestBody: RoomKeyRequestBody) { fun cancelRoomKeyRequest(requestBody: RoomKeyRequestBody) {
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { cryptoCoroutineScope.launch(coroutineDispatchers.computation) {
cancelRoomKeyRequest(requestBody, false) cancelRoomKeyRequest(requestBody, false)
} }
} }
@ -99,7 +99,7 @@ internal class OutgoingGossipingRequestManager @Inject constructor(
* @param requestBody requestBody * @param requestBody requestBody
*/ */
fun resendRoomKeyRequest(requestBody: RoomKeyRequestBody) { fun resendRoomKeyRequest(requestBody: RoomKeyRequestBody) {
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { cryptoCoroutineScope.launch(coroutineDispatchers.computation) {
cancelRoomKeyRequest(requestBody, true) cancelRoomKeyRequest(requestBody, true)
} }
} }

View File

@ -16,10 +16,13 @@
package org.matrix.android.sdk.internal.crypto.algorithms.megolm package org.matrix.android.sdk.internal.crypto.algorithms.megolm
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.MatrixCallback
import org.matrix.android.sdk.api.auth.data.Credentials import org.matrix.android.sdk.api.auth.data.Credentials
import org.matrix.android.sdk.api.session.crypto.MXCryptoError import org.matrix.android.sdk.api.session.crypto.MXCryptoError
import org.matrix.android.sdk.api.session.events.model.Content import org.matrix.android.sdk.api.session.events.model.Content
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.internal.crypto.DeviceListManager import org.matrix.android.sdk.internal.crypto.DeviceListManager
import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
@ -39,6 +42,7 @@ import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask
import org.matrix.android.sdk.internal.task.TaskExecutor import org.matrix.android.sdk.internal.task.TaskExecutor
import org.matrix.android.sdk.internal.task.configureWith import org.matrix.android.sdk.internal.task.configureWith
import org.matrix.android.sdk.internal.util.JsonCanonicalizer import org.matrix.android.sdk.internal.util.JsonCanonicalizer
import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers
import org.matrix.android.sdk.internal.util.convertToUTF8 import org.matrix.android.sdk.internal.util.convertToUTF8
import timber.log.Timber import timber.log.Timber
@ -54,7 +58,9 @@ internal class MXMegolmEncryption(
private val sendToDeviceTask: SendToDeviceTask, private val sendToDeviceTask: SendToDeviceTask,
private val messageEncrypter: MessageEncrypter, private val messageEncrypter: MessageEncrypter,
private val warnOnUnknownDevicesRepository: WarnOnUnknownDeviceRepository, private val warnOnUnknownDevicesRepository: WarnOnUnknownDeviceRepository,
private val taskExecutor: TaskExecutor private val taskExecutor: TaskExecutor,
private val coroutineDispatchers: MatrixCoroutineDispatchers,
private val cryptoCoroutineScope: CoroutineScope
) : IMXEncrypting { ) : IMXEncrypting {
// OutboundSessionInfo. Null if we haven't yet started setting one up. Note // OutboundSessionInfo. Null if we haven't yet started setting one up. Note
@ -84,6 +90,8 @@ internal class MXMegolmEncryption(
} }
private fun notifyWithheldForSession(devices: MXUsersDevicesMap<WithHeldCode>, outboundSession: MXOutboundSessionInfo) { private fun notifyWithheldForSession(devices: MXUsersDevicesMap<WithHeldCode>, outboundSession: MXOutboundSessionInfo) {
// offload to computation thread
cryptoCoroutineScope.launch(coroutineDispatchers.computation) {
mutableListOf<Pair<UserDevice, WithHeldCode>>().apply { mutableListOf<Pair<UserDevice, WithHeldCode>>().apply {
devices.forEach { userId, deviceId, withheldCode -> devices.forEach { userId, deviceId, withheldCode ->
this.add(UserDevice(userId, deviceId) to withheldCode) this.add(UserDevice(userId, deviceId) to withheldCode)
@ -95,6 +103,7 @@ internal class MXMegolmEncryption(
notifyKeyWithHeld(targets, outboundSession.sessionId, olmDevice.deviceCurve25519Key, code) notifyKeyWithHeld(targets, outboundSession.sessionId, olmDevice.deviceCurve25519Key, code)
} }
} }
}
override fun discardSessionKey() { override fun discardSessionKey() {
outboundSession = null outboundSession = null
@ -247,6 +256,15 @@ internal class MXMegolmEncryption(
for ((userId, devicesToShareWith) in devicesByUser) { for ((userId, devicesToShareWith) in devicesByUser) {
for ((deviceId) in devicesToShareWith) { for ((deviceId) in devicesToShareWith) {
session.sharedWithHelper.markedSessionAsShared(userId, deviceId, chainIndex) session.sharedWithHelper.markedSessionAsShared(userId, deviceId, chainIndex)
cryptoStore.saveGossipingEvent(Event(
type = EventType.ROOM_KEY,
senderId = credentials.userId,
content = submap.apply {
this["session_key"] = ""
// we add a fake key for trail
this["_dest"] = "$userId|$deviceId"
}
))
} }
} }
@ -420,7 +438,7 @@ internal class MXMegolmEncryption(
sendToDeviceTask.execute(sendToDeviceParams) sendToDeviceTask.execute(sendToDeviceParams)
true true
} catch (failure: Throwable) { } catch (failure: Throwable) {
Timber.v("## CRYPTO | CRYPTO | reshareKey() : fail to send <$sessionId> to $userId:$deviceId") Timber.e(failure, "## CRYPTO | CRYPTO | reshareKey() : fail to send <$sessionId> to $userId:$deviceId")
false false
} }
} }

View File

@ -16,6 +16,7 @@
package org.matrix.android.sdk.internal.crypto.algorithms.megolm package org.matrix.android.sdk.internal.crypto.algorithms.megolm
import kotlinx.coroutines.CoroutineScope
import org.matrix.android.sdk.api.auth.data.Credentials import org.matrix.android.sdk.api.auth.data.Credentials
import org.matrix.android.sdk.internal.crypto.DeviceListManager import org.matrix.android.sdk.internal.crypto.DeviceListManager
import org.matrix.android.sdk.internal.crypto.MXOlmDevice import org.matrix.android.sdk.internal.crypto.MXOlmDevice
@ -26,6 +27,7 @@ import org.matrix.android.sdk.internal.crypto.repository.WarnOnUnknownDeviceRepo
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask
import org.matrix.android.sdk.internal.task.TaskExecutor import org.matrix.android.sdk.internal.task.TaskExecutor
import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers
import javax.inject.Inject import javax.inject.Inject
internal class MXMegolmEncryptionFactory @Inject constructor( internal class MXMegolmEncryptionFactory @Inject constructor(
@ -38,7 +40,9 @@ internal class MXMegolmEncryptionFactory @Inject constructor(
private val sendToDeviceTask: SendToDeviceTask, private val sendToDeviceTask: SendToDeviceTask,
private val messageEncrypter: MessageEncrypter, private val messageEncrypter: MessageEncrypter,
private val warnOnUnknownDevicesRepository: WarnOnUnknownDeviceRepository, private val warnOnUnknownDevicesRepository: WarnOnUnknownDeviceRepository,
private val taskExecutor: TaskExecutor) { private val taskExecutor: TaskExecutor,
private val coroutineDispatchers: MatrixCoroutineDispatchers,
private val cryptoCoroutineScope: CoroutineScope) {
fun create(roomId: String): MXMegolmEncryption { fun create(roomId: String): MXMegolmEncryption {
return MXMegolmEncryption( return MXMegolmEncryption(
@ -52,7 +56,9 @@ internal class MXMegolmEncryptionFactory @Inject constructor(
sendToDeviceTask, sendToDeviceTask,
messageEncrypter, messageEncrypter,
warnOnUnknownDevicesRepository, warnOnUnknownDevicesRepository,
taskExecutor taskExecutor,
coroutineDispatchers,
cryptoCoroutineScope
) )
} }
} }

View File

@ -17,6 +17,8 @@
package org.matrix.android.sdk.internal.crypto.crosssigning package org.matrix.android.sdk.internal.crypto.crosssigning
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.work.BackoffPolicy
import androidx.work.ExistingWorkPolicy
import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.MatrixCallback
import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.session.crypto.crosssigning.CrossSigningService import org.matrix.android.sdk.api.session.crypto.crosssigning.CrossSigningService
@ -39,15 +41,20 @@ import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers
import org.matrix.android.sdk.internal.util.withoutPrefix import org.matrix.android.sdk.internal.util.withoutPrefix
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.greenrobot.eventbus.EventBus import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo
import org.matrix.android.sdk.internal.di.SessionId
import org.matrix.android.sdk.internal.di.WorkManagerProvider
import org.matrix.android.sdk.internal.worker.WorkerParamsFactory
import org.matrix.olm.OlmPkSigning import org.matrix.olm.OlmPkSigning
import org.matrix.olm.OlmUtility import org.matrix.olm.OlmUtility
import timber.log.Timber import timber.log.Timber
import java.util.concurrent.TimeUnit
import javax.inject.Inject import javax.inject.Inject
@SessionScope @SessionScope
internal class DefaultCrossSigningService @Inject constructor( internal class DefaultCrossSigningService @Inject constructor(
@UserId private val userId: String, @UserId private val userId: String,
@SessionId private val sessionId: String,
private val cryptoStore: IMXCryptoStore, private val cryptoStore: IMXCryptoStore,
private val deviceListManager: DeviceListManager, private val deviceListManager: DeviceListManager,
private val initializeCrossSigningTask: InitializeCrossSigningTask, private val initializeCrossSigningTask: InitializeCrossSigningTask,
@ -55,7 +62,7 @@ internal class DefaultCrossSigningService @Inject constructor(
private val taskExecutor: TaskExecutor, private val taskExecutor: TaskExecutor,
private val coroutineDispatchers: MatrixCoroutineDispatchers, private val coroutineDispatchers: MatrixCoroutineDispatchers,
private val cryptoCoroutineScope: CoroutineScope, private val cryptoCoroutineScope: CoroutineScope,
private val eventBus: EventBus) : CrossSigningService, DeviceListManager.UserDevicesUpdateListener { private val workManagerProvider: WorkManagerProvider) : CrossSigningService, DeviceListManager.UserDevicesUpdateListener {
private var olmUtility: OlmUtility? = null private var olmUtility: OlmUtility? = null
@ -360,6 +367,12 @@ internal class DefaultCrossSigningService @Inject constructor(
// First let's get my user key // First let's get my user key
val myCrossSigningInfo = cryptoStore.getCrossSigningInfo(userId) val myCrossSigningInfo = cryptoStore.getCrossSigningInfo(userId)
checkOtherMSKTrusted(myCrossSigningInfo, cryptoStore.getCrossSigningInfo(otherUserId))
return UserTrustResult.Success
}
fun checkOtherMSKTrusted(myCrossSigningInfo: MXCrossSigningInfo?, otherInfo: MXCrossSigningInfo?): UserTrustResult {
val myUserKey = myCrossSigningInfo?.userKey() val myUserKey = myCrossSigningInfo?.userKey()
?: return UserTrustResult.CrossSigningNotConfigured(userId) ?: return UserTrustResult.CrossSigningNotConfigured(userId)
@ -368,15 +381,15 @@ internal class DefaultCrossSigningService @Inject constructor(
} }
// Let's get the other user master key // Let's get the other user master key
val otherMasterKey = cryptoStore.getCrossSigningInfo(otherUserId)?.masterKey() val otherMasterKey = otherInfo?.masterKey()
?: return UserTrustResult.UnknownCrossSignatureInfo(otherUserId) ?: return UserTrustResult.UnknownCrossSignatureInfo(otherInfo?.userId ?: "")
val masterKeySignaturesMadeByMyUserKey = otherMasterKey.signatures val masterKeySignaturesMadeByMyUserKey = otherMasterKey.signatures
?.get(userId) // Signatures made by me ?.get(userId) // Signatures made by me
?.get("ed25519:${myUserKey.unpaddedBase64PublicKey}") ?.get("ed25519:${myUserKey.unpaddedBase64PublicKey}")
if (masterKeySignaturesMadeByMyUserKey.isNullOrBlank()) { if (masterKeySignaturesMadeByMyUserKey.isNullOrBlank()) {
Timber.d("## CrossSigning checkUserTrust false for $otherUserId, not signed by my UserSigningKey") Timber.d("## CrossSigning checkUserTrust false for ${otherInfo.userId}, not signed by my UserSigningKey")
return UserTrustResult.KeyNotSigned(otherMasterKey) return UserTrustResult.KeyNotSigned(otherMasterKey)
} }
@ -396,6 +409,15 @@ internal class DefaultCrossSigningService @Inject constructor(
// and that MSK is trusted (i know the private key, or is signed by a trusted device) // and that MSK is trusted (i know the private key, or is signed by a trusted device)
val myCrossSigningInfo = cryptoStore.getCrossSigningInfo(userId) val myCrossSigningInfo = cryptoStore.getCrossSigningInfo(userId)
return checkSelfTrust(myCrossSigningInfo, cryptoStore.getUserDeviceList(userId))
}
fun checkSelfTrust(myCrossSigningInfo: MXCrossSigningInfo?, myDevices: List<CryptoDeviceInfo>?): UserTrustResult {
// Special case when it's me,
// I have to check that MSK -> USK -> SSK
// and that MSK is trusted (i know the private key, or is signed by a trusted device)
// val myCrossSigningInfo = cryptoStore.getCrossSigningInfo(userId)
val myMasterKey = myCrossSigningInfo?.masterKey() val myMasterKey = myCrossSigningInfo?.masterKey()
?: return UserTrustResult.CrossSigningNotConfigured(userId) ?: return UserTrustResult.CrossSigningNotConfigured(userId)
@ -423,7 +445,7 @@ internal class DefaultCrossSigningService @Inject constructor(
// Maybe it's signed by a locally trusted device? // Maybe it's signed by a locally trusted device?
myMasterKey.signatures?.get(userId)?.forEach { (key, value) -> myMasterKey.signatures?.get(userId)?.forEach { (key, value) ->
val potentialDeviceId = key.withoutPrefix("ed25519:") val potentialDeviceId = key.withoutPrefix("ed25519:")
val potentialDevice = cryptoStore.getUserDevice(userId, potentialDeviceId) val potentialDevice = myDevices?.firstOrNull { it.deviceId == potentialDeviceId } // cryptoStore.getUserDevice(userId, potentialDeviceId)
if (potentialDevice != null && potentialDevice.isVerified) { if (potentialDevice != null && potentialDevice.isVerified) {
// Check signature validity? // Check signature validity?
try { try {
@ -561,6 +583,8 @@ internal class DefaultCrossSigningService @Inject constructor(
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
cryptoStore.markMyMasterKeyAsLocallyTrusted(true) cryptoStore.markMyMasterKeyAsLocallyTrusted(true)
checkSelfTrust() checkSelfTrust()
// re-verify all trusts
onUsersDeviceUpdate(listOf(userId))
} }
} }
@ -666,6 +690,55 @@ internal class DefaultCrossSigningService @Inject constructor(
return DeviceTrustResult.Success(DeviceTrustLevel(crossSigningVerified = true, locallyVerified = locallyTrusted)) return DeviceTrustResult.Success(DeviceTrustLevel(crossSigningVerified = true, locallyVerified = locallyTrusted))
} }
fun checkDeviceTrust(myKeys: MXCrossSigningInfo?, otherKeys: MXCrossSigningInfo?, otherDevice: CryptoDeviceInfo) : DeviceTrustResult {
val locallyTrusted = otherDevice.trustLevel?.isLocallyVerified()
myKeys ?: return legacyFallbackTrust(locallyTrusted, DeviceTrustResult.CrossSigningNotConfigured(userId))
if (!myKeys.isTrusted()) return legacyFallbackTrust(locallyTrusted, DeviceTrustResult.KeysNotTrusted(myKeys))
otherKeys ?: return legacyFallbackTrust(locallyTrusted, DeviceTrustResult.CrossSigningNotConfigured(otherDevice.userId))
// TODO should we force verification ?
if (!otherKeys.isTrusted()) return legacyFallbackTrust(locallyTrusted, DeviceTrustResult.KeysNotTrusted(otherKeys))
// Check if the trust chain is valid
/*
*
* ALICE BOB
*
* MSK MSK
*
*
* SSK SSK
*
* USK
* USK (not visible by
* Alice)
*
*
* BOB's Device
*
*/
val otherSSKSignature = otherDevice.signatures?.get(otherKeys.userId)?.get("ed25519:${otherKeys.selfSigningKey()?.unpaddedBase64PublicKey}")
?: return legacyFallbackTrust(
locallyTrusted,
DeviceTrustResult.MissingDeviceSignature(otherDevice.deviceId, otherKeys.selfSigningKey()
?.unpaddedBase64PublicKey
?: ""
)
)
// Check bob's device is signed by bob's SSK
try {
olmUtility!!.verifyEd25519Signature(otherSSKSignature, otherKeys.selfSigningKey()?.unpaddedBase64PublicKey, otherDevice.canonicalSignable())
} catch (e: Throwable) {
return legacyFallbackTrust(locallyTrusted, DeviceTrustResult.InvalidDeviceSignature(otherDevice.deviceId, otherSSKSignature, e))
}
return DeviceTrustResult.Success(DeviceTrustLevel(crossSigningVerified = true, locallyVerified = locallyTrusted))
}
private fun legacyFallbackTrust(locallyTrusted: Boolean?, crossSignTrustFail: DeviceTrustResult): DeviceTrustResult { private fun legacyFallbackTrust(locallyTrusted: Boolean?, crossSignTrustFail: DeviceTrustResult): DeviceTrustResult {
return if (locallyTrusted == true) { return if (locallyTrusted == true) {
DeviceTrustResult.Success(DeviceTrustLevel(crossSigningVerified = false, locallyVerified = true)) DeviceTrustResult.Success(DeviceTrustLevel(crossSigningVerified = false, locallyVerified = true))
@ -675,36 +748,18 @@ internal class DefaultCrossSigningService @Inject constructor(
} }
override fun onUsersDeviceUpdate(userIds: List<String>) { override fun onUsersDeviceUpdate(userIds: List<String>) {
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { Timber.d("## CrossSigning - onUsersDeviceUpdate for $userIds")
Timber.d("## CrossSigning - onUsersDeviceUpdate for ${userIds.size} users") val workerParams = UpdateTrustWorker.Params(sessionId = sessionId, updatedUserIds = userIds)
userIds.forEach { otherUserId -> val workerData = WorkerParamsFactory.toData(workerParams)
checkUserTrust(otherUserId).let {
Timber.v("## CrossSigning - update trust for $otherUserId , verified=${it.isVerified()}")
setUserKeysAsTrusted(otherUserId, it.isVerified())
}
}
}
// now check device trust val workRequest = workManagerProvider.matrixOneTimeWorkRequestBuilder<UpdateTrustWorker>()
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { .setInputData(workerData)
userIds.forEach { otherUserId -> .setBackoffCriteria(BackoffPolicy.LINEAR, 2_000L, TimeUnit.MILLISECONDS)
// TODO if my keys have changes, i should recheck all devices of all users? .build()
val devices = cryptoStore.getUserDeviceList(otherUserId)
devices?.forEach { device ->
val updatedTrust = checkDeviceTrust(otherUserId, device.deviceId, device.trustLevel?.isLocallyVerified() ?: false)
Timber.v("## CrossSigning - update trust for device ${device.deviceId} of user $otherUserId , verified=$updatedTrust")
cryptoStore.setDeviceTrust(otherUserId, device.deviceId, updatedTrust.isCrossSignedVerified(), updatedTrust.isLocallyVerified())
}
if (otherUserId == userId) { workManagerProvider.workManager
// It's me, i should check if a newly trusted device is signing my master key .beginUniqueWork("TRUST_UPDATE_QUEUE", ExistingWorkPolicy.APPEND_OR_REPLACE, workRequest)
// In this case it will change my MSK trust, and should then re-trigger a check of all other user trust .enqueue()
setUserKeysAsTrusted(otherUserId, checkSelfTrust().isVerified())
}
}
eventBus.post(CryptoToSessionUserTrustChange(userIds))
}
} }
private fun setUserKeysAsTrusted(otherUserId: String, trusted: Boolean) { private fun setUserKeysAsTrusted(otherUserId: String, trusted: Boolean) {

View File

@ -1,126 +0,0 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.crypto.crosssigning
import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntity
import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntityFields
import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity
import org.matrix.android.sdk.internal.database.query.where
import org.matrix.android.sdk.internal.di.SessionDatabase
import org.matrix.android.sdk.internal.session.SessionLifecycleObserver
import org.matrix.android.sdk.internal.session.room.summary.RoomSummaryUpdater
import org.matrix.android.sdk.internal.session.room.membership.RoomMemberHelper
import org.matrix.android.sdk.internal.task.TaskExecutor
import org.matrix.android.sdk.internal.util.createBackgroundHandler
import io.realm.Realm
import io.realm.RealmConfiguration
import kotlinx.coroutines.android.asCoroutineDispatcher
import kotlinx.coroutines.launch
import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
import timber.log.Timber
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicReference
import javax.inject.Inject
internal class ShieldTrustUpdater @Inject constructor(
private val eventBus: EventBus,
private val computeTrustTask: ComputeTrustTask,
private val taskExecutor: TaskExecutor,
@SessionDatabase private val sessionRealmConfiguration: RealmConfiguration,
private val roomSummaryUpdater: RoomSummaryUpdater
) : SessionLifecycleObserver {
companion object {
private val BACKGROUND_HANDLER = createBackgroundHandler("SHIELD_CRYPTO_DB_THREAD")
private val BACKGROUND_HANDLER_DISPATCHER = BACKGROUND_HANDLER.asCoroutineDispatcher()
}
private val backgroundSessionRealm = AtomicReference<Realm>()
private val isStarted = AtomicBoolean()
override fun onStart() {
if (isStarted.compareAndSet(false, true)) {
eventBus.register(this)
BACKGROUND_HANDLER.post {
backgroundSessionRealm.set(Realm.getInstance(sessionRealmConfiguration))
}
}
}
override fun onStop() {
if (isStarted.compareAndSet(true, false)) {
eventBus.unregister(this)
BACKGROUND_HANDLER.post {
backgroundSessionRealm.getAndSet(null).also {
it?.close()
}
}
}
}
@Subscribe
fun onRoomMemberChange(update: SessionToCryptoRoomMembersUpdate) {
if (!isStarted.get()) {
return
}
taskExecutor.executorScope.launch(BACKGROUND_HANDLER_DISPATCHER) {
val updatedTrust = computeTrustTask.execute(ComputeTrustTask.Params(update.userIds, update.isDirect))
// We need to send that back to session base
backgroundSessionRealm.get()?.executeTransaction { realm ->
roomSummaryUpdater.updateShieldTrust(realm, update.roomId, updatedTrust)
}
}
}
@Subscribe
fun onTrustUpdate(update: CryptoToSessionUserTrustChange) {
if (!isStarted.get()) {
return
}
onCryptoDevicesChange(update.userIds)
}
private fun onCryptoDevicesChange(users: List<String>) {
taskExecutor.executorScope.launch(BACKGROUND_HANDLER_DISPATCHER) {
val realm = backgroundSessionRealm.get() ?: return@launch
val distinctRoomIds = realm.where(RoomMemberSummaryEntity::class.java)
.`in`(RoomMemberSummaryEntityFields.USER_ID, users.toTypedArray())
.distinct(RoomMemberSummaryEntityFields.ROOM_ID)
.findAll()
.map { it.roomId }
distinctRoomIds.forEach { roomId ->
val roomSummary = RoomSummaryEntity.where(realm, roomId).findFirst()
if (roomSummary?.isEncrypted.orFalse()) {
val allActiveRoomMembers = RoomMemberHelper(realm, roomId).getActiveRoomMemberIds()
try {
val updatedTrust = computeTrustTask.execute(
ComputeTrustTask.Params(allActiveRoomMembers, roomSummary?.isDirect == true)
)
realm.executeTransaction {
roomSummaryUpdater.updateShieldTrust(it, roomId, updatedTrust)
}
} catch (failure: Throwable) {
Timber.e(failure)
}
}
}
}
}
}

View File

@ -0,0 +1,322 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.crypto.crosssigning
import android.content.Context
import androidx.work.WorkerParameters
import com.squareup.moshi.JsonClass
import io.realm.Realm
import io.realm.RealmConfiguration
import io.realm.kotlin.where
import org.matrix.android.sdk.api.crypto.RoomEncryptionTrustLevel
import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.session.crypto.crosssigning.MXCrossSigningInfo
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
import org.matrix.android.sdk.internal.crypto.store.db.mapper.CrossSigningKeysMapper
import org.matrix.android.sdk.internal.crypto.store.db.model.CrossSigningInfoEntity
import org.matrix.android.sdk.internal.crypto.store.db.model.CrossSigningInfoEntityFields
import org.matrix.android.sdk.internal.crypto.store.db.model.CryptoMapper
import org.matrix.android.sdk.internal.crypto.store.db.model.TrustLevelEntity
import org.matrix.android.sdk.internal.crypto.store.db.model.UserEntity
import org.matrix.android.sdk.internal.crypto.store.db.model.UserEntityFields
import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntity
import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntityFields
import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity
import org.matrix.android.sdk.internal.database.query.where
import org.matrix.android.sdk.internal.di.CryptoDatabase
import org.matrix.android.sdk.internal.di.SessionDatabase
import org.matrix.android.sdk.internal.di.UserId
import org.matrix.android.sdk.internal.session.SessionComponent
import org.matrix.android.sdk.internal.session.room.membership.RoomMemberHelper
import org.matrix.android.sdk.internal.worker.SessionSafeCoroutineWorker
import org.matrix.android.sdk.internal.worker.SessionWorkerParams
import timber.log.Timber
import javax.inject.Inject
internal class UpdateTrustWorker(context: Context,
params: WorkerParameters)
: SessionSafeCoroutineWorker<UpdateTrustWorker.Params>(context, params, Params::class.java) {
@JsonClass(generateAdapter = true)
internal data class Params(
override val sessionId: String,
override val lastFailureMessage: String? = null,
val updatedUserIds: List<String>
) : SessionWorkerParams
@Inject lateinit var crossSigningService: DefaultCrossSigningService
// It breaks the crypto store contract, but we need to batch things :/
@CryptoDatabase @Inject lateinit var realmConfiguration: RealmConfiguration
@UserId @Inject lateinit var myUserId: String
@Inject lateinit var crossSigningKeysMapper: CrossSigningKeysMapper
@SessionDatabase @Inject lateinit var sessionRealmConfiguration: RealmConfiguration
// @Inject lateinit var roomSummaryUpdater: RoomSummaryUpdater
@Inject lateinit var cryptoStore: IMXCryptoStore
override fun injectWith(injector: SessionComponent) {
injector.inject(this)
}
override suspend fun doSafeWork(params: Params): Result {
var userList = params.updatedUserIds
// Unfortunately we don't have much info on what did exactly changed (is it the cross signing keys of that user,
// or a new device?) So we check all again :/
Timber.d("## CrossSigning - Updating trust for $userList")
// First we check that the users MSK are trusted by mine
// After that we check the trust chain for each devices of each users
Realm.getInstance(realmConfiguration).use { realm ->
realm.executeTransaction {
// By mapping here to model, this object is not live
// I should update it if needed
var myCrossSigningInfo = realm.where(CrossSigningInfoEntity::class.java)
.equalTo(CrossSigningInfoEntityFields.USER_ID, myUserId)
.findFirst()?.let { mapCrossSigningInfoEntity(it) }
var myTrustResult: UserTrustResult? = null
if (userList.contains(myUserId)) {
Timber.d("## CrossSigning - Clear all trust as a change on my user was detected")
// i am in the list.. but i don't know exactly the delta of change :/
// If it's my cross signing keys we should refresh all trust
// do it anyway ?
userList = realm.where(CrossSigningInfoEntity::class.java)
.findAll().mapNotNull { it.userId }
Timber.d("## CrossSigning - Updating trust for all $userList")
// check right now my keys and mark it as trusted as other trust depends on it
val myDevices = realm.where<UserEntity>()
.equalTo(UserEntityFields.USER_ID, myUserId)
.findFirst()
?.devices
?.map { deviceInfo ->
CryptoMapper.mapToModel(deviceInfo)
}
myTrustResult = crossSigningService.checkSelfTrust(myCrossSigningInfo, myDevices).also {
updateCrossSigningKeysTrust(realm, myUserId, it.isVerified())
// update model reference
myCrossSigningInfo = realm.where(CrossSigningInfoEntity::class.java)
.equalTo(CrossSigningInfoEntityFields.USER_ID, myUserId)
.findFirst()?.let { mapCrossSigningInfoEntity(it) }
}
}
val otherInfos = userList.map {
it to realm.where(CrossSigningInfoEntity::class.java)
.equalTo(CrossSigningInfoEntityFields.USER_ID, it)
.findFirst()?.let { mapCrossSigningInfoEntity(it) }
}
.toMap()
val trusts = otherInfos.map { infoEntry ->
infoEntry.key to when (infoEntry.key) {
myUserId -> myTrustResult
else -> {
crossSigningService.checkOtherMSKTrusted(myCrossSigningInfo, infoEntry.value).also {
Timber.d("## CrossSigning - user:${infoEntry.key} result:$it")
}
}
}
}.toMap()
// TODO! if it's me and my keys has changed... I have to reset trust for everyone!
// i have all the new trusts, update DB
trusts.forEach {
val verified = it.value?.isVerified() == true
updateCrossSigningKeysTrust(realm, it.key, verified)
}
// Ok so now we have to check device trust for all these users..
Timber.v("## CrossSigning - Updating devices cross trust users ${trusts.keys}")
trusts.keys.forEach {
val devicesEntities = realm.where<UserEntity>()
.equalTo(UserEntityFields.USER_ID, it)
.findFirst()
?.devices
val trustMap = devicesEntities?.map { device ->
// get up to date from DB has could have been updated
val otherInfo = realm.where(CrossSigningInfoEntity::class.java)
.equalTo(CrossSigningInfoEntityFields.USER_ID, it)
.findFirst()?.let { mapCrossSigningInfoEntity(it) }
device to crossSigningService.checkDeviceTrust(myCrossSigningInfo, otherInfo, CryptoMapper.mapToModel(device))
}?.toMap()
// Update trust if needed
devicesEntities?.forEach { device ->
val crossSignedVerified = trustMap?.get(device)?.isCrossSignedVerified()
Timber.d("## CrossSigning - Trust for ${device.userId}|${device.deviceId} : cross verified: ${trustMap?.get(device)}")
if (device.trustLevelEntity?.crossSignedVerified != crossSignedVerified) {
Timber.d("## CrossSigning - Trust change detected for ${device.userId}|${device.deviceId} : cross verified: $crossSignedVerified")
// need to save
val trustEntity = device.trustLevelEntity
if (trustEntity == null) {
realm.createObject(TrustLevelEntity::class.java).let {
it.locallyVerified = false
it.crossSignedVerified = crossSignedVerified
device.trustLevelEntity = it
}
} else {
trustEntity.crossSignedVerified = crossSignedVerified
}
}
}
}
}
}
// So Cross Signing keys trust is updated, device trust is updated
// We can now update room shields? in the session DB?
Timber.d("## CrossSigning - Updating shields for impacted rooms...")
Realm.getInstance(sessionRealmConfiguration).use { it ->
it.executeTransaction { realm ->
val distinctRoomIds = realm.where(RoomMemberSummaryEntity::class.java)
.`in`(RoomMemberSummaryEntityFields.USER_ID, userList.toTypedArray())
.distinct(RoomMemberSummaryEntityFields.ROOM_ID)
.findAll()
.map { it.roomId }
Timber.d("## CrossSigning - ... impacted rooms $distinctRoomIds")
distinctRoomIds.forEach { roomId ->
val roomSummary = RoomSummaryEntity.where(realm, roomId).findFirst()
if (roomSummary?.isEncrypted == true) {
Timber.d("## CrossSigning - Check shield state for room $roomId")
val allActiveRoomMembers = RoomMemberHelper(realm, roomId).getActiveRoomMemberIds()
try {
val updatedTrust = computeRoomShield(allActiveRoomMembers, roomSummary)
if (roomSummary.roomEncryptionTrustLevel != updatedTrust) {
Timber.d("## CrossSigning - Shield change detected for $roomId -> $updatedTrust")
roomSummary.roomEncryptionTrustLevel = updatedTrust
}
} catch (failure: Throwable) {
Timber.e(failure)
}
}
}
}
}
return Result.success()
}
private fun updateCrossSigningKeysTrust(realm: Realm, userId: String, verified: Boolean) {
val xInfoEntity = realm.where(CrossSigningInfoEntity::class.java)
.equalTo(CrossSigningInfoEntityFields.USER_ID, userId)
.findFirst()
xInfoEntity?.crossSigningKeys?.forEach { info ->
// optimization to avoid trigger updates when there is no change..
if (info.trustLevelEntity?.isVerified() != verified) {
Timber.d("## CrossSigning - Trust change for $userId : $verified")
val level = info.trustLevelEntity
if (level == null) {
val newLevel = realm.createObject(TrustLevelEntity::class.java)
newLevel.locallyVerified = verified
newLevel.crossSignedVerified = verified
info.trustLevelEntity = newLevel
} else {
level.locallyVerified = verified
level.crossSignedVerified = verified
}
}
}
}
private fun computeRoomShield(activeMemberUserIds: List<String>, roomSummaryEntity: RoomSummaryEntity): RoomEncryptionTrustLevel {
Timber.d("## CrossSigning - computeRoomShield ${roomSummaryEntity.roomId} -> $activeMemberUserIds")
// The set of “all users” depends on the type of room:
// For regular / topic rooms, all users including yourself, are considered when decorating a room
// For 1:1 and group DM rooms, all other users (i.e. excluding yourself) are considered when decorating a room
val listToCheck = if (roomSummaryEntity.isDirect) {
activeMemberUserIds.filter { it != myUserId }
} else {
activeMemberUserIds
}
val allTrustedUserIds = listToCheck
.filter { userId ->
Realm.getInstance(realmConfiguration).use {
it.where(CrossSigningInfoEntity::class.java)
.equalTo(CrossSigningInfoEntityFields.USER_ID, userId)
.findFirst()?.let { mapCrossSigningInfoEntity(it) }?.isTrusted() == true
}
}
val myCrossKeys = Realm.getInstance(realmConfiguration).use {
it.where(CrossSigningInfoEntity::class.java)
.equalTo(CrossSigningInfoEntityFields.USER_ID, myUserId)
.findFirst()?.let { mapCrossSigningInfoEntity(it) }
}
return if (allTrustedUserIds.isEmpty()) {
RoomEncryptionTrustLevel.Default
} else {
// If one of the verified user as an untrusted device -> warning
// If all devices of all verified users are trusted -> green
// else -> black
allTrustedUserIds
.mapNotNull { uid ->
Realm.getInstance(realmConfiguration).use {
it.where<UserEntity>()
.equalTo(UserEntityFields.USER_ID, uid)
.findFirst()
?.devices
?.map {
CryptoMapper.mapToModel(it)
}
}
}
.flatten()
.let { allDevices ->
Timber.v("## CrossSigning - computeRoomShield ${roomSummaryEntity.roomId} devices ${allDevices.map { it.deviceId }}")
if (myCrossKeys != null) {
allDevices.any { !it.trustLevel?.crossSigningVerified.orFalse() }
} else {
// Legacy method
allDevices.any { !it.isVerified }
}
}
.let { hasWarning ->
if (hasWarning) {
RoomEncryptionTrustLevel.Warning
} else {
if (listToCheck.size == allTrustedUserIds.size) {
// all users are trusted and all devices are verified
RoomEncryptionTrustLevel.Trusted
} else {
RoomEncryptionTrustLevel.Default
}
}
}
}
}
private fun mapCrossSigningInfoEntity(xsignInfo: CrossSigningInfoEntity): MXCrossSigningInfo {
val userId = xsignInfo.userId ?: ""
return MXCrossSigningInfo(
userId = userId,
crossSigningKeys = xsignInfo.crossSigningKeys.mapNotNull {
crossSigningKeysMapper.map(userId, it)
}
)
}
override fun buildErrorParams(params: Params, message: String): Params {
return params.copy(lastFailureMessage = params.lastFailureMessage ?: message)
}
}

View File

@ -30,7 +30,6 @@ import org.matrix.android.sdk.api.listeners.StepProgressListener
import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupService import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupService
import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupState import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupState
import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupStateListener import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupStateListener
import org.matrix.android.sdk.api.util.JsonDict
import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM_BACKUP import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM_BACKUP
import org.matrix.android.sdk.internal.crypto.MXOlmDevice import org.matrix.android.sdk.internal.crypto.MXOlmDevice
import org.matrix.android.sdk.internal.crypto.MegolmSessionData import org.matrix.android.sdk.internal.crypto.MegolmSessionData
@ -85,6 +84,7 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.matrix.android.sdk.internal.crypto.keysbackup.model.SignalableMegolmBackupAuthData
import org.matrix.olm.OlmException import org.matrix.olm.OlmException
import org.matrix.olm.OlmPkDecryption import org.matrix.olm.OlmPkDecryption
import org.matrix.olm.OlmPkEncryption import org.matrix.olm.OlmPkEncryption
@ -170,7 +170,7 @@ internal class DefaultKeysBackupService @Inject constructor(
runCatching { runCatching {
withContext(coroutineDispatchers.crypto) { withContext(coroutineDispatchers.crypto) {
val olmPkDecryption = OlmPkDecryption() val olmPkDecryption = OlmPkDecryption()
val megolmBackupAuthData = if (password != null) { val signalableMegolmBackupAuthData = if (password != null) {
// Generate a private key from the password // Generate a private key from the password
val backgroundProgressListener = if (progressListener == null) { val backgroundProgressListener = if (progressListener == null) {
null null
@ -189,7 +189,7 @@ internal class DefaultKeysBackupService @Inject constructor(
} }
val generatePrivateKeyResult = generatePrivateKeyWithPassword(password, backgroundProgressListener) val generatePrivateKeyResult = generatePrivateKeyWithPassword(password, backgroundProgressListener)
MegolmBackupAuthData( SignalableMegolmBackupAuthData(
publicKey = olmPkDecryption.setPrivateKey(generatePrivateKeyResult.privateKey), publicKey = olmPkDecryption.setPrivateKey(generatePrivateKeyResult.privateKey),
privateKeySalt = generatePrivateKeyResult.salt, privateKeySalt = generatePrivateKeyResult.salt,
privateKeyIterations = generatePrivateKeyResult.iterations privateKeyIterations = generatePrivateKeyResult.iterations
@ -197,14 +197,17 @@ internal class DefaultKeysBackupService @Inject constructor(
} else { } else {
val publicKey = olmPkDecryption.generateKey() val publicKey = olmPkDecryption.generateKey()
MegolmBackupAuthData( SignalableMegolmBackupAuthData(
publicKey = publicKey publicKey = publicKey
) )
} }
val canonicalJson = JsonCanonicalizer.getCanonicalJson(Map::class.java, megolmBackupAuthData.signalableJSONDictionary()) val canonicalJson = JsonCanonicalizer.getCanonicalJson(Map::class.java, signalableMegolmBackupAuthData.signalableJSONDictionary())
val signedMegolmBackupAuthData = megolmBackupAuthData.copy( val signedMegolmBackupAuthData = MegolmBackupAuthData(
publicKey = signalableMegolmBackupAuthData.publicKey,
privateKeySalt = signalableMegolmBackupAuthData.privateKeySalt,
privateKeyIterations = signalableMegolmBackupAuthData.privateKeyIterations,
signatures = objectSigner.signObject(canonicalJson) signatures = objectSigner.signObject(canonicalJson)
) )
@ -223,8 +226,7 @@ internal class DefaultKeysBackupService @Inject constructor(
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
val createKeysBackupVersionBody = CreateKeysBackupVersionBody( val createKeysBackupVersionBody = CreateKeysBackupVersionBody(
algorithm = keysBackupCreationInfo.algorithm, algorithm = keysBackupCreationInfo.algorithm,
authData = MoshiProvider.providesMoshi().adapter(Map::class.java) authData = keysBackupCreationInfo.authData.toJsonDict()
.fromJson(keysBackupCreationInfo.authData?.toJsonString() ?: "") as JsonDict?
) )
keysBackupStateManager.state = KeysBackupState.Enabling keysBackupStateManager.state = KeysBackupState.Enabling
@ -234,7 +236,10 @@ internal class DefaultKeysBackupService @Inject constructor(
this.callback = object : MatrixCallback<KeysVersion> { this.callback = object : MatrixCallback<KeysVersion> {
override fun onSuccess(data: KeysVersion) { override fun onSuccess(data: KeysVersion) {
// Reset backup markers. // Reset backup markers.
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
// move tx out of UI thread
cryptoStore.resetBackupMarkers() cryptoStore.resetBackupMarkers()
}
val keyBackupVersion = KeysVersionResult( val keyBackupVersion = KeysVersionResult(
algorithm = createKeysBackupVersionBody.algorithm, algorithm = createKeysBackupVersionBody.algorithm,
@ -242,7 +247,7 @@ internal class DefaultKeysBackupService @Inject constructor(
version = data.version, version = data.version,
// We can consider that the server does not have keys yet // We can consider that the server does not have keys yet
count = 0, count = 0,
hash = null hash = ""
) )
enableKeysBackup(keyBackupVersion) enableKeysBackup(keyBackupVersion)
@ -264,7 +269,7 @@ internal class DefaultKeysBackupService @Inject constructor(
withContext(coroutineDispatchers.crypto) { withContext(coroutineDispatchers.crypto) {
// If we're currently backing up to this backup... stop. // If we're currently backing up to this backup... stop.
// (We start using it automatically in createKeysBackupVersion so this is symmetrical). // (We start using it automatically in createKeysBackupVersion so this is symmetrical).
if (keysBackupVersion != null && version == keysBackupVersion!!.version) { if (keysBackupVersion != null && version == keysBackupVersion?.version) {
resetKeysBackupData() resetKeysBackupData()
keysBackupVersion = null keysBackupVersion = null
keysBackupStateManager.state = KeysBackupState.Unknown keysBackupStateManager.state = KeysBackupState.Unknown
@ -405,10 +410,7 @@ internal class DefaultKeysBackupService @Inject constructor(
val keysBackupVersionTrust = KeysBackupVersionTrust() val keysBackupVersionTrust = KeysBackupVersionTrust()
val authData = keysBackupVersion.getAuthDataAsMegolmBackupAuthData() val authData = keysBackupVersion.getAuthDataAsMegolmBackupAuthData()
if (keysBackupVersion.algorithm == null if (authData == null || authData.publicKey.isEmpty() || authData.signatures.isEmpty()) {
|| authData == null
|| authData.publicKey.isEmpty()
|| authData.signatures.isNullOrEmpty()) {
Timber.v("getKeysBackupTrust: Key backup is absent or missing required data") Timber.v("getKeysBackupTrust: Key backup is absent or missing required data")
return keysBackupVersionTrust return keysBackupVersionTrust
} }
@ -476,7 +478,7 @@ internal class DefaultKeysBackupService @Inject constructor(
cryptoCoroutineScope.launch(coroutineDispatchers.main) { cryptoCoroutineScope.launch(coroutineDispatchers.main) {
val updateKeysBackupVersionBody = withContext(coroutineDispatchers.crypto) { val updateKeysBackupVersionBody = withContext(coroutineDispatchers.crypto) {
// Get current signatures, or create an empty set // Get current signatures, or create an empty set
val myUserSignatures = authData.signatures?.get(userId)?.toMutableMap() ?: HashMap() val myUserSignatures = authData.signatures[userId].orEmpty().toMutableMap()
if (trust) { if (trust) {
// Add current device signature // Add current device signature
@ -495,26 +497,23 @@ internal class DefaultKeysBackupService @Inject constructor(
// Create an updated version of KeysVersionResult // Create an updated version of KeysVersionResult
val newMegolmBackupAuthData = authData.copy() val newMegolmBackupAuthData = authData.copy()
val newSignatures = newMegolmBackupAuthData.signatures!!.toMutableMap() val newSignatures = newMegolmBackupAuthData.signatures.toMutableMap()
newSignatures[userId] = myUserSignatures newSignatures[userId] = myUserSignatures
val newMegolmBackupAuthDataWithNewSignature = newMegolmBackupAuthData.copy( val newMegolmBackupAuthDataWithNewSignature = newMegolmBackupAuthData.copy(
signatures = newSignatures signatures = newSignatures
) )
val moshi = MoshiProvider.providesMoshi()
val adapter = moshi.adapter(Map::class.java)
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
UpdateKeysBackupVersionBody( UpdateKeysBackupVersionBody(
algorithm = keysBackupVersion.algorithm, algorithm = keysBackupVersion.algorithm,
authData = adapter.fromJson(newMegolmBackupAuthDataWithNewSignature.toJsonString()) as Map<String, Any>?, authData = newMegolmBackupAuthDataWithNewSignature.toJsonDict(),
version = keysBackupVersion.version!!) version = keysBackupVersion.version)
} }
// 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> { this.callback = 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
@ -596,7 +595,9 @@ internal class DefaultKeysBackupService @Inject constructor(
val importResult = awaitCallback<ImportRoomKeysResult> { val importResult = awaitCallback<ImportRoomKeysResult> {
restoreKeysWithRecoveryKey(keysBackupVersion, recoveryKey, null, null, null, it) restoreKeysWithRecoveryKey(keysBackupVersion, recoveryKey, null, null, null, it)
} }
withContext(coroutineDispatchers.crypto) {
cryptoStore.saveBackupRecoveryKey(recoveryKey, keysBackupVersion.version) cryptoStore.saveBackupRecoveryKey(recoveryKey, keysBackupVersion.version)
}
Timber.i("onSecretKeyGossip: Recovered keys ${importResult.successfullyNumberOfImportedKeys} out of ${importResult.totalNumberOfKeys}") Timber.i("onSecretKeyGossip: Recovered keys ${importResult.successfullyNumberOfImportedKeys} out of ${importResult.totalNumberOfKeys}")
} else { } else {
Timber.e("onSecretKeyGossip: Recovery key is not valid ${keysBackupVersion.version}") Timber.e("onSecretKeyGossip: Recovery key is not valid ${keysBackupVersion.version}")
@ -683,9 +684,9 @@ internal class DefaultKeysBackupService @Inject constructor(
stepProgressListener?.onStepProgress(StepProgressListener.Step.DownloadingKey) stepProgressListener?.onStepProgress(StepProgressListener.Step.DownloadingKey)
// Get backed up keys from the homeserver // Get backed up keys from the homeserver
val data = getKeys(sessionId, roomId, keysVersionResult.version!!) val data = getKeys(sessionId, roomId, keysVersionResult.version)
withContext(coroutineDispatchers.crypto) { withContext(coroutineDispatchers.computation) {
val sessionsData = ArrayList<MegolmSessionData>() val sessionsData = ArrayList<MegolmSessionData>()
// Restore that data // Restore that data
var sessionsFromHsCount = 0 var sessionsFromHsCount = 0
@ -1018,19 +1019,10 @@ internal class DefaultKeysBackupService @Inject constructor(
* @return the authentication if found and valid, null in other case * @return the authentication if found and valid, null in other case
*/ */
private fun getMegolmBackupAuthData(keysBackupData: KeysVersionResult): MegolmBackupAuthData? { private fun getMegolmBackupAuthData(keysBackupData: KeysVersionResult): MegolmBackupAuthData? {
if (keysBackupData.version.isNullOrBlank() return keysBackupData
|| keysBackupData.algorithm != MXCRYPTO_ALGORITHM_MEGOLM_BACKUP .takeIf { it.version.isNotEmpty() && it.algorithm == MXCRYPTO_ALGORITHM_MEGOLM_BACKUP }
|| keysBackupData.authData == null) { ?.getAuthDataAsMegolmBackupAuthData()
return null ?.takeIf { it.publicKey.isNotEmpty() }
}
val authData = keysBackupData.getAuthDataAsMegolmBackupAuthData()
if (authData?.signatures == null || authData.publicKey.isBlank()) {
return null
}
return authData
} }
/** /**
@ -1118,12 +1110,13 @@ internal class DefaultKeysBackupService @Inject constructor(
* @param keysVersionResult backup information object as returned by [getCurrentVersion]. * @param keysVersionResult backup information object as returned by [getCurrentVersion].
*/ */
private fun enableKeysBackup(keysVersionResult: KeysVersionResult) { private fun enableKeysBackup(keysVersionResult: KeysVersionResult) {
if (keysVersionResult.authData != null) {
val retrievedMegolmBackupAuthData = keysVersionResult.getAuthDataAsMegolmBackupAuthData() val retrievedMegolmBackupAuthData = keysVersionResult.getAuthDataAsMegolmBackupAuthData()
if (retrievedMegolmBackupAuthData != null) { if (retrievedMegolmBackupAuthData != null) {
keysBackupVersion = keysVersionResult keysBackupVersion = keysVersionResult
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
cryptoStore.setKeyBackupVersion(keysVersionResult.version) cryptoStore.setKeyBackupVersion(keysVersionResult.version)
}
onServerDataRetrieved(keysVersionResult.count, keysVersionResult.hash) onServerDataRetrieved(keysVersionResult.count, keysVersionResult.hash)
@ -1144,20 +1137,16 @@ internal class DefaultKeysBackupService @Inject constructor(
Timber.e("Invalid authentication data") Timber.e("Invalid authentication data")
keysBackupStateManager.state = KeysBackupState.Disabled keysBackupStateManager.state = KeysBackupState.Disabled
} }
} else {
Timber.e("Invalid authentication data")
keysBackupStateManager.state = KeysBackupState.Disabled
}
} }
/** /**
* Update the DB with data fetch from the server * Update the DB with data fetch from the server
*/ */
private fun onServerDataRetrieved(count: Int?, hash: String?) { private fun onServerDataRetrieved(count: Int?, etag: String?) {
cryptoStore.setKeysBackupData(KeysBackupDataEntity() cryptoStore.setKeysBackupData(KeysBackupDataEntity()
.apply { .apply {
backupLastServerNumberOfKeys = count backupLastServerNumberOfKeys = count
backupLastServerHash = hash backupLastServerHash = etag
} }
) )
} }
@ -1172,6 +1161,7 @@ internal class DefaultKeysBackupService @Inject constructor(
cryptoStore.setKeyBackupVersion(null) cryptoStore.setKeyBackupVersion(null)
cryptoStore.setKeysBackupData(null) cryptoStore.setKeysBackupData(null)
backupOlmPkEncryption?.releaseEncryption()
backupOlmPkEncryption = null backupOlmPkEncryption = null
// Reset backup markers // Reset backup markers
@ -1222,22 +1212,19 @@ internal class DefaultKeysBackupService @Inject constructor(
// Gather data to send to the homeserver // Gather data to send to the homeserver
// roomId -> sessionId -> MXKeyBackupData // roomId -> sessionId -> MXKeyBackupData
val keysBackupData = KeysBackupData( val keysBackupData = KeysBackupData()
roomIdToRoomKeysBackupData = HashMap()
)
for (olmInboundGroupSessionWrapper in olmInboundGroupSessionWrappers) { olmInboundGroupSessionWrappers.forEach { olmInboundGroupSessionWrapper ->
val keyBackupData = encryptGroupSession(olmInboundGroupSessionWrapper) val roomId = olmInboundGroupSessionWrapper.roomId ?: return@forEach
if (keysBackupData.roomIdToRoomKeysBackupData[olmInboundGroupSessionWrapper.roomId] == null) { val olmInboundGroupSession = olmInboundGroupSessionWrapper.olmInboundGroupSession ?: return@forEach
val roomKeysBackupData = RoomKeysBackupData(
sessionIdToKeyBackupData = HashMap()
)
keysBackupData.roomIdToRoomKeysBackupData[olmInboundGroupSessionWrapper.roomId!!] = roomKeysBackupData
}
try { try {
keysBackupData.roomIdToRoomKeysBackupData[olmInboundGroupSessionWrapper.roomId]!! encryptGroupSession(olmInboundGroupSessionWrapper)
.sessionIdToKeyBackupData[olmInboundGroupSessionWrapper.olmInboundGroupSession!!.sessionIdentifier()] = keyBackupData ?.let {
keysBackupData.roomIdToRoomKeysBackupData
.getOrPut(roomId) { RoomKeysBackupData() }
.sessionIdToKeyBackupData[olmInboundGroupSession.sessionIdentifier()] = it
}
} catch (e: OlmException) { } catch (e: OlmException) {
Timber.e(e, "OlmException") Timber.e(e, "OlmException")
} }
@ -1245,7 +1232,12 @@ internal class DefaultKeysBackupService @Inject constructor(
Timber.v("backupKeys: 4 - Sending request") Timber.v("backupKeys: 4 - Sending request")
val sendingRequestCallback = object : MatrixCallback<BackupKeysResult> { // Make the request
val version = keysBackupVersion?.version ?: return@withContext
storeSessionDataTask
.configureWith(StoreSessionsDataTask.Params(version, keysBackupData)) {
this.callback = object : MatrixCallback<BackupKeysResult> {
override fun onSuccess(data: BackupKeysResult) { override fun onSuccess(data: BackupKeysResult) {
uiHandler.post { uiHandler.post {
Timber.v("backupKeys: 5a - Request complete") Timber.v("backupKeys: 5a - Request complete")
@ -1305,11 +1297,6 @@ internal class DefaultKeysBackupService @Inject constructor(
} }
} }
} }
// Make the request
storeSessionDataTask
.configureWith(StoreSessionsDataTask.Params(keysBackupVersion!!.version!!, keysBackupData)) {
this.callback = sendingRequestCallback
} }
.executeBy(taskExecutor) .executeBy(taskExecutor)
} }
@ -1318,47 +1305,45 @@ internal class DefaultKeysBackupService @Inject constructor(
@VisibleForTesting @VisibleForTesting
@WorkerThread @WorkerThread
fun encryptGroupSession(olmInboundGroupSessionWrapper: OlmInboundGroupSessionWrapper2): KeyBackupData { fun encryptGroupSession(olmInboundGroupSessionWrapper: OlmInboundGroupSessionWrapper2): KeyBackupData? {
// Gather information for each key // Gather information for each key
val device = cryptoStore.deviceWithIdentityKey(olmInboundGroupSessionWrapper.senderKey!!) val device = olmInboundGroupSessionWrapper.senderKey?.let { cryptoStore.deviceWithIdentityKey(it) }
// Build the m.megolm_backup.v1.curve25519-aes-sha2 data as defined at // Build the m.megolm_backup.v1.curve25519-aes-sha2 data as defined at
// https://github.com/uhoreg/matrix-doc/blob/e2e_backup/proposals/1219-storing-megolm-keys-serverside.md#mmegolm_backupv1curve25519-aes-sha2-key-format // https://github.com/uhoreg/matrix-doc/blob/e2e_backup/proposals/1219-storing-megolm-keys-serverside.md#mmegolm_backupv1curve25519-aes-sha2-key-format
val sessionData = olmInboundGroupSessionWrapper.exportKeys() val sessionData = olmInboundGroupSessionWrapper.exportKeys() ?: return null
val sessionBackupData = mapOf( val sessionBackupData = mapOf(
"algorithm" to sessionData!!.algorithm, "algorithm" to sessionData.algorithm,
"sender_key" to sessionData.senderKey, "sender_key" to sessionData.senderKey,
"sender_claimed_keys" to sessionData.senderClaimedKeys, "sender_claimed_keys" to sessionData.senderClaimedKeys,
"forwarding_curve25519_key_chain" to (sessionData.forwardingCurve25519KeyChain "forwarding_curve25519_key_chain" to (sessionData.forwardingCurve25519KeyChain.orEmpty()),
?: ArrayList<Any>()),
"session_key" to sessionData.sessionKey) "session_key" to sessionData.sessionKey)
var encryptedSessionBackupData: OlmPkMessage? = null val json = MoshiProvider.providesMoshi()
.adapter(Map::class.java)
.toJson(sessionBackupData)
val moshi = MoshiProvider.providesMoshi() val encryptedSessionBackupData = try {
val adapter = moshi.adapter(Map::class.java) backupOlmPkEncryption?.encrypt(json)
try {
val json = adapter.toJson(sessionBackupData)
encryptedSessionBackupData = backupOlmPkEncryption?.encrypt(json)
} catch (e: OlmException) { } catch (e: OlmException) {
Timber.e(e, "OlmException") Timber.e(e, "OlmException")
null
} }
?: return null
// Build backup data for that key // Build backup data for that key
return KeyBackupData( return KeyBackupData(
firstMessageIndex = try { firstMessageIndex = try {
olmInboundGroupSessionWrapper.olmInboundGroupSession!!.firstKnownIndex olmInboundGroupSessionWrapper.olmInboundGroupSession?.firstKnownIndex ?: 0
} catch (e: OlmException) { } catch (e: OlmException) {
Timber.e(e, "OlmException") Timber.e(e, "OlmException")
0L 0L
}, },
forwardedCount = olmInboundGroupSessionWrapper.forwardingCurve25519KeyChain!!.size, forwardedCount = olmInboundGroupSessionWrapper.forwardingCurve25519KeyChain.orEmpty().size,
isVerified = device?.isVerified == true, isVerified = device?.isVerified == true,
sessionData = mapOf( sessionData = mapOf(
"ciphertext" to encryptedSessionBackupData!!.mCipherText, "ciphertext" to encryptedSessionBackupData.mCipherText,
"mac" to encryptedSessionBackupData.mMac, "mac" to encryptedSessionBackupData.mMac,
"ephemeral" to encryptedSessionBackupData.mEphemeralKey) "ephemeral" to encryptedSessionBackupData.mEphemeralKey)
) )
@ -1371,9 +1356,9 @@ internal class DefaultKeysBackupService @Inject constructor(
val jsonObject = keyBackupData.sessionData val jsonObject = keyBackupData.sessionData
val ciphertext = jsonObject?.get("ciphertext")?.toString() val ciphertext = jsonObject["ciphertext"]?.toString()
val mac = jsonObject?.get("mac")?.toString() val mac = jsonObject["mac"]?.toString()
val ephemeralKey = jsonObject?.get("ephemeral")?.toString() val ephemeralKey = jsonObject["ephemeral"]?.toString()
if (ciphertext != null && mac != null && ephemeralKey != null) { if (ciphertext != null && mac != null && ephemeralKey != null) {
val encrypted = OlmPkMessage() val encrypted = OlmPkMessage()
@ -1418,8 +1403,7 @@ internal class DefaultKeysBackupService @Inject constructor(
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
val createKeysBackupVersionBody = CreateKeysBackupVersionBody( val createKeysBackupVersionBody = CreateKeysBackupVersionBody(
algorithm = keysBackupCreationInfo.algorithm, algorithm = keysBackupCreationInfo.algorithm,
authData = MoshiProvider.providesMoshi().adapter(Map::class.java) authData = keysBackupCreationInfo.authData.toJsonDict()
.fromJson(keysBackupCreationInfo.authData?.toJsonString() ?: "") as JsonDict?
) )
createKeysBackupVersionTask createKeysBackupVersionTask

View File

@ -35,7 +35,7 @@ import retrofit2.http.Path
import retrofit2.http.Query import retrofit2.http.Query
/** /**
* Ref: https://github.com/uhoreg/matrix-doc/blob/e2e_backup/proposals/1219-storing-megolm-keys-serverside.md * Ref: https://matrix.org/docs/spec/client_server/unstable#server-side-key-backups
*/ */
internal interface RoomKeysApi { internal interface RoomKeysApi {

View File

@ -18,6 +18,7 @@ package org.matrix.android.sdk.internal.crypto.keysbackup.model
import com.squareup.moshi.Json import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
import org.matrix.android.sdk.api.util.JsonDict
import org.matrix.android.sdk.internal.di.MoshiProvider import org.matrix.android.sdk.internal.di.MoshiProvider
/** /**
@ -30,7 +31,7 @@ data class MegolmBackupAuthData(
* The curve25519 public key used to encrypt the backups. * The curve25519 public key used to encrypt the backups.
*/ */
@Json(name = "public_key") @Json(name = "public_key")
val publicKey: String = "", val publicKey: String,
/** /**
* In case of a backup created from a password, the salt associated with the backup * In case of a backup created from a password, the salt associated with the backup
@ -50,20 +51,38 @@ data class MegolmBackupAuthData(
* userId -> (deviceSignKeyId -> signature) * userId -> (deviceSignKeyId -> signature)
*/ */
@Json(name = "signatures") @Json(name = "signatures")
val signatures: Map<String, Map<String, String>>? = null val signatures: Map<String, Map<String, String>>
) { ) {
fun toJsonString(): String { fun toJsonDict(): JsonDict {
return MoshiProvider.providesMoshi() val moshi = MoshiProvider.providesMoshi()
val adapter = moshi.adapter(Map::class.java)
return moshi
.adapter(MegolmBackupAuthData::class.java) .adapter(MegolmBackupAuthData::class.java)
.toJson(this) .toJson(this)
.let {
@Suppress("UNCHECKED_CAST")
adapter.fromJson(it) as JsonDict
}
} }
/** fun signalableJSONDictionary(): JsonDict {
* Same as the parent [MXJSONModel JSONDictionary] but return only return SignalableMegolmBackupAuthData(
* data that must be signed. publicKey = publicKey,
*/ privateKeySalt = privateKeySalt,
fun signalableJSONDictionary(): Map<String, Any> = HashMap<String, Any>().apply { privateKeyIterations = privateKeyIterations
)
.signalableJSONDictionary()
}
}
internal data class SignalableMegolmBackupAuthData(
val publicKey: String,
val privateKeySalt: String? = null,
val privateKeyIterations: Int? = null
) {
fun signalableJSONDictionary(): JsonDict = HashMap<String, Any>().apply {
put("public_key", publicKey) put("public_key", publicKey)
privateKeySalt?.let { privateKeySalt?.let {

View File

@ -23,15 +23,15 @@ data class MegolmBackupCreationInfo(
/** /**
* The algorithm used for storing backups [org.matrix.androidsdk.crypto.MXCRYPTO_ALGORITHM_MEGOLM_BACKUP]. * The algorithm used for storing backups [org.matrix.androidsdk.crypto.MXCRYPTO_ALGORITHM_MEGOLM_BACKUP].
*/ */
val algorithm: String = "", val algorithm: String,
/** /**
* Authentication data. * Authentication data.
*/ */
val authData: MegolmBackupAuthData? = null, val authData: MegolmBackupAuthData,
/** /**
* The Base58 recovery key. * The Base58 recovery key.
*/ */
val recoveryKey: String = "" val recoveryKey: String
) )

View File

@ -16,15 +16,16 @@
package org.matrix.android.sdk.internal.crypto.keysbackup.model.rest package org.matrix.android.sdk.internal.crypto.keysbackup.model.rest
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class BackupKeysResult( internal data class BackupKeysResult(
// The hash value which is an opaque string representing stored keys in the backup // The hash value which is an opaque string representing stored keys in the backup
var hash: String? = null, @Json(name = "etag")
val hash: String,
// The number of keys stored in the backup. // The number of keys stored in the backup.
var count: Int? = null @Json(name = "count")
val count: Int
) )

View File

@ -21,17 +21,17 @@ import com.squareup.moshi.JsonClass
import org.matrix.android.sdk.api.util.JsonDict import org.matrix.android.sdk.api.util.JsonDict
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class CreateKeysBackupVersionBody( internal data class CreateKeysBackupVersionBody(
/** /**
* The algorithm used for storing backups. Currently, only "m.megolm_backup.v1.curve25519-aes-sha2" is defined * The algorithm used for storing backups. Currently, only "m.megolm_backup.v1.curve25519-aes-sha2" is defined
*/ */
@Json(name = "algorithm") @Json(name = "algorithm")
override val algorithm: String? = null, override val algorithm: String,
/** /**
* algorithm-dependent data, for "m.megolm_backup.v1.curve25519-aes-sha2" * algorithm-dependent data, for "m.megolm_backup.v1.curve25519-aes-sha2"
* see [org.matrix.android.sdk.internal.crypto.keysbackup.MegolmBackupAuthData] * see [org.matrix.android.sdk.internal.crypto.keysbackup.MegolmBackupAuthData]
*/ */
@Json(name = "auth_data") @Json(name = "auth_data")
override val authData: JsonDict? = null override val authData: JsonDict
) : KeysAlgorithmAndData ) : KeysAlgorithmAndData

View File

@ -18,7 +18,7 @@ package org.matrix.android.sdk.internal.crypto.keysbackup.model.rest
import com.squareup.moshi.Json import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
import org.matrix.android.sdk.internal.di.MoshiProvider import org.matrix.android.sdk.api.util.JsonDict
import org.matrix.android.sdk.internal.network.parsing.ForceToBoolean import org.matrix.android.sdk.internal.network.parsing.ForceToBoolean
/** /**
@ -30,13 +30,13 @@ data class KeyBackupData(
* Required. The index of the first message in the session that the key can decrypt. * Required. The index of the first message in the session that the key can decrypt.
*/ */
@Json(name = "first_message_index") @Json(name = "first_message_index")
val firstMessageIndex: Long = 0, val firstMessageIndex: Long,
/** /**
* Required. The number of times this key has been forwarded. * Required. The number of times this key has been forwarded.
*/ */
@Json(name = "forwarded_count") @Json(name = "forwarded_count")
val forwardedCount: Int = 0, val forwardedCount: Int,
/** /**
* Whether the device backing up the key has verified the device that the key is from. * Whether the device backing up the key has verified the device that the key is from.
@ -44,16 +44,11 @@ data class KeyBackupData(
*/ */
@ForceToBoolean @ForceToBoolean
@Json(name = "is_verified") @Json(name = "is_verified")
val isVerified: Boolean = false, val isVerified: Boolean,
/** /**
* Algorithm-dependent data. * Algorithm-dependent data.
*/ */
@Json(name = "session_data") @Json(name = "session_data")
val sessionData: Map<String, Any>? = null val sessionData: JsonDict
) { )
fun toJsonString(): String {
return MoshiProvider.providesMoshi().adapter(KeyBackupData::class.java).toJson(this)
}
}

View File

@ -17,6 +17,7 @@
package org.matrix.android.sdk.internal.crypto.keysbackup.model.rest package org.matrix.android.sdk.internal.crypto.keysbackup.model.rest
import org.matrix.android.sdk.api.util.JsonDict import org.matrix.android.sdk.api.util.JsonDict
import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM_BACKUP
import org.matrix.android.sdk.internal.crypto.keysbackup.model.MegolmBackupAuthData import org.matrix.android.sdk.internal.crypto.keysbackup.model.MegolmBackupAuthData
import org.matrix.android.sdk.internal.di.MoshiProvider import org.matrix.android.sdk.internal.di.MoshiProvider
@ -37,24 +38,25 @@ import org.matrix.android.sdk.internal.di.MoshiProvider
* } * }
* </pre> * </pre>
*/ */
interface KeysAlgorithmAndData { internal interface KeysAlgorithmAndData {
/** /**
* The algorithm used for storing backups. Currently, only "m.megolm_backup.v1.curve25519-aes-sha2" is defined * The algorithm used for storing backups. Currently, only "m.megolm_backup.v1.curve25519-aes-sha2" is defined
*/ */
val algorithm: String? val algorithm: String
/** /**
* algorithm-dependent data, for "m.megolm_backup.v1.curve25519-aes-sha2" see [org.matrix.android.sdk.internal.crypto.keysbackup.MegolmBackupAuthData] * algorithm-dependent data, for "m.megolm_backup.v1.curve25519-aes-sha2" see [org.matrix.android.sdk.internal.crypto.keysbackup.MegolmBackupAuthData]
*/ */
val authData: JsonDict? val authData: JsonDict
/** /**
* Facility method to convert authData to a MegolmBackupAuthData object * Facility method to convert authData to a MegolmBackupAuthData object
*/ */
fun getAuthDataAsMegolmBackupAuthData(): MegolmBackupAuthData? { fun getAuthDataAsMegolmBackupAuthData(): MegolmBackupAuthData? {
return MoshiProvider.providesMoshi() return MoshiProvider.providesMoshi()
.adapter(MegolmBackupAuthData::class.java) .takeIf { algorithm == MXCRYPTO_ALGORITHM_MEGOLM_BACKUP }
.fromJsonValue(authData) ?.adapter(MegolmBackupAuthData::class.java)
?.fromJsonValue(authData)
} }
} }

View File

@ -23,5 +23,5 @@ import com.squareup.moshi.JsonClass
data class KeysVersion( data class KeysVersion(
// the keys backup version // the keys backup version
@Json(name = "version") @Json(name = "version")
val version: String? = null val version: String
) )

View File

@ -26,24 +26,24 @@ data class KeysVersionResult(
* The algorithm used for storing backups. Currently, only "m.megolm_backup.v1.curve25519-aes-sha2" is defined * The algorithm used for storing backups. Currently, only "m.megolm_backup.v1.curve25519-aes-sha2" is defined
*/ */
@Json(name = "algorithm") @Json(name = "algorithm")
override val algorithm: String? = null, override val algorithm: String,
/** /**
* algorithm-dependent data, for "m.megolm_backup.v1.curve25519-aes-sha2" * algorithm-dependent data, for "m.megolm_backup.v1.curve25519-aes-sha2"
* see [org.matrix.android.sdk.internal.crypto.keysbackup.MegolmBackupAuthData] * see [org.matrix.android.sdk.internal.crypto.keysbackup.MegolmBackupAuthData]
*/ */
@Json(name = "auth_data") @Json(name = "auth_data")
override val authData: JsonDict? = null, override val authData: JsonDict,
// the backup version // the backup version
@Json(name = "version") @Json(name = "version")
val version: String? = null, val version: String,
// The hash value which is an opaque string representing stored keys in the backup // The hash value which is an opaque string representing stored keys in the backup
@Json(name = "hash") @Json(name = "etag")
val hash: String? = null, val hash: String,
// The number of keys stored in the backup. // The number of keys stored in the backup.
@Json(name = "count") @Json(name = "count")
val count: Int? = null val count: Int
) : KeysAlgorithmAndData ) : KeysAlgorithmAndData

View File

@ -26,16 +26,16 @@ data class UpdateKeysBackupVersionBody(
* The algorithm used for storing backups. Currently, only "m.megolm_backup.v1.curve25519-aes-sha2" is defined * The algorithm used for storing backups. Currently, only "m.megolm_backup.v1.curve25519-aes-sha2" is defined
*/ */
@Json(name = "algorithm") @Json(name = "algorithm")
override val algorithm: String? = null, override val algorithm: String,
/** /**
* algorithm-dependent data, for "m.megolm_backup.v1.curve25519-aes-sha2" * algorithm-dependent data, for "m.megolm_backup.v1.curve25519-aes-sha2"
* see [org.matrix.android.sdk.internal.crypto.keysbackup.MegolmBackupAuthData] * see [org.matrix.android.sdk.internal.crypto.keysbackup.MegolmBackupAuthData]
*/ */
@Json(name = "auth_data") @Json(name = "auth_data")
override val authData: JsonDict? = null, override val authData: JsonDict,
// the backup version, mandatory // Optional. The backup version. If present, must be the same as the path parameter.
@Json(name = "version") @Json(name = "version")
val version: String val version: String? = null
) : KeysAlgorithmAndData ) : KeysAlgorithmAndData

View File

@ -48,17 +48,14 @@ class OlmInboundGroupSessionWrapper2 : Serializable {
*/ */
val firstKnownIndex: Long? val firstKnownIndex: Long?
get() { get() {
if (null != olmInboundGroupSession) { return try {
try { olmInboundGroupSession?.firstKnownIndex
return olmInboundGroupSession!!.firstKnownIndex
} catch (e: Exception) { } catch (e: Exception) {
Timber.e(e, "## getFirstKnownIndex() : getFirstKnownIndex failed") Timber.e(e, "## getFirstKnownIndex() : getFirstKnownIndex failed")
null
} }
} }
return null
}
/** /**
* Constructor * Constructor
* *
@ -90,11 +87,13 @@ class OlmInboundGroupSessionWrapper2 : Serializable {
@Throws(Exception::class) @Throws(Exception::class)
constructor(megolmSessionData: MegolmSessionData) { constructor(megolmSessionData: MegolmSessionData) {
try { try {
olmInboundGroupSession = OlmInboundGroupSession.importSession(megolmSessionData.sessionKey!!) val safeSessionKey = megolmSessionData.sessionKey ?: throw Exception("invalid data")
olmInboundGroupSession = OlmInboundGroupSession.importSession(safeSessionKey)
if (olmInboundGroupSession!!.sessionIdentifier() != megolmSessionData.sessionId) { .also {
if (it.sessionIdentifier() != megolmSessionData.sessionId) {
throw Exception("Mismatched group session Id") throw Exception("Mismatched group session Id")
} }
}
senderKey = megolmSessionData.senderKey senderKey = megolmSessionData.senderKey
keysClaimed = megolmSessionData.senderClaimedKeys keysClaimed = megolmSessionData.senderClaimedKeys
@ -120,16 +119,18 @@ class OlmInboundGroupSessionWrapper2 : Serializable {
return null return null
} }
val wantedIndex = index ?: olmInboundGroupSession!!.firstKnownIndex val safeOlmInboundGroupSession = olmInboundGroupSession ?: return null
val wantedIndex = index ?: safeOlmInboundGroupSession.firstKnownIndex
MegolmSessionData( MegolmSessionData(
senderClaimedEd25519Key = keysClaimed?.get("ed25519"), senderClaimedEd25519Key = keysClaimed?.get("ed25519"),
forwardingCurve25519KeyChain = ArrayList(forwardingCurve25519KeyChain!!), forwardingCurve25519KeyChain = forwardingCurve25519KeyChain?.toList().orEmpty(),
senderKey = senderKey, senderKey = senderKey,
senderClaimedKeys = keysClaimed, senderClaimedKeys = keysClaimed,
roomId = roomId, roomId = roomId,
sessionId = olmInboundGroupSession!!.sessionIdentifier(), sessionId = safeOlmInboundGroupSession.sessionIdentifier(),
sessionKey = olmInboundGroupSession!!.export(wantedIndex), sessionKey = safeOlmInboundGroupSession.export(wantedIndex),
algorithm = MXCRYPTO_ALGORITHM_MEGOLM algorithm = MXCRYPTO_ALGORITHM_MEGOLM
) )
} catch (e: Exception) { } catch (e: Exception) {
@ -145,14 +146,11 @@ class OlmInboundGroupSessionWrapper2 : Serializable {
* @return the exported data * @return the exported data
*/ */
fun exportSession(messageIndex: Long): String? { fun exportSession(messageIndex: Long): String? {
if (null != olmInboundGroupSession) { return try {
try { return olmInboundGroupSession?.export(messageIndex)
return olmInboundGroupSession!!.export(messageIndex)
} catch (e: Exception) { } catch (e: Exception) {
Timber.e(e, "## exportSession() : export failed") Timber.e(e, "## exportSession() : export failed")
null
} }
} }
return null
}
} }

View File

@ -17,6 +17,7 @@
package org.matrix.android.sdk.internal.crypto.store package org.matrix.android.sdk.internal.crypto.store
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.paging.PagedList
import org.matrix.android.sdk.api.session.crypto.crosssigning.MXCrossSigningInfo import org.matrix.android.sdk.api.session.crypto.crosssigning.MXCrossSigningInfo
import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.util.Optional import org.matrix.android.sdk.api.util.Optional
@ -126,7 +127,10 @@ internal interface IMXCryptoStore {
fun getPendingIncomingRoomKeyRequests(): List<IncomingRoomKeyRequest> fun getPendingIncomingRoomKeyRequests(): List<IncomingRoomKeyRequest>
fun getPendingIncomingGossipingRequests(): List<IncomingShareRequestCommon> fun getPendingIncomingGossipingRequests(): List<IncomingShareRequestCommon>
fun storeIncomingGossipingRequest(request: IncomingShareRequestCommon, ageLocalTS: Long?) fun storeIncomingGossipingRequest(request: IncomingShareRequestCommon, ageLocalTS: Long?)
fun storeIncomingGossipingRequests(requests: List<IncomingShareRequestCommon>)
// fun getPendingIncomingSecretShareRequests(): List<IncomingSecretShareRequest> // fun getPendingIncomingSecretShareRequests(): List<IncomingSecretShareRequest>
/** /**
@ -364,6 +368,7 @@ internal interface IMXCryptoStore {
fun getOrAddOutgoingSecretShareRequest(secretName: String, recipients: Map<String, List<String>>): OutgoingSecretRequest? fun getOrAddOutgoingSecretShareRequest(secretName: String, recipients: Map<String, List<String>>): OutgoingSecretRequest?
fun saveGossipingEvent(event: Event) fun saveGossipingEvent(event: Event)
fun saveGossipingEvents(events: List<Event>)
fun updateGossipingRequestState(request: IncomingShareRequestCommon, state: GossipingRequestState) { fun updateGossipingRequestState(request: IncomingShareRequestCommon, state: GossipingRequestState) {
updateGossipingRequestState( updateGossipingRequestState(
@ -441,11 +446,16 @@ internal interface IMXCryptoStore {
// Dev tools // Dev tools
fun getOutgoingRoomKeyRequests(): List<OutgoingRoomKeyRequest> fun getOutgoingRoomKeyRequests(): List<OutgoingRoomKeyRequest>
fun getOutgoingRoomKeyRequestsPaged(): LiveData<PagedList<OutgoingRoomKeyRequest>>
fun getOutgoingSecretKeyRequests(): List<OutgoingSecretRequest> fun getOutgoingSecretKeyRequests(): List<OutgoingSecretRequest>
fun getOutgoingSecretRequest(secretName: String): OutgoingSecretRequest? fun getOutgoingSecretRequest(secretName: String): OutgoingSecretRequest?
fun getIncomingRoomKeyRequests(): List<IncomingRoomKeyRequest> fun getIncomingRoomKeyRequests(): List<IncomingRoomKeyRequest>
fun getGossipingEventsTrail(): List<Event> fun getIncomingRoomKeyRequestsPaged(): LiveData<PagedList<IncomingRoomKeyRequest>>
fun getGossipingEventsTrail(): LiveData<PagedList<Event>>
fun getGossipingEvents(): List<Event>
fun setDeviceKeysUploaded(uploaded: Boolean) fun setDeviceKeysUploaded(uploaded: Boolean)
fun getDeviceKeysUploaded(): Boolean fun getDeviceKeysUploaded(): Boolean
fun tidyUpDataBase()
fun logDbUsageInfo()
} }

View File

@ -18,6 +18,8 @@ package org.matrix.android.sdk.internal.crypto.store.db
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.Transformations import androidx.lifecycle.Transformations
import androidx.paging.LivePagedListBuilder
import androidx.paging.PagedList
import com.zhuinden.monarchy.Monarchy import com.zhuinden.monarchy.Monarchy
import io.realm.Realm import io.realm.Realm
import io.realm.RealmConfiguration import io.realm.RealmConfiguration
@ -62,6 +64,7 @@ import org.matrix.android.sdk.internal.crypto.store.db.model.CryptoRoomEntityFie
import org.matrix.android.sdk.internal.crypto.store.db.model.DeviceInfoEntity import org.matrix.android.sdk.internal.crypto.store.db.model.DeviceInfoEntity
import org.matrix.android.sdk.internal.crypto.store.db.model.DeviceInfoEntityFields import org.matrix.android.sdk.internal.crypto.store.db.model.DeviceInfoEntityFields
import org.matrix.android.sdk.internal.crypto.store.db.model.GossipingEventEntity import org.matrix.android.sdk.internal.crypto.store.db.model.GossipingEventEntity
import org.matrix.android.sdk.internal.crypto.store.db.model.GossipingEventEntityFields
import org.matrix.android.sdk.internal.crypto.store.db.model.IncomingGossipingRequestEntity import org.matrix.android.sdk.internal.crypto.store.db.model.IncomingGossipingRequestEntity
import org.matrix.android.sdk.internal.crypto.store.db.model.IncomingGossipingRequestEntityFields import org.matrix.android.sdk.internal.crypto.store.db.model.IncomingGossipingRequestEntityFields
import org.matrix.android.sdk.internal.crypto.store.db.model.KeysBackupDataEntity import org.matrix.android.sdk.internal.crypto.store.db.model.KeysBackupDataEntity
@ -84,6 +87,7 @@ import org.matrix.android.sdk.internal.crypto.store.db.query.get
import org.matrix.android.sdk.internal.crypto.store.db.query.getById import org.matrix.android.sdk.internal.crypto.store.db.query.getById
import org.matrix.android.sdk.internal.crypto.store.db.query.getOrCreate import org.matrix.android.sdk.internal.crypto.store.db.query.getOrCreate
import org.matrix.android.sdk.internal.database.mapper.ContentMapper import org.matrix.android.sdk.internal.database.mapper.ContentMapper
import org.matrix.android.sdk.internal.database.tools.RealmDebugTools
import org.matrix.android.sdk.internal.di.CryptoDatabase import org.matrix.android.sdk.internal.di.CryptoDatabase
import org.matrix.android.sdk.internal.di.DeviceId import org.matrix.android.sdk.internal.di.DeviceId
import org.matrix.android.sdk.internal.di.MoshiProvider import org.matrix.android.sdk.internal.di.MoshiProvider
@ -998,7 +1002,50 @@ internal class RealmCryptoStore @Inject constructor(
} }
} }
override fun getGossipingEventsTrail(): List<Event> { override fun getIncomingRoomKeyRequestsPaged(): LiveData<PagedList<IncomingRoomKeyRequest>> {
val realmDataSourceFactory = monarchy.createDataSourceFactory { realm ->
realm.where<IncomingGossipingRequestEntity>()
.equalTo(IncomingGossipingRequestEntityFields.TYPE_STR, GossipRequestType.KEY.name)
.sort(IncomingGossipingRequestEntityFields.LOCAL_CREATION_TIMESTAMP, Sort.DESCENDING)
}
val dataSourceFactory = realmDataSourceFactory.map {
it.toIncomingGossipingRequest() as? IncomingRoomKeyRequest
?: IncomingRoomKeyRequest(
requestBody = null,
deviceId = "",
userId = "",
requestId = "",
state = GossipingRequestState.NONE,
localCreationTimestamp = 0
)
}
return monarchy.findAllPagedWithChanges(realmDataSourceFactory,
LivePagedListBuilder(dataSourceFactory,
PagedList.Config.Builder()
.setPageSize(20)
.setEnablePlaceholders(false)
.setPrefetchDistance(1)
.build())
)
}
override fun getGossipingEventsTrail(): LiveData<PagedList<Event>> {
val realmDataSourceFactory = monarchy.createDataSourceFactory { realm ->
realm.where<GossipingEventEntity>().sort(GossipingEventEntityFields.AGE_LOCAL_TS, Sort.DESCENDING)
}
val dataSourceFactory = realmDataSourceFactory.map { it.toModel() }
val trail = monarchy.findAllPagedWithChanges(realmDataSourceFactory,
LivePagedListBuilder(dataSourceFactory,
PagedList.Config.Builder()
.setPageSize(20)
.setEnablePlaceholders(false)
.setPrefetchDistance(1)
.build())
)
return trail
}
override fun getGossipingEvents(): List<Event> {
return monarchy.fetchAllCopiedSync { realm -> return monarchy.fetchAllCopiedSync { realm ->
realm.where<GossipingEventEntity>() realm.where<GossipingEventEntity>()
}.map { }.map {
@ -1066,7 +1113,28 @@ internal class RealmCryptoStore @Inject constructor(
return request return request
} }
override fun saveGossipingEvents(events: List<Event>) {
val now = System.currentTimeMillis()
monarchy.writeAsync { realm ->
events.forEach { event ->
val ageLocalTs = event.unsignedData?.age?.let { now - it } ?: now
val entity = GossipingEventEntity(
type = event.type,
sender = event.senderId,
ageLocalTs = ageLocalTs,
content = ContentMapper.map(event.content)
).apply {
sendState = SendState.SYNCED
decryptionResultJson = MoshiProvider.providesMoshi().adapter(OlmDecryptionResult::class.java).toJson(event.mxDecryptionResult)
decryptionErrorCode = event.mCryptoError?.name
}
realm.insertOrUpdate(entity)
}
}
}
override fun saveGossipingEvent(event: Event) { override fun saveGossipingEvent(event: Event) {
monarchy.writeAsync { realm ->
val now = System.currentTimeMillis() val now = System.currentTimeMillis()
val ageLocalTs = event.unsignedData?.age?.let { now - it } ?: now val ageLocalTs = event.unsignedData?.age?.let { now - it } ?: now
val entity = GossipingEventEntity( val entity = GossipingEventEntity(
@ -1076,14 +1144,12 @@ internal class RealmCryptoStore @Inject constructor(
content = ContentMapper.map(event.content) content = ContentMapper.map(event.content)
).apply { ).apply {
sendState = SendState.SYNCED sendState = SendState.SYNCED
decryptionResultJson = MoshiProvider.providesMoshi().adapter<OlmDecryptionResult>(OlmDecryptionResult::class.java).toJson(event.mxDecryptionResult) decryptionResultJson = MoshiProvider.providesMoshi().adapter(OlmDecryptionResult::class.java).toJson(event.mxDecryptionResult)
decryptionErrorCode = event.mCryptoError?.name decryptionErrorCode = event.mCryptoError?.name
} }
doRealmTransaction(realmConfiguration) { realm ->
realm.insertOrUpdate(entity) realm.insertOrUpdate(entity)
} }
} }
// override fun getOutgoingRoomKeyRequestByState(states: Set<ShareRequestState>): OutgoingRoomKeyRequest? { // override fun getOutgoingRoomKeyRequestByState(states: Set<ShareRequestState>): OutgoingRoomKeyRequest? {
// val statesIndex = states.map { it.ordinal }.toTypedArray() // val statesIndex = states.map { it.ordinal }.toTypedArray()
// return doRealmQueryAndCopy(realmConfiguration) { realm -> // return doRealmQueryAndCopy(realmConfiguration) { realm ->
@ -1284,6 +1350,28 @@ internal class RealmCryptoStore @Inject constructor(
} }
} }
override fun storeIncomingGossipingRequests(requests: List<IncomingShareRequestCommon>) {
doRealmTransactionAsync(realmConfiguration) { realm ->
requests.forEach { request ->
// After a clear cache, we might have a
realm.createObject(IncomingGossipingRequestEntity::class.java).let {
it.otherDeviceId = request.deviceId
it.otherUserId = request.userId
it.requestId = request.requestId ?: ""
it.requestState = GossipingRequestState.PENDING
it.localCreationTimestamp = request.localCreationTimestamp ?: System.currentTimeMillis()
if (request is IncomingSecretShareRequest) {
it.type = GossipRequestType.SECRET
it.requestedInfoStr = request.secretName
} else if (request is IncomingRoomKeyRequest) {
it.type = GossipRequestType.KEY
it.requestedInfoStr = request.requestBody?.toJson()
}
}
}
}
}
// override fun getPendingIncomingSecretShareRequests(): List<IncomingSecretShareRequest> { // override fun getPendingIncomingSecretShareRequests(): List<IncomingSecretShareRequest> {
// return doRealmQueryAndCopyList(realmConfiguration) { // return doRealmQueryAndCopyList(realmConfiguration) {
// it.where<GossipingEventEntity>() // it.where<GossipingEventEntity>()
@ -1417,6 +1505,27 @@ internal class RealmCryptoStore @Inject constructor(
.filterNotNull() .filterNotNull()
} }
override fun getOutgoingRoomKeyRequestsPaged(): LiveData<PagedList<OutgoingRoomKeyRequest>> {
val realmDataSourceFactory = monarchy.createDataSourceFactory { realm ->
realm
.where(OutgoingGossipingRequestEntity::class.java)
.equalTo(OutgoingGossipingRequestEntityFields.TYPE_STR, GossipRequestType.KEY.name)
}
val dataSourceFactory = realmDataSourceFactory.map {
it.toOutgoingGossipingRequest() as? OutgoingRoomKeyRequest
?: OutgoingRoomKeyRequest(requestBody = null, requestId = "?", recipients = emptyMap(), state = OutgoingGossipingRequestState.CANCELLED)
}
val trail = monarchy.findAllPagedWithChanges(realmDataSourceFactory,
LivePagedListBuilder(dataSourceFactory,
PagedList.Config.Builder()
.setPageSize(20)
.setEnablePlaceholders(false)
.setPrefetchDistance(1)
.build())
)
return trail
}
override fun getCrossSigningInfo(userId: String): MXCrossSigningInfo? { override fun getCrossSigningInfo(userId: String): MXCrossSigningInfo? {
return doWithRealm(realmConfiguration) { realm -> return doWithRealm(realmConfiguration) { realm ->
val crossSigningInfo = realm.where(CrossSigningInfoEntity::class.java) val crossSigningInfo = realm.where(CrossSigningInfoEntity::class.java)
@ -1558,4 +1667,48 @@ internal class RealmCryptoStore @Inject constructor(
result result
} }
} }
/**
* Some entries in the DB can get a bit out of control with time
* So we need to tidy up a bit
*/
override fun tidyUpDataBase() {
val prevWeekTs = System.currentTimeMillis() - 7 * 24 * 60 * 60 * 1_000
doRealmTransaction(realmConfiguration) { realm ->
// Only keep one week history
realm.where<IncomingGossipingRequestEntity>()
.lessThan(IncomingGossipingRequestEntityFields.LOCAL_CREATION_TIMESTAMP, prevWeekTs)
.findAll().let {
Timber.i("## Crypto Clean up ${it.size} IncomingGossipingRequestEntity")
it.deleteAllFromRealm()
}
// Clean the cancelled ones?
realm.where<OutgoingGossipingRequestEntity>()
.equalTo(OutgoingGossipingRequestEntityFields.REQUEST_STATE_STR, OutgoingGossipingRequestState.CANCELLED.name)
.equalTo(OutgoingGossipingRequestEntityFields.TYPE_STR, GossipRequestType.KEY.name)
.findAll().let {
Timber.i("## Crypto Clean up ${it.size} OutgoingGossipingRequestEntity")
it.deleteAllFromRealm()
}
// Only keep one week history
realm.where<GossipingEventEntity>()
.lessThan(GossipingEventEntityFields.AGE_LOCAL_TS, prevWeekTs)
.findAll().let {
Timber.i("## Crypto Clean up ${it.size} GossipingEventEntityFields")
it.deleteAllFromRealm()
}
// Can we do something for WithHeldSessionEntity?
}
}
/**
* Prints out database info
*/
override fun logDbUsageInfo() {
RealmDebugTools(realmConfiguration).logInfo("Crypto")
}
} }

View File

@ -43,6 +43,7 @@ import org.matrix.android.sdk.internal.crypto.store.db.model.WithHeldSessionEnti
import org.matrix.android.sdk.internal.di.SerializeNulls import org.matrix.android.sdk.internal.di.SerializeNulls
import io.realm.DynamicRealm import io.realm.DynamicRealm
import io.realm.RealmMigration import io.realm.RealmMigration
import io.realm.RealmObjectSchema
import org.matrix.androidsdk.crypto.data.MXOlmInboundGroupSession2 import org.matrix.androidsdk.crypto.data.MXOlmInboundGroupSession2
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
@ -57,6 +58,27 @@ internal class RealmCryptoStoreMigration @Inject constructor(private val crossSi
const val CRYPTO_STORE_SCHEMA_VERSION = 11L const val CRYPTO_STORE_SCHEMA_VERSION = 11L
} }
private fun RealmObjectSchema.addFieldIfNotExists(fieldName: String, fieldType: Class<*>): RealmObjectSchema {
if (!hasField(fieldName)) {
addField(fieldName, fieldType)
}
return this
}
private fun RealmObjectSchema.removeFieldIfExists(fieldName: String): RealmObjectSchema {
if (hasField(fieldName)) {
removeField(fieldName)
}
return this
}
private fun RealmObjectSchema.setRequiredIfNotAlready(fieldName: String, isRequired: Boolean): RealmObjectSchema {
if (isRequired != isRequired(fieldName)) {
setRequired(fieldName, isRequired)
}
return this
}
override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) { override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) {
Timber.v("Migrating Realm Crypto from $oldVersion to $newVersion") Timber.v("Migrating Realm Crypto from $oldVersion to $newVersion")
@ -89,13 +111,13 @@ internal class RealmCryptoStoreMigration @Inject constructor(private val crossSi
Timber.d("Update IncomingRoomKeyRequestEntity format: requestBodyString field is exploded into several fields") Timber.d("Update IncomingRoomKeyRequestEntity format: requestBodyString field is exploded into several fields")
realm.schema.get("IncomingRoomKeyRequestEntity") realm.schema.get("IncomingRoomKeyRequestEntity")
?.addField("requestBodyAlgorithm", String::class.java) ?.addFieldIfNotExists("requestBodyAlgorithm", String::class.java)
?.addField("requestBodyRoomId", String::class.java) ?.addFieldIfNotExists("requestBodyRoomId", String::class.java)
?.addField("requestBodySenderKey", String::class.java) ?.addFieldIfNotExists("requestBodySenderKey", String::class.java)
?.addField("requestBodySessionId", String::class.java) ?.addFieldIfNotExists("requestBodySessionId", String::class.java)
?.transform { dynamicObject -> ?.transform { dynamicObject ->
val requestBodyString = dynamicObject.getString("requestBodyString")
try { try {
val requestBodyString = dynamicObject.getString("requestBodyString")
// It was a map before // It was a map before
val map: Map<String, String>? = deserializeFromRealm(requestBodyString) val map: Map<String, String>? = deserializeFromRealm(requestBodyString)
@ -109,18 +131,18 @@ internal class RealmCryptoStoreMigration @Inject constructor(private val crossSi
Timber.e(e, "Error") Timber.e(e, "Error")
} }
} }
?.removeField("requestBodyString") ?.removeFieldIfExists("requestBodyString")
Timber.d("Update IncomingRoomKeyRequestEntity format: requestBodyString field is exploded into several fields") Timber.d("Update IncomingRoomKeyRequestEntity format: requestBodyString field is exploded into several fields")
realm.schema.get("OutgoingRoomKeyRequestEntity") realm.schema.get("OutgoingRoomKeyRequestEntity")
?.addField("requestBodyAlgorithm", String::class.java) ?.addFieldIfNotExists("requestBodyAlgorithm", String::class.java)
?.addField("requestBodyRoomId", String::class.java) ?.addFieldIfNotExists("requestBodyRoomId", String::class.java)
?.addField("requestBodySenderKey", String::class.java) ?.addFieldIfNotExists("requestBodySenderKey", String::class.java)
?.addField("requestBodySessionId", String::class.java) ?.addFieldIfNotExists("requestBodySessionId", String::class.java)
?.transform { dynamicObject -> ?.transform { dynamicObject ->
val requestBodyString = dynamicObject.getString("requestBodyString")
try { try {
val requestBodyString = dynamicObject.getString("requestBodyString")
// It was a map before // It was a map before
val map: Map<String, String>? = deserializeFromRealm(requestBodyString) val map: Map<String, String>? = deserializeFromRealm(requestBodyString)
@ -134,10 +156,11 @@ internal class RealmCryptoStoreMigration @Inject constructor(private val crossSi
Timber.e(e, "Error") Timber.e(e, "Error")
} }
} }
?.removeField("requestBodyString") ?.removeFieldIfExists("requestBodyString")
Timber.d("Create KeysBackupDataEntity") Timber.d("Create KeysBackupDataEntity")
if (!realm.schema.contains("KeysBackupDataEntity")) {
realm.schema.create("KeysBackupDataEntity") realm.schema.create("KeysBackupDataEntity")
.addField(KeysBackupDataEntityFields.PRIMARY_KEY, Integer::class.java) .addField(KeysBackupDataEntityFields.PRIMARY_KEY, Integer::class.java)
.addPrimaryKey(KeysBackupDataEntityFields.PRIMARY_KEY) .addPrimaryKey(KeysBackupDataEntityFields.PRIMARY_KEY)
@ -145,14 +168,15 @@ internal class RealmCryptoStoreMigration @Inject constructor(private val crossSi
.addField(KeysBackupDataEntityFields.BACKUP_LAST_SERVER_HASH, String::class.java) .addField(KeysBackupDataEntityFields.BACKUP_LAST_SERVER_HASH, String::class.java)
.addField(KeysBackupDataEntityFields.BACKUP_LAST_SERVER_NUMBER_OF_KEYS, Integer::class.java) .addField(KeysBackupDataEntityFields.BACKUP_LAST_SERVER_NUMBER_OF_KEYS, Integer::class.java)
} }
}
private fun migrateTo3RiotX(realm: DynamicRealm) { private fun migrateTo3RiotX(realm: DynamicRealm) {
Timber.d("Step 2 -> 3") Timber.d("Step 2 -> 3")
Timber.d("Migrate to RiotX model") Timber.d("Migrate to RiotX model")
realm.schema.get("CryptoRoomEntity") realm.schema.get("CryptoRoomEntity")
?.addField(CryptoRoomEntityFields.SHOULD_ENCRYPT_FOR_INVITED_MEMBERS, Boolean::class.java) ?.addFieldIfNotExists(CryptoRoomEntityFields.SHOULD_ENCRYPT_FOR_INVITED_MEMBERS, Boolean::class.java)
?.setRequired(CryptoRoomEntityFields.SHOULD_ENCRYPT_FOR_INVITED_MEMBERS, false) ?.setRequiredIfNotAlready(CryptoRoomEntityFields.SHOULD_ENCRYPT_FOR_INVITED_MEMBERS, false)
// Convert format of MXDeviceInfo, package has to be the same. // Convert format of MXDeviceInfo, package has to be the same.
realm.schema.get("DeviceInfoEntity") realm.schema.get("DeviceInfoEntity")
@ -204,8 +228,13 @@ internal class RealmCryptoStoreMigration @Inject constructor(private val crossSi
// Version 4L added Cross Signing info persistence // Version 4L added Cross Signing info persistence
private fun migrateTo4(realm: DynamicRealm) { private fun migrateTo4(realm: DynamicRealm) {
Timber.d("Step 3 -> 4") Timber.d("Step 3 -> 4")
Timber.d("Create KeyInfoEntity")
if (realm.schema.contains("TrustLevelEntity")) {
Timber.d("Skipping Step 3 -> 4 because entities already exist")
return
}
Timber.d("Create KeyInfoEntity")
val trustLevelEntityEntitySchema = realm.schema.create("TrustLevelEntity") val trustLevelEntityEntitySchema = realm.schema.create("TrustLevelEntity")
.addField(TrustLevelEntityFields.CROSS_SIGNED_VERIFIED, Boolean::class.java) .addField(TrustLevelEntityFields.CROSS_SIGNED_VERIFIED, Boolean::class.java)
.setNullable(TrustLevelEntityFields.CROSS_SIGNED_VERIFIED, true) .setNullable(TrustLevelEntityFields.CROSS_SIGNED_VERIFIED, true)

View File

@ -17,8 +17,13 @@ package org.matrix.android.sdk.internal.crypto.tasks
import org.matrix.android.sdk.api.session.crypto.CryptoService import org.matrix.android.sdk.api.session.crypto.CryptoService
import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.toContent
import org.matrix.android.sdk.api.session.room.send.SendState import org.matrix.android.sdk.api.session.room.send.SendState
import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
import org.matrix.android.sdk.internal.crypto.MXEventDecryptionResult
import org.matrix.android.sdk.internal.crypto.model.MXEncryptEventContentResult import org.matrix.android.sdk.internal.crypto.model.MXEncryptEventContentResult
import org.matrix.android.sdk.internal.database.mapper.ContentMapper
import org.matrix.android.sdk.internal.session.room.send.LocalEchoRepository import org.matrix.android.sdk.internal.session.room.send.LocalEchoRepository
import org.matrix.android.sdk.internal.task.Task import org.matrix.android.sdk.internal.task.Task
import org.matrix.android.sdk.internal.util.awaitCallback import org.matrix.android.sdk.internal.util.awaitCallback
@ -28,23 +33,23 @@ internal interface EncryptEventTask : Task<EncryptEventTask.Params, Event> {
data class Params(val roomId: String, data class Params(val roomId: String,
val event: Event, val event: Event,
/**Do not encrypt these keys, keep them as is in encrypted content (e.g. m.relates_to)*/ /**Do not encrypt these keys, keep them as is in encrypted content (e.g. m.relates_to)*/
val keepKeys: List<String>? = null, val keepKeys: List<String>? = null
val crypto: CryptoService
) )
} }
internal class DefaultEncryptEventTask @Inject constructor( internal class DefaultEncryptEventTask @Inject constructor(
// private val crypto: CryptoService private val localEchoRepository: LocalEchoRepository,
private val localEchoRepository: LocalEchoRepository private val cryptoService: CryptoService
) : EncryptEventTask { ) : EncryptEventTask {
override suspend fun execute(params: EncryptEventTask.Params): Event { override suspend fun execute(params: EncryptEventTask.Params): Event {
if (!params.crypto.isRoomEncrypted(params.roomId)) return params.event // don't want to wait for any query
// if (!params.crypto.isRoomEncrypted(params.roomId)) return params.event
val localEvent = params.event val localEvent = params.event
if (localEvent.eventId == null) { if (localEvent.eventId == null) {
throw IllegalArgumentException() throw IllegalArgumentException()
} }
localEchoRepository.updateSendState(localEvent.eventId, SendState.ENCRYPTING) localEchoRepository.updateSendState(localEvent.eventId, localEvent.roomId, SendState.ENCRYPTING)
val localMutableContent = localEvent.content?.toMutableMap() ?: mutableMapOf() val localMutableContent = localEvent.content?.toMutableMap() ?: mutableMapOf()
params.keepKeys?.forEach { params.keepKeys?.forEach {
@ -52,8 +57,9 @@ internal class DefaultEncryptEventTask @Inject constructor(
} }
// try { // try {
// let it throws
awaitCallback<MXEncryptEventContentResult> { awaitCallback<MXEncryptEventContentResult> {
params.crypto.encryptEventContent(localMutableContent, localEvent.type, params.roomId, it) cryptoService.encryptEventContent(localMutableContent, localEvent.type, params.roomId, it)
}.let { result -> }.let { result ->
val modifiedContent = HashMap(result.eventContent) val modifiedContent = HashMap(result.eventContent)
params.keepKeys?.forEach { toKeep -> params.keepKeys?.forEach { toKeep ->
@ -63,18 +69,34 @@ internal class DefaultEncryptEventTask @Inject constructor(
} }
} }
val safeResult = result.copy(eventContent = modifiedContent) val safeResult = result.copy(eventContent = modifiedContent)
// Better handling of local echo, to avoid decrypting transition on remote echo
// Should I only do it for text messages?
val decryptionLocalEcho = if (result.eventContent["algorithm"] == MXCRYPTO_ALGORITHM_MEGOLM) {
MXEventDecryptionResult(
clearEvent = Event(
type = localEvent.type,
content = localEvent.content,
roomId = localEvent.roomId
).toContent(),
forwardingCurve25519KeyChain = emptyList(),
senderCurve25519Key = result.eventContent["sender_key"] as? String,
claimedEd25519Key = cryptoService.getMyDevice().fingerprint()
)
} else {
null
}
localEchoRepository.updateEcho(localEvent.eventId) { _, localEcho ->
localEcho.type = EventType.ENCRYPTED
localEcho.content = ContentMapper.map(modifiedContent)
decryptionLocalEcho?.also {
localEcho.setDecryptionResult(it)
}
}
return localEvent.copy( return localEvent.copy(
type = safeResult.eventType, type = safeResult.eventType,
content = safeResult.eventContent content = safeResult.eventContent
) )
} }
// } catch (throwable: Throwable) {
// val sendState = when (throwable) {
// is Failure.CryptoError -> SendState.FAILED_UNKNOWN_DEVICES
// else -> SendState.UNDELIVERED
// }
// localEchoUpdater.updateSendState(localEvent.eventId, sendState)
// throw throwable
// }
} }
} }

View File

@ -0,0 +1,49 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.crypto.tasks
import org.greenrobot.eventbus.EventBus
import org.matrix.android.sdk.internal.network.executeRequest
import org.matrix.android.sdk.internal.session.room.RoomAPI
import org.matrix.android.sdk.internal.session.room.send.SendResponse
import org.matrix.android.sdk.internal.task.Task
import javax.inject.Inject
internal interface RedactEventTask : Task<RedactEventTask.Params, String> {
data class Params(
val txID: String,
val roomId: String,
val eventId: String,
val reason: String?
)
}
internal class DefaultRedactEventTask @Inject constructor(
private val roomAPI: RoomAPI,
private val eventBus: EventBus) : RedactEventTask {
override suspend fun execute(params: RedactEventTask.Params): String {
val executeRequest = executeRequest<SendResponse>(eventBus) {
apiCall = roomAPI.redactEvent(
txId = params.txID,
roomId = params.roomId,
eventId = params.eventId,
reason = if (params.reason == null) emptyMap() else mapOf("reason" to params.reason)
)
}
return executeRequest.eventId
}
}

View File

@ -15,7 +15,7 @@
*/ */
package org.matrix.android.sdk.internal.crypto.tasks package org.matrix.android.sdk.internal.crypto.tasks
import org.matrix.android.sdk.api.session.crypto.CryptoService import org.greenrobot.eventbus.EventBus
import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.room.send.SendState import org.matrix.android.sdk.api.session.room.send.SendState
import org.matrix.android.sdk.internal.network.executeRequest import org.matrix.android.sdk.internal.network.executeRequest
@ -23,13 +23,12 @@ import org.matrix.android.sdk.internal.session.room.RoomAPI
import org.matrix.android.sdk.internal.session.room.send.LocalEchoRepository import org.matrix.android.sdk.internal.session.room.send.LocalEchoRepository
import org.matrix.android.sdk.internal.session.room.send.SendResponse import org.matrix.android.sdk.internal.session.room.send.SendResponse
import org.matrix.android.sdk.internal.task.Task import org.matrix.android.sdk.internal.task.Task
import org.greenrobot.eventbus.EventBus
import javax.inject.Inject import javax.inject.Inject
internal interface SendEventTask : Task<SendEventTask.Params, String> { internal interface SendEventTask : Task<SendEventTask.Params, String> {
data class Params( data class Params(
val event: Event, val event: Event,
val cryptoService: CryptoService? val encrypt: Boolean
) )
} }
@ -40,11 +39,11 @@ internal class DefaultSendEventTask @Inject constructor(
private val eventBus: EventBus) : SendEventTask { private val eventBus: EventBus) : SendEventTask {
override suspend fun execute(params: SendEventTask.Params): String { override suspend fun execute(params: SendEventTask.Params): String {
try {
val event = handleEncryption(params) val event = handleEncryption(params)
val localId = event.eventId!! val localId = event.eventId!!
try { localEchoRepository.updateSendState(localId, params.event.roomId, SendState.SENDING)
localEchoRepository.updateSendState(localId, SendState.SENDING)
val executeRequest = executeRequest<SendResponse>(eventBus) { val executeRequest = executeRequest<SendResponse>(eventBus) {
apiCall = roomAPI.send( apiCall = roomAPI.send(
localId, localId,
@ -53,26 +52,22 @@ internal class DefaultSendEventTask @Inject constructor(
eventType = event.type eventType = event.type
) )
} }
localEchoRepository.updateSendState(localId, SendState.SENT) localEchoRepository.updateSendState(localId, params.event.roomId, SendState.SENT)
return executeRequest.eventId return executeRequest.eventId
} catch (e: Throwable) { } catch (e: Throwable) {
localEchoRepository.updateSendState(localId, SendState.UNDELIVERED) // localEchoRepository.updateSendState(params.event.eventId!!, SendState.UNDELIVERED)
throw e throw e
} }
} }
@Throws
private suspend fun handleEncryption(params: SendEventTask.Params): Event { private suspend fun handleEncryption(params: SendEventTask.Params): Event {
if (params.cryptoService?.isRoomEncrypted(params.event.roomId ?: "") == true) { if (params.encrypt && !params.event.isEncrypted()) {
try {
return encryptEventTask.execute(EncryptEventTask.Params( return encryptEventTask.execute(EncryptEventTask.Params(
params.event.roomId ?: "", params.event.roomId ?: "",
params.event, params.event,
listOf("m.relates_to"), listOf("m.relates_to")
params.cryptoService
)) ))
} catch (throwable: Throwable) {
// We said it's ok to send verification request in clear
}
} }
return params.event return params.event
} }

View File

@ -15,21 +15,20 @@
*/ */
package org.matrix.android.sdk.internal.crypto.tasks package org.matrix.android.sdk.internal.crypto.tasks
import org.matrix.android.sdk.api.session.crypto.CryptoService import org.greenrobot.eventbus.EventBus
import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.room.send.SendState import org.matrix.android.sdk.api.session.room.send.SendState
import org.matrix.android.sdk.internal.crypto.CryptoSessionInfoProvider
import org.matrix.android.sdk.internal.network.executeRequest import org.matrix.android.sdk.internal.network.executeRequest
import org.matrix.android.sdk.internal.session.room.RoomAPI import org.matrix.android.sdk.internal.session.room.RoomAPI
import org.matrix.android.sdk.internal.session.room.send.LocalEchoRepository import org.matrix.android.sdk.internal.session.room.send.LocalEchoRepository
import org.matrix.android.sdk.internal.session.room.send.SendResponse import org.matrix.android.sdk.internal.session.room.send.SendResponse
import org.matrix.android.sdk.internal.task.Task import org.matrix.android.sdk.internal.task.Task
import org.greenrobot.eventbus.EventBus
import javax.inject.Inject import javax.inject.Inject
internal interface SendVerificationMessageTask : Task<SendVerificationMessageTask.Params, String> { internal interface SendVerificationMessageTask : Task<SendVerificationMessageTask.Params, String> {
data class Params( data class Params(
val event: Event, val event: Event
val cryptoService: CryptoService?
) )
} }
@ -37,6 +36,7 @@ internal class DefaultSendVerificationMessageTask @Inject constructor(
private val localEchoRepository: LocalEchoRepository, private val localEchoRepository: LocalEchoRepository,
private val encryptEventTask: DefaultEncryptEventTask, private val encryptEventTask: DefaultEncryptEventTask,
private val roomAPI: RoomAPI, private val roomAPI: RoomAPI,
private val cryptoSessionInfoProvider: CryptoSessionInfoProvider,
private val eventBus: EventBus) : SendVerificationMessageTask { private val eventBus: EventBus) : SendVerificationMessageTask {
override suspend fun execute(params: SendVerificationMessageTask.Params): String { override suspend fun execute(params: SendVerificationMessageTask.Params): String {
@ -44,7 +44,7 @@ internal class DefaultSendVerificationMessageTask @Inject constructor(
val localId = event.eventId!! val localId = event.eventId!!
try { try {
localEchoRepository.updateSendState(localId, SendState.SENDING) localEchoRepository.updateSendState(localId, event.roomId, SendState.SENDING)
val executeRequest = executeRequest<SendResponse>(eventBus) { val executeRequest = executeRequest<SendResponse>(eventBus) {
apiCall = roomAPI.send( apiCall = roomAPI.send(
localId, localId,
@ -53,22 +53,21 @@ internal class DefaultSendVerificationMessageTask @Inject constructor(
eventType = event.type eventType = event.type
) )
} }
localEchoRepository.updateSendState(localId, SendState.SENT) localEchoRepository.updateSendState(localId, event.roomId, SendState.SENT)
return executeRequest.eventId return executeRequest.eventId
} catch (e: Throwable) { } catch (e: Throwable) {
localEchoRepository.updateSendState(localId, SendState.UNDELIVERED) localEchoRepository.updateSendState(localId, event.roomId, SendState.UNDELIVERED)
throw e throw e
} }
} }
private suspend fun handleEncryption(params: SendVerificationMessageTask.Params): Event { private suspend fun handleEncryption(params: SendVerificationMessageTask.Params): Event {
if (params.cryptoService?.isRoomEncrypted(params.event.roomId ?: "") == true) { if (cryptoSessionInfoProvider.isRoomEncrypted(params.event.roomId ?: "")) {
try { try {
return encryptEventTask.execute(EncryptEventTask.Params( return encryptEventTask.execute(EncryptEventTask.Params(
params.event.roomId ?: "", params.event.roomId ?: "",
params.event, params.event,
listOf("m.relates_to"), listOf("m.relates_to")
params.cryptoService
)) ))
} catch (throwable: Throwable) { } catch (throwable: Throwable) {
// We said it's ok to send verification request in clear // We said it's ok to send verification request in clear

View File

@ -20,7 +20,6 @@ import android.os.Handler
import android.os.Looper import android.os.Looper
import dagger.Lazy import dagger.Lazy
import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.MatrixCallback
import org.matrix.android.sdk.api.session.crypto.CryptoService
import org.matrix.android.sdk.api.session.crypto.crosssigning.CrossSigningService import org.matrix.android.sdk.api.session.crypto.crosssigning.CrossSigningService
import org.matrix.android.sdk.api.session.crypto.crosssigning.KEYBACKUP_SECRET_SSSS_NAME import org.matrix.android.sdk.api.session.crypto.crosssigning.KEYBACKUP_SECRET_SSSS_NAME
import org.matrix.android.sdk.api.session.crypto.crosssigning.MASTER_KEY_SSSS_NAME import org.matrix.android.sdk.api.session.crypto.crosssigning.MASTER_KEY_SSSS_NAME
@ -111,9 +110,6 @@ internal class DefaultVerificationService @Inject constructor(
private val uiHandler = Handler(Looper.getMainLooper()) private val uiHandler = Handler(Looper.getMainLooper())
// Cannot be injected in constructor as it creates a dependency cycle
lateinit var cryptoService: CryptoService
// map [sender : [transaction]] // map [sender : [transaction]]
private val txMap = HashMap<String, HashMap<String, DefaultVerificationTransaction>>() private val txMap = HashMap<String, HashMap<String, DefaultVerificationTransaction>>()
@ -129,7 +125,8 @@ internal class DefaultVerificationService @Inject constructor(
// Event received from the sync // Event received from the sync
fun onToDeviceEvent(event: Event) { fun onToDeviceEvent(event: Event) {
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { Timber.d("## SAS onToDeviceEvent ${event.getClearType()}")
cryptoCoroutineScope.launch(coroutineDispatchers.dmVerif) {
when (event.getClearType()) { when (event.getClearType()) {
EventType.KEY_VERIFICATION_START -> { EventType.KEY_VERIFICATION_START -> {
onStartRequestReceived(event) onStartRequestReceived(event)
@ -163,7 +160,7 @@ internal class DefaultVerificationService @Inject constructor(
} }
fun onRoomEvent(event: Event) { fun onRoomEvent(event: Event) {
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { cryptoCoroutineScope.launch(coroutineDispatchers.dmVerif) {
when (event.getClearType()) { when (event.getClearType()) {
EventType.KEY_VERIFICATION_START -> { EventType.KEY_VERIFICATION_START -> {
onRoomStartRequestReceived(event) onRoomStartRequestReceived(event)
@ -240,6 +237,7 @@ internal class DefaultVerificationService @Inject constructor(
} }
private fun dispatchRequestAdded(tx: PendingVerificationRequest) { private fun dispatchRequestAdded(tx: PendingVerificationRequest) {
Timber.v("## SAS dispatchRequestAdded txId:${tx.transactionId}")
uiHandler.post { uiHandler.post {
listeners.forEach { listeners.forEach {
try { try {
@ -303,11 +301,14 @@ internal class DefaultVerificationService @Inject constructor(
// We don't want to block here // We don't want to block here
val otherDeviceId = validRequestInfo.fromDevice val otherDeviceId = validRequestInfo.fromDevice
Timber.v("## SAS onRequestReceived from $senderId and device $otherDeviceId, txId:${validRequestInfo.transactionId}")
cryptoCoroutineScope.launch { cryptoCoroutineScope.launch {
if (checkKeysAreDownloaded(senderId, otherDeviceId) == null) { if (checkKeysAreDownloaded(senderId, otherDeviceId) == null) {
Timber.e("## Verification device $otherDeviceId is not known") Timber.e("## Verification device $otherDeviceId is not known")
} }
} }
Timber.v("## SAS onRequestReceived .. checkKeysAreDownloaded launched")
// Remember this request // Remember this request
val requestsForUser = pendingRequests.getOrPut(senderId) { mutableListOf() } val requestsForUser = pendingRequests.getOrPut(senderId) { mutableListOf() }
@ -1203,7 +1204,9 @@ internal class DefaultVerificationService @Inject constructor(
// TODO refactor this with the DM one // TODO refactor this with the DM one
Timber.i("## Requesting verification to user: $otherUserId with device list $otherDevices") Timber.i("## Requesting verification to user: $otherUserId with device list $otherDevices")
val targetDevices = otherDevices ?: cryptoService.getUserDevices(otherUserId).map { it.deviceId } val targetDevices = otherDevices ?: cryptoStore.getUserDevices(otherUserId)
?.values?.map { it.deviceId } ?: emptyList()
val requestsForUser = pendingRequests.getOrPut(otherUserId) { mutableListOf() } val requestsForUser = pendingRequests.getOrPut(otherUserId) { mutableListOf() }
val transport = verificationTransportToDeviceFactory.createTransport(null) val transport = verificationTransportToDeviceFactory.createTransport(null)

View File

@ -20,7 +20,6 @@ import androidx.work.Data
import androidx.work.WorkerParameters import androidx.work.WorkerParameters
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
import org.matrix.android.sdk.api.failure.shouldBeRetried import org.matrix.android.sdk.api.failure.shouldBeRetried
import org.matrix.android.sdk.api.session.crypto.CryptoService
import org.matrix.android.sdk.internal.crypto.tasks.SendVerificationMessageTask import org.matrix.android.sdk.internal.crypto.tasks.SendVerificationMessageTask
import org.matrix.android.sdk.internal.session.SessionComponent import org.matrix.android.sdk.internal.session.SessionComponent
import org.matrix.android.sdk.internal.session.room.send.CancelSendTracker import org.matrix.android.sdk.internal.session.room.send.CancelSendTracker
@ -47,7 +46,6 @@ internal class SendVerificationMessageWorker(context: Context,
@Inject lateinit var sendVerificationMessageTask: SendVerificationMessageTask @Inject lateinit var sendVerificationMessageTask: SendVerificationMessageTask
@Inject lateinit var localEchoRepository: LocalEchoRepository @Inject lateinit var localEchoRepository: LocalEchoRepository
@Inject lateinit var cryptoService: CryptoService
@Inject lateinit var cancelSendTracker: CancelSendTracker @Inject lateinit var cancelSendTracker: CancelSendTracker
override fun injectWith(injector: SessionComponent) { override fun injectWith(injector: SessionComponent) {
@ -70,8 +68,7 @@ internal class SendVerificationMessageWorker(context: Context,
return try { return try {
val resultEventId = sendVerificationMessageTask.execute( val resultEventId = sendVerificationMessageTask.execute(
SendVerificationMessageTask.Params( SendVerificationMessageTask.Params(
event = localEvent, event = localEvent
cryptoService = cryptoService
) )
) )

View File

@ -15,7 +15,6 @@
*/ */
package org.matrix.android.sdk.internal.crypto.verification package org.matrix.android.sdk.internal.crypto.verification
import org.matrix.android.sdk.api.session.crypto.CryptoService
import org.matrix.android.sdk.api.session.crypto.MXCryptoError import org.matrix.android.sdk.api.session.crypto.MXCryptoError
import org.matrix.android.sdk.api.session.crypto.verification.VerificationService import org.matrix.android.sdk.api.session.crypto.verification.VerificationService
import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.Event
@ -34,12 +33,13 @@ import org.matrix.android.sdk.internal.di.DeviceId
import org.matrix.android.sdk.internal.di.UserId import org.matrix.android.sdk.internal.di.UserId
import org.matrix.android.sdk.internal.session.EventInsertLiveProcessor import org.matrix.android.sdk.internal.session.EventInsertLiveProcessor
import io.realm.Realm import io.realm.Realm
import org.matrix.android.sdk.internal.crypto.EventDecryptor
import timber.log.Timber import timber.log.Timber
import java.util.ArrayList import java.util.ArrayList
import javax.inject.Inject import javax.inject.Inject
internal class VerificationMessageProcessor @Inject constructor( internal class VerificationMessageProcessor @Inject constructor(
private val cryptoService: CryptoService, private val eventDecryptor: EventDecryptor,
private val verificationService: DefaultVerificationService, private val verificationService: DefaultVerificationService,
@UserId private val userId: String, @UserId private val userId: String,
@DeviceId private val deviceId: String? @DeviceId private val deviceId: String?
@ -82,7 +82,7 @@ internal class VerificationMessageProcessor @Inject constructor(
// TODO use a global event decryptor? attache to session and that listen to new sessionId? // TODO use a global event decryptor? attache to session and that listen to new sessionId?
// for now decrypt sync // for now decrypt sync
try { try {
val result = cryptoService.decryptEvent(event, "") val result = eventDecryptor.decryptEvent(event, "")
event.mxDecryptionResult = OlmDecryptionResult( event.mxDecryptionResult = OlmDecryptionResult(
payload = result.clearEvent, payload = result.clearEvent,
senderKey = result.senderCurve25519Key, senderKey = result.senderCurve25519Key,

View File

@ -17,10 +17,6 @@
package org.matrix.android.sdk.internal.database package org.matrix.android.sdk.internal.database
import com.zhuinden.monarchy.Monarchy import com.zhuinden.monarchy.Monarchy
import org.matrix.android.sdk.api.session.crypto.CryptoService
import org.matrix.android.sdk.api.session.crypto.MXCryptoError
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.internal.crypto.algorithms.olm.OlmDecryptionResult
import org.matrix.android.sdk.internal.database.mapper.asDomain import org.matrix.android.sdk.internal.database.mapper.asDomain
import org.matrix.android.sdk.internal.database.model.EventEntity import org.matrix.android.sdk.internal.database.model.EventEntity
import org.matrix.android.sdk.internal.database.model.EventInsertEntity import org.matrix.android.sdk.internal.database.model.EventInsertEntity
@ -31,12 +27,13 @@ import org.matrix.android.sdk.internal.session.EventInsertLiveProcessor
import io.realm.RealmConfiguration import io.realm.RealmConfiguration
import io.realm.RealmResults import io.realm.RealmResults
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.matrix.android.sdk.internal.crypto.EventDecryptor
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
internal class EventInsertLiveObserver @Inject constructor(@SessionDatabase realmConfiguration: RealmConfiguration, internal class EventInsertLiveObserver @Inject constructor(@SessionDatabase realmConfiguration: RealmConfiguration,
private val processors: Set<@JvmSuppressWildcards EventInsertLiveProcessor>, private val processors: Set<@JvmSuppressWildcards EventInsertLiveProcessor>,
private val cryptoService: CryptoService) private val eventDecryptor: EventDecryptor)
: RealmLiveEntityObserver<EventInsertEntity>(realmConfiguration) { : RealmLiveEntityObserver<EventInsertEntity>(realmConfiguration) {
override val query = Monarchy.Query<EventInsertEntity> { override val query = Monarchy.Query<EventInsertEntity> {
@ -74,7 +71,7 @@ internal class EventInsertLiveObserver @Inject constructor(@SessionDatabase real
return@forEach return@forEach
} }
val domainEvent = event.asDomain() val domainEvent = event.asDomain()
decryptIfNeeded(domainEvent) // decryptIfNeeded(domainEvent)
processors.filter { processors.filter {
it.shouldProcess(eventId, domainEvent.getClearType(), eventInsert.insertType) it.shouldProcess(eventId, domainEvent.getClearType(), eventInsert.insertType)
}.forEach { }.forEach {
@ -89,22 +86,22 @@ internal class EventInsertLiveObserver @Inject constructor(@SessionDatabase real
} }
} }
private fun decryptIfNeeded(event: Event) { // private fun decryptIfNeeded(event: Event) {
if (event.isEncrypted() && event.mxDecryptionResult == null) { // if (event.isEncrypted() && event.mxDecryptionResult == null) {
try { // try {
val result = cryptoService.decryptEvent(event, event.roomId ?: "") // val result = eventDecryptor.decryptEvent(event, event.roomId ?: "")
event.mxDecryptionResult = OlmDecryptionResult( // event.mxDecryptionResult = OlmDecryptionResult(
payload = result.clearEvent, // payload = result.clearEvent,
senderKey = result.senderCurve25519Key, // senderKey = result.senderCurve25519Key,
keysClaimed = result.claimedEd25519Key?.let { k -> mapOf("ed25519" to k) }, // keysClaimed = result.claimedEd25519Key?.let { k -> mapOf("ed25519" to k) },
forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain // forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain
) // )
} catch (e: MXCryptoError) { // } catch (e: MXCryptoError) {
Timber.v("Failed to decrypt event") // Timber.v("Failed to decrypt event")
// TODO -> we should keep track of this and retry, or some processing will never be handled // // TODO -> we should keep track of this and retry, or some processing will never be handled
} // }
} // }
} // }
private fun shouldProcess(eventInsertEntity: EventInsertEntity): Boolean { private fun shouldProcess(eventInsertEntity: EventInsertEntity): Boolean {
return processors.any { return processors.any {

View File

@ -0,0 +1,61 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.database.tools
import io.realm.Realm
import io.realm.RealmConfiguration
import org.matrix.android.sdk.BuildConfig
import timber.log.Timber
internal class RealmDebugTools(
private val realmConfiguration: RealmConfiguration
) {
/**
* Log info about the DB
*/
fun logInfo(baseName: String) {
buildString {
append("\n$baseName Realm located at : ${realmConfiguration.realmDirectory}/${realmConfiguration.realmFileName}")
if (BuildConfig.LOG_PRIVATE_DATA) {
val key = realmConfiguration.encryptionKey.joinToString("") { byte -> "%02x".format(byte) }
append("\n$baseName Realm encryption key : $key")
}
Realm.getInstance(realmConfiguration).use { realm ->
// Check if we have data
separator()
separator()
append("\n$baseName Realm is empty: ${realm.isEmpty}")
var total = 0L
val maxNameLength = realmConfiguration.realmObjectClasses.maxOf { it.simpleName.length }
realmConfiguration.realmObjectClasses.forEach { modelClazz ->
val count = realm.where(modelClazz).count()
total += count
append("\n$baseName Realm - count ${modelClazz.simpleName.padEnd(maxNameLength)} : $count")
}
separator()
append("\n$baseName Realm - total count: $total")
separator()
separator()
}
}
.let { Timber.i(it) }
}
private fun StringBuilder.separator() = append("\n==============================================")
}

View File

@ -59,12 +59,13 @@ import org.matrix.android.sdk.api.session.user.UserService
import org.matrix.android.sdk.api.session.widgets.WidgetService import org.matrix.android.sdk.api.session.widgets.WidgetService
import org.matrix.android.sdk.internal.auth.SessionParamsStore import org.matrix.android.sdk.internal.auth.SessionParamsStore
import org.matrix.android.sdk.internal.crypto.DefaultCryptoService import org.matrix.android.sdk.internal.crypto.DefaultCryptoService
import org.matrix.android.sdk.internal.database.tools.RealmDebugTools
import org.matrix.android.sdk.internal.di.SessionDatabase import org.matrix.android.sdk.internal.di.SessionDatabase
import org.matrix.android.sdk.internal.di.SessionId import org.matrix.android.sdk.internal.di.SessionId
import org.matrix.android.sdk.internal.di.UnauthenticatedWithCertificate import org.matrix.android.sdk.internal.di.UnauthenticatedWithCertificate
import org.matrix.android.sdk.internal.di.WorkManagerProvider import org.matrix.android.sdk.internal.di.WorkManagerProvider
import org.matrix.android.sdk.internal.session.identity.DefaultIdentityService import org.matrix.android.sdk.internal.session.identity.DefaultIdentityService
import org.matrix.android.sdk.internal.session.room.timeline.TimelineEventDecryptor import org.matrix.android.sdk.internal.session.room.send.queue.EventSenderProcessor
import org.matrix.android.sdk.internal.session.sync.SyncTokenStore import org.matrix.android.sdk.internal.session.sync.SyncTokenStore
import org.matrix.android.sdk.internal.session.sync.job.SyncThread import org.matrix.android.sdk.internal.session.sync.job.SyncThread
import org.matrix.android.sdk.internal.session.sync.job.SyncWorker import org.matrix.android.sdk.internal.session.sync.job.SyncWorker
@ -114,14 +115,14 @@ internal class DefaultSession @Inject constructor(
private val accountDataService: Lazy<AccountDataService>, private val accountDataService: Lazy<AccountDataService>,
private val _sharedSecretStorageService: Lazy<SharedSecretStorageService>, private val _sharedSecretStorageService: Lazy<SharedSecretStorageService>,
private val accountService: Lazy<AccountService>, private val accountService: Lazy<AccountService>,
private val timelineEventDecryptor: TimelineEventDecryptor,
private val coroutineDispatchers: MatrixCoroutineDispatchers, private val coroutineDispatchers: MatrixCoroutineDispatchers,
private val defaultIdentityService: DefaultIdentityService, private val defaultIdentityService: DefaultIdentityService,
private val integrationManagerService: IntegrationManagerService, private val integrationManagerService: IntegrationManagerService,
private val taskExecutor: TaskExecutor, private val taskExecutor: TaskExecutor,
private val callSignalingService: Lazy<CallSignalingService>, private val callSignalingService: Lazy<CallSignalingService>,
@UnauthenticatedWithCertificate @UnauthenticatedWithCertificate
private val unauthenticatedWithCertificateOkHttpClient: Lazy<OkHttpClient> private val unauthenticatedWithCertificateOkHttpClient: Lazy<OkHttpClient>,
private val eventSenderProcessor: EventSenderProcessor
) : Session, ) : Session,
RoomService by roomService.get(), RoomService by roomService.get(),
RoomDirectoryService by roomDirectoryService.get(), RoomDirectoryService by roomDirectoryService.get(),
@ -160,7 +161,7 @@ internal class DefaultSession @Inject constructor(
lifecycleObservers.forEach { it.onStart() } lifecycleObservers.forEach { it.onStart() }
} }
eventBus.register(this) eventBus.register(this)
timelineEventDecryptor.start() eventSenderProcessor.start()
} }
override fun requireBackgroundSync() { override fun requireBackgroundSync() {
@ -197,13 +198,14 @@ internal class DefaultSession @Inject constructor(
override fun close() { override fun close() {
assert(isOpen) assert(isOpen)
stopSync() stopSync()
timelineEventDecryptor.destroy() // timelineEventDecryptor.destroy()
uiHandler.post { uiHandler.post {
lifecycleObservers.forEach { it.onStop() } lifecycleObservers.forEach { it.onStop() }
} }
cryptoService.get().close() cryptoService.get().close()
isOpen = false isOpen = false
eventBus.unregister(this) eventBus.unregister(this)
eventSenderProcessor.interrupt()
} }
override fun getSyncStateLive() = getSyncThread().liveState() override fun getSyncStateLive() = getSyncThread().liveState()
@ -283,4 +285,8 @@ internal class DefaultSession @Inject constructor(
override fun toString(): String { override fun toString(): String {
return "$myUserId - ${sessionParams.deviceId}" return "$myUserId - ${sessionParams.deviceId}"
} }
override fun logDbUsageInfo() {
RealmDebugTools(realmConfiguration).logInfo("Session")
}
} }

View File

@ -24,6 +24,7 @@ import org.matrix.android.sdk.internal.crypto.CancelGossipRequestWorker
import org.matrix.android.sdk.internal.crypto.CryptoModule import org.matrix.android.sdk.internal.crypto.CryptoModule
import org.matrix.android.sdk.internal.crypto.SendGossipRequestWorker import org.matrix.android.sdk.internal.crypto.SendGossipRequestWorker
import org.matrix.android.sdk.internal.crypto.SendGossipWorker import org.matrix.android.sdk.internal.crypto.SendGossipWorker
import org.matrix.android.sdk.internal.crypto.crosssigning.UpdateTrustWorker
import org.matrix.android.sdk.internal.crypto.verification.SendVerificationMessageWorker import org.matrix.android.sdk.internal.crypto.verification.SendVerificationMessageWorker
import org.matrix.android.sdk.internal.di.MatrixComponent import org.matrix.android.sdk.internal.di.MatrixComponent
import org.matrix.android.sdk.internal.di.SessionAssistedInjectModule import org.matrix.android.sdk.internal.di.SessionAssistedInjectModule
@ -45,7 +46,6 @@ import org.matrix.android.sdk.internal.session.pushers.AddHttpPusherWorker
import org.matrix.android.sdk.internal.session.pushers.PushersModule import org.matrix.android.sdk.internal.session.pushers.PushersModule
import org.matrix.android.sdk.internal.session.room.RoomModule import org.matrix.android.sdk.internal.session.room.RoomModule
import org.matrix.android.sdk.internal.session.room.relation.SendRelationWorker import org.matrix.android.sdk.internal.session.room.relation.SendRelationWorker
import org.matrix.android.sdk.internal.session.room.send.EncryptEventWorker
import org.matrix.android.sdk.internal.session.room.send.MultipleEventSendingDispatcherWorker import org.matrix.android.sdk.internal.session.room.send.MultipleEventSendingDispatcherWorker
import org.matrix.android.sdk.internal.session.room.send.RedactEventWorker import org.matrix.android.sdk.internal.session.room.send.RedactEventWorker
import org.matrix.android.sdk.internal.session.room.send.SendEventWorker import org.matrix.android.sdk.internal.session.room.send.SendEventWorker
@ -109,8 +109,6 @@ internal interface SessionComponent {
fun inject(worker: SendRelationWorker) fun inject(worker: SendRelationWorker)
fun inject(worker: EncryptEventWorker)
fun inject(worker: MultipleEventSendingDispatcherWorker) fun inject(worker: MultipleEventSendingDispatcherWorker)
fun inject(worker: RedactEventWorker) fun inject(worker: RedactEventWorker)
@ -131,6 +129,8 @@ internal interface SessionComponent {
fun inject(worker: SendGossipWorker) fun inject(worker: SendGossipWorker)
fun inject(worker: UpdateTrustWorker)
@Component.Factory @Component.Factory
interface Factory { interface Factory {
fun create( fun create(

View File

@ -41,8 +41,9 @@ import org.matrix.android.sdk.api.session.permalinks.PermalinkService
import org.matrix.android.sdk.api.session.securestorage.SecureStorageService import org.matrix.android.sdk.api.session.securestorage.SecureStorageService
import org.matrix.android.sdk.api.session.securestorage.SharedSecretStorageService import org.matrix.android.sdk.api.session.securestorage.SharedSecretStorageService
import org.matrix.android.sdk.api.session.typing.TypingUsersTracker import org.matrix.android.sdk.api.session.typing.TypingUsersTracker
import org.matrix.android.sdk.internal.crypto.crosssigning.ShieldTrustUpdater
import org.matrix.android.sdk.internal.crypto.secrets.DefaultSharedSecretStorageService import org.matrix.android.sdk.internal.crypto.secrets.DefaultSharedSecretStorageService
import org.matrix.android.sdk.internal.crypto.tasks.DefaultRedactEventTask
import org.matrix.android.sdk.internal.crypto.tasks.RedactEventTask
import org.matrix.android.sdk.internal.crypto.verification.VerificationMessageProcessor import org.matrix.android.sdk.internal.crypto.verification.VerificationMessageProcessor
import org.matrix.android.sdk.internal.database.DatabaseCleaner import org.matrix.android.sdk.internal.database.DatabaseCleaner
import org.matrix.android.sdk.internal.database.EventInsertLiveObserver import org.matrix.android.sdk.internal.database.EventInsertLiveObserver
@ -331,10 +332,6 @@ internal abstract class SessionModule {
@IntoSet @IntoSet
abstract fun bindWidgetUrlFormatter(formatter: DefaultWidgetURLFormatter): SessionLifecycleObserver abstract fun bindWidgetUrlFormatter(formatter: DefaultWidgetURLFormatter): SessionLifecycleObserver
@Binds
@IntoSet
abstract fun bindShieldTrustUpdated(updater: ShieldTrustUpdater): SessionLifecycleObserver
@Binds @Binds
@IntoSet @IntoSet
abstract fun bindIdentityService(service: DefaultIdentityService): SessionLifecycleObserver abstract fun bindIdentityService(service: DefaultIdentityService): SessionLifecycleObserver
@ -367,4 +364,7 @@ internal abstract class SessionModule {
@Binds @Binds
abstract fun bindTypingUsersTracker(tracker: DefaultTypingUsersTracker): TypingUsersTracker abstract fun bindTypingUsersTracker(tracker: DefaultTypingUsersTracker): TypingUsersTracker
@Binds
abstract fun bindRedactEventTask(task: DefaultRedactEventTask): RedactEventTask
} }

View File

@ -36,8 +36,8 @@ import org.matrix.android.sdk.api.util.NoOpCancellable
import org.matrix.android.sdk.internal.di.UserId import org.matrix.android.sdk.internal.di.UserId
import org.matrix.android.sdk.internal.session.SessionScope import org.matrix.android.sdk.internal.session.SessionScope
import org.matrix.android.sdk.internal.session.call.model.MxCallImpl import org.matrix.android.sdk.internal.session.call.model.MxCallImpl
import org.matrix.android.sdk.internal.session.room.send.queue.EventSenderProcessor
import org.matrix.android.sdk.internal.session.room.send.LocalEchoEventFactory import org.matrix.android.sdk.internal.session.room.send.LocalEchoEventFactory
import org.matrix.android.sdk.internal.session.room.send.RoomEventSender
import org.matrix.android.sdk.internal.task.TaskExecutor import org.matrix.android.sdk.internal.task.TaskExecutor
import org.matrix.android.sdk.internal.task.configureWith import org.matrix.android.sdk.internal.task.configureWith
import timber.log.Timber import timber.log.Timber
@ -50,7 +50,7 @@ internal class DefaultCallSignalingService @Inject constructor(
private val userId: String, private val userId: String,
private val activeCallHandler: ActiveCallHandler, private val activeCallHandler: ActiveCallHandler,
private val localEchoEventFactory: LocalEchoEventFactory, private val localEchoEventFactory: LocalEchoEventFactory,
private val roomEventSender: RoomEventSender, private val eventSenderProcessor: EventSenderProcessor,
private val taskExecutor: TaskExecutor, private val taskExecutor: TaskExecutor,
private val turnServerTask: GetTurnServerTask private val turnServerTask: GetTurnServerTask
) : CallSignalingService { ) : CallSignalingService {
@ -103,7 +103,7 @@ internal class DefaultCallSignalingService @Inject constructor(
otherUserId = otherUserId, otherUserId = otherUserId,
isVideoCall = isVideoCall, isVideoCall = isVideoCall,
localEchoEventFactory = localEchoEventFactory, localEchoEventFactory = localEchoEventFactory,
roomEventSender = roomEventSender eventSenderProcessor = eventSenderProcessor
) )
activeCallHandler.addCall(call).also { activeCallHandler.addCall(call).also {
return call return call
@ -165,7 +165,7 @@ internal class DefaultCallSignalingService @Inject constructor(
otherUserId = event.senderId ?: return@let, otherUserId = event.senderId ?: return@let,
isVideoCall = content.isVideo(), isVideoCall = content.isVideo(),
localEchoEventFactory = localEchoEventFactory, localEchoEventFactory = localEchoEventFactory,
roomEventSender = roomEventSender eventSenderProcessor = eventSenderProcessor
) )
activeCallHandler.addCall(incomingCall) activeCallHandler.addCall(incomingCall)
onCallInvite(incomingCall, content) onCallInvite(incomingCall, content)

View File

@ -29,8 +29,8 @@ import org.matrix.android.sdk.api.session.room.model.call.CallCandidatesContent
import org.matrix.android.sdk.api.session.room.model.call.CallHangupContent import org.matrix.android.sdk.api.session.room.model.call.CallHangupContent
import org.matrix.android.sdk.api.session.room.model.call.CallInviteContent import org.matrix.android.sdk.api.session.room.model.call.CallInviteContent
import org.matrix.android.sdk.internal.session.call.DefaultCallSignalingService import org.matrix.android.sdk.internal.session.call.DefaultCallSignalingService
import org.matrix.android.sdk.internal.session.room.send.queue.EventSenderProcessor
import org.matrix.android.sdk.internal.session.room.send.LocalEchoEventFactory import org.matrix.android.sdk.internal.session.room.send.LocalEchoEventFactory
import org.matrix.android.sdk.internal.session.room.send.RoomEventSender
import org.webrtc.IceCandidate import org.webrtc.IceCandidate
import org.webrtc.SessionDescription import org.webrtc.SessionDescription
import timber.log.Timber import timber.log.Timber
@ -43,7 +43,7 @@ internal class MxCallImpl(
override val otherUserId: String, override val otherUserId: String,
override val isVideoCall: Boolean, override val isVideoCall: Boolean,
private val localEchoEventFactory: LocalEchoEventFactory, private val localEchoEventFactory: LocalEchoEventFactory,
private val roomEventSender: RoomEventSender private val eventSenderProcessor: EventSenderProcessor
) : MxCall { ) : MxCall {
override var state: CallState = CallState.Idle override var state: CallState = CallState.Idle
@ -91,7 +91,7 @@ internal class MxCallImpl(
offer = CallInviteContent.Offer(sdp = sdp.description) offer = CallInviteContent.Offer(sdp = sdp.description)
) )
.let { createEventAndLocalEcho(type = EventType.CALL_INVITE, roomId = roomId, content = it.toContent()) } .let { createEventAndLocalEcho(type = EventType.CALL_INVITE, roomId = roomId, content = it.toContent()) }
.also { roomEventSender.sendEvent(it) } .also { eventSenderProcessor.postEvent(it) }
} }
override fun sendLocalIceCandidates(candidates: List<IceCandidate>) { override fun sendLocalIceCandidates(candidates: List<IceCandidate>) {
@ -106,7 +106,7 @@ internal class MxCallImpl(
} }
) )
.let { createEventAndLocalEcho(type = EventType.CALL_CANDIDATES, roomId = roomId, content = it.toContent()) } .let { createEventAndLocalEcho(type = EventType.CALL_CANDIDATES, roomId = roomId, content = it.toContent()) }
.also { roomEventSender.sendEvent(it) } .also { eventSenderProcessor.postEvent(it) }
} }
override fun sendLocalIceCandidateRemovals(candidates: List<IceCandidate>) { override fun sendLocalIceCandidateRemovals(candidates: List<IceCandidate>) {
@ -119,7 +119,7 @@ internal class MxCallImpl(
callId = callId callId = callId
) )
.let { createEventAndLocalEcho(type = EventType.CALL_HANGUP, roomId = roomId, content = it.toContent()) } .let { createEventAndLocalEcho(type = EventType.CALL_HANGUP, roomId = roomId, content = it.toContent()) }
.also { roomEventSender.sendEvent(it) } .also { eventSenderProcessor.postEvent(it) }
state = CallState.Terminated state = CallState.Terminated
} }
@ -132,7 +132,7 @@ internal class MxCallImpl(
answer = CallAnswerContent.Answer(sdp = sdp.description) answer = CallAnswerContent.Answer(sdp = sdp.description)
) )
.let { createEventAndLocalEcho(type = EventType.CALL_ANSWER, roomId = roomId, content = it.toContent()) } .let { createEventAndLocalEcho(type = EventType.CALL_ANSWER, roomId = roomId, content = it.toContent()) }
.also { roomEventSender.sendEvent(it) } .also { eventSenderProcessor.postEvent(it) }
} }
private fun createEventAndLocalEcho(localId: String = LocalEcho.createLocalEchoId(), type: String, roomId: String, content: Content): Event { private fun createEventAndLocalEcho(localId: String = LocalEcho.createLocalEchoId(), type: String, roomId: String, content: Content): Event {

View File

@ -61,7 +61,7 @@ internal class DefaultRoomService @Inject constructor(
return roomGetter.getRoom(roomId) return roomGetter.getRoom(roomId)
} }
override fun getExistingDirectRoomWithUser(otherUserId: String): Room? { override fun getExistingDirectRoomWithUser(otherUserId: String): String? {
return roomGetter.getDirectRoomWith(otherUserId) return roomGetter.getDirectRoomWith(otherUserId)
} }

View File

@ -15,7 +15,7 @@
*/ */
package org.matrix.android.sdk.internal.session.room package org.matrix.android.sdk.internal.session.room
import org.matrix.android.sdk.api.session.crypto.CryptoService import io.realm.Realm
import org.matrix.android.sdk.api.session.events.model.AggregatedAnnotation import org.matrix.android.sdk.api.session.events.model.AggregatedAnnotation
import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.EventType
@ -47,7 +47,6 @@ import org.matrix.android.sdk.internal.database.query.getOrCreate
import org.matrix.android.sdk.internal.database.query.where import org.matrix.android.sdk.internal.database.query.where
import org.matrix.android.sdk.internal.di.UserId import org.matrix.android.sdk.internal.di.UserId
import org.matrix.android.sdk.internal.session.EventInsertLiveProcessor import org.matrix.android.sdk.internal.session.EventInsertLiveProcessor
import io.realm.Realm
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
@ -78,9 +77,8 @@ private fun VerificationState?.toState(newState: VerificationState): Verificatio
return newState return newState
} }
internal class EventRelationsAggregationProcessor @Inject constructor(@UserId private val userId: String, internal class EventRelationsAggregationProcessor @Inject constructor(@UserId private val userId: String)
private val cryptoService: CryptoService : EventInsertLiveProcessor {
) : EventInsertLiveProcessor {
private val allowedTypes = listOf( private val allowedTypes = listOf(
EventType.MESSAGE, EventType.MESSAGE,

View File

@ -25,13 +25,12 @@ import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity
import org.matrix.android.sdk.internal.database.model.RoomSummaryEntityFields import org.matrix.android.sdk.internal.database.model.RoomSummaryEntityFields
import org.matrix.android.sdk.internal.database.query.where import org.matrix.android.sdk.internal.database.query.where
import org.matrix.android.sdk.internal.session.SessionScope import org.matrix.android.sdk.internal.session.SessionScope
import org.matrix.android.sdk.internal.session.room.membership.RoomMemberHelper
import javax.inject.Inject import javax.inject.Inject
internal interface RoomGetter { internal interface RoomGetter {
fun getRoom(roomId: String): Room? fun getRoom(roomId: String): Room?
fun getDirectRoomWith(otherUserId: String): Room? fun getDirectRoomWith(otherUserId: String): String?
} }
@SessionScope @SessionScope
@ -46,16 +45,14 @@ internal class DefaultRoomGetter @Inject constructor(
} }
} }
override fun getDirectRoomWith(otherUserId: String): Room? { override fun getDirectRoomWith(otherUserId: String): String? {
return realmSessionProvider.withRealm { realm -> return realmSessionProvider.withRealm { realm ->
RoomSummaryEntity.where(realm) RoomSummaryEntity.where(realm)
.equalTo(RoomSummaryEntityFields.IS_DIRECT, true) .equalTo(RoomSummaryEntityFields.IS_DIRECT, true)
.equalTo(RoomSummaryEntityFields.MEMBERSHIP_STR, Membership.JOIN.name) .equalTo(RoomSummaryEntityFields.MEMBERSHIP_STR, Membership.JOIN.name)
.findAll() .findAll()
.filter { dm -> dm.otherMemberIds.contains(otherUserId) } .firstOrNull { dm -> dm.otherMemberIds.size == 1 && dm.otherMemberIds.first() == otherUserId }
.map { it.roomId } ?.roomId
.firstOrNull { roomId -> otherUserId in RoomMemberHelper(realm, roomId).getActiveRoomMemberIds() }
?.let { roomId -> createRoom(realm, roomId) }
} }
} }

View File

@ -101,6 +101,7 @@ internal abstract class RoomModule {
fun providesHtmlRenderer(): HtmlRenderer { fun providesHtmlRenderer(): HtmlRenderer {
return HtmlRenderer return HtmlRenderer
.builder() .builder()
.softbreak("<br />")
.build() .build()
} }
} }

View File

@ -16,10 +16,10 @@
package org.matrix.android.sdk.internal.session.room.create package org.matrix.android.sdk.internal.session.room.create
import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.session.crypto.crosssigning.CrossSigningService import org.matrix.android.sdk.api.session.crypto.crosssigning.CrossSigningService
import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.toContent
import org.matrix.android.sdk.api.session.identity.IdentityServiceError import org.matrix.android.sdk.api.session.identity.IdentityServiceError
import org.matrix.android.sdk.api.session.identity.toMedium import org.matrix.android.sdk.api.session.identity.toMedium
import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams
@ -27,11 +27,13 @@ import org.matrix.android.sdk.internal.crypto.DeviceListManager
import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
import org.matrix.android.sdk.internal.di.AuthenticatedIdentity import org.matrix.android.sdk.internal.di.AuthenticatedIdentity
import org.matrix.android.sdk.internal.network.token.AccessTokenProvider import org.matrix.android.sdk.internal.network.token.AccessTokenProvider
import org.matrix.android.sdk.internal.session.content.FileUploader
import org.matrix.android.sdk.internal.session.identity.EnsureIdentityTokenTask import org.matrix.android.sdk.internal.session.identity.EnsureIdentityTokenTask
import org.matrix.android.sdk.internal.session.identity.data.IdentityStore import org.matrix.android.sdk.internal.session.identity.data.IdentityStore
import org.matrix.android.sdk.internal.session.identity.data.getIdentityServerUrlWithoutProtocol import org.matrix.android.sdk.internal.session.identity.data.getIdentityServerUrlWithoutProtocol
import org.matrix.android.sdk.internal.session.room.membership.threepid.ThreePidInviteBody import org.matrix.android.sdk.internal.session.room.membership.threepid.ThreePidInviteBody
import java.security.InvalidParameterException import java.security.InvalidParameterException
import java.util.UUID
import javax.inject.Inject import javax.inject.Inject
internal class CreateRoomBodyBuilder @Inject constructor( internal class CreateRoomBodyBuilder @Inject constructor(
@ -39,6 +41,7 @@ internal class CreateRoomBodyBuilder @Inject constructor(
private val crossSigningService: CrossSigningService, private val crossSigningService: CrossSigningService,
private val deviceListManager: DeviceListManager, private val deviceListManager: DeviceListManager,
private val identityStore: IdentityStore, private val identityStore: IdentityStore,
private val fileUploader: FileUploader,
@AuthenticatedIdentity @AuthenticatedIdentity
private val accessTokenProvider: AccessTokenProvider private val accessTokenProvider: AccessTokenProvider
) { ) {
@ -66,7 +69,8 @@ internal class CreateRoomBodyBuilder @Inject constructor(
val initialStates = listOfNotNull( val initialStates = listOfNotNull(
buildEncryptionWithAlgorithmEvent(params), buildEncryptionWithAlgorithmEvent(params),
buildHistoryVisibilityEvent(params) buildHistoryVisibilityEvent(params),
buildAvatarEvent(params)
) )
.takeIf { it.isNotEmpty() } .takeIf { it.isNotEmpty() }
@ -85,15 +89,33 @@ internal class CreateRoomBodyBuilder @Inject constructor(
) )
} }
private suspend fun buildAvatarEvent(params: CreateRoomParams): Event? {
return params.avatarUri?.let { avatarUri ->
// First upload the image, ignoring any error
tryOrNull {
fileUploader.uploadFromUri(
uri = avatarUri,
filename = UUID.randomUUID().toString(),
mimeType = "image/jpeg")
}
?.let { response ->
Event(
type = EventType.STATE_ROOM_AVATAR,
stateKey = "",
content = mapOf("url" to response.contentUri)
)
}
}
}
private fun buildHistoryVisibilityEvent(params: CreateRoomParams): Event? { private fun buildHistoryVisibilityEvent(params: CreateRoomParams): Event? {
return params.historyVisibility return params.historyVisibility
?.let { ?.let {
val contentMap = mapOf("history_visibility" to it)
Event( Event(
type = EventType.STATE_ROOM_HISTORY_VISIBILITY, type = EventType.STATE_ROOM_HISTORY_VISIBILITY,
stateKey = "", stateKey = "",
content = contentMap.toContent()) content = mapOf("history_visibility" to it)
)
} }
} }
@ -111,12 +133,10 @@ internal class CreateRoomBodyBuilder @Inject constructor(
if (it != MXCRYPTO_ALGORITHM_MEGOLM) { if (it != MXCRYPTO_ALGORITHM_MEGOLM) {
throw InvalidParameterException("Unsupported algorithm: $it") throw InvalidParameterException("Unsupported algorithm: $it")
} }
val contentMap = mapOf("algorithm" to it)
Event( Event(
type = EventType.STATE_ROOM_ENCRYPTION, type = EventType.STATE_ROOM_ENCRYPTION,
stateKey = "", stateKey = "",
content = contentMap.toContent() content = mapOf("algorithm" to it)
) )
} }
} }

View File

@ -17,12 +17,10 @@ package org.matrix.android.sdk.internal.session.room.relation
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.Transformations import androidx.lifecycle.Transformations
import androidx.work.OneTimeWorkRequest
import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject import com.squareup.inject.assisted.AssistedInject
import com.zhuinden.monarchy.Monarchy import com.zhuinden.monarchy.Monarchy
import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.MatrixCallback
import org.matrix.android.sdk.api.session.crypto.CryptoService
import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.room.model.EventAnnotationsSummary import org.matrix.android.sdk.api.session.room.model.EventAnnotationsSummary
import org.matrix.android.sdk.api.session.room.model.message.MessageType import org.matrix.android.sdk.api.session.room.model.message.MessageType
@ -32,30 +30,25 @@ import org.matrix.android.sdk.api.util.Cancelable
import org.matrix.android.sdk.api.util.NoOpCancellable import org.matrix.android.sdk.api.util.NoOpCancellable
import org.matrix.android.sdk.api.util.Optional import org.matrix.android.sdk.api.util.Optional
import org.matrix.android.sdk.api.util.toOptional import org.matrix.android.sdk.api.util.toOptional
import org.matrix.android.sdk.internal.crypto.CryptoSessionInfoProvider
import org.matrix.android.sdk.internal.database.mapper.TimelineEventMapper import org.matrix.android.sdk.internal.database.mapper.TimelineEventMapper
import org.matrix.android.sdk.internal.database.mapper.asDomain import org.matrix.android.sdk.internal.database.mapper.asDomain
import org.matrix.android.sdk.internal.database.model.EventAnnotationsSummaryEntity import org.matrix.android.sdk.internal.database.model.EventAnnotationsSummaryEntity
import org.matrix.android.sdk.internal.database.model.TimelineEventEntity import org.matrix.android.sdk.internal.database.model.TimelineEventEntity
import org.matrix.android.sdk.internal.database.query.where import org.matrix.android.sdk.internal.database.query.where
import org.matrix.android.sdk.internal.di.SessionDatabase import org.matrix.android.sdk.internal.di.SessionDatabase
import org.matrix.android.sdk.internal.di.SessionId
import org.matrix.android.sdk.internal.session.room.send.EncryptEventWorker
import org.matrix.android.sdk.internal.session.room.send.LocalEchoEventFactory import org.matrix.android.sdk.internal.session.room.send.LocalEchoEventFactory
import org.matrix.android.sdk.internal.session.room.send.RedactEventWorker import org.matrix.android.sdk.internal.session.room.send.queue.EventSenderProcessor
import org.matrix.android.sdk.internal.session.room.send.SendEventWorker
import org.matrix.android.sdk.internal.session.room.timeline.TimelineSendEventWorkCommon
import org.matrix.android.sdk.internal.task.TaskExecutor import org.matrix.android.sdk.internal.task.TaskExecutor
import org.matrix.android.sdk.internal.task.configureWith import org.matrix.android.sdk.internal.task.configureWith
import org.matrix.android.sdk.internal.util.fetchCopyMap import org.matrix.android.sdk.internal.util.fetchCopyMap
import org.matrix.android.sdk.internal.worker.WorkerParamsFactory
import timber.log.Timber import timber.log.Timber
internal class DefaultRelationService @AssistedInject constructor( internal class DefaultRelationService @AssistedInject constructor(
@Assisted private val roomId: String, @Assisted private val roomId: String,
@SessionId private val sessionId: String, private val eventSenderProcessor: EventSenderProcessor,
private val timeLineSendEventWorkCommon: TimelineSendEventWorkCommon,
private val eventFactory: LocalEchoEventFactory, private val eventFactory: LocalEchoEventFactory,
private val cryptoService: CryptoService, private val cryptoSessionInfoProvider: CryptoSessionInfoProvider,
private val findReactionEventForUndoTask: FindReactionEventForUndoTask, private val findReactionEventForUndoTask: FindReactionEventForUndoTask,
private val fetchEditHistoryTask: FetchEditHistoryTask, private val fetchEditHistoryTask: FetchEditHistoryTask,
private val timelineEventMapper: TimelineEventMapper, private val timelineEventMapper: TimelineEventMapper,
@ -83,8 +76,7 @@ internal class DefaultRelationService @AssistedInject constructor(
.none { it.addedByMe && it.key == reaction }) { .none { it.addedByMe && it.key == reaction }) {
val event = eventFactory.createReactionEvent(roomId, targetEventId, reaction) val event = eventFactory.createReactionEvent(roomId, targetEventId, reaction)
.also { saveLocalEcho(it) } .also { saveLocalEcho(it) }
val sendRelationWork = createSendEventWork(event, true) return eventSenderProcessor.postEvent(event, false /* reaction are not encrypted*/)
timeLineSendEventWorkCommon.postWork(roomId, sendRelationWork)
} else { } else {
Timber.w("Reaction already added") Timber.w("Reaction already added")
NoOpCancellable NoOpCancellable
@ -107,9 +99,7 @@ internal class DefaultRelationService @AssistedInject constructor(
data.redactEventId?.let { toRedact -> data.redactEventId?.let { toRedact ->
val redactEvent = eventFactory.createRedactEvent(roomId, toRedact, null) val redactEvent = eventFactory.createRedactEvent(roomId, toRedact, null)
.also { saveLocalEcho(it) } .also { saveLocalEcho(it) }
val redactWork = createRedactEventWork(redactEvent, toRedact, null) eventSenderProcessor.postRedaction(redactEvent, null)
timeLineSendEventWorkCommon.postWork(roomId, redactWork)
} }
} }
} }
@ -121,18 +111,6 @@ internal class DefaultRelationService @AssistedInject constructor(
.executeBy(taskExecutor) .executeBy(taskExecutor)
} }
// TODO duplicate with send service?
private fun createRedactEventWork(localEvent: Event, eventId: String, reason: String?): OneTimeWorkRequest {
val sendContentWorkerParams = RedactEventWorker.Params(
sessionId,
localEvent.eventId!!,
roomId,
eventId,
reason)
val redactWorkData = WorkerParamsFactory.toData(sendContentWorkerParams)
return timeLineSendEventWorkCommon.createWork<RedactEventWorker>(redactWorkData, true)
}
override fun editTextMessage(targetEventId: String, override fun editTextMessage(targetEventId: String,
msgType: String, msgType: String,
newBodyText: CharSequence, newBodyText: CharSequence,
@ -141,14 +119,7 @@ internal class DefaultRelationService @AssistedInject constructor(
val event = eventFactory val event = eventFactory
.createReplaceTextEvent(roomId, targetEventId, newBodyText, newBodyAutoMarkdown, msgType, compatibilityBodyText) .createReplaceTextEvent(roomId, targetEventId, newBodyText, newBodyAutoMarkdown, msgType, compatibilityBodyText)
.also { saveLocalEcho(it) } .also { saveLocalEcho(it) }
return if (cryptoService.isRoomEncrypted(roomId)) { return eventSenderProcessor.postEvent(event, cryptoSessionInfoProvider.isRoomEncrypted(roomId))
val encryptWork = createEncryptEventWork(event, listOf("m.relates_to"))
val workRequest = createSendEventWork(event, false)
timeLineSendEventWorkCommon.postSequentialWorks(roomId, encryptWork, workRequest)
} else {
val workRequest = createSendEventWork(event, true)
timeLineSendEventWorkCommon.postWork(roomId, workRequest)
}
} }
override fun editReply(replyToEdit: TimelineEvent, override fun editReply(replyToEdit: TimelineEvent,
@ -165,18 +136,11 @@ internal class DefaultRelationService @AssistedInject constructor(
compatibilityBodyText compatibilityBodyText
) )
.also { saveLocalEcho(it) } .also { saveLocalEcho(it) }
return if (cryptoService.isRoomEncrypted(roomId)) { return eventSenderProcessor.postEvent(event, cryptoSessionInfoProvider.isRoomEncrypted(roomId))
val encryptWork = createEncryptEventWork(event, listOf("m.relates_to"))
val workRequest = createSendEventWork(event, false)
timeLineSendEventWorkCommon.postSequentialWorks(roomId, encryptWork, workRequest)
} else {
val workRequest = createSendEventWork(event, true)
timeLineSendEventWorkCommon.postWork(roomId, workRequest)
}
} }
override fun fetchEditHistory(eventId: String, callback: MatrixCallback<List<Event>>) { override fun fetchEditHistory(eventId: String, callback: MatrixCallback<List<Event>>) {
val params = FetchEditHistoryTask.Params(roomId, cryptoService.isRoomEncrypted(roomId), eventId) val params = FetchEditHistoryTask.Params(roomId, cryptoSessionInfoProvider.isRoomEncrypted(roomId), eventId)
fetchEditHistoryTask fetchEditHistoryTask
.configureWith(params) { .configureWith(params) {
this.callback = callback this.callback = callback
@ -189,27 +153,7 @@ internal class DefaultRelationService @AssistedInject constructor(
?.also { saveLocalEcho(it) } ?.also { saveLocalEcho(it) }
?: return null ?: return null
return if (cryptoService.isRoomEncrypted(roomId)) { return eventSenderProcessor.postEvent(event, cryptoSessionInfoProvider.isRoomEncrypted(roomId))
val encryptWork = createEncryptEventWork(event, listOf("m.relates_to"))
val workRequest = createSendEventWork(event, false)
timeLineSendEventWorkCommon.postSequentialWorks(roomId, encryptWork, workRequest)
} else {
val workRequest = createSendEventWork(event, true)
timeLineSendEventWorkCommon.postWork(roomId, workRequest)
}
}
private fun createEncryptEventWork(event: Event, keepKeys: List<String>?): OneTimeWorkRequest {
// Same parameter
val params = EncryptEventWorker.Params(sessionId, event.eventId!!, keepKeys)
val sendWorkData = WorkerParamsFactory.toData(params)
return timeLineSendEventWorkCommon.createWork<EncryptEventWorker>(sendWorkData, true)
}
private fun createSendEventWork(event: Event, startChain: Boolean): OneTimeWorkRequest {
val sendContentWorkerParams = SendEventWorker.Params(sessionId = sessionId, eventId = event.eventId!!)
val sendWorkData = WorkerParamsFactory.toData(sendContentWorkerParams)
return timeLineSendEventWorkCommon.createWork<SendEventWorker>(sendWorkData, startChain)
} }
override fun getEventAnnotationsSummary(eventId: String): EventAnnotationsSummary? { override fun getEventAnnotationsSummary(eventId: String): EventAnnotationsSummary? {

View File

@ -25,7 +25,6 @@ import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject import com.squareup.inject.assisted.AssistedInject
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.session.content.ContentAttachmentData import org.matrix.android.sdk.api.session.content.ContentAttachmentData
import org.matrix.android.sdk.api.session.crypto.CryptoService
import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.events.model.isAttachmentMessage import org.matrix.android.sdk.api.session.events.model.isAttachmentMessage
import org.matrix.android.sdk.api.session.events.model.isTextMessage import org.matrix.android.sdk.api.session.events.model.isTextMessage
@ -45,13 +44,13 @@ import org.matrix.android.sdk.api.util.Cancelable
import org.matrix.android.sdk.api.util.CancelableBag import org.matrix.android.sdk.api.util.CancelableBag
import org.matrix.android.sdk.api.util.JsonDict import org.matrix.android.sdk.api.util.JsonDict
import org.matrix.android.sdk.api.util.NoOpCancellable import org.matrix.android.sdk.api.util.NoOpCancellable
import org.matrix.android.sdk.internal.crypto.CryptoSessionInfoProvider
import org.matrix.android.sdk.internal.di.SessionId import org.matrix.android.sdk.internal.di.SessionId
import org.matrix.android.sdk.internal.di.WorkManagerProvider import org.matrix.android.sdk.internal.di.WorkManagerProvider
import org.matrix.android.sdk.internal.session.content.UploadContentWorker import org.matrix.android.sdk.internal.session.content.UploadContentWorker
import org.matrix.android.sdk.internal.session.room.timeline.TimelineSendEventWorkCommon import org.matrix.android.sdk.internal.session.room.send.queue.EventSenderProcessor
import org.matrix.android.sdk.internal.task.TaskExecutor import org.matrix.android.sdk.internal.task.TaskExecutor
import org.matrix.android.sdk.internal.util.CancelableWork import org.matrix.android.sdk.internal.util.CancelableWork
import org.matrix.android.sdk.internal.worker.AlwaysSuccessfulWorker
import org.matrix.android.sdk.internal.worker.WorkerParamsFactory import org.matrix.android.sdk.internal.worker.WorkerParamsFactory
import org.matrix.android.sdk.internal.worker.startChain import org.matrix.android.sdk.internal.worker.startChain
import timber.log.Timber import timber.log.Timber
@ -63,13 +62,12 @@ private const val UPLOAD_WORK = "UPLOAD_WORK"
internal class DefaultSendService @AssistedInject constructor( internal class DefaultSendService @AssistedInject constructor(
@Assisted private val roomId: String, @Assisted private val roomId: String,
private val workManagerProvider: WorkManagerProvider, private val workManagerProvider: WorkManagerProvider,
private val timelineSendEventWorkCommon: TimelineSendEventWorkCommon,
@SessionId private val sessionId: String, @SessionId private val sessionId: String,
private val localEchoEventFactory: LocalEchoEventFactory, private val localEchoEventFactory: LocalEchoEventFactory,
private val cryptoService: CryptoService, private val cryptoSessionInfoProvider: CryptoSessionInfoProvider,
private val taskExecutor: TaskExecutor, private val taskExecutor: TaskExecutor,
private val localEchoRepository: LocalEchoRepository, private val localEchoRepository: LocalEchoRepository,
private val roomEventSender: RoomEventSender, private val eventSenderProcessor: EventSenderProcessor,
private val cancelSendTracker: CancelSendTracker private val cancelSendTracker: CancelSendTracker
) : SendService { ) : SendService {
@ -92,19 +90,6 @@ internal class DefaultSendService @AssistedInject constructor(
.let { sendEvent(it) } .let { sendEvent(it) }
} }
// For test only
private fun sendTextMessages(text: CharSequence, msgType: String, autoMarkdown: Boolean, times: Int): Cancelable {
return CancelableBag().apply {
// Send the event several times
repeat(times) { i ->
localEchoEventFactory.createTextEvent(roomId, msgType, "$text - $i", autoMarkdown)
.also { createLocalEcho(it) }
.let { sendEvent(it) }
.also { add(it) }
}
}
}
override fun sendFormattedTextMessage(text: String, formattedText: String, msgType: String): Cancelable { override fun sendFormattedTextMessage(text: String, formattedText: String, msgType: String): Cancelable {
return localEchoEventFactory.createFormattedTextEvent(roomId, TextContent(text, formattedText), msgType) return localEchoEventFactory.createFormattedTextEvent(roomId, TextContent(text, formattedText), msgType)
.also { createLocalEcho(it) } .also { createLocalEcho(it) }
@ -133,13 +118,14 @@ internal class DefaultSendService @AssistedInject constructor(
override fun redactEvent(event: Event, reason: String?): Cancelable { override fun redactEvent(event: Event, reason: String?): Cancelable {
// TODO manage media/attachements? // TODO manage media/attachements?
return createRedactEventWork(event, reason) val redactionEcho = localEchoEventFactory.createRedactEvent(roomId, event.eventId!!, reason)
.let { timelineSendEventWorkCommon.postWork(roomId, it) } .also { createLocalEcho(it) }
return eventSenderProcessor.postRedaction(redactionEcho, reason)
} }
override fun resendTextMessage(localEcho: TimelineEvent): Cancelable { override fun resendTextMessage(localEcho: TimelineEvent): Cancelable {
if (localEcho.root.isTextMessage() && localEcho.root.sendState.hasFailed()) { if (localEcho.root.isTextMessage() && localEcho.root.sendState.hasFailed()) {
localEchoRepository.updateSendState(localEcho.eventId, SendState.UNSENT) localEchoRepository.updateSendState(localEcho.eventId, roomId, SendState.UNSENT)
return sendEvent(localEcho.root) return sendEvent(localEcho.root)
} }
return NoOpCancellable return NoOpCancellable
@ -153,7 +139,7 @@ internal class DefaultSendService @AssistedInject constructor(
val url = messageContent.getFileUrl() ?: return NoOpCancellable val url = messageContent.getFileUrl() ?: return NoOpCancellable
if (url.startsWith("mxc://")) { if (url.startsWith("mxc://")) {
// We need to resend only the message as the attachment is ok // We need to resend only the message as the attachment is ok
localEchoRepository.updateSendState(localEcho.eventId, SendState.UNSENT) localEchoRepository.updateSendState(localEcho.eventId, roomId, SendState.UNSENT)
return sendEvent(localEcho.root) return sendEvent(localEcho.root)
} }
@ -170,7 +156,7 @@ internal class DefaultSendService @AssistedInject constructor(
queryUri = Uri.parse(messageContent.url), queryUri = Uri.parse(messageContent.url),
type = ContentAttachmentData.Type.IMAGE type = ContentAttachmentData.Type.IMAGE
) )
localEchoRepository.updateSendState(localEcho.eventId, SendState.UNSENT) localEchoRepository.updateSendState(localEcho.eventId, roomId, SendState.UNSENT)
internalSendMedia(listOf(localEcho.root), attachmentData, true) internalSendMedia(listOf(localEcho.root), attachmentData, true)
} }
is MessageVideoContent -> { is MessageVideoContent -> {
@ -184,7 +170,7 @@ internal class DefaultSendService @AssistedInject constructor(
queryUri = Uri.parse(messageContent.url), queryUri = Uri.parse(messageContent.url),
type = ContentAttachmentData.Type.VIDEO type = ContentAttachmentData.Type.VIDEO
) )
localEchoRepository.updateSendState(localEcho.eventId, SendState.UNSENT) localEchoRepository.updateSendState(localEcho.eventId, roomId, SendState.UNSENT)
internalSendMedia(listOf(localEcho.root), attachmentData, true) internalSendMedia(listOf(localEcho.root), attachmentData, true)
} }
is MessageFileContent -> { is MessageFileContent -> {
@ -195,7 +181,7 @@ internal class DefaultSendService @AssistedInject constructor(
queryUri = Uri.parse(messageContent.url), queryUri = Uri.parse(messageContent.url),
type = ContentAttachmentData.Type.FILE type = ContentAttachmentData.Type.FILE
) )
localEchoRepository.updateSendState(localEcho.eventId, SendState.UNSENT) localEchoRepository.updateSendState(localEcho.eventId, roomId, SendState.UNSENT)
internalSendMedia(listOf(localEcho.root), attachmentData, true) internalSendMedia(listOf(localEcho.root), attachmentData, true)
} }
is MessageAudioContent -> { is MessageAudioContent -> {
@ -207,7 +193,7 @@ internal class DefaultSendService @AssistedInject constructor(
queryUri = Uri.parse(messageContent.url), queryUri = Uri.parse(messageContent.url),
type = ContentAttachmentData.Type.AUDIO type = ContentAttachmentData.Type.AUDIO
) )
localEchoRepository.updateSendState(localEcho.eventId, SendState.UNSENT) localEchoRepository.updateSendState(localEcho.eventId, roomId, SendState.UNSENT)
internalSendMedia(listOf(localEcho.root), attachmentData, true) internalSendMedia(listOf(localEcho.root), attachmentData, true)
} }
else -> NoOpCancellable else -> NoOpCancellable
@ -222,25 +208,6 @@ internal class DefaultSendService @AssistedInject constructor(
} }
} }
override fun clearSendingQueue() {
timelineSendEventWorkCommon.cancelAllWorks(roomId)
workManagerProvider.workManager.cancelUniqueWork(buildWorkName(UPLOAD_WORK))
// Replace the worker chains with a AlwaysSuccessfulWorker, to ensure the queues are well emptied
workManagerProvider.matrixOneTimeWorkRequestBuilder<AlwaysSuccessfulWorker>()
.build().let {
timelineSendEventWorkCommon.postWork(roomId, it, ExistingWorkPolicy.REPLACE)
// need to clear also image sending queue
workManagerProvider.workManager
.beginUniqueWork(buildWorkName(UPLOAD_WORK), ExistingWorkPolicy.REPLACE, it)
.enqueue()
}
taskExecutor.executorScope.launch {
localEchoRepository.clearSendingQueue(roomId)
}
}
override fun cancelSend(eventId: String) { override fun cancelSend(eventId: String) {
cancelSendTracker.markLocalEchoForCancel(eventId, roomId) cancelSendTracker.markLocalEchoForCancel(eventId, roomId)
taskExecutor.executorScope.launch { taskExecutor.executorScope.launch {
@ -262,13 +229,6 @@ internal class DefaultSendService @AssistedInject constructor(
} }
} }
// override fun failAllPendingMessages() {
// taskExecutor.executorScope.launch {
// val eventsToResend = localEchoRepository.getAllEventsWithStates(roomId, SendState.PENDING_STATES)
// localEchoRepository.updateSendState(roomId, eventsToResend.map { it.eventId }, SendState.UNDELIVERED)
// }
// }
override fun sendMedia(attachment: ContentAttachmentData, override fun sendMedia(attachment: ContentAttachmentData,
compressBeforeSending: Boolean, compressBeforeSending: Boolean,
roomIds: Set<String>): Cancelable { roomIds: Set<String>): Cancelable {
@ -291,7 +251,7 @@ internal class DefaultSendService @AssistedInject constructor(
private fun internalSendMedia(allLocalEchoes: List<Event>, attachment: ContentAttachmentData, compressBeforeSending: Boolean): Cancelable { private fun internalSendMedia(allLocalEchoes: List<Event>, attachment: ContentAttachmentData, compressBeforeSending: Boolean): Cancelable {
val cancelableBag = CancelableBag() val cancelableBag = CancelableBag()
allLocalEchoes.groupBy { cryptoService.isRoomEncrypted(it.roomId!!) } allLocalEchoes.groupBy { cryptoSessionInfoProvider.isRoomEncrypted(it.roomId!!) }
.apply { .apply {
keys.forEach { isRoomEncrypted -> keys.forEach { isRoomEncrypted ->
// Should never be empty // Should never be empty
@ -301,7 +261,7 @@ internal class DefaultSendService @AssistedInject constructor(
val dispatcherWork = createMultipleEventDispatcherWork(isRoomEncrypted) val dispatcherWork = createMultipleEventDispatcherWork(isRoomEncrypted)
workManagerProvider.workManager workManagerProvider.workManager
.beginUniqueWork(buildWorkName(UPLOAD_WORK), ExistingWorkPolicy.APPEND, uploadWork) .beginUniqueWork(buildWorkName(UPLOAD_WORK), ExistingWorkPolicy.APPEND_OR_REPLACE, uploadWork)
.then(dispatcherWork) .then(dispatcherWork)
.enqueue() .enqueue()
.also { operation -> .also { operation ->
@ -322,7 +282,7 @@ internal class DefaultSendService @AssistedInject constructor(
} }
private fun sendEvent(event: Event): Cancelable { private fun sendEvent(event: Event): Cancelable {
return roomEventSender.sendEvent(event) return eventSenderProcessor.postEvent(event, cryptoSessionInfoProvider.isRoomEncrypted(event.roomId!!))
} }
private fun createLocalEcho(event: Event) { private fun createLocalEcho(event: Event) {
@ -333,28 +293,6 @@ internal class DefaultSendService @AssistedInject constructor(
return "${roomId}_$identifier" return "${roomId}_$identifier"
} }
private fun createEncryptEventWork(event: Event, startChain: Boolean): OneTimeWorkRequest {
// Same parameter
return EncryptEventWorker.Params(sessionId, event.eventId ?: "")
.let { WorkerParamsFactory.toData(it) }
.let {
workManagerProvider.matrixOneTimeWorkRequestBuilder<EncryptEventWorker>()
.setConstraints(WorkManagerProvider.workConstraints)
.setInputData(it)
.startChain(startChain)
.setBackoffCriteria(BackoffPolicy.LINEAR, WorkManagerProvider.BACKOFF_DELAY, TimeUnit.MILLISECONDS)
.build()
}
}
private fun createRedactEventWork(event: Event, reason: String?): OneTimeWorkRequest {
return localEchoEventFactory.createRedactEvent(roomId, event.eventId!!, reason)
.also { createLocalEcho(it) }
.let { RedactEventWorker.Params(sessionId, it.eventId!!, roomId, event.eventId, reason) }
.let { WorkerParamsFactory.toData(it) }
.let { timelineSendEventWorkCommon.createWork<RedactEventWorker>(it, true) }
}
private fun createUploadMediaWork(allLocalEchos: List<Event>, private fun createUploadMediaWork(allLocalEchos: List<Event>,
attachment: ContentAttachmentData, attachment: ContentAttachmentData,
isRoomEncrypted: Boolean, isRoomEncrypted: Boolean,

View File

@ -1,146 +0,0 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.session.room.send
import android.content.Context
import androidx.work.WorkerParameters
import com.squareup.moshi.JsonClass
import org.matrix.android.sdk.api.failure.Failure
import org.matrix.android.sdk.api.session.crypto.CryptoService
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.toContent
import org.matrix.android.sdk.api.session.room.send.SendState
import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
import org.matrix.android.sdk.internal.crypto.MXEventDecryptionResult
import org.matrix.android.sdk.internal.crypto.model.MXEncryptEventContentResult
import org.matrix.android.sdk.internal.database.mapper.ContentMapper
import org.matrix.android.sdk.internal.session.SessionComponent
import org.matrix.android.sdk.internal.util.awaitCallback
import org.matrix.android.sdk.internal.worker.SessionSafeCoroutineWorker
import org.matrix.android.sdk.internal.worker.SessionWorkerParams
import org.matrix.android.sdk.internal.worker.WorkerParamsFactory
import timber.log.Timber
import javax.inject.Inject
/**
* Possible previous worker: None
* Possible next worker : Always [SendEventWorker]
*/
internal class EncryptEventWorker(context: Context, params: WorkerParameters)
: SessionSafeCoroutineWorker<EncryptEventWorker.Params>(context, params, Params::class.java) {
@JsonClass(generateAdapter = true)
internal data class Params(
override val sessionId: String,
val eventId: String,
/** Do not encrypt these keys, keep them as is in encrypted content (e.g. m.relates_to) */
val keepKeys: List<String>? = null,
override val lastFailureMessage: String? = null
) : SessionWorkerParams
@Inject lateinit var crypto: CryptoService
@Inject lateinit var localEchoRepository: LocalEchoRepository
@Inject lateinit var cancelSendTracker: CancelSendTracker
override fun injectWith(injector: SessionComponent) {
injector.inject(this)
}
override suspend fun doSafeWork(params: Params): Result {
Timber.v("## SendEvent: Start Encrypt work for event ${params.eventId}")
val localEvent = localEchoRepository.getUpToDateEcho(params.eventId)
if (localEvent?.eventId == null) {
return Result.success()
}
if (cancelSendTracker.isCancelRequestedFor(localEvent.eventId, localEvent.roomId)) {
return Result.success()
.also { Timber.e("## SendEvent: Event sending has been cancelled ${localEvent.eventId}") }
}
localEchoRepository.updateSendState(localEvent.eventId, SendState.ENCRYPTING)
val localMutableContent = localEvent.content?.toMutableMap() ?: mutableMapOf()
params.keepKeys?.forEach {
localMutableContent.remove(it)
}
var error: Throwable? = null
var result: MXEncryptEventContentResult? = null
try {
result = awaitCallback {
crypto.encryptEventContent(localMutableContent, localEvent.type, localEvent.roomId!!, it)
}
} catch (throwable: Throwable) {
error = throwable
}
if (result != null) {
val modifiedContent = HashMap(result.eventContent)
params.keepKeys?.forEach { toKeep ->
localEvent.content?.get(toKeep)?.let {
// put it back in the encrypted thing
modifiedContent[toKeep] = it
}
}
// Better handling of local echo, to avoid decrypting transition on remote echo
// Should I only do it for text messages?
val decryptionLocalEcho = if (result.eventContent["algorithm"] == MXCRYPTO_ALGORITHM_MEGOLM) {
MXEventDecryptionResult(
clearEvent = Event(
type = localEvent.type,
content = localEvent.content,
roomId = localEvent.roomId
).toContent(),
forwardingCurve25519KeyChain = emptyList(),
senderCurve25519Key = result.eventContent["sender_key"] as? String,
claimedEd25519Key = crypto.getMyDevice().fingerprint()
)
} else {
null
}
localEchoRepository.updateEcho(localEvent.eventId) { _, localEcho ->
localEcho.type = EventType.ENCRYPTED
localEcho.content = ContentMapper.map(modifiedContent)
decryptionLocalEcho?.also {
localEcho.setDecryptionResult(it)
}
}
val nextWorkerParams = SendEventWorker.Params(sessionId = params.sessionId, eventId = params.eventId)
return Result.success(WorkerParamsFactory.toData(nextWorkerParams))
} else {
val sendState = when (error) {
is Failure.CryptoError -> SendState.FAILED_UNKNOWN_DEVICES
else -> SendState.UNDELIVERED
}
localEchoRepository.updateSendState(localEvent.eventId, sendState)
// always return success, or the chain will be stuck for ever!
val nextWorkerParams = SendEventWorker.Params(
sessionId = params.sessionId,
eventId = localEvent.eventId,
lastFailureMessage = error?.localizedMessage ?: "Error"
)
return Result.success(WorkerParamsFactory.toData(nextWorkerParams))
}
}
override fun buildErrorParams(params: Params, message: String): Params {
return params.copy(lastFailureMessage = params.lastFailureMessage ?: message)
}
}

View File

@ -90,8 +90,7 @@ internal class LocalEchoEventFactory @Inject constructor(
private fun createTextContent(text: CharSequence, autoMarkdown: Boolean): TextContent { private fun createTextContent(text: CharSequence, autoMarkdown: Boolean): TextContent {
if (autoMarkdown) { if (autoMarkdown) {
val source = textPillsUtils.processSpecialSpansToMarkdown(text) ?: text.toString() return markdownParser.parse(text)
return markdownParser.parse(source)
} else { } else {
// Try to detect pills // Try to detect pills
textPillsUtils.processSpecialSpansToHtml(text)?.let { textPillsUtils.processSpecialSpansToHtml(text)?.let {

View File

@ -88,8 +88,9 @@ internal class LocalEchoRepository @Inject constructor(@SessionDatabase private
} }
} }
fun updateSendState(eventId: String, sendState: SendState) { fun updateSendState(eventId: String, roomId: String?, sendState: SendState) {
Timber.v("## SendEvent: [${System.currentTimeMillis()}] Update local state of $eventId to ${sendState.name}") Timber.v("## SendEvent: [${System.currentTimeMillis()}] Update local state of $eventId to ${sendState.name}")
eventBus.post(DefaultTimeline.OnLocalEchoUpdated(roomId ?: "", eventId, sendState))
updateEchoAsync(eventId) { realm, sendingEventEntity -> updateEchoAsync(eventId) { realm, sendingEventEntity ->
if (sendState == SendState.SENT && sendingEventEntity.sendState == SendState.SYNCED) { if (sendState == SendState.SENT && sendingEventEntity.sendState == SendState.SYNCED) {
// If already synced, do not put as sent // If already synced, do not put as sent
@ -137,6 +138,14 @@ internal class LocalEchoRepository @Inject constructor(@SessionDatabase private
} }
} }
fun deleteFailedEchoAsync(roomId: String, eventId: String?) {
monarchy.runTransactionSync { realm ->
TimelineEventEntity.where(realm, roomId = roomId, eventId = eventId ?: "").findFirst()?.deleteFromRealm()
EventEntity.where(realm, eventId = eventId ?: "").findFirst()?.deleteFromRealm()
roomSummaryUpdater.updateSendingInformation(realm, roomId)
}
}
suspend fun clearSendingQueue(roomId: String) { suspend fun clearSendingQueue(roomId: String) {
monarchy.awaitTransaction { realm -> monarchy.awaitTransaction { realm ->
TimelineEventEntity TimelineEventEntity

View File

@ -18,6 +18,7 @@ package org.matrix.android.sdk.internal.session.room.send
import org.commonmark.parser.Parser import org.commonmark.parser.Parser
import org.commonmark.renderer.html.HtmlRenderer import org.commonmark.renderer.html.HtmlRenderer
import org.matrix.android.sdk.internal.session.room.send.pills.TextPillsUtils
import javax.inject.Inject import javax.inject.Inject
/** /**
@ -27,18 +28,21 @@ import javax.inject.Inject
*/ */
internal class MarkdownParser @Inject constructor( internal class MarkdownParser @Inject constructor(
private val parser: Parser, private val parser: Parser,
private val htmlRenderer: HtmlRenderer private val htmlRenderer: HtmlRenderer,
private val textPillsUtils: TextPillsUtils
) { ) {
private val mdSpecialChars = "[`_\\-*>.\\[\\]#~]".toRegex() private val mdSpecialChars = "[`_\\-*>.\\[\\]#~]".toRegex()
fun parse(text: String): TextContent { fun parse(text: CharSequence): TextContent {
val source = textPillsUtils.processSpecialSpansToMarkdown(text) ?: text.toString()
// If no special char are detected, just return plain text // If no special char are detected, just return plain text
if (text.contains(mdSpecialChars).not()) { if (source.contains(mdSpecialChars).not()) {
return TextContent(text) return TextContent(source)
} }
val document = parser.parse(text) val document = parser.parse(source)
val htmlText = htmlRenderer.render(document) val htmlText = htmlRenderer.render(document)
// Cleanup extra paragraph // Cleanup extra paragraph
@ -48,13 +52,14 @@ internal class MarkdownParser @Inject constructor(
htmlText htmlText
} }
return if (isFormattedTextPertinent(text, cleanHtmlText)) { return if (isFormattedTextPertinent(source, cleanHtmlText)) {
// According to https://matrix.org/docs/spec/client_server/latest#m-room-message-msgtypes: // According to https://matrix.org/docs/spec/client_server/latest#m-room-message-msgtypes:
// The plain text version of the HTML should be provided in the body. // The plain text version of the HTML should be provided in the body.
// But it caused too many problems so it has been removed in #2002 // But it caused too many problems so it has been removed in #2002
TextContent(text, cleanHtmlText.postTreatment()) // See #739
TextContent(text.toString(), cleanHtmlText.postTreatment())
} else { } else {
TextContent(text) TextContent(source)
} }
} }

View File

@ -17,7 +17,6 @@
package org.matrix.android.sdk.internal.session.room.send package org.matrix.android.sdk.internal.session.room.send
import android.content.Context import android.content.Context
import androidx.work.BackoffPolicy
import androidx.work.OneTimeWorkRequest import androidx.work.OneTimeWorkRequest
import androidx.work.WorkerParameters import androidx.work.WorkerParameters
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
@ -31,7 +30,6 @@ import org.matrix.android.sdk.internal.worker.SessionWorkerParams
import org.matrix.android.sdk.internal.worker.WorkerParamsFactory import org.matrix.android.sdk.internal.worker.WorkerParamsFactory
import org.matrix.android.sdk.internal.worker.startChain import org.matrix.android.sdk.internal.worker.startChain
import timber.log.Timber import timber.log.Timber
import java.util.concurrent.TimeUnit
import javax.inject.Inject import javax.inject.Inject
/** /**
@ -57,7 +55,7 @@ internal class MultipleEventSendingDispatcherWorker(context: Context, params: Wo
override fun doOnError(params: Params): Result { override fun doOnError(params: Params): Result {
params.localEchoIds.forEach { localEchoIds -> params.localEchoIds.forEach { localEchoIds ->
localEchoRepository.updateSendState(localEchoIds.eventId, SendState.UNDELIVERED) localEchoRepository.updateSendState(localEchoIds.eventId, localEchoIds.roomId, SendState.UNDELIVERED)
} }
return super.doOnError(params) return super.doOnError(params)
@ -73,20 +71,11 @@ internal class MultipleEventSendingDispatcherWorker(context: Context, params: Wo
params.localEchoIds.forEach { localEchoIds -> params.localEchoIds.forEach { localEchoIds ->
val roomId = localEchoIds.roomId val roomId = localEchoIds.roomId
val eventId = localEchoIds.eventId val eventId = localEchoIds.eventId
if (params.isEncrypted) { localEchoRepository.updateSendState(eventId, roomId, SendState.SENDING)
localEchoRepository.updateSendState(eventId, SendState.ENCRYPTING)
Timber.v("## SendEvent: [${System.currentTimeMillis()}] Schedule encrypt and send event $eventId")
val encryptWork = createEncryptEventWork(params.sessionId, eventId, true)
// Note that event will be replaced by the result of the previous work
val sendWork = createSendEventWork(params.sessionId, eventId, false)
timelineSendEventWorkCommon.postSequentialWorks(roomId, encryptWork, sendWork)
} else {
localEchoRepository.updateSendState(eventId, SendState.SENDING)
Timber.v("## SendEvent: [${System.currentTimeMillis()}] Schedule send event $eventId") Timber.v("## SendEvent: [${System.currentTimeMillis()}] Schedule send event $eventId")
val sendWork = createSendEventWork(params.sessionId, eventId, true) val sendWork = createSendEventWork(params.sessionId, eventId, true)
timelineSendEventWorkCommon.postWork(roomId, sendWork) timelineSendEventWorkCommon.postWork(roomId, sendWork)
} }
}
return Result.success() return Result.success()
} }
@ -95,18 +84,6 @@ internal class MultipleEventSendingDispatcherWorker(context: Context, params: Wo
return params.copy(lastFailureMessage = params.lastFailureMessage ?: message) return params.copy(lastFailureMessage = params.lastFailureMessage ?: message)
} }
private fun createEncryptEventWork(sessionId: String, eventId: String, startChain: Boolean): OneTimeWorkRequest {
val params = EncryptEventWorker.Params(sessionId, eventId)
val sendWorkData = WorkerParamsFactory.toData(params)
return workManagerProvider.matrixOneTimeWorkRequestBuilder<EncryptEventWorker>()
.setConstraints(WorkManagerProvider.workConstraints)
.setInputData(sendWorkData)
.startChain(startChain)
.setBackoffCriteria(BackoffPolicy.LINEAR, WorkManagerProvider.BACKOFF_DELAY, TimeUnit.MILLISECONDS)
.build()
}
private fun createSendEventWork(sessionId: String, eventId: String, startChain: Boolean): OneTimeWorkRequest { private fun createSendEventWork(sessionId: String, eventId: String, startChain: Boolean): OneTimeWorkRequest {
val sendContentWorkerParams = SendEventWorker.Params(sessionId = sessionId, eventId = eventId) val sendContentWorkerParams = SendEventWorker.Params(sessionId = sessionId, eventId = eventId)
val sendWorkData = WorkerParamsFactory.toData(sendContentWorkerParams) val sendWorkData = WorkerParamsFactory.toData(sendContentWorkerParams)

View File

@ -1,75 +0,0 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.session.room.send
import androidx.work.BackoffPolicy
import androidx.work.OneTimeWorkRequest
import org.matrix.android.sdk.api.session.crypto.CryptoService
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.util.Cancelable
import org.matrix.android.sdk.internal.di.SessionId
import org.matrix.android.sdk.internal.di.WorkManagerProvider
import org.matrix.android.sdk.internal.session.room.timeline.TimelineSendEventWorkCommon
import org.matrix.android.sdk.internal.worker.WorkerParamsFactory
import org.matrix.android.sdk.internal.worker.startChain
import timber.log.Timber
import java.util.concurrent.TimeUnit
import javax.inject.Inject
internal class RoomEventSender @Inject constructor(
private val workManagerProvider: WorkManagerProvider,
private val timelineSendEventWorkCommon: TimelineSendEventWorkCommon,
@SessionId private val sessionId: String,
private val cryptoService: CryptoService
) {
fun sendEvent(event: Event): Cancelable {
// Encrypted room handling
return if (cryptoService.isRoomEncrypted(event.roomId ?: "")
&& !event.isEncrypted() // In case of resend where it's already encrypted so skip to send
) {
Timber.v("## SendEvent: [${System.currentTimeMillis()}] Schedule encrypt and send event ${event.eventId}")
val encryptWork = createEncryptEventWork(event, true)
// Note that event will be replaced by the result of the previous work
val sendWork = createSendEventWork(event, false)
timelineSendEventWorkCommon.postSequentialWorks(event.roomId ?: "", encryptWork, sendWork)
} else {
Timber.v("## SendEvent: [${System.currentTimeMillis()}] Schedule send event ${event.eventId}")
val sendWork = createSendEventWork(event, true)
timelineSendEventWorkCommon.postWork(event.roomId ?: "", sendWork)
}
}
private fun createEncryptEventWork(event: Event, startChain: Boolean): OneTimeWorkRequest {
// Same parameter
val params = EncryptEventWorker.Params(sessionId, event.eventId!!)
val sendWorkData = WorkerParamsFactory.toData(params)
return workManagerProvider.matrixOneTimeWorkRequestBuilder<EncryptEventWorker>()
.setConstraints(WorkManagerProvider.workConstraints)
.setInputData(sendWorkData)
.startChain(startChain)
.setBackoffCriteria(BackoffPolicy.LINEAR, WorkManagerProvider.BACKOFF_DELAY, TimeUnit.MILLISECONDS)
.build()
}
private fun createSendEventWork(event: Event, startChain: Boolean): OneTimeWorkRequest {
val sendContentWorkerParams = SendEventWorker.Params(sessionId = sessionId, eventId = event.eventId!!)
val sendWorkData = WorkerParamsFactory.toData(sendContentWorkerParams)
return timelineSendEventWorkCommon.createWork<SendEventWorker>(sendWorkData, startChain)
}
}

View File

@ -22,12 +22,11 @@ import com.squareup.moshi.JsonClass
import io.realm.RealmConfiguration import io.realm.RealmConfiguration
import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.EventBus
import org.matrix.android.sdk.api.failure.shouldBeRetried import org.matrix.android.sdk.api.failure.shouldBeRetried
import org.matrix.android.sdk.api.session.events.model.Content import org.matrix.android.sdk.api.session.crypto.CryptoService
import org.matrix.android.sdk.api.session.room.send.SendState import org.matrix.android.sdk.api.session.room.send.SendState
import org.matrix.android.sdk.internal.crypto.tasks.SendEventTask
import org.matrix.android.sdk.internal.di.SessionDatabase import org.matrix.android.sdk.internal.di.SessionDatabase
import org.matrix.android.sdk.internal.network.executeRequest
import org.matrix.android.sdk.internal.session.SessionComponent import org.matrix.android.sdk.internal.session.SessionComponent
import org.matrix.android.sdk.internal.session.room.RoomAPI
import org.matrix.android.sdk.internal.worker.SessionSafeCoroutineWorker import org.matrix.android.sdk.internal.worker.SessionSafeCoroutineWorker
import org.matrix.android.sdk.internal.worker.SessionWorkerParams import org.matrix.android.sdk.internal.worker.SessionWorkerParams
import timber.log.Timber import timber.log.Timber
@ -47,11 +46,14 @@ internal class SendEventWorker(context: Context,
internal data class Params( internal data class Params(
override val sessionId: String, override val sessionId: String,
override val lastFailureMessage: String? = null, override val lastFailureMessage: String? = null,
val eventId: String val eventId: String,
// use this as an override if you want to send in clear in encrypted room
val isEncrypted: Boolean? = null
) : SessionWorkerParams ) : SessionWorkerParams
@Inject lateinit var localEchoRepository: LocalEchoRepository @Inject lateinit var localEchoRepository: LocalEchoRepository
@Inject lateinit var roomAPI: RoomAPI @Inject lateinit var sendEventTask: SendEventTask
@Inject lateinit var cryptoService: CryptoService
@Inject lateinit var eventBus: EventBus @Inject lateinit var eventBus: EventBus
@Inject lateinit var cancelSendTracker: CancelSendTracker @Inject lateinit var cancelSendTracker: CancelSendTracker
@SessionDatabase @Inject lateinit var realmConfiguration: RealmConfiguration @SessionDatabase @Inject lateinit var realmConfiguration: RealmConfiguration
@ -63,7 +65,7 @@ internal class SendEventWorker(context: Context,
override suspend fun doSafeWork(params: Params): Result { override suspend fun doSafeWork(params: Params): Result {
val event = localEchoRepository.getUpToDateEcho(params.eventId) val event = localEchoRepository.getUpToDateEcho(params.eventId)
if (event?.eventId == null || event.roomId == null) { if (event?.eventId == null || event.roomId == null) {
localEchoRepository.updateSendState(params.eventId, SendState.UNDELIVERED) localEchoRepository.updateSendState(params.eventId, event?.roomId, SendState.UNDELIVERED)
return Result.success() return Result.success()
.also { Timber.e("Work cancelled due to bad input data") } .also { Timber.e("Work cancelled due to bad input data") }
} }
@ -77,7 +79,7 @@ internal class SendEventWorker(context: Context,
} }
if (params.lastFailureMessage != null) { if (params.lastFailureMessage != null) {
localEchoRepository.updateSendState(event.eventId, SendState.UNDELIVERED) localEchoRepository.updateSendState(event.eventId, event.roomId, SendState.UNDELIVERED)
// Transmit the error // Transmit the error
return Result.success(inputData) return Result.success(inputData)
.also { Timber.e("Work cancelled due to input error from parent") } .also { Timber.e("Work cancelled due to input error from parent") }
@ -85,12 +87,12 @@ internal class SendEventWorker(context: Context,
Timber.v("## SendEvent: [${System.currentTimeMillis()}] Send event ${params.eventId}") Timber.v("## SendEvent: [${System.currentTimeMillis()}] Send event ${params.eventId}")
return try { return try {
sendEvent(event.eventId, event.roomId, event.type, event.content) sendEventTask.execute(SendEventTask.Params(event, params.isEncrypted ?: cryptoService.isRoomEncrypted(event.roomId)))
Result.success() Result.success()
} catch (exception: Throwable) { } catch (exception: Throwable) {
if (/*currentAttemptCount >= MAX_NUMBER_OF_RETRY_BEFORE_FAILING ||**/ !exception.shouldBeRetried()) { if (/*currentAttemptCount >= MAX_NUMBER_OF_RETRY_BEFORE_FAILING ||**/ !exception.shouldBeRetried()) {
Timber.e("## SendEvent: [${System.currentTimeMillis()}] Send event Failed cannot retry ${params.eventId} > ${exception.localizedMessage}") Timber.e("## SendEvent: [${System.currentTimeMillis()}] Send event Failed cannot retry ${params.eventId} > ${exception.localizedMessage}")
localEchoRepository.updateSendState(event.eventId, SendState.UNDELIVERED) localEchoRepository.updateSendState(event.eventId, event.roomId, SendState.UNDELIVERED)
return Result.success() return Result.success()
} else { } else {
Timber.e("## SendEvent: [${System.currentTimeMillis()}] Send event Failed schedule retry ${params.eventId} > ${exception.localizedMessage}") Timber.e("## SendEvent: [${System.currentTimeMillis()}] Send event Failed schedule retry ${params.eventId} > ${exception.localizedMessage}")
@ -102,12 +104,4 @@ internal class SendEventWorker(context: Context,
override fun buildErrorParams(params: Params, message: String): Params { override fun buildErrorParams(params: Params, message: String): Params {
return params.copy(lastFailureMessage = params.lastFailureMessage ?: message) return params.copy(lastFailureMessage = params.lastFailureMessage ?: message)
} }
private suspend fun sendEvent(eventId: String, roomId: String, type: String, content: Content?) {
localEchoRepository.updateSendState(eventId, SendState.SENDING)
executeRequest<SendResponse>(eventBus) {
apiCall = roomAPI.send(eventId, roomId, type, content)
}
localEchoRepository.updateSendState(eventId, SendState.SENT)
}
} }

View File

@ -0,0 +1,239 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.session.room.send.queue
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import org.matrix.android.sdk.api.auth.data.SessionParams
import org.matrix.android.sdk.api.auth.data.sessionId
import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.failure.Failure
import org.matrix.android.sdk.api.failure.MatrixError
import org.matrix.android.sdk.api.failure.isTokenError
import org.matrix.android.sdk.api.session.crypto.CryptoService
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.sync.SyncState
import org.matrix.android.sdk.api.util.Cancelable
import org.matrix.android.sdk.internal.session.SessionScope
import org.matrix.android.sdk.internal.task.TaskExecutor
import timber.log.Timber
import java.io.IOException
import java.net.InetAddress
import java.net.InetSocketAddress
import java.net.Socket
import java.util.Timer
import java.util.TimerTask
import java.util.concurrent.LinkedBlockingQueue
import javax.inject.Inject
import kotlin.concurrent.schedule
/**
* A simple ever running thread unique for that session responsible of sending events in order.
* Each send is retried 3 times, if there is no network (e.g if cannot ping home server) it will wait and
* periodically test reachability before resume (does not count as a retry)
*
* If the app is killed before all event were sent, on next wakeup the scheduled events will be re posted
*/
@SessionScope
internal class EventSenderProcessor @Inject constructor(
private val cryptoService: CryptoService,
private val sessionParams: SessionParams,
private val queuedTaskFactory: QueuedTaskFactory,
private val taskExecutor: TaskExecutor,
private val memento: QueueMemento
) : Thread("SENDER_THREAD_SID_${sessionParams.credentials.sessionId()}") {
private fun markAsManaged(task: QueuedTask) {
memento.track(task)
}
private fun markAsFinished(task: QueuedTask) {
memento.unTrack(task)
}
// API
fun postEvent(event: Event): Cancelable {
return postEvent(event, event.roomId?.let { cryptoService.isRoomEncrypted(it) } ?: false)
}
override fun start() {
super.start()
// We should check for sending events not handled because app was killed
// But we should be careful of only took those that was submitted to us, because if it's
// for example it's a media event it is handled by some worker and he will handle it
// This is a bit fragile :/
// also some events cannot be retried manually by users, e.g reactions
// they were previously relying on workers to do the work :/ and was expected to always finally succeed
// Also some echos are not to be resent like redaction echos (fake event created for aggregation)
tryOrNull {
taskExecutor.executorScope.launch {
Timber.d("## Send relaunched pending events on restart")
memento.restoreTasks(this@EventSenderProcessor)
}
}
}
fun postEvent(event: Event, encrypt: Boolean): Cancelable {
val task = queuedTaskFactory.createSendTask(event, encrypt)
return postTask(task)
}
fun postRedaction(redactionLocalEcho: Event, reason: String?): Cancelable {
return postRedaction(redactionLocalEcho.eventId!!, redactionLocalEcho.redacts!!, redactionLocalEcho.roomId!!, reason)
}
fun postRedaction(redactionLocalEchoId: String, eventToRedactId: String, roomId: String, reason: String?): Cancelable {
val task = queuedTaskFactory.createRedactTask(redactionLocalEchoId, eventToRedactId, roomId, reason)
return postTask(task)
}
fun postTask(task: QueuedTask): Cancelable {
// non blocking add to queue
sendingQueue.add(task)
markAsManaged(task)
return object : Cancelable {
override fun cancel() {
task.cancel()
}
}
}
companion object {
private const val RETRY_WAIT_TIME_MS = 10_000L
}
private var sendingQueue = LinkedBlockingQueue<QueuedTask>()
private var networkAvailableLock = Object()
private var canReachServer = true
private var retryNoNetworkTask: TimerTask? = null
override fun run() {
Timber.v("## SendThread started ts:${System.currentTimeMillis()}")
try {
while (!isInterrupted) {
Timber.v("## SendThread wait for task to process")
val task = sendingQueue.take()
Timber.v("## SendThread Found task to process $task")
if (task.isCancelled()) {
Timber.v("## SendThread send cancelled for $task")
// we do not execute this one
continue
}
// we check for network connectivity
while (!canReachServer) {
Timber.v("## SendThread cannot reach server, wait ts:${System.currentTimeMillis()}")
// schedule to retry
waitForNetwork()
// if thread as been killed meanwhile
// if (state == State.KILLING) break
}
Timber.v("## Server is Reachable")
// so network is available
runBlocking {
retryLoop@ while (task.retryCount < 3) {
try {
// SendPerformanceProfiler.startStage(task.event.eventId!!, SendPerformanceProfiler.Stages.SEND_WORKER)
Timber.v("## SendThread retryLoop for $task retryCount ${task.retryCount}")
task.execute()
// sendEventTask.execute(SendEventTask.Params(task.event, task.encrypt, cryptoService))
// SendPerformanceProfiler.stopStage(task.event.eventId, SendPerformanceProfiler.Stages.SEND_WORKER)
break@retryLoop
} catch (exception: Throwable) {
when {
exception is IOException || exception is Failure.NetworkConnection -> {
canReachServer = false
task.retryCount++
if (task.retryCount >= 3) task.onTaskFailed()
while (!canReachServer) {
Timber.v("## SendThread retryLoop cannot reach server, wait ts:${System.currentTimeMillis()}")
// schedule to retry
waitForNetwork()
}
}
(exception is Failure.ServerError && exception.error.code == MatrixError.M_LIMIT_EXCEEDED) -> {
task.retryCount++
if (task.retryCount >= 3) task.onTaskFailed()
Timber.v("## SendThread retryLoop retryable error for $task reason: ${exception.localizedMessage}")
// wait a bit
// Todo if its a quota exception can we get timout?
sleep(3_000)
continue@retryLoop
}
exception.isTokenError() -> {
Timber.v("## SendThread retryLoop retryable TOKEN error, interrupt")
// we can exit the loop
task.onTaskFailed()
throw InterruptedException()
}
else -> {
Timber.v("## SendThread retryLoop Un-Retryable error, try next task")
// this task is in error, check next one?
break@retryLoop
}
}
}
}
}
markAsFinished(task)
}
} catch (interruptionException: InterruptedException) {
// will be thrown is thread is interrupted while seeping
interrupt()
Timber.v("## InterruptedException!! ${interruptionException.localizedMessage}")
}
// state = State.KILLED
// is this needed?
retryNoNetworkTask?.cancel()
Timber.w("## SendThread finished ${System.currentTimeMillis()}")
}
private fun waitForNetwork() {
retryNoNetworkTask = Timer(SyncState.NoNetwork.toString(), false).schedule(RETRY_WAIT_TIME_MS) {
synchronized(networkAvailableLock) {
canReachServer = checkHostAvailable().also {
Timber.v("## SendThread checkHostAvailable $it")
}
networkAvailableLock.notify()
}
}
synchronized(networkAvailableLock) { networkAvailableLock.wait() }
}
/**
* Check if homeserver is reachable.
*/
private fun checkHostAvailable(): Boolean {
val host = sessionParams.homeServerConnectionConfig.homeServerUri.host ?: return false
val port = sessionParams.homeServerConnectionConfig.homeServerUri.port.takeIf { it != -1 } ?: 80
val timeout = 30_000
try {
Socket().use { socket ->
val inetAddress: InetAddress = InetAddress.getByName(host)
val inetSocketAddress = InetSocketAddress(inetAddress, port)
socket.connect(inetSocketAddress, timeout)
return true
}
} catch (e: IOException) {
Timber.v("## EventSender isHostAvailable failure ${e.localizedMessage}")
return false
}
}
}

View File

@ -0,0 +1,121 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.session.room.send.queue
import android.content.Context
import org.matrix.android.sdk.api.auth.data.sessionId
import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.session.crypto.CryptoService
import org.matrix.android.sdk.api.session.room.send.SendState
import org.matrix.android.sdk.internal.di.SessionId
import org.matrix.android.sdk.internal.session.room.send.LocalEchoRepository
import timber.log.Timber
import javax.inject.Inject
/**
* Simple lightweight persistence
* Don't want to go in DB due to current issues
* Will never manage lots of events, it simply uses sharedPreferences.
* It is just used to remember what events/localEchos was managed by the event sender in order to
* reschedule them (and only them) on next restart
*/
internal class QueueMemento @Inject constructor(context: Context,
@SessionId sessionId: String,
private val queuedTaskFactory: QueuedTaskFactory,
private val localEchoRepository: LocalEchoRepository,
private val cryptoService: CryptoService) {
private val storage = context.getSharedPreferences("QueueMemento_$sessionId", Context.MODE_PRIVATE)
private val managedTaskInfos = mutableListOf<QueuedTask>()
fun track(task: QueuedTask) {
synchronized(managedTaskInfos) {
managedTaskInfos.add(task)
persist()
}
}
fun unTrack(task: QueuedTask) {
managedTaskInfos.remove(task)
persist()
}
private fun persist() {
managedTaskInfos.mapIndexedNotNull { index, queuedTask ->
toTaskInfo(queuedTask, index)?.let { TaskInfo.map(it) }
}.toSet().let { set ->
storage.edit()
.putStringSet("ManagedBySender", set)
.apply()
}
}
private fun toTaskInfo(task: QueuedTask, order: Int): TaskInfo? {
synchronized(managedTaskInfos) {
return when (task) {
is SendEventQueuedTask -> SendEventTaskInfo(
localEchoId = task.event.eventId ?: "",
encrypt = task.encrypt,
order = order
)
is RedactQueuedTask -> RedactEventTaskInfo(
redactionLocalEcho = task.redactionLocalEchoId,
order = order
)
else -> null
}
}
}
suspend fun restoreTasks(eventProcessor: EventSenderProcessor) {
// events should be restarted in correct order
storage.getStringSet("ManagedBySender", null)?.let { pending ->
Timber.d("## Send - Recovering unsent events $pending")
pending.mapNotNull { tryOrNull { TaskInfo.map(it) } }
}
?.sortedBy { it.order }
?.forEach { info ->
try {
when (info) {
is SendEventTaskInfo -> {
localEchoRepository.getUpToDateEcho(info.localEchoId)?.let {
if (it.sendState.isSending() && it.eventId != null && it.roomId != null) {
localEchoRepository.updateSendState(it.eventId, it.roomId, SendState.UNSENT)
Timber.d("## Send -Reschedule send $info")
eventProcessor.postTask(queuedTaskFactory.createSendTask(it, info.encrypt ?: cryptoService.isRoomEncrypted(it.roomId)))
}
}
}
is RedactEventTaskInfo -> {
info.redactionLocalEcho?.let { localEchoRepository.getUpToDateEcho(it) }?.let {
localEchoRepository.updateSendState(it.eventId!!, it.roomId, SendState.UNSENT)
// try to get reason
val reason = it.content?.get("reason") as? String
if (it.redacts != null && it.roomId != null) {
Timber.d("## Send -Reschedule redact $info")
eventProcessor.postTask(queuedTaskFactory.createRedactTask(it.eventId, it.redacts, it.roomId, reason))
}
}
// postTask(queuedTaskFactory.createRedactTask(info.eventToRedactId, info.)
}
}
} catch (failure: Throwable) {
Timber.e("failed to restore task $info")
}
}
}
}

View File

@ -13,14 +13,17 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.matrix.android.sdk.internal.crypto.crosssigning
data class SessionToCryptoRoomMembersUpdate( package org.matrix.android.sdk.internal.session.room.send.queue
val roomId: String,
val isDirect: Boolean,
val userIds: List<String>
)
data class CryptoToSessionUserTrustChange( abstract class QueuedTask {
val userIds: List<String> var retryCount = 0
)
abstract suspend fun execute()
abstract fun onTaskFailed()
abstract fun isCancelled() : Boolean
abstract fun cancel()
}

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