mirror of
https://github.com/SchildiChat/SchildiChat-android.git
synced 2025-02-18 04:50:08 +03:00
Merge branch 'develop' into refresh-summary
This commit is contained in:
commit
634c8947bd
397 changed files with 17130 additions and 3386 deletions
|
@ -23,10 +23,10 @@ android:
|
||||||
- platform-tools
|
- platform-tools
|
||||||
|
|
||||||
# The BuildTools version used by your project
|
# The BuildTools version used by your project
|
||||||
- build-tools-28.0.3
|
- build-tools-29.0.3
|
||||||
|
|
||||||
# The SDK version used to compile your project
|
# The SDK version used to compile your project
|
||||||
- android-28
|
- android-29
|
||||||
|
|
||||||
before_cache:
|
before_cache:
|
||||||
- rm -f $HOME/.gradle/caches/modules-2/modules-2.lock
|
- rm -f $HOME/.gradle/caches/modules-2/modules-2.lock
|
||||||
|
|
46
CHANGES.md
46
CHANGES.md
|
@ -2,26 +2,60 @@ Changes in RiotX 0.19.0 (2020-XX-XX)
|
||||||
===================================================
|
===================================================
|
||||||
|
|
||||||
Features ✨:
|
Features ✨:
|
||||||
-
|
- Cross-Signing | Support SSSS secret sharing (#944)
|
||||||
|
- Cross-Signing | Verify new session from existing session (#1134)
|
||||||
|
- Cross-Signing | Bootstraping cross signing with 4S from mobile (#985)
|
||||||
|
|
||||||
|
|
||||||
Improvements 🙌:
|
Improvements 🙌:
|
||||||
-
|
- Verification DM / Handle concurrent .start after .ready (#794)
|
||||||
|
- Reimplementation of multiple attachment picker
|
||||||
|
- Cross-Signing | Update Shield Logic for DM (#963)
|
||||||
|
- Cross-Signing | Complete security new session design update (#1135)
|
||||||
|
- Cross-Signing | Setup key backup as part of SSSS bootstrapping (#1201)
|
||||||
|
- Cross-Signing | Gossip key backup recovery key (#1200)
|
||||||
|
- Show room encryption status as a bubble tile (#1078)
|
||||||
|
- UX/UI | Add indicator to home tab on invite (#957)
|
||||||
|
- Cross-Signing | Restore history after recover from passphrase (#1214)
|
||||||
|
- Cross-Sign | QR code scan confirmation screens design update (#1187)
|
||||||
|
- Emoji Verification | It's not the same butterfly! (#1220)
|
||||||
|
- Cross-Signing | Composer decoration: shields (#1077)
|
||||||
|
|
||||||
Bugfix 🐛:
|
Bugfix 🐛:
|
||||||
- Message transitions in encrypted rooms are jarring #518
|
|
||||||
- Fix summary notification staying after "mark as read"
|
- Fix summary notification staying after "mark as read"
|
||||||
|
- Missing avatar/displayname after verification request message (#841)
|
||||||
|
- Crypto | RiotX sometimes rotate the current device keys (#1170)
|
||||||
|
- RiotX can't restore cross signing keys saved by web in SSSS (#1174)
|
||||||
|
- Cross- Signing | After signin in new session, verification paper trail in DM is off (#1191)
|
||||||
|
- Failed to encrypt message in room (message stays in red), [thanks to pwr22] (#925)
|
||||||
|
- Cross-Signing | web <-> riotX After QR code scan, gossiping fails (#1210)
|
||||||
|
- Fix crash when trying to download file without internet connection (#1229)
|
||||||
|
- Local echo are not updated in timeline (for failed & encrypted states)
|
||||||
|
|
||||||
Translations 🗣:
|
Translations 🗣:
|
||||||
-
|
-
|
||||||
|
|
||||||
SDK API changes ⚠️:
|
SDK API changes ⚠️:
|
||||||
-
|
- Increase targetSdkVersion to 29
|
||||||
|
|
||||||
Build 🧱:
|
Build 🧱:
|
||||||
-
|
- Compile with Android SDK 29 (Android Q)
|
||||||
|
|
||||||
Other changes:
|
Other changes:
|
||||||
-
|
- Increase File Logger capacities ( + use dev log preferences)
|
||||||
|
|
||||||
|
Changes in RiotX 0.18.1 (2020-03-17)
|
||||||
|
===================================================
|
||||||
|
|
||||||
|
Improvements 🙌:
|
||||||
|
- Implementation of /join command
|
||||||
|
|
||||||
|
Bugfix 🐛:
|
||||||
|
- Message transitions in encrypted rooms are jarring #518
|
||||||
|
- Images that failed to send are waiting to be sent forever #1145
|
||||||
|
- Fix / Crashed when trying to send a gif from the Gboard #1136
|
||||||
|
- Fix / Cannot click on key backup banner when new keys are available
|
||||||
|
|
||||||
|
|
||||||
Changes in RiotX 0.18.0 (2020-03-11)
|
Changes in RiotX 0.18.0 (2020-03-11)
|
||||||
===================================================
|
===================================================
|
||||||
|
|
|
@ -3,11 +3,11 @@ apply plugin: 'kotlin-android'
|
||||||
apply plugin: 'kotlin-kapt'
|
apply plugin: 'kotlin-kapt'
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion 28
|
compileSdkVersion 29
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
minSdkVersion 16
|
minSdkVersion 16
|
||||||
targetSdkVersion 28
|
targetSdkVersion 29
|
||||||
versionCode 1
|
versionCode 1
|
||||||
versionName "1.0"
|
versionName "1.0"
|
||||||
|
|
||||||
|
|
|
@ -19,12 +19,12 @@ androidExtensions {
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion 28
|
compileSdkVersion 29
|
||||||
testOptions.unitTests.includeAndroidResources = true
|
testOptions.unitTests.includeAndroidResources = true
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
minSdkVersion 16
|
minSdkVersion 16
|
||||||
targetSdkVersion 28
|
targetSdkVersion 29
|
||||||
versionCode 1
|
versionCode 1
|
||||||
versionName "0.0.1"
|
versionName "0.0.1"
|
||||||
// Multidex is useful for tests
|
// Multidex is useful for tests
|
||||||
|
@ -97,6 +97,7 @@ dependencies {
|
||||||
def coroutines_version = "1.3.2"
|
def coroutines_version = "1.3.2"
|
||||||
def markwon_version = '3.1.0'
|
def markwon_version = '3.1.0'
|
||||||
def daggerVersion = '2.25.4'
|
def daggerVersion = '2.25.4'
|
||||||
|
def work_version = '2.3.3'
|
||||||
|
|
||||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
||||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
|
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
|
||||||
|
@ -118,7 +119,7 @@ dependencies {
|
||||||
implementation "ru.noties.markwon:core:$markwon_version"
|
implementation "ru.noties.markwon:core:$markwon_version"
|
||||||
|
|
||||||
// Image
|
// Image
|
||||||
implementation 'androidx.exifinterface:exifinterface:1.1.0'
|
implementation 'androidx.exifinterface:exifinterface:1.3.0-alpha01'
|
||||||
implementation 'id.zelory:compressor:3.0.0'
|
implementation 'id.zelory:compressor:3.0.0'
|
||||||
|
|
||||||
// Database
|
// Database
|
||||||
|
@ -126,7 +127,7 @@ dependencies {
|
||||||
kapt 'dk.ilios:realmfieldnameshelper:1.1.1'
|
kapt 'dk.ilios:realmfieldnameshelper:1.1.1'
|
||||||
|
|
||||||
// Work
|
// Work
|
||||||
implementation "androidx.work:work-runtime-ktx:2.3.3"
|
implementation "androidx.work:work-runtime-ktx:$work_version"
|
||||||
|
|
||||||
// FP
|
// FP
|
||||||
implementation "io.arrow-kt:arrow-core:$arrow_version"
|
implementation "io.arrow-kt:arrow-core:$arrow_version"
|
||||||
|
|
|
@ -38,6 +38,7 @@ import im.vector.matrix.android.api.session.room.timeline.TimelineSettings
|
||||||
import im.vector.matrix.android.api.session.sync.SyncState
|
import im.vector.matrix.android.api.session.sync.SyncState
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.GlobalScope
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import org.junit.Assert.assertEquals
|
import org.junit.Assert.assertEquals
|
||||||
|
@ -265,8 +266,26 @@ class CommonTestHelper(context: Context) {
|
||||||
* @param latch
|
* @param latch
|
||||||
* @throws InterruptedException
|
* @throws InterruptedException
|
||||||
*/
|
*/
|
||||||
fun await(latch: CountDownLatch) {
|
fun await(latch: CountDownLatch, timout: Long? = TestConstants.timeOutMillis) {
|
||||||
assertTrue(latch.await(TestConstants.timeOutMillis, TimeUnit.MILLISECONDS))
|
assertTrue(latch.await(timout ?: TestConstants.timeOutMillis, TimeUnit.MILLISECONDS))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun retryPeriodicallyWithLatch(latch: CountDownLatch, condition: (() -> Boolean)) {
|
||||||
|
GlobalScope.launch {
|
||||||
|
while (true) {
|
||||||
|
delay(1000)
|
||||||
|
if (condition()) {
|
||||||
|
latch.countDown()
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun waitWithLatch(timout: Long? = TestConstants.timeOutMillis, block: (CountDownLatch) -> Unit) {
|
||||||
|
val latch = CountDownLatch(1)
|
||||||
|
block(latch)
|
||||||
|
await(latch, timout)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Transform a method with a MatrixCallback to a synchronous method
|
// Transform a method with a MatrixCallback to a synchronous method
|
||||||
|
|
|
@ -22,8 +22,8 @@ object TestConstants {
|
||||||
|
|
||||||
const val TESTS_HOME_SERVER_URL = "http://10.0.2.2:8080"
|
const val TESTS_HOME_SERVER_URL = "http://10.0.2.2:8080"
|
||||||
|
|
||||||
// Time out to use when waiting for server response. 10s
|
// Time out to use when waiting for server response. 20s
|
||||||
private const val AWAIT_TIME_OUT_MILLIS = 10_000
|
private const val AWAIT_TIME_OUT_MILLIS = 20_000
|
||||||
|
|
||||||
// Time out to use when waiting for server response, when the debugger is connected. 10 minutes
|
// Time out to use when waiting for server response, when the debugger is connected. 10 minutes
|
||||||
private const val AWAIT_TIME_OUT_WITH_DEBUGGER_MILLIS = 10 * 60_000
|
private const val AWAIT_TIME_OUT_WITH_DEBUGGER_MILLIS = 10 * 60_000
|
||||||
|
|
|
@ -0,0 +1,289 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2020 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.matrix.android.internal.crypto.gossiping
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
import im.vector.matrix.android.InstrumentedTest
|
||||||
|
import im.vector.matrix.android.api.session.crypto.verification.IncomingSasVerificationTransaction
|
||||||
|
import im.vector.matrix.android.api.session.crypto.verification.SasVerificationTransaction
|
||||||
|
import im.vector.matrix.android.api.session.crypto.verification.VerificationMethod
|
||||||
|
import im.vector.matrix.android.api.session.crypto.verification.VerificationService
|
||||||
|
import im.vector.matrix.android.api.session.crypto.verification.VerificationTransaction
|
||||||
|
import im.vector.matrix.android.api.session.crypto.verification.VerificationTxState
|
||||||
|
import im.vector.matrix.android.api.session.events.model.toModel
|
||||||
|
import im.vector.matrix.android.api.session.room.model.RoomDirectoryVisibility
|
||||||
|
import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams
|
||||||
|
import im.vector.matrix.android.common.CommonTestHelper
|
||||||
|
import im.vector.matrix.android.common.SessionTestParams
|
||||||
|
import im.vector.matrix.android.common.TestConstants
|
||||||
|
import im.vector.matrix.android.internal.crypto.GossipingRequestState
|
||||||
|
import im.vector.matrix.android.internal.crypto.OutgoingGossipingRequestState
|
||||||
|
import im.vector.matrix.android.internal.crypto.crosssigning.DeviceTrustLevel
|
||||||
|
import im.vector.matrix.android.internal.crypto.keysbackup.model.MegolmBackupCreationInfo
|
||||||
|
import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.KeysVersion
|
||||||
|
import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
|
||||||
|
import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap
|
||||||
|
import im.vector.matrix.android.internal.crypto.model.event.EncryptedEventContent
|
||||||
|
import im.vector.matrix.android.internal.crypto.model.rest.UserPasswordAuth
|
||||||
|
import junit.framework.TestCase.assertEquals
|
||||||
|
import junit.framework.TestCase.assertNotNull
|
||||||
|
import junit.framework.TestCase.assertTrue
|
||||||
|
import junit.framework.TestCase.fail
|
||||||
|
import org.junit.Assert
|
||||||
|
import org.junit.FixMethodOrder
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
import org.junit.runners.MethodSorters
|
||||||
|
import java.util.concurrent.CountDownLatch
|
||||||
|
|
||||||
|
@RunWith(AndroidJUnit4::class)
|
||||||
|
@FixMethodOrder(MethodSorters.JVM)
|
||||||
|
class KeyShareTests : InstrumentedTest {
|
||||||
|
|
||||||
|
private val mTestHelper = CommonTestHelper(context())
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun test_DoNotSelfShareIfNotTrusted() {
|
||||||
|
val aliceSession = mTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true))
|
||||||
|
|
||||||
|
// Create an encrypted room and add a message
|
||||||
|
val roomId = mTestHelper.doSync<String> {
|
||||||
|
aliceSession.createRoom(
|
||||||
|
CreateRoomParams(RoomDirectoryVisibility.PRIVATE).enableEncryptionWithAlgorithm(true),
|
||||||
|
it
|
||||||
|
)
|
||||||
|
}
|
||||||
|
val room = aliceSession.getRoom(roomId)
|
||||||
|
assertNotNull(room)
|
||||||
|
Thread.sleep(4_000)
|
||||||
|
assertTrue(room?.isEncrypted() == true)
|
||||||
|
val sentEventId = mTestHelper.sendTextMessage(room!!, "My Message", 1).first().eventId
|
||||||
|
|
||||||
|
// Open a new sessionx
|
||||||
|
|
||||||
|
val aliceSession2 = mTestHelper.logIntoAccount(aliceSession.myUserId, SessionTestParams(true))
|
||||||
|
|
||||||
|
val roomSecondSessionPOV = aliceSession2.getRoom(roomId)
|
||||||
|
|
||||||
|
val receivedEvent = roomSecondSessionPOV?.getTimeLineEvent(sentEventId)
|
||||||
|
assertNotNull(receivedEvent)
|
||||||
|
assert(receivedEvent!!.isEncrypted())
|
||||||
|
|
||||||
|
try {
|
||||||
|
aliceSession2.cryptoService().decryptEvent(receivedEvent.root, "foo")
|
||||||
|
fail("should fail")
|
||||||
|
} catch (failure: Throwable) {
|
||||||
|
}
|
||||||
|
|
||||||
|
val outgoingRequestBefore = aliceSession2.cryptoService().getOutgoingRoomKeyRequest()
|
||||||
|
// Try to request
|
||||||
|
aliceSession2.cryptoService().requestRoomKeyForEvent(receivedEvent.root)
|
||||||
|
|
||||||
|
val waitLatch = CountDownLatch(1)
|
||||||
|
val eventMegolmSessionId = receivedEvent.root.content.toModel<EncryptedEventContent>()?.sessionId
|
||||||
|
|
||||||
|
var outGoingRequestId: String? = null
|
||||||
|
|
||||||
|
mTestHelper.retryPeriodicallyWithLatch(waitLatch) {
|
||||||
|
aliceSession2.cryptoService().getOutgoingRoomKeyRequest()
|
||||||
|
.filter { req ->
|
||||||
|
// filter out request that was known before
|
||||||
|
!outgoingRequestBefore.any { req.requestId == it.requestId }
|
||||||
|
}
|
||||||
|
.let {
|
||||||
|
val outgoing = it.firstOrNull { it.sessionId == eventMegolmSessionId }
|
||||||
|
outGoingRequestId = outgoing?.requestId
|
||||||
|
outgoing != null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mTestHelper.await(waitLatch)
|
||||||
|
|
||||||
|
Log.v("TEST", "=======> Outgoing requet Id is $outGoingRequestId")
|
||||||
|
|
||||||
|
val outgoingRequestAfter = aliceSession2.cryptoService().getOutgoingRoomKeyRequest()
|
||||||
|
|
||||||
|
// We should have a new request
|
||||||
|
Assert.assertTrue(outgoingRequestAfter.size > outgoingRequestBefore.size)
|
||||||
|
Assert.assertNotNull(outgoingRequestAfter.first { it.sessionId == eventMegolmSessionId })
|
||||||
|
|
||||||
|
// The first session should see an incoming request
|
||||||
|
// the request should be refused, because the device is not trusted
|
||||||
|
mTestHelper.waitWithLatch { latch ->
|
||||||
|
mTestHelper.retryPeriodicallyWithLatch(latch) {
|
||||||
|
// DEBUG LOGS
|
||||||
|
aliceSession.cryptoService().getIncomingRoomKeyRequest().let {
|
||||||
|
Log.v("TEST", "Incoming request Session 1 (looking for $outGoingRequestId)")
|
||||||
|
Log.v("TEST", "=========================")
|
||||||
|
it.forEach { keyRequest ->
|
||||||
|
Log.v("TEST", "[ts${keyRequest.localCreationTimestamp}] requestId ${keyRequest.requestId}, for sessionId ${keyRequest.requestBody?.sessionId} is ${keyRequest.state}")
|
||||||
|
}
|
||||||
|
Log.v("TEST", "=========================")
|
||||||
|
}
|
||||||
|
|
||||||
|
val incoming = aliceSession.cryptoService().getIncomingRoomKeyRequest().firstOrNull { it.requestId == outGoingRequestId }
|
||||||
|
incoming?.state == GossipingRequestState.REJECTED
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
aliceSession2.cryptoService().decryptEvent(receivedEvent.root, "foo")
|
||||||
|
fail("should fail")
|
||||||
|
} catch (failure: Throwable) {
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mark the device as trusted
|
||||||
|
aliceSession.cryptoService().setDeviceVerification(DeviceTrustLevel(crossSigningVerified = false, locallyVerified = true), aliceSession.myUserId,
|
||||||
|
aliceSession2.sessionParams.credentials.deviceId ?: "")
|
||||||
|
|
||||||
|
// Re request
|
||||||
|
aliceSession2.cryptoService().reRequestRoomKeyForEvent(receivedEvent.root)
|
||||||
|
|
||||||
|
mTestHelper.waitWithLatch { latch ->
|
||||||
|
mTestHelper.retryPeriodicallyWithLatch(latch) {
|
||||||
|
aliceSession.cryptoService().getIncomingRoomKeyRequest().let {
|
||||||
|
Log.v("TEST", "Incoming request Session 1")
|
||||||
|
Log.v("TEST", "=========================")
|
||||||
|
it.forEach {
|
||||||
|
Log.v("TEST", "requestId ${it.requestId}, for sessionId ${it.requestBody?.sessionId} is ${it.state}")
|
||||||
|
}
|
||||||
|
Log.v("TEST", "=========================")
|
||||||
|
|
||||||
|
it.any { it.requestBody?.sessionId == eventMegolmSessionId && it.state == GossipingRequestState.ACCEPTED }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Thread.sleep(6_000)
|
||||||
|
mTestHelper.waitWithLatch { latch ->
|
||||||
|
mTestHelper.retryPeriodicallyWithLatch(latch) {
|
||||||
|
aliceSession2.cryptoService().getOutgoingRoomKeyRequest().let {
|
||||||
|
it.any { it.requestBody?.sessionId == eventMegolmSessionId && it.state == OutgoingGossipingRequestState.CANCELLED }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
aliceSession2.cryptoService().decryptEvent(receivedEvent.root, "foo")
|
||||||
|
} catch (failure: Throwable) {
|
||||||
|
fail("should have been able to decrypt")
|
||||||
|
}
|
||||||
|
|
||||||
|
mTestHelper.signOutAndClose(aliceSession)
|
||||||
|
mTestHelper.signOutAndClose(aliceSession2)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun test_ShareSSSSSecret() {
|
||||||
|
val aliceSession1 = mTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true))
|
||||||
|
|
||||||
|
mTestHelper.doSync<Unit> {
|
||||||
|
aliceSession1.cryptoService().crossSigningService()
|
||||||
|
.initializeCrossSigning(UserPasswordAuth(
|
||||||
|
user = aliceSession1.myUserId,
|
||||||
|
password = TestConstants.PASSWORD
|
||||||
|
), it)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also bootstrap keybackup on first session
|
||||||
|
val creationInfo = mTestHelper.doSync<MegolmBackupCreationInfo> {
|
||||||
|
aliceSession1.cryptoService().keysBackupService().prepareKeysBackupVersion(null, null, it)
|
||||||
|
}
|
||||||
|
val version = mTestHelper.doSync<KeysVersion> {
|
||||||
|
aliceSession1.cryptoService().keysBackupService().createKeysBackupVersion(creationInfo, it)
|
||||||
|
}
|
||||||
|
// Save it for gossiping
|
||||||
|
aliceSession1.cryptoService().keysBackupService().saveBackupRecoveryKey(creationInfo.recoveryKey, version = version.version)
|
||||||
|
|
||||||
|
val aliceSession2 = mTestHelper.logIntoAccount(aliceSession1.myUserId, SessionTestParams(true))
|
||||||
|
|
||||||
|
val aliceVerificationService1 = aliceSession1.cryptoService().verificationService()
|
||||||
|
val aliceVerificationService2 = aliceSession2.cryptoService().verificationService()
|
||||||
|
|
||||||
|
// force keys download
|
||||||
|
mTestHelper.doSync<MXUsersDevicesMap<CryptoDeviceInfo>> {
|
||||||
|
aliceSession1.cryptoService().downloadKeys(listOf(aliceSession1.myUserId), true, it)
|
||||||
|
}
|
||||||
|
mTestHelper.doSync<MXUsersDevicesMap<CryptoDeviceInfo>> {
|
||||||
|
aliceSession2.cryptoService().downloadKeys(listOf(aliceSession2.myUserId), true, it)
|
||||||
|
}
|
||||||
|
|
||||||
|
var session1ShortCode: String? = null
|
||||||
|
var session2ShortCode: String? = null
|
||||||
|
|
||||||
|
aliceVerificationService1.addListener(object : VerificationService.Listener {
|
||||||
|
override fun transactionUpdated(tx: VerificationTransaction) {
|
||||||
|
Log.d("#TEST", "AA: tx incoming?:${tx.isIncoming} state ${tx.state}")
|
||||||
|
if (tx is SasVerificationTransaction) {
|
||||||
|
if (tx.state == VerificationTxState.OnStarted) {
|
||||||
|
(tx as IncomingSasVerificationTransaction).performAccept()
|
||||||
|
}
|
||||||
|
if (tx.state == VerificationTxState.ShortCodeReady) {
|
||||||
|
session1ShortCode = tx.getDecimalCodeRepresentation()
|
||||||
|
tx.userHasVerifiedShortCode()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
aliceVerificationService2.addListener(object : VerificationService.Listener {
|
||||||
|
override fun transactionUpdated(tx: VerificationTransaction) {
|
||||||
|
Log.d("#TEST", "BB: tx incoming?:${tx.isIncoming} state ${tx.state}")
|
||||||
|
if (tx is SasVerificationTransaction) {
|
||||||
|
if (tx.state == VerificationTxState.ShortCodeReady) {
|
||||||
|
session2ShortCode = tx.getDecimalCodeRepresentation()
|
||||||
|
tx.userHasVerifiedShortCode()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
val txId: String = "m.testVerif12"
|
||||||
|
aliceVerificationService2.beginKeyVerification(VerificationMethod.SAS, aliceSession1.myUserId, aliceSession1.sessionParams.credentials.deviceId
|
||||||
|
?: "", txId)
|
||||||
|
|
||||||
|
mTestHelper.waitWithLatch { latch ->
|
||||||
|
mTestHelper.retryPeriodicallyWithLatch(latch) {
|
||||||
|
aliceSession1.cryptoService().getDeviceInfo(aliceSession1.myUserId, aliceSession2.sessionParams.credentials.deviceId ?: "")?.isVerified == true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assertNotNull(session1ShortCode)
|
||||||
|
Log.d("#TEST", "session1ShortCode: $session1ShortCode")
|
||||||
|
assertNotNull(session2ShortCode)
|
||||||
|
Log.d("#TEST", "session2ShortCode: $session2ShortCode")
|
||||||
|
assertEquals(session1ShortCode, session2ShortCode)
|
||||||
|
|
||||||
|
// SSK and USK private keys should have been shared
|
||||||
|
|
||||||
|
mTestHelper.waitWithLatch(60_000) { latch ->
|
||||||
|
mTestHelper.retryPeriodicallyWithLatch(latch) {
|
||||||
|
Log.d("#TEST", "CAN XS :${aliceSession2.cryptoService().crossSigningService().getMyCrossSigningKeys()}")
|
||||||
|
aliceSession2.cryptoService().crossSigningService().canCrossSign()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test that key backup key has been shared to
|
||||||
|
mTestHelper.waitWithLatch(60_000) { latch ->
|
||||||
|
val keysBackupService = aliceSession2.cryptoService().keysBackupService()
|
||||||
|
mTestHelper.retryPeriodicallyWithLatch(latch) {
|
||||||
|
Log.d("#TEST", "Recovery :${ keysBackupService.getKeyBackupRecoveryKeyInfo()?.recoveryKey}")
|
||||||
|
keysBackupService.getKeyBackupRecoveryKeyInfo()?.recoveryKey == creationInfo.recoveryKey
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -34,7 +34,6 @@ import im.vector.matrix.android.common.assertDictEquals
|
||||||
import im.vector.matrix.android.common.assertListEquals
|
import im.vector.matrix.android.common.assertListEquals
|
||||||
import im.vector.matrix.android.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM_BACKUP
|
import im.vector.matrix.android.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM_BACKUP
|
||||||
import im.vector.matrix.android.internal.crypto.MegolmSessionData
|
import im.vector.matrix.android.internal.crypto.MegolmSessionData
|
||||||
import im.vector.matrix.android.internal.crypto.OutgoingRoomKeyRequest
|
|
||||||
import im.vector.matrix.android.internal.crypto.crosssigning.DeviceTrustLevel
|
import im.vector.matrix.android.internal.crypto.crosssigning.DeviceTrustLevel
|
||||||
import im.vector.matrix.android.internal.crypto.keysbackup.model.KeysBackupVersionTrust
|
import im.vector.matrix.android.internal.crypto.keysbackup.model.KeysBackupVersionTrust
|
||||||
import im.vector.matrix.android.internal.crypto.keysbackup.model.MegolmBackupCreationInfo
|
import im.vector.matrix.android.internal.crypto.keysbackup.model.MegolmBackupCreationInfo
|
||||||
|
@ -326,46 +325,46 @@ class KeysBackupTest : InstrumentedTest {
|
||||||
* - Restore must be successful
|
* - Restore must be successful
|
||||||
* - *** There must be no more pending key share requests
|
* - *** There must be no more pending key share requests
|
||||||
*/
|
*/
|
||||||
@Test
|
// @Test
|
||||||
fun restoreKeysBackupAndKeyShareRequestTest() {
|
// fun restoreKeysBackupAndKeyShareRequestTest() {
|
||||||
fail("Check with Valere for this test. I think we do not send key share request")
|
// fail("Check with Valere for this test. I think we do not send key share request")
|
||||||
|
//
|
||||||
val testData = createKeysBackupScenarioWithPassword(null)
|
// val testData = createKeysBackupScenarioWithPassword(null)
|
||||||
|
//
|
||||||
// - Check the SDK sent key share requests
|
// // - Check the SDK sent key share requests
|
||||||
val cryptoStore2 = (testData.aliceSession2.cryptoService().keysBackupService() as DefaultKeysBackupService).store
|
// val cryptoStore2 = (testData.aliceSession2.cryptoService().keysBackupService() as DefaultKeysBackupService).store
|
||||||
val unsentRequest = cryptoStore2
|
// val unsentRequest = cryptoStore2
|
||||||
.getOutgoingRoomKeyRequestByState(setOf(OutgoingRoomKeyRequest.RequestState.UNSENT))
|
// .getOutgoingRoomKeyRequestByState(setOf(ShareRequestState.UNSENT))
|
||||||
val sentRequest = cryptoStore2
|
// val sentRequest = cryptoStore2
|
||||||
.getOutgoingRoomKeyRequestByState(setOf(OutgoingRoomKeyRequest.RequestState.SENT))
|
// .getOutgoingRoomKeyRequestByState(setOf(ShareRequestState.SENT))
|
||||||
|
//
|
||||||
// Request is either sent or unsent
|
// // Request is either sent or unsent
|
||||||
assertTrue(unsentRequest != null || sentRequest != null)
|
// assertTrue(unsentRequest != null || sentRequest != null)
|
||||||
|
//
|
||||||
// - Restore the e2e backup from the homeserver
|
// // - Restore the e2e backup from the homeserver
|
||||||
val importRoomKeysResult = mTestHelper.doSync<ImportRoomKeysResult> {
|
// val importRoomKeysResult = mTestHelper.doSync<ImportRoomKeysResult> {
|
||||||
testData.aliceSession2.cryptoService().keysBackupService().restoreKeysWithRecoveryKey(testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion!!,
|
// testData.aliceSession2.cryptoService().keysBackupService().restoreKeysWithRecoveryKey(testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion!!,
|
||||||
testData.prepareKeysBackupDataResult.megolmBackupCreationInfo.recoveryKey,
|
// testData.prepareKeysBackupDataResult.megolmBackupCreationInfo.recoveryKey,
|
||||||
null,
|
// null,
|
||||||
null,
|
// null,
|
||||||
null,
|
// null,
|
||||||
it
|
// it
|
||||||
)
|
// )
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
checkRestoreSuccess(testData, importRoomKeysResult.totalNumberOfKeys, importRoomKeysResult.successfullyNumberOfImportedKeys)
|
// checkRestoreSuccess(testData, importRoomKeysResult.totalNumberOfKeys, importRoomKeysResult.successfullyNumberOfImportedKeys)
|
||||||
|
//
|
||||||
// - There must be no more pending key share requests
|
// // - There must be no more pending key share requests
|
||||||
val unsentRequestAfterRestoration = cryptoStore2
|
// val unsentRequestAfterRestoration = cryptoStore2
|
||||||
.getOutgoingRoomKeyRequestByState(setOf(OutgoingRoomKeyRequest.RequestState.UNSENT))
|
// .getOutgoingRoomKeyRequestByState(setOf(ShareRequestState.UNSENT))
|
||||||
val sentRequestAfterRestoration = cryptoStore2
|
// val sentRequestAfterRestoration = cryptoStore2
|
||||||
.getOutgoingRoomKeyRequestByState(setOf(OutgoingRoomKeyRequest.RequestState.SENT))
|
// .getOutgoingRoomKeyRequestByState(setOf(ShareRequestState.SENT))
|
||||||
|
//
|
||||||
// Request is either sent or unsent
|
// // Request is either sent or unsent
|
||||||
assertTrue(unsentRequestAfterRestoration == null && sentRequestAfterRestoration == null)
|
// assertTrue(unsentRequestAfterRestoration == null && sentRequestAfterRestoration == null)
|
||||||
|
//
|
||||||
testData.cleanUp(mTestHelper)
|
// testData.cleanUp(mTestHelper)
|
||||||
}
|
// }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* - Do an e2e backup to the homeserver with a recovery key
|
* - Do an e2e backup to the homeserver with a recovery key
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
|
|
||||||
package im.vector.matrix.android.internal.crypto.verification
|
package im.vector.matrix.android.internal.crypto.verification
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
import im.vector.matrix.android.InstrumentedTest
|
import im.vector.matrix.android.InstrumentedTest
|
||||||
import im.vector.matrix.android.api.session.Session
|
import im.vector.matrix.android.api.session.Session
|
||||||
|
@ -23,6 +24,7 @@ import im.vector.matrix.android.api.session.crypto.verification.CancelCode
|
||||||
import im.vector.matrix.android.api.session.crypto.verification.IncomingSasVerificationTransaction
|
import im.vector.matrix.android.api.session.crypto.verification.IncomingSasVerificationTransaction
|
||||||
import im.vector.matrix.android.api.session.crypto.verification.OutgoingSasVerificationTransaction
|
import im.vector.matrix.android.api.session.crypto.verification.OutgoingSasVerificationTransaction
|
||||||
import im.vector.matrix.android.api.session.crypto.verification.SasMode
|
import im.vector.matrix.android.api.session.crypto.verification.SasMode
|
||||||
|
import im.vector.matrix.android.api.session.crypto.verification.SasVerificationTransaction
|
||||||
import im.vector.matrix.android.api.session.crypto.verification.VerificationMethod
|
import im.vector.matrix.android.api.session.crypto.verification.VerificationMethod
|
||||||
import im.vector.matrix.android.api.session.crypto.verification.VerificationService
|
import im.vector.matrix.android.api.session.crypto.verification.VerificationService
|
||||||
import im.vector.matrix.android.api.session.crypto.verification.VerificationTransaction
|
import im.vector.matrix.android.api.session.crypto.verification.VerificationTransaction
|
||||||
|
@ -355,6 +357,7 @@ class SASTest : InstrumentedTest {
|
||||||
val aliceAcceptedLatch = CountDownLatch(1)
|
val aliceAcceptedLatch = CountDownLatch(1)
|
||||||
val aliceListener = object : VerificationService.Listener {
|
val aliceListener = object : VerificationService.Listener {
|
||||||
override fun transactionUpdated(tx: VerificationTransaction) {
|
override fun transactionUpdated(tx: VerificationTransaction) {
|
||||||
|
Log.v("TEST", "== aliceTx state ${tx.state} => ${(tx as? OutgoingSasVerificationTransaction)?.uxState}")
|
||||||
if ((tx as SASDefaultVerificationTransaction).state === VerificationTxState.OnAccepted) {
|
if ((tx as SASDefaultVerificationTransaction).state === VerificationTxState.OnAccepted) {
|
||||||
val at = tx as SASDefaultVerificationTransaction
|
val at = tx as SASDefaultVerificationTransaction
|
||||||
accepted = at.accepted
|
accepted = at.accepted
|
||||||
|
@ -367,7 +370,9 @@ class SASTest : InstrumentedTest {
|
||||||
|
|
||||||
val bobListener = object : VerificationService.Listener {
|
val bobListener = object : VerificationService.Listener {
|
||||||
override fun transactionUpdated(tx: VerificationTransaction) {
|
override fun transactionUpdated(tx: VerificationTransaction) {
|
||||||
|
Log.v("TEST", "== bobTx state ${tx.state} => ${(tx as? IncomingSasVerificationTransaction)?.uxState}")
|
||||||
if ((tx as IncomingSasVerificationTransaction).uxState === IncomingSasVerificationTransaction.UxState.SHOW_ACCEPT) {
|
if ((tx as IncomingSasVerificationTransaction).uxState === IncomingSasVerificationTransaction.UxState.SHOW_ACCEPT) {
|
||||||
|
bobVerificationService.removeListener(this)
|
||||||
val at = tx as IncomingSasVerificationTransaction
|
val at = tx as IncomingSasVerificationTransaction
|
||||||
at.performAccept()
|
at.performAccept()
|
||||||
}
|
}
|
||||||
|
@ -515,4 +520,96 @@ class SASTest : InstrumentedTest {
|
||||||
assertTrue("bob device should be verified from alice point of view", bobDeviceInfoFromAlicePOV!!.isVerified)
|
assertTrue("bob device should be verified from alice point of view", bobDeviceInfoFromAlicePOV!!.isVerified)
|
||||||
cryptoTestData.cleanUp(mTestHelper)
|
cryptoTestData.cleanUp(mTestHelper)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun test_ConcurrentStart() {
|
||||||
|
val cryptoTestData = mCryptoTestHelper.doE2ETestWithAliceAndBobInARoom()
|
||||||
|
|
||||||
|
val aliceSession = cryptoTestData.firstSession
|
||||||
|
val bobSession = cryptoTestData.secondSession
|
||||||
|
|
||||||
|
val aliceVerificationService = aliceSession.cryptoService().verificationService()
|
||||||
|
val bobVerificationService = bobSession!!.cryptoService().verificationService()
|
||||||
|
|
||||||
|
val req = aliceVerificationService.requestKeyVerificationInDMs(
|
||||||
|
listOf(VerificationMethod.SAS, VerificationMethod.QR_CODE_SCAN, VerificationMethod.QR_CODE_SHOW),
|
||||||
|
bobSession.myUserId,
|
||||||
|
cryptoTestData.roomId
|
||||||
|
)
|
||||||
|
|
||||||
|
var requestID : String? = null
|
||||||
|
|
||||||
|
mTestHelper.waitWithLatch {
|
||||||
|
mTestHelper.retryPeriodicallyWithLatch(it) {
|
||||||
|
val prAlicePOV = aliceVerificationService.getExistingVerificationRequest(bobSession.myUserId)?.firstOrNull()
|
||||||
|
requestID = prAlicePOV?.transactionId
|
||||||
|
Log.v("TEST", "== alicePOV is $prAlicePOV")
|
||||||
|
prAlicePOV?.transactionId != null && prAlicePOV.localId == req.localId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.v("TEST", "== requestID is $requestID")
|
||||||
|
|
||||||
|
mTestHelper.waitWithLatch {
|
||||||
|
mTestHelper.retryPeriodicallyWithLatch(it) {
|
||||||
|
val prBobPOV = bobVerificationService.getExistingVerificationRequest(aliceSession.myUserId)?.firstOrNull()
|
||||||
|
Log.v("TEST", "== prBobPOV is $prBobPOV")
|
||||||
|
prBobPOV?.transactionId == requestID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bobVerificationService.readyPendingVerification(
|
||||||
|
listOf(VerificationMethod.SAS, VerificationMethod.QR_CODE_SCAN, VerificationMethod.QR_CODE_SHOW),
|
||||||
|
aliceSession.myUserId,
|
||||||
|
requestID!!
|
||||||
|
)
|
||||||
|
|
||||||
|
// wait for alice to get the ready
|
||||||
|
mTestHelper.waitWithLatch {
|
||||||
|
mTestHelper.retryPeriodicallyWithLatch(it) {
|
||||||
|
val prAlicePOV = aliceVerificationService.getExistingVerificationRequest(bobSession.myUserId)?.firstOrNull()
|
||||||
|
Log.v("TEST", "== prAlicePOV is $prAlicePOV")
|
||||||
|
prAlicePOV?.transactionId == requestID && prAlicePOV?.isReady != null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start concurrent!
|
||||||
|
aliceVerificationService.beginKeyVerificationInDMs(
|
||||||
|
VerificationMethod.SAS,
|
||||||
|
requestID!!,
|
||||||
|
cryptoTestData.roomId,
|
||||||
|
bobSession.myUserId,
|
||||||
|
bobSession.sessionParams.credentials.deviceId!!,
|
||||||
|
null)
|
||||||
|
|
||||||
|
bobVerificationService.beginKeyVerificationInDMs(
|
||||||
|
VerificationMethod.SAS,
|
||||||
|
requestID!!,
|
||||||
|
cryptoTestData.roomId,
|
||||||
|
aliceSession.myUserId,
|
||||||
|
aliceSession.sessionParams.credentials.deviceId!!,
|
||||||
|
null)
|
||||||
|
|
||||||
|
// we should reach SHOW SAS on both
|
||||||
|
var alicePovTx: SasVerificationTransaction?
|
||||||
|
var bobPovTx: SasVerificationTransaction?
|
||||||
|
|
||||||
|
mTestHelper.waitWithLatch {
|
||||||
|
mTestHelper.retryPeriodicallyWithLatch(it) {
|
||||||
|
alicePovTx = aliceVerificationService.getExistingTransaction(bobSession.myUserId, requestID!!) as? SasVerificationTransaction
|
||||||
|
Log.v("TEST", "== alicePovTx is $alicePovTx")
|
||||||
|
alicePovTx?.state == VerificationTxState.ShortCodeReady
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// wait for alice to get the ready
|
||||||
|
mTestHelper.waitWithLatch {
|
||||||
|
mTestHelper.retryPeriodicallyWithLatch(it) {
|
||||||
|
bobPovTx = bobVerificationService.getExistingTransaction(aliceSession.myUserId, requestID!!) as? SasVerificationTransaction
|
||||||
|
Log.v("TEST", "== bobPovTx is $bobPovTx")
|
||||||
|
bobPovTx?.state == VerificationTxState.ShortCodeReady
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cryptoTestData.cleanUp(mTestHelper)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,5 +23,14 @@ data class MXCryptoConfig(
|
||||||
// Tell whether the encryption of the event content is enabled for the invited members.
|
// Tell whether the encryption of the event content is enabled for the invited members.
|
||||||
// SDK clients can disable this by settings it to false.
|
// SDK clients can disable this by settings it to false.
|
||||||
// Note that the encryption for the invited members will be blocked if the history visibility is "joined".
|
// Note that the encryption for the invited members will be blocked if the history visibility is "joined".
|
||||||
var enableEncryptionForInvitedMembers: Boolean = true
|
var enableEncryptionForInvitedMembers: Boolean = true,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If set to true, the SDK will automatically ignore room key request (gossiping)
|
||||||
|
* coming from your other untrusted sessions (or blocked).
|
||||||
|
* If set to false, the request will be forwarded to the application layer; in this
|
||||||
|
* case the application can decide to prompt the user.
|
||||||
|
*/
|
||||||
|
var discardRoomKeyRequestsFromUntrustedDevices : Boolean = true
|
||||||
|
|
||||||
)
|
)
|
||||||
|
|
|
@ -0,0 +1,25 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2020 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.matrix.android.api.extensions
|
||||||
|
|
||||||
|
inline fun <A> tryThis(operation: () -> A): A? {
|
||||||
|
return try {
|
||||||
|
operation()
|
||||||
|
} catch (any: Throwable) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
|
@ -16,6 +16,7 @@
|
||||||
|
|
||||||
package im.vector.matrix.android.api.session.content
|
package im.vector.matrix.android.api.session.content
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import androidx.exifinterface.media.ExifInterface
|
import androidx.exifinterface.media.ExifInterface
|
||||||
import kotlinx.android.parcel.Parcelize
|
import kotlinx.android.parcel.Parcelize
|
||||||
|
@ -29,8 +30,7 @@ data class ContentAttachmentData(
|
||||||
val width: Long? = 0,
|
val width: Long? = 0,
|
||||||
val exifOrientation: Int = ExifInterface.ORIENTATION_UNDEFINED,
|
val exifOrientation: Int = ExifInterface.ORIENTATION_UNDEFINED,
|
||||||
val name: String? = null,
|
val name: String? = null,
|
||||||
val queryUri: String,
|
val queryUri: Uri,
|
||||||
val path: String,
|
|
||||||
private val mimeType: String?,
|
private val mimeType: String?,
|
||||||
val type: Type
|
val type: Type
|
||||||
) : Parcelable {
|
) : Parcelable {
|
||||||
|
|
|
@ -22,12 +22,14 @@ import im.vector.matrix.android.api.MatrixCallback
|
||||||
import im.vector.matrix.android.api.listeners.ProgressListener
|
import im.vector.matrix.android.api.listeners.ProgressListener
|
||||||
import im.vector.matrix.android.api.session.crypto.crosssigning.CrossSigningService
|
import im.vector.matrix.android.api.session.crypto.crosssigning.CrossSigningService
|
||||||
import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupService
|
import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupService
|
||||||
import im.vector.matrix.android.api.session.crypto.keyshare.RoomKeysRequestListener
|
import im.vector.matrix.android.api.session.crypto.keyshare.GossipingRequestListener
|
||||||
import im.vector.matrix.android.api.session.crypto.verification.VerificationService
|
import im.vector.matrix.android.api.session.crypto.verification.VerificationService
|
||||||
import im.vector.matrix.android.api.session.events.model.Content
|
import im.vector.matrix.android.api.session.events.model.Content
|
||||||
import im.vector.matrix.android.api.session.events.model.Event
|
import im.vector.matrix.android.api.session.events.model.Event
|
||||||
|
import im.vector.matrix.android.internal.crypto.IncomingRoomKeyRequest
|
||||||
import im.vector.matrix.android.internal.crypto.MXEventDecryptionResult
|
import im.vector.matrix.android.internal.crypto.MXEventDecryptionResult
|
||||||
import im.vector.matrix.android.internal.crypto.NewSessionListener
|
import im.vector.matrix.android.internal.crypto.NewSessionListener
|
||||||
|
import im.vector.matrix.android.internal.crypto.OutgoingRoomKeyRequest
|
||||||
import im.vector.matrix.android.internal.crypto.crosssigning.DeviceTrustLevel
|
import im.vector.matrix.android.internal.crypto.crosssigning.DeviceTrustLevel
|
||||||
import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
|
import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
|
||||||
import im.vector.matrix.android.internal.crypto.model.ImportRoomKeysResult
|
import im.vector.matrix.android.internal.crypto.model.ImportRoomKeysResult
|
||||||
|
@ -86,13 +88,15 @@ interface CryptoService {
|
||||||
|
|
||||||
fun getDeviceInfo(userId: String, deviceId: String?): CryptoDeviceInfo?
|
fun getDeviceInfo(userId: String, deviceId: String?): CryptoDeviceInfo?
|
||||||
|
|
||||||
|
fun requestRoomKeyForEvent(event: Event)
|
||||||
|
|
||||||
fun reRequestRoomKeyForEvent(event: Event)
|
fun reRequestRoomKeyForEvent(event: Event)
|
||||||
|
|
||||||
fun cancelRoomKeyRequest(requestBody: RoomKeyRequestBody)
|
fun cancelRoomKeyRequest(requestBody: RoomKeyRequestBody)
|
||||||
|
|
||||||
fun addRoomKeysRequestListener(listener: RoomKeysRequestListener)
|
fun addRoomKeysRequestListener(listener: GossipingRequestListener)
|
||||||
|
|
||||||
fun removeRoomKeysRequestListener(listener: RoomKeysRequestListener)
|
fun removeRoomKeysRequestListener(listener: GossipingRequestListener)
|
||||||
|
|
||||||
fun getDevicesList(callback: MatrixCallback<DevicesListResponse>)
|
fun getDevicesList(callback: MatrixCallback<DevicesListResponse>)
|
||||||
|
|
||||||
|
@ -129,4 +133,8 @@ interface CryptoService {
|
||||||
fun addNewSessionListener(newSessionListener: NewSessionListener)
|
fun addNewSessionListener(newSessionListener: NewSessionListener)
|
||||||
|
|
||||||
fun removeSessionListener(listener: NewSessionListener)
|
fun removeSessionListener(listener: NewSessionListener)
|
||||||
|
|
||||||
|
fun getOutgoingRoomKeyRequest(): List<OutgoingRoomKeyRequest>
|
||||||
|
fun getIncomingRoomKeyRequest(): List<IncomingRoomKeyRequest>
|
||||||
|
fun getGossipingEventsTrail(): List<Event>
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,6 +22,7 @@ import im.vector.matrix.android.api.util.Optional
|
||||||
import im.vector.matrix.android.internal.crypto.crosssigning.DeviceTrustResult
|
import im.vector.matrix.android.internal.crypto.crosssigning.DeviceTrustResult
|
||||||
import im.vector.matrix.android.internal.crypto.crosssigning.UserTrustResult
|
import im.vector.matrix.android.internal.crypto.crosssigning.UserTrustResult
|
||||||
import im.vector.matrix.android.internal.crypto.model.rest.UserPasswordAuth
|
import im.vector.matrix.android.internal.crypto.model.rest.UserPasswordAuth
|
||||||
|
import im.vector.matrix.android.internal.crypto.store.PrivateKeysInfo
|
||||||
|
|
||||||
interface CrossSigningService {
|
interface CrossSigningService {
|
||||||
|
|
||||||
|
@ -52,6 +53,8 @@ interface CrossSigningService {
|
||||||
|
|
||||||
fun getMyCrossSigningKeys(): MXCrossSigningInfo?
|
fun getMyCrossSigningKeys(): MXCrossSigningInfo?
|
||||||
|
|
||||||
|
fun getCrossSigningPrivateKeys(): PrivateKeysInfo?
|
||||||
|
|
||||||
fun canCrossSign(): Boolean
|
fun canCrossSign(): Boolean
|
||||||
|
|
||||||
fun trustUser(otherUserId: String,
|
fun trustUser(otherUserId: String,
|
||||||
|
@ -68,4 +71,7 @@ interface CrossSigningService {
|
||||||
fun checkDeviceTrust(otherUserId: String,
|
fun checkDeviceTrust(otherUserId: String,
|
||||||
otherDeviceId: String,
|
otherDeviceId: String,
|
||||||
locallyTrusted: Boolean?): DeviceTrustResult
|
locallyTrusted: Boolean?): DeviceTrustResult
|
||||||
|
|
||||||
|
fun onSecretSSKGossip(sskPrivateKey: String)
|
||||||
|
fun onSecretUSKGossip(uskPrivateKey: String)
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,3 +21,5 @@ const val MASTER_KEY_SSSS_NAME = "m.cross_signing.master"
|
||||||
const val USER_SIGNING_KEY_SSSS_NAME = "m.cross_signing.user_signing"
|
const val USER_SIGNING_KEY_SSSS_NAME = "m.cross_signing.user_signing"
|
||||||
|
|
||||||
const val SELF_SIGNING_KEY_SSSS_NAME = "m.cross_signing.self_signing"
|
const val SELF_SIGNING_KEY_SSSS_NAME = "m.cross_signing.self_signing"
|
||||||
|
|
||||||
|
const val KEYBACKUP_SECRET_SSSS_NAME = "m.megolm_backup.v1"
|
||||||
|
|
|
@ -24,6 +24,7 @@ import im.vector.matrix.android.internal.crypto.keysbackup.model.MegolmBackupCre
|
||||||
import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.KeysVersion
|
import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.KeysVersion
|
||||||
import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.KeysVersionResult
|
import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.KeysVersionResult
|
||||||
import im.vector.matrix.android.internal.crypto.model.ImportRoomKeysResult
|
import im.vector.matrix.android.internal.crypto.model.ImportRoomKeysResult
|
||||||
|
import im.vector.matrix.android.internal.crypto.store.SavedKeyBackupKeyInfo
|
||||||
|
|
||||||
interface KeysBackupService {
|
interface KeysBackupService {
|
||||||
/**
|
/**
|
||||||
|
@ -172,6 +173,8 @@ interface KeysBackupService {
|
||||||
password: String,
|
password: String,
|
||||||
callback: MatrixCallback<Unit>)
|
callback: MatrixCallback<Unit>)
|
||||||
|
|
||||||
|
fun onSecretKeyGossip(secret: String)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Restore a backup with a recovery key from a given backup version stored on the homeserver.
|
* Restore a backup with a recovery key from a given backup version stored on the homeserver.
|
||||||
*
|
*
|
||||||
|
@ -210,4 +213,8 @@ interface KeysBackupService {
|
||||||
val isEnabled: Boolean
|
val isEnabled: Boolean
|
||||||
val isStucked: Boolean
|
val isStucked: Boolean
|
||||||
val state: KeysBackupState
|
val state: KeysBackupState
|
||||||
|
|
||||||
|
// For gossiping
|
||||||
|
fun saveBackupRecoveryKey(recoveryKey: String?, version: String?)
|
||||||
|
fun getKeyBackupRecoveryKeyInfo() : SavedKeyBackupKeyInfo?
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,12 +17,13 @@
|
||||||
package im.vector.matrix.android.api.session.crypto.keyshare
|
package im.vector.matrix.android.api.session.crypto.keyshare
|
||||||
|
|
||||||
import im.vector.matrix.android.internal.crypto.IncomingRoomKeyRequest
|
import im.vector.matrix.android.internal.crypto.IncomingRoomKeyRequest
|
||||||
import im.vector.matrix.android.internal.crypto.IncomingRoomKeyRequestCancellation
|
import im.vector.matrix.android.internal.crypto.IncomingRequestCancellation
|
||||||
|
import im.vector.matrix.android.internal.crypto.IncomingSecretShareRequest
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Room keys events listener
|
* Room keys events listener
|
||||||
*/
|
*/
|
||||||
interface RoomKeysRequestListener {
|
interface GossipingRequestListener {
|
||||||
/**
|
/**
|
||||||
* An room key request has been received.
|
* An room key request has been received.
|
||||||
*
|
*
|
||||||
|
@ -30,10 +31,16 @@ interface RoomKeysRequestListener {
|
||||||
*/
|
*/
|
||||||
fun onRoomKeyRequest(request: IncomingRoomKeyRequest)
|
fun onRoomKeyRequest(request: IncomingRoomKeyRequest)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the secret value to be shared
|
||||||
|
* @return true if is handled
|
||||||
|
*/
|
||||||
|
fun onSecretShareRequest(request: IncomingSecretShareRequest) : Boolean
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A room key request cancellation has been received.
|
* A room key request cancellation has been received.
|
||||||
*
|
*
|
||||||
* @param request the cancellation request
|
* @param request the cancellation request
|
||||||
*/
|
*/
|
||||||
fun onRoomKeyRequestCancellation(request: IncomingRoomKeyRequestCancellation)
|
fun onRoomKeyRequestCancellation(request: IncomingRequestCancellation)
|
||||||
}
|
}
|
|
@ -16,7 +16,10 @@
|
||||||
|
|
||||||
package im.vector.matrix.android.api.session.crypto.verification
|
package im.vector.matrix.android.api.session.crypto.verification
|
||||||
|
|
||||||
|
import androidx.annotation.DrawableRes
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
|
|
||||||
data class EmojiRepresentation(val emoji: String,
|
data class EmojiRepresentation(val emoji: String,
|
||||||
@StringRes val nameResId: Int)
|
@StringRes val nameResId: Int,
|
||||||
|
@DrawableRes val drawableRes: Int? = null
|
||||||
|
)
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
package im.vector.matrix.android.api.session.crypto.verification
|
package im.vector.matrix.android.api.session.crypto.verification
|
||||||
|
|
||||||
import im.vector.matrix.android.api.MatrixCallback
|
import im.vector.matrix.android.api.MatrixCallback
|
||||||
|
import im.vector.matrix.android.api.session.events.model.Event
|
||||||
import im.vector.matrix.android.api.session.events.model.LocalEcho
|
import im.vector.matrix.android.api.session.events.model.LocalEcho
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -59,6 +60,8 @@ interface VerificationService {
|
||||||
roomId: String,
|
roomId: String,
|
||||||
localId: String? = LocalEcho.createLocalEchoId()): PendingVerificationRequest
|
localId: String? = LocalEcho.createLocalEchoId()): PendingVerificationRequest
|
||||||
|
|
||||||
|
fun cancelVerificationRequest(request: PendingVerificationRequest)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Request a key verification from another user using toDevice events.
|
* Request a key verification from another user using toDevice events.
|
||||||
*/
|
*/
|
||||||
|
@ -136,4 +139,6 @@ interface VerificationService {
|
||||||
return age in tooInThePast..tooInTheFuture
|
return age in tooInThePast..tooInTheFuture
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun onPotentiallyInterestingEventRoomFailToDecrypt(event: Event)
|
||||||
}
|
}
|
||||||
|
|
|
@ -43,6 +43,7 @@ sealed class VerificationTxState {
|
||||||
|
|
||||||
// Will be used to ask the user if the other user has correctly scanned
|
// Will be used to ask the user if the other user has correctly scanned
|
||||||
object QrScannedByOther : VerificationQrTxState()
|
object QrScannedByOther : VerificationQrTxState()
|
||||||
|
object WaitingOtherReciprocateConfirm : VerificationQrTxState()
|
||||||
|
|
||||||
// Terminal states
|
// Terminal states
|
||||||
abstract class TerminalTxState : VerificationTxState()
|
abstract class TerminalTxState : VerificationTxState()
|
||||||
|
|
|
@ -142,12 +142,12 @@ data class Event(
|
||||||
}
|
}
|
||||||
|
|
||||||
fun toContentStringWithIndent(): String {
|
fun toContentStringWithIndent(): String {
|
||||||
val contentMap = toContent().toMutableMap()
|
val contentMap = toContent()
|
||||||
return JSONObject(contentMap).toString(4)
|
return JSONObject(contentMap).toString(4)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun toClearContentStringWithIndent(): String? {
|
fun toClearContentStringWithIndent(): String? {
|
||||||
val contentMap = this.mxDecryptionResult?.payload?.toMutableMap()
|
val contentMap = this.mxDecryptionResult?.payload
|
||||||
val adapter = MoshiProvider.providesMoshi().adapter(Map::class.java)
|
val adapter = MoshiProvider.providesMoshi().adapter(Map::class.java)
|
||||||
return contentMap?.let { JSONObject(adapter.toJson(it)).toString(4) }
|
return contentMap?.let { JSONObject(adapter.toJson(it)).toString(4) }
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,3 +19,7 @@ package im.vector.matrix.android.api.session.securestorage
|
||||||
interface KeySigner {
|
interface KeySigner {
|
||||||
fun sign(canonicalJson: String): Map<String, Map<String, String>>?
|
fun sign(canonicalJson: String): Map<String, Map<String, String>>?
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class EmptyKeySigner : KeySigner {
|
||||||
|
override fun sign(canonicalJson: String): Map<String, Map<String, String>>? = null
|
||||||
|
}
|
||||||
|
|
|
@ -111,6 +111,8 @@ interface SharedSecretStorageService {
|
||||||
|
|
||||||
fun checkShouldBeAbleToAccessSecrets(secretNames: List<String>, keyId: String?) : IntegrityResult
|
fun checkShouldBeAbleToAccessSecrets(secretNames: List<String>, keyId: String?) : IntegrityResult
|
||||||
|
|
||||||
|
fun requestSecret(name: String, myOtherDeviceId: String)
|
||||||
|
|
||||||
data class KeyRef(
|
data class KeyRef(
|
||||||
val keyId: String?,
|
val keyId: String?,
|
||||||
val keySpec: SsssKeySpec?
|
val keySpec: SsssKeySpec?
|
||||||
|
|
|
@ -19,5 +19,6 @@ package im.vector.matrix.android.api.session.securestorage
|
||||||
data class SsssKeyCreationInfo(
|
data class SsssKeyCreationInfo(
|
||||||
val keyId: String = "",
|
val keyId: String = "",
|
||||||
var content: SecretStorageKeyContent?,
|
var content: SecretStorageKeyContent?,
|
||||||
val recoveryKey: String = ""
|
val recoveryKey: String = "",
|
||||||
|
val keySpec: SsssKeySpec
|
||||||
)
|
)
|
||||||
|
|
|
@ -0,0 +1,118 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2020 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.matrix.android.internal.crypto
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.work.CoroutineWorker
|
||||||
|
import androidx.work.Data
|
||||||
|
import androidx.work.WorkerParameters
|
||||||
|
import com.squareup.moshi.JsonClass
|
||||||
|
import im.vector.matrix.android.api.auth.data.Credentials
|
||||||
|
import im.vector.matrix.android.api.failure.shouldBeRetried
|
||||||
|
import im.vector.matrix.android.api.session.events.model.Event
|
||||||
|
import im.vector.matrix.android.api.session.events.model.EventType
|
||||||
|
import im.vector.matrix.android.api.session.events.model.LocalEcho
|
||||||
|
import im.vector.matrix.android.api.session.events.model.toContent
|
||||||
|
import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap
|
||||||
|
import im.vector.matrix.android.internal.crypto.model.rest.ShareRequestCancellation
|
||||||
|
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
|
||||||
|
import im.vector.matrix.android.internal.crypto.tasks.SendToDeviceTask
|
||||||
|
import im.vector.matrix.android.internal.worker.WorkerParamsFactory
|
||||||
|
import im.vector.matrix.android.internal.worker.getSessionComponent
|
||||||
|
import org.greenrobot.eventbus.EventBus
|
||||||
|
import timber.log.Timber
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
internal class CancelGossipRequestWorker(context: Context,
|
||||||
|
params: WorkerParameters)
|
||||||
|
: CoroutineWorker(context, params) {
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
|
internal data class Params(
|
||||||
|
val sessionId: String,
|
||||||
|
val requestId: String,
|
||||||
|
val recipients: Map<String, List<String>>
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
fun fromRequest(sessionId: String, request: OutgoingGossipingRequest): Params {
|
||||||
|
return Params(
|
||||||
|
sessionId = sessionId,
|
||||||
|
requestId = request.requestId,
|
||||||
|
recipients = request.recipients
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Inject lateinit var sendToDeviceTask: SendToDeviceTask
|
||||||
|
@Inject lateinit var cryptoStore: IMXCryptoStore
|
||||||
|
@Inject lateinit var eventBus: EventBus
|
||||||
|
@Inject lateinit var credentials: Credentials
|
||||||
|
|
||||||
|
override suspend fun doWork(): Result {
|
||||||
|
val errorOutputData = Data.Builder().putBoolean("failed", true).build()
|
||||||
|
val params = WorkerParamsFactory.fromData<Params>(inputData)
|
||||||
|
?: return Result.success(errorOutputData)
|
||||||
|
|
||||||
|
val sessionComponent = getSessionComponent(params.sessionId)
|
||||||
|
?: return Result.success(errorOutputData).also {
|
||||||
|
// TODO, can this happen? should I update local echo?
|
||||||
|
Timber.e("Unknown Session, cannot send message, sessionId: ${params.sessionId}")
|
||||||
|
}
|
||||||
|
sessionComponent.inject(this)
|
||||||
|
|
||||||
|
val localId = LocalEcho.createLocalEchoId()
|
||||||
|
val contentMap = MXUsersDevicesMap<Any>()
|
||||||
|
val toDeviceContent = ShareRequestCancellation(
|
||||||
|
requestingDeviceId = credentials.deviceId,
|
||||||
|
requestId = params.requestId
|
||||||
|
)
|
||||||
|
cryptoStore.saveGossipingEvent(Event(
|
||||||
|
type = EventType.ROOM_KEY_REQUEST,
|
||||||
|
content = toDeviceContent.toContent(),
|
||||||
|
senderId = credentials.userId
|
||||||
|
).also {
|
||||||
|
it.ageLocalTs = System.currentTimeMillis()
|
||||||
|
})
|
||||||
|
|
||||||
|
params.recipients.forEach { userToDeviceMap ->
|
||||||
|
userToDeviceMap.value.forEach { deviceId ->
|
||||||
|
contentMap.setObject(userToDeviceMap.key, deviceId, toDeviceContent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
cryptoStore.updateOutgoingGossipingRequestState(params.requestId, OutgoingGossipingRequestState.CANCELLING)
|
||||||
|
sendToDeviceTask.execute(
|
||||||
|
SendToDeviceTask.Params(
|
||||||
|
eventType = EventType.ROOM_KEY_REQUEST,
|
||||||
|
contentMap = contentMap,
|
||||||
|
transactionId = localId
|
||||||
|
)
|
||||||
|
)
|
||||||
|
cryptoStore.updateOutgoingGossipingRequestState(params.requestId, OutgoingGossipingRequestState.CANCELLED)
|
||||||
|
return Result.success()
|
||||||
|
} catch (exception: Throwable) {
|
||||||
|
return if (exception.shouldBeRetried()) {
|
||||||
|
Result.retry()
|
||||||
|
} else {
|
||||||
|
cryptoStore.updateOutgoingGossipingRequestState(params.requestId, OutgoingGossipingRequestState.FAILED_TO_CANCEL)
|
||||||
|
Result.success(errorOutputData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -66,6 +66,7 @@ import im.vector.matrix.android.internal.crypto.tasks.DefaultDownloadKeysForUser
|
||||||
import im.vector.matrix.android.internal.crypto.tasks.DefaultEncryptEventTask
|
import im.vector.matrix.android.internal.crypto.tasks.DefaultEncryptEventTask
|
||||||
import im.vector.matrix.android.internal.crypto.tasks.DefaultGetDeviceInfoTask
|
import im.vector.matrix.android.internal.crypto.tasks.DefaultGetDeviceInfoTask
|
||||||
import im.vector.matrix.android.internal.crypto.tasks.DefaultGetDevicesTask
|
import im.vector.matrix.android.internal.crypto.tasks.DefaultGetDevicesTask
|
||||||
|
import im.vector.matrix.android.internal.crypto.tasks.DefaultInitializeCrossSigningTask
|
||||||
import im.vector.matrix.android.internal.crypto.tasks.DefaultSendToDeviceTask
|
import im.vector.matrix.android.internal.crypto.tasks.DefaultSendToDeviceTask
|
||||||
import im.vector.matrix.android.internal.crypto.tasks.DefaultSendVerificationMessageTask
|
import im.vector.matrix.android.internal.crypto.tasks.DefaultSendVerificationMessageTask
|
||||||
import im.vector.matrix.android.internal.crypto.tasks.DefaultSetDeviceNameTask
|
import im.vector.matrix.android.internal.crypto.tasks.DefaultSetDeviceNameTask
|
||||||
|
@ -78,6 +79,7 @@ import im.vector.matrix.android.internal.crypto.tasks.DownloadKeysForUsersTask
|
||||||
import im.vector.matrix.android.internal.crypto.tasks.EncryptEventTask
|
import im.vector.matrix.android.internal.crypto.tasks.EncryptEventTask
|
||||||
import im.vector.matrix.android.internal.crypto.tasks.GetDeviceInfoTask
|
import im.vector.matrix.android.internal.crypto.tasks.GetDeviceInfoTask
|
||||||
import im.vector.matrix.android.internal.crypto.tasks.GetDevicesTask
|
import im.vector.matrix.android.internal.crypto.tasks.GetDevicesTask
|
||||||
|
import im.vector.matrix.android.internal.crypto.tasks.InitializeCrossSigningTask
|
||||||
import im.vector.matrix.android.internal.crypto.tasks.SendToDeviceTask
|
import im.vector.matrix.android.internal.crypto.tasks.SendToDeviceTask
|
||||||
import im.vector.matrix.android.internal.crypto.tasks.SendVerificationMessageTask
|
import im.vector.matrix.android.internal.crypto.tasks.SendVerificationMessageTask
|
||||||
import im.vector.matrix.android.internal.crypto.tasks.SetDeviceNameTask
|
import im.vector.matrix.android.internal.crypto.tasks.SetDeviceNameTask
|
||||||
|
@ -245,4 +247,7 @@ internal abstract class CryptoModule {
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
abstract fun bindComputeShieldTrustTask(task: DefaultComputeTrustTask): ComputeTrustTask
|
abstract fun bindComputeShieldTrustTask(task: DefaultComputeTrustTask): ComputeTrustTask
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
abstract fun bindInitializeCrossSigningTask(task: DefaultInitializeCrossSigningTask): InitializeCrossSigningTask
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,7 +33,10 @@ import im.vector.matrix.android.api.failure.Failure
|
||||||
import im.vector.matrix.android.api.listeners.ProgressListener
|
import im.vector.matrix.android.api.listeners.ProgressListener
|
||||||
import im.vector.matrix.android.api.session.crypto.CryptoService
|
import im.vector.matrix.android.api.session.crypto.CryptoService
|
||||||
import im.vector.matrix.android.api.session.crypto.MXCryptoError
|
import im.vector.matrix.android.api.session.crypto.MXCryptoError
|
||||||
import im.vector.matrix.android.api.session.crypto.keyshare.RoomKeysRequestListener
|
import im.vector.matrix.android.api.session.crypto.crosssigning.KEYBACKUP_SECRET_SSSS_NAME
|
||||||
|
import im.vector.matrix.android.api.session.crypto.crosssigning.SELF_SIGNING_KEY_SSSS_NAME
|
||||||
|
import im.vector.matrix.android.api.session.crypto.crosssigning.USER_SIGNING_KEY_SSSS_NAME
|
||||||
|
import im.vector.matrix.android.api.session.crypto.keyshare.GossipingRequestListener
|
||||||
import im.vector.matrix.android.api.session.events.model.Content
|
import im.vector.matrix.android.api.session.events.model.Content
|
||||||
import im.vector.matrix.android.api.session.events.model.Event
|
import im.vector.matrix.android.api.session.events.model.Event
|
||||||
import im.vector.matrix.android.api.session.events.model.EventType
|
import im.vector.matrix.android.api.session.events.model.EventType
|
||||||
|
@ -55,7 +58,9 @@ import im.vector.matrix.android.internal.crypto.model.ImportRoomKeysResult
|
||||||
import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo
|
import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo
|
||||||
import im.vector.matrix.android.internal.crypto.model.MXEncryptEventContentResult
|
import im.vector.matrix.android.internal.crypto.model.MXEncryptEventContentResult
|
||||||
import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap
|
import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap
|
||||||
|
import im.vector.matrix.android.internal.crypto.model.event.EncryptedEventContent
|
||||||
import im.vector.matrix.android.internal.crypto.model.event.RoomKeyContent
|
import im.vector.matrix.android.internal.crypto.model.event.RoomKeyContent
|
||||||
|
import im.vector.matrix.android.internal.crypto.model.event.SecretSendEventContent
|
||||||
import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
|
import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
|
||||||
import im.vector.matrix.android.internal.crypto.model.rest.DevicesListResponse
|
import im.vector.matrix.android.internal.crypto.model.rest.DevicesListResponse
|
||||||
import im.vector.matrix.android.internal.crypto.model.rest.KeysUploadResponse
|
import im.vector.matrix.android.internal.crypto.model.rest.KeysUploadResponse
|
||||||
|
@ -80,6 +85,7 @@ import im.vector.matrix.android.internal.session.room.membership.LoadRoomMembers
|
||||||
import im.vector.matrix.android.internal.session.room.membership.RoomMemberHelper
|
import im.vector.matrix.android.internal.session.room.membership.RoomMemberHelper
|
||||||
import im.vector.matrix.android.internal.session.sync.model.SyncResponse
|
import im.vector.matrix.android.internal.session.sync.model.SyncResponse
|
||||||
import im.vector.matrix.android.internal.task.TaskExecutor
|
import im.vector.matrix.android.internal.task.TaskExecutor
|
||||||
|
import im.vector.matrix.android.internal.task.TaskThread
|
||||||
import im.vector.matrix.android.internal.task.configureWith
|
import im.vector.matrix.android.internal.task.configureWith
|
||||||
import im.vector.matrix.android.internal.util.JsonCanonicalizer
|
import im.vector.matrix.android.internal.util.JsonCanonicalizer
|
||||||
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
|
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
|
||||||
|
@ -115,6 +121,7 @@ internal class DefaultCryptoService @Inject constructor(
|
||||||
private val myDeviceInfoHolder: Lazy<MyDeviceInfoHolder>,
|
private val myDeviceInfoHolder: Lazy<MyDeviceInfoHolder>,
|
||||||
// the crypto store
|
// the crypto store
|
||||||
private val cryptoStore: IMXCryptoStore,
|
private val cryptoStore: IMXCryptoStore,
|
||||||
|
|
||||||
// Olm device
|
// Olm device
|
||||||
private val olmDevice: MXOlmDevice,
|
private val olmDevice: MXOlmDevice,
|
||||||
// Set of parameters used to configure/customize the end-to-end crypto.
|
// Set of parameters used to configure/customize the end-to-end crypto.
|
||||||
|
@ -134,9 +141,9 @@ internal class DefaultCryptoService @Inject constructor(
|
||||||
|
|
||||||
private val crossSigningService: DefaultCrossSigningService,
|
private val crossSigningService: DefaultCrossSigningService,
|
||||||
//
|
//
|
||||||
private val incomingRoomKeyRequestManager: IncomingRoomKeyRequestManager,
|
private val incomingGossipingRequestManager: IncomingGossipingRequestManager,
|
||||||
//
|
//
|
||||||
private val outgoingRoomKeyRequestManager: OutgoingRoomKeyRequestManager,
|
private val outgoingGossipingRequestManager: OutgoingGossipingRequestManager,
|
||||||
// Actions
|
// Actions
|
||||||
private val setDeviceVerificationAction: SetDeviceVerificationAction,
|
private val setDeviceVerificationAction: SetDeviceVerificationAction,
|
||||||
private val megolmSessionDataImporter: MegolmSessionDataImporter,
|
private val megolmSessionDataImporter: MegolmSessionDataImporter,
|
||||||
|
@ -188,6 +195,7 @@ internal class DefaultCryptoService @Inject constructor(
|
||||||
override fun setDeviceName(deviceId: String, deviceName: String, callback: MatrixCallback<Unit>) {
|
override fun setDeviceName(deviceId: String, deviceName: String, callback: MatrixCallback<Unit>) {
|
||||||
setDeviceNameTask
|
setDeviceNameTask
|
||||||
.configureWith(SetDeviceNameTask.Params(deviceId, deviceName)) {
|
.configureWith(SetDeviceNameTask.Params(deviceId, deviceName)) {
|
||||||
|
this.executionThread = TaskThread.CRYPTO
|
||||||
this.callback = object : MatrixCallback<Unit> {
|
this.callback = object : MatrixCallback<Unit> {
|
||||||
override fun onSuccess(data: Unit) {
|
override fun onSuccess(data: Unit) {
|
||||||
// bg refresh of crypto device
|
// bg refresh of crypto device
|
||||||
|
@ -206,6 +214,7 @@ internal class DefaultCryptoService @Inject constructor(
|
||||||
override fun deleteDevice(deviceId: String, callback: MatrixCallback<Unit>) {
|
override fun deleteDevice(deviceId: String, callback: MatrixCallback<Unit>) {
|
||||||
deleteDeviceTask
|
deleteDeviceTask
|
||||||
.configureWith(DeleteDeviceTask.Params(deviceId)) {
|
.configureWith(DeleteDeviceTask.Params(deviceId)) {
|
||||||
|
this.executionThread = TaskThread.CRYPTO
|
||||||
this.callback = callback
|
this.callback = callback
|
||||||
}
|
}
|
||||||
.executeBy(taskExecutor)
|
.executeBy(taskExecutor)
|
||||||
|
@ -214,6 +223,7 @@ internal class DefaultCryptoService @Inject constructor(
|
||||||
override fun deleteDeviceWithUserPassword(deviceId: String, authSession: String?, password: String, callback: MatrixCallback<Unit>) {
|
override fun deleteDeviceWithUserPassword(deviceId: String, authSession: String?, password: String, callback: MatrixCallback<Unit>) {
|
||||||
deleteDeviceWithUserPasswordTask
|
deleteDeviceWithUserPasswordTask
|
||||||
.configureWith(DeleteDeviceWithUserPasswordTask.Params(deviceId, authSession, password)) {
|
.configureWith(DeleteDeviceWithUserPasswordTask.Params(deviceId, authSession, password)) {
|
||||||
|
this.executionThread = TaskThread.CRYPTO
|
||||||
this.callback = callback
|
this.callback = callback
|
||||||
}
|
}
|
||||||
.executeBy(taskExecutor)
|
.executeBy(taskExecutor)
|
||||||
|
@ -230,6 +240,7 @@ internal class DefaultCryptoService @Inject constructor(
|
||||||
override fun getDevicesList(callback: MatrixCallback<DevicesListResponse>) {
|
override fun getDevicesList(callback: MatrixCallback<DevicesListResponse>) {
|
||||||
getDevicesTask
|
getDevicesTask
|
||||||
.configureWith {
|
.configureWith {
|
||||||
|
// this.executionThread = TaskThread.CRYPTO
|
||||||
this.callback = callback
|
this.callback = callback
|
||||||
}
|
}
|
||||||
.executeBy(taskExecutor)
|
.executeBy(taskExecutor)
|
||||||
|
@ -238,6 +249,7 @@ internal class DefaultCryptoService @Inject constructor(
|
||||||
override fun getDeviceInfo(deviceId: String, callback: MatrixCallback<DeviceInfo>) {
|
override fun getDeviceInfo(deviceId: String, callback: MatrixCallback<DeviceInfo>) {
|
||||||
getDeviceInfoTask
|
getDeviceInfoTask
|
||||||
.configureWith(GetDeviceInfoTask.Params(deviceId)) {
|
.configureWith(GetDeviceInfoTask.Params(deviceId)) {
|
||||||
|
this.executionThread = TaskThread.CRYPTO
|
||||||
this.callback = callback
|
this.callback = callback
|
||||||
}
|
}
|
||||||
.executeBy(taskExecutor)
|
.executeBy(taskExecutor)
|
||||||
|
@ -300,14 +312,13 @@ internal class DefaultCryptoService @Inject constructor(
|
||||||
runCatching {
|
runCatching {
|
||||||
uploadDeviceKeys()
|
uploadDeviceKeys()
|
||||||
oneTimeKeysUploader.maybeUploadOneTimeKeys()
|
oneTimeKeysUploader.maybeUploadOneTimeKeys()
|
||||||
outgoingRoomKeyRequestManager.start()
|
|
||||||
keysBackupService.checkAndStartKeysBackup()
|
keysBackupService.checkAndStartKeysBackup()
|
||||||
if (isInitialSync) {
|
if (isInitialSync) {
|
||||||
// refresh the devices list for each known room members
|
// refresh the devices list for each known room members
|
||||||
deviceListManager.invalidateAllDeviceLists()
|
deviceListManager.invalidateAllDeviceLists()
|
||||||
deviceListManager.refreshOutdatedDeviceLists()
|
deviceListManager.refreshOutdatedDeviceLists()
|
||||||
} else {
|
} else {
|
||||||
incomingRoomKeyRequestManager.processReceivedRoomKeyRequests()
|
incomingGossipingRequestManager.processReceivedGossipingRequests()
|
||||||
}
|
}
|
||||||
}.fold(
|
}.fold(
|
||||||
{
|
{
|
||||||
|
@ -328,8 +339,6 @@ 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"))
|
||||||
|
|
||||||
outgoingRoomKeyRequestManager.stop()
|
|
||||||
|
|
||||||
olmDevice.release()
|
olmDevice.release()
|
||||||
cryptoStore.close()
|
cryptoStore.close()
|
||||||
}
|
}
|
||||||
|
@ -368,7 +377,7 @@ internal class DefaultCryptoService @Inject constructor(
|
||||||
// Make sure we process to-device messages before generating new one-time-keys #2782
|
// Make sure we process to-device messages before generating new one-time-keys #2782
|
||||||
deviceListManager.refreshOutdatedDeviceLists()
|
deviceListManager.refreshOutdatedDeviceLists()
|
||||||
oneTimeKeysUploader.maybeUploadOneTimeKeys()
|
oneTimeKeysUploader.maybeUploadOneTimeKeys()
|
||||||
incomingRoomKeyRequestManager.processReceivedRoomKeyRequests()
|
incomingGossipingRequestManager.processReceivedGossipingRequests()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -627,7 +636,7 @@ internal class DefaultCryptoService @Inject constructor(
|
||||||
*/
|
*/
|
||||||
@Throws(MXCryptoError::class)
|
@Throws(MXCryptoError::class)
|
||||||
override fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult {
|
override fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult {
|
||||||
return internalDecryptEvent(event, timeline)
|
return internalDecryptEvent(event, timeline)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -688,13 +697,24 @@ internal class DefaultCryptoService @Inject constructor(
|
||||||
* @param event the event
|
* @param event the event
|
||||||
*/
|
*/
|
||||||
fun onToDeviceEvent(event: Event) {
|
fun onToDeviceEvent(event: Event) {
|
||||||
|
// event have already been decrypted
|
||||||
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)
|
||||||
|
// Keys are imported directly, not waiting for end of sync
|
||||||
onRoomKeyEvent(event)
|
onRoomKeyEvent(event)
|
||||||
}
|
}
|
||||||
|
EventType.REQUEST_SECRET,
|
||||||
EventType.ROOM_KEY_REQUEST -> {
|
EventType.ROOM_KEY_REQUEST -> {
|
||||||
incomingRoomKeyRequestManager.onRoomKeyRequestEvent(event)
|
// save audit trail
|
||||||
|
cryptoStore.saveGossipingEvent(event)
|
||||||
|
// Requests are stacked, and will be handled one by one at the end of the sync (onSyncComplete)
|
||||||
|
incomingGossipingRequestManager.onGossipingRequestEvent(event)
|
||||||
|
}
|
||||||
|
EventType.SEND_SECRET -> {
|
||||||
|
cryptoStore.saveGossipingEvent(event)
|
||||||
|
onSecretSendReceived(event)
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
// ignore
|
// ignore
|
||||||
|
@ -710,18 +730,70 @@ 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("## GOSSIP onRoomKeyEvent() : type<${event.type}> , sessionId<${roomKeyContent.sessionId}>")
|
||||||
if (roomKeyContent.roomId.isNullOrEmpty() || roomKeyContent.algorithm.isNullOrEmpty()) {
|
if (roomKeyContent.roomId.isNullOrEmpty() || roomKeyContent.algorithm.isNullOrEmpty()) {
|
||||||
Timber.e("## onRoomKeyEvent() : missing fields")
|
Timber.e("## GOSSIP onRoomKeyEvent() : missing fields")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
val alg = roomDecryptorProvider.getOrCreateRoomDecryptor(roomKeyContent.roomId, roomKeyContent.algorithm)
|
val alg = roomDecryptorProvider.getOrCreateRoomDecryptor(roomKeyContent.roomId, roomKeyContent.algorithm)
|
||||||
if (alg == null) {
|
if (alg == null) {
|
||||||
Timber.e("## onRoomKeyEvent() : Unable to handle keys for ${roomKeyContent.algorithm}")
|
Timber.e("## GOSSIP onRoomKeyEvent() : Unable to handle keys for ${roomKeyContent.algorithm}")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
alg.onRoomKeyEvent(event, keysBackupService)
|
alg.onRoomKeyEvent(event, keysBackupService)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun onSecretSendReceived(event: Event) {
|
||||||
|
Timber.i("## GOSSIP onSecretSend() : onSecretSendReceived ${event.content?.get("sender_key")}")
|
||||||
|
if (!event.isEncrypted()) {
|
||||||
|
// secret send messages must be encrypted
|
||||||
|
Timber.e("## GOSSIP onSecretSend() :Received unencrypted secret send event")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Was that sent by us?
|
||||||
|
if (event.senderId != credentials.userId) {
|
||||||
|
Timber.e("## GOSSIP onSecretSend() : Ignore secret from other user ${event.senderId}")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val secretContent = event.getClearContent().toModel<SecretSendEventContent>() ?: return
|
||||||
|
|
||||||
|
val existingRequest = cryptoStore
|
||||||
|
.getOutgoingSecretKeyRequests().firstOrNull { it.requestId == secretContent.requestId }
|
||||||
|
|
||||||
|
if (existingRequest == null) {
|
||||||
|
Timber.i("## GOSSIP onSecretSend() : Ignore secret that was not requested: ${secretContent.requestId}")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!handleSDKLevelGossip(existingRequest.secretName, secretContent.secretValue)) {
|
||||||
|
// TODO Ask to application layer?
|
||||||
|
Timber.v("## onSecretSend() : secret not handled by SDK")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if handled by SDK, otherwise should be sent to application layer
|
||||||
|
*/
|
||||||
|
private fun handleSDKLevelGossip(secretName: String?, secretValue: String): Boolean {
|
||||||
|
return when (secretName) {
|
||||||
|
SELF_SIGNING_KEY_SSSS_NAME -> {
|
||||||
|
crossSigningService.onSecretSSKGossip(secretValue)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
USER_SIGNING_KEY_SSSS_NAME -> {
|
||||||
|
crossSigningService.onSecretUSKGossip(secretValue)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
KEYBACKUP_SECRET_SSSS_NAME -> {
|
||||||
|
keysBackupService.onSecretKeyGossip(secretValue)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle an m.room.encryption event.
|
* Handle an m.room.encryption event.
|
||||||
*
|
*
|
||||||
|
@ -732,10 +804,11 @@ internal class DefaultCryptoService @Inject constructor(
|
||||||
val params = LoadRoomMembersTask.Params(roomId)
|
val params = LoadRoomMembersTask.Params(roomId)
|
||||||
try {
|
try {
|
||||||
loadRoomMembersTask.execute(params)
|
loadRoomMembersTask.execute(params)
|
||||||
|
} catch (throwable: Throwable) {
|
||||||
|
Timber.e(throwable, "## onRoomEncryptionEvent ERROR FAILED TO SETUP CRYPTO ")
|
||||||
|
} finally {
|
||||||
val userIds = getRoomUserIds(roomId)
|
val userIds = getRoomUserIds(roomId)
|
||||||
setEncryptionInRoom(roomId, event.content?.get("algorithm")?.toString(), true, userIds)
|
setEncryptionInRoom(roomId, event.content?.get("algorithm")?.toString(), true, userIds)
|
||||||
} catch (throwable: Throwable) {
|
|
||||||
Timber.e(throwable)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -997,14 +1070,14 @@ internal class DefaultCryptoService @Inject constructor(
|
||||||
setRoomBlacklistUnverifiedDevices(roomId, false)
|
setRoomBlacklistUnverifiedDevices(roomId, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO Check if this method is still necessary
|
// TODO Check if this method is still necessary
|
||||||
/**
|
/**
|
||||||
* Cancel any earlier room key request
|
* Cancel any earlier room key request
|
||||||
*
|
*
|
||||||
* @param requestBody requestBody
|
* @param requestBody requestBody
|
||||||
*/
|
*/
|
||||||
override fun cancelRoomKeyRequest(requestBody: RoomKeyRequestBody) {
|
override fun cancelRoomKeyRequest(requestBody: RoomKeyRequestBody) {
|
||||||
outgoingRoomKeyRequestManager.cancelRoomKeyRequest(requestBody)
|
outgoingGossipingRequestManager.cancelRoomKeyRequest(requestBody)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1013,38 +1086,54 @@ internal class DefaultCryptoService @Inject constructor(
|
||||||
* @param event the event to decrypt again.
|
* @param event the event to decrypt again.
|
||||||
*/
|
*/
|
||||||
override fun reRequestRoomKeyForEvent(event: Event) {
|
override fun reRequestRoomKeyForEvent(event: Event) {
|
||||||
val wireContent = event.content
|
val wireContent = event.content.toModel<EncryptedEventContent>() ?: return Unit.also {
|
||||||
if (wireContent == null) {
|
|
||||||
Timber.e("## reRequestRoomKeyForEvent Failed to re-request key, null content")
|
Timber.e("## reRequestRoomKeyForEvent Failed to re-request key, null content")
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val requestBody = RoomKeyRequestBody(
|
val requestBody = RoomKeyRequestBody(
|
||||||
algorithm = wireContent["algorithm"]?.toString(),
|
algorithm = wireContent.algorithm,
|
||||||
roomId = event.roomId,
|
roomId = event.roomId,
|
||||||
senderKey = wireContent["sender_key"]?.toString(),
|
senderKey = wireContent.senderKey,
|
||||||
sessionId = wireContent["session_id"]?.toString()
|
sessionId = wireContent.sessionId
|
||||||
)
|
)
|
||||||
|
|
||||||
outgoingRoomKeyRequestManager.resendRoomKeyRequest(requestBody)
|
outgoingGossipingRequestManager.resendRoomKeyRequest(requestBody)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun requestRoomKeyForEvent(event: Event) {
|
||||||
|
val wireContent = event.content.toModel<EncryptedEventContent>() ?: return Unit.also {
|
||||||
|
Timber.e("## requestRoomKeyForEvent Failed to request key, null content eventId: ${event.eventId}")
|
||||||
|
}
|
||||||
|
|
||||||
|
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
|
||||||
|
if (!isStarted()) {
|
||||||
|
Timber.v("## requestRoomKeyForEvent() : wait after e2e init")
|
||||||
|
internalStart(false)
|
||||||
|
}
|
||||||
|
roomDecryptorProvider
|
||||||
|
.getOrCreateRoomDecryptor(event.roomId, wireContent.algorithm)
|
||||||
|
?.requestKeysForEvent(event) ?: run {
|
||||||
|
Timber.v("## requestRoomKeyForEvent() : No room decryptor for roomId:${event.roomId} algorithm:${wireContent.algorithm}")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add a RoomKeysRequestListener listener.
|
* Add a GossipingRequestListener listener.
|
||||||
*
|
*
|
||||||
* @param listener listener
|
* @param listener listener
|
||||||
*/
|
*/
|
||||||
override fun addRoomKeysRequestListener(listener: RoomKeysRequestListener) {
|
override fun addRoomKeysRequestListener(listener: GossipingRequestListener) {
|
||||||
incomingRoomKeyRequestManager.addRoomKeysRequestListener(listener)
|
incomingGossipingRequestManager.addRoomKeysRequestListener(listener)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add a RoomKeysRequestListener listener.
|
* Add a GossipingRequestListener listener.
|
||||||
*
|
*
|
||||||
* @param listener listener
|
* @param listener listener
|
||||||
*/
|
*/
|
||||||
override fun removeRoomKeysRequestListener(listener: RoomKeysRequestListener) {
|
override fun removeRoomKeysRequestListener(listener: GossipingRequestListener) {
|
||||||
incomingRoomKeyRequestManager.removeRoomKeysRequestListener(listener)
|
incomingGossipingRequestManager.removeRoomKeysRequestListener(listener)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1084,11 +1173,23 @@ internal class DefaultCryptoService @Inject constructor(
|
||||||
override fun removeSessionListener(listener: NewSessionListener) {
|
override fun removeSessionListener(listener: NewSessionListener) {
|
||||||
roomDecryptorProvider.removeSessionListener(listener)
|
roomDecryptorProvider.removeSessionListener(listener)
|
||||||
}
|
}
|
||||||
/* ==========================================================================================
|
/* ==========================================================================================
|
||||||
* DEBUG INFO
|
* DEBUG INFO
|
||||||
* ========================================================================================== */
|
* ========================================================================================== */
|
||||||
|
|
||||||
override fun toString(): String {
|
override fun toString(): String {
|
||||||
return "DefaultCryptoService of " + credentials.userId + " (" + credentials.deviceId + ")"
|
return "DefaultCryptoService of " + credentials.userId + " (" + credentials.deviceId + ")"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun getOutgoingRoomKeyRequest(): List<OutgoingRoomKeyRequest> {
|
||||||
|
return cryptoStore.getOutgoingRoomKeyRequests()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getIncomingRoomKeyRequest(): List<IncomingRoomKeyRequest> {
|
||||||
|
return cryptoStore.getIncomingRoomKeyRequests()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getGossipingEventsTrail(): List<Event> {
|
||||||
|
return cryptoStore.getGossipingEventsTrail()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -361,13 +361,13 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
|
||||||
|
|
||||||
// Handle cross signing keys update
|
// Handle cross signing keys update
|
||||||
val masterKey = response.masterKeys?.get(userId)?.toCryptoModel().also {
|
val masterKey = response.masterKeys?.get(userId)?.toCryptoModel().also {
|
||||||
Timber.d("## CrossSigning : Got keys for $userId : MSK ${it?.unpaddedBase64PublicKey}")
|
Timber.v("## CrossSigning : Got keys for $userId : MSK ${it?.unpaddedBase64PublicKey}")
|
||||||
}
|
}
|
||||||
val selfSigningKey = response.selfSigningKeys?.get(userId)?.toCryptoModel()?.also {
|
val selfSigningKey = response.selfSigningKeys?.get(userId)?.toCryptoModel()?.also {
|
||||||
Timber.d("## CrossSigning : Got keys for $userId : SSK ${it.unpaddedBase64PublicKey}")
|
Timber.v("## CrossSigning : Got keys for $userId : SSK ${it.unpaddedBase64PublicKey}")
|
||||||
}
|
}
|
||||||
val userSigningKey = response.userSigningKeys?.get(userId)?.toCryptoModel()?.also {
|
val userSigningKey = response.userSigningKeys?.get(userId)?.toCryptoModel()?.also {
|
||||||
Timber.d("## CrossSigning : Got keys for $userId : USK ${it.unpaddedBase64PublicKey}")
|
Timber.v("## CrossSigning : Got keys for $userId : USK ${it.unpaddedBase64PublicKey}")
|
||||||
}
|
}
|
||||||
cryptoStore.storeUserCrossSigningKeys(
|
cryptoStore.storeUserCrossSigningKeys(
|
||||||
userId,
|
userId,
|
||||||
|
|
|
@ -0,0 +1,45 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2020 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.matrix.android.internal.crypto
|
||||||
|
|
||||||
|
enum class GossipRequestType {
|
||||||
|
KEY,
|
||||||
|
SECRET
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class GossipingRequestState {
|
||||||
|
NONE,
|
||||||
|
PENDING,
|
||||||
|
REJECTED,
|
||||||
|
ACCEPTING,
|
||||||
|
ACCEPTED,
|
||||||
|
FAILED_TO_ACCEPTED,
|
||||||
|
// USER_REJECTED,
|
||||||
|
UNABLE_TO_PROCESS,
|
||||||
|
CANCELLED_BY_REQUESTER,
|
||||||
|
RE_REQUESTED
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class OutgoingGossipingRequestState {
|
||||||
|
UNSENT,
|
||||||
|
SENDING,
|
||||||
|
SENT,
|
||||||
|
CANCELLING,
|
||||||
|
CANCELLED,
|
||||||
|
FAILED_TO_SEND,
|
||||||
|
FAILED_TO_CANCEL
|
||||||
|
}
|
|
@ -0,0 +1,57 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2020 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.matrix.android.internal.crypto
|
||||||
|
|
||||||
|
import androidx.work.BackoffPolicy
|
||||||
|
import androidx.work.Data
|
||||||
|
import androidx.work.ExistingWorkPolicy
|
||||||
|
import androidx.work.ListenableWorker
|
||||||
|
import androidx.work.OneTimeWorkRequest
|
||||||
|
import im.vector.matrix.android.api.util.Cancelable
|
||||||
|
import im.vector.matrix.android.internal.di.WorkManagerProvider
|
||||||
|
import im.vector.matrix.android.internal.session.SessionScope
|
||||||
|
import im.vector.matrix.android.internal.util.CancelableWork
|
||||||
|
import im.vector.matrix.android.internal.worker.startChain
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@SessionScope
|
||||||
|
internal class GossipingWorkManager @Inject constructor(
|
||||||
|
private val workManagerProvider: WorkManagerProvider
|
||||||
|
) {
|
||||||
|
|
||||||
|
inline fun <reified W : ListenableWorker> createWork(data: Data, startChain: Boolean): OneTimeWorkRequest {
|
||||||
|
return workManagerProvider.matrixOneTimeWorkRequestBuilder<W>()
|
||||||
|
.setConstraints(WorkManagerProvider.workConstraints)
|
||||||
|
.startChain(startChain)
|
||||||
|
.setInputData(data)
|
||||||
|
.setBackoffCriteria(BackoffPolicy.LINEAR, 10_000L, TimeUnit.MILLISECONDS)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prevent sending queue to stay broken after app restart
|
||||||
|
// The unique queue id will stay the same as long as this object is instanciated
|
||||||
|
val queueSuffixApp = System.currentTimeMillis()
|
||||||
|
|
||||||
|
fun postWork(workRequest: OneTimeWorkRequest, policy: ExistingWorkPolicy = ExistingWorkPolicy.APPEND): Cancelable {
|
||||||
|
workManagerProvider.workManager
|
||||||
|
.beginUniqueWork(this::class.java.name + "_$queueSuffixApp", policy, workRequest)
|
||||||
|
.enqueue()
|
||||||
|
|
||||||
|
return CancelableWork(workManagerProvider.workManager, workRequest.id)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,401 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2019 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.matrix.android.internal.crypto
|
||||||
|
|
||||||
|
import im.vector.matrix.android.api.auth.data.Credentials
|
||||||
|
import im.vector.matrix.android.api.crypto.MXCryptoConfig
|
||||||
|
import im.vector.matrix.android.api.session.crypto.crosssigning.KEYBACKUP_SECRET_SSSS_NAME
|
||||||
|
import im.vector.matrix.android.api.session.crypto.crosssigning.SELF_SIGNING_KEY_SSSS_NAME
|
||||||
|
import im.vector.matrix.android.api.session.crypto.crosssigning.USER_SIGNING_KEY_SSSS_NAME
|
||||||
|
import im.vector.matrix.android.api.session.crypto.keyshare.GossipingRequestListener
|
||||||
|
import im.vector.matrix.android.api.session.events.model.Event
|
||||||
|
import im.vector.matrix.android.api.session.events.model.EventType
|
||||||
|
import im.vector.matrix.android.api.session.events.model.toModel
|
||||||
|
import im.vector.matrix.android.internal.crypto.crosssigning.toBase64NoPadding
|
||||||
|
import im.vector.matrix.android.internal.crypto.keysbackup.util.extractCurveKeyFromRecoveryKey
|
||||||
|
import im.vector.matrix.android.internal.crypto.model.rest.GossipingDefaultContent
|
||||||
|
import im.vector.matrix.android.internal.crypto.model.rest.GossipingToDeviceObject
|
||||||
|
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
|
||||||
|
import im.vector.matrix.android.internal.di.SessionId
|
||||||
|
import im.vector.matrix.android.internal.session.SessionScope
|
||||||
|
import im.vector.matrix.android.internal.worker.WorkerParamsFactory
|
||||||
|
import timber.log.Timber
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@SessionScope
|
||||||
|
internal class IncomingGossipingRequestManager @Inject constructor(
|
||||||
|
@SessionId private val sessionId: String,
|
||||||
|
private val credentials: Credentials,
|
||||||
|
private val cryptoStore: IMXCryptoStore,
|
||||||
|
private val cryptoConfig: MXCryptoConfig,
|
||||||
|
private val gossipingWorkManager: GossipingWorkManager,
|
||||||
|
private val roomDecryptorProvider: RoomDecryptorProvider) {
|
||||||
|
|
||||||
|
// list of IncomingRoomKeyRequests/IncomingRoomKeyRequestCancellations
|
||||||
|
// we received in the current sync.
|
||||||
|
private val receivedGossipingRequests = ArrayList<IncomingShareRequestCommon>()
|
||||||
|
private val receivedRequestCancellations = ArrayList<IncomingRequestCancellation>()
|
||||||
|
|
||||||
|
// the listeners
|
||||||
|
private val gossipingRequestListeners: MutableSet<GossipingRequestListener> = HashSet()
|
||||||
|
|
||||||
|
init {
|
||||||
|
receivedGossipingRequests.addAll(cryptoStore.getPendingIncomingGossipingRequests())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recently verified devices (map of deviceId and timestamp)
|
||||||
|
private val recentlyVerifiedDevices = HashMap<String, Long>()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when a session has been verified.
|
||||||
|
* This information can be used by the manager to decide whether or not to fullfil gossiping requests
|
||||||
|
*/
|
||||||
|
fun onVerificationCompleteForDevice(deviceId: String) {
|
||||||
|
// For now we just keep an in memory cache
|
||||||
|
synchronized(recentlyVerifiedDevices) {
|
||||||
|
recentlyVerifiedDevices[deviceId] = System.currentTimeMillis()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun hasBeenVerifiedLessThanFiveMinutesFromNow(deviceId: String): Boolean {
|
||||||
|
val verifTimestamp: Long?
|
||||||
|
synchronized(recentlyVerifiedDevices) {
|
||||||
|
verifTimestamp = recentlyVerifiedDevices[deviceId]
|
||||||
|
}
|
||||||
|
if (verifTimestamp == null) return false
|
||||||
|
|
||||||
|
val age = System.currentTimeMillis() - verifTimestamp
|
||||||
|
|
||||||
|
return age < FIVE_MINUTES_IN_MILLIS
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when we get an m.room_key_request event
|
||||||
|
* It must be called on CryptoThread
|
||||||
|
*
|
||||||
|
* @param event the announcement event.
|
||||||
|
*/
|
||||||
|
fun onGossipingRequestEvent(event: Event) {
|
||||||
|
Timber.v("## GOSSIP onGossipingRequestEvent type ${event.type} from user ${event.senderId}")
|
||||||
|
val roomKeyShare = event.getClearContent().toModel<GossipingDefaultContent>()
|
||||||
|
val ageLocalTs = event.unsignedData?.age?.let { System.currentTimeMillis() - it }
|
||||||
|
when (roomKeyShare?.action) {
|
||||||
|
GossipingToDeviceObject.ACTION_SHARE_REQUEST -> {
|
||||||
|
if (event.getClearType() == EventType.REQUEST_SECRET) {
|
||||||
|
IncomingSecretShareRequest.fromEvent(event)?.let {
|
||||||
|
if (event.senderId == credentials.userId && it.deviceId == credentials.deviceId) {
|
||||||
|
// ignore, it was sent by me as *
|
||||||
|
Timber.v("## GOSSIP onGossipingRequestEvent type ${event.type} ignore remote echo")
|
||||||
|
} else {
|
||||||
|
// save in DB
|
||||||
|
cryptoStore.storeIncomingGossipingRequest(it, ageLocalTs)
|
||||||
|
receivedGossipingRequests.add(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (event.getClearType() == EventType.ROOM_KEY_REQUEST) {
|
||||||
|
IncomingRoomKeyRequest.fromEvent(event)?.let {
|
||||||
|
if (event.senderId == credentials.userId && it.deviceId == credentials.deviceId) {
|
||||||
|
// ignore, it was sent by me as *
|
||||||
|
Timber.v("## GOSSIP onGossipingRequestEvent type ${event.type} ignore remote echo")
|
||||||
|
} else {
|
||||||
|
cryptoStore.storeIncomingGossipingRequest(it, ageLocalTs)
|
||||||
|
receivedGossipingRequests.add(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
GossipingToDeviceObject.ACTION_SHARE_CANCELLATION -> {
|
||||||
|
IncomingRequestCancellation.fromEvent(event)?.let {
|
||||||
|
receivedRequestCancellations.add(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
Timber.e("## GOSSIP onGossipingRequestEvent() : unsupported action ${roomKeyShare?.action}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process any m.room_key_request or m.secret.request events which were queued up during the
|
||||||
|
* current sync.
|
||||||
|
* It must be called on CryptoThread
|
||||||
|
*/
|
||||||
|
fun processReceivedGossipingRequests() {
|
||||||
|
val roomKeyRequestsToProcess = receivedGossipingRequests.toList()
|
||||||
|
receivedGossipingRequests.clear()
|
||||||
|
for (request in roomKeyRequestsToProcess) {
|
||||||
|
if (request is IncomingRoomKeyRequest) {
|
||||||
|
processIncomingRoomKeyRequest(request)
|
||||||
|
} else if (request is IncomingSecretShareRequest) {
|
||||||
|
processIncomingSecretShareRequest(request)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var receivedRequestCancellations: List<IncomingRequestCancellation>? = null
|
||||||
|
|
||||||
|
synchronized(this.receivedRequestCancellations) {
|
||||||
|
if (this.receivedRequestCancellations.isNotEmpty()) {
|
||||||
|
receivedRequestCancellations = this.receivedRequestCancellations.toList()
|
||||||
|
this.receivedRequestCancellations.clear()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
receivedRequestCancellations?.forEach { request ->
|
||||||
|
Timber.v("## GOSSIP processReceivedGossipingRequests() : m.room_key_request cancellation $request")
|
||||||
|
// we should probably only notify the app of cancellations we told it
|
||||||
|
// about, but we don't currently have a record of that, so we just pass
|
||||||
|
// everything through.
|
||||||
|
if (request.userId == credentials.userId && request.deviceId == credentials.deviceId) {
|
||||||
|
// ignore remote echo
|
||||||
|
return@forEach
|
||||||
|
}
|
||||||
|
val matchingIncoming = cryptoStore.getIncomingRoomKeyRequest(request.userId ?: "", request.deviceId ?: "", request.requestId ?: "")
|
||||||
|
if (matchingIncoming == null) {
|
||||||
|
// ignore that?
|
||||||
|
return@forEach
|
||||||
|
} else {
|
||||||
|
// If it was accepted from this device, keep the information, do not mark as cancelled
|
||||||
|
if (matchingIncoming.state != GossipingRequestState.ACCEPTED) {
|
||||||
|
onRoomKeyRequestCancellation(request)
|
||||||
|
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.CANCELLED_BY_REQUESTER)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun processIncomingRoomKeyRequest(request: IncomingRoomKeyRequest) {
|
||||||
|
val userId = request.userId
|
||||||
|
val deviceId = request.deviceId
|
||||||
|
val body = request.requestBody
|
||||||
|
val roomId = body!!.roomId
|
||||||
|
val alg = body.algorithm
|
||||||
|
|
||||||
|
Timber.v("## GOSSIP processIncomingRoomKeyRequest from $userId:$deviceId for $roomId / ${body.sessionId} id ${request.requestId}")
|
||||||
|
if (userId == null || credentials.userId != userId) {
|
||||||
|
// TODO: determine if we sent this device the keys already: in
|
||||||
|
Timber.w("## GOSSIP processReceivedGossipingRequests() : Ignoring room key request from other user for now")
|
||||||
|
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// TODO: should we queue up requests we don't yet have keys for, in case they turn up later?
|
||||||
|
// if we don't have a decryptor for this room/alg, we don't have
|
||||||
|
// the keys for the requested events, and can drop the requests.
|
||||||
|
val decryptor = roomDecryptorProvider.getRoomDecryptor(roomId, alg)
|
||||||
|
if (null == decryptor) {
|
||||||
|
Timber.w("## GOSSIP processReceivedGossipingRequests() : room key request for unknown $alg in room $roomId")
|
||||||
|
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!decryptor.hasKeysForKeyRequest(request)) {
|
||||||
|
Timber.w("## GOSSIP processReceivedGossipingRequests() : room key request for unknown session ${body.sessionId!!}")
|
||||||
|
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (credentials.deviceId == deviceId && credentials.userId == userId) {
|
||||||
|
Timber.v("## GOSSIP processReceivedGossipingRequests() : oneself device - ignored")
|
||||||
|
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
request.share = Runnable {
|
||||||
|
decryptor.shareKeysWithDevice(request)
|
||||||
|
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.ACCEPTED)
|
||||||
|
}
|
||||||
|
request.ignore = Runnable {
|
||||||
|
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED)
|
||||||
|
}
|
||||||
|
// if the device is verified already, share the keys
|
||||||
|
val device = cryptoStore.getUserDevice(userId, deviceId!!)
|
||||||
|
if (device != null) {
|
||||||
|
if (device.isVerified) {
|
||||||
|
Timber.v("## GOSSIP processReceivedGossipingRequests() : device is already verified: sharing keys")
|
||||||
|
request.share?.run()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (device.isBlocked) {
|
||||||
|
Timber.v("## GOSSIP processReceivedGossipingRequests() : device is blocked -> ignored")
|
||||||
|
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// As per config we automatically discard untrusted devices request
|
||||||
|
if (cryptoConfig.discardRoomKeyRequestsFromUntrustedDevices) {
|
||||||
|
Timber.v("## processReceivedGossipingRequests() : discardRoomKeyRequestsFromUntrustedDevices")
|
||||||
|
// At this point the device is unknown, we don't want to bother user with that
|
||||||
|
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pass to application layer to decide what to do
|
||||||
|
onRoomKeyRequest(request)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun processIncomingSecretShareRequest(request: IncomingSecretShareRequest) {
|
||||||
|
val secretName = request.secretName ?: return Unit.also {
|
||||||
|
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED)
|
||||||
|
Timber.v("## GOSSIP processIncomingSecretShareRequest() : Missing secret name")
|
||||||
|
}
|
||||||
|
|
||||||
|
val userId = request.userId
|
||||||
|
if (userId == null || credentials.userId != userId) {
|
||||||
|
Timber.e("## GOSSIP processIncomingSecretShareRequest() : Ignoring secret share request from other users")
|
||||||
|
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val deviceId = request.deviceId
|
||||||
|
?: return Unit.also {
|
||||||
|
Timber.e("## GOSSIP processIncomingSecretShareRequest() : Malformed request, no ")
|
||||||
|
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED)
|
||||||
|
}
|
||||||
|
|
||||||
|
val device = cryptoStore.getUserDevice(userId, deviceId)
|
||||||
|
?: return Unit.also {
|
||||||
|
Timber.e("## GOSSIP processIncomingSecretShareRequest() : Received secret share request from unknown device ${request.deviceId}")
|
||||||
|
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!device.isVerified || device.isBlocked) {
|
||||||
|
Timber.v("## GOSSIP processIncomingSecretShareRequest() : Ignoring secret share request from untrusted/blocked session $device")
|
||||||
|
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val isDeviceLocallyVerified = cryptoStore.getUserDevice(userId, deviceId)?.trustLevel?.isLocallyVerified()
|
||||||
|
|
||||||
|
// Should SDK always Silently reject any request for the master key?
|
||||||
|
when (secretName) {
|
||||||
|
SELF_SIGNING_KEY_SSSS_NAME -> cryptoStore.getCrossSigningPrivateKeys()?.selfSigned
|
||||||
|
USER_SIGNING_KEY_SSSS_NAME -> cryptoStore.getCrossSigningPrivateKeys()?.user
|
||||||
|
KEYBACKUP_SECRET_SSSS_NAME -> cryptoStore.getKeyBackupRecoveryKeyInfo()?.recoveryKey
|
||||||
|
?.let {
|
||||||
|
extractCurveKeyFromRecoveryKey(it)?.toBase64NoPadding()
|
||||||
|
}
|
||||||
|
else -> null
|
||||||
|
}?.let { secretValue ->
|
||||||
|
Timber.i("## GOSSIP processIncomingSecretShareRequest() : Sharing secret $secretName with $device locally trusted")
|
||||||
|
if (isDeviceLocallyVerified == true && hasBeenVerifiedLessThanFiveMinutesFromNow(deviceId)) {
|
||||||
|
val params = SendGossipWorker.Params(
|
||||||
|
sessionId = sessionId,
|
||||||
|
secretValue = secretValue,
|
||||||
|
request = request
|
||||||
|
)
|
||||||
|
|
||||||
|
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.ACCEPTING)
|
||||||
|
val workRequest = gossipingWorkManager.createWork<SendGossipWorker>(WorkerParamsFactory.toData(params), true)
|
||||||
|
gossipingWorkManager.postWork(workRequest)
|
||||||
|
} else {
|
||||||
|
Timber.v("## GOSSIP processIncomingSecretShareRequest() : Can't share secret $secretName with $device, verification too old")
|
||||||
|
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
Timber.v("## GOSSIP processIncomingSecretShareRequest() : $secretName unknown at SDK level, asking to app layer")
|
||||||
|
|
||||||
|
request.ignore = Runnable {
|
||||||
|
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED)
|
||||||
|
}
|
||||||
|
|
||||||
|
request.share = { secretValue ->
|
||||||
|
|
||||||
|
val params = SendGossipWorker.Params(
|
||||||
|
sessionId = userId,
|
||||||
|
secretValue = secretValue,
|
||||||
|
request = request
|
||||||
|
)
|
||||||
|
|
||||||
|
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.ACCEPTING)
|
||||||
|
val workRequest = gossipingWorkManager.createWork<SendGossipWorker>(WorkerParamsFactory.toData(params), true)
|
||||||
|
gossipingWorkManager.postWork(workRequest)
|
||||||
|
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.ACCEPTED)
|
||||||
|
}
|
||||||
|
|
||||||
|
onShareRequest(request)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dispatch onRoomKeyRequest
|
||||||
|
*
|
||||||
|
* @param request the request
|
||||||
|
*/
|
||||||
|
private fun onRoomKeyRequest(request: IncomingRoomKeyRequest) {
|
||||||
|
synchronized(gossipingRequestListeners) {
|
||||||
|
for (listener in gossipingRequestListeners) {
|
||||||
|
try {
|
||||||
|
listener.onRoomKeyRequest(request)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.e(e, "## onRoomKeyRequest() failed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ask for a value to the listeners, and take the first one
|
||||||
|
*/
|
||||||
|
private fun onShareRequest(request: IncomingSecretShareRequest) {
|
||||||
|
synchronized(gossipingRequestListeners) {
|
||||||
|
for (listener in gossipingRequestListeners) {
|
||||||
|
try {
|
||||||
|
if (listener.onSecretShareRequest(request)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.e(e, "## GOSSIP onRoomKeyRequest() failed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Not handled, ignore
|
||||||
|
request.ignore?.run()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A room key request cancellation has been received.
|
||||||
|
*
|
||||||
|
* @param request the cancellation request
|
||||||
|
*/
|
||||||
|
private fun onRoomKeyRequestCancellation(request: IncomingRequestCancellation) {
|
||||||
|
synchronized(gossipingRequestListeners) {
|
||||||
|
for (listener in gossipingRequestListeners) {
|
||||||
|
try {
|
||||||
|
listener.onRoomKeyRequestCancellation(request)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.e(e, "## GOSSIP onRoomKeyRequestCancellation() failed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addRoomKeysRequestListener(listener: GossipingRequestListener) {
|
||||||
|
synchronized(gossipingRequestListeners) {
|
||||||
|
gossipingRequestListeners.add(listener)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeRoomKeysRequestListener(listener: GossipingRequestListener) {
|
||||||
|
synchronized(gossipingRequestListeners) {
|
||||||
|
gossipingRequestListeners.remove(listener)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val FIVE_MINUTES_IN_MILLIS = 5 * 60 * 1000
|
||||||
|
}
|
||||||
|
}
|
|
@ -18,12 +18,12 @@ package im.vector.matrix.android.internal.crypto
|
||||||
|
|
||||||
import im.vector.matrix.android.api.session.events.model.Event
|
import im.vector.matrix.android.api.session.events.model.Event
|
||||||
import im.vector.matrix.android.api.session.events.model.toModel
|
import im.vector.matrix.android.api.session.events.model.toModel
|
||||||
import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyShareCancellation
|
import im.vector.matrix.android.internal.crypto.model.rest.ShareRequestCancellation
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* IncomingRoomKeyRequestCancellation describes the incoming room key cancellation.
|
* IncomingRequestCancellation describes the incoming room key cancellation.
|
||||||
*/
|
*/
|
||||||
data class IncomingRoomKeyRequestCancellation(
|
data class IncomingRequestCancellation(
|
||||||
/**
|
/**
|
||||||
* The user id
|
* The user id
|
||||||
*/
|
*/
|
||||||
|
@ -37,22 +37,24 @@ data class IncomingRoomKeyRequestCancellation(
|
||||||
/**
|
/**
|
||||||
* The request id
|
* The request id
|
||||||
*/
|
*/
|
||||||
override val requestId: String? = null
|
override val requestId: String? = null,
|
||||||
) : IncomingRoomKeyRequestCommon {
|
override val localCreationTimestamp: Long?
|
||||||
|
) : IncomingShareRequestCommon {
|
||||||
companion object {
|
companion object {
|
||||||
/**
|
/**
|
||||||
* Factory
|
* Factory
|
||||||
*
|
*
|
||||||
* @param event the event
|
* @param event the event
|
||||||
*/
|
*/
|
||||||
fun fromEvent(event: Event): IncomingRoomKeyRequestCancellation? {
|
fun fromEvent(event: Event): IncomingRequestCancellation? {
|
||||||
return event.getClearContent()
|
return event.getClearContent()
|
||||||
.toModel<RoomKeyShareCancellation>()
|
.toModel<ShareRequestCancellation>()
|
||||||
?.let {
|
?.let {
|
||||||
IncomingRoomKeyRequestCancellation(
|
IncomingRequestCancellation(
|
||||||
userId = event.senderId,
|
userId = event.senderId,
|
||||||
deviceId = it.requestingDeviceId,
|
deviceId = it.requestingDeviceId,
|
||||||
requestId = it.requestId
|
requestId = it.requestId,
|
||||||
|
localCreationTimestamp = event.ageLocalTs ?: System.currentTimeMillis()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -46,6 +46,8 @@ data class IncomingRoomKeyRequest(
|
||||||
*/
|
*/
|
||||||
val requestBody: RoomKeyRequestBody? = null,
|
val requestBody: RoomKeyRequestBody? = null,
|
||||||
|
|
||||||
|
val state: GossipingRequestState = GossipingRequestState.NONE,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The runnable to call to accept to share the keys
|
* The runnable to call to accept to share the keys
|
||||||
*/
|
*/
|
||||||
|
@ -56,8 +58,9 @@ data class IncomingRoomKeyRequest(
|
||||||
* The runnable to call to ignore the key share request.
|
* The runnable to call to ignore the key share request.
|
||||||
*/
|
*/
|
||||||
@Transient
|
@Transient
|
||||||
var ignore: Runnable? = null
|
var ignore: Runnable? = null,
|
||||||
) : IncomingRoomKeyRequestCommon {
|
override val localCreationTimestamp: Long?
|
||||||
|
) : IncomingShareRequestCommon {
|
||||||
companion object {
|
companion object {
|
||||||
/**
|
/**
|
||||||
* Factory
|
* Factory
|
||||||
|
@ -72,7 +75,8 @@ data class IncomingRoomKeyRequest(
|
||||||
userId = event.senderId,
|
userId = event.senderId,
|
||||||
deviceId = it.requestingDeviceId,
|
deviceId = it.requestingDeviceId,
|
||||||
requestId = it.requestId,
|
requestId = it.requestId,
|
||||||
requestBody = it.body ?: RoomKeyRequestBody()
|
requestBody = it.body ?: RoomKeyRequestBody(),
|
||||||
|
localCreationTimestamp = event.ageLocalTs ?: System.currentTimeMillis()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,203 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2019 New Vector Ltd
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package im.vector.matrix.android.internal.crypto
|
|
||||||
|
|
||||||
import im.vector.matrix.android.api.auth.data.Credentials
|
|
||||||
import im.vector.matrix.android.api.session.crypto.keyshare.RoomKeysRequestListener
|
|
||||||
import im.vector.matrix.android.api.session.events.model.Event
|
|
||||||
import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyShare
|
|
||||||
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
|
|
||||||
import im.vector.matrix.android.internal.session.SessionScope
|
|
||||||
import timber.log.Timber
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
@SessionScope
|
|
||||||
internal class IncomingRoomKeyRequestManager @Inject constructor(
|
|
||||||
private val credentials: Credentials,
|
|
||||||
private val cryptoStore: IMXCryptoStore,
|
|
||||||
private val roomDecryptorProvider: RoomDecryptorProvider) {
|
|
||||||
|
|
||||||
// list of IncomingRoomKeyRequests/IncomingRoomKeyRequestCancellations
|
|
||||||
// we received in the current sync.
|
|
||||||
private val receivedRoomKeyRequests = ArrayList<IncomingRoomKeyRequest>()
|
|
||||||
private val receivedRoomKeyRequestCancellations = ArrayList<IncomingRoomKeyRequestCancellation>()
|
|
||||||
|
|
||||||
// the listeners
|
|
||||||
private val roomKeysRequestListeners: MutableSet<RoomKeysRequestListener> = HashSet()
|
|
||||||
|
|
||||||
init {
|
|
||||||
receivedRoomKeyRequests.addAll(cryptoStore.getPendingIncomingRoomKeyRequests())
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called when we get an m.room_key_request event
|
|
||||||
* It must be called on CryptoThread
|
|
||||||
*
|
|
||||||
* @param event the announcement event.
|
|
||||||
*/
|
|
||||||
fun onRoomKeyRequestEvent(event: Event) {
|
|
||||||
when (val roomKeyShareAction = event.getClearContent()?.get("action") as? String) {
|
|
||||||
RoomKeyShare.ACTION_SHARE_REQUEST -> IncomingRoomKeyRequest.fromEvent(event)?.let { receivedRoomKeyRequests.add(it) }
|
|
||||||
RoomKeyShare.ACTION_SHARE_CANCELLATION -> IncomingRoomKeyRequestCancellation.fromEvent(event)?.let { receivedRoomKeyRequestCancellations.add(it) }
|
|
||||||
else -> Timber.e("## onRoomKeyRequestEvent() : unsupported action $roomKeyShareAction")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Process any m.room_key_request events which were queued up during the
|
|
||||||
* current sync.
|
|
||||||
* It must be called on CryptoThread
|
|
||||||
*/
|
|
||||||
fun processReceivedRoomKeyRequests() {
|
|
||||||
val roomKeyRequestsToProcess = receivedRoomKeyRequests.toList()
|
|
||||||
receivedRoomKeyRequests.clear()
|
|
||||||
for (request in roomKeyRequestsToProcess) {
|
|
||||||
val userId = request.userId
|
|
||||||
val deviceId = request.deviceId
|
|
||||||
val body = request.requestBody
|
|
||||||
val roomId = body!!.roomId
|
|
||||||
val alg = body.algorithm
|
|
||||||
|
|
||||||
Timber.v("m.room_key_request from $userId:$deviceId for $roomId / ${body.sessionId} id ${request.requestId}")
|
|
||||||
if (userId == null || credentials.userId != userId) {
|
|
||||||
// TODO: determine if we sent this device the keys already: in
|
|
||||||
Timber.w("## processReceivedRoomKeyRequests() : Ignoring room key request from other user for now")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// TODO: should we queue up requests we don't yet have keys for, in case they turn up later?
|
|
||||||
// if we don't have a decryptor for this room/alg, we don't have
|
|
||||||
// the keys for the requested events, and can drop the requests.
|
|
||||||
val decryptor = roomDecryptorProvider.getRoomDecryptor(roomId, alg)
|
|
||||||
if (null == decryptor) {
|
|
||||||
Timber.w("## processReceivedRoomKeyRequests() : room key request for unknown $alg in room $roomId")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if (!decryptor.hasKeysForKeyRequest(request)) {
|
|
||||||
Timber.w("## processReceivedRoomKeyRequests() : room key request for unknown session ${body.sessionId!!}")
|
|
||||||
cryptoStore.deleteIncomingRoomKeyRequest(request)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if (credentials.deviceId == deviceId && credentials.userId == userId) {
|
|
||||||
Timber.v("## processReceivedRoomKeyRequests() : oneself device - ignored")
|
|
||||||
cryptoStore.deleteIncomingRoomKeyRequest(request)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
request.share = Runnable {
|
|
||||||
decryptor.shareKeysWithDevice(request)
|
|
||||||
cryptoStore.deleteIncomingRoomKeyRequest(request)
|
|
||||||
}
|
|
||||||
request.ignore = Runnable {
|
|
||||||
cryptoStore.deleteIncomingRoomKeyRequest(request)
|
|
||||||
}
|
|
||||||
// if the device is verified already, share the keys
|
|
||||||
val device = cryptoStore.getUserDevice(userId, deviceId!!)
|
|
||||||
if (device != null) {
|
|
||||||
if (device.isVerified) {
|
|
||||||
Timber.v("## processReceivedRoomKeyRequests() : device is already verified: sharing keys")
|
|
||||||
cryptoStore.deleteIncomingRoomKeyRequest(request)
|
|
||||||
request.share?.run()
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if (device.isBlocked) {
|
|
||||||
Timber.v("## processReceivedRoomKeyRequests() : device is blocked -> ignored")
|
|
||||||
cryptoStore.deleteIncomingRoomKeyRequest(request)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If cross signing is available on account we automatically discard untrust devices request
|
|
||||||
if (cryptoStore.getMyCrossSigningInfo() != null) {
|
|
||||||
// At this point the device is unknown, we don't want to bother user with that
|
|
||||||
cryptoStore.deleteIncomingRoomKeyRequest(request)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
cryptoStore.storeIncomingRoomKeyRequest(request)
|
|
||||||
onRoomKeyRequest(request)
|
|
||||||
}
|
|
||||||
|
|
||||||
var receivedRoomKeyRequestCancellations: List<IncomingRoomKeyRequestCancellation>? = null
|
|
||||||
|
|
||||||
synchronized(this.receivedRoomKeyRequestCancellations) {
|
|
||||||
if (this.receivedRoomKeyRequestCancellations.isNotEmpty()) {
|
|
||||||
receivedRoomKeyRequestCancellations = this.receivedRoomKeyRequestCancellations.toList()
|
|
||||||
this.receivedRoomKeyRequestCancellations.clear()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (null != receivedRoomKeyRequestCancellations) {
|
|
||||||
for (request in receivedRoomKeyRequestCancellations!!) {
|
|
||||||
Timber.v("## ## processReceivedRoomKeyRequests() : m.room_key_request cancellation for " + request.userId
|
|
||||||
+ ":" + request.deviceId + " id " + request.requestId)
|
|
||||||
|
|
||||||
// we should probably only notify the app of cancellations we told it
|
|
||||||
// about, but we don't currently have a record of that, so we just pass
|
|
||||||
// everything through.
|
|
||||||
onRoomKeyRequestCancellation(request)
|
|
||||||
cryptoStore.deleteIncomingRoomKeyRequest(request)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Dispatch onRoomKeyRequest
|
|
||||||
*
|
|
||||||
* @param request the request
|
|
||||||
*/
|
|
||||||
private fun onRoomKeyRequest(request: IncomingRoomKeyRequest) {
|
|
||||||
synchronized(roomKeysRequestListeners) {
|
|
||||||
for (listener in roomKeysRequestListeners) {
|
|
||||||
try {
|
|
||||||
listener.onRoomKeyRequest(request)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Timber.e(e, "## onRoomKeyRequest() failed")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A room key request cancellation has been received.
|
|
||||||
*
|
|
||||||
* @param request the cancellation request
|
|
||||||
*/
|
|
||||||
private fun onRoomKeyRequestCancellation(request: IncomingRoomKeyRequestCancellation) {
|
|
||||||
synchronized(roomKeysRequestListeners) {
|
|
||||||
for (listener in roomKeysRequestListeners) {
|
|
||||||
try {
|
|
||||||
listener.onRoomKeyRequestCancellation(request)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Timber.e(e, "## onRoomKeyRequestCancellation() failed")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun addRoomKeysRequestListener(listener: RoomKeysRequestListener) {
|
|
||||||
synchronized(roomKeysRequestListeners) {
|
|
||||||
roomKeysRequestListeners.add(listener)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun removeRoomKeysRequestListener(listener: RoomKeysRequestListener) {
|
|
||||||
synchronized(roomKeysRequestListeners) {
|
|
||||||
roomKeysRequestListeners.remove(listener)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,82 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2020 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.matrix.android.internal.crypto
|
||||||
|
|
||||||
|
import im.vector.matrix.android.api.session.events.model.Event
|
||||||
|
import im.vector.matrix.android.api.session.events.model.toModel
|
||||||
|
import im.vector.matrix.android.internal.crypto.model.rest.SecretShareRequest
|
||||||
|
|
||||||
|
/**
|
||||||
|
* IncomingRoomKeyRequest class defines the incoming room keys request.
|
||||||
|
*/
|
||||||
|
data class IncomingSecretShareRequest(
|
||||||
|
/**
|
||||||
|
* The user id
|
||||||
|
*/
|
||||||
|
override val userId: String? = null,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The device id
|
||||||
|
*/
|
||||||
|
override val deviceId: String? = null,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The request id
|
||||||
|
*/
|
||||||
|
override val requestId: String? = null,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The request body
|
||||||
|
*/
|
||||||
|
val secretName: String? = null,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The runnable to call to accept to share the keys
|
||||||
|
*/
|
||||||
|
@Transient
|
||||||
|
var share: ((String) -> Unit)? = null,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The runnable to call to ignore the key share request.
|
||||||
|
*/
|
||||||
|
@Transient
|
||||||
|
var ignore: Runnable? = null,
|
||||||
|
|
||||||
|
override val localCreationTimestamp: Long?
|
||||||
|
|
||||||
|
) : IncomingShareRequestCommon {
|
||||||
|
companion object {
|
||||||
|
/**
|
||||||
|
* Factory
|
||||||
|
*
|
||||||
|
* @param event the event
|
||||||
|
*/
|
||||||
|
fun fromEvent(event: Event): IncomingSecretShareRequest? {
|
||||||
|
return event.getClearContent()
|
||||||
|
.toModel<SecretShareRequest>()
|
||||||
|
?.let {
|
||||||
|
IncomingSecretShareRequest(
|
||||||
|
userId = event.senderId,
|
||||||
|
deviceId = it.requestingDeviceId,
|
||||||
|
requestId = it.requestId,
|
||||||
|
secretName = it.secretName,
|
||||||
|
localCreationTimestamp = event.ageLocalTs ?: System.currentTimeMillis()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -16,7 +16,7 @@
|
||||||
|
|
||||||
package im.vector.matrix.android.internal.crypto
|
package im.vector.matrix.android.internal.crypto
|
||||||
|
|
||||||
interface IncomingRoomKeyRequestCommon {
|
interface IncomingShareRequestCommon {
|
||||||
/**
|
/**
|
||||||
* The user id
|
* The user id
|
||||||
*/
|
*/
|
||||||
|
@ -31,4 +31,6 @@ interface IncomingRoomKeyRequestCommon {
|
||||||
* The request id
|
* The request id
|
||||||
*/
|
*/
|
||||||
val requestId: String?
|
val requestId: String?
|
||||||
|
|
||||||
|
val localCreationTimestamp: Long?
|
||||||
}
|
}
|
|
@ -59,9 +59,6 @@ internal class MXOlmDevice @Inject constructor(
|
||||||
var deviceEd25519Key: String? = null
|
var deviceEd25519Key: String? = null
|
||||||
private set
|
private set
|
||||||
|
|
||||||
// The OLM lib account instance.
|
|
||||||
private var olmAccount: OlmAccount? = null
|
|
||||||
|
|
||||||
// The OLM lib utility instance.
|
// The OLM lib utility instance.
|
||||||
private var olmUtility: OlmUtility? = null
|
private var olmUtility: OlmUtility? = null
|
||||||
|
|
||||||
|
@ -86,19 +83,10 @@ internal class MXOlmDevice @Inject constructor(
|
||||||
|
|
||||||
init {
|
init {
|
||||||
// Retrieve the account from the store
|
// Retrieve the account from the store
|
||||||
olmAccount = store.getAccount()
|
try {
|
||||||
|
store.getOrCreateOlmAccount()
|
||||||
if (null == olmAccount) {
|
} catch (e: Exception) {
|
||||||
Timber.v("MXOlmDevice : create a new olm account")
|
Timber.e(e, "MXOlmDevice : cannot initialize olmAccount")
|
||||||
// Else, create it
|
|
||||||
try {
|
|
||||||
olmAccount = OlmAccount()
|
|
||||||
store.storeAccount(olmAccount!!)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Timber.e(e, "MXOlmDevice : cannot initialize olmAccount")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Timber.v("MXOlmDevice : use an existing account")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -109,13 +97,13 @@ internal class MXOlmDevice @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
deviceCurve25519Key = olmAccount!!.identityKeys()[OlmAccount.JSON_KEY_IDENTITY_KEY]
|
deviceCurve25519Key = store.getOlmAccount().identityKeys()[OlmAccount.JSON_KEY_IDENTITY_KEY]
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.e(e, "## MXOlmDevice : cannot find ${OlmAccount.JSON_KEY_IDENTITY_KEY} with error")
|
Timber.e(e, "## MXOlmDevice : cannot find ${OlmAccount.JSON_KEY_IDENTITY_KEY} with error")
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
deviceEd25519Key = olmAccount!!.identityKeys()[OlmAccount.JSON_KEY_FINGER_PRINT_KEY]
|
deviceEd25519Key = store.getOlmAccount().identityKeys()[OlmAccount.JSON_KEY_FINGER_PRINT_KEY]
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.e(e, "## MXOlmDevice : cannot find ${OlmAccount.JSON_KEY_FINGER_PRINT_KEY} with error")
|
Timber.e(e, "## MXOlmDevice : cannot find ${OlmAccount.JSON_KEY_FINGER_PRINT_KEY} with error")
|
||||||
}
|
}
|
||||||
|
@ -126,7 +114,7 @@ internal class MXOlmDevice @Inject constructor(
|
||||||
*/
|
*/
|
||||||
fun getOneTimeKeys(): Map<String, Map<String, String>>? {
|
fun getOneTimeKeys(): Map<String, Map<String, String>>? {
|
||||||
try {
|
try {
|
||||||
return olmAccount!!.oneTimeKeys()
|
return store.getOlmAccount().oneTimeKeys()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.e(e, "## getOneTimeKeys() : failed")
|
Timber.e(e, "## getOneTimeKeys() : failed")
|
||||||
}
|
}
|
||||||
|
@ -138,14 +126,13 @@ internal class MXOlmDevice @Inject constructor(
|
||||||
* @return The maximum number of one-time keys the olm account can store.
|
* @return The maximum number of one-time keys the olm account can store.
|
||||||
*/
|
*/
|
||||||
fun getMaxNumberOfOneTimeKeys(): Long {
|
fun getMaxNumberOfOneTimeKeys(): Long {
|
||||||
return olmAccount?.maxOneTimeKeys() ?: -1
|
return store.getOlmAccount().maxOneTimeKeys()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Release the instance
|
* Release the instance
|
||||||
*/
|
*/
|
||||||
fun release() {
|
fun release() {
|
||||||
olmAccount?.releaseAccount()
|
|
||||||
olmUtility?.releaseUtility()
|
olmUtility?.releaseUtility()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -157,7 +144,7 @@ internal class MXOlmDevice @Inject constructor(
|
||||||
*/
|
*/
|
||||||
fun signMessage(message: String): String? {
|
fun signMessage(message: String): String? {
|
||||||
try {
|
try {
|
||||||
return olmAccount!!.signMessage(message)
|
return store.getOlmAccount().signMessage(message)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.e(e, "## signMessage() : failed")
|
Timber.e(e, "## signMessage() : failed")
|
||||||
}
|
}
|
||||||
|
@ -170,8 +157,8 @@ internal class MXOlmDevice @Inject constructor(
|
||||||
*/
|
*/
|
||||||
fun markKeysAsPublished() {
|
fun markKeysAsPublished() {
|
||||||
try {
|
try {
|
||||||
olmAccount!!.markOneTimeKeysAsPublished()
|
store.getOlmAccount().markOneTimeKeysAsPublished()
|
||||||
store.storeAccount(olmAccount!!)
|
store.saveOlmAccount()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.e(e, "## markKeysAsPublished() : failed")
|
Timber.e(e, "## markKeysAsPublished() : failed")
|
||||||
}
|
}
|
||||||
|
@ -184,8 +171,8 @@ internal class MXOlmDevice @Inject constructor(
|
||||||
*/
|
*/
|
||||||
fun generateOneTimeKeys(numKeys: Int) {
|
fun generateOneTimeKeys(numKeys: Int) {
|
||||||
try {
|
try {
|
||||||
olmAccount!!.generateOneTimeKeys(numKeys)
|
store.getOlmAccount().generateOneTimeKeys(numKeys)
|
||||||
store.storeAccount(olmAccount!!)
|
store.saveOlmAccount()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.e(e, "## generateOneTimeKeys() : failed")
|
Timber.e(e, "## generateOneTimeKeys() : failed")
|
||||||
}
|
}
|
||||||
|
@ -205,7 +192,7 @@ internal class MXOlmDevice @Inject constructor(
|
||||||
|
|
||||||
try {
|
try {
|
||||||
olmSession = OlmSession()
|
olmSession = OlmSession()
|
||||||
olmSession.initOutboundSession(olmAccount!!, theirIdentityKey, theirOneTimeKey)
|
olmSession.initOutboundSession(store.getOlmAccount(), theirIdentityKey, theirOneTimeKey)
|
||||||
|
|
||||||
val olmSessionWrapper = OlmSessionWrapper(olmSession, 0)
|
val olmSessionWrapper = OlmSessionWrapper(olmSession, 0)
|
||||||
|
|
||||||
|
@ -245,7 +232,7 @@ internal class MXOlmDevice @Inject constructor(
|
||||||
try {
|
try {
|
||||||
try {
|
try {
|
||||||
olmSession = OlmSession()
|
olmSession = OlmSession()
|
||||||
olmSession.initInboundSessionFrom(olmAccount!!, theirDeviceIdentityKey, ciphertext)
|
olmSession.initInboundSessionFrom(store.getOlmAccount(), theirDeviceIdentityKey, ciphertext)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.e(e, "## createInboundSession() : the session creation failed")
|
Timber.e(e, "## createInboundSession() : the session creation failed")
|
||||||
return null
|
return null
|
||||||
|
@ -254,8 +241,8 @@ internal class MXOlmDevice @Inject constructor(
|
||||||
Timber.v("## createInboundSession() : sessionId: ${olmSession.sessionIdentifier()}")
|
Timber.v("## createInboundSession() : sessionId: ${olmSession.sessionIdentifier()}")
|
||||||
|
|
||||||
try {
|
try {
|
||||||
olmAccount!!.removeOneTimeKeys(olmSession)
|
store.getOlmAccount().removeOneTimeKeys(olmSession)
|
||||||
store.storeAccount(olmAccount!!)
|
store.saveOlmAccount()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.e(e, "## createInboundSession() : removeOneTimeKeys failed")
|
Timber.e(e, "## createInboundSession() : removeOneTimeKeys failed")
|
||||||
}
|
}
|
||||||
|
@ -654,7 +641,7 @@ internal class MXOlmDevice @Inject constructor(
|
||||||
throw MXCryptoError.OlmError(e)
|
throw MXCryptoError.OlmError(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (null != timeline) {
|
if (timeline?.isNotBlank() == true) {
|
||||||
val timelineSet = inboundGroupSessionMessageIndexes.getOrPut(timeline) { mutableSetOf() }
|
val timelineSet = inboundGroupSessionMessageIndexes.getOrPut(timeline) { mutableSetOf() }
|
||||||
|
|
||||||
val messageIndexKey = senderKey + "|" + sessionId + "|" + decryptResult.mIndex
|
val messageIndexKey = senderKey + "|" + sessionId + "|" + decryptResult.mIndex
|
||||||
|
@ -770,7 +757,7 @@ internal class MXOlmDevice @Inject constructor(
|
||||||
return session
|
return session
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Timber.w("## getInboundGroupSession() : Cannot retrieve inbound group session $sessionId")
|
Timber.v("## getInboundGroupSession() : Cannot retrieve inbound group session $sessionId")
|
||||||
throw MXCryptoError.Base(MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID, MXCryptoError.UNKNOWN_INBOUND_SESSION_ID_REASON)
|
throw MXCryptoError.Base(MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID, MXCryptoError.UNKNOWN_INBOUND_SESSION_ID_REASON)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,25 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2020 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.matrix.android.internal.crypto
|
||||||
|
|
||||||
|
interface OutgoingGossipingRequest {
|
||||||
|
var recipients: Map<String, List<String>>
|
||||||
|
var requestId: String
|
||||||
|
var state: OutgoingGossipingRequestState
|
||||||
|
// transaction id for the cancellation, if any
|
||||||
|
// var cancellationTxnId: String?
|
||||||
|
}
|
|
@ -0,0 +1,162 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2016 OpenMarket Ltd
|
||||||
|
* Copyright 2018 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.matrix.android.internal.crypto
|
||||||
|
|
||||||
|
import im.vector.matrix.android.api.session.events.model.LocalEcho
|
||||||
|
import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody
|
||||||
|
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
|
||||||
|
import im.vector.matrix.android.internal.di.SessionId
|
||||||
|
import im.vector.matrix.android.internal.session.SessionScope
|
||||||
|
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
|
||||||
|
import im.vector.matrix.android.internal.worker.WorkerParamsFactory
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import timber.log.Timber
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@SessionScope
|
||||||
|
internal class OutgoingGossipingRequestManager @Inject constructor(
|
||||||
|
@SessionId private val sessionId: String,
|
||||||
|
private val cryptoStore: IMXCryptoStore,
|
||||||
|
private val coroutineDispatchers: MatrixCoroutineDispatchers,
|
||||||
|
private val cryptoCoroutineScope: CoroutineScope,
|
||||||
|
private val gossipingWorkManager: GossipingWorkManager) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send off a room key request, if we haven't already done so.
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* The `requestBody` is compared (with a deep-equality check) against
|
||||||
|
* previous queued or sent requests and if it matches, no change is made.
|
||||||
|
* Otherwise, a request is added to the pending list, and a job is started
|
||||||
|
* in the background to send it.
|
||||||
|
*
|
||||||
|
* @param requestBody requestBody
|
||||||
|
* @param recipients recipients
|
||||||
|
*/
|
||||||
|
fun sendRoomKeyRequest(requestBody: RoomKeyRequestBody, recipients: Map<String, List<String>>) {
|
||||||
|
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
|
||||||
|
cryptoStore.getOrAddOutgoingRoomKeyRequest(requestBody, recipients)?.let {
|
||||||
|
// Don't resend if it's already done, you need to cancel first (reRequest)
|
||||||
|
if (it.state == OutgoingGossipingRequestState.SENDING || it.state == OutgoingGossipingRequestState.SENT) {
|
||||||
|
Timber.v("## GOSSIP sendOutgoingRoomKeyRequest() : we already request for that session: $it")
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
|
||||||
|
sendOutgoingGossipingRequest(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun sendSecretShareRequest(secretName: String, recipients: Map<String, List<String>>) {
|
||||||
|
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
|
||||||
|
// A bit dirty, but for better stability give other party some time to mark
|
||||||
|
// devices trusted :/
|
||||||
|
delay(1500)
|
||||||
|
cryptoStore.getOrAddOutgoingSecretShareRequest(secretName, recipients)?.let {
|
||||||
|
// TODO check if there is already one that is being sent?
|
||||||
|
if (it.state == OutgoingGossipingRequestState.SENDING || it.state == OutgoingGossipingRequestState.SENT) {
|
||||||
|
Timber.v("## GOSSIP sendSecretShareRequest() : we already request for that session: $it")
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
|
||||||
|
sendOutgoingGossipingRequest(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cancel room key requests, if any match the given details
|
||||||
|
*
|
||||||
|
* @param requestBody requestBody
|
||||||
|
*/
|
||||||
|
fun cancelRoomKeyRequest(requestBody: RoomKeyRequestBody) {
|
||||||
|
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
|
||||||
|
cancelRoomKeyRequest(requestBody, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cancel room key requests, if any match the given details, and resend
|
||||||
|
*
|
||||||
|
* @param requestBody requestBody
|
||||||
|
*/
|
||||||
|
fun resendRoomKeyRequest(requestBody: RoomKeyRequestBody) {
|
||||||
|
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
|
||||||
|
cancelRoomKeyRequest(requestBody, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cancel room key requests, if any match the given details, and resend
|
||||||
|
*
|
||||||
|
* @param requestBody requestBody
|
||||||
|
* @param andResend true to resend the key request
|
||||||
|
*/
|
||||||
|
private fun cancelRoomKeyRequest(requestBody: RoomKeyRequestBody, andResend: Boolean) {
|
||||||
|
val req = cryptoStore.getOutgoingRoomKeyRequest(requestBody)
|
||||||
|
?: // no request was made for this key
|
||||||
|
return Unit.also {
|
||||||
|
Timber.v("## GOSSIP cancelRoomKeyRequest() Unknown request")
|
||||||
|
}
|
||||||
|
|
||||||
|
sendOutgoingRoomKeyRequestCancellation(req, andResend)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send the outgoing key request.
|
||||||
|
*
|
||||||
|
* @param request the request
|
||||||
|
*/
|
||||||
|
private fun sendOutgoingGossipingRequest(request: OutgoingGossipingRequest) {
|
||||||
|
Timber.v("## GOSSIP sendOutgoingRoomKeyRequest() : Requesting keys $request")
|
||||||
|
|
||||||
|
val params = SendGossipRequestWorker.Params(
|
||||||
|
sessionId = sessionId,
|
||||||
|
keyShareRequest = request as? OutgoingRoomKeyRequest,
|
||||||
|
secretShareRequest = request as? OutgoingSecretRequest
|
||||||
|
)
|
||||||
|
cryptoStore.updateOutgoingGossipingRequestState(request.requestId, OutgoingGossipingRequestState.SENDING)
|
||||||
|
val workRequest = gossipingWorkManager.createWork<SendGossipRequestWorker>(WorkerParamsFactory.toData(params), true)
|
||||||
|
gossipingWorkManager.postWork(workRequest)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given a OutgoingRoomKeyRequest, cancel it and delete the request record
|
||||||
|
*
|
||||||
|
* @param request the request
|
||||||
|
*/
|
||||||
|
private fun sendOutgoingRoomKeyRequestCancellation(request: OutgoingRoomKeyRequest, resend: Boolean = false) {
|
||||||
|
Timber.v("$request")
|
||||||
|
val params = CancelGossipRequestWorker.Params.fromRequest(sessionId, request)
|
||||||
|
cryptoStore.updateOutgoingGossipingRequestState(request.requestId, OutgoingGossipingRequestState.CANCELLING)
|
||||||
|
|
||||||
|
val workRequest = gossipingWorkManager.createWork<CancelGossipRequestWorker>(WorkerParamsFactory.toData(params), true)
|
||||||
|
gossipingWorkManager.postWork(workRequest)
|
||||||
|
|
||||||
|
if (resend) {
|
||||||
|
val reSendParams = SendGossipRequestWorker.Params(
|
||||||
|
sessionId = sessionId,
|
||||||
|
keyShareRequest = request.copy(requestId = LocalEcho.createLocalEchoId())
|
||||||
|
)
|
||||||
|
val reSendWorkRequest = gossipingWorkManager.createWork<SendGossipRequestWorker>(WorkerParamsFactory.toData(reSendParams), true)
|
||||||
|
gossipingWorkManager.postWork(reSendWorkRequest)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -17,22 +17,26 @@
|
||||||
|
|
||||||
package im.vector.matrix.android.internal.crypto
|
package im.vector.matrix.android.internal.crypto
|
||||||
|
|
||||||
|
import com.squareup.moshi.JsonClass
|
||||||
import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody
|
import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents an outgoing room key request
|
* Represents an outgoing room key request
|
||||||
*/
|
*/
|
||||||
class OutgoingRoomKeyRequest(
|
@JsonClass(generateAdapter = true)
|
||||||
|
data class OutgoingRoomKeyRequest(
|
||||||
// RequestBody
|
// RequestBody
|
||||||
var requestBody: RoomKeyRequestBody?, // list of recipients for the request
|
var requestBody: RoomKeyRequestBody?,
|
||||||
var recipients: List<Map<String, String>>, // Unique id for this request. Used for both
|
// list of recipients for the request
|
||||||
|
override var recipients: Map<String, List<String>>,
|
||||||
|
// Unique id for this request. Used for both
|
||||||
// an id within the request for later pairing with a cancellation, and for
|
// an id within the request for later pairing with a cancellation, and for
|
||||||
// the transaction id when sending the to_device messages to our local
|
// the transaction id when sending the to_device messages to our local
|
||||||
var requestId: String, // current state of this request
|
override var requestId: String, // current state of this request
|
||||||
var state: RequestState) {
|
override var state: OutgoingGossipingRequestState
|
||||||
|
// transaction id for the cancellation, if any
|
||||||
// transaction id for the cancellation, if any
|
// override var cancellationTxnId: String? = null
|
||||||
var cancellationTxnId: String? = null
|
) : OutgoingGossipingRequest {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Used only for log.
|
* Used only for log.
|
||||||
|
@ -53,66 +57,4 @@ class OutgoingRoomKeyRequest(
|
||||||
get() = if (null != requestBody) {
|
get() = if (null != requestBody) {
|
||||||
requestBody!!.sessionId
|
requestBody!!.sessionId
|
||||||
} else null
|
} else null
|
||||||
|
|
||||||
/**
|
|
||||||
* possible states for a room key request
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* The state machine looks like:
|
|
||||||
* <pre>
|
|
||||||
*
|
|
||||||
* |
|
|
||||||
* V
|
|
||||||
* UNSENT -----------------------------+
|
|
||||||
* | |
|
|
||||||
* | (send successful) | (cancellation requested)
|
|
||||||
* V |
|
|
||||||
* SENT |
|
|
||||||
* |-------------------------------- | --------------+
|
|
||||||
* | | |
|
|
||||||
* | | | (cancellation requested with intent
|
|
||||||
* | | | to resend a new request)
|
|
||||||
* | (cancellation requested) | |
|
|
||||||
* V | V
|
|
||||||
* CANCELLATION_PENDING | CANCELLATION_PENDING_AND_WILL_RESEND
|
|
||||||
* | | |
|
|
||||||
* | (cancellation sent) | | (cancellation sent. Create new request
|
|
||||||
* | | | in the UNSENT state)
|
|
||||||
* V | |
|
|
||||||
* (deleted) <---------------------------+----------------+
|
|
||||||
* </pre>
|
|
||||||
*/
|
|
||||||
|
|
||||||
enum class RequestState {
|
|
||||||
/**
|
|
||||||
* request not yet sent
|
|
||||||
*/
|
|
||||||
UNSENT,
|
|
||||||
/**
|
|
||||||
* request sent, awaiting reply
|
|
||||||
*/
|
|
||||||
SENT,
|
|
||||||
/**
|
|
||||||
* reply received, cancellation not yet sent
|
|
||||||
*/
|
|
||||||
CANCELLATION_PENDING,
|
|
||||||
/**
|
|
||||||
* Cancellation not yet sent, once sent, a new request will be done
|
|
||||||
*/
|
|
||||||
CANCELLATION_PENDING_AND_WILL_RESEND,
|
|
||||||
/**
|
|
||||||
* sending failed
|
|
||||||
*/
|
|
||||||
FAILED;
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
fun from(state: Int) = when (state) {
|
|
||||||
0 -> UNSENT
|
|
||||||
1 -> SENT
|
|
||||||
2 -> CANCELLATION_PENDING
|
|
||||||
3 -> CANCELLATION_PENDING_AND_WILL_RESEND
|
|
||||||
else /*4*/ -> FAILED
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,320 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2016 OpenMarket Ltd
|
|
||||||
* Copyright 2018 New Vector Ltd
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package im.vector.matrix.android.internal.crypto
|
|
||||||
|
|
||||||
import im.vector.matrix.android.api.MatrixCallback
|
|
||||||
import im.vector.matrix.android.api.session.events.model.EventType
|
|
||||||
import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap
|
|
||||||
import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody
|
|
||||||
import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyShareCancellation
|
|
||||||
import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyShareRequest
|
|
||||||
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
|
|
||||||
import im.vector.matrix.android.internal.crypto.tasks.SendToDeviceTask
|
|
||||||
import im.vector.matrix.android.internal.session.SessionScope
|
|
||||||
import im.vector.matrix.android.internal.task.TaskExecutor
|
|
||||||
import im.vector.matrix.android.internal.task.TaskThread
|
|
||||||
import im.vector.matrix.android.internal.task.configureWith
|
|
||||||
import im.vector.matrix.android.internal.util.createBackgroundHandler
|
|
||||||
import timber.log.Timber
|
|
||||||
import java.util.concurrent.atomic.AtomicBoolean
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
@SessionScope
|
|
||||||
internal class OutgoingRoomKeyRequestManager @Inject constructor(
|
|
||||||
private val cryptoStore: IMXCryptoStore,
|
|
||||||
private val sendToDeviceTask: SendToDeviceTask,
|
|
||||||
private val taskExecutor: TaskExecutor) {
|
|
||||||
|
|
||||||
// running
|
|
||||||
private var isClientRunning: Boolean = false
|
|
||||||
|
|
||||||
// transaction counter
|
|
||||||
private var txnCtr: Int = 0
|
|
||||||
|
|
||||||
// sanity check to ensure that we don't end up with two concurrent runs
|
|
||||||
// of sendOutgoingRoomKeyRequestsTimer
|
|
||||||
private val sendOutgoingRoomKeyRequestsRunning = AtomicBoolean(false)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called when the client is started. Sets background processes running.
|
|
||||||
*/
|
|
||||||
fun start() {
|
|
||||||
isClientRunning = true
|
|
||||||
startTimer()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called when the client is stopped. Stops any running background processes.
|
|
||||||
*/
|
|
||||||
fun stop() {
|
|
||||||
isClientRunning = false
|
|
||||||
stopTimer()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Make up a new transaction id
|
|
||||||
*
|
|
||||||
* @return {string} a new, unique, transaction id
|
|
||||||
*/
|
|
||||||
private fun makeTxnId(): String {
|
|
||||||
return "m" + System.currentTimeMillis() + "." + txnCtr++
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Send off a room key request, if we haven't already done so.
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* The `requestBody` is compared (with a deep-equality check) against
|
|
||||||
* previous queued or sent requests and if it matches, no change is made.
|
|
||||||
* Otherwise, a request is added to the pending list, and a job is started
|
|
||||||
* in the background to send it.
|
|
||||||
*
|
|
||||||
* @param requestBody requestBody
|
|
||||||
* @param recipients recipients
|
|
||||||
*/
|
|
||||||
fun sendRoomKeyRequest(requestBody: RoomKeyRequestBody?, recipients: List<Map<String, String>>) {
|
|
||||||
val req = cryptoStore.getOrAddOutgoingRoomKeyRequest(
|
|
||||||
OutgoingRoomKeyRequest(requestBody, recipients, makeTxnId(), OutgoingRoomKeyRequest.RequestState.UNSENT))
|
|
||||||
|
|
||||||
if (req?.state == OutgoingRoomKeyRequest.RequestState.UNSENT) {
|
|
||||||
startTimer()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Cancel room key requests, if any match the given details
|
|
||||||
*
|
|
||||||
* @param requestBody requestBody
|
|
||||||
*/
|
|
||||||
fun cancelRoomKeyRequest(requestBody: RoomKeyRequestBody) {
|
|
||||||
BACKGROUND_HANDLER.post {
|
|
||||||
cancelRoomKeyRequest(requestBody, false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Cancel room key requests, if any match the given details, and resend
|
|
||||||
*
|
|
||||||
* @param requestBody requestBody
|
|
||||||
*/
|
|
||||||
fun resendRoomKeyRequest(requestBody: RoomKeyRequestBody) {
|
|
||||||
BACKGROUND_HANDLER.post {
|
|
||||||
cancelRoomKeyRequest(requestBody, true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Cancel room key requests, if any match the given details, and resend
|
|
||||||
*
|
|
||||||
* @param requestBody requestBody
|
|
||||||
* @param andResend true to resend the key request
|
|
||||||
*/
|
|
||||||
private fun cancelRoomKeyRequest(requestBody: RoomKeyRequestBody, andResend: Boolean) {
|
|
||||||
val req = cryptoStore.getOutgoingRoomKeyRequest(requestBody)
|
|
||||||
?: // no request was made for this key
|
|
||||||
return
|
|
||||||
|
|
||||||
Timber.v("cancelRoomKeyRequest: requestId: " + req.requestId + " state: " + req.state + " andResend: " + andResend)
|
|
||||||
|
|
||||||
when (req.state) {
|
|
||||||
OutgoingRoomKeyRequest.RequestState.CANCELLATION_PENDING,
|
|
||||||
OutgoingRoomKeyRequest.RequestState.CANCELLATION_PENDING_AND_WILL_RESEND -> {
|
|
||||||
// nothing to do here
|
|
||||||
}
|
|
||||||
OutgoingRoomKeyRequest.RequestState.UNSENT,
|
|
||||||
OutgoingRoomKeyRequest.RequestState.FAILED -> {
|
|
||||||
Timber.v("## cancelRoomKeyRequest() : deleting unnecessary room key request for $requestBody")
|
|
||||||
cryptoStore.deleteOutgoingRoomKeyRequest(req.requestId)
|
|
||||||
}
|
|
||||||
OutgoingRoomKeyRequest.RequestState.SENT -> {
|
|
||||||
if (andResend) {
|
|
||||||
req.state = OutgoingRoomKeyRequest.RequestState.CANCELLATION_PENDING_AND_WILL_RESEND
|
|
||||||
} else {
|
|
||||||
req.state = OutgoingRoomKeyRequest.RequestState.CANCELLATION_PENDING
|
|
||||||
}
|
|
||||||
req.cancellationTxnId = makeTxnId()
|
|
||||||
cryptoStore.updateOutgoingRoomKeyRequest(req)
|
|
||||||
sendOutgoingRoomKeyRequestCancellation(req)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Start the background timer to send queued requests, if the timer isn't already running.
|
|
||||||
*/
|
|
||||||
private fun startTimer() {
|
|
||||||
if (sendOutgoingRoomKeyRequestsRunning.get()) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
BACKGROUND_HANDLER.postDelayed(Runnable {
|
|
||||||
if (sendOutgoingRoomKeyRequestsRunning.get()) {
|
|
||||||
Timber.v("## startTimer() : RoomKeyRequestSend already in progress!")
|
|
||||||
return@Runnable
|
|
||||||
}
|
|
||||||
|
|
||||||
sendOutgoingRoomKeyRequestsRunning.set(true)
|
|
||||||
sendOutgoingRoomKeyRequests()
|
|
||||||
}, SEND_KEY_REQUESTS_DELAY_MS.toLong())
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun stopTimer() {
|
|
||||||
BACKGROUND_HANDLER.removeCallbacksAndMessages(null)
|
|
||||||
}
|
|
||||||
|
|
||||||
// look for and send any queued requests. Runs itself recursively until
|
|
||||||
// there are no more requests, or there is an error (in which case, the
|
|
||||||
// timer will be restarted before the promise resolves).
|
|
||||||
private fun sendOutgoingRoomKeyRequests() {
|
|
||||||
if (!isClientRunning) {
|
|
||||||
sendOutgoingRoomKeyRequestsRunning.set(false)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
Timber.v("## sendOutgoingRoomKeyRequests() : Looking for queued outgoing room key requests")
|
|
||||||
val outgoingRoomKeyRequest = cryptoStore.getOutgoingRoomKeyRequestByState(
|
|
||||||
setOf(OutgoingRoomKeyRequest.RequestState.UNSENT,
|
|
||||||
OutgoingRoomKeyRequest.RequestState.CANCELLATION_PENDING,
|
|
||||||
OutgoingRoomKeyRequest.RequestState.CANCELLATION_PENDING_AND_WILL_RESEND))
|
|
||||||
|
|
||||||
if (null == outgoingRoomKeyRequest) {
|
|
||||||
Timber.v("## sendOutgoingRoomKeyRequests() : No more outgoing room key requests")
|
|
||||||
sendOutgoingRoomKeyRequestsRunning.set(false)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (OutgoingRoomKeyRequest.RequestState.UNSENT === outgoingRoomKeyRequest.state) {
|
|
||||||
sendOutgoingRoomKeyRequest(outgoingRoomKeyRequest)
|
|
||||||
} else {
|
|
||||||
sendOutgoingRoomKeyRequestCancellation(outgoingRoomKeyRequest)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Send the outgoing key request.
|
|
||||||
*
|
|
||||||
* @param request the request
|
|
||||||
*/
|
|
||||||
private fun sendOutgoingRoomKeyRequest(request: OutgoingRoomKeyRequest) {
|
|
||||||
Timber.v("## sendOutgoingRoomKeyRequest() : Requesting keys " + request.requestBody
|
|
||||||
+ " from " + request.recipients + " id " + request.requestId)
|
|
||||||
|
|
||||||
val requestMessage = RoomKeyShareRequest(
|
|
||||||
requestingDeviceId = cryptoStore.getDeviceId(),
|
|
||||||
requestId = request.requestId,
|
|
||||||
body = request.requestBody
|
|
||||||
)
|
|
||||||
|
|
||||||
sendMessageToDevices(requestMessage, request.recipients, request.requestId, object : MatrixCallback<Unit> {
|
|
||||||
private fun onDone(state: OutgoingRoomKeyRequest.RequestState) {
|
|
||||||
if (request.state !== OutgoingRoomKeyRequest.RequestState.UNSENT) {
|
|
||||||
Timber.v("## sendOutgoingRoomKeyRequest() : Cannot update room key request from UNSENT as it was already updated to ${request.state}")
|
|
||||||
} else {
|
|
||||||
request.state = state
|
|
||||||
cryptoStore.updateOutgoingRoomKeyRequest(request)
|
|
||||||
}
|
|
||||||
|
|
||||||
sendOutgoingRoomKeyRequestsRunning.set(false)
|
|
||||||
startTimer()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onSuccess(data: Unit) {
|
|
||||||
Timber.v("## sendOutgoingRoomKeyRequest succeed")
|
|
||||||
onDone(OutgoingRoomKeyRequest.RequestState.SENT)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onFailure(failure: Throwable) {
|
|
||||||
Timber.e("## sendOutgoingRoomKeyRequest failed")
|
|
||||||
onDone(OutgoingRoomKeyRequest.RequestState.FAILED)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Given a OutgoingRoomKeyRequest, cancel it and delete the request record
|
|
||||||
*
|
|
||||||
* @param request the request
|
|
||||||
*/
|
|
||||||
private fun sendOutgoingRoomKeyRequestCancellation(request: OutgoingRoomKeyRequest) {
|
|
||||||
Timber.v("## sendOutgoingRoomKeyRequestCancellation() : Sending cancellation for key request for " + request.requestBody
|
|
||||||
+ " to " + request.recipients
|
|
||||||
+ " cancellation id " + request.cancellationTxnId)
|
|
||||||
|
|
||||||
val roomKeyShareCancellation = RoomKeyShareCancellation(
|
|
||||||
requestingDeviceId = cryptoStore.getDeviceId(),
|
|
||||||
requestId = request.cancellationTxnId
|
|
||||||
)
|
|
||||||
|
|
||||||
sendMessageToDevices(roomKeyShareCancellation, request.recipients, request.cancellationTxnId, object : MatrixCallback<Unit> {
|
|
||||||
private fun onDone() {
|
|
||||||
cryptoStore.deleteOutgoingRoomKeyRequest(request.requestId)
|
|
||||||
sendOutgoingRoomKeyRequestsRunning.set(false)
|
|
||||||
startTimer()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onSuccess(data: Unit) {
|
|
||||||
Timber.v("## sendOutgoingRoomKeyRequestCancellation() : done")
|
|
||||||
val resend = request.state === OutgoingRoomKeyRequest.RequestState.CANCELLATION_PENDING_AND_WILL_RESEND
|
|
||||||
|
|
||||||
onDone()
|
|
||||||
|
|
||||||
// Resend the request with a new ID
|
|
||||||
if (resend) {
|
|
||||||
sendRoomKeyRequest(request.requestBody, request.recipients)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onFailure(failure: Throwable) {
|
|
||||||
Timber.e("## sendOutgoingRoomKeyRequestCancellation failed")
|
|
||||||
onDone()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Send a SendToDeviceObject to a list of recipients
|
|
||||||
*
|
|
||||||
* @param message the message
|
|
||||||
* @param recipients the recipients.
|
|
||||||
* @param transactionId the transaction id
|
|
||||||
* @param callback the asynchronous callback.
|
|
||||||
*/
|
|
||||||
private fun sendMessageToDevices(message: Any,
|
|
||||||
recipients: List<Map<String, String>>,
|
|
||||||
transactionId: String?,
|
|
||||||
callback: MatrixCallback<Unit>) {
|
|
||||||
val contentMap = MXUsersDevicesMap<Any>()
|
|
||||||
|
|
||||||
for (recipient in recipients) {
|
|
||||||
// TODO Change this two hard coded key to something better
|
|
||||||
contentMap.setObject(recipient["userId"], recipient["deviceId"], message)
|
|
||||||
}
|
|
||||||
sendToDeviceTask
|
|
||||||
.configureWith(SendToDeviceTask.Params(EventType.ROOM_KEY_REQUEST, contentMap, transactionId)) {
|
|
||||||
this.callback = callback
|
|
||||||
this.callbackThread = TaskThread.CALLER
|
|
||||||
this.executionThread = TaskThread.CALLER
|
|
||||||
}
|
|
||||||
.executeBy(taskExecutor)
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private const val SEND_KEY_REQUESTS_DELAY_MS = 500
|
|
||||||
|
|
||||||
private val BACKGROUND_HANDLER = createBackgroundHandler("OutgoingRoomKeyRequest")
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,38 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2020 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.matrix.android.internal.crypto
|
||||||
|
|
||||||
|
import com.squareup.moshi.JsonClass
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents an outgoing room key request
|
||||||
|
*/
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
|
class OutgoingSecretRequest(
|
||||||
|
// Secret Name
|
||||||
|
val secretName: String?,
|
||||||
|
// list of recipients for the request
|
||||||
|
override var recipients: Map<String, List<String>>,
|
||||||
|
// Unique id for this request. Used for both
|
||||||
|
// an id within the request for later pairing with a cancellation, and for
|
||||||
|
// the transaction id when sending the to_device messages to our local
|
||||||
|
override var requestId: String,
|
||||||
|
// current state of this request
|
||||||
|
override var state: OutgoingGossipingRequestState) : OutgoingGossipingRequest {
|
||||||
|
|
||||||
|
// transaction id for the cancellation, if any
|
||||||
|
}
|
|
@ -0,0 +1,148 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2020 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.matrix.android.internal.crypto
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.work.CoroutineWorker
|
||||||
|
import androidx.work.Data
|
||||||
|
import androidx.work.WorkerParameters
|
||||||
|
import com.squareup.moshi.JsonClass
|
||||||
|
import im.vector.matrix.android.api.auth.data.Credentials
|
||||||
|
import im.vector.matrix.android.api.failure.shouldBeRetried
|
||||||
|
import im.vector.matrix.android.api.session.events.model.Event
|
||||||
|
import im.vector.matrix.android.api.session.events.model.EventType
|
||||||
|
import im.vector.matrix.android.api.session.events.model.LocalEcho
|
||||||
|
import im.vector.matrix.android.api.session.events.model.toContent
|
||||||
|
import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap
|
||||||
|
import im.vector.matrix.android.internal.crypto.model.rest.GossipingToDeviceObject
|
||||||
|
import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyShareRequest
|
||||||
|
import im.vector.matrix.android.internal.crypto.model.rest.SecretShareRequest
|
||||||
|
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
|
||||||
|
import im.vector.matrix.android.internal.crypto.tasks.SendToDeviceTask
|
||||||
|
import im.vector.matrix.android.internal.worker.WorkerParamsFactory
|
||||||
|
import im.vector.matrix.android.internal.worker.getSessionComponent
|
||||||
|
import org.greenrobot.eventbus.EventBus
|
||||||
|
import timber.log.Timber
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
internal class SendGossipRequestWorker(context: Context,
|
||||||
|
params: WorkerParameters)
|
||||||
|
: CoroutineWorker(context, params) {
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
|
internal data class Params(
|
||||||
|
val sessionId: String,
|
||||||
|
val keyShareRequest: OutgoingRoomKeyRequest? = null,
|
||||||
|
val secretShareRequest: OutgoingSecretRequest? = null
|
||||||
|
)
|
||||||
|
|
||||||
|
@Inject lateinit var sendToDeviceTask: SendToDeviceTask
|
||||||
|
@Inject lateinit var cryptoStore: IMXCryptoStore
|
||||||
|
@Inject lateinit var eventBus: EventBus
|
||||||
|
@Inject lateinit var credentials: Credentials
|
||||||
|
|
||||||
|
override suspend fun doWork(): Result {
|
||||||
|
val errorOutputData = Data.Builder().putBoolean("failed", true).build()
|
||||||
|
val params = WorkerParamsFactory.fromData<Params>(inputData)
|
||||||
|
?: return Result.success(errorOutputData)
|
||||||
|
|
||||||
|
val sessionComponent = getSessionComponent(params.sessionId)
|
||||||
|
?: return Result.success(errorOutputData).also {
|
||||||
|
// TODO, can this happen? should I update local echo?
|
||||||
|
Timber.e("Unknown Session, cannot send message, sessionId: ${params.sessionId}")
|
||||||
|
}
|
||||||
|
sessionComponent.inject(this)
|
||||||
|
|
||||||
|
val localId = LocalEcho.createLocalEchoId()
|
||||||
|
val contentMap = MXUsersDevicesMap<Any>()
|
||||||
|
val eventType: String
|
||||||
|
val requestId: String
|
||||||
|
when {
|
||||||
|
params.keyShareRequest != null -> {
|
||||||
|
eventType = EventType.ROOM_KEY_REQUEST
|
||||||
|
requestId = params.keyShareRequest.requestId
|
||||||
|
val toDeviceContent = RoomKeyShareRequest(
|
||||||
|
requestingDeviceId = credentials.deviceId,
|
||||||
|
requestId = params.keyShareRequest.requestId,
|
||||||
|
action = GossipingToDeviceObject.ACTION_SHARE_REQUEST,
|
||||||
|
body = params.keyShareRequest.requestBody
|
||||||
|
)
|
||||||
|
cryptoStore.saveGossipingEvent(Event(
|
||||||
|
type = eventType,
|
||||||
|
content = toDeviceContent.toContent(),
|
||||||
|
senderId = credentials.userId
|
||||||
|
).also {
|
||||||
|
it.ageLocalTs = System.currentTimeMillis()
|
||||||
|
})
|
||||||
|
|
||||||
|
params.keyShareRequest.recipients.forEach { userToDeviceMap ->
|
||||||
|
userToDeviceMap.value.forEach { deviceId ->
|
||||||
|
contentMap.setObject(userToDeviceMap.key, deviceId, toDeviceContent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
params.secretShareRequest != null -> {
|
||||||
|
eventType = EventType.REQUEST_SECRET
|
||||||
|
requestId = params.secretShareRequest.requestId
|
||||||
|
val toDeviceContent = SecretShareRequest(
|
||||||
|
requestingDeviceId = credentials.deviceId,
|
||||||
|
requestId = params.secretShareRequest.requestId,
|
||||||
|
action = GossipingToDeviceObject.ACTION_SHARE_REQUEST,
|
||||||
|
secretName = params.secretShareRequest.secretName
|
||||||
|
)
|
||||||
|
|
||||||
|
cryptoStore.saveGossipingEvent(Event(
|
||||||
|
type = eventType,
|
||||||
|
content = toDeviceContent.toContent(),
|
||||||
|
senderId = credentials.userId
|
||||||
|
).also {
|
||||||
|
it.ageLocalTs = System.currentTimeMillis()
|
||||||
|
})
|
||||||
|
|
||||||
|
params.secretShareRequest.recipients.forEach { userToDeviceMap ->
|
||||||
|
userToDeviceMap.value.forEach { deviceId ->
|
||||||
|
contentMap.setObject(userToDeviceMap.key, deviceId, toDeviceContent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
return Result.success(errorOutputData).also {
|
||||||
|
Timber.e("Unknown empty gossiping request: $params")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
cryptoStore.updateOutgoingGossipingRequestState(requestId, OutgoingGossipingRequestState.SENDING)
|
||||||
|
sendToDeviceTask.execute(
|
||||||
|
SendToDeviceTask.Params(
|
||||||
|
eventType = eventType,
|
||||||
|
contentMap = contentMap,
|
||||||
|
transactionId = localId
|
||||||
|
)
|
||||||
|
)
|
||||||
|
cryptoStore.updateOutgoingGossipingRequestState(requestId, OutgoingGossipingRequestState.SENT)
|
||||||
|
return Result.success()
|
||||||
|
} catch (exception: Throwable) {
|
||||||
|
return if (exception.shouldBeRetried()) {
|
||||||
|
Result.retry()
|
||||||
|
} else {
|
||||||
|
cryptoStore.updateOutgoingGossipingRequestState(requestId, OutgoingGossipingRequestState.FAILED_TO_SEND)
|
||||||
|
Result.success(errorOutputData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,141 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2020 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.matrix.android.internal.crypto
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.work.CoroutineWorker
|
||||||
|
import androidx.work.Data
|
||||||
|
import androidx.work.WorkerParameters
|
||||||
|
import com.squareup.moshi.JsonClass
|
||||||
|
import im.vector.matrix.android.api.auth.data.Credentials
|
||||||
|
import im.vector.matrix.android.api.failure.shouldBeRetried
|
||||||
|
import im.vector.matrix.android.api.session.events.model.Event
|
||||||
|
import im.vector.matrix.android.api.session.events.model.EventType
|
||||||
|
import im.vector.matrix.android.api.session.events.model.LocalEcho
|
||||||
|
import im.vector.matrix.android.api.session.events.model.toContent
|
||||||
|
import im.vector.matrix.android.internal.crypto.actions.EnsureOlmSessionsForDevicesAction
|
||||||
|
import im.vector.matrix.android.internal.crypto.actions.MessageEncrypter
|
||||||
|
import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap
|
||||||
|
import im.vector.matrix.android.internal.crypto.model.event.SecretSendEventContent
|
||||||
|
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
|
||||||
|
import im.vector.matrix.android.internal.crypto.tasks.SendToDeviceTask
|
||||||
|
import im.vector.matrix.android.internal.worker.WorkerParamsFactory
|
||||||
|
import im.vector.matrix.android.internal.worker.getSessionComponent
|
||||||
|
import org.greenrobot.eventbus.EventBus
|
||||||
|
import timber.log.Timber
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
internal class SendGossipWorker(context: Context,
|
||||||
|
params: WorkerParameters)
|
||||||
|
: CoroutineWorker(context, params) {
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
|
internal data class Params(
|
||||||
|
val sessionId: String,
|
||||||
|
val secretValue: String,
|
||||||
|
val request: IncomingSecretShareRequest
|
||||||
|
)
|
||||||
|
|
||||||
|
@Inject lateinit var sendToDeviceTask: SendToDeviceTask
|
||||||
|
@Inject lateinit var cryptoStore: IMXCryptoStore
|
||||||
|
@Inject lateinit var eventBus: EventBus
|
||||||
|
@Inject lateinit var credentials: Credentials
|
||||||
|
@Inject lateinit var messageEncrypter: MessageEncrypter
|
||||||
|
@Inject lateinit var ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction
|
||||||
|
|
||||||
|
override suspend fun doWork(): Result {
|
||||||
|
val errorOutputData = Data.Builder().putBoolean("failed", true).build()
|
||||||
|
val params = WorkerParamsFactory.fromData<Params>(inputData)
|
||||||
|
?: return Result.success(errorOutputData)
|
||||||
|
|
||||||
|
val sessionComponent = getSessionComponent(params.sessionId)
|
||||||
|
?: return Result.success(errorOutputData).also {
|
||||||
|
// TODO, can this happen? should I update local echo?
|
||||||
|
Timber.e("Unknown Session, cannot send message, sessionId: ${params.sessionId}")
|
||||||
|
}
|
||||||
|
sessionComponent.inject(this)
|
||||||
|
|
||||||
|
val localId = LocalEcho.createLocalEchoId()
|
||||||
|
val eventType: String = EventType.SEND_SECRET
|
||||||
|
|
||||||
|
val toDeviceContent = SecretSendEventContent(
|
||||||
|
requestId = params.request.requestId ?: "",
|
||||||
|
secretValue = params.secretValue
|
||||||
|
)
|
||||||
|
|
||||||
|
val requestingUserId = params.request.userId ?: ""
|
||||||
|
val requestingDeviceId = params.request.deviceId ?: ""
|
||||||
|
val deviceInfo = cryptoStore.getUserDevice(requestingUserId, requestingDeviceId)
|
||||||
|
?: return Result.success(errorOutputData).also {
|
||||||
|
cryptoStore.updateGossipingRequestState(params.request, GossipingRequestState.FAILED_TO_ACCEPTED)
|
||||||
|
Timber.e("Unknown deviceInfo, cannot send message, sessionId: ${params.request.deviceId}")
|
||||||
|
}
|
||||||
|
|
||||||
|
val sendToDeviceMap = MXUsersDevicesMap<Any>()
|
||||||
|
|
||||||
|
val devicesByUser = mapOf(requestingUserId to listOf(deviceInfo))
|
||||||
|
val usersDeviceMap = ensureOlmSessionsForDevicesAction.handle(devicesByUser)
|
||||||
|
val olmSessionResult = usersDeviceMap.getObject(requestingUserId, requestingDeviceId)
|
||||||
|
if (olmSessionResult?.sessionId == null) {
|
||||||
|
// no session with this device, probably because there
|
||||||
|
// were no one-time keys.
|
||||||
|
return Result.success(errorOutputData).also {
|
||||||
|
cryptoStore.updateGossipingRequestState(params.request, GossipingRequestState.FAILED_TO_ACCEPTED)
|
||||||
|
Timber.e("no session with this device, probably because there were no one-time keys.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val payloadJson = mapOf(
|
||||||
|
"type" to EventType.SEND_SECRET,
|
||||||
|
"content" to toDeviceContent.toContent()
|
||||||
|
)
|
||||||
|
|
||||||
|
try {
|
||||||
|
val encodedPayload = messageEncrypter.encryptMessage(payloadJson, listOf(deviceInfo))
|
||||||
|
sendToDeviceMap.setObject(requestingUserId, requestingDeviceId, encodedPayload)
|
||||||
|
} catch (failure: Throwable) {
|
||||||
|
Timber.e("## Fail to encrypt gossip + ${failure.localizedMessage}")
|
||||||
|
}
|
||||||
|
|
||||||
|
cryptoStore.saveGossipingEvent(Event(
|
||||||
|
type = eventType,
|
||||||
|
content = toDeviceContent.toContent(),
|
||||||
|
senderId = credentials.userId
|
||||||
|
).also {
|
||||||
|
it.ageLocalTs = System.currentTimeMillis()
|
||||||
|
})
|
||||||
|
|
||||||
|
try {
|
||||||
|
sendToDeviceTask.execute(
|
||||||
|
SendToDeviceTask.Params(
|
||||||
|
eventType = EventType.ENCRYPTED,
|
||||||
|
contentMap = sendToDeviceMap,
|
||||||
|
transactionId = localId
|
||||||
|
)
|
||||||
|
)
|
||||||
|
cryptoStore.updateGossipingRequestState(params.request, GossipingRequestState.ACCEPTED)
|
||||||
|
return Result.success()
|
||||||
|
} catch (exception: Throwable) {
|
||||||
|
return if (exception.shouldBeRetried()) {
|
||||||
|
Result.retry()
|
||||||
|
} else {
|
||||||
|
cryptoStore.updateGossipingRequestState(params.request, GossipingRequestState.FAILED_TO_ACCEPTED)
|
||||||
|
Result.success(errorOutputData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -20,7 +20,7 @@ import androidx.annotation.WorkerThread
|
||||||
import im.vector.matrix.android.api.listeners.ProgressListener
|
import im.vector.matrix.android.api.listeners.ProgressListener
|
||||||
import im.vector.matrix.android.internal.crypto.MXOlmDevice
|
import im.vector.matrix.android.internal.crypto.MXOlmDevice
|
||||||
import im.vector.matrix.android.internal.crypto.MegolmSessionData
|
import im.vector.matrix.android.internal.crypto.MegolmSessionData
|
||||||
import im.vector.matrix.android.internal.crypto.OutgoingRoomKeyRequestManager
|
import im.vector.matrix.android.internal.crypto.OutgoingGossipingRequestManager
|
||||||
import im.vector.matrix.android.internal.crypto.RoomDecryptorProvider
|
import im.vector.matrix.android.internal.crypto.RoomDecryptorProvider
|
||||||
import im.vector.matrix.android.internal.crypto.model.ImportRoomKeysResult
|
import im.vector.matrix.android.internal.crypto.model.ImportRoomKeysResult
|
||||||
import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody
|
import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody
|
||||||
|
@ -30,7 +30,7 @@ import javax.inject.Inject
|
||||||
|
|
||||||
internal class MegolmSessionDataImporter @Inject constructor(private val olmDevice: MXOlmDevice,
|
internal class MegolmSessionDataImporter @Inject constructor(private val olmDevice: MXOlmDevice,
|
||||||
private val roomDecryptorProvider: RoomDecryptorProvider,
|
private val roomDecryptorProvider: RoomDecryptorProvider,
|
||||||
private val outgoingRoomKeyRequestManager: OutgoingRoomKeyRequestManager,
|
private val outgoingGossipingRequestManager: OutgoingGossipingRequestManager,
|
||||||
private val cryptoStore: IMXCryptoStore) {
|
private val cryptoStore: IMXCryptoStore) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -73,7 +73,7 @@ internal class MegolmSessionDataImporter @Inject constructor(private val olmDevi
|
||||||
sessionId = megolmSessionData.sessionId
|
sessionId = megolmSessionData.sessionId
|
||||||
)
|
)
|
||||||
|
|
||||||
outgoingRoomKeyRequestManager.cancelRoomKeyRequest(roomKeyRequestBody)
|
outgoingGossipingRequestManager.cancelRoomKeyRequest(roomKeyRequestBody)
|
||||||
|
|
||||||
// Have another go at decrypting events sent with this session
|
// Have another go at decrypting events sent with this session
|
||||||
decrypting.onNewSession(megolmSessionData.senderKey!!, sessionId!!)
|
decrypting.onNewSession(megolmSessionData.senderKey!!, sessionId!!)
|
||||||
|
|
|
@ -19,6 +19,7 @@ package im.vector.matrix.android.internal.crypto.algorithms
|
||||||
|
|
||||||
import im.vector.matrix.android.api.session.events.model.Event
|
import im.vector.matrix.android.api.session.events.model.Event
|
||||||
import im.vector.matrix.android.internal.crypto.IncomingRoomKeyRequest
|
import im.vector.matrix.android.internal.crypto.IncomingRoomKeyRequest
|
||||||
|
import im.vector.matrix.android.internal.crypto.IncomingSecretShareRequest
|
||||||
import im.vector.matrix.android.internal.crypto.MXEventDecryptionResult
|
import im.vector.matrix.android.internal.crypto.MXEventDecryptionResult
|
||||||
import im.vector.matrix.android.internal.crypto.keysbackup.DefaultKeysBackupService
|
import im.vector.matrix.android.internal.crypto.keysbackup.DefaultKeysBackupService
|
||||||
|
|
||||||
|
@ -65,4 +66,8 @@ internal interface IMXDecrypting {
|
||||||
* @param request keyRequest
|
* @param request keyRequest
|
||||||
*/
|
*/
|
||||||
fun shareKeysWithDevice(request: IncomingRoomKeyRequest) {}
|
fun shareKeysWithDevice(request: IncomingRoomKeyRequest) {}
|
||||||
|
|
||||||
|
fun shareSecretWithDevice(request: IncomingSecretShareRequest, secretValue : String) {}
|
||||||
|
|
||||||
|
fun requestKeysForEvent(event: Event)
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,7 +26,7 @@ import im.vector.matrix.android.internal.crypto.IncomingRoomKeyRequest
|
||||||
import im.vector.matrix.android.internal.crypto.MXEventDecryptionResult
|
import im.vector.matrix.android.internal.crypto.MXEventDecryptionResult
|
||||||
import im.vector.matrix.android.internal.crypto.MXOlmDevice
|
import im.vector.matrix.android.internal.crypto.MXOlmDevice
|
||||||
import im.vector.matrix.android.internal.crypto.NewSessionListener
|
import im.vector.matrix.android.internal.crypto.NewSessionListener
|
||||||
import im.vector.matrix.android.internal.crypto.OutgoingRoomKeyRequestManager
|
import im.vector.matrix.android.internal.crypto.OutgoingGossipingRequestManager
|
||||||
import im.vector.matrix.android.internal.crypto.actions.EnsureOlmSessionsForDevicesAction
|
import im.vector.matrix.android.internal.crypto.actions.EnsureOlmSessionsForDevicesAction
|
||||||
import im.vector.matrix.android.internal.crypto.actions.MessageEncrypter
|
import im.vector.matrix.android.internal.crypto.actions.MessageEncrypter
|
||||||
import im.vector.matrix.android.internal.crypto.algorithms.IMXDecrypting
|
import im.vector.matrix.android.internal.crypto.algorithms.IMXDecrypting
|
||||||
|
@ -46,7 +46,7 @@ import timber.log.Timber
|
||||||
internal class MXMegolmDecryption(private val userId: String,
|
internal class MXMegolmDecryption(private val userId: String,
|
||||||
private val olmDevice: MXOlmDevice,
|
private val olmDevice: MXOlmDevice,
|
||||||
private val deviceListManager: DeviceListManager,
|
private val deviceListManager: DeviceListManager,
|
||||||
private val outgoingRoomKeyRequestManager: OutgoingRoomKeyRequestManager,
|
private val outgoingGossipingRequestManager: OutgoingGossipingRequestManager,
|
||||||
private val messageEncrypter: MessageEncrypter,
|
private val messageEncrypter: MessageEncrypter,
|
||||||
private val ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction,
|
private val ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction,
|
||||||
private val cryptoStore: IMXCryptoStore,
|
private val cryptoStore: IMXCryptoStore,
|
||||||
|
@ -144,23 +144,23 @@ internal class MXMegolmDecryption(private val userId: String,
|
||||||
*
|
*
|
||||||
* @param event the event
|
* @param event the event
|
||||||
*/
|
*/
|
||||||
private fun requestKeysForEvent(event: Event) {
|
override fun requestKeysForEvent(event: Event) {
|
||||||
val sender = event.senderId!!
|
val sender = event.senderId ?: return
|
||||||
val encryptedEventContent = event.content.toModel<EncryptedEventContent>()!!
|
val encryptedEventContent = event.content.toModel<EncryptedEventContent>()
|
||||||
|
val senderDevice = encryptedEventContent?.deviceId ?: return
|
||||||
|
|
||||||
val recipients = ArrayList<Map<String, String>>()
|
val recipients = if (event.senderId == userId) {
|
||||||
|
mapOf(
|
||||||
val selfMap = HashMap<String, String>()
|
userId to listOf("*")
|
||||||
// TODO Replace this hard coded keys (see OutgoingRoomKeyRequestManager)
|
)
|
||||||
selfMap["userId"] = userId
|
} else {
|
||||||
selfMap["deviceId"] = "*"
|
// for the case where you share the key with a device that has a broken olm session
|
||||||
recipients.add(selfMap)
|
// The other user might Re-shares a megolm session key with devices if the key has already been
|
||||||
|
// sent to them.
|
||||||
if (sender != userId) {
|
mapOf(
|
||||||
val senderMap = HashMap<String, String>()
|
userId to listOf("*"),
|
||||||
senderMap["userId"] = sender
|
sender to listOf(senderDevice)
|
||||||
senderMap["deviceId"] = encryptedEventContent.deviceId!!
|
)
|
||||||
recipients.add(senderMap)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val requestBody = RoomKeyRequestBody(
|
val requestBody = RoomKeyRequestBody(
|
||||||
|
@ -170,7 +170,7 @@ internal class MXMegolmDecryption(private val userId: String,
|
||||||
sessionId = encryptedEventContent.sessionId
|
sessionId = encryptedEventContent.sessionId
|
||||||
)
|
)
|
||||||
|
|
||||||
outgoingRoomKeyRequestManager.sendRoomKeyRequest(requestBody, recipients)
|
outgoingGossipingRequestManager.sendRoomKeyRequest(requestBody, recipients)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -271,7 +271,7 @@ internal class MXMegolmDecryption(private val userId: String,
|
||||||
senderKey = senderKey
|
senderKey = senderKey
|
||||||
)
|
)
|
||||||
|
|
||||||
outgoingRoomKeyRequestManager.cancelRoomKeyRequest(content)
|
outgoingGossipingRequestManager.cancelRoomKeyRequest(content)
|
||||||
|
|
||||||
onNewSession(senderKey, roomKeyContent.sessionId)
|
onNewSession(senderKey, roomKeyContent.sessionId)
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,7 +18,7 @@ package im.vector.matrix.android.internal.crypto.algorithms.megolm
|
||||||
|
|
||||||
import im.vector.matrix.android.internal.crypto.DeviceListManager
|
import im.vector.matrix.android.internal.crypto.DeviceListManager
|
||||||
import im.vector.matrix.android.internal.crypto.MXOlmDevice
|
import im.vector.matrix.android.internal.crypto.MXOlmDevice
|
||||||
import im.vector.matrix.android.internal.crypto.OutgoingRoomKeyRequestManager
|
import im.vector.matrix.android.internal.crypto.OutgoingGossipingRequestManager
|
||||||
import im.vector.matrix.android.internal.crypto.actions.EnsureOlmSessionsForDevicesAction
|
import im.vector.matrix.android.internal.crypto.actions.EnsureOlmSessionsForDevicesAction
|
||||||
import im.vector.matrix.android.internal.crypto.actions.MessageEncrypter
|
import im.vector.matrix.android.internal.crypto.actions.MessageEncrypter
|
||||||
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
|
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
|
||||||
|
@ -32,7 +32,7 @@ internal class MXMegolmDecryptionFactory @Inject constructor(
|
||||||
@UserId private val userId: String,
|
@UserId private val userId: String,
|
||||||
private val olmDevice: MXOlmDevice,
|
private val olmDevice: MXOlmDevice,
|
||||||
private val deviceListManager: DeviceListManager,
|
private val deviceListManager: DeviceListManager,
|
||||||
private val outgoingRoomKeyRequestManager: OutgoingRoomKeyRequestManager,
|
private val outgoingGossipingRequestManager: OutgoingGossipingRequestManager,
|
||||||
private val messageEncrypter: MessageEncrypter,
|
private val messageEncrypter: MessageEncrypter,
|
||||||
private val ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction,
|
private val ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction,
|
||||||
private val cryptoStore: IMXCryptoStore,
|
private val cryptoStore: IMXCryptoStore,
|
||||||
|
@ -46,7 +46,7 @@ internal class MXMegolmDecryptionFactory @Inject constructor(
|
||||||
userId,
|
userId,
|
||||||
olmDevice,
|
olmDevice,
|
||||||
deviceListManager,
|
deviceListManager,
|
||||||
outgoingRoomKeyRequestManager,
|
outgoingGossipingRequestManager,
|
||||||
messageEncrypter,
|
messageEncrypter,
|
||||||
ensureOlmSessionsForDevicesAction,
|
ensureOlmSessionsForDevicesAction,
|
||||||
cryptoStore,
|
cryptoStore,
|
||||||
|
|
|
@ -210,4 +210,8 @@ internal class MXOlmDecryption(
|
||||||
|
|
||||||
return res["payload"]
|
return res["payload"]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun requestKeysForEvent(event: Event) {
|
||||||
|
// nop
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,21 +17,17 @@
|
||||||
package im.vector.matrix.android.internal.crypto.crosssigning
|
package im.vector.matrix.android.internal.crypto.crosssigning
|
||||||
|
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import dagger.Lazy
|
|
||||||
import im.vector.matrix.android.api.MatrixCallback
|
import im.vector.matrix.android.api.MatrixCallback
|
||||||
import im.vector.matrix.android.api.session.crypto.crosssigning.CrossSigningService
|
import im.vector.matrix.android.api.session.crypto.crosssigning.CrossSigningService
|
||||||
import im.vector.matrix.android.api.session.crypto.crosssigning.MXCrossSigningInfo
|
import im.vector.matrix.android.api.session.crypto.crosssigning.MXCrossSigningInfo
|
||||||
import im.vector.matrix.android.api.util.Optional
|
import im.vector.matrix.android.api.util.Optional
|
||||||
import im.vector.matrix.android.internal.crypto.DeviceListManager
|
import im.vector.matrix.android.internal.crypto.DeviceListManager
|
||||||
import im.vector.matrix.android.internal.crypto.MXOlmDevice
|
|
||||||
import im.vector.matrix.android.internal.crypto.MyDeviceInfoHolder
|
|
||||||
import im.vector.matrix.android.internal.crypto.model.CryptoCrossSigningKey
|
|
||||||
import im.vector.matrix.android.internal.crypto.model.KeyUsage
|
|
||||||
import im.vector.matrix.android.internal.crypto.model.rest.UploadSignatureQueryBuilder
|
import im.vector.matrix.android.internal.crypto.model.rest.UploadSignatureQueryBuilder
|
||||||
import im.vector.matrix.android.internal.crypto.model.rest.UserPasswordAuth
|
import im.vector.matrix.android.internal.crypto.model.rest.UserPasswordAuth
|
||||||
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
|
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
|
||||||
|
import im.vector.matrix.android.internal.crypto.store.PrivateKeysInfo
|
||||||
|
import im.vector.matrix.android.internal.crypto.tasks.InitializeCrossSigningTask
|
||||||
import im.vector.matrix.android.internal.crypto.tasks.UploadSignaturesTask
|
import im.vector.matrix.android.internal.crypto.tasks.UploadSignaturesTask
|
||||||
import im.vector.matrix.android.internal.crypto.tasks.UploadSigningKeysTask
|
|
||||||
import im.vector.matrix.android.internal.di.UserId
|
import im.vector.matrix.android.internal.di.UserId
|
||||||
import im.vector.matrix.android.internal.session.SessionScope
|
import im.vector.matrix.android.internal.session.SessionScope
|
||||||
import im.vector.matrix.android.internal.task.TaskExecutor
|
import im.vector.matrix.android.internal.task.TaskExecutor
|
||||||
|
@ -52,12 +48,9 @@ import javax.inject.Inject
|
||||||
internal class DefaultCrossSigningService @Inject constructor(
|
internal class DefaultCrossSigningService @Inject constructor(
|
||||||
@UserId private val userId: String,
|
@UserId private val userId: String,
|
||||||
private val cryptoStore: IMXCryptoStore,
|
private val cryptoStore: IMXCryptoStore,
|
||||||
private val myDeviceInfoHolder: Lazy<MyDeviceInfoHolder>,
|
|
||||||
private val olmDevice: MXOlmDevice,
|
|
||||||
private val deviceListManager: DeviceListManager,
|
private val deviceListManager: DeviceListManager,
|
||||||
private val uploadSigningKeysTask: UploadSigningKeysTask,
|
private val initializeCrossSigningTask: InitializeCrossSigningTask,
|
||||||
private val uploadSignaturesTask: UploadSignaturesTask,
|
private val uploadSignaturesTask: UploadSignaturesTask,
|
||||||
private val computeTrustTask: ComputeTrustTask,
|
|
||||||
private val taskExecutor: TaskExecutor,
|
private val taskExecutor: TaskExecutor,
|
||||||
private val coroutineDispatchers: MatrixCoroutineDispatchers,
|
private val coroutineDispatchers: MatrixCoroutineDispatchers,
|
||||||
private val cryptoCoroutineScope: CoroutineScope,
|
private val cryptoCoroutineScope: CoroutineScope,
|
||||||
|
@ -150,151 +143,82 @@ internal class DefaultCrossSigningService @Inject constructor(
|
||||||
override fun initializeCrossSigning(authParams: UserPasswordAuth?, callback: MatrixCallback<Unit>?) {
|
override fun initializeCrossSigning(authParams: UserPasswordAuth?, callback: MatrixCallback<Unit>?) {
|
||||||
Timber.d("## CrossSigning initializeCrossSigning")
|
Timber.d("## CrossSigning initializeCrossSigning")
|
||||||
|
|
||||||
// =================
|
val params = InitializeCrossSigningTask.Params(
|
||||||
// MASTER KEY
|
authParams = authParams
|
||||||
// =================
|
|
||||||
val masterPkOlm = OlmPkSigning()
|
|
||||||
val masterKeyPrivateKey = OlmPkSigning.generateSeed()
|
|
||||||
val masterPublicKey = masterPkOlm.initWithSeed(masterKeyPrivateKey)
|
|
||||||
|
|
||||||
Timber.v("## CrossSigning - masterPublicKey:$masterPublicKey")
|
|
||||||
|
|
||||||
// =================
|
|
||||||
// USER KEY
|
|
||||||
// =================
|
|
||||||
val userSigningPkOlm = OlmPkSigning()
|
|
||||||
val uskPrivateKey = OlmPkSigning.generateSeed()
|
|
||||||
val uskPublicKey = userSigningPkOlm.initWithSeed(uskPrivateKey)
|
|
||||||
|
|
||||||
Timber.v("## CrossSigning - uskPublicKey:$uskPublicKey")
|
|
||||||
|
|
||||||
// Sign userSigningKey with master
|
|
||||||
val signedUSK = CryptoCrossSigningKey.Builder(userId, KeyUsage.USER_SIGNING)
|
|
||||||
.key(uskPublicKey)
|
|
||||||
.build()
|
|
||||||
.canonicalSignable()
|
|
||||||
.let { masterPkOlm.sign(it) }
|
|
||||||
|
|
||||||
// =================
|
|
||||||
// SELF SIGNING KEY
|
|
||||||
// =================
|
|
||||||
val selfSigningPkOlm = OlmPkSigning()
|
|
||||||
val sskPrivateKey = OlmPkSigning.generateSeed()
|
|
||||||
val sskPublicKey = selfSigningPkOlm.initWithSeed(sskPrivateKey)
|
|
||||||
|
|
||||||
Timber.v("## CrossSigning - sskPublicKey:$sskPublicKey")
|
|
||||||
|
|
||||||
// Sign userSigningKey with master
|
|
||||||
val signedSSK = JsonCanonicalizer.getCanonicalJson(Map::class.java, CryptoCrossSigningKey.Builder(userId, KeyUsage.SELF_SIGNING)
|
|
||||||
.key(sskPublicKey)
|
|
||||||
.build().signalableJSONDictionary()).let { masterPkOlm.sign(it) }
|
|
||||||
|
|
||||||
// I need to upload the keys
|
|
||||||
val mskCrossSigningKeyInfo = CryptoCrossSigningKey.Builder(userId, KeyUsage.MASTER)
|
|
||||||
.key(masterPublicKey)
|
|
||||||
.build()
|
|
||||||
val params = UploadSigningKeysTask.Params(
|
|
||||||
masterKey = mskCrossSigningKeyInfo,
|
|
||||||
userKey = CryptoCrossSigningKey.Builder(userId, KeyUsage.USER_SIGNING)
|
|
||||||
.key(uskPublicKey)
|
|
||||||
.signature(userId, masterPublicKey, signedUSK)
|
|
||||||
.build(),
|
|
||||||
selfSignedKey = CryptoCrossSigningKey.Builder(userId, KeyUsage.SELF_SIGNING)
|
|
||||||
.key(sskPublicKey)
|
|
||||||
.signature(userId, masterPublicKey, signedSSK)
|
|
||||||
.build(),
|
|
||||||
userPasswordAuth = authParams
|
|
||||||
)
|
)
|
||||||
|
initializeCrossSigningTask.configureWith(params) {
|
||||||
this.masterPkSigning = masterPkOlm
|
this.callbackThread = TaskThread.CRYPTO
|
||||||
this.userPkSigning = userSigningPkOlm
|
this.callback = object : MatrixCallback<InitializeCrossSigningTask.Result> {
|
||||||
this.selfSigningPkSigning = selfSigningPkOlm
|
override fun onFailure(failure: Throwable) {
|
||||||
|
callback?.onFailure(failure)
|
||||||
val crossSigningInfo = MXCrossSigningInfo(userId, listOf(params.masterKey, params.userKey, params.selfSignedKey))
|
|
||||||
cryptoStore.setMyCrossSigningInfo(crossSigningInfo)
|
|
||||||
setUserKeysAsTrusted(userId, true)
|
|
||||||
cryptoStore.storePrivateKeysInfo(masterKeyPrivateKey?.toBase64NoPadding(), uskPrivateKey?.toBase64NoPadding(), sskPrivateKey?.toBase64NoPadding())
|
|
||||||
|
|
||||||
uploadSigningKeysTask.configureWith(params) {
|
|
||||||
this.executionThread = TaskThread.CRYPTO
|
|
||||||
this.callback = object : MatrixCallback<Unit> {
|
|
||||||
override fun onSuccess(data: Unit) {
|
|
||||||
Timber.i("## CrossSigning - Keys successfully uploaded")
|
|
||||||
|
|
||||||
// Sign the current device with SSK
|
|
||||||
val uploadSignatureQueryBuilder = UploadSignatureQueryBuilder()
|
|
||||||
|
|
||||||
val myDevice = myDeviceInfoHolder.get().myDevice
|
|
||||||
val canonicalJson = JsonCanonicalizer.getCanonicalJson(Map::class.java, myDevice.signalableJSONDictionary())
|
|
||||||
val signedDevice = selfSigningPkOlm.sign(canonicalJson)
|
|
||||||
val updateSignatures = (myDevice.signatures?.toMutableMap() ?: HashMap())
|
|
||||||
.also {
|
|
||||||
it[userId] = (it[userId]
|
|
||||||
?: HashMap()) + mapOf("ed25519:$sskPublicKey" to signedDevice)
|
|
||||||
}
|
|
||||||
myDevice.copy(signatures = updateSignatures).let {
|
|
||||||
uploadSignatureQueryBuilder.withDeviceInfo(it)
|
|
||||||
}
|
|
||||||
|
|
||||||
// sign MSK with device key (migration) and upload signatures
|
|
||||||
val message = JsonCanonicalizer.getCanonicalJson(Map::class.java, mskCrossSigningKeyInfo.signalableJSONDictionary())
|
|
||||||
olmDevice.signMessage(message)?.let { sign ->
|
|
||||||
val mskUpdatedSignatures = (mskCrossSigningKeyInfo.signatures?.toMutableMap()
|
|
||||||
?: HashMap()).also {
|
|
||||||
it[userId] = (it[userId]
|
|
||||||
?: HashMap()) + mapOf("ed25519:${myDevice.deviceId}" to sign)
|
|
||||||
}
|
|
||||||
mskCrossSigningKeyInfo.copy(
|
|
||||||
signatures = mskUpdatedSignatures
|
|
||||||
).let {
|
|
||||||
uploadSignatureQueryBuilder.withSigningKeyInfo(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
resetTrustOnKeyChange()
|
|
||||||
uploadSignaturesTask.configureWith(UploadSignaturesTask.Params(uploadSignatureQueryBuilder.build())) {
|
|
||||||
// this.retryCount = 3
|
|
||||||
this.executionThread = TaskThread.CRYPTO
|
|
||||||
this.callback = object : MatrixCallback<Unit> {
|
|
||||||
override fun onSuccess(data: Unit) {
|
|
||||||
Timber.i("## CrossSigning - signatures successfully uploaded")
|
|
||||||
callback?.onSuccess(Unit)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onFailure(failure: Throwable) {
|
|
||||||
// Clear
|
|
||||||
Timber.e(failure, "## CrossSigning - Failed to upload signatures")
|
|
||||||
clearSigningKeys()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}.executeBy(taskExecutor)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onFailure(failure: Throwable) {
|
override fun onSuccess(data: InitializeCrossSigningTask.Result) {
|
||||||
Timber.e(failure, "## CrossSigning - Failed to upload signing keys")
|
val crossSigningInfo = MXCrossSigningInfo(userId, listOf(data.masterKeyInfo, data.userKeyInfo, data.selfSignedKeyInfo))
|
||||||
clearSigningKeys()
|
cryptoStore.setMyCrossSigningInfo(crossSigningInfo)
|
||||||
callback?.onFailure(failure)
|
setUserKeysAsTrusted(userId, true)
|
||||||
|
cryptoStore.storePrivateKeysInfo(data.masterKeyPK, data.userKeyPK, data.selfSigningKeyPK)
|
||||||
|
masterPkSigning = OlmPkSigning().apply { initWithSeed(data.masterKeyPK.fromBase64()) }
|
||||||
|
userPkSigning = OlmPkSigning().apply { initWithSeed(data.userKeyPK.fromBase64()) }
|
||||||
|
selfSigningPkSigning = OlmPkSigning().apply { initWithSeed(data.selfSigningKeyPK.fromBase64()) }
|
||||||
|
|
||||||
|
callback?.onSuccess(Unit)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}.executeBy(taskExecutor)
|
}.executeBy(taskExecutor)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun clearSigningKeys() {
|
override fun onSecretSSKGossip(sskPrivateKey: String) {
|
||||||
masterPkSigning?.releaseSigning()
|
Timber.i("## CrossSigning - onSecretSSKGossip")
|
||||||
userPkSigning?.releaseSigning()
|
val mxCrossSigningInfo = getMyCrossSigningKeys() ?: return Unit.also {
|
||||||
selfSigningPkSigning?.releaseSigning()
|
Timber.e("## CrossSigning - onSecretSSKGossip() received secret but public key is not known")
|
||||||
|
}
|
||||||
|
|
||||||
masterPkSigning = null
|
sskPrivateKey.fromBase64()
|
||||||
userPkSigning = null
|
.let { privateKeySeed ->
|
||||||
selfSigningPkSigning = null
|
val pkSigning = OlmPkSigning()
|
||||||
|
try {
|
||||||
cryptoStore.setMyCrossSigningInfo(null)
|
if (pkSigning.initWithSeed(privateKeySeed) == mxCrossSigningInfo.selfSigningKey()?.unpaddedBase64PublicKey) {
|
||||||
cryptoStore.storePrivateKeysInfo(null, null, null)
|
selfSigningPkSigning?.releaseSigning()
|
||||||
|
selfSigningPkSigning = pkSigning
|
||||||
|
Timber.i("## CrossSigning - Loading SSK success")
|
||||||
|
cryptoStore.storeSSKPrivateKey(sskPrivateKey)
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
Timber.e("## CrossSigning - onSecretSSKGossip() private key do not match public key")
|
||||||
|
pkSigning.releaseSigning()
|
||||||
|
}
|
||||||
|
} catch (failure: Throwable) {
|
||||||
|
Timber.e("## CrossSigning - onSecretSSKGossip() ${failure.localizedMessage}")
|
||||||
|
pkSigning.releaseSigning()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun resetTrustOnKeyChange() {
|
override fun onSecretUSKGossip(uskPrivateKey: String) {
|
||||||
Timber.i("## CrossSigning - Clear all other user trust")
|
Timber.i("## CrossSigning - onSecretUSKGossip")
|
||||||
cryptoStore.clearOtherUserTrust()
|
val mxCrossSigningInfo = getMyCrossSigningKeys() ?: return Unit.also {
|
||||||
|
Timber.e("## CrossSigning - onSecretUSKGossip() received secret but public key is not knwow ")
|
||||||
|
}
|
||||||
|
|
||||||
|
uskPrivateKey.fromBase64()
|
||||||
|
.let { privateKeySeed ->
|
||||||
|
val pkSigning = OlmPkSigning()
|
||||||
|
try {
|
||||||
|
if (pkSigning.initWithSeed(privateKeySeed) == mxCrossSigningInfo.userKey()?.unpaddedBase64PublicKey) {
|
||||||
|
userPkSigning?.releaseSigning()
|
||||||
|
userPkSigning = pkSigning
|
||||||
|
Timber.i("## CrossSigning - Loading USK success")
|
||||||
|
cryptoStore.storeUSKPrivateKey(uskPrivateKey)
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
Timber.e("## CrossSigning - onSecretUSKGossip() private key do not match public key")
|
||||||
|
pkSigning.releaseSigning()
|
||||||
|
}
|
||||||
|
} catch (failure: Throwable) {
|
||||||
|
pkSigning.releaseSigning()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun checkTrustFromPrivateKeys(masterKeyPrivateKey: String?,
|
override fun checkTrustFromPrivateKeys(masterKeyPrivateKey: String?,
|
||||||
|
@ -396,7 +320,7 @@ internal class DefaultCrossSigningService @Inject constructor(
|
||||||
* Will not force a download of the key, but will verify signatures trust chain
|
* Will not force a download of the key, but will verify signatures trust chain
|
||||||
*/
|
*/
|
||||||
override fun checkUserTrust(otherUserId: String): UserTrustResult {
|
override fun checkUserTrust(otherUserId: String): UserTrustResult {
|
||||||
Timber.d("## CrossSigning checkUserTrust for $otherUserId")
|
Timber.v("## CrossSigning checkUserTrust for $otherUserId")
|
||||||
if (otherUserId == userId) {
|
if (otherUserId == userId) {
|
||||||
return checkSelfTrust()
|
return checkSelfTrust()
|
||||||
}
|
}
|
||||||
|
@ -542,102 +466,113 @@ internal class DefaultCrossSigningService @Inject constructor(
|
||||||
return cryptoStore.getMyCrossSigningInfo()
|
return cryptoStore.getMyCrossSigningInfo()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun getCrossSigningPrivateKeys(): PrivateKeysInfo? {
|
||||||
|
return cryptoStore.getCrossSigningPrivateKeys()
|
||||||
|
}
|
||||||
|
|
||||||
override fun canCrossSign(): Boolean {
|
override fun canCrossSign(): Boolean {
|
||||||
return checkSelfTrust().isVerified() && cryptoStore.getCrossSigningPrivateKeys()?.selfSigned != null
|
return checkSelfTrust().isVerified() && cryptoStore.getCrossSigningPrivateKeys()?.selfSigned != null
|
||||||
|
&& cryptoStore.getCrossSigningPrivateKeys()?.user != null
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun trustUser(otherUserId: String, callback: MatrixCallback<Unit>) {
|
override fun trustUser(otherUserId: String, callback: MatrixCallback<Unit>) {
|
||||||
Timber.d("## CrossSigning - Mark user $userId as trusted ")
|
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
|
||||||
// We should have this user keys
|
Timber.d("## CrossSigning - Mark user $userId as trusted ")
|
||||||
val otherMasterKeys = getUserCrossSigningKeys(otherUserId)?.masterKey()
|
// We should have this user keys
|
||||||
if (otherMasterKeys == null) {
|
val otherMasterKeys = getUserCrossSigningKeys(otherUserId)?.masterKey()
|
||||||
callback.onFailure(Throwable("## CrossSigning - Other master signing key is not known"))
|
if (otherMasterKeys == null) {
|
||||||
return
|
callback.onFailure(Throwable("## CrossSigning - Other master signing key is not known"))
|
||||||
}
|
return@launch
|
||||||
val myKeys = getUserCrossSigningKeys(userId)
|
}
|
||||||
if (myKeys == null) {
|
val myKeys = getUserCrossSigningKeys(userId)
|
||||||
callback.onFailure(Throwable("## CrossSigning - CrossSigning is not setup for this account"))
|
if (myKeys == null) {
|
||||||
return
|
callback.onFailure(Throwable("## CrossSigning - CrossSigning is not setup for this account"))
|
||||||
}
|
return@launch
|
||||||
val userPubKey = myKeys.userKey()?.unpaddedBase64PublicKey
|
}
|
||||||
if (userPubKey == null || userPkSigning == null) {
|
val userPubKey = myKeys.userKey()?.unpaddedBase64PublicKey
|
||||||
callback.onFailure(Throwable("## CrossSigning - Cannot sign from this account, privateKeyUnknown $userPubKey"))
|
if (userPubKey == null || userPkSigning == null) {
|
||||||
return
|
callback.onFailure(Throwable("## CrossSigning - Cannot sign from this account, privateKeyUnknown $userPubKey"))
|
||||||
}
|
return@launch
|
||||||
|
}
|
||||||
|
|
||||||
// Sign the other MasterKey with our UserSigning key
|
// Sign the other MasterKey with our UserSigning key
|
||||||
val newSignature = JsonCanonicalizer.getCanonicalJson(Map::class.java,
|
val newSignature = JsonCanonicalizer.getCanonicalJson(Map::class.java,
|
||||||
otherMasterKeys.signalableJSONDictionary()).let { userPkSigning?.sign(it) }
|
otherMasterKeys.signalableJSONDictionary()).let { userPkSigning?.sign(it) }
|
||||||
|
|
||||||
if (newSignature == null) {
|
if (newSignature == null) {
|
||||||
// race??
|
// race??
|
||||||
callback.onFailure(Throwable("## CrossSigning - Failed to sign"))
|
callback.onFailure(Throwable("## CrossSigning - Failed to sign"))
|
||||||
return
|
return@launch
|
||||||
|
}
|
||||||
|
|
||||||
|
cryptoStore.setUserKeysAsTrusted(otherUserId, true)
|
||||||
|
// TODO update local copy with new signature directly here? kind of local echo of trust?
|
||||||
|
|
||||||
|
Timber.d("## CrossSigning - Upload signature of $userId MSK signed by USK")
|
||||||
|
val uploadQuery = UploadSignatureQueryBuilder()
|
||||||
|
.withSigningKeyInfo(otherMasterKeys.copyForSignature(userId, userPubKey, newSignature))
|
||||||
|
.build()
|
||||||
|
uploadSignaturesTask.configureWith(UploadSignaturesTask.Params(uploadQuery)) {
|
||||||
|
this.executionThread = TaskThread.CRYPTO
|
||||||
|
this.callback = callback
|
||||||
|
}.executeBy(taskExecutor)
|
||||||
}
|
}
|
||||||
|
|
||||||
cryptoStore.setUserKeysAsTrusted(otherUserId, true)
|
|
||||||
// TODO update local copy with new signature directly here? kind of local echo of trust?
|
|
||||||
|
|
||||||
Timber.d("## CrossSigning - Upload signature of $userId MSK signed by USK")
|
|
||||||
val uploadQuery = UploadSignatureQueryBuilder()
|
|
||||||
.withSigningKeyInfo(otherMasterKeys.copyForSignature(userId, userPubKey, newSignature))
|
|
||||||
.build()
|
|
||||||
uploadSignaturesTask.configureWith(UploadSignaturesTask.Params(uploadQuery)) {
|
|
||||||
this.executionThread = TaskThread.CRYPTO
|
|
||||||
this.callback = callback
|
|
||||||
}.executeBy(taskExecutor)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun markMyMasterKeyAsTrusted() {
|
override fun markMyMasterKeyAsTrusted() {
|
||||||
cryptoStore.markMyMasterKeyAsLocallyTrusted(true)
|
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
|
||||||
checkSelfTrust()
|
cryptoStore.markMyMasterKeyAsLocallyTrusted(true)
|
||||||
|
checkSelfTrust()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun trustDevice(deviceId: String, callback: MatrixCallback<Unit>) {
|
override fun trustDevice(deviceId: String, callback: MatrixCallback<Unit>) {
|
||||||
// This device should be yours
|
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
|
||||||
val device = cryptoStore.getUserDevice(userId, deviceId)
|
// This device should be yours
|
||||||
if (device == null) {
|
val device = cryptoStore.getUserDevice(userId, deviceId)
|
||||||
callback.onFailure(IllegalArgumentException("This device [$deviceId] is not known, or not yours"))
|
if (device == null) {
|
||||||
return
|
callback.onFailure(IllegalArgumentException("This device [$deviceId] is not known, or not yours"))
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
|
||||||
|
val myKeys = getUserCrossSigningKeys(userId)
|
||||||
|
if (myKeys == null) {
|
||||||
|
callback.onFailure(Throwable("CrossSigning is not setup for this account"))
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
|
||||||
|
val ssPubKey = myKeys.selfSigningKey()?.unpaddedBase64PublicKey
|
||||||
|
if (ssPubKey == null || selfSigningPkSigning == null) {
|
||||||
|
callback.onFailure(Throwable("Cannot sign from this account, public and/or privateKey Unknown $ssPubKey"))
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sign with self signing
|
||||||
|
val newSignature = selfSigningPkSigning?.sign(device.canonicalSignable())
|
||||||
|
|
||||||
|
if (newSignature == null) {
|
||||||
|
// race??
|
||||||
|
callback.onFailure(Throwable("Failed to sign"))
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
val toUpload = device.copy(
|
||||||
|
signatures = mapOf(
|
||||||
|
userId
|
||||||
|
to
|
||||||
|
mapOf(
|
||||||
|
"ed25519:$ssPubKey" to newSignature
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
val uploadQuery = UploadSignatureQueryBuilder()
|
||||||
|
.withDeviceInfo(toUpload)
|
||||||
|
.build()
|
||||||
|
uploadSignaturesTask.configureWith(UploadSignaturesTask.Params(uploadQuery)) {
|
||||||
|
this.executionThread = TaskThread.CRYPTO
|
||||||
|
this.callback = callback
|
||||||
|
}.executeBy(taskExecutor)
|
||||||
}
|
}
|
||||||
|
|
||||||
val myKeys = getUserCrossSigningKeys(userId)
|
|
||||||
if (myKeys == null) {
|
|
||||||
callback.onFailure(Throwable("CrossSigning is not setup for this account"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
val ssPubKey = myKeys.selfSigningKey()?.unpaddedBase64PublicKey
|
|
||||||
if (ssPubKey == null || selfSigningPkSigning == null) {
|
|
||||||
callback.onFailure(Throwable("Cannot sign from this account, public and/or privateKey Unknown $ssPubKey"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sign with self signing
|
|
||||||
val newSignature = selfSigningPkSigning?.sign(device.canonicalSignable())
|
|
||||||
|
|
||||||
if (newSignature == null) {
|
|
||||||
// race??
|
|
||||||
callback.onFailure(Throwable("Failed to sign"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
val toUpload = device.copy(
|
|
||||||
signatures = mapOf(
|
|
||||||
userId
|
|
||||||
to
|
|
||||||
mapOf(
|
|
||||||
"ed25519:$ssPubKey" to newSignature
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
val uploadQuery = UploadSignatureQueryBuilder()
|
|
||||||
.withDeviceInfo(toUpload)
|
|
||||||
.build()
|
|
||||||
uploadSignaturesTask.configureWith(UploadSignaturesTask.Params(uploadQuery)) {
|
|
||||||
this.executionThread = TaskThread.CRYPTO
|
|
||||||
this.callback = callback
|
|
||||||
}.executeBy(taskExecutor)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun checkDeviceTrust(otherUserId: String, otherDeviceId: String, locallyTrusted: Boolean?): DeviceTrustResult {
|
override fun checkDeviceTrust(otherUserId: String, otherDeviceId: String, locallyTrusted: Boolean?): DeviceTrustResult {
|
||||||
|
@ -706,15 +641,20 @@ internal class DefaultCrossSigningService @Inject constructor(
|
||||||
Timber.d("## CrossSigning - onUsersDeviceUpdate for ${userIds.size} users")
|
Timber.d("## CrossSigning - onUsersDeviceUpdate for ${userIds.size} users")
|
||||||
userIds.forEach { otherUserId ->
|
userIds.forEach { otherUserId ->
|
||||||
checkUserTrust(otherUserId).let {
|
checkUserTrust(otherUserId).let {
|
||||||
Timber.d("## CrossSigning - update trust for $otherUserId , verified=${it.isVerified()}")
|
Timber.v("## CrossSigning - update trust for $otherUserId , verified=${it.isVerified()}")
|
||||||
setUserKeysAsTrusted(otherUserId, it.isVerified())
|
setUserKeysAsTrusted(otherUserId, it.isVerified())
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// now check device trust
|
||||||
|
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
|
||||||
|
userIds.forEach { otherUserId ->
|
||||||
// TODO if my keys have changes, i should recheck all devices of all users?
|
// TODO if my keys have changes, i should recheck all devices of all users?
|
||||||
val devices = cryptoStore.getUserDeviceList(otherUserId)
|
val devices = cryptoStore.getUserDeviceList(otherUserId)
|
||||||
devices?.forEach { device ->
|
devices?.forEach { device ->
|
||||||
val updatedTrust = checkDeviceTrust(otherUserId, device.deviceId, device.trustLevel?.isLocallyVerified() ?: false)
|
val updatedTrust = checkDeviceTrust(otherUserId, device.deviceId, device.trustLevel?.isLocallyVerified() ?: false)
|
||||||
Timber.d("## CrossSigning - update trust for device ${device.deviceId} of user $otherUserId , verified=$updatedTrust")
|
Timber.v("## CrossSigning - update trust for device ${device.deviceId} of user $otherUserId , verified=$updatedTrust")
|
||||||
cryptoStore.setDeviceTrust(otherUserId, device.deviceId, updatedTrust.isCrossSignedVerified(), updatedTrust.isLocallyVerified())
|
cryptoStore.setDeviceTrust(otherUserId, device.deviceId, updatedTrust.isCrossSignedVerified(), updatedTrust.isLocallyVerified())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -730,25 +670,39 @@ internal class DefaultCrossSigningService @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setUserKeysAsTrusted(otherUserId: String, trusted: Boolean) {
|
private fun setUserKeysAsTrusted(otherUserId: String, trusted: Boolean) {
|
||||||
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
|
val currentTrust = cryptoStore.getCrossSigningInfo(otherUserId)?.isTrusted()
|
||||||
val currentTrust = cryptoStore.getCrossSigningInfo(otherUserId)?.isTrusted()
|
cryptoStore.setUserKeysAsTrusted(otherUserId, trusted)
|
||||||
cryptoStore.setUserKeysAsTrusted(otherUserId, trusted)
|
// If it's me, recheck trust of all users and devices?
|
||||||
// If it's me, recheck trust of all users and devices?
|
val users = ArrayList<String>()
|
||||||
val users = ArrayList<String>()
|
if (otherUserId == userId && currentTrust != trusted) {
|
||||||
if (otherUserId == userId && currentTrust != trusted) {
|
// reRequestAllPendingRoomKeyRequest()
|
||||||
cryptoStore.updateUsersTrust {
|
cryptoStore.updateUsersTrust {
|
||||||
users.add(it)
|
users.add(it)
|
||||||
checkUserTrust(it).isVerified()
|
checkUserTrust(it).isVerified()
|
||||||
}
|
}
|
||||||
|
|
||||||
users.forEach {
|
users.forEach {
|
||||||
cryptoStore.getUserDeviceList(it)?.forEach { device ->
|
cryptoStore.getUserDeviceList(it)?.forEach { device ->
|
||||||
val updatedTrust = checkDeviceTrust(it, device.deviceId, device.trustLevel?.isLocallyVerified() ?: false)
|
val updatedTrust = checkDeviceTrust(it, device.deviceId, device.trustLevel?.isLocallyVerified() ?: false)
|
||||||
Timber.d("## CrossSigning - update trust for device ${device.deviceId} of user $otherUserId , verified=$updatedTrust")
|
Timber.v("## CrossSigning - update trust for device ${device.deviceId} of user $otherUserId , verified=$updatedTrust")
|
||||||
cryptoStore.setDeviceTrust(it, device.deviceId, updatedTrust.isCrossSignedVerified(), updatedTrust.isLocallyVerified())
|
cryptoStore.setDeviceTrust(it, device.deviceId, updatedTrust.isCrossSignedVerified(), updatedTrust.isLocallyVerified())
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// private fun reRequestAllPendingRoomKeyRequest() {
|
||||||
|
// cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
|
||||||
|
// Timber.d("## CrossSigning - reRequest pending outgoing room key requests")
|
||||||
|
// cryptoStore.getOutgoingRoomKeyRequests().forEach {
|
||||||
|
// it.requestBody?.let { requestBody ->
|
||||||
|
// if (cryptoStore.getInboundGroupSession(requestBody.sessionId ?: "", requestBody.senderKey ?: "") == null) {
|
||||||
|
// outgoingRoomKeyRequestManager.resendRoomKeyRequest(requestBody)
|
||||||
|
// } else {
|
||||||
|
// outgoingRoomKeyRequestManager.cancelRoomKeyRequest(requestBody)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
|
|
|
@ -36,6 +36,7 @@ import im.vector.matrix.android.internal.crypto.MXOlmDevice
|
||||||
import im.vector.matrix.android.internal.crypto.MegolmSessionData
|
import im.vector.matrix.android.internal.crypto.MegolmSessionData
|
||||||
import im.vector.matrix.android.internal.crypto.ObjectSigner
|
import im.vector.matrix.android.internal.crypto.ObjectSigner
|
||||||
import im.vector.matrix.android.internal.crypto.actions.MegolmSessionDataImporter
|
import im.vector.matrix.android.internal.crypto.actions.MegolmSessionDataImporter
|
||||||
|
import im.vector.matrix.android.internal.crypto.crosssigning.fromBase64
|
||||||
import im.vector.matrix.android.internal.crypto.keysbackup.model.KeysBackupVersionTrust
|
import im.vector.matrix.android.internal.crypto.keysbackup.model.KeysBackupVersionTrust
|
||||||
import im.vector.matrix.android.internal.crypto.keysbackup.model.KeysBackupVersionTrustSignature
|
import im.vector.matrix.android.internal.crypto.keysbackup.model.KeysBackupVersionTrustSignature
|
||||||
import im.vector.matrix.android.internal.crypto.keysbackup.model.MegolmBackupAuthData
|
import im.vector.matrix.android.internal.crypto.keysbackup.model.MegolmBackupAuthData
|
||||||
|
@ -67,6 +68,7 @@ import im.vector.matrix.android.internal.crypto.keysbackup.util.extractCurveKeyF
|
||||||
import im.vector.matrix.android.internal.crypto.model.ImportRoomKeysResult
|
import im.vector.matrix.android.internal.crypto.model.ImportRoomKeysResult
|
||||||
import im.vector.matrix.android.internal.crypto.model.OlmInboundGroupSessionWrapper
|
import im.vector.matrix.android.internal.crypto.model.OlmInboundGroupSessionWrapper
|
||||||
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
|
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
|
||||||
|
import im.vector.matrix.android.internal.crypto.store.SavedKeyBackupKeyInfo
|
||||||
import im.vector.matrix.android.internal.crypto.store.db.model.KeysBackupDataEntity
|
import im.vector.matrix.android.internal.crypto.store.db.model.KeysBackupDataEntity
|
||||||
import im.vector.matrix.android.internal.di.MoshiProvider
|
import im.vector.matrix.android.internal.di.MoshiProvider
|
||||||
import im.vector.matrix.android.internal.di.UserId
|
import im.vector.matrix.android.internal.di.UserId
|
||||||
|
@ -580,6 +582,31 @@ internal class DefaultKeysBackupService @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onSecretKeyGossip(secret: String) {
|
||||||
|
Timber.i("## CrossSigning - onSecretKeyGossip")
|
||||||
|
|
||||||
|
cryptoCoroutineScope.launch(coroutineDispatchers.main) {
|
||||||
|
try {
|
||||||
|
val keysBackupVersion = getKeysBackupLastVersionTask.execute(Unit)
|
||||||
|
val recoveryKey = computeRecoveryKey(secret.fromBase64())
|
||||||
|
if (isValidRecoveryKeyForKeysBackupVersion(recoveryKey, keysBackupVersion)) {
|
||||||
|
awaitCallback<Unit> {
|
||||||
|
trustKeysBackupVersion(keysBackupVersion, true, it)
|
||||||
|
}
|
||||||
|
val importResult = awaitCallback<ImportRoomKeysResult> {
|
||||||
|
restoreKeysWithRecoveryKey(keysBackupVersion, recoveryKey, null, null, null, it)
|
||||||
|
}
|
||||||
|
cryptoStore.saveBackupRecoveryKey(recoveryKey, keysBackupVersion.version)
|
||||||
|
Timber.i("onSecretKeyGossip: Recovered keys ${importResult.successfullyNumberOfImportedKeys} out of ${importResult.totalNumberOfKeys}")
|
||||||
|
} else {
|
||||||
|
Timber.e("onSecretKeyGossip: Recovery key is not valid ${keysBackupVersion.version}")
|
||||||
|
}
|
||||||
|
} catch (failure: Throwable) {
|
||||||
|
Timber.e("onSecretKeyGossip: failed to trust key backup version ${keysBackupVersion?.version}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get public key from a Recovery key
|
* Get public key from a Recovery key
|
||||||
*
|
*
|
||||||
|
@ -1391,6 +1418,14 @@ internal class DefaultKeysBackupService @Inject constructor(
|
||||||
.executeBy(taskExecutor)
|
.executeBy(taskExecutor)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun getKeyBackupRecoveryKeyInfo(): SavedKeyBackupKeyInfo? {
|
||||||
|
return cryptoStore.getKeyBackupRecoveryKeyInfo()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun saveBackupRecoveryKey(recoveryKey: String?, version: String?) {
|
||||||
|
cryptoStore.saveBackupRecoveryKey(recoveryKey, version)
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
// Maximum delay in ms in {@link maybeBackupKeys}
|
// Maximum delay in ms in {@link maybeBackupKeys}
|
||||||
private const val KEY_BACKUP_WAITING_TIME_TO_SEND_KEY_BACKUP_MILLIS = 10_000L
|
private const val KEY_BACKUP_WAITING_TIME_TO_SEND_KEY_BACKUP_MILLIS = 10_000L
|
||||||
|
|
|
@ -33,7 +33,7 @@ data class CryptoDeviceInfo(
|
||||||
) : CryptoInfo {
|
) : CryptoInfo {
|
||||||
|
|
||||||
val isVerified: Boolean
|
val isVerified: Boolean
|
||||||
get() = trustLevel?.isVerified() ?: false
|
get() = trustLevel?.isVerified() == true
|
||||||
|
|
||||||
val isUnknown: Boolean
|
val isUnknown: Boolean
|
||||||
get() = trustLevel == null
|
get() = trustLevel == null
|
||||||
|
|
|
@ -0,0 +1,28 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2020 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package im.vector.matrix.android.internal.crypto.model.event
|
||||||
|
|
||||||
|
import com.squareup.moshi.Json
|
||||||
|
import com.squareup.moshi.JsonClass
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class representing an encrypted event content
|
||||||
|
*/
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
|
data class SecretSendEventContent(
|
||||||
|
@Json(name = "request_id") val requestId: String,
|
||||||
|
@Json(name = "secret") val secretValue: String
|
||||||
|
)
|
|
@ -15,11 +15,14 @@
|
||||||
*/
|
*/
|
||||||
package im.vector.matrix.android.internal.crypto.model.rest
|
package im.vector.matrix.android.internal.crypto.model.rest
|
||||||
|
|
||||||
|
import com.squareup.moshi.Json
|
||||||
|
import com.squareup.moshi.JsonClass
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Interface representing an room key action request
|
* Interface representing an room key action request
|
||||||
* Note: this class cannot be abstract because of [org.matrix.androidsdk.core.JsonUtils.toRoomKeyShare]
|
* Note: this class cannot be abstract because of [org.matrix.androidsdk.core.JsonUtils.toRoomKeyShare]
|
||||||
*/
|
*/
|
||||||
internal interface RoomKeyShare : SendToDeviceObject {
|
interface GossipingToDeviceObject : SendToDeviceObject {
|
||||||
|
|
||||||
val action: String?
|
val action: String?
|
||||||
|
|
||||||
|
@ -32,3 +35,10 @@ internal interface RoomKeyShare : SendToDeviceObject {
|
||||||
const val ACTION_SHARE_CANCELLATION = "request_cancellation"
|
const val ACTION_SHARE_CANCELLATION = "request_cancellation"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
|
data class GossipingDefaultContent(
|
||||||
|
@Json(name = "action") override val action: String?,
|
||||||
|
@Json(name = "requesting_device_id") override val requestingDeviceId: String?,
|
||||||
|
@Json(name = "m.request_id") override val requestId: String? = null
|
||||||
|
) : GossipingToDeviceObject
|
|
@ -28,7 +28,7 @@ import com.squareup.moshi.JsonClass
|
||||||
* The user_signing_keys property will only be included when a user requests their own keys.
|
* The user_signing_keys property will only be included when a user requests their own keys.
|
||||||
*/
|
*/
|
||||||
@JsonClass(generateAdapter = true)
|
@JsonClass(generateAdapter = true)
|
||||||
internal data class KeysQueryResponse(
|
internal data class KeysQueryResponse(
|
||||||
/**
|
/**
|
||||||
* The device keys per devices per users.
|
* The device keys per devices per users.
|
||||||
* Map from userId to map from deviceId to MXDeviceInfo
|
* Map from userId to map from deviceId to MXDeviceInfo
|
||||||
|
|
|
@ -18,6 +18,7 @@ package im.vector.matrix.android.internal.crypto.model.rest
|
||||||
|
|
||||||
import com.squareup.moshi.Json
|
import com.squareup.moshi.Json
|
||||||
import com.squareup.moshi.JsonClass
|
import com.squareup.moshi.JsonClass
|
||||||
|
import im.vector.matrix.android.internal.di.MoshiProvider
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class representing an room key request body content
|
* Class representing an room key request body content
|
||||||
|
@ -35,4 +36,14 @@ data class RoomKeyRequestBody(
|
||||||
|
|
||||||
@Json(name = "session_id")
|
@Json(name = "session_id")
|
||||||
val sessionId: String? = null
|
val sessionId: String? = null
|
||||||
)
|
) {
|
||||||
|
fun toJson(): String {
|
||||||
|
return MoshiProvider.providesMoshi().adapter(RoomKeyRequestBody::class.java).toJson(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun fromJson(json: String?): RoomKeyRequestBody? {
|
||||||
|
return json?.let { MoshiProvider.providesMoshi().adapter(RoomKeyRequestBody::class.java).fromJson(it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -23,9 +23,9 @@ import com.squareup.moshi.JsonClass
|
||||||
* Class representing a room key request content
|
* Class representing a room key request content
|
||||||
*/
|
*/
|
||||||
@JsonClass(generateAdapter = true)
|
@JsonClass(generateAdapter = true)
|
||||||
internal data class RoomKeyShareRequest(
|
data class RoomKeyShareRequest(
|
||||||
@Json(name = "action")
|
@Json(name = "action")
|
||||||
override val action: String? = RoomKeyShare.ACTION_SHARE_REQUEST,
|
override val action: String? = GossipingToDeviceObject.ACTION_SHARE_REQUEST,
|
||||||
|
|
||||||
@Json(name = "requesting_device_id")
|
@Json(name = "requesting_device_id")
|
||||||
override val requestingDeviceId: String? = null,
|
override val requestingDeviceId: String? = null,
|
||||||
|
@ -35,4 +35,4 @@ internal data class RoomKeyShareRequest(
|
||||||
|
|
||||||
@Json(name = "body")
|
@Json(name = "body")
|
||||||
val body: RoomKeyRequestBody? = null
|
val body: RoomKeyRequestBody? = null
|
||||||
) : RoomKeyShare
|
) : GossipingToDeviceObject
|
||||||
|
|
|
@ -0,0 +1,37 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2020 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package im.vector.matrix.android.internal.crypto.model.rest
|
||||||
|
|
||||||
|
import com.squareup.moshi.Json
|
||||||
|
import com.squareup.moshi.JsonClass
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class representing a room key request content
|
||||||
|
*/
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
|
data class SecretShareRequest(
|
||||||
|
@Json(name = "action")
|
||||||
|
override val action: String? = GossipingToDeviceObject.ACTION_SHARE_REQUEST,
|
||||||
|
|
||||||
|
@Json(name = "requesting_device_id")
|
||||||
|
override val requestingDeviceId: String? = null,
|
||||||
|
|
||||||
|
@Json(name = "request_id")
|
||||||
|
override val requestId: String? = null,
|
||||||
|
|
||||||
|
@Json(name = "name")
|
||||||
|
val secretName: String? = null
|
||||||
|
) : GossipingToDeviceObject
|
|
@ -17,18 +17,19 @@ package im.vector.matrix.android.internal.crypto.model.rest
|
||||||
|
|
||||||
import com.squareup.moshi.Json
|
import com.squareup.moshi.Json
|
||||||
import com.squareup.moshi.JsonClass
|
import com.squareup.moshi.JsonClass
|
||||||
|
import im.vector.matrix.android.internal.crypto.model.rest.GossipingToDeviceObject.Companion.ACTION_SHARE_CANCELLATION
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class representing a room key request cancellation content
|
* Class representing a room key request cancellation content
|
||||||
*/
|
*/
|
||||||
@JsonClass(generateAdapter = true)
|
@JsonClass(generateAdapter = true)
|
||||||
internal data class RoomKeyShareCancellation(
|
internal data class ShareRequestCancellation(
|
||||||
@Json(name = "action")
|
@Json(name = "action")
|
||||||
override val action: String? = RoomKeyShare.ACTION_SHARE_CANCELLATION,
|
override val action: String? = ACTION_SHARE_CANCELLATION,
|
||||||
|
|
||||||
@Json(name = "requesting_device_id")
|
@Json(name = "requesting_device_id")
|
||||||
override val requestingDeviceId: String? = null,
|
override val requestingDeviceId: String? = null,
|
||||||
|
|
||||||
@Json(name = "request_id")
|
@Json(name = "request_id")
|
||||||
override val requestId: String? = null
|
override val requestId: String? = null
|
||||||
) : RoomKeyShare
|
) : GossipingToDeviceObject
|
|
@ -33,6 +33,7 @@ import im.vector.matrix.android.api.session.securestorage.SharedSecretStorageSer
|
||||||
import im.vector.matrix.android.api.session.securestorage.SsssKeyCreationInfo
|
import im.vector.matrix.android.api.session.securestorage.SsssKeyCreationInfo
|
||||||
import im.vector.matrix.android.api.session.securestorage.SsssKeySpec
|
import im.vector.matrix.android.api.session.securestorage.SsssKeySpec
|
||||||
import im.vector.matrix.android.api.session.securestorage.SsssPassphrase
|
import im.vector.matrix.android.api.session.securestorage.SsssPassphrase
|
||||||
|
import im.vector.matrix.android.internal.crypto.OutgoingGossipingRequestManager
|
||||||
import im.vector.matrix.android.internal.crypto.SSSS_ALGORITHM_AES_HMAC_SHA2
|
import im.vector.matrix.android.internal.crypto.SSSS_ALGORITHM_AES_HMAC_SHA2
|
||||||
import im.vector.matrix.android.internal.crypto.SSSS_ALGORITHM_CURVE25519_AES_SHA2
|
import im.vector.matrix.android.internal.crypto.SSSS_ALGORITHM_CURVE25519_AES_SHA2
|
||||||
import im.vector.matrix.android.internal.crypto.crosssigning.fromBase64
|
import im.vector.matrix.android.internal.crypto.crosssigning.fromBase64
|
||||||
|
@ -41,6 +42,7 @@ import im.vector.matrix.android.internal.crypto.keysbackup.generatePrivateKeyWit
|
||||||
import im.vector.matrix.android.internal.crypto.keysbackup.util.computeRecoveryKey
|
import im.vector.matrix.android.internal.crypto.keysbackup.util.computeRecoveryKey
|
||||||
import im.vector.matrix.android.internal.crypto.tools.HkdfSha256
|
import im.vector.matrix.android.internal.crypto.tools.HkdfSha256
|
||||||
import im.vector.matrix.android.internal.crypto.tools.withOlmDecryption
|
import im.vector.matrix.android.internal.crypto.tools.withOlmDecryption
|
||||||
|
import im.vector.matrix.android.internal.di.UserId
|
||||||
import im.vector.matrix.android.internal.extensions.foldToCallback
|
import im.vector.matrix.android.internal.extensions.foldToCallback
|
||||||
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
|
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
@ -55,7 +57,9 @@ import javax.inject.Inject
|
||||||
import kotlin.experimental.and
|
import kotlin.experimental.and
|
||||||
|
|
||||||
internal class DefaultSharedSecretStorageService @Inject constructor(
|
internal class DefaultSharedSecretStorageService @Inject constructor(
|
||||||
|
@UserId private val userId: String,
|
||||||
private val accountDataService: AccountDataService,
|
private val accountDataService: AccountDataService,
|
||||||
|
private val outgoingGossipingRequestManager: OutgoingGossipingRequestManager,
|
||||||
private val coroutineDispatchers: MatrixCoroutineDispatchers,
|
private val coroutineDispatchers: MatrixCoroutineDispatchers,
|
||||||
private val cryptoCoroutineScope: CoroutineScope
|
private val cryptoCoroutineScope: CoroutineScope
|
||||||
) : SharedSecretStorageService {
|
) : SharedSecretStorageService {
|
||||||
|
@ -98,7 +102,8 @@ internal class DefaultSharedSecretStorageService @Inject constructor(
|
||||||
callback.onSuccess(SsssKeyCreationInfo(
|
callback.onSuccess(SsssKeyCreationInfo(
|
||||||
keyId = keyId,
|
keyId = keyId,
|
||||||
content = storageKeyContent,
|
content = storageKeyContent,
|
||||||
recoveryKey = computeRecoveryKey(key)
|
recoveryKey = computeRecoveryKey(key),
|
||||||
|
keySpec = RawBytesKeySpec(key)
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -138,7 +143,8 @@ internal class DefaultSharedSecretStorageService @Inject constructor(
|
||||||
callback.onSuccess(SsssKeyCreationInfo(
|
callback.onSuccess(SsssKeyCreationInfo(
|
||||||
keyId = keyId,
|
keyId = keyId,
|
||||||
content = storageKeyContent,
|
content = storageKeyContent,
|
||||||
recoveryKey = computeRecoveryKey(privatePart.privateKey)
|
recoveryKey = computeRecoveryKey(privatePart.privateKey),
|
||||||
|
keySpec = RawBytesKeySpec(privatePart.privateKey)
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -268,7 +274,7 @@ internal class DefaultSharedSecretStorageService @Inject constructor(
|
||||||
val ivParameterSpec = IvParameterSpec(iv)
|
val ivParameterSpec = IvParameterSpec(iv)
|
||||||
cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, ivParameterSpec)
|
cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, ivParameterSpec)
|
||||||
// secret are not that big, just do Final
|
// secret are not that big, just do Final
|
||||||
val cipherBytes = cipher.doFinal(clearDataBase64.fromBase64())
|
val cipherBytes = cipher.doFinal(clearDataBase64.toByteArray())
|
||||||
require(cipherBytes.isNotEmpty())
|
require(cipherBytes.isNotEmpty())
|
||||||
|
|
||||||
val macKeySpec = SecretKeySpec(macKey, "HmacSHA256")
|
val macKeySpec = SecretKeySpec(macKey, "HmacSHA256")
|
||||||
|
@ -299,6 +305,15 @@ internal class DefaultSharedSecretStorageService @Inject constructor(
|
||||||
|
|
||||||
val cipherRawBytes = cipherContent.ciphertext?.fromBase64() ?: throw SharedSecretStorageError.BadCipherText
|
val cipherRawBytes = cipherContent.ciphertext?.fromBase64() ?: throw SharedSecretStorageError.BadCipherText
|
||||||
|
|
||||||
|
// Check Signature
|
||||||
|
val macKeySpec = SecretKeySpec(macKey, "HmacSHA256")
|
||||||
|
val mac = Mac.getInstance("HmacSHA256").apply { init(macKeySpec) }
|
||||||
|
val digest = mac.doFinal(cipherRawBytes)
|
||||||
|
|
||||||
|
if (!cipherContent.mac?.fromBase64()?.contentEquals(digest).orFalse()) {
|
||||||
|
throw SharedSecretStorageError.BadMac
|
||||||
|
}
|
||||||
|
|
||||||
val cipher = Cipher.getInstance("AES/CTR/NoPadding")
|
val cipher = Cipher.getInstance("AES/CTR/NoPadding")
|
||||||
|
|
||||||
val secretKeySpec = SecretKeySpec(aesKey, "AES")
|
val secretKeySpec = SecretKeySpec(aesKey, "AES")
|
||||||
|
@ -309,17 +324,7 @@ internal class DefaultSharedSecretStorageService @Inject constructor(
|
||||||
|
|
||||||
require(decryptedSecret.isNotEmpty())
|
require(decryptedSecret.isNotEmpty())
|
||||||
|
|
||||||
// Check Signature
|
return String(decryptedSecret, Charsets.UTF_8)
|
||||||
val macKeySpec = SecretKeySpec(macKey, "HmacSHA256")
|
|
||||||
val mac = Mac.getInstance("HmacSHA256").apply { init(macKeySpec) }
|
|
||||||
val digest = mac.doFinal(cipherRawBytes)
|
|
||||||
|
|
||||||
if (!cipherContent.mac?.fromBase64()?.contentEquals(digest).orFalse()) {
|
|
||||||
throw SharedSecretStorageError.BadMac
|
|
||||||
} else {
|
|
||||||
// we are good
|
|
||||||
return decryptedSecret.toBase64NoPadding()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getAlgorithmsForSecret(name: String): List<KeyInfoResult> {
|
override fun getAlgorithmsForSecret(name: String): List<KeyInfoResult> {
|
||||||
|
@ -429,4 +434,11 @@ internal class DefaultSharedSecretStorageService @Inject constructor(
|
||||||
|
|
||||||
return IntegrityResult.Success(keyInfo.content.passphrase != null)
|
return IntegrityResult.Success(keyInfo.content.passphrase != null)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun requestSecret(name: String, myOtherDeviceId: String) {
|
||||||
|
outgoingGossipingRequestManager.sendSecretShareRequest(
|
||||||
|
name,
|
||||||
|
mapOf(userId to listOf(myOtherDeviceId))
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,11 +19,15 @@ package im.vector.matrix.android.internal.crypto.store
|
||||||
|
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import im.vector.matrix.android.api.session.crypto.crosssigning.MXCrossSigningInfo
|
import im.vector.matrix.android.api.session.crypto.crosssigning.MXCrossSigningInfo
|
||||||
|
import im.vector.matrix.android.api.session.events.model.Event
|
||||||
import im.vector.matrix.android.api.util.Optional
|
import im.vector.matrix.android.api.util.Optional
|
||||||
|
import im.vector.matrix.android.internal.crypto.GossipingRequestState
|
||||||
import im.vector.matrix.android.internal.crypto.IncomingRoomKeyRequest
|
import im.vector.matrix.android.internal.crypto.IncomingRoomKeyRequest
|
||||||
import im.vector.matrix.android.internal.crypto.IncomingRoomKeyRequestCommon
|
import im.vector.matrix.android.internal.crypto.IncomingShareRequestCommon
|
||||||
import im.vector.matrix.android.internal.crypto.NewSessionListener
|
import im.vector.matrix.android.internal.crypto.NewSessionListener
|
||||||
|
import im.vector.matrix.android.internal.crypto.OutgoingGossipingRequestState
|
||||||
import im.vector.matrix.android.internal.crypto.OutgoingRoomKeyRequest
|
import im.vector.matrix.android.internal.crypto.OutgoingRoomKeyRequest
|
||||||
|
import im.vector.matrix.android.internal.crypto.OutgoingSecretRequest
|
||||||
import im.vector.matrix.android.internal.crypto.model.CryptoCrossSigningKey
|
import im.vector.matrix.android.internal.crypto.model.CryptoCrossSigningKey
|
||||||
import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
|
import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
|
||||||
import im.vector.matrix.android.internal.crypto.model.OlmInboundGroupSessionWrapper
|
import im.vector.matrix.android.internal.crypto.model.OlmInboundGroupSessionWrapper
|
||||||
|
@ -45,7 +49,9 @@ internal interface IMXCryptoStore {
|
||||||
/**
|
/**
|
||||||
* @return the olm account
|
* @return the olm account
|
||||||
*/
|
*/
|
||||||
fun getAccount(): OlmAccount?
|
fun getOlmAccount(): OlmAccount
|
||||||
|
|
||||||
|
fun getOrCreateOlmAccount(): OlmAccount
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve the known inbound group sessions.
|
* Retrieve the known inbound group sessions.
|
||||||
|
@ -117,6 +123,10 @@ internal interface IMXCryptoStore {
|
||||||
*/
|
*/
|
||||||
fun getPendingIncomingRoomKeyRequests(): List<IncomingRoomKeyRequest>
|
fun getPendingIncomingRoomKeyRequests(): List<IncomingRoomKeyRequest>
|
||||||
|
|
||||||
|
fun getPendingIncomingGossipingRequests(): List<IncomingShareRequestCommon>
|
||||||
|
fun storeIncomingGossipingRequest(request: IncomingShareRequestCommon, ageLocalTS: Long?)
|
||||||
|
// fun getPendingIncomingSecretShareRequests(): List<IncomingSecretShareRequest>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Indicate if the store contains data for the passed account.
|
* Indicate if the store contains data for the passed account.
|
||||||
*
|
*
|
||||||
|
@ -151,7 +161,7 @@ internal interface IMXCryptoStore {
|
||||||
*
|
*
|
||||||
* @param account the account to save
|
* @param account the account to save
|
||||||
*/
|
*/
|
||||||
fun storeAccount(account: OlmAccount)
|
fun saveOlmAccount()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Store a device for a user.
|
* Store a device for a user.
|
||||||
|
@ -187,8 +197,8 @@ internal interface IMXCryptoStore {
|
||||||
fun storeUserDevices(userId: String, devices: Map<String, CryptoDeviceInfo>?)
|
fun storeUserDevices(userId: String, devices: Map<String, CryptoDeviceInfo>?)
|
||||||
|
|
||||||
fun storeUserCrossSigningKeys(userId: String, masterKey: CryptoCrossSigningKey?,
|
fun storeUserCrossSigningKeys(userId: String, masterKey: CryptoCrossSigningKey?,
|
||||||
selfSigningKey: CryptoCrossSigningKey?,
|
selfSigningKey: CryptoCrossSigningKey?,
|
||||||
userSigningKey: CryptoCrossSigningKey?)
|
userSigningKey: CryptoCrossSigningKey?)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve the known devices for a user.
|
* Retrieve the known devices for a user.
|
||||||
|
@ -206,6 +216,7 @@ internal interface IMXCryptoStore {
|
||||||
|
|
||||||
// TODO temp
|
// TODO temp
|
||||||
fun getLiveDeviceList(): LiveData<List<CryptoDeviceInfo>>
|
fun getLiveDeviceList(): LiveData<List<CryptoDeviceInfo>>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Store the crypto algorithm for a room.
|
* Store the crypto algorithm for a room.
|
||||||
*
|
*
|
||||||
|
@ -347,43 +358,13 @@ internal interface IMXCryptoStore {
|
||||||
* @param request the request
|
* @param request the request
|
||||||
* @return either the same instance as passed in, or the existing one.
|
* @return either the same instance as passed in, or the existing one.
|
||||||
*/
|
*/
|
||||||
fun getOrAddOutgoingRoomKeyRequest(request: OutgoingRoomKeyRequest): OutgoingRoomKeyRequest?
|
fun getOrAddOutgoingRoomKeyRequest(requestBody: RoomKeyRequestBody, recipients: Map<String, List<String>>): OutgoingRoomKeyRequest?
|
||||||
|
|
||||||
/**
|
fun getOrAddOutgoingSecretShareRequest(secretName: String, recipients: Map<String, List<String>>): OutgoingSecretRequest?
|
||||||
* Look for room key requests by state.
|
|
||||||
*
|
|
||||||
* @param states the states
|
|
||||||
* @return an OutgoingRoomKeyRequest or null
|
|
||||||
*/
|
|
||||||
fun getOutgoingRoomKeyRequestByState(states: Set<OutgoingRoomKeyRequest.RequestState>): OutgoingRoomKeyRequest?
|
|
||||||
|
|
||||||
/**
|
fun saveGossipingEvent(event: Event)
|
||||||
* Update an existing outgoing request.
|
|
||||||
*
|
|
||||||
* @param request the request
|
|
||||||
*/
|
|
||||||
fun updateOutgoingRoomKeyRequest(request: OutgoingRoomKeyRequest)
|
|
||||||
|
|
||||||
/**
|
fun updateGossipingRequestState(request: IncomingShareRequestCommon, state: GossipingRequestState)
|
||||||
* Delete an outgoing room key request.
|
|
||||||
*
|
|
||||||
* @param transactionId the transaction id.
|
|
||||||
*/
|
|
||||||
fun deleteOutgoingRoomKeyRequest(transactionId: String)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Store an incomingRoomKeyRequest instance
|
|
||||||
*
|
|
||||||
* @param incomingRoomKeyRequest the incoming key request
|
|
||||||
*/
|
|
||||||
fun storeIncomingRoomKeyRequest(incomingRoomKeyRequest: IncomingRoomKeyRequest?)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Delete an incomingRoomKeyRequest instance
|
|
||||||
*
|
|
||||||
* @param incomingRoomKeyRequest the incoming key request
|
|
||||||
*/
|
|
||||||
fun deleteIncomingRoomKeyRequest(incomingRoomKeyRequest: IncomingRoomKeyRequestCommon)
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Search an IncomingRoomKeyRequest
|
* Search an IncomingRoomKeyRequest
|
||||||
|
@ -395,6 +376,8 @@ internal interface IMXCryptoStore {
|
||||||
*/
|
*/
|
||||||
fun getIncomingRoomKeyRequest(userId: String, deviceId: String, requestId: String): IncomingRoomKeyRequest?
|
fun getIncomingRoomKeyRequest(userId: String, deviceId: String, requestId: String): IncomingRoomKeyRequest?
|
||||||
|
|
||||||
|
fun updateOutgoingGossipingRequestState(requestId: String, state: OutgoingGossipingRequestState)
|
||||||
|
|
||||||
fun addNewSessionListener(listener: NewSessionListener)
|
fun addNewSessionListener(listener: NewSessionListener)
|
||||||
|
|
||||||
fun removeSessionListener(listener: NewSessionListener)
|
fun removeSessionListener(listener: NewSessionListener)
|
||||||
|
@ -406,22 +389,37 @@ internal interface IMXCryptoStore {
|
||||||
/**
|
/**
|
||||||
* Gets the current crosssigning info
|
* Gets the current crosssigning info
|
||||||
*/
|
*/
|
||||||
fun getMyCrossSigningInfo() : MXCrossSigningInfo?
|
fun getMyCrossSigningInfo(): MXCrossSigningInfo?
|
||||||
|
|
||||||
fun setMyCrossSigningInfo(info: MXCrossSigningInfo?)
|
fun setMyCrossSigningInfo(info: MXCrossSigningInfo?)
|
||||||
|
|
||||||
fun getCrossSigningInfo(userId: String) : MXCrossSigningInfo?
|
fun getCrossSigningInfo(userId: String): MXCrossSigningInfo?
|
||||||
fun getLiveCrossSigningInfo(userId: String) : LiveData<Optional<MXCrossSigningInfo>>
|
fun getLiveCrossSigningInfo(userId: String): LiveData<Optional<MXCrossSigningInfo>>
|
||||||
fun setCrossSigningInfo(userId: String, info: MXCrossSigningInfo?)
|
fun setCrossSigningInfo(userId: String, info: MXCrossSigningInfo?)
|
||||||
|
|
||||||
fun markMyMasterKeyAsLocallyTrusted(trusted: Boolean)
|
fun markMyMasterKeyAsLocallyTrusted(trusted: Boolean)
|
||||||
|
|
||||||
fun storePrivateKeysInfo(msk: String?, usk: String?, ssk: String?)
|
fun storePrivateKeysInfo(msk: String?, usk: String?, ssk: String?)
|
||||||
fun getCrossSigningPrivateKeys() : PrivateKeysInfo?
|
fun storeSSKPrivateKey(ssk: String?)
|
||||||
|
fun storeUSKPrivateKey(usk: String?)
|
||||||
|
|
||||||
|
fun getCrossSigningPrivateKeys(): PrivateKeysInfo?
|
||||||
|
|
||||||
|
fun saveBackupRecoveryKey(recoveryKey: String?, version: String?)
|
||||||
|
fun getKeyBackupRecoveryKeyInfo() : SavedKeyBackupKeyInfo?
|
||||||
|
|
||||||
fun setUserKeysAsTrusted(userId: String, trusted: Boolean = true)
|
fun setUserKeysAsTrusted(userId: String, trusted: Boolean = true)
|
||||||
fun setDeviceTrust(userId: String, deviceId: String, crossSignedVerified: Boolean, locallyVerified : Boolean)
|
fun setDeviceTrust(userId: String, deviceId: String, crossSignedVerified: Boolean, locallyVerified: Boolean)
|
||||||
|
|
||||||
fun clearOtherUserTrust()
|
fun clearOtherUserTrust()
|
||||||
|
|
||||||
fun updateUsersTrust(check: (String) -> Boolean)
|
fun updateUsersTrust(check: (String) -> Boolean)
|
||||||
|
|
||||||
|
// Dev tools
|
||||||
|
|
||||||
|
fun getOutgoingRoomKeyRequests(): List<OutgoingRoomKeyRequest>
|
||||||
|
fun getOutgoingSecretKeyRequests(): List<OutgoingSecretRequest>
|
||||||
|
fun getOutgoingSecretRequest(secretName: String): OutgoingSecretRequest?
|
||||||
|
fun getIncomingRoomKeyRequests(): List<IncomingRoomKeyRequest>
|
||||||
|
fun getGossipingEventsTrail(): List<Event>
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,22 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2020 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.matrix.android.internal.crypto.store
|
||||||
|
|
||||||
|
data class SavedKeyBackupKeyInfo(
|
||||||
|
val recoveryKey : String,
|
||||||
|
val version: String
|
||||||
|
)
|
|
@ -59,7 +59,12 @@ fun <T : RealmObject> doRealmQueryAndCopyList(realmConfiguration: RealmConfigura
|
||||||
*/
|
*/
|
||||||
fun doRealmTransaction(realmConfiguration: RealmConfiguration, action: (Realm) -> Unit) {
|
fun doRealmTransaction(realmConfiguration: RealmConfiguration, action: (Realm) -> Unit) {
|
||||||
Realm.getInstance(realmConfiguration).use { realm ->
|
Realm.getInstance(realmConfiguration).use { realm ->
|
||||||
realm.executeTransaction { action.invoke(realm) }
|
realm.executeTransaction { action.invoke(it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fun doRealmTransactionAsync(realmConfiguration: RealmConfiguration, action: (Realm) -> Unit) {
|
||||||
|
Realm.getInstance(realmConfiguration).use { realm ->
|
||||||
|
realm.executeTransactionAsync { action.invoke(it) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -21,12 +21,21 @@ import androidx.lifecycle.Transformations
|
||||||
import com.zhuinden.monarchy.Monarchy
|
import com.zhuinden.monarchy.Monarchy
|
||||||
import im.vector.matrix.android.api.auth.data.Credentials
|
import im.vector.matrix.android.api.auth.data.Credentials
|
||||||
import im.vector.matrix.android.api.session.crypto.crosssigning.MXCrossSigningInfo
|
import im.vector.matrix.android.api.session.crypto.crosssigning.MXCrossSigningInfo
|
||||||
|
import im.vector.matrix.android.api.session.events.model.Event
|
||||||
|
import im.vector.matrix.android.api.session.events.model.LocalEcho
|
||||||
|
import im.vector.matrix.android.api.session.room.send.SendState
|
||||||
import im.vector.matrix.android.api.util.Optional
|
import im.vector.matrix.android.api.util.Optional
|
||||||
import im.vector.matrix.android.api.util.toOptional
|
import im.vector.matrix.android.api.util.toOptional
|
||||||
|
import im.vector.matrix.android.internal.crypto.GossipRequestType
|
||||||
|
import im.vector.matrix.android.internal.crypto.GossipingRequestState
|
||||||
import im.vector.matrix.android.internal.crypto.IncomingRoomKeyRequest
|
import im.vector.matrix.android.internal.crypto.IncomingRoomKeyRequest
|
||||||
import im.vector.matrix.android.internal.crypto.IncomingRoomKeyRequestCommon
|
import im.vector.matrix.android.internal.crypto.IncomingSecretShareRequest
|
||||||
|
import im.vector.matrix.android.internal.crypto.IncomingShareRequestCommon
|
||||||
import im.vector.matrix.android.internal.crypto.NewSessionListener
|
import im.vector.matrix.android.internal.crypto.NewSessionListener
|
||||||
|
import im.vector.matrix.android.internal.crypto.OutgoingGossipingRequestState
|
||||||
import im.vector.matrix.android.internal.crypto.OutgoingRoomKeyRequest
|
import im.vector.matrix.android.internal.crypto.OutgoingRoomKeyRequest
|
||||||
|
import im.vector.matrix.android.internal.crypto.OutgoingSecretRequest
|
||||||
|
import im.vector.matrix.android.internal.crypto.algorithms.olm.OlmDecryptionResult
|
||||||
import im.vector.matrix.android.internal.crypto.crosssigning.DeviceTrustLevel
|
import im.vector.matrix.android.internal.crypto.crosssigning.DeviceTrustLevel
|
||||||
import im.vector.matrix.android.internal.crypto.model.CryptoCrossSigningKey
|
import im.vector.matrix.android.internal.crypto.model.CryptoCrossSigningKey
|
||||||
import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
|
import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
|
||||||
|
@ -36,6 +45,7 @@ import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody
|
||||||
import im.vector.matrix.android.internal.crypto.model.toEntity
|
import im.vector.matrix.android.internal.crypto.model.toEntity
|
||||||
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
|
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
|
||||||
import im.vector.matrix.android.internal.crypto.store.PrivateKeysInfo
|
import im.vector.matrix.android.internal.crypto.store.PrivateKeysInfo
|
||||||
|
import im.vector.matrix.android.internal.crypto.store.SavedKeyBackupKeyInfo
|
||||||
import im.vector.matrix.android.internal.crypto.store.db.model.CrossSigningInfoEntity
|
import im.vector.matrix.android.internal.crypto.store.db.model.CrossSigningInfoEntity
|
||||||
import im.vector.matrix.android.internal.crypto.store.db.model.CrossSigningInfoEntityFields
|
import im.vector.matrix.android.internal.crypto.store.db.model.CrossSigningInfoEntityFields
|
||||||
import im.vector.matrix.android.internal.crypto.store.db.model.CryptoMapper
|
import im.vector.matrix.android.internal.crypto.store.db.model.CryptoMapper
|
||||||
|
@ -44,16 +54,17 @@ import im.vector.matrix.android.internal.crypto.store.db.model.CryptoRoomEntity
|
||||||
import im.vector.matrix.android.internal.crypto.store.db.model.CryptoRoomEntityFields
|
import im.vector.matrix.android.internal.crypto.store.db.model.CryptoRoomEntityFields
|
||||||
import im.vector.matrix.android.internal.crypto.store.db.model.DeviceInfoEntity
|
import im.vector.matrix.android.internal.crypto.store.db.model.DeviceInfoEntity
|
||||||
import im.vector.matrix.android.internal.crypto.store.db.model.DeviceInfoEntityFields
|
import im.vector.matrix.android.internal.crypto.store.db.model.DeviceInfoEntityFields
|
||||||
import im.vector.matrix.android.internal.crypto.store.db.model.IncomingRoomKeyRequestEntity
|
import im.vector.matrix.android.internal.crypto.store.db.model.GossipingEventEntity
|
||||||
import im.vector.matrix.android.internal.crypto.store.db.model.IncomingRoomKeyRequestEntityFields
|
import im.vector.matrix.android.internal.crypto.store.db.model.IncomingGossipingRequestEntity
|
||||||
|
import im.vector.matrix.android.internal.crypto.store.db.model.IncomingGossipingRequestEntityFields
|
||||||
import im.vector.matrix.android.internal.crypto.store.db.model.KeyInfoEntity
|
import im.vector.matrix.android.internal.crypto.store.db.model.KeyInfoEntity
|
||||||
import im.vector.matrix.android.internal.crypto.store.db.model.KeysBackupDataEntity
|
import im.vector.matrix.android.internal.crypto.store.db.model.KeysBackupDataEntity
|
||||||
import im.vector.matrix.android.internal.crypto.store.db.model.OlmInboundGroupSessionEntity
|
import im.vector.matrix.android.internal.crypto.store.db.model.OlmInboundGroupSessionEntity
|
||||||
import im.vector.matrix.android.internal.crypto.store.db.model.OlmInboundGroupSessionEntityFields
|
import im.vector.matrix.android.internal.crypto.store.db.model.OlmInboundGroupSessionEntityFields
|
||||||
import im.vector.matrix.android.internal.crypto.store.db.model.OlmSessionEntity
|
import im.vector.matrix.android.internal.crypto.store.db.model.OlmSessionEntity
|
||||||
import im.vector.matrix.android.internal.crypto.store.db.model.OlmSessionEntityFields
|
import im.vector.matrix.android.internal.crypto.store.db.model.OlmSessionEntityFields
|
||||||
import im.vector.matrix.android.internal.crypto.store.db.model.OutgoingRoomKeyRequestEntity
|
import im.vector.matrix.android.internal.crypto.store.db.model.OutgoingGossipingRequestEntity
|
||||||
import im.vector.matrix.android.internal.crypto.store.db.model.OutgoingRoomKeyRequestEntityFields
|
import im.vector.matrix.android.internal.crypto.store.db.model.OutgoingGossipingRequestEntityFields
|
||||||
import im.vector.matrix.android.internal.crypto.store.db.model.TrustLevelEntity
|
import im.vector.matrix.android.internal.crypto.store.db.model.TrustLevelEntity
|
||||||
import im.vector.matrix.android.internal.crypto.store.db.model.UserEntity
|
import im.vector.matrix.android.internal.crypto.store.db.model.UserEntity
|
||||||
import im.vector.matrix.android.internal.crypto.store.db.model.UserEntityFields
|
import im.vector.matrix.android.internal.crypto.store.db.model.UserEntityFields
|
||||||
|
@ -62,7 +73,9 @@ import im.vector.matrix.android.internal.crypto.store.db.query.delete
|
||||||
import im.vector.matrix.android.internal.crypto.store.db.query.get
|
import im.vector.matrix.android.internal.crypto.store.db.query.get
|
||||||
import im.vector.matrix.android.internal.crypto.store.db.query.getById
|
import im.vector.matrix.android.internal.crypto.store.db.query.getById
|
||||||
import im.vector.matrix.android.internal.crypto.store.db.query.getOrCreate
|
import im.vector.matrix.android.internal.crypto.store.db.query.getOrCreate
|
||||||
|
import im.vector.matrix.android.internal.database.mapper.ContentMapper
|
||||||
import im.vector.matrix.android.internal.di.CryptoDatabase
|
import im.vector.matrix.android.internal.di.CryptoDatabase
|
||||||
|
import im.vector.matrix.android.internal.di.MoshiProvider
|
||||||
import im.vector.matrix.android.internal.session.SessionScope
|
import im.vector.matrix.android.internal.session.SessionScope
|
||||||
import io.realm.Realm
|
import io.realm.Realm
|
||||||
import io.realm.RealmConfiguration
|
import io.realm.RealmConfiguration
|
||||||
|
@ -110,27 +123,7 @@ internal class RealmCryptoStore @Inject constructor(
|
||||||
.setRealmConfiguration(realmConfiguration)
|
.setRealmConfiguration(realmConfiguration)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
/* ==========================================================================================
|
init {
|
||||||
* Other data
|
|
||||||
* ========================================================================================== */
|
|
||||||
|
|
||||||
override fun hasData(): Boolean {
|
|
||||||
return doWithRealm(realmConfiguration) {
|
|
||||||
!it.isEmpty
|
|
||||||
// Check if there is a MetaData object
|
|
||||||
&& it.where<CryptoMetadataEntity>().count() > 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun deleteStore() {
|
|
||||||
doRealmTransaction(realmConfiguration) {
|
|
||||||
it.deleteAll()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun open() {
|
|
||||||
realmLocker = Realm.getInstance(realmConfiguration)
|
|
||||||
|
|
||||||
// Ensure CryptoMetadataEntity is inserted in DB
|
// Ensure CryptoMetadataEntity is inserted in DB
|
||||||
doRealmTransaction(realmConfiguration) { realm ->
|
doRealmTransaction(realmConfiguration) { realm ->
|
||||||
var currentMetadata = realm.where<CryptoMetadataEntity>().findFirst()
|
var currentMetadata = realm.where<CryptoMetadataEntity>().findFirst()
|
||||||
|
@ -161,6 +154,27 @@ internal class RealmCryptoStore @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/* ==========================================================================================
|
||||||
|
* Other data
|
||||||
|
* ========================================================================================== */
|
||||||
|
|
||||||
|
override fun hasData(): Boolean {
|
||||||
|
return doWithRealm(realmConfiguration) {
|
||||||
|
!it.isEmpty
|
||||||
|
// Check if there is a MetaData object
|
||||||
|
&& it.where<CryptoMetadataEntity>().count() > 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun deleteStore() {
|
||||||
|
doRealmTransaction(realmConfiguration) {
|
||||||
|
it.deleteAll()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun open() {
|
||||||
|
realmLocker = Realm.getInstance(realmConfiguration)
|
||||||
|
}
|
||||||
|
|
||||||
override fun close() {
|
override fun close() {
|
||||||
olmSessionsToRelease.forEach {
|
olmSessionsToRelease.forEach {
|
||||||
|
@ -191,20 +205,31 @@ internal class RealmCryptoStore @Inject constructor(
|
||||||
}?.deviceId ?: ""
|
}?.deviceId ?: ""
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun storeAccount(account: OlmAccount) {
|
override fun saveOlmAccount() {
|
||||||
olmAccount = account
|
|
||||||
|
|
||||||
doRealmTransaction(realmConfiguration) {
|
doRealmTransaction(realmConfiguration) {
|
||||||
it.where<CryptoMetadataEntity>().findFirst()?.putOlmAccount(account)
|
it.where<CryptoMetadataEntity>().findFirst()?.putOlmAccount(olmAccount)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getAccount(): OlmAccount? {
|
override fun getOlmAccount(): OlmAccount {
|
||||||
if (olmAccount == null) {
|
return olmAccount!!
|
||||||
olmAccount = doRealmQueryAndCopy(realmConfiguration) { it.where<CryptoMetadataEntity>().findFirst() }?.getOlmAccount()
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return olmAccount
|
override fun getOrCreateOlmAccount(): OlmAccount {
|
||||||
|
doRealmTransaction(realmConfiguration) {
|
||||||
|
val metaData = it.where<CryptoMetadataEntity>().findFirst()
|
||||||
|
val existing = metaData!!.getOlmAccount()
|
||||||
|
if (existing == null) {
|
||||||
|
Timber.d("## Crypto Creating olm account")
|
||||||
|
val created = OlmAccount()
|
||||||
|
metaData.putOlmAccount(created)
|
||||||
|
olmAccount = created
|
||||||
|
} else {
|
||||||
|
Timber.d("## Crypto Access existing account")
|
||||||
|
olmAccount = existing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return olmAccount!!
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun storeUserDevice(userId: String?, deviceInfo: CryptoDeviceInfo?) {
|
override fun storeUserDevice(userId: String?, deviceInfo: CryptoDeviceInfo?) {
|
||||||
|
@ -359,7 +384,46 @@ internal class RealmCryptoStore @Inject constructor(
|
||||||
doRealmTransaction(realmConfiguration) { realm ->
|
doRealmTransaction(realmConfiguration) { realm ->
|
||||||
realm.where<CryptoMetadataEntity>().findFirst()?.apply {
|
realm.where<CryptoMetadataEntity>().findFirst()?.apply {
|
||||||
xSignMasterPrivateKey = msk
|
xSignMasterPrivateKey = msk
|
||||||
|
xSignUserPrivateKey = usk
|
||||||
xSignSelfSignedPrivateKey = ssk
|
xSignSelfSignedPrivateKey = ssk
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun saveBackupRecoveryKey(recoveryKey: String?, version: String?) {
|
||||||
|
doRealmTransaction(realmConfiguration) { realm ->
|
||||||
|
realm.where<CryptoMetadataEntity>().findFirst()?.apply {
|
||||||
|
keyBackupRecoveryKey = recoveryKey
|
||||||
|
keyBackupRecoveryKeyVersion = version
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getKeyBackupRecoveryKeyInfo(): SavedKeyBackupKeyInfo? {
|
||||||
|
return doRealmQueryAndCopy(realmConfiguration) { realm ->
|
||||||
|
realm.where<CryptoMetadataEntity>().findFirst()
|
||||||
|
}?.let {
|
||||||
|
val key = it.keyBackupRecoveryKey
|
||||||
|
val version = it.keyBackupRecoveryKeyVersion
|
||||||
|
if (!key.isNullOrBlank() && !version.isNullOrBlank()) {
|
||||||
|
SavedKeyBackupKeyInfo(recoveryKey = key, version = version)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun storeSSKPrivateKey(ssk: String?) {
|
||||||
|
doRealmTransaction(realmConfiguration) { realm ->
|
||||||
|
realm.where<CryptoMetadataEntity>().findFirst()?.apply {
|
||||||
|
xSignSelfSignedPrivateKey = ssk
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun storeUSKPrivateKey(usk: String?) {
|
||||||
|
doRealmTransaction(realmConfiguration) { realm ->
|
||||||
|
realm.where<CryptoMetadataEntity>().findFirst()?.apply {
|
||||||
xSignUserPrivateKey = usk
|
xSignUserPrivateKey = usk
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -797,131 +861,328 @@ internal class RealmCryptoStore @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getOutgoingRoomKeyRequest(requestBody: RoomKeyRequestBody): OutgoingRoomKeyRequest? {
|
override fun getOutgoingRoomKeyRequest(requestBody: RoomKeyRequestBody): OutgoingRoomKeyRequest? {
|
||||||
return doRealmQueryAndCopy(realmConfiguration) {
|
return monarchy.fetchAllCopiedSync { realm ->
|
||||||
it.where<OutgoingRoomKeyRequestEntity>()
|
realm.where<OutgoingGossipingRequestEntity>()
|
||||||
.equalTo(OutgoingRoomKeyRequestEntityFields.REQUEST_BODY_ALGORITHM, requestBody.algorithm)
|
.equalTo(OutgoingGossipingRequestEntityFields.TYPE_STR, GossipRequestType.KEY.name)
|
||||||
.equalTo(OutgoingRoomKeyRequestEntityFields.REQUEST_BODY_ROOM_ID, requestBody.roomId)
|
}.mapNotNull {
|
||||||
.equalTo(OutgoingRoomKeyRequestEntityFields.REQUEST_BODY_SENDER_KEY, requestBody.senderKey)
|
it.toOutgoingGossipingRequest() as? OutgoingRoomKeyRequest
|
||||||
.equalTo(OutgoingRoomKeyRequestEntityFields.REQUEST_BODY_SESSION_ID, requestBody.sessionId)
|
}.firstOrNull {
|
||||||
.findFirst()
|
it.requestBody?.algorithm == requestBody.algorithm
|
||||||
|
it.requestBody?.roomId == requestBody.roomId
|
||||||
|
it.requestBody?.senderKey == requestBody.senderKey
|
||||||
|
it.requestBody?.sessionId == requestBody.sessionId
|
||||||
}
|
}
|
||||||
?.toOutgoingRoomKeyRequest()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getOrAddOutgoingRoomKeyRequest(request: OutgoingRoomKeyRequest): OutgoingRoomKeyRequest? {
|
override fun getOutgoingSecretRequest(secretName: String): OutgoingSecretRequest? {
|
||||||
if (request.requestBody == null) {
|
return monarchy.fetchAllCopiedSync { realm ->
|
||||||
return null
|
realm.where<OutgoingGossipingRequestEntity>()
|
||||||
}
|
.equalTo(OutgoingGossipingRequestEntityFields.TYPE_STR, GossipRequestType.SECRET.name)
|
||||||
|
.equalTo(OutgoingGossipingRequestEntityFields.REQUESTED_INFO_STR, secretName)
|
||||||
|
}.mapNotNull {
|
||||||
|
it.toOutgoingGossipingRequest() as? OutgoingSecretRequest
|
||||||
|
}.firstOrNull()
|
||||||
|
}
|
||||||
|
|
||||||
val existingOne = getOutgoingRoomKeyRequest(request.requestBody!!)
|
override fun getIncomingRoomKeyRequests(): List<IncomingRoomKeyRequest> {
|
||||||
|
return monarchy.fetchAllCopiedSync { realm ->
|
||||||
if (existingOne != null) {
|
realm.where<IncomingGossipingRequestEntity>()
|
||||||
return existingOne
|
.equalTo(IncomingGossipingRequestEntityFields.TYPE_STR, GossipRequestType.KEY.name)
|
||||||
|
}.mapNotNull {
|
||||||
|
it.toIncomingGossipingRequest() as? IncomingRoomKeyRequest
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getGossipingEventsTrail(): List<Event> {
|
||||||
|
return monarchy.fetchAllCopiedSync { realm ->
|
||||||
|
realm.where<GossipingEventEntity>()
|
||||||
|
}.map {
|
||||||
|
it.toModel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getOrAddOutgoingRoomKeyRequest(requestBody: RoomKeyRequestBody, recipients: Map<String, List<String>>): OutgoingRoomKeyRequest? {
|
||||||
|
// Insert the request and return the one passed in parameter
|
||||||
|
var request: OutgoingRoomKeyRequest? = null
|
||||||
|
doRealmTransaction(realmConfiguration) { realm ->
|
||||||
|
|
||||||
|
val existing = realm.where<OutgoingGossipingRequestEntity>()
|
||||||
|
.equalTo(OutgoingGossipingRequestEntityFields.TYPE_STR, GossipRequestType.KEY.name)
|
||||||
|
.findAll()
|
||||||
|
.mapNotNull {
|
||||||
|
it.toOutgoingGossipingRequest() as? OutgoingRoomKeyRequest
|
||||||
|
}.firstOrNull {
|
||||||
|
it.requestBody?.algorithm == requestBody.algorithm
|
||||||
|
&& it.requestBody?.sessionId == requestBody.sessionId
|
||||||
|
&& it.requestBody?.senderKey == requestBody.senderKey
|
||||||
|
&& it.requestBody?.roomId == requestBody.roomId
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existing == null) {
|
||||||
|
request = realm.createObject(OutgoingGossipingRequestEntity::class.java).apply {
|
||||||
|
this.requestId = LocalEcho.createLocalEchoId()
|
||||||
|
this.setRecipients(recipients)
|
||||||
|
this.requestState = OutgoingGossipingRequestState.UNSENT
|
||||||
|
this.type = GossipRequestType.KEY
|
||||||
|
this.requestedInfoStr = requestBody.toJson()
|
||||||
|
}.toOutgoingGossipingRequest() as? OutgoingRoomKeyRequest
|
||||||
|
} else {
|
||||||
|
request = existing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return request
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getOrAddOutgoingSecretShareRequest(secretName: String, recipients: Map<String, List<String>>): OutgoingSecretRequest? {
|
||||||
|
var request: OutgoingSecretRequest? = null
|
||||||
|
|
||||||
// Insert the request and return the one passed in parameter
|
// Insert the request and return the one passed in parameter
|
||||||
doRealmTransaction(realmConfiguration) {
|
doRealmTransaction(realmConfiguration) { realm ->
|
||||||
it.createObject(OutgoingRoomKeyRequestEntity::class.java, request.requestId).apply {
|
val existing = realm.where<OutgoingGossipingRequestEntity>()
|
||||||
putRequestBody(request.requestBody)
|
.equalTo(OutgoingGossipingRequestEntityFields.TYPE_STR, GossipRequestType.SECRET.name)
|
||||||
putRecipients(request.recipients)
|
.equalTo(OutgoingGossipingRequestEntityFields.REQUESTED_INFO_STR, secretName)
|
||||||
cancellationTxnId = request.cancellationTxnId
|
.findAll()
|
||||||
state = request.state.ordinal
|
.mapNotNull {
|
||||||
|
it.toOutgoingGossipingRequest() as? OutgoingSecretRequest
|
||||||
|
}.firstOrNull()
|
||||||
|
if (existing == null) {
|
||||||
|
request = realm.createObject(OutgoingGossipingRequestEntity::class.java).apply {
|
||||||
|
this.type = GossipRequestType.SECRET
|
||||||
|
setRecipients(recipients)
|
||||||
|
this.requestState = OutgoingGossipingRequestState.UNSENT
|
||||||
|
this.requestId = LocalEcho.createLocalEchoId()
|
||||||
|
this.requestedInfoStr = secretName
|
||||||
|
}.toOutgoingGossipingRequest() as? OutgoingSecretRequest
|
||||||
|
} else {
|
||||||
|
request = existing
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return request
|
return request
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getOutgoingRoomKeyRequestByState(states: Set<OutgoingRoomKeyRequest.RequestState>): OutgoingRoomKeyRequest? {
|
override fun saveGossipingEvent(event: Event) {
|
||||||
val statesIndex = states.map { it.ordinal }.toTypedArray()
|
val now = System.currentTimeMillis()
|
||||||
return doRealmQueryAndCopy(realmConfiguration) {
|
val ageLocalTs = event.unsignedData?.age?.let { now - it } ?: now
|
||||||
it.where<OutgoingRoomKeyRequestEntity>()
|
val entity = GossipingEventEntity(
|
||||||
.`in`(OutgoingRoomKeyRequestEntityFields.STATE, statesIndex)
|
type = event.type,
|
||||||
.findFirst()
|
sender = event.senderId,
|
||||||
|
ageLocalTs = ageLocalTs,
|
||||||
|
content = ContentMapper.map(event.content)
|
||||||
|
).apply {
|
||||||
|
sendState = SendState.SYNCED
|
||||||
|
decryptionResultJson = MoshiProvider.providesMoshi().adapter<OlmDecryptionResult>(OlmDecryptionResult::class.java).toJson(event.mxDecryptionResult)
|
||||||
|
decryptionErrorCode = event.mCryptoError?.name
|
||||||
}
|
}
|
||||||
?.toOutgoingRoomKeyRequest()
|
doRealmTransaction(realmConfiguration) { realm ->
|
||||||
}
|
realm.insertOrUpdate(entity)
|
||||||
|
|
||||||
override fun updateOutgoingRoomKeyRequest(request: OutgoingRoomKeyRequest) {
|
|
||||||
doRealmTransaction(realmConfiguration) {
|
|
||||||
val obj = OutgoingRoomKeyRequestEntity().apply {
|
|
||||||
requestId = request.requestId
|
|
||||||
cancellationTxnId = request.cancellationTxnId
|
|
||||||
state = request.state.ordinal
|
|
||||||
putRecipients(request.recipients)
|
|
||||||
putRequestBody(request.requestBody)
|
|
||||||
}
|
|
||||||
|
|
||||||
it.insertOrUpdate(obj)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun deleteOutgoingRoomKeyRequest(transactionId: String) {
|
// override fun getOutgoingRoomKeyRequestByState(states: Set<ShareRequestState>): OutgoingRoomKeyRequest? {
|
||||||
doRealmTransaction(realmConfiguration) {
|
// val statesIndex = states.map { it.ordinal }.toTypedArray()
|
||||||
it.where<OutgoingRoomKeyRequestEntity>()
|
// return doRealmQueryAndCopy(realmConfiguration) { realm ->
|
||||||
.equalTo(OutgoingRoomKeyRequestEntityFields.REQUEST_ID, transactionId)
|
// realm.where<GossipingEventEntity>()
|
||||||
.findFirst()
|
// .equalTo(GossipingEventEntityFields.SENDER, credentials.userId)
|
||||||
?.deleteFromRealm()
|
// .findAll()
|
||||||
|
// .filter {entity ->
|
||||||
|
// states.any { it == entity.requestState}
|
||||||
|
// }
|
||||||
|
// }.mapNotNull {
|
||||||
|
// ContentMapper.map(it.content)?.toModel<OutgoingSecretRequest>()
|
||||||
|
// }
|
||||||
|
// ?.toOutgoingRoomKeyRequest()
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// override fun getOutgoingSecretShareRequestByState(states: Set<ShareRequestState>): OutgoingSecretRequest? {
|
||||||
|
// val statesIndex = states.map { it.ordinal }.toTypedArray()
|
||||||
|
// return doRealmQueryAndCopy(realmConfiguration) {
|
||||||
|
// it.where<OutgoingSecretRequestEntity>()
|
||||||
|
// .`in`(OutgoingSecretRequestEntityFields.STATE, statesIndex)
|
||||||
|
// .findFirst()
|
||||||
|
// }
|
||||||
|
// ?.toOutgoingSecretRequest()
|
||||||
|
// }
|
||||||
|
|
||||||
|
// override fun updateOutgoingRoomKeyRequest(request: OutgoingRoomKeyRequest) {
|
||||||
|
// doRealmTransaction(realmConfiguration) {
|
||||||
|
// val obj = OutgoingRoomKeyRequestEntity().apply {
|
||||||
|
// requestId = request.requestId
|
||||||
|
// cancellationTxnId = request.cancellationTxnId
|
||||||
|
// state = request.state.ordinal
|
||||||
|
// putRecipients(request.recipients)
|
||||||
|
// putRequestBody(request.requestBody)
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// it.insertOrUpdate(obj)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// override fun deleteOutgoingRoomKeyRequest(transactionId: String) {
|
||||||
|
// doRealmTransaction(realmConfiguration) {
|
||||||
|
// it.where<OutgoingRoomKeyRequestEntity>()
|
||||||
|
// .equalTo(OutgoingRoomKeyRequestEntityFields.REQUEST_ID, transactionId)
|
||||||
|
// .findFirst()
|
||||||
|
// ?.deleteFromRealm()
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// override fun storeIncomingRoomKeyRequest(incomingRoomKeyRequest: IncomingRoomKeyRequest?) {
|
||||||
|
// if (incomingRoomKeyRequest == null) {
|
||||||
|
// return
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// doRealmTransaction(realmConfiguration) {
|
||||||
|
// // Delete any previous store request with the same parameters
|
||||||
|
// it.where<IncomingRoomKeyRequestEntity>()
|
||||||
|
// .equalTo(IncomingRoomKeyRequestEntityFields.USER_ID, incomingRoomKeyRequest.userId)
|
||||||
|
// .equalTo(IncomingRoomKeyRequestEntityFields.DEVICE_ID, incomingRoomKeyRequest.deviceId)
|
||||||
|
// .equalTo(IncomingRoomKeyRequestEntityFields.REQUEST_ID, incomingRoomKeyRequest.requestId)
|
||||||
|
// .findAll()
|
||||||
|
// .deleteAllFromRealm()
|
||||||
|
//
|
||||||
|
// // Then store it
|
||||||
|
// it.createObject(IncomingRoomKeyRequestEntity::class.java).apply {
|
||||||
|
// userId = incomingRoomKeyRequest.userId
|
||||||
|
// deviceId = incomingRoomKeyRequest.deviceId
|
||||||
|
// requestId = incomingRoomKeyRequest.requestId
|
||||||
|
// putRequestBody(incomingRoomKeyRequest.requestBody)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// override fun deleteIncomingRoomKeyRequest(incomingRoomKeyRequest: IncomingShareRequestCommon) {
|
||||||
|
// doRealmTransaction(realmConfiguration) {
|
||||||
|
// it.where<GossipingEventEntity>()
|
||||||
|
// .equalTo(GossipingEventEntityFields.TYPE, EventType.ROOM_KEY_REQUEST)
|
||||||
|
// .notEqualTo(GossipingEventEntityFields.SENDER, credentials.userId)
|
||||||
|
// .findAll()
|
||||||
|
// .filter {
|
||||||
|
// ContentMapper.map(it.content).toModel<IncomingRoomKeyRequest>()?.let {
|
||||||
|
//
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// // .equalTo(IncomingRoomKeyRequestEntityFields.USER_ID, incomingRoomKeyRequest.userId)
|
||||||
|
// // .equalTo(IncomingRoomKeyRequestEntityFields.DEVICE_ID, incomingRoomKeyRequest.deviceId)
|
||||||
|
// // .equalTo(IncomingRoomKeyRequestEntityFields.REQUEST_ID, incomingRoomKeyRequest.requestId)
|
||||||
|
// // .findAll()
|
||||||
|
// // .deleteAllFromRealm()
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
override fun updateGossipingRequestState(request: IncomingShareRequestCommon, state: GossipingRequestState) {
|
||||||
|
doRealmTransaction(realmConfiguration) { realm ->
|
||||||
|
realm.where<IncomingGossipingRequestEntity>()
|
||||||
|
.equalTo(IncomingGossipingRequestEntityFields.OTHER_USER_ID, request.userId)
|
||||||
|
.equalTo(IncomingGossipingRequestEntityFields.OTHER_DEVICE_ID, request.deviceId)
|
||||||
|
.equalTo(IncomingGossipingRequestEntityFields.REQUEST_ID, request.requestId)
|
||||||
|
.findAll().forEach {
|
||||||
|
it.requestState = state
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun storeIncomingRoomKeyRequest(incomingRoomKeyRequest: IncomingRoomKeyRequest?) {
|
override fun updateOutgoingGossipingRequestState(requestId: String, state: OutgoingGossipingRequestState) {
|
||||||
if (incomingRoomKeyRequest == null) {
|
doRealmTransaction(realmConfiguration) { realm ->
|
||||||
return
|
realm.where<OutgoingGossipingRequestEntity>()
|
||||||
}
|
.equalTo(OutgoingGossipingRequestEntityFields.REQUEST_ID, requestId)
|
||||||
|
.findAll().forEach {
|
||||||
doRealmTransaction(realmConfiguration) {
|
it.requestState = state
|
||||||
// Delete any previous store request with the same parameters
|
}
|
||||||
it.where<IncomingRoomKeyRequestEntity>()
|
|
||||||
.equalTo(IncomingRoomKeyRequestEntityFields.USER_ID, incomingRoomKeyRequest.userId)
|
|
||||||
.equalTo(IncomingRoomKeyRequestEntityFields.DEVICE_ID, incomingRoomKeyRequest.deviceId)
|
|
||||||
.equalTo(IncomingRoomKeyRequestEntityFields.REQUEST_ID, incomingRoomKeyRequest.requestId)
|
|
||||||
.findAll()
|
|
||||||
.deleteAllFromRealm()
|
|
||||||
|
|
||||||
// Then store it
|
|
||||||
it.createObject(IncomingRoomKeyRequestEntity::class.java).apply {
|
|
||||||
userId = incomingRoomKeyRequest.userId
|
|
||||||
deviceId = incomingRoomKeyRequest.deviceId
|
|
||||||
requestId = incomingRoomKeyRequest.requestId
|
|
||||||
putRequestBody(incomingRoomKeyRequest.requestBody)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun deleteIncomingRoomKeyRequest(incomingRoomKeyRequest: IncomingRoomKeyRequestCommon) {
|
|
||||||
doRealmTransaction(realmConfiguration) {
|
|
||||||
it.where<IncomingRoomKeyRequestEntity>()
|
|
||||||
.equalTo(IncomingRoomKeyRequestEntityFields.USER_ID, incomingRoomKeyRequest.userId)
|
|
||||||
.equalTo(IncomingRoomKeyRequestEntityFields.DEVICE_ID, incomingRoomKeyRequest.deviceId)
|
|
||||||
.equalTo(IncomingRoomKeyRequestEntityFields.REQUEST_ID, incomingRoomKeyRequest.requestId)
|
|
||||||
.findAll()
|
|
||||||
.deleteAllFromRealm()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getIncomingRoomKeyRequest(userId: String, deviceId: String, requestId: String): IncomingRoomKeyRequest? {
|
override fun getIncomingRoomKeyRequest(userId: String, deviceId: String, requestId: String): IncomingRoomKeyRequest? {
|
||||||
return doRealmQueryAndCopy(realmConfiguration) {
|
return doRealmQueryAndCopyList(realmConfiguration) { realm ->
|
||||||
it.where<IncomingRoomKeyRequestEntity>()
|
realm.where<IncomingGossipingRequestEntity>()
|
||||||
.equalTo(IncomingRoomKeyRequestEntityFields.USER_ID, userId)
|
.equalTo(IncomingGossipingRequestEntityFields.TYPE_STR, GossipRequestType.KEY.name)
|
||||||
.equalTo(IncomingRoomKeyRequestEntityFields.DEVICE_ID, deviceId)
|
.equalTo(IncomingGossipingRequestEntityFields.OTHER_DEVICE_ID, deviceId)
|
||||||
.equalTo(IncomingRoomKeyRequestEntityFields.REQUEST_ID, requestId)
|
.equalTo(IncomingGossipingRequestEntityFields.OTHER_USER_ID, userId)
|
||||||
.findFirst()
|
.findAll()
|
||||||
}
|
}.mapNotNull { entity ->
|
||||||
?.toIncomingRoomKeyRequest()
|
entity.toIncomingGossipingRequest() as? IncomingRoomKeyRequest
|
||||||
|
}.firstOrNull()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getPendingIncomingRoomKeyRequests(): MutableList<IncomingRoomKeyRequest> {
|
override fun getPendingIncomingRoomKeyRequests(): List<IncomingRoomKeyRequest> {
|
||||||
return doRealmQueryAndCopyList(realmConfiguration) {
|
return doRealmQueryAndCopyList(realmConfiguration) {
|
||||||
it.where<IncomingRoomKeyRequestEntity>()
|
it.where<IncomingGossipingRequestEntity>()
|
||||||
|
.equalTo(IncomingGossipingRequestEntityFields.TYPE_STR, GossipRequestType.KEY.name)
|
||||||
|
.equalTo(IncomingGossipingRequestEntityFields.REQUEST_STATE_STR, GossipingRequestState.PENDING.name)
|
||||||
.findAll()
|
.findAll()
|
||||||
}
|
}
|
||||||
.map {
|
.map { entity ->
|
||||||
it.toIncomingRoomKeyRequest()
|
IncomingRoomKeyRequest(
|
||||||
|
userId = entity.otherUserId,
|
||||||
|
deviceId = entity.otherDeviceId,
|
||||||
|
requestId = entity.requestId,
|
||||||
|
requestBody = entity.getRequestedKeyInfo(),
|
||||||
|
localCreationTimestamp = entity.localCreationTimestamp
|
||||||
|
)
|
||||||
}
|
}
|
||||||
.toMutableList()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun getPendingIncomingGossipingRequests(): List<IncomingShareRequestCommon> {
|
||||||
|
return doRealmQueryAndCopyList(realmConfiguration) {
|
||||||
|
it.where<IncomingGossipingRequestEntity>()
|
||||||
|
.equalTo(IncomingGossipingRequestEntityFields.REQUEST_STATE_STR, GossipingRequestState.PENDING.name)
|
||||||
|
.findAll()
|
||||||
|
}
|
||||||
|
.mapNotNull { entity ->
|
||||||
|
when (entity.type) {
|
||||||
|
GossipRequestType.KEY -> {
|
||||||
|
IncomingRoomKeyRequest(
|
||||||
|
userId = entity.otherUserId,
|
||||||
|
deviceId = entity.otherDeviceId,
|
||||||
|
requestId = entity.requestId,
|
||||||
|
requestBody = entity.getRequestedKeyInfo(),
|
||||||
|
localCreationTimestamp = entity.localCreationTimestamp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
GossipRequestType.SECRET -> {
|
||||||
|
IncomingSecretShareRequest(
|
||||||
|
userId = entity.otherUserId,
|
||||||
|
deviceId = entity.otherDeviceId,
|
||||||
|
requestId = entity.requestId,
|
||||||
|
secretName = entity.getRequestedSecretName(),
|
||||||
|
localCreationTimestamp = entity.localCreationTimestamp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun storeIncomingGossipingRequest(request: IncomingShareRequestCommon, ageLocalTS: Long?) {
|
||||||
|
doRealmTransactionAsync(realmConfiguration) { realm ->
|
||||||
|
|
||||||
|
// 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 = ageLocalTS ?: 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> {
|
||||||
|
// return doRealmQueryAndCopyList(realmConfiguration) {
|
||||||
|
// it.where<GossipingEventEntity>()
|
||||||
|
// .findAll()
|
||||||
|
// }.map {
|
||||||
|
// it.toIncomingSecretShareRequest()
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
/* ==========================================================================================
|
/* ==========================================================================================
|
||||||
* Cross Signing
|
* Cross Signing
|
||||||
* ========================================================================================== */
|
* ========================================================================================== */
|
||||||
|
@ -1024,6 +1285,28 @@ internal class RealmCryptoStore @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun getOutgoingRoomKeyRequests(): List<OutgoingRoomKeyRequest> {
|
||||||
|
return monarchy.fetchAllMappedSync({ realm ->
|
||||||
|
realm
|
||||||
|
.where(OutgoingGossipingRequestEntity::class.java)
|
||||||
|
.equalTo(OutgoingGossipingRequestEntityFields.TYPE_STR, GossipRequestType.KEY.name)
|
||||||
|
}, { entity ->
|
||||||
|
entity.toOutgoingGossipingRequest() as? OutgoingRoomKeyRequest
|
||||||
|
})
|
||||||
|
.filterNotNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getOutgoingSecretKeyRequests(): List<OutgoingSecretRequest> {
|
||||||
|
return monarchy.fetchAllMappedSync({ realm ->
|
||||||
|
realm
|
||||||
|
.where(OutgoingGossipingRequestEntity::class.java)
|
||||||
|
.equalTo(OutgoingGossipingRequestEntityFields.TYPE_STR, GossipRequestType.SECRET.name)
|
||||||
|
}, { entity ->
|
||||||
|
entity.toOutgoingGossipingRequest() as? OutgoingSecretRequest
|
||||||
|
})
|
||||||
|
.filterNotNull()
|
||||||
|
}
|
||||||
|
|
||||||
override fun getCrossSigningInfo(userId: String): MXCrossSigningInfo? {
|
override fun getCrossSigningInfo(userId: String): MXCrossSigningInfo? {
|
||||||
return doRealmQueryAndCopy(realmConfiguration) { realm ->
|
return doRealmQueryAndCopy(realmConfiguration) { realm ->
|
||||||
realm.where(CrossSigningInfoEntity::class.java)
|
realm.where(CrossSigningInfoEntity::class.java)
|
||||||
|
|
|
@ -23,7 +23,10 @@ import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo
|
||||||
import im.vector.matrix.android.internal.crypto.store.db.model.CrossSigningInfoEntityFields
|
import im.vector.matrix.android.internal.crypto.store.db.model.CrossSigningInfoEntityFields
|
||||||
import im.vector.matrix.android.internal.crypto.store.db.model.CryptoMetadataEntityFields
|
import im.vector.matrix.android.internal.crypto.store.db.model.CryptoMetadataEntityFields
|
||||||
import im.vector.matrix.android.internal.crypto.store.db.model.DeviceInfoEntityFields
|
import im.vector.matrix.android.internal.crypto.store.db.model.DeviceInfoEntityFields
|
||||||
|
import im.vector.matrix.android.internal.crypto.store.db.model.GossipingEventEntityFields
|
||||||
|
import im.vector.matrix.android.internal.crypto.store.db.model.IncomingGossipingRequestEntityFields
|
||||||
import im.vector.matrix.android.internal.crypto.store.db.model.KeyInfoEntityFields
|
import im.vector.matrix.android.internal.crypto.store.db.model.KeyInfoEntityFields
|
||||||
|
import im.vector.matrix.android.internal.crypto.store.db.model.OutgoingGossipingRequestEntityFields
|
||||||
import im.vector.matrix.android.internal.crypto.store.db.model.TrustLevelEntityFields
|
import im.vector.matrix.android.internal.crypto.store.db.model.TrustLevelEntityFields
|
||||||
import im.vector.matrix.android.internal.crypto.store.db.model.UserEntityFields
|
import im.vector.matrix.android.internal.crypto.store.db.model.UserEntityFields
|
||||||
import im.vector.matrix.android.internal.di.SerializeNulls
|
import im.vector.matrix.android.internal.di.SerializeNulls
|
||||||
|
@ -34,67 +37,73 @@ import timber.log.Timber
|
||||||
internal object RealmCryptoStoreMigration : RealmMigration {
|
internal object RealmCryptoStoreMigration : RealmMigration {
|
||||||
|
|
||||||
// Version 1L added Cross Signing info persistence
|
// Version 1L added Cross Signing info persistence
|
||||||
const val CRYPTO_STORE_SCHEMA_VERSION = 1L
|
const val CRYPTO_STORE_SCHEMA_VERSION = 3L
|
||||||
|
|
||||||
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")
|
||||||
|
|
||||||
if (oldVersion <= 0) {
|
if (oldVersion <= 0) migrateTo1(realm)
|
||||||
Timber.d("Step 0 -> 1")
|
if (oldVersion <= 1) migrateTo2(realm)
|
||||||
Timber.d("Create KeyInfoEntity")
|
if (oldVersion <= 2) migrateTo3(realm)
|
||||||
|
}
|
||||||
|
|
||||||
val trustLevelentityEntitySchema = realm.schema.create("TrustLevelEntity")
|
private fun migrateTo1(realm: DynamicRealm) {
|
||||||
.addField(TrustLevelEntityFields.CROSS_SIGNED_VERIFIED, Boolean::class.java)
|
Timber.d("Step 0 -> 1")
|
||||||
.setNullable(TrustLevelEntityFields.CROSS_SIGNED_VERIFIED, true)
|
Timber.d("Create KeyInfoEntity")
|
||||||
.addField(TrustLevelEntityFields.LOCALLY_VERIFIED, Boolean::class.java)
|
|
||||||
.setNullable(TrustLevelEntityFields.LOCALLY_VERIFIED, true)
|
|
||||||
|
|
||||||
val keyInfoEntitySchema = realm.schema.create("KeyInfoEntity")
|
val trustLevelentityEntitySchema = realm.schema.create("TrustLevelEntity")
|
||||||
.addField(KeyInfoEntityFields.PUBLIC_KEY_BASE64, String::class.java)
|
.addField(TrustLevelEntityFields.CROSS_SIGNED_VERIFIED, Boolean::class.java)
|
||||||
.addField(KeyInfoEntityFields.SIGNATURES, String::class.java)
|
.setNullable(TrustLevelEntityFields.CROSS_SIGNED_VERIFIED, true)
|
||||||
.addRealmListField(KeyInfoEntityFields.USAGES.`$`, String::class.java)
|
.addField(TrustLevelEntityFields.LOCALLY_VERIFIED, Boolean::class.java)
|
||||||
.addRealmObjectField(KeyInfoEntityFields.TRUST_LEVEL_ENTITY.`$`, trustLevelentityEntitySchema)
|
.setNullable(TrustLevelEntityFields.LOCALLY_VERIFIED, true)
|
||||||
|
|
||||||
Timber.d("Create CrossSigningInfoEntity")
|
val keyInfoEntitySchema = realm.schema.create("KeyInfoEntity")
|
||||||
|
.addField(KeyInfoEntityFields.PUBLIC_KEY_BASE64, String::class.java)
|
||||||
|
.addField(KeyInfoEntityFields.SIGNATURES, String::class.java)
|
||||||
|
.addRealmListField(KeyInfoEntityFields.USAGES.`$`, String::class.java)
|
||||||
|
.addRealmObjectField(KeyInfoEntityFields.TRUST_LEVEL_ENTITY.`$`, trustLevelentityEntitySchema)
|
||||||
|
|
||||||
val crossSigningInfoSchema = realm.schema.create("CrossSigningInfoEntity")
|
Timber.d("Create CrossSigningInfoEntity")
|
||||||
.addField(CrossSigningInfoEntityFields.USER_ID, String::class.java)
|
|
||||||
.addPrimaryKey(CrossSigningInfoEntityFields.USER_ID)
|
|
||||||
.addRealmListField(CrossSigningInfoEntityFields.CROSS_SIGNING_KEYS.`$`, keyInfoEntitySchema)
|
|
||||||
|
|
||||||
Timber.d("Updating UserEntity table")
|
val crossSigningInfoSchema = realm.schema.create("CrossSigningInfoEntity")
|
||||||
realm.schema.get("UserEntity")
|
.addField(CrossSigningInfoEntityFields.USER_ID, String::class.java)
|
||||||
?.addRealmObjectField(UserEntityFields.CROSS_SIGNING_INFO_ENTITY.`$`, crossSigningInfoSchema)
|
.addPrimaryKey(CrossSigningInfoEntityFields.USER_ID)
|
||||||
|
.addRealmListField(CrossSigningInfoEntityFields.CROSS_SIGNING_KEYS.`$`, keyInfoEntitySchema)
|
||||||
|
|
||||||
Timber.d("Updating CryptoMetadataEntity table")
|
Timber.d("Updating UserEntity table")
|
||||||
realm.schema.get("CryptoMetadataEntity")
|
realm.schema.get("UserEntity")
|
||||||
?.addField(CryptoMetadataEntityFields.X_SIGN_MASTER_PRIVATE_KEY, String::class.java)
|
?.addRealmObjectField(UserEntityFields.CROSS_SIGNING_INFO_ENTITY.`$`, crossSigningInfoSchema)
|
||||||
?.addField(CryptoMetadataEntityFields.X_SIGN_USER_PRIVATE_KEY, String::class.java)
|
|
||||||
?.addField(CryptoMetadataEntityFields.X_SIGN_SELF_SIGNED_PRIVATE_KEY, String::class.java)
|
|
||||||
|
|
||||||
val moshi = Moshi.Builder().add(SerializeNulls.JSON_ADAPTER_FACTORY).build()
|
Timber.d("Updating CryptoMetadataEntity table")
|
||||||
val listMigrationAdapter = moshi.adapter<List<String>>(Types.newParameterizedType(
|
realm.schema.get("CryptoMetadataEntity")
|
||||||
List::class.java,
|
?.addField(CryptoMetadataEntityFields.X_SIGN_MASTER_PRIVATE_KEY, String::class.java)
|
||||||
String::class.java,
|
?.addField(CryptoMetadataEntityFields.X_SIGN_USER_PRIVATE_KEY, String::class.java)
|
||||||
Any::class.java
|
?.addField(CryptoMetadataEntityFields.X_SIGN_SELF_SIGNED_PRIVATE_KEY, String::class.java)
|
||||||
))
|
|
||||||
val mapMigrationAdapter = moshi.adapter<JsonDict>(Types.newParameterizedType(
|
|
||||||
Map::class.java,
|
|
||||||
String::class.java,
|
|
||||||
Any::class.java
|
|
||||||
))
|
|
||||||
|
|
||||||
realm.schema.get("DeviceInfoEntity")
|
val moshi = Moshi.Builder().add(SerializeNulls.JSON_ADAPTER_FACTORY).build()
|
||||||
?.addField(DeviceInfoEntityFields.USER_ID, String::class.java)
|
val listMigrationAdapter = moshi.adapter<List<String>>(Types.newParameterizedType(
|
||||||
?.addField(DeviceInfoEntityFields.ALGORITHM_LIST_JSON, String::class.java)
|
List::class.java,
|
||||||
?.addField(DeviceInfoEntityFields.KEYS_MAP_JSON, String::class.java)
|
String::class.java,
|
||||||
?.addField(DeviceInfoEntityFields.SIGNATURE_MAP_JSON, String::class.java)
|
Any::class.java
|
||||||
?.addField(DeviceInfoEntityFields.UNSIGNED_MAP_JSON, String::class.java)
|
))
|
||||||
?.addField(DeviceInfoEntityFields.IS_BLOCKED, Boolean::class.java)
|
val mapMigrationAdapter = moshi.adapter<JsonDict>(Types.newParameterizedType(
|
||||||
?.setNullable(DeviceInfoEntityFields.IS_BLOCKED, true)
|
Map::class.java,
|
||||||
?.addRealmObjectField(DeviceInfoEntityFields.TRUST_LEVEL_ENTITY.`$`, trustLevelentityEntitySchema)
|
String::class.java,
|
||||||
?.transform { obj ->
|
Any::class.java
|
||||||
|
))
|
||||||
|
|
||||||
|
realm.schema.get("DeviceInfoEntity")
|
||||||
|
?.addField(DeviceInfoEntityFields.USER_ID, String::class.java)
|
||||||
|
?.addField(DeviceInfoEntityFields.ALGORITHM_LIST_JSON, String::class.java)
|
||||||
|
?.addField(DeviceInfoEntityFields.KEYS_MAP_JSON, String::class.java)
|
||||||
|
?.addField(DeviceInfoEntityFields.SIGNATURE_MAP_JSON, String::class.java)
|
||||||
|
?.addField(DeviceInfoEntityFields.UNSIGNED_MAP_JSON, String::class.java)
|
||||||
|
?.addField(DeviceInfoEntityFields.IS_BLOCKED, Boolean::class.java)
|
||||||
|
?.setNullable(DeviceInfoEntityFields.IS_BLOCKED, true)
|
||||||
|
?.addRealmObjectField(DeviceInfoEntityFields.TRUST_LEVEL_ENTITY.`$`, trustLevelentityEntitySchema)
|
||||||
|
?.transform { obj ->
|
||||||
|
|
||||||
|
try {
|
||||||
val oldSerializedData = obj.getString("deviceInfoData")
|
val oldSerializedData = obj.getString("deviceInfoData")
|
||||||
deserializeFromRealm<MXDeviceInfo>(oldSerializedData)?.let { oldDevice ->
|
deserializeFromRealm<MXDeviceInfo>(oldSerializedData)?.let { oldDevice ->
|
||||||
|
|
||||||
|
@ -128,8 +137,60 @@ internal object RealmCryptoStoreMigration : RealmMigration {
|
||||||
obj.setString(DeviceInfoEntityFields.SIGNATURE_MAP_JSON, mapMigrationAdapter.toJson(oldDevice.signatures))
|
obj.setString(DeviceInfoEntityFields.SIGNATURE_MAP_JSON, mapMigrationAdapter.toJson(oldDevice.signatures))
|
||||||
obj.setString(DeviceInfoEntityFields.UNSIGNED_MAP_JSON, mapMigrationAdapter.toJson(oldDevice.unsigned))
|
obj.setString(DeviceInfoEntityFields.UNSIGNED_MAP_JSON, mapMigrationAdapter.toJson(oldDevice.unsigned))
|
||||||
}
|
}
|
||||||
|
} catch (failure: Throwable) {
|
||||||
|
Timber.w(failure, "Crypto Data base migration error")
|
||||||
|
// an unfortunate refactor did modify that class, making deserialization failing
|
||||||
|
// so we just skip and ignore..
|
||||||
}
|
}
|
||||||
?.removeField("deviceInfoData")
|
}
|
||||||
}
|
?.removeField("deviceInfoData")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun migrateTo2(realm: DynamicRealm) {
|
||||||
|
Timber.d("Step 1 -> 2")
|
||||||
|
realm.schema.remove("OutgoingRoomKeyRequestEntity")
|
||||||
|
realm.schema.remove("IncomingRoomKeyRequestEntity")
|
||||||
|
|
||||||
|
// Not need to migrate existing request, just start fresh?
|
||||||
|
|
||||||
|
realm.schema.create("GossipingEventEntity")
|
||||||
|
.addField(GossipingEventEntityFields.TYPE, String::class.java)
|
||||||
|
.addIndex(GossipingEventEntityFields.TYPE)
|
||||||
|
.addField(GossipingEventEntityFields.CONTENT, String::class.java)
|
||||||
|
.addField(GossipingEventEntityFields.SENDER, String::class.java)
|
||||||
|
.addIndex(GossipingEventEntityFields.SENDER)
|
||||||
|
.addField(GossipingEventEntityFields.DECRYPTION_RESULT_JSON, String::class.java)
|
||||||
|
.addField(GossipingEventEntityFields.DECRYPTION_ERROR_CODE, String::class.java)
|
||||||
|
.addField(GossipingEventEntityFields.AGE_LOCAL_TS, Long::class.java)
|
||||||
|
.setNullable(GossipingEventEntityFields.AGE_LOCAL_TS, true)
|
||||||
|
.addField(GossipingEventEntityFields.SEND_STATE_STR, String::class.java)
|
||||||
|
|
||||||
|
realm.schema.create("IncomingGossipingRequestEntity")
|
||||||
|
.addField(IncomingGossipingRequestEntityFields.REQUEST_ID, String::class.java)
|
||||||
|
.addIndex(IncomingGossipingRequestEntityFields.REQUEST_ID)
|
||||||
|
.addField(IncomingGossipingRequestEntityFields.TYPE_STR, String::class.java)
|
||||||
|
.addIndex(IncomingGossipingRequestEntityFields.TYPE_STR)
|
||||||
|
.addField(IncomingGossipingRequestEntityFields.OTHER_USER_ID, String::class.java)
|
||||||
|
.addField(IncomingGossipingRequestEntityFields.REQUESTED_INFO_STR, String::class.java)
|
||||||
|
.addField(IncomingGossipingRequestEntityFields.OTHER_DEVICE_ID, String::class.java)
|
||||||
|
.addField(IncomingGossipingRequestEntityFields.REQUEST_STATE_STR, String::class.java)
|
||||||
|
.addField(IncomingGossipingRequestEntityFields.LOCAL_CREATION_TIMESTAMP, Long::class.java)
|
||||||
|
.setNullable(IncomingGossipingRequestEntityFields.LOCAL_CREATION_TIMESTAMP, true)
|
||||||
|
|
||||||
|
realm.schema.create("OutgoingGossipingRequestEntity")
|
||||||
|
.addField(OutgoingGossipingRequestEntityFields.REQUEST_ID, String::class.java)
|
||||||
|
.addIndex(OutgoingGossipingRequestEntityFields.REQUEST_ID)
|
||||||
|
.addField(OutgoingGossipingRequestEntityFields.RECIPIENTS_DATA, String::class.java)
|
||||||
|
.addField(OutgoingGossipingRequestEntityFields.REQUESTED_INFO_STR, String::class.java)
|
||||||
|
.addField(OutgoingGossipingRequestEntityFields.TYPE_STR, String::class.java)
|
||||||
|
.addIndex(OutgoingGossipingRequestEntityFields.TYPE_STR)
|
||||||
|
.addField(OutgoingGossipingRequestEntityFields.REQUEST_STATE_STR, String::class.java)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun migrateTo3(realm: DynamicRealm) {
|
||||||
|
Timber.d("Updating CryptoMetadataEntity table")
|
||||||
|
realm.schema.get("CryptoMetadataEntity")
|
||||||
|
?.addField(CryptoMetadataEntityFields.KEY_BACKUP_RECOVERY_KEY, String::class.java)
|
||||||
|
?.addField(CryptoMetadataEntityFields.KEY_BACKUP_RECOVERY_KEY_VERSION, String::class.java)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,12 +20,13 @@ import im.vector.matrix.android.internal.crypto.store.db.model.CrossSigningInfoE
|
||||||
import im.vector.matrix.android.internal.crypto.store.db.model.CryptoMetadataEntity
|
import im.vector.matrix.android.internal.crypto.store.db.model.CryptoMetadataEntity
|
||||||
import im.vector.matrix.android.internal.crypto.store.db.model.CryptoRoomEntity
|
import im.vector.matrix.android.internal.crypto.store.db.model.CryptoRoomEntity
|
||||||
import im.vector.matrix.android.internal.crypto.store.db.model.DeviceInfoEntity
|
import im.vector.matrix.android.internal.crypto.store.db.model.DeviceInfoEntity
|
||||||
import im.vector.matrix.android.internal.crypto.store.db.model.IncomingRoomKeyRequestEntity
|
import im.vector.matrix.android.internal.crypto.store.db.model.GossipingEventEntity
|
||||||
|
import im.vector.matrix.android.internal.crypto.store.db.model.IncomingGossipingRequestEntity
|
||||||
import im.vector.matrix.android.internal.crypto.store.db.model.KeyInfoEntity
|
import im.vector.matrix.android.internal.crypto.store.db.model.KeyInfoEntity
|
||||||
import im.vector.matrix.android.internal.crypto.store.db.model.KeysBackupDataEntity
|
import im.vector.matrix.android.internal.crypto.store.db.model.KeysBackupDataEntity
|
||||||
import im.vector.matrix.android.internal.crypto.store.db.model.OlmInboundGroupSessionEntity
|
import im.vector.matrix.android.internal.crypto.store.db.model.OlmInboundGroupSessionEntity
|
||||||
import im.vector.matrix.android.internal.crypto.store.db.model.OlmSessionEntity
|
import im.vector.matrix.android.internal.crypto.store.db.model.OlmSessionEntity
|
||||||
import im.vector.matrix.android.internal.crypto.store.db.model.OutgoingRoomKeyRequestEntity
|
import im.vector.matrix.android.internal.crypto.store.db.model.OutgoingGossipingRequestEntity
|
||||||
import im.vector.matrix.android.internal.crypto.store.db.model.TrustLevelEntity
|
import im.vector.matrix.android.internal.crypto.store.db.model.TrustLevelEntity
|
||||||
import im.vector.matrix.android.internal.crypto.store.db.model.UserEntity
|
import im.vector.matrix.android.internal.crypto.store.db.model.UserEntity
|
||||||
import io.realm.annotations.RealmModule
|
import io.realm.annotations.RealmModule
|
||||||
|
@ -38,14 +39,15 @@ import io.realm.annotations.RealmModule
|
||||||
CryptoMetadataEntity::class,
|
CryptoMetadataEntity::class,
|
||||||
CryptoRoomEntity::class,
|
CryptoRoomEntity::class,
|
||||||
DeviceInfoEntity::class,
|
DeviceInfoEntity::class,
|
||||||
IncomingRoomKeyRequestEntity::class,
|
|
||||||
KeysBackupDataEntity::class,
|
KeysBackupDataEntity::class,
|
||||||
OlmInboundGroupSessionEntity::class,
|
OlmInboundGroupSessionEntity::class,
|
||||||
OlmSessionEntity::class,
|
OlmSessionEntity::class,
|
||||||
OutgoingRoomKeyRequestEntity::class,
|
|
||||||
UserEntity::class,
|
UserEntity::class,
|
||||||
KeyInfoEntity::class,
|
KeyInfoEntity::class,
|
||||||
CrossSigningInfoEntity::class,
|
CrossSigningInfoEntity::class,
|
||||||
TrustLevelEntity::class
|
TrustLevelEntity::class,
|
||||||
|
GossipingEventEntity::class,
|
||||||
|
IncomingGossipingRequestEntity::class,
|
||||||
|
OutgoingGossipingRequestEntity::class
|
||||||
])
|
])
|
||||||
internal class RealmCryptoStoreModule
|
internal class RealmCryptoStoreModule
|
||||||
|
|
|
@ -38,7 +38,9 @@ internal open class CryptoMetadataEntity(
|
||||||
|
|
||||||
var xSignMasterPrivateKey: String? = null,
|
var xSignMasterPrivateKey: String? = null,
|
||||||
var xSignUserPrivateKey: String? = null,
|
var xSignUserPrivateKey: String? = null,
|
||||||
var xSignSelfSignedPrivateKey: String? = null
|
var xSignSelfSignedPrivateKey: String? = null,
|
||||||
|
var keyBackupRecoveryKey: String? = null,
|
||||||
|
var keyBackupRecoveryKeyVersion: String? = null
|
||||||
|
|
||||||
// var crossSigningInfoEntity: CrossSigningInfoEntity? = null
|
// var crossSigningInfoEntity: CrossSigningInfoEntity? = null
|
||||||
) : RealmObject() {
|
) : RealmObject() {
|
||||||
|
|
|
@ -0,0 +1,88 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2020 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.matrix.android.internal.crypto.store.db.model
|
||||||
|
|
||||||
|
import com.squareup.moshi.JsonDataException
|
||||||
|
import im.vector.matrix.android.api.session.crypto.MXCryptoError
|
||||||
|
import im.vector.matrix.android.api.session.events.model.Event
|
||||||
|
import im.vector.matrix.android.api.session.room.send.SendState
|
||||||
|
import im.vector.matrix.android.internal.crypto.MXEventDecryptionResult
|
||||||
|
import im.vector.matrix.android.internal.crypto.algorithms.olm.OlmDecryptionResult
|
||||||
|
import im.vector.matrix.android.internal.database.mapper.ContentMapper
|
||||||
|
import im.vector.matrix.android.internal.di.MoshiProvider
|
||||||
|
import io.realm.RealmObject
|
||||||
|
import io.realm.annotations.Index
|
||||||
|
import timber.log.Timber
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Keep track of gossiping event received in toDevice messages
|
||||||
|
* (room key request, or sss secret sharing, as well as cancellations)
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
internal open class GossipingEventEntity(@Index var type: String? = "",
|
||||||
|
var content: String? = null,
|
||||||
|
@Index var sender: String? = null,
|
||||||
|
var decryptionResultJson: String? = null,
|
||||||
|
var decryptionErrorCode: String? = null,
|
||||||
|
var ageLocalTs: Long? = null) : RealmObject() {
|
||||||
|
|
||||||
|
private var sendStateStr: String = SendState.UNKNOWN.name
|
||||||
|
|
||||||
|
var sendState: SendState
|
||||||
|
get() {
|
||||||
|
return SendState.valueOf(sendStateStr)
|
||||||
|
}
|
||||||
|
set(value) {
|
||||||
|
sendStateStr = value.name
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object
|
||||||
|
|
||||||
|
fun setDecryptionResult(result: MXEventDecryptionResult) {
|
||||||
|
val decryptionResult = OlmDecryptionResult(
|
||||||
|
payload = result.clearEvent,
|
||||||
|
senderKey = result.senderCurve25519Key,
|
||||||
|
keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) },
|
||||||
|
forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain
|
||||||
|
)
|
||||||
|
val adapter = MoshiProvider.providesMoshi().adapter<OlmDecryptionResult>(OlmDecryptionResult::class.java)
|
||||||
|
decryptionResultJson = adapter.toJson(decryptionResult)
|
||||||
|
decryptionErrorCode = null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toModel(): Event {
|
||||||
|
return Event(
|
||||||
|
type = this.type ?: "",
|
||||||
|
content = ContentMapper.map(this.content),
|
||||||
|
senderId = this.sender
|
||||||
|
).also {
|
||||||
|
it.ageLocalTs = this.ageLocalTs
|
||||||
|
it.sendState = this.sendState
|
||||||
|
this.decryptionResultJson?.let { json ->
|
||||||
|
try {
|
||||||
|
it.mxDecryptionResult = MoshiProvider.providesMoshi().adapter(OlmDecryptionResult::class.java).fromJson(json)
|
||||||
|
} catch (t: JsonDataException) {
|
||||||
|
Timber.e(t, "Failed to parse decryption result")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// TODO get the full crypto error object
|
||||||
|
it.mCryptoError = this.decryptionErrorCode?.let { errorCode ->
|
||||||
|
MXCryptoError.ErrorType.valueOf(errorCode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,89 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2020 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.matrix.android.internal.crypto.store.db.model
|
||||||
|
|
||||||
|
import im.vector.matrix.android.api.extensions.tryThis
|
||||||
|
import im.vector.matrix.android.internal.crypto.GossipRequestType
|
||||||
|
import im.vector.matrix.android.internal.crypto.GossipingRequestState
|
||||||
|
import im.vector.matrix.android.internal.crypto.IncomingRoomKeyRequest
|
||||||
|
import im.vector.matrix.android.internal.crypto.IncomingSecretShareRequest
|
||||||
|
import im.vector.matrix.android.internal.crypto.IncomingShareRequestCommon
|
||||||
|
import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody
|
||||||
|
import io.realm.RealmObject
|
||||||
|
import io.realm.annotations.Index
|
||||||
|
|
||||||
|
internal open class IncomingGossipingRequestEntity(@Index var requestId: String? = "",
|
||||||
|
@Index var typeStr: String? = null,
|
||||||
|
var otherUserId: String? = null,
|
||||||
|
var requestedInfoStr: String? = null,
|
||||||
|
var otherDeviceId: String? = null,
|
||||||
|
var localCreationTimestamp: Long? = null
|
||||||
|
) : RealmObject() {
|
||||||
|
|
||||||
|
fun getRequestedSecretName(): String? = if (type == GossipRequestType.SECRET) {
|
||||||
|
requestedInfoStr
|
||||||
|
} else null
|
||||||
|
|
||||||
|
fun getRequestedKeyInfo(): RoomKeyRequestBody? = if (type == GossipRequestType.KEY) {
|
||||||
|
RoomKeyRequestBody.fromJson(requestedInfoStr)
|
||||||
|
} else null
|
||||||
|
|
||||||
|
var type: GossipRequestType
|
||||||
|
get() {
|
||||||
|
return tryThis { typeStr?.let { GossipRequestType.valueOf(it) } } ?: GossipRequestType.KEY
|
||||||
|
}
|
||||||
|
set(value) {
|
||||||
|
typeStr = value.name
|
||||||
|
}
|
||||||
|
|
||||||
|
private var requestStateStr: String = GossipingRequestState.NONE.name
|
||||||
|
|
||||||
|
var requestState: GossipingRequestState
|
||||||
|
get() {
|
||||||
|
return tryThis { GossipingRequestState.valueOf(requestStateStr) }
|
||||||
|
?: GossipingRequestState.NONE
|
||||||
|
}
|
||||||
|
set(value) {
|
||||||
|
requestStateStr = value.name
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object
|
||||||
|
|
||||||
|
fun toIncomingGossipingRequest(): IncomingShareRequestCommon {
|
||||||
|
return when (type) {
|
||||||
|
GossipRequestType.KEY -> {
|
||||||
|
IncomingRoomKeyRequest(
|
||||||
|
requestBody = getRequestedKeyInfo(),
|
||||||
|
deviceId = otherDeviceId,
|
||||||
|
userId = otherUserId,
|
||||||
|
requestId = requestId,
|
||||||
|
state = requestState,
|
||||||
|
localCreationTimestamp = localCreationTimestamp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
GossipRequestType.SECRET -> {
|
||||||
|
IncomingSecretShareRequest(
|
||||||
|
secretName = getRequestedSecretName(),
|
||||||
|
deviceId = otherDeviceId,
|
||||||
|
userId = otherUserId,
|
||||||
|
requestId = requestId,
|
||||||
|
localCreationTimestamp = localCreationTimestamp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,56 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2018 New Vector Ltd
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package im.vector.matrix.android.internal.crypto.store.db.model
|
|
||||||
|
|
||||||
import im.vector.matrix.android.internal.crypto.IncomingRoomKeyRequest
|
|
||||||
import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody
|
|
||||||
import io.realm.RealmObject
|
|
||||||
|
|
||||||
internal open class IncomingRoomKeyRequestEntity(
|
|
||||||
var requestId: String? = null,
|
|
||||||
var userId: String? = null,
|
|
||||||
var deviceId: String? = null,
|
|
||||||
// RoomKeyRequestBody fields
|
|
||||||
var requestBodyAlgorithm: String? = null,
|
|
||||||
var requestBodyRoomId: String? = null,
|
|
||||||
var requestBodySenderKey: String? = null,
|
|
||||||
var requestBodySessionId: String? = null
|
|
||||||
) : RealmObject() {
|
|
||||||
|
|
||||||
fun toIncomingRoomKeyRequest(): IncomingRoomKeyRequest {
|
|
||||||
return IncomingRoomKeyRequest(
|
|
||||||
requestId = requestId,
|
|
||||||
userId = userId,
|
|
||||||
deviceId = deviceId,
|
|
||||||
requestBody = RoomKeyRequestBody(
|
|
||||||
algorithm = requestBodyAlgorithm,
|
|
||||||
roomId = requestBodyRoomId,
|
|
||||||
senderKey = requestBodySenderKey,
|
|
||||||
sessionId = requestBodySessionId
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun putRequestBody(requestBody: RoomKeyRequestBody?) {
|
|
||||||
requestBody?.let {
|
|
||||||
requestBodyAlgorithm = it.algorithm
|
|
||||||
requestBodyRoomId = it.roomId
|
|
||||||
requestBodySenderKey = it.senderKey
|
|
||||||
requestBodySessionId = it.sessionId
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,104 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2020 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.matrix.android.internal.crypto.store.db.model
|
||||||
|
|
||||||
|
import com.squareup.moshi.JsonAdapter
|
||||||
|
import com.squareup.moshi.Types
|
||||||
|
import im.vector.matrix.android.api.extensions.tryThis
|
||||||
|
import im.vector.matrix.android.internal.crypto.GossipRequestType
|
||||||
|
import im.vector.matrix.android.internal.crypto.OutgoingGossipingRequest
|
||||||
|
import im.vector.matrix.android.internal.crypto.OutgoingGossipingRequestState
|
||||||
|
import im.vector.matrix.android.internal.crypto.OutgoingRoomKeyRequest
|
||||||
|
import im.vector.matrix.android.internal.crypto.OutgoingSecretRequest
|
||||||
|
import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody
|
||||||
|
import im.vector.matrix.android.internal.di.MoshiProvider
|
||||||
|
import io.realm.RealmObject
|
||||||
|
import io.realm.annotations.Index
|
||||||
|
|
||||||
|
internal open class OutgoingGossipingRequestEntity(
|
||||||
|
@Index var requestId: String? = null,
|
||||||
|
var recipientsData: String? = null,
|
||||||
|
var requestedInfoStr: String? = null,
|
||||||
|
@Index var typeStr: String? = null
|
||||||
|
) : RealmObject() {
|
||||||
|
|
||||||
|
fun getRequestedSecretName(): String? = if (type == GossipRequestType.SECRET) {
|
||||||
|
requestedInfoStr
|
||||||
|
} else null
|
||||||
|
|
||||||
|
fun getRequestedKeyInfo(): RoomKeyRequestBody? = if (type == GossipRequestType.KEY) {
|
||||||
|
RoomKeyRequestBody.fromJson(requestedInfoStr)
|
||||||
|
} else null
|
||||||
|
|
||||||
|
var type: GossipRequestType
|
||||||
|
get() {
|
||||||
|
return tryThis { typeStr?.let { GossipRequestType.valueOf(it) } } ?: GossipRequestType.KEY
|
||||||
|
}
|
||||||
|
set(value) {
|
||||||
|
typeStr = value.name
|
||||||
|
}
|
||||||
|
|
||||||
|
private var requestStateStr: String = OutgoingGossipingRequestState.UNSENT.name
|
||||||
|
|
||||||
|
var requestState: OutgoingGossipingRequestState
|
||||||
|
get() {
|
||||||
|
return tryThis { OutgoingGossipingRequestState.valueOf(requestStateStr) }
|
||||||
|
?: OutgoingGossipingRequestState.UNSENT
|
||||||
|
}
|
||||||
|
set(value) {
|
||||||
|
requestStateStr = value.name
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
private val recipientsDataMapper: JsonAdapter<Map<String, List<String>>> =
|
||||||
|
MoshiProvider
|
||||||
|
.providesMoshi()
|
||||||
|
.adapter<Map<String, List<String>>>(
|
||||||
|
Types.newParameterizedType(Map::class.java, String::class.java, List::class.java)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toOutgoingGossipingRequest(): OutgoingGossipingRequest {
|
||||||
|
return when (type) {
|
||||||
|
GossipRequestType.KEY -> {
|
||||||
|
OutgoingRoomKeyRequest(
|
||||||
|
requestBody = getRequestedKeyInfo(),
|
||||||
|
recipients = getRecipients() ?: emptyMap(),
|
||||||
|
requestId = requestId ?: "",
|
||||||
|
state = requestState
|
||||||
|
)
|
||||||
|
}
|
||||||
|
GossipRequestType.SECRET -> {
|
||||||
|
OutgoingSecretRequest(
|
||||||
|
secretName = getRequestedSecretName(),
|
||||||
|
recipients = getRecipients() ?: emptyMap(),
|
||||||
|
requestId = requestId ?: "",
|
||||||
|
state = requestState
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getRecipients(): Map<String, List<String>>? {
|
||||||
|
return this.recipientsData?.let { recipientsDataMapper.fromJson(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setRecipients(recipients: Map<String, List<String>>) {
|
||||||
|
this.recipientsData = recipientsDataMapper.toJson(recipients)
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,76 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2018 New Vector Ltd
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package im.vector.matrix.android.internal.crypto.store.db.model
|
|
||||||
|
|
||||||
import im.vector.matrix.android.internal.crypto.OutgoingRoomKeyRequest
|
|
||||||
import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody
|
|
||||||
import im.vector.matrix.android.internal.crypto.store.db.deserializeFromRealm
|
|
||||||
import im.vector.matrix.android.internal.crypto.store.db.serializeForRealm
|
|
||||||
import io.realm.RealmObject
|
|
||||||
import io.realm.annotations.PrimaryKey
|
|
||||||
|
|
||||||
internal open class OutgoingRoomKeyRequestEntity(
|
|
||||||
@PrimaryKey var requestId: String? = null,
|
|
||||||
var cancellationTxnId: String? = null,
|
|
||||||
// Serialized Json
|
|
||||||
var recipientsData: String? = null,
|
|
||||||
// RoomKeyRequestBody fields
|
|
||||||
var requestBodyAlgorithm: String? = null,
|
|
||||||
var requestBodyRoomId: String? = null,
|
|
||||||
var requestBodySenderKey: String? = null,
|
|
||||||
var requestBodySessionId: String? = null,
|
|
||||||
// State
|
|
||||||
var state: Int = 0
|
|
||||||
) : RealmObject() {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Convert to OutgoingRoomKeyRequest
|
|
||||||
*/
|
|
||||||
fun toOutgoingRoomKeyRequest(): OutgoingRoomKeyRequest {
|
|
||||||
val cancellationTxnId = this.cancellationTxnId
|
|
||||||
return OutgoingRoomKeyRequest(
|
|
||||||
RoomKeyRequestBody(
|
|
||||||
algorithm = requestBodyAlgorithm,
|
|
||||||
roomId = requestBodyRoomId,
|
|
||||||
senderKey = requestBodySenderKey,
|
|
||||||
sessionId = requestBodySessionId
|
|
||||||
),
|
|
||||||
getRecipients()!!,
|
|
||||||
requestId!!,
|
|
||||||
OutgoingRoomKeyRequest.RequestState.from(state)
|
|
||||||
).apply {
|
|
||||||
this.cancellationTxnId = cancellationTxnId
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getRecipients(): List<Map<String, String>>? {
|
|
||||||
return deserializeFromRealm(recipientsData)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun putRecipients(recipients: List<Map<String, String>>?) {
|
|
||||||
recipientsData = serializeForRealm(recipients)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun putRequestBody(requestBody: RoomKeyRequestBody?) {
|
|
||||||
requestBody?.let {
|
|
||||||
requestBodyAlgorithm = it.algorithm
|
|
||||||
requestBodyRoomId = it.roomId
|
|
||||||
requestBodySenderKey = it.senderKey
|
|
||||||
requestBodySessionId = it.sessionId
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,172 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2020 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.matrix.android.internal.crypto.tasks
|
||||||
|
|
||||||
|
import dagger.Lazy
|
||||||
|
import im.vector.matrix.android.internal.crypto.MXOlmDevice
|
||||||
|
import im.vector.matrix.android.internal.crypto.MyDeviceInfoHolder
|
||||||
|
import im.vector.matrix.android.internal.crypto.crosssigning.canonicalSignable
|
||||||
|
import im.vector.matrix.android.internal.crypto.crosssigning.toBase64NoPadding
|
||||||
|
import im.vector.matrix.android.internal.crypto.model.CryptoCrossSigningKey
|
||||||
|
import im.vector.matrix.android.internal.crypto.model.KeyUsage
|
||||||
|
import im.vector.matrix.android.internal.crypto.model.rest.UploadSignatureQueryBuilder
|
||||||
|
import im.vector.matrix.android.internal.crypto.model.rest.UserPasswordAuth
|
||||||
|
import im.vector.matrix.android.internal.di.UserId
|
||||||
|
import im.vector.matrix.android.internal.task.Task
|
||||||
|
import im.vector.matrix.android.internal.util.JsonCanonicalizer
|
||||||
|
import org.matrix.olm.OlmPkSigning
|
||||||
|
import timber.log.Timber
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
internal interface InitializeCrossSigningTask : Task<InitializeCrossSigningTask.Params, InitializeCrossSigningTask.Result> {
|
||||||
|
data class Params(
|
||||||
|
val authParams: UserPasswordAuth?
|
||||||
|
)
|
||||||
|
|
||||||
|
data class Result(
|
||||||
|
val masterKeyPK: String,
|
||||||
|
val userKeyPK: String,
|
||||||
|
val selfSigningKeyPK: String,
|
||||||
|
val masterKeyInfo: CryptoCrossSigningKey,
|
||||||
|
val userKeyInfo: CryptoCrossSigningKey,
|
||||||
|
val selfSignedKeyInfo: CryptoCrossSigningKey
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class DefaultInitializeCrossSigningTask @Inject constructor(
|
||||||
|
@UserId private val userId: String,
|
||||||
|
private val olmDevice: MXOlmDevice,
|
||||||
|
private val myDeviceInfoHolder: Lazy<MyDeviceInfoHolder>,
|
||||||
|
private val uploadSigningKeysTask: UploadSigningKeysTask,
|
||||||
|
private val uploadSignaturesTask: UploadSignaturesTask
|
||||||
|
) : InitializeCrossSigningTask {
|
||||||
|
|
||||||
|
override suspend fun execute(params: InitializeCrossSigningTask.Params): InitializeCrossSigningTask.Result {
|
||||||
|
var masterPkOlm: OlmPkSigning? = null
|
||||||
|
var userSigningPkOlm: OlmPkSigning? = null
|
||||||
|
var selfSigningPkOlm: OlmPkSigning? = null
|
||||||
|
|
||||||
|
try {
|
||||||
|
// =================
|
||||||
|
// MASTER KEY
|
||||||
|
// =================
|
||||||
|
|
||||||
|
masterPkOlm = OlmPkSigning()
|
||||||
|
val masterKeyPrivateKey = OlmPkSigning.generateSeed()
|
||||||
|
val masterPublicKey = masterPkOlm.initWithSeed(masterKeyPrivateKey)
|
||||||
|
|
||||||
|
Timber.v("## CrossSigning - masterPublicKey:$masterPublicKey")
|
||||||
|
|
||||||
|
// =================
|
||||||
|
// USER KEY
|
||||||
|
// =================
|
||||||
|
userSigningPkOlm = OlmPkSigning()
|
||||||
|
val uskPrivateKey = OlmPkSigning.generateSeed()
|
||||||
|
val uskPublicKey = userSigningPkOlm.initWithSeed(uskPrivateKey)
|
||||||
|
|
||||||
|
Timber.v("## CrossSigning - uskPublicKey:$uskPublicKey")
|
||||||
|
|
||||||
|
// Sign userSigningKey with master
|
||||||
|
val signedUSK = CryptoCrossSigningKey.Builder(userId, KeyUsage.USER_SIGNING)
|
||||||
|
.key(uskPublicKey)
|
||||||
|
.build()
|
||||||
|
.canonicalSignable()
|
||||||
|
.let { masterPkOlm.sign(it) }
|
||||||
|
|
||||||
|
// =================
|
||||||
|
// SELF SIGNING KEY
|
||||||
|
// =================
|
||||||
|
selfSigningPkOlm = OlmPkSigning()
|
||||||
|
val sskPrivateKey = OlmPkSigning.generateSeed()
|
||||||
|
val sskPublicKey = selfSigningPkOlm.initWithSeed(sskPrivateKey)
|
||||||
|
|
||||||
|
Timber.v("## CrossSigning - sskPublicKey:$sskPublicKey")
|
||||||
|
|
||||||
|
// Sign userSigningKey with master
|
||||||
|
val signedSSK = CryptoCrossSigningKey.Builder(userId, KeyUsage.SELF_SIGNING)
|
||||||
|
.key(sskPublicKey)
|
||||||
|
.build()
|
||||||
|
.canonicalSignable()
|
||||||
|
.let { masterPkOlm.sign(it) }
|
||||||
|
|
||||||
|
// I need to upload the keys
|
||||||
|
val mskCrossSigningKeyInfo = CryptoCrossSigningKey.Builder(userId, KeyUsage.MASTER)
|
||||||
|
.key(masterPublicKey)
|
||||||
|
.build()
|
||||||
|
val uploadSigningKeysParams = UploadSigningKeysTask.Params(
|
||||||
|
masterKey = mskCrossSigningKeyInfo,
|
||||||
|
userKey = CryptoCrossSigningKey.Builder(userId, KeyUsage.USER_SIGNING)
|
||||||
|
.key(uskPublicKey)
|
||||||
|
.signature(userId, masterPublicKey, signedUSK)
|
||||||
|
.build(),
|
||||||
|
selfSignedKey = CryptoCrossSigningKey.Builder(userId, KeyUsage.SELF_SIGNING)
|
||||||
|
.key(sskPublicKey)
|
||||||
|
.signature(userId, masterPublicKey, signedSSK)
|
||||||
|
.build(),
|
||||||
|
userPasswordAuth = params.authParams
|
||||||
|
)
|
||||||
|
|
||||||
|
uploadSigningKeysTask.execute(uploadSigningKeysParams)
|
||||||
|
|
||||||
|
// Sign the current device with SSK
|
||||||
|
val uploadSignatureQueryBuilder = UploadSignatureQueryBuilder()
|
||||||
|
|
||||||
|
val myDevice = myDeviceInfoHolder.get().myDevice
|
||||||
|
val canonicalJson = JsonCanonicalizer.getCanonicalJson(Map::class.java, myDevice.signalableJSONDictionary())
|
||||||
|
val signedDevice = selfSigningPkOlm.sign(canonicalJson)
|
||||||
|
val updateSignatures = (myDevice.signatures?.toMutableMap() ?: HashMap())
|
||||||
|
.also {
|
||||||
|
it[userId] = (it[userId]
|
||||||
|
?: HashMap()) + mapOf("ed25519:$sskPublicKey" to signedDevice)
|
||||||
|
}
|
||||||
|
myDevice.copy(signatures = updateSignatures).let {
|
||||||
|
uploadSignatureQueryBuilder.withDeviceInfo(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
// sign MSK with device key (migration) and upload signatures
|
||||||
|
val message = JsonCanonicalizer.getCanonicalJson(Map::class.java, mskCrossSigningKeyInfo.signalableJSONDictionary())
|
||||||
|
olmDevice.signMessage(message)?.let { sign ->
|
||||||
|
val mskUpdatedSignatures = (mskCrossSigningKeyInfo.signatures?.toMutableMap()
|
||||||
|
?: HashMap()).also {
|
||||||
|
it[userId] = (it[userId]
|
||||||
|
?: HashMap()) + mapOf("ed25519:${myDevice.deviceId}" to sign)
|
||||||
|
}
|
||||||
|
mskCrossSigningKeyInfo.copy(
|
||||||
|
signatures = mskUpdatedSignatures
|
||||||
|
).let {
|
||||||
|
uploadSignatureQueryBuilder.withSigningKeyInfo(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO should we ignore failure of that?
|
||||||
|
uploadSignaturesTask.execute(UploadSignaturesTask.Params(uploadSignatureQueryBuilder.build()))
|
||||||
|
|
||||||
|
return InitializeCrossSigningTask.Result(
|
||||||
|
masterKeyPK = masterKeyPrivateKey.toBase64NoPadding(),
|
||||||
|
userKeyPK = uskPrivateKey.toBase64NoPadding(),
|
||||||
|
selfSigningKeyPK = sskPrivateKey.toBase64NoPadding(),
|
||||||
|
masterKeyInfo = uploadSigningKeysParams.masterKey,
|
||||||
|
userKeyInfo = uploadSigningKeysParams.userKey,
|
||||||
|
selfSignedKeyInfo = uploadSigningKeysParams.selfSignedKey
|
||||||
|
)
|
||||||
|
} finally {
|
||||||
|
masterPkOlm?.releaseSigning()
|
||||||
|
userSigningPkOlm?.releaseSigning()
|
||||||
|
selfSigningPkOlm?.releaseSigning()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -35,7 +35,6 @@ import im.vector.matrix.android.internal.di.UserId
|
||||||
import im.vector.matrix.android.internal.task.Task
|
import im.vector.matrix.android.internal.task.Task
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.util.ArrayList
|
import java.util.ArrayList
|
||||||
import java.util.UUID
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
internal interface RoomVerificationUpdateTask : Task<RoomVerificationUpdateTask.Params, Unit> {
|
internal interface RoomVerificationUpdateTask : Task<RoomVerificationUpdateTask.Params, Unit> {
|
||||||
|
@ -60,8 +59,7 @@ internal class DefaultRoomVerificationUpdateTask @Inject constructor(
|
||||||
// TODO ignore initial sync or back pagination?
|
// TODO ignore initial sync or back pagination?
|
||||||
|
|
||||||
params.events.forEach { event ->
|
params.events.forEach { event ->
|
||||||
Timber.d("## SAS Verification live observer: received msgId: ${event.eventId} msgtype: ${event.type} from ${event.senderId}")
|
Timber.v("## SAS Verification live observer: received msgId: ${event.eventId} msgtype: ${event.type} from ${event.senderId}")
|
||||||
Timber.v("## SAS Verification live observer: received msgId: $event")
|
|
||||||
|
|
||||||
// If the request is in the future by more than 5 minutes or more than 10 minutes in the past,
|
// If the request is in the future by more than 5 minutes or more than 10 minutes in the past,
|
||||||
// the message should be ignored by the receiver.
|
// the message should be ignored by the receiver.
|
||||||
|
@ -76,7 +74,7 @@ internal class DefaultRoomVerificationUpdateTask @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, event.roomId + UUID.randomUUID().toString())
|
val result = cryptoService.decryptEvent(event, "")
|
||||||
event.mxDecryptionResult = OlmDecryptionResult(
|
event.mxDecryptionResult = OlmDecryptionResult(
|
||||||
payload = result.clearEvent,
|
payload = result.clearEvent,
|
||||||
senderKey = result.senderCurve25519Key,
|
senderKey = result.senderCurve25519Key,
|
||||||
|
@ -85,6 +83,7 @@ internal class DefaultRoomVerificationUpdateTask @Inject constructor(
|
||||||
)
|
)
|
||||||
} catch (e: MXCryptoError) {
|
} catch (e: MXCryptoError) {
|
||||||
Timber.e("## SAS Failed to decrypt event: ${event.eventId}")
|
Timber.e("## SAS Failed to decrypt event: ${event.eventId}")
|
||||||
|
params.verificationService.onPotentiallyInterestingEventRoomFailToDecrypt(event)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Timber.v("## SAS Verification live observer: received msgId: ${event.eventId} type: ${event.getClearType()}")
|
Timber.v("## SAS Verification live observer: received msgId: ${event.eventId} type: ${event.getClearType()}")
|
||||||
|
|
|
@ -25,6 +25,7 @@ import im.vector.matrix.android.internal.network.executeRequest
|
||||||
import im.vector.matrix.android.internal.task.Task
|
import im.vector.matrix.android.internal.task.Task
|
||||||
import im.vector.matrix.android.internal.util.convertToUTF8
|
import im.vector.matrix.android.internal.util.convertToUTF8
|
||||||
import org.greenrobot.eventbus.EventBus
|
import org.greenrobot.eventbus.EventBus
|
||||||
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
internal interface UploadKeysTask : Task<UploadKeysTask.Params, KeysUploadResponse> {
|
internal interface UploadKeysTask : Task<UploadKeysTask.Params, KeysUploadResponse> {
|
||||||
|
@ -50,6 +51,8 @@ internal class DefaultUploadKeysTask @Inject constructor(
|
||||||
oneTimeKeys = params.oneTimeKeys
|
oneTimeKeys = params.oneTimeKeys
|
||||||
)
|
)
|
||||||
|
|
||||||
|
Timber.i("## Uploading device keys -> $body")
|
||||||
|
|
||||||
return executeRequest(eventBus) {
|
return executeRequest(eventBus) {
|
||||||
apiCall = if (encodedDeviceId.isBlank()) {
|
apiCall = if (encodedDeviceId.isBlank()) {
|
||||||
cryptoApi.uploadKeys(body)
|
cryptoApi.uploadKeys(body)
|
||||||
|
|
|
@ -18,6 +18,7 @@ package im.vector.matrix.android.internal.crypto.tools
|
||||||
|
|
||||||
import org.matrix.olm.OlmPkDecryption
|
import org.matrix.olm.OlmPkDecryption
|
||||||
import org.matrix.olm.OlmPkEncryption
|
import org.matrix.olm.OlmPkEncryption
|
||||||
|
import org.matrix.olm.OlmPkSigning
|
||||||
|
|
||||||
fun <T> withOlmEncryption(block: (OlmPkEncryption) -> T): T {
|
fun <T> withOlmEncryption(block: (OlmPkEncryption) -> T): T {
|
||||||
val olmPkEncryption = OlmPkEncryption()
|
val olmPkEncryption = OlmPkEncryption()
|
||||||
|
@ -36,3 +37,12 @@ fun <T> withOlmDecryption(block: (OlmPkDecryption) -> T): T {
|
||||||
olmPkDecryption.releaseDecryption()
|
olmPkDecryption.releaseDecryption()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun <T> withOlmSigning(block: (OlmPkSigning) -> T): T {
|
||||||
|
val olmPkSigning = OlmPkSigning()
|
||||||
|
try {
|
||||||
|
return block(olmPkSigning)
|
||||||
|
} finally {
|
||||||
|
olmPkSigning.releaseSigning()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -23,6 +23,8 @@ import im.vector.matrix.android.api.session.crypto.verification.IncomingSasVerif
|
||||||
import im.vector.matrix.android.api.session.crypto.verification.SasMode
|
import im.vector.matrix.android.api.session.crypto.verification.SasMode
|
||||||
import im.vector.matrix.android.api.session.crypto.verification.VerificationTxState
|
import im.vector.matrix.android.api.session.crypto.verification.VerificationTxState
|
||||||
import im.vector.matrix.android.api.session.events.model.EventType
|
import im.vector.matrix.android.api.session.events.model.EventType
|
||||||
|
import im.vector.matrix.android.internal.crypto.IncomingGossipingRequestManager
|
||||||
|
import im.vector.matrix.android.internal.crypto.OutgoingGossipingRequestManager
|
||||||
import im.vector.matrix.android.internal.crypto.actions.SetDeviceVerificationAction
|
import im.vector.matrix.android.internal.crypto.actions.SetDeviceVerificationAction
|
||||||
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
|
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
@ -33,6 +35,8 @@ internal class DefaultIncomingSASDefaultVerificationTransaction(
|
||||||
override val deviceId: String?,
|
override val deviceId: String?,
|
||||||
private val cryptoStore: IMXCryptoStore,
|
private val cryptoStore: IMXCryptoStore,
|
||||||
crossSigningService: CrossSigningService,
|
crossSigningService: CrossSigningService,
|
||||||
|
outgoingGossipingRequestManager: OutgoingGossipingRequestManager,
|
||||||
|
incomingGossipingRequestManager: IncomingGossipingRequestManager,
|
||||||
deviceFingerprint: String,
|
deviceFingerprint: String,
|
||||||
transactionId: String,
|
transactionId: String,
|
||||||
otherUserID: String,
|
otherUserID: String,
|
||||||
|
@ -43,6 +47,8 @@ internal class DefaultIncomingSASDefaultVerificationTransaction(
|
||||||
deviceId,
|
deviceId,
|
||||||
cryptoStore,
|
cryptoStore,
|
||||||
crossSigningService,
|
crossSigningService,
|
||||||
|
outgoingGossipingRequestManager,
|
||||||
|
incomingGossipingRequestManager,
|
||||||
deviceFingerprint,
|
deviceFingerprint,
|
||||||
transactionId,
|
transactionId,
|
||||||
otherUserID,
|
otherUserID,
|
||||||
|
|
|
@ -20,6 +20,8 @@ import im.vector.matrix.android.api.session.crypto.verification.CancelCode
|
||||||
import im.vector.matrix.android.api.session.crypto.verification.OutgoingSasVerificationTransaction
|
import im.vector.matrix.android.api.session.crypto.verification.OutgoingSasVerificationTransaction
|
||||||
import im.vector.matrix.android.api.session.crypto.verification.VerificationTxState
|
import im.vector.matrix.android.api.session.crypto.verification.VerificationTxState
|
||||||
import im.vector.matrix.android.api.session.events.model.EventType
|
import im.vector.matrix.android.api.session.events.model.EventType
|
||||||
|
import im.vector.matrix.android.internal.crypto.IncomingGossipingRequestManager
|
||||||
|
import im.vector.matrix.android.internal.crypto.OutgoingGossipingRequestManager
|
||||||
import im.vector.matrix.android.internal.crypto.actions.SetDeviceVerificationAction
|
import im.vector.matrix.android.internal.crypto.actions.SetDeviceVerificationAction
|
||||||
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
|
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
@ -30,6 +32,8 @@ internal class DefaultOutgoingSASDefaultVerificationTransaction(
|
||||||
deviceId: String?,
|
deviceId: String?,
|
||||||
cryptoStore: IMXCryptoStore,
|
cryptoStore: IMXCryptoStore,
|
||||||
crossSigningService: CrossSigningService,
|
crossSigningService: CrossSigningService,
|
||||||
|
outgoingGossipingRequestManager: OutgoingGossipingRequestManager,
|
||||||
|
incomingGossipingRequestManager: IncomingGossipingRequestManager,
|
||||||
deviceFingerprint: String,
|
deviceFingerprint: String,
|
||||||
transactionId: String,
|
transactionId: String,
|
||||||
otherUserId: String,
|
otherUserId: String,
|
||||||
|
@ -40,6 +44,8 @@ internal class DefaultOutgoingSASDefaultVerificationTransaction(
|
||||||
deviceId,
|
deviceId,
|
||||||
cryptoStore,
|
cryptoStore,
|
||||||
crossSigningService,
|
crossSigningService,
|
||||||
|
outgoingGossipingRequestManager,
|
||||||
|
incomingGossipingRequestManager,
|
||||||
deviceFingerprint,
|
deviceFingerprint,
|
||||||
transactionId,
|
transactionId,
|
||||||
otherUserId,
|
otherUserId,
|
||||||
|
|
|
@ -22,6 +22,9 @@ import dagger.Lazy
|
||||||
import im.vector.matrix.android.api.MatrixCallback
|
import im.vector.matrix.android.api.MatrixCallback
|
||||||
import im.vector.matrix.android.api.session.crypto.CryptoService
|
import im.vector.matrix.android.api.session.crypto.CryptoService
|
||||||
import im.vector.matrix.android.api.session.crypto.crosssigning.CrossSigningService
|
import im.vector.matrix.android.api.session.crypto.crosssigning.CrossSigningService
|
||||||
|
import im.vector.matrix.android.api.session.crypto.crosssigning.KEYBACKUP_SECRET_SSSS_NAME
|
||||||
|
import im.vector.matrix.android.api.session.crypto.crosssigning.SELF_SIGNING_KEY_SSSS_NAME
|
||||||
|
import im.vector.matrix.android.api.session.crypto.crosssigning.USER_SIGNING_KEY_SSSS_NAME
|
||||||
import im.vector.matrix.android.api.session.crypto.verification.CancelCode
|
import im.vector.matrix.android.api.session.crypto.verification.CancelCode
|
||||||
import im.vector.matrix.android.api.session.crypto.verification.PendingVerificationRequest
|
import im.vector.matrix.android.api.session.crypto.verification.PendingVerificationRequest
|
||||||
import im.vector.matrix.android.api.session.crypto.verification.QrCodeVerificationTransaction
|
import im.vector.matrix.android.api.session.crypto.verification.QrCodeVerificationTransaction
|
||||||
|
@ -35,6 +38,7 @@ import im.vector.matrix.android.api.session.crypto.verification.safeValueOf
|
||||||
import im.vector.matrix.android.api.session.events.model.Event
|
import im.vector.matrix.android.api.session.events.model.Event
|
||||||
import im.vector.matrix.android.api.session.events.model.EventType
|
import im.vector.matrix.android.api.session.events.model.EventType
|
||||||
import im.vector.matrix.android.api.session.events.model.LocalEcho
|
import im.vector.matrix.android.api.session.events.model.LocalEcho
|
||||||
|
import im.vector.matrix.android.api.session.events.model.RelationType
|
||||||
import im.vector.matrix.android.api.session.events.model.toModel
|
import im.vector.matrix.android.api.session.events.model.toModel
|
||||||
import im.vector.matrix.android.api.session.room.model.message.MessageContent
|
import im.vector.matrix.android.api.session.room.model.message.MessageContent
|
||||||
import im.vector.matrix.android.api.session.room.model.message.MessageRelationContent
|
import im.vector.matrix.android.api.session.room.model.message.MessageRelationContent
|
||||||
|
@ -49,13 +53,17 @@ import im.vector.matrix.android.api.session.room.model.message.MessageVerificati
|
||||||
import im.vector.matrix.android.api.session.room.model.message.MessageVerificationStartContent
|
import im.vector.matrix.android.api.session.room.model.message.MessageVerificationStartContent
|
||||||
import im.vector.matrix.android.api.session.room.model.message.ValidVerificationDone
|
import im.vector.matrix.android.api.session.room.model.message.ValidVerificationDone
|
||||||
import im.vector.matrix.android.internal.crypto.DeviceListManager
|
import im.vector.matrix.android.internal.crypto.DeviceListManager
|
||||||
|
import im.vector.matrix.android.internal.crypto.IncomingGossipingRequestManager
|
||||||
import im.vector.matrix.android.internal.crypto.MyDeviceInfoHolder
|
import im.vector.matrix.android.internal.crypto.MyDeviceInfoHolder
|
||||||
|
import im.vector.matrix.android.internal.crypto.OutgoingGossipingRequestManager
|
||||||
import im.vector.matrix.android.internal.crypto.actions.SetDeviceVerificationAction
|
import im.vector.matrix.android.internal.crypto.actions.SetDeviceVerificationAction
|
||||||
import im.vector.matrix.android.internal.crypto.crosssigning.DeviceTrustLevel
|
import im.vector.matrix.android.internal.crypto.crosssigning.DeviceTrustLevel
|
||||||
import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
|
import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
|
||||||
import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap
|
import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap
|
||||||
|
import im.vector.matrix.android.internal.crypto.model.event.EncryptedEventContent
|
||||||
import im.vector.matrix.android.internal.crypto.model.rest.KeyVerificationAccept
|
import im.vector.matrix.android.internal.crypto.model.rest.KeyVerificationAccept
|
||||||
import im.vector.matrix.android.internal.crypto.model.rest.KeyVerificationCancel
|
import im.vector.matrix.android.internal.crypto.model.rest.KeyVerificationCancel
|
||||||
|
import im.vector.matrix.android.internal.crypto.model.rest.KeyVerificationDone
|
||||||
import im.vector.matrix.android.internal.crypto.model.rest.KeyVerificationKey
|
import im.vector.matrix.android.internal.crypto.model.rest.KeyVerificationKey
|
||||||
import im.vector.matrix.android.internal.crypto.model.rest.KeyVerificationMac
|
import im.vector.matrix.android.internal.crypto.model.rest.KeyVerificationMac
|
||||||
import im.vector.matrix.android.internal.crypto.model.rest.KeyVerificationReady
|
import im.vector.matrix.android.internal.crypto.model.rest.KeyVerificationReady
|
||||||
|
@ -86,6 +94,8 @@ internal class DefaultVerificationService @Inject constructor(
|
||||||
@UserId private val userId: String,
|
@UserId private val userId: String,
|
||||||
@DeviceId private val deviceId: String?,
|
@DeviceId private val deviceId: String?,
|
||||||
private val cryptoStore: IMXCryptoStore,
|
private val cryptoStore: IMXCryptoStore,
|
||||||
|
private val outgoingGossipingRequestManager: OutgoingGossipingRequestManager,
|
||||||
|
private val incomingGossipingRequestManager: IncomingGossipingRequestManager,
|
||||||
private val myDeviceInfoHolder: Lazy<MyDeviceInfoHolder>,
|
private val myDeviceInfoHolder: Lazy<MyDeviceInfoHolder>,
|
||||||
private val deviceListManager: DeviceListManager,
|
private val deviceListManager: DeviceListManager,
|
||||||
private val setDeviceVerificationAction: SetDeviceVerificationAction,
|
private val setDeviceVerificationAction: SetDeviceVerificationAction,
|
||||||
|
@ -103,6 +113,10 @@ internal class DefaultVerificationService @Inject constructor(
|
||||||
// map [sender : [transaction]]
|
// map [sender : [transaction]]
|
||||||
private val txMap = HashMap<String, HashMap<String, DefaultVerificationTransaction>>()
|
private val txMap = HashMap<String, HashMap<String, DefaultVerificationTransaction>>()
|
||||||
|
|
||||||
|
// we need to keep track of finished transaction
|
||||||
|
// It will be used for gossiping (to send request after request is completed and 'done' by other)
|
||||||
|
private val pastTransactions = HashMap<String, HashMap<String, DefaultVerificationTransaction>>()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Map [sender: [PendingVerificationRequest]]
|
* Map [sender: [PendingVerificationRequest]]
|
||||||
* For now we keep all requests (even terminated ones) during the lifetime of the app.
|
* For now we keep all requests (even terminated ones) during the lifetime of the app.
|
||||||
|
@ -131,6 +145,9 @@ internal class DefaultVerificationService @Inject constructor(
|
||||||
EventType.KEY_VERIFICATION_READY -> {
|
EventType.KEY_VERIFICATION_READY -> {
|
||||||
onReadyReceived(event)
|
onReadyReceived(event)
|
||||||
}
|
}
|
||||||
|
EventType.KEY_VERIFICATION_DONE -> {
|
||||||
|
onDoneReceived(event)
|
||||||
|
}
|
||||||
MessageType.MSGTYPE_VERIFICATION_REQUEST -> {
|
MessageType.MSGTYPE_VERIFICATION_REQUEST -> {
|
||||||
onRequestReceived(event)
|
onRequestReceived(event)
|
||||||
}
|
}
|
||||||
|
@ -354,6 +371,27 @@ internal class DefaultVerificationService @Inject constructor(
|
||||||
*/
|
*/
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onPotentiallyInterestingEventRoomFailToDecrypt(event: Event) {
|
||||||
|
// When Should/Can we cancel??
|
||||||
|
val relationContent = event.content.toModel<EncryptedEventContent>()?.relatesTo
|
||||||
|
if (relationContent?.type == RelationType.REFERENCE) {
|
||||||
|
val relatedId = relationContent.eventId ?: return
|
||||||
|
// at least if request was sent by me, I can safely cancel without interfering
|
||||||
|
pendingRequests[event.senderId]?.firstOrNull {
|
||||||
|
it.transactionId == relatedId && !it.isIncoming
|
||||||
|
}?.let { pr ->
|
||||||
|
verificationTransportRoomMessageFactory.createTransport(event.roomId ?: "", null)
|
||||||
|
.cancelTransaction(
|
||||||
|
relatedId,
|
||||||
|
event.senderId ?: "",
|
||||||
|
event.getSenderKey() ?: "",
|
||||||
|
CancelCode.InvalidMessage
|
||||||
|
)
|
||||||
|
updatePendingRequest(pr.copy(cancelConclusion = CancelCode.InvalidMessage))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private suspend fun onRoomStartRequestReceived(event: Event) {
|
private suspend fun onRoomStartRequestReceived(event: Event) {
|
||||||
val startReq = event.getClearContent().toModel<MessageVerificationStartContent>()
|
val startReq = event.getClearContent().toModel<MessageVerificationStartContent>()
|
||||||
?.copy(
|
?.copy(
|
||||||
|
@ -429,10 +467,34 @@ internal class DefaultVerificationService @Inject constructor(
|
||||||
private suspend fun handleStart(otherUserId: String?,
|
private suspend fun handleStart(otherUserId: String?,
|
||||||
startReq: ValidVerificationInfoStart,
|
startReq: ValidVerificationInfoStart,
|
||||||
txConfigure: (DefaultVerificationTransaction) -> Unit): CancelCode? {
|
txConfigure: (DefaultVerificationTransaction) -> Unit): CancelCode? {
|
||||||
Timber.d("## SAS onStartRequestReceived ${startReq.transactionId}")
|
Timber.d("## SAS onStartRequestReceived $startReq")
|
||||||
if (checkKeysAreDownloaded(otherUserId!!, startReq.fromDevice) != null) {
|
if (otherUserId?.let { checkKeysAreDownloaded(it, startReq.fromDevice) } != null) {
|
||||||
val tid = startReq.transactionId
|
val tid = startReq.transactionId
|
||||||
val existing = getExistingTransaction(otherUserId, tid)
|
var existing = getExistingTransaction(otherUserId, tid)
|
||||||
|
|
||||||
|
// After the m.key.verification.ready event is sent, either party can send an
|
||||||
|
// m.key.verification.start event to begin the verification. If both parties
|
||||||
|
// send an m.key.verification.start event, and they both specify the same
|
||||||
|
// verification method, then the event sent by the user whose user ID is the
|
||||||
|
// smallest is used, and the other m.key.verification.start event is ignored.
|
||||||
|
// In the case of a single user verifying two of their devices, the device ID is
|
||||||
|
// compared instead .
|
||||||
|
if (existing is DefaultOutgoingSASDefaultVerificationTransaction) {
|
||||||
|
val readyRequest = getExistingVerificationRequest(otherUserId, tid)
|
||||||
|
if (readyRequest?.isReady == true) {
|
||||||
|
if (isOtherPrioritary(otherUserId, existing.otherDeviceId ?: "")) {
|
||||||
|
Timber.d("## SAS concurrent start isOtherPrioritary, clear")
|
||||||
|
// The other is prioritary!
|
||||||
|
// I should replace my outgoing with an incoming
|
||||||
|
removeTransaction(otherUserId, tid)
|
||||||
|
existing = null
|
||||||
|
} else {
|
||||||
|
Timber.d("## SAS concurrent start i am prioritary, ignore")
|
||||||
|
// i am prioritary, ignore this start event!
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
when (startReq) {
|
when (startReq) {
|
||||||
is ValidVerificationInfoStart.SasVerificationInfoStart -> {
|
is ValidVerificationInfoStart.SasVerificationInfoStart -> {
|
||||||
|
@ -482,6 +544,8 @@ internal class DefaultVerificationService @Inject constructor(
|
||||||
deviceId,
|
deviceId,
|
||||||
cryptoStore,
|
cryptoStore,
|
||||||
crossSigningService,
|
crossSigningService,
|
||||||
|
outgoingGossipingRequestManager,
|
||||||
|
incomingGossipingRequestManager,
|
||||||
myDeviceInfoHolder.get().myDevice.fingerprint()!!,
|
myDeviceInfoHolder.get().myDevice.fingerprint()!!,
|
||||||
startReq.transactionId,
|
startReq.transactionId,
|
||||||
otherUserId,
|
otherUserId,
|
||||||
|
@ -496,7 +560,7 @@ internal class DefaultVerificationService @Inject constructor(
|
||||||
existing.onStartReceived(startReq)
|
existing.onStartReceived(startReq)
|
||||||
return null
|
return null
|
||||||
} else {
|
} else {
|
||||||
Timber.w("## SAS onStartRequestReceived - unexpected message ${startReq.transactionId}")
|
Timber.w("## SAS onStartRequestReceived - unexpected message ${startReq.transactionId} / $existing")
|
||||||
return CancelCode.UnexpectedMessage
|
return CancelCode.UnexpectedMessage
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -506,6 +570,16 @@ internal class DefaultVerificationService @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun isOtherPrioritary(otherUserId: String, otherDeviceId: String): Boolean {
|
||||||
|
if (userId < otherUserId) {
|
||||||
|
return false
|
||||||
|
} else if (userId > otherUserId) {
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
return otherDeviceId < deviceId ?: ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// TODO Refacto: It could just return a boolean
|
// TODO Refacto: It could just return a boolean
|
||||||
private suspend fun checkKeysAreDownloaded(otherUserId: String,
|
private suspend fun checkKeysAreDownloaded(otherUserId: String,
|
||||||
otherDeviceId: String): MXUsersDevicesMap<CryptoDeviceInfo>? {
|
otherDeviceId: String): MXUsersDevicesMap<CryptoDeviceInfo>? {
|
||||||
|
@ -572,9 +646,7 @@ internal class DefaultVerificationService @Inject constructor(
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
if (existingTransaction is SASDefaultVerificationTransaction) {
|
existingTransaction?.state = VerificationTxState.Cancelled(safeValueOf(cancelReq.code), false)
|
||||||
existingTransaction.state = VerificationTxState.Cancelled(safeValueOf(cancelReq.code), false)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onRoomAcceptReceived(event: Event) {
|
private fun onRoomAcceptReceived(event: Event) {
|
||||||
|
@ -696,6 +768,7 @@ internal class DefaultVerificationService @Inject constructor(
|
||||||
|
|
||||||
private suspend fun onReadyReceived(event: Event) {
|
private suspend fun onReadyReceived(event: Event) {
|
||||||
val readyReq = event.getClearContent().toModel<KeyVerificationReady>()?.asValidObject()
|
val readyReq = event.getClearContent().toModel<KeyVerificationReady>()?.asValidObject()
|
||||||
|
Timber.v("## SAS onReadyReceived $readyReq")
|
||||||
|
|
||||||
if (readyReq == null || event.senderId == null) {
|
if (readyReq == null || event.senderId == null) {
|
||||||
// ignore
|
// ignore
|
||||||
|
@ -714,6 +787,58 @@ internal class DefaultVerificationService @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun onDoneReceived(event: Event) {
|
||||||
|
Timber.v("## onDoneReceived")
|
||||||
|
val doneReq = event.getClearContent().toModel<KeyVerificationDone>()?.asValidObject()
|
||||||
|
if (doneReq == null || event.senderId == null) {
|
||||||
|
// ignore
|
||||||
|
Timber.e("## SAS Received invalid done request")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
handleDoneReceived(event.senderId, doneReq)
|
||||||
|
|
||||||
|
if (event.senderId == userId) {
|
||||||
|
// We only send gossiping request when the other sent us a done
|
||||||
|
// We can ask without checking too much thinks (like trust), because we will check validity of secret on reception
|
||||||
|
getExistingTransaction(userId, doneReq.transactionId)
|
||||||
|
?: getOldTransaction(userId, doneReq.transactionId)
|
||||||
|
?.let { vt ->
|
||||||
|
val otherDeviceId = vt.otherDeviceId
|
||||||
|
if (!crossSigningService.canCrossSign()) {
|
||||||
|
outgoingGossipingRequestManager.sendSecretShareRequest(SELF_SIGNING_KEY_SSSS_NAME, mapOf(userId to listOf(otherDeviceId
|
||||||
|
?: "*")))
|
||||||
|
outgoingGossipingRequestManager.sendSecretShareRequest(USER_SIGNING_KEY_SSSS_NAME, mapOf(userId to listOf(otherDeviceId
|
||||||
|
?: "*")))
|
||||||
|
}
|
||||||
|
outgoingGossipingRequestManager.sendSecretShareRequest(KEYBACKUP_SECRET_SSSS_NAME, mapOf(userId to listOf(otherDeviceId
|
||||||
|
?: "*")))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleDoneReceived(senderId: String, doneReq: ValidVerificationDone) {
|
||||||
|
Timber.v("## SAS Done receieved $doneReq")
|
||||||
|
val existing = getExistingTransaction(senderId, doneReq.transactionId)
|
||||||
|
if (existing == null) {
|
||||||
|
Timber.e("## SAS Received invalid Done request")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (existing is DefaultQrCodeVerificationTransaction) {
|
||||||
|
existing.onDoneReceived()
|
||||||
|
} else {
|
||||||
|
// SAS do not care for now?
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now transactions are udated, let's also update Requests
|
||||||
|
val existingRequest = getExistingVerificationRequest(senderId)?.find { it.transactionId == doneReq.transactionId }
|
||||||
|
if (existingRequest == null) {
|
||||||
|
Timber.e("## SAS Received Done for unknown request txId:${doneReq.transactionId}")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
updatePendingRequest(existingRequest.copy(isSuccessful = true))
|
||||||
|
}
|
||||||
|
|
||||||
private fun onRoomDoneReceived(event: Event) {
|
private fun onRoomDoneReceived(event: Event) {
|
||||||
val doneReq = event.getClearContent().toModel<MessageVerificationDoneContent>()
|
val doneReq = event.getClearContent().toModel<MessageVerificationDoneContent>()
|
||||||
?.copy(
|
?.copy(
|
||||||
|
@ -776,16 +901,18 @@ internal class DefaultVerificationService @Inject constructor(
|
||||||
if (readyReq.methods.contains(VERIFICATION_METHOD_RECIPROCATE)) {
|
if (readyReq.methods.contains(VERIFICATION_METHOD_RECIPROCATE)) {
|
||||||
// Create the pending transaction
|
// Create the pending transaction
|
||||||
val tx = DefaultQrCodeVerificationTransaction(
|
val tx = DefaultQrCodeVerificationTransaction(
|
||||||
setDeviceVerificationAction,
|
setDeviceVerificationAction = setDeviceVerificationAction,
|
||||||
readyReq.transactionId,
|
transactionId = readyReq.transactionId,
|
||||||
senderId,
|
otherUserId = senderId,
|
||||||
readyReq.fromDevice,
|
otherDeviceId = readyReq.fromDevice,
|
||||||
crossSigningService,
|
crossSigningService = crossSigningService,
|
||||||
cryptoStore,
|
outgoingGossipingRequestManager = outgoingGossipingRequestManager,
|
||||||
qrCodeData,
|
incomingGossipingRequestManager = incomingGossipingRequestManager,
|
||||||
userId,
|
cryptoStore = cryptoStore,
|
||||||
deviceId ?: "",
|
qrCodeData = qrCodeData,
|
||||||
false)
|
userId = userId,
|
||||||
|
deviceId = deviceId ?: "",
|
||||||
|
isIncoming = false)
|
||||||
|
|
||||||
tx.transport = transportCreator.invoke(tx)
|
tx.transport = transportCreator.invoke(tx)
|
||||||
|
|
||||||
|
@ -891,14 +1018,14 @@ internal class DefaultVerificationService @Inject constructor(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleDoneReceived(senderId: String, doneInfo: ValidVerificationDone) {
|
// private fun handleDoneReceived(senderId: String, doneInfo: ValidVerificationDone) {
|
||||||
val existingRequest = getExistingVerificationRequest(senderId)?.find { it.transactionId == doneInfo.transactionId }
|
// val existingRequest = getExistingVerificationRequest(senderId)?.find { it.transactionId == doneInfo.transactionId }
|
||||||
if (existingRequest == null) {
|
// if (existingRequest == null) {
|
||||||
Timber.e("## SAS Received Done for unknown request txId:${doneInfo.transactionId}")
|
// Timber.e("## SAS Received Done for unknown request txId:${doneInfo.transactionId}")
|
||||||
return
|
// return
|
||||||
}
|
// }
|
||||||
updatePendingRequest(existingRequest.copy(isSuccessful = true))
|
// updatePendingRequest(existingRequest.copy(isSuccessful = true))
|
||||||
}
|
// }
|
||||||
|
|
||||||
// TODO All this methods should be delegated to a TransactionStore
|
// TODO All this methods should be delegated to a TransactionStore
|
||||||
override fun getExistingTransaction(otherUserId: String, tid: String): VerificationTransaction? {
|
override fun getExistingTransaction(otherUserId: String, tid: String): VerificationTransaction? {
|
||||||
|
@ -937,17 +1064,33 @@ internal class DefaultVerificationService @Inject constructor(
|
||||||
|
|
||||||
private fun removeTransaction(otherUser: String, tid: String) {
|
private fun removeTransaction(otherUser: String, tid: String) {
|
||||||
synchronized(txMap) {
|
synchronized(txMap) {
|
||||||
txMap[otherUser]?.remove(tid)?.removeListener(this)
|
txMap[otherUser]?.remove(tid)?.also {
|
||||||
|
it.removeListener(this)
|
||||||
|
}
|
||||||
|
}?.let {
|
||||||
|
rememberOldTransaction(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun addTransaction(tx: DefaultVerificationTransaction) {
|
private fun addTransaction(tx: DefaultVerificationTransaction) {
|
||||||
tx.otherUserId.let { otherUserId ->
|
synchronized(txMap) {
|
||||||
synchronized(txMap) {
|
val txInnerMap = txMap.getOrPut(tx.otherUserId) { HashMap() }
|
||||||
val txInnerMap = txMap.getOrPut(otherUserId) { HashMap() }
|
txInnerMap[tx.transactionId] = tx
|
||||||
txInnerMap[tx.transactionId] = tx
|
dispatchTxAdded(tx)
|
||||||
dispatchTxAdded(tx)
|
tx.addListener(this)
|
||||||
tx.addListener(this)
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun rememberOldTransaction(tx: DefaultVerificationTransaction) {
|
||||||
|
synchronized(pastTransactions) {
|
||||||
|
pastTransactions.getOrPut(tx.otherUserId) { HashMap() }[tx.transactionId] = tx
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getOldTransaction(userId: String, tid: String?): DefaultVerificationTransaction? {
|
||||||
|
return tid?.let {
|
||||||
|
synchronized(pastTransactions) {
|
||||||
|
pastTransactions[userId]?.get(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -962,6 +1105,8 @@ internal class DefaultVerificationService @Inject constructor(
|
||||||
deviceId,
|
deviceId,
|
||||||
cryptoStore,
|
cryptoStore,
|
||||||
crossSigningService,
|
crossSigningService,
|
||||||
|
outgoingGossipingRequestManager,
|
||||||
|
incomingGossipingRequestManager,
|
||||||
myDeviceInfoHolder.get().myDevice.fingerprint()!!,
|
myDeviceInfoHolder.get().myDevice.fingerprint()!!,
|
||||||
txID,
|
txID,
|
||||||
otherUserId,
|
otherUserId,
|
||||||
|
@ -1036,6 +1181,18 @@ internal class DefaultVerificationService @Inject constructor(
|
||||||
return verificationRequest
|
return verificationRequest
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun cancelVerificationRequest(request: PendingVerificationRequest) {
|
||||||
|
if (request.roomId != null) {
|
||||||
|
val transport = verificationTransportRoomMessageFactory.createTransport(request.roomId, null)
|
||||||
|
transport.cancelTransaction(request.transactionId ?: "", request.otherUserId, null, CancelCode.User)
|
||||||
|
} else {
|
||||||
|
val transport = verificationTransportToDeviceFactory.createTransport(null)
|
||||||
|
request.targetDevices?.forEach { deviceId ->
|
||||||
|
transport.cancelTransaction(request.transactionId ?: "", request.otherUserId, deviceId, CancelCode.User)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun requestKeyVerification(methods: List<VerificationMethod>, otherUserId: String, otherDevices: List<String>?): PendingVerificationRequest {
|
override fun requestKeyVerification(methods: List<VerificationMethod>, otherUserId: String, otherDevices: List<String>?): PendingVerificationRequest {
|
||||||
// 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")
|
||||||
|
@ -1137,6 +1294,8 @@ internal class DefaultVerificationService @Inject constructor(
|
||||||
deviceId,
|
deviceId,
|
||||||
cryptoStore,
|
cryptoStore,
|
||||||
crossSigningService,
|
crossSigningService,
|
||||||
|
outgoingGossipingRequestManager,
|
||||||
|
incomingGossipingRequestManager,
|
||||||
myDeviceInfoHolder.get().myDevice.fingerprint()!!,
|
myDeviceInfoHolder.get().myDevice.fingerprint()!!,
|
||||||
transactionId,
|
transactionId,
|
||||||
otherUserId,
|
otherUserId,
|
||||||
|
@ -1268,16 +1427,18 @@ internal class DefaultVerificationService @Inject constructor(
|
||||||
if (VERIFICATION_METHOD_RECIPROCATE in result) {
|
if (VERIFICATION_METHOD_RECIPROCATE in result) {
|
||||||
// Create the pending transaction
|
// Create the pending transaction
|
||||||
val tx = DefaultQrCodeVerificationTransaction(
|
val tx = DefaultQrCodeVerificationTransaction(
|
||||||
setDeviceVerificationAction,
|
setDeviceVerificationAction = setDeviceVerificationAction,
|
||||||
transactionId,
|
transactionId = transactionId,
|
||||||
otherUserId,
|
otherUserId = otherUserId,
|
||||||
otherDeviceId,
|
otherDeviceId = otherDeviceId,
|
||||||
crossSigningService,
|
crossSigningService = crossSigningService,
|
||||||
cryptoStore,
|
outgoingGossipingRequestManager = outgoingGossipingRequestManager,
|
||||||
qrCodeData,
|
incomingGossipingRequestManager = incomingGossipingRequestManager,
|
||||||
userId,
|
cryptoStore = cryptoStore,
|
||||||
deviceId ?: "",
|
qrCodeData = qrCodeData,
|
||||||
false)
|
userId = userId,
|
||||||
|
deviceId = deviceId ?: "",
|
||||||
|
isIncoming = false)
|
||||||
|
|
||||||
tx.transport = transportCreator.invoke(tx)
|
tx.transport = transportCreator.invoke(tx)
|
||||||
|
|
||||||
|
|
|
@ -19,6 +19,8 @@ import im.vector.matrix.android.api.MatrixCallback
|
||||||
import im.vector.matrix.android.api.session.crypto.crosssigning.CrossSigningService
|
import im.vector.matrix.android.api.session.crypto.crosssigning.CrossSigningService
|
||||||
import im.vector.matrix.android.api.session.crypto.verification.VerificationTransaction
|
import im.vector.matrix.android.api.session.crypto.verification.VerificationTransaction
|
||||||
import im.vector.matrix.android.api.session.crypto.verification.VerificationTxState
|
import im.vector.matrix.android.api.session.crypto.verification.VerificationTxState
|
||||||
|
import im.vector.matrix.android.internal.crypto.IncomingGossipingRequestManager
|
||||||
|
import im.vector.matrix.android.internal.crypto.OutgoingGossipingRequestManager
|
||||||
import im.vector.matrix.android.internal.crypto.actions.SetDeviceVerificationAction
|
import im.vector.matrix.android.internal.crypto.actions.SetDeviceVerificationAction
|
||||||
import im.vector.matrix.android.internal.crypto.crosssigning.DeviceTrustLevel
|
import im.vector.matrix.android.internal.crypto.crosssigning.DeviceTrustLevel
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
@ -29,6 +31,8 @@ import timber.log.Timber
|
||||||
internal abstract class DefaultVerificationTransaction(
|
internal abstract class DefaultVerificationTransaction(
|
||||||
private val setDeviceVerificationAction: SetDeviceVerificationAction,
|
private val setDeviceVerificationAction: SetDeviceVerificationAction,
|
||||||
private val crossSigningService: CrossSigningService,
|
private val crossSigningService: CrossSigningService,
|
||||||
|
private val outgoingGossipingRequestManager: OutgoingGossipingRequestManager,
|
||||||
|
private val incomingGossipingRequestManager: IncomingGossipingRequestManager,
|
||||||
private val userId: String,
|
private val userId: String,
|
||||||
override val transactionId: String,
|
override val transactionId: String,
|
||||||
override val otherUserId: String,
|
override val otherUserId: String,
|
||||||
|
@ -53,7 +57,15 @@ internal abstract class DefaultVerificationTransaction(
|
||||||
|
|
||||||
protected fun trust(canTrustOtherUserMasterKey: Boolean,
|
protected fun trust(canTrustOtherUserMasterKey: Boolean,
|
||||||
toVerifyDeviceIds: List<String>,
|
toVerifyDeviceIds: List<String>,
|
||||||
eventuallyMarkMyMasterKeyAsTrusted: Boolean) {
|
eventuallyMarkMyMasterKeyAsTrusted: Boolean, autoDone : Boolean = true) {
|
||||||
|
Timber.d("## Verification: trust ($otherUserId,$otherDeviceId) , verifiedDevices:$toVerifyDeviceIds")
|
||||||
|
Timber.d("## Verification: trust Mark myMSK trusted $eventuallyMarkMyMasterKeyAsTrusted")
|
||||||
|
|
||||||
|
// TODO what if the otherDevice is not in this list? and should we
|
||||||
|
toVerifyDeviceIds.forEach {
|
||||||
|
setDeviceVerified(otherUserId, it)
|
||||||
|
}
|
||||||
|
|
||||||
// If not me sign his MSK and upload the signature
|
// If not me sign his MSK and upload the signature
|
||||||
if (canTrustOtherUserMasterKey) {
|
if (canTrustOtherUserMasterKey) {
|
||||||
// we should trust this master key
|
// we should trust this master key
|
||||||
|
@ -74,6 +86,8 @@ internal abstract class DefaultVerificationTransaction(
|
||||||
}
|
}
|
||||||
|
|
||||||
if (otherUserId == userId) {
|
if (otherUserId == userId) {
|
||||||
|
incomingGossipingRequestManager.onVerificationCompleteForDevice(otherDeviceId!!)
|
||||||
|
|
||||||
// If me it's reasonable to sign and upload the device signature
|
// If me it's reasonable to sign and upload the device signature
|
||||||
// Notice that i might not have the private keys, so may not be able to do it
|
// Notice that i might not have the private keys, so may not be able to do it
|
||||||
crossSigningService.trustDevice(otherDeviceId!!, object : MatrixCallback<Unit> {
|
crossSigningService.trustDevice(otherDeviceId!!, object : MatrixCallback<Unit> {
|
||||||
|
@ -83,12 +97,10 @@ internal abstract class DefaultVerificationTransaction(
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO what if the otherDevice is not in this list? and should we
|
if (autoDone) {
|
||||||
toVerifyDeviceIds.forEach {
|
state = VerificationTxState.Verified
|
||||||
setDeviceVerified(otherUserId, it)
|
transport.done(transactionId) {}
|
||||||
}
|
}
|
||||||
transport.done(transactionId)
|
|
||||||
state = VerificationTxState.Verified
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setDeviceVerified(userId: String, deviceId: String) {
|
private fun setDeviceVerified(userId: String, deviceId: String) {
|
||||||
|
|
|
@ -15,7 +15,6 @@
|
||||||
*/
|
*/
|
||||||
package im.vector.matrix.android.internal.crypto.verification
|
package im.vector.matrix.android.internal.crypto.verification
|
||||||
|
|
||||||
import android.os.Build
|
|
||||||
import im.vector.matrix.android.api.extensions.orFalse
|
import im.vector.matrix.android.api.extensions.orFalse
|
||||||
import im.vector.matrix.android.api.session.crypto.crosssigning.CrossSigningService
|
import im.vector.matrix.android.api.session.crypto.crosssigning.CrossSigningService
|
||||||
import im.vector.matrix.android.api.session.crypto.verification.CancelCode
|
import im.vector.matrix.android.api.session.crypto.verification.CancelCode
|
||||||
|
@ -24,6 +23,8 @@ import im.vector.matrix.android.api.session.crypto.verification.SasMode
|
||||||
import im.vector.matrix.android.api.session.crypto.verification.SasVerificationTransaction
|
import im.vector.matrix.android.api.session.crypto.verification.SasVerificationTransaction
|
||||||
import im.vector.matrix.android.api.session.crypto.verification.VerificationTxState
|
import im.vector.matrix.android.api.session.crypto.verification.VerificationTxState
|
||||||
import im.vector.matrix.android.api.session.events.model.EventType
|
import im.vector.matrix.android.api.session.events.model.EventType
|
||||||
|
import im.vector.matrix.android.internal.crypto.IncomingGossipingRequestManager
|
||||||
|
import im.vector.matrix.android.internal.crypto.OutgoingGossipingRequestManager
|
||||||
import im.vector.matrix.android.internal.crypto.actions.SetDeviceVerificationAction
|
import im.vector.matrix.android.internal.crypto.actions.SetDeviceVerificationAction
|
||||||
import im.vector.matrix.android.internal.crypto.model.MXKey
|
import im.vector.matrix.android.internal.crypto.model.MXKey
|
||||||
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
|
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
|
||||||
|
@ -42,6 +43,8 @@ internal abstract class SASDefaultVerificationTransaction(
|
||||||
open val deviceId: String?,
|
open val deviceId: String?,
|
||||||
private val cryptoStore: IMXCryptoStore,
|
private val cryptoStore: IMXCryptoStore,
|
||||||
crossSigningService: CrossSigningService,
|
crossSigningService: CrossSigningService,
|
||||||
|
outgoingGossipingRequestManager: OutgoingGossipingRequestManager,
|
||||||
|
incomingGossipingRequestManager: IncomingGossipingRequestManager,
|
||||||
private val deviceFingerprint: String,
|
private val deviceFingerprint: String,
|
||||||
transactionId: String,
|
transactionId: String,
|
||||||
otherUserId: String,
|
otherUserId: String,
|
||||||
|
@ -50,6 +53,8 @@ internal abstract class SASDefaultVerificationTransaction(
|
||||||
) : DefaultVerificationTransaction(
|
) : DefaultVerificationTransaction(
|
||||||
setDeviceVerificationAction,
|
setDeviceVerificationAction,
|
||||||
crossSigningService,
|
crossSigningService,
|
||||||
|
outgoingGossipingRequestManager,
|
||||||
|
incomingGossipingRequestManager,
|
||||||
userId,
|
userId,
|
||||||
transactionId,
|
transactionId,
|
||||||
otherUserId,
|
otherUserId,
|
||||||
|
@ -68,13 +73,9 @@ internal abstract class SASDefaultVerificationTransaction(
|
||||||
// ordered by preferred order
|
// ordered by preferred order
|
||||||
val KNOWN_MACS = listOf(SAS_MAC_SHA256, SAS_MAC_SHA256_LONGKDF)
|
val KNOWN_MACS = listOf(SAS_MAC_SHA256, SAS_MAC_SHA256_LONGKDF)
|
||||||
|
|
||||||
// older devices have limited support of emoji, so reply with decimal
|
// older devices have limited support of emoji but SDK offers images for the 64 verification emojis
|
||||||
val KNOWN_SHORT_CODES =
|
// so always send that we support EMOJI
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
val KNOWN_SHORT_CODES = listOf(SasMode.EMOJI, SasMode.DECIMAL)
|
||||||
listOf(SasMode.EMOJI, SasMode.DECIMAL)
|
|
||||||
} else {
|
|
||||||
listOf(SasMode.DECIMAL)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override var state: VerificationTxState = VerificationTxState.None
|
override var state: VerificationTxState = VerificationTxState.None
|
||||||
|
|
|
@ -20,69 +20,69 @@ import im.vector.matrix.android.api.session.crypto.verification.EmojiRepresentat
|
||||||
|
|
||||||
internal fun getEmojiForCode(code: Int): EmojiRepresentation {
|
internal fun getEmojiForCode(code: Int): EmojiRepresentation {
|
||||||
return when (code % 64) {
|
return when (code % 64) {
|
||||||
0 -> EmojiRepresentation("🐶", R.string.verification_emoji_dog)
|
0 -> EmojiRepresentation("🐶", R.string.verification_emoji_dog, R.drawable.ic_verification_dog)
|
||||||
1 -> EmojiRepresentation("🐱", R.string.verification_emoji_cat)
|
1 -> EmojiRepresentation("🐱", R.string.verification_emoji_cat, R.drawable.ic_verification_cat)
|
||||||
2 -> EmojiRepresentation("🦁", R.string.verification_emoji_lion)
|
2 -> EmojiRepresentation("🦁", R.string.verification_emoji_lion, R.drawable.ic_verification_lion)
|
||||||
3 -> EmojiRepresentation("🐎", R.string.verification_emoji_horse)
|
3 -> EmojiRepresentation("🐎", R.string.verification_emoji_horse, R.drawable.ic_verification_horse)
|
||||||
4 -> EmojiRepresentation("🦄", R.string.verification_emoji_unicorn)
|
4 -> EmojiRepresentation("🦄", R.string.verification_emoji_unicorn, R.drawable.ic_verification_unicorn)
|
||||||
5 -> EmojiRepresentation("🐷", R.string.verification_emoji_pig)
|
5 -> EmojiRepresentation("🐷", R.string.verification_emoji_pig, R.drawable.ic_verification_pig)
|
||||||
6 -> EmojiRepresentation("🐘", R.string.verification_emoji_elephant)
|
6 -> EmojiRepresentation("🐘", R.string.verification_emoji_elephant, R.drawable.ic_verification_elephant)
|
||||||
7 -> EmojiRepresentation("🐰", R.string.verification_emoji_rabbit)
|
7 -> EmojiRepresentation("🐰", R.string.verification_emoji_rabbit, R.drawable.ic_verification_rabbit)
|
||||||
8 -> EmojiRepresentation("🐼", R.string.verification_emoji_panda)
|
8 -> EmojiRepresentation("🐼", R.string.verification_emoji_panda, R.drawable.ic_verification_panda)
|
||||||
9 -> EmojiRepresentation("🐓", R.string.verification_emoji_rooster)
|
9 -> EmojiRepresentation("🐓", R.string.verification_emoji_rooster, R.drawable.ic_verification_rooster)
|
||||||
10 -> EmojiRepresentation("🐧", R.string.verification_emoji_penguin)
|
10 -> EmojiRepresentation("🐧", R.string.verification_emoji_penguin, R.drawable.ic_verification_penguin)
|
||||||
11 -> EmojiRepresentation("🐢", R.string.verification_emoji_turtle)
|
11 -> EmojiRepresentation("🐢", R.string.verification_emoji_turtle, R.drawable.ic_verification_turtle)
|
||||||
12 -> EmojiRepresentation("🐟", R.string.verification_emoji_fish)
|
12 -> EmojiRepresentation("🐟", R.string.verification_emoji_fish, R.drawable.ic_verification_fish)
|
||||||
13 -> EmojiRepresentation("🐙", R.string.verification_emoji_octopus)
|
13 -> EmojiRepresentation("🐙", R.string.verification_emoji_octopus, R.drawable.ic_verification_octopus)
|
||||||
14 -> EmojiRepresentation("🦋", R.string.verification_emoji_butterfly)
|
14 -> EmojiRepresentation("🦋", R.string.verification_emoji_butterfly, R.drawable.ic_verification_butterfly)
|
||||||
15 -> EmojiRepresentation("🌷", R.string.verification_emoji_flower)
|
15 -> EmojiRepresentation("🌷", R.string.verification_emoji_flower, R.drawable.ic_verification_flower)
|
||||||
16 -> EmojiRepresentation("🌳", R.string.verification_emoji_tree)
|
16 -> EmojiRepresentation("🌳", R.string.verification_emoji_tree, R.drawable.ic_verification_tree)
|
||||||
17 -> EmojiRepresentation("🌵", R.string.verification_emoji_cactus)
|
17 -> EmojiRepresentation("🌵", R.string.verification_emoji_cactus, R.drawable.ic_verification_cactus)
|
||||||
18 -> EmojiRepresentation("🍄", R.string.verification_emoji_mushroom)
|
18 -> EmojiRepresentation("🍄", R.string.verification_emoji_mushroom, R.drawable.ic_verification_mushroom)
|
||||||
19 -> EmojiRepresentation("🌏", R.string.verification_emoji_globe)
|
19 -> EmojiRepresentation("🌏", R.string.verification_emoji_globe, R.drawable.ic_verification_globe)
|
||||||
20 -> EmojiRepresentation("🌙", R.string.verification_emoji_moon)
|
20 -> EmojiRepresentation("🌙", R.string.verification_emoji_moon, R.drawable.ic_verification_moon)
|
||||||
21 -> EmojiRepresentation("☁️", R.string.verification_emoji_cloud)
|
21 -> EmojiRepresentation("☁️", R.string.verification_emoji_cloud, R.drawable.ic_verification_cloud)
|
||||||
22 -> EmojiRepresentation("🔥", R.string.verification_emoji_fire)
|
22 -> EmojiRepresentation("🔥", R.string.verification_emoji_fire, R.drawable.ic_verification_fire)
|
||||||
23 -> EmojiRepresentation("🍌", R.string.verification_emoji_banana)
|
23 -> EmojiRepresentation("🍌", R.string.verification_emoji_banana, R.drawable.ic_verification_banana)
|
||||||
24 -> EmojiRepresentation("🍎", R.string.verification_emoji_apple)
|
24 -> EmojiRepresentation("🍎", R.string.verification_emoji_apple, R.drawable.ic_verification_apple)
|
||||||
25 -> EmojiRepresentation("🍓", R.string.verification_emoji_strawberry)
|
25 -> EmojiRepresentation("🍓", R.string.verification_emoji_strawberry, R.drawable.ic_verification_strawberry)
|
||||||
26 -> EmojiRepresentation("🌽", R.string.verification_emoji_corn)
|
26 -> EmojiRepresentation("🌽", R.string.verification_emoji_corn, R.drawable.ic_verification_corn)
|
||||||
27 -> EmojiRepresentation("🍕", R.string.verification_emoji_pizza)
|
27 -> EmojiRepresentation("🍕", R.string.verification_emoji_pizza, R.drawable.ic_verification_pizza)
|
||||||
28 -> EmojiRepresentation("🎂", R.string.verification_emoji_cake)
|
28 -> EmojiRepresentation("🎂", R.string.verification_emoji_cake, R.drawable.ic_verification_cake)
|
||||||
29 -> EmojiRepresentation("❤️", R.string.verification_emoji_heart)
|
29 -> EmojiRepresentation("❤️", R.string.verification_emoji_heart, R.drawable.ic_verification_heart)
|
||||||
30 -> EmojiRepresentation("😀", R.string.verification_emoji_smiley)
|
30 -> EmojiRepresentation("🙂", R.string.verification_emoji_smiley, R.drawable.ic_verification_smiley)
|
||||||
31 -> EmojiRepresentation("🤖", R.string.verification_emoji_robot)
|
31 -> EmojiRepresentation("🤖", R.string.verification_emoji_robot, R.drawable.ic_verification_robot)
|
||||||
32 -> EmojiRepresentation("🎩", R.string.verification_emoji_hat)
|
32 -> EmojiRepresentation("🎩", R.string.verification_emoji_hat, R.drawable.ic_verification_hat)
|
||||||
33 -> EmojiRepresentation("👓", R.string.verification_emoji_glasses)
|
33 -> EmojiRepresentation("👓", R.string.verification_emoji_glasses, R.drawable.ic_verification_glasses)
|
||||||
34 -> EmojiRepresentation("🔧", R.string.verification_emoji_wrench)
|
34 -> EmojiRepresentation("🔧", R.string.verification_emoji_wrench, R.drawable.ic_verification_wrench)
|
||||||
35 -> EmojiRepresentation("🎅", R.string.verification_emoji_santa)
|
35 -> EmojiRepresentation("🎅", R.string.verification_emoji_santa, R.drawable.ic_verification_santa)
|
||||||
36 -> EmojiRepresentation("👍", R.string.verification_emoji_thumbsup)
|
36 -> EmojiRepresentation("👍", R.string.verification_emoji_thumbsup, R.drawable.ic_verification_thumbs_up)
|
||||||
37 -> EmojiRepresentation("☂️", R.string.verification_emoji_umbrella)
|
37 -> EmojiRepresentation("☂️", R.string.verification_emoji_umbrella, R.drawable.ic_verification_umbrella)
|
||||||
38 -> EmojiRepresentation("⌛", R.string.verification_emoji_hourglass)
|
38 -> EmojiRepresentation("⌛", R.string.verification_emoji_hourglass, R.drawable.ic_verification_hourglass)
|
||||||
39 -> EmojiRepresentation("⏰", R.string.verification_emoji_clock)
|
39 -> EmojiRepresentation("⏰", R.string.verification_emoji_clock, R.drawable.ic_verification_clock)
|
||||||
40 -> EmojiRepresentation("🎁", R.string.verification_emoji_gift)
|
40 -> EmojiRepresentation("🎁", R.string.verification_emoji_gift, R.drawable.ic_verification_gift)
|
||||||
41 -> EmojiRepresentation("💡", R.string.verification_emoji_lightbulb)
|
41 -> EmojiRepresentation("💡", R.string.verification_emoji_lightbulb, R.drawable.ic_verification_light_bulb)
|
||||||
42 -> EmojiRepresentation("📕", R.string.verification_emoji_book)
|
42 -> EmojiRepresentation("📕", R.string.verification_emoji_book, R.drawable.ic_verification_book)
|
||||||
43 -> EmojiRepresentation("✏️", R.string.verification_emoji_pencil)
|
43 -> EmojiRepresentation("✏️", R.string.verification_emoji_pencil, R.drawable.ic_verification_pencil)
|
||||||
44 -> EmojiRepresentation("📎", R.string.verification_emoji_paperclip)
|
44 -> EmojiRepresentation("📎", R.string.verification_emoji_paperclip, R.drawable.ic_verification_paperclip)
|
||||||
45 -> EmojiRepresentation("✂️", R.string.verification_emoji_scissors)
|
45 -> EmojiRepresentation("✂️", R.string.verification_emoji_scissors, R.drawable.ic_verification_scissors)
|
||||||
46 -> EmojiRepresentation("🔒", R.string.verification_emoji_lock)
|
46 -> EmojiRepresentation("🔒", R.string.verification_emoji_lock, R.drawable.ic_verification_lock)
|
||||||
47 -> EmojiRepresentation("🔑", R.string.verification_emoji_key)
|
47 -> EmojiRepresentation("🔑", R.string.verification_emoji_key, R.drawable.ic_verification_key)
|
||||||
48 -> EmojiRepresentation("🔨", R.string.verification_emoji_hammer)
|
48 -> EmojiRepresentation("🔨", R.string.verification_emoji_hammer, R.drawable.ic_verification_hammer)
|
||||||
49 -> EmojiRepresentation("☎️", R.string.verification_emoji_telephone)
|
49 -> EmojiRepresentation("☎️", R.string.verification_emoji_telephone, R.drawable.ic_verification_phone)
|
||||||
50 -> EmojiRepresentation("🏁", R.string.verification_emoji_flag)
|
50 -> EmojiRepresentation("🏁", R.string.verification_emoji_flag, R.drawable.ic_verification_flag)
|
||||||
51 -> EmojiRepresentation("🚂", R.string.verification_emoji_train)
|
51 -> EmojiRepresentation("🚂", R.string.verification_emoji_train, R.drawable.ic_verification_train)
|
||||||
52 -> EmojiRepresentation("🚲", R.string.verification_emoji_bicycle)
|
52 -> EmojiRepresentation("🚲", R.string.verification_emoji_bicycle, R.drawable.ic_verification_bicycle)
|
||||||
53 -> EmojiRepresentation("✈️", R.string.verification_emoji_airplane)
|
53 -> EmojiRepresentation("✈️", R.string.verification_emoji_airplane, R.drawable.ic_verification_airplane)
|
||||||
54 -> EmojiRepresentation("🚀", R.string.verification_emoji_rocket)
|
54 -> EmojiRepresentation("🚀", R.string.verification_emoji_rocket, R.drawable.ic_verification_rocket)
|
||||||
55 -> EmojiRepresentation("🏆", R.string.verification_emoji_trophy)
|
55 -> EmojiRepresentation("🏆", R.string.verification_emoji_trophy, R.drawable.ic_verification_trophy)
|
||||||
56 -> EmojiRepresentation("⚽", R.string.verification_emoji_ball)
|
56 -> EmojiRepresentation("⚽", R.string.verification_emoji_ball, R.drawable.ic_verification_ball)
|
||||||
57 -> EmojiRepresentation("🎸", R.string.verification_emoji_guitar)
|
57 -> EmojiRepresentation("🎸", R.string.verification_emoji_guitar, R.drawable.ic_verification_guitar)
|
||||||
58 -> EmojiRepresentation("🎺", R.string.verification_emoji_trumpet)
|
58 -> EmojiRepresentation("🎺", R.string.verification_emoji_trumpet, R.drawable.ic_verification_trumpet)
|
||||||
59 -> EmojiRepresentation("🔔", R.string.verification_emoji_bell)
|
59 -> EmojiRepresentation("🔔", R.string.verification_emoji_bell, R.drawable.ic_verification_bell)
|
||||||
60 -> EmojiRepresentation("⚓", R.string.verification_emoji_anchor)
|
60 -> EmojiRepresentation("⚓", R.string.verification_emoji_anchor, R.drawable.ic_verification_anchor)
|
||||||
61 -> EmojiRepresentation("🎧", R.string.verification_emoji_headphone)
|
61 -> EmojiRepresentation("🎧", R.string.verification_emoji_headphone, R.drawable.ic_verification_headphone)
|
||||||
62 -> EmojiRepresentation("📁", R.string.verification_emoji_folder)
|
62 -> EmojiRepresentation("📁", R.string.verification_emoji_folder, R.drawable.ic_verification_folder)
|
||||||
/* 63 */ else -> EmojiRepresentation("📌", R.string.verification_emoji_pin)
|
/* 63 */ else -> EmojiRepresentation("📌", R.string.verification_emoji_pin, R.drawable.ic_verification_pin)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,14 +15,12 @@
|
||||||
*/
|
*/
|
||||||
package im.vector.matrix.android.internal.crypto.verification
|
package im.vector.matrix.android.internal.crypto.verification
|
||||||
|
|
||||||
internal interface VerificationInfoDone : VerificationInfo<ValidVerificationInfoDone> {
|
import im.vector.matrix.android.api.session.room.model.message.ValidVerificationDone
|
||||||
|
|
||||||
override fun asValidObject(): ValidVerificationInfoDone? {
|
internal interface VerificationInfoDone : VerificationInfo<ValidVerificationDone> {
|
||||||
if (transactionId.isNullOrEmpty()) {
|
|
||||||
return null
|
override fun asValidObject(): ValidVerificationDone? {
|
||||||
}
|
val validTransactionId = transactionId?.takeIf { it.isNotEmpty() } ?: return null
|
||||||
return ValidVerificationInfoDone
|
return ValidVerificationDone(validTransactionId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal object ValidVerificationInfoDone
|
|
||||||
|
|
|
@ -46,7 +46,8 @@ internal interface VerificationTransport {
|
||||||
otherUserDeviceId: String?,
|
otherUserDeviceId: String?,
|
||||||
code: CancelCode)
|
code: CancelCode)
|
||||||
|
|
||||||
fun done(transactionId: String)
|
fun done(transactionId: String,
|
||||||
|
onDone: (() -> Unit)?)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates an accept message suitable for this transport
|
* Creates an accept message suitable for this transport
|
||||||
|
|
|
@ -22,8 +22,8 @@ import androidx.work.ExistingWorkPolicy
|
||||||
import androidx.work.Operation
|
import androidx.work.Operation
|
||||||
import androidx.work.WorkInfo
|
import androidx.work.WorkInfo
|
||||||
import im.vector.matrix.android.R
|
import im.vector.matrix.android.R
|
||||||
import im.vector.matrix.android.api.session.crypto.verification.ValidVerificationInfoRequest
|
|
||||||
import im.vector.matrix.android.api.session.crypto.verification.CancelCode
|
import im.vector.matrix.android.api.session.crypto.verification.CancelCode
|
||||||
|
import im.vector.matrix.android.api.session.crypto.verification.ValidVerificationInfoRequest
|
||||||
import im.vector.matrix.android.api.session.crypto.verification.VerificationTxState
|
import im.vector.matrix.android.api.session.crypto.verification.VerificationTxState
|
||||||
import im.vector.matrix.android.api.session.events.model.Content
|
import im.vector.matrix.android.api.session.events.model.Content
|
||||||
import im.vector.matrix.android.api.session.events.model.Event
|
import im.vector.matrix.android.api.session.events.model.Event
|
||||||
|
@ -107,7 +107,7 @@ internal class VerificationTransportRoomMessage(
|
||||||
// }, listenerExecutor)
|
// }, listenerExecutor)
|
||||||
|
|
||||||
val workLiveData = workManagerProvider.workManager
|
val workLiveData = workManagerProvider.workManager
|
||||||
.getWorkInfosForUniqueWorkLiveData("${roomId}_VerificationWork")
|
.getWorkInfosForUniqueWorkLiveData(uniqueQueueName())
|
||||||
|
|
||||||
val observer = object : Observer<List<WorkInfo>> {
|
val observer = object : Observer<List<WorkInfo>> {
|
||||||
override fun onChanged(workInfoList: List<WorkInfo>?) {
|
override fun onChanged(workInfoList: List<WorkInfo>?) {
|
||||||
|
@ -228,7 +228,8 @@ internal class VerificationTransportRoomMessage(
|
||||||
enqueueSendWork(workerParams)
|
enqueueSendWork(workerParams)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun done(transactionId: String) {
|
override fun done(transactionId: String,
|
||||||
|
onDone: (() -> Unit)?) {
|
||||||
Timber.d("## SAS sending done for $transactionId")
|
Timber.d("## SAS sending done for $transactionId")
|
||||||
val event = createEventAndLocalEcho(
|
val event = createEventAndLocalEcho(
|
||||||
type = EventType.KEY_VERIFICATION_DONE,
|
type = EventType.KEY_VERIFICATION_DONE,
|
||||||
|
@ -244,7 +245,26 @@ internal class VerificationTransportRoomMessage(
|
||||||
sessionId = sessionId,
|
sessionId = sessionId,
|
||||||
event = event
|
event = event
|
||||||
))
|
))
|
||||||
enqueueSendWork(workerParams)
|
val enqueueInfo = enqueueSendWork(workerParams)
|
||||||
|
|
||||||
|
val workLiveData = workManagerProvider.workManager
|
||||||
|
.getWorkInfosForUniqueWorkLiveData(uniqueQueueName())
|
||||||
|
val observer = object : Observer<List<WorkInfo>> {
|
||||||
|
override fun onChanged(workInfoList: List<WorkInfo>?) {
|
||||||
|
workInfoList
|
||||||
|
?.filter { it.state == WorkInfo.State.SUCCEEDED }
|
||||||
|
?.firstOrNull { it.id == enqueueInfo.second }
|
||||||
|
?.let { _ ->
|
||||||
|
onDone?.invoke()
|
||||||
|
workLiveData.removeObserver(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO listen to DB to get synced info
|
||||||
|
GlobalScope.launch(Dispatchers.Main) {
|
||||||
|
workLiveData.observeForever(observer)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun enqueueSendWork(workerParams: Data): Pair<Operation, UUID> {
|
private fun enqueueSendWork(workerParams: Data): Pair<Operation, UUID> {
|
||||||
|
@ -254,10 +274,12 @@ internal class VerificationTransportRoomMessage(
|
||||||
.setBackoffCriteria(BackoffPolicy.LINEAR, 2_000L, TimeUnit.MILLISECONDS)
|
.setBackoffCriteria(BackoffPolicy.LINEAR, 2_000L, TimeUnit.MILLISECONDS)
|
||||||
.build()
|
.build()
|
||||||
return workManagerProvider.workManager
|
return workManagerProvider.workManager
|
||||||
.beginUniqueWork("${roomId}_VerificationWork", ExistingWorkPolicy.APPEND, workRequest)
|
.beginUniqueWork(uniqueQueueName(), ExistingWorkPolicy.APPEND, workRequest)
|
||||||
.enqueue() to workRequest.id
|
.enqueue() to workRequest.id
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun uniqueQueueName() = "${roomId}_VerificationWork"
|
||||||
|
|
||||||
override fun createAccept(tid: String,
|
override fun createAccept(tid: String,
|
||||||
keyAgreementProtocol: String,
|
keyAgreementProtocol: String,
|
||||||
hash: String,
|
hash: String,
|
||||||
|
|
|
@ -145,7 +145,7 @@ internal class VerificationTransportToDevice(
|
||||||
.executeBy(taskExecutor)
|
.executeBy(taskExecutor)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun done(transactionId: String) {
|
override fun done(transactionId: String, onDone: (() -> Unit)?) {
|
||||||
val otherUserId = tx?.otherUserId ?: return
|
val otherUserId = tx?.otherUserId ?: return
|
||||||
val otherUserDeviceId = tx?.otherDeviceId ?: return
|
val otherUserDeviceId = tx?.otherDeviceId ?: return
|
||||||
val cancelMessage = KeyVerificationDone(transactionId)
|
val cancelMessage = KeyVerificationDone(transactionId)
|
||||||
|
@ -155,6 +155,7 @@ internal class VerificationTransportToDevice(
|
||||||
.configureWith(SendToDeviceTask.Params(EventType.KEY_VERIFICATION_DONE, contentMap, transactionId)) {
|
.configureWith(SendToDeviceTask.Params(EventType.KEY_VERIFICATION_DONE, contentMap, transactionId)) {
|
||||||
this.callback = object : MatrixCallback<Unit> {
|
this.callback = object : MatrixCallback<Unit> {
|
||||||
override fun onSuccess(data: Unit) {
|
override fun onSuccess(data: Unit) {
|
||||||
|
onDone?.invoke()
|
||||||
Timber.v("## SAS verification [$transactionId] done")
|
Timber.v("## SAS verification [$transactionId] done")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -21,6 +21,8 @@ import im.vector.matrix.android.api.session.crypto.verification.CancelCode
|
||||||
import im.vector.matrix.android.api.session.crypto.verification.QrCodeVerificationTransaction
|
import im.vector.matrix.android.api.session.crypto.verification.QrCodeVerificationTransaction
|
||||||
import im.vector.matrix.android.api.session.crypto.verification.VerificationTxState
|
import im.vector.matrix.android.api.session.crypto.verification.VerificationTxState
|
||||||
import im.vector.matrix.android.api.session.events.model.EventType
|
import im.vector.matrix.android.api.session.events.model.EventType
|
||||||
|
import im.vector.matrix.android.internal.crypto.IncomingGossipingRequestManager
|
||||||
|
import im.vector.matrix.android.internal.crypto.OutgoingGossipingRequestManager
|
||||||
import im.vector.matrix.android.internal.crypto.actions.SetDeviceVerificationAction
|
import im.vector.matrix.android.internal.crypto.actions.SetDeviceVerificationAction
|
||||||
import im.vector.matrix.android.internal.crypto.crosssigning.fromBase64
|
import im.vector.matrix.android.internal.crypto.crosssigning.fromBase64
|
||||||
import im.vector.matrix.android.internal.crypto.crosssigning.fromBase64Safe
|
import im.vector.matrix.android.internal.crypto.crosssigning.fromBase64Safe
|
||||||
|
@ -36,6 +38,8 @@ internal class DefaultQrCodeVerificationTransaction(
|
||||||
override val otherUserId: String,
|
override val otherUserId: String,
|
||||||
override var otherDeviceId: String?,
|
override var otherDeviceId: String?,
|
||||||
private val crossSigningService: CrossSigningService,
|
private val crossSigningService: CrossSigningService,
|
||||||
|
outgoingGossipingRequestManager: OutgoingGossipingRequestManager,
|
||||||
|
incomingGossipingRequestManager: IncomingGossipingRequestManager,
|
||||||
private val cryptoStore: IMXCryptoStore,
|
private val cryptoStore: IMXCryptoStore,
|
||||||
// Not null only if other user is able to scan QR code
|
// Not null only if other user is able to scan QR code
|
||||||
private val qrCodeData: QrCodeData?,
|
private val qrCodeData: QrCodeData?,
|
||||||
|
@ -45,6 +49,8 @@ internal class DefaultQrCodeVerificationTransaction(
|
||||||
) : DefaultVerificationTransaction(
|
) : DefaultVerificationTransaction(
|
||||||
setDeviceVerificationAction,
|
setDeviceVerificationAction,
|
||||||
crossSigningService,
|
crossSigningService,
|
||||||
|
outgoingGossipingRequestManager,
|
||||||
|
incomingGossipingRequestManager,
|
||||||
userId,
|
userId,
|
||||||
transactionId,
|
transactionId,
|
||||||
otherUserId,
|
otherUserId,
|
||||||
|
@ -181,19 +187,22 @@ internal class DefaultQrCodeVerificationTransaction(
|
||||||
// qrCodeData.sharedSecret will be used to send the start request
|
// qrCodeData.sharedSecret will be used to send the start request
|
||||||
start(otherQrCodeData.sharedSecret)
|
start(otherQrCodeData.sharedSecret)
|
||||||
|
|
||||||
// Trust the other user
|
trust(
|
||||||
trust(canTrustOtherUserMasterKey,
|
canTrustOtherUserMasterKey = canTrustOtherUserMasterKey,
|
||||||
toVerifyDeviceIds.distinct(),
|
toVerifyDeviceIds = toVerifyDeviceIds.distinct(),
|
||||||
eventuallyMarkMyMasterKeyAsTrusted = true)
|
eventuallyMarkMyMasterKeyAsTrusted = true,
|
||||||
|
autoDone = false
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun start(remoteSecret: String) {
|
private fun start(remoteSecret: String, onDone: (() -> Unit)? = null) {
|
||||||
if (state != VerificationTxState.None) {
|
if (state != VerificationTxState.None) {
|
||||||
Timber.e("## Verification QR: start verification from invalid state")
|
Timber.e("## Verification QR: start verification from invalid state")
|
||||||
// should I cancel??
|
// should I cancel??
|
||||||
throw IllegalStateException("Interactive Key verification already started")
|
throw IllegalStateException("Interactive Key verification already started")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
state = VerificationTxState.Started
|
||||||
val startMessage = transport.createStartForQrCode(
|
val startMessage = transport.createStartForQrCode(
|
||||||
deviceId,
|
deviceId,
|
||||||
transactionId,
|
transactionId,
|
||||||
|
@ -203,9 +212,9 @@ internal class DefaultQrCodeVerificationTransaction(
|
||||||
transport.sendToOther(
|
transport.sendToOther(
|
||||||
EventType.KEY_VERIFICATION_START,
|
EventType.KEY_VERIFICATION_START,
|
||||||
startMessage,
|
startMessage,
|
||||||
VerificationTxState.Started,
|
VerificationTxState.WaitingOtherReciprocateConfirm,
|
||||||
CancelCode.User,
|
CancelCode.User,
|
||||||
null
|
onDone
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -239,6 +248,15 @@ internal class DefaultQrCodeVerificationTransaction(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun onDoneReceived() {
|
||||||
|
if (state != VerificationTxState.WaitingOtherReciprocateConfirm) {
|
||||||
|
cancel(CancelCode.UnexpectedMessage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
state = VerificationTxState.Verified
|
||||||
|
transport.done(transactionId) {}
|
||||||
|
}
|
||||||
|
|
||||||
override fun otherUserScannedMyQrCode() {
|
override fun otherUserScannedMyQrCode() {
|
||||||
when (qrCodeData) {
|
when (qrCodeData) {
|
||||||
is QrCodeData.VerifyingAnotherUser -> {
|
is QrCodeData.VerifyingAnotherUser -> {
|
||||||
|
@ -260,6 +278,6 @@ internal class DefaultQrCodeVerificationTransaction(
|
||||||
override fun otherUserDidNotScannedMyQrCode() {
|
override fun otherUserDidNotScannedMyQrCode() {
|
||||||
// What can I do then?
|
// What can I do then?
|
||||||
// At least remove the transaction...
|
// At least remove the transaction...
|
||||||
state = VerificationTxState.Cancelled(CancelCode.MismatchedKeys, true)
|
cancel(CancelCode.MismatchedKeys)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,3 +29,7 @@ annotation class SessionCacheDirectory
|
||||||
@Qualifier
|
@Qualifier
|
||||||
@Retention(AnnotationRetention.RUNTIME)
|
@Retention(AnnotationRetention.RUNTIME)
|
||||||
annotation class CacheDirectory
|
annotation class CacheDirectory
|
||||||
|
|
||||||
|
@Qualifier
|
||||||
|
@Retention(AnnotationRetention.RUNTIME)
|
||||||
|
annotation class ExternalFilesDirectory
|
||||||
|
|
|
@ -56,6 +56,9 @@ internal interface MatrixComponent {
|
||||||
@CacheDirectory
|
@CacheDirectory
|
||||||
fun cacheDir(): File
|
fun cacheDir(): File
|
||||||
|
|
||||||
|
@ExternalFilesDirectory
|
||||||
|
fun externalFilesDir(): File?
|
||||||
|
|
||||||
fun olmManager(): OlmManager
|
fun olmManager(): OlmManager
|
||||||
|
|
||||||
fun taskExecutor(): TaskExecutor
|
fun taskExecutor(): TaskExecutor
|
||||||
|
|
|
@ -57,6 +57,13 @@ internal object MatrixModule {
|
||||||
return context.cacheDir
|
return context.cacheDir
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
@Provides
|
||||||
|
@ExternalFilesDirectory
|
||||||
|
fun providesExternalFilesDir(context: Context): File? {
|
||||||
|
return context.getExternalFilesDir(null)
|
||||||
|
}
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
@Provides
|
@Provides
|
||||||
@MatrixScope
|
@MatrixScope
|
||||||
|
|
|
@ -14,6 +14,9 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
// This BroadcastReceiver is used only if the build code is below 24.
|
||||||
|
@file:Suppress("DEPRECATION")
|
||||||
|
|
||||||
package im.vector.matrix.android.internal.network
|
package im.vector.matrix.android.internal.network
|
||||||
|
|
||||||
import android.content.BroadcastReceiver
|
import android.content.BroadcastReceiver
|
||||||
|
|
|
@ -16,7 +16,6 @@
|
||||||
|
|
||||||
package im.vector.matrix.android.internal.session
|
package im.vector.matrix.android.internal.session
|
||||||
|
|
||||||
import android.os.Environment
|
|
||||||
import arrow.core.Try
|
import arrow.core.Try
|
||||||
import im.vector.matrix.android.api.MatrixCallback
|
import im.vector.matrix.android.api.MatrixCallback
|
||||||
import im.vector.matrix.android.api.session.content.ContentUrlResolver
|
import im.vector.matrix.android.api.session.content.ContentUrlResolver
|
||||||
|
@ -25,6 +24,7 @@ import im.vector.matrix.android.api.util.Cancelable
|
||||||
import im.vector.matrix.android.internal.crypto.attachments.ElementToDecrypt
|
import im.vector.matrix.android.internal.crypto.attachments.ElementToDecrypt
|
||||||
import im.vector.matrix.android.internal.crypto.attachments.MXEncryptedAttachments
|
import im.vector.matrix.android.internal.crypto.attachments.MXEncryptedAttachments
|
||||||
import im.vector.matrix.android.internal.di.CacheDirectory
|
import im.vector.matrix.android.internal.di.CacheDirectory
|
||||||
|
import im.vector.matrix.android.internal.di.ExternalFilesDirectory
|
||||||
import im.vector.matrix.android.internal.di.SessionCacheDirectory
|
import im.vector.matrix.android.internal.di.SessionCacheDirectory
|
||||||
import im.vector.matrix.android.internal.di.Unauthenticated
|
import im.vector.matrix.android.internal.di.Unauthenticated
|
||||||
import im.vector.matrix.android.internal.extensions.foldToCallback
|
import im.vector.matrix.android.internal.extensions.foldToCallback
|
||||||
|
@ -44,6 +44,8 @@ import javax.inject.Inject
|
||||||
internal class DefaultFileService @Inject constructor(
|
internal class DefaultFileService @Inject constructor(
|
||||||
@CacheDirectory
|
@CacheDirectory
|
||||||
private val cacheDirectory: File,
|
private val cacheDirectory: File,
|
||||||
|
@ExternalFilesDirectory
|
||||||
|
private val externalFilesDirectory: File?,
|
||||||
@SessionCacheDirectory
|
@SessionCacheDirectory
|
||||||
private val sessionCacheDirectory: File,
|
private val sessionCacheDirectory: File,
|
||||||
private val contentUrlResolver: ContentUrlResolver,
|
private val contentUrlResolver: ContentUrlResolver,
|
||||||
|
@ -77,9 +79,15 @@ internal class DefaultFileService @Inject constructor(
|
||||||
.url(resolvedUrl)
|
.url(resolvedUrl)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
val response = okHttpClient.newCall(request).execute()
|
val response = try {
|
||||||
|
okHttpClient.newCall(request).execute()
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
return@flatMap Try.Failure(e)
|
||||||
|
}
|
||||||
|
|
||||||
var inputStream = response.body?.byteStream()
|
var inputStream = response.body?.byteStream()
|
||||||
Timber.v("Response size ${response.body?.contentLength()} - Stream available: ${inputStream?.available()}")
|
Timber.v("Response size ${response.body?.contentLength()} - Stream available: ${inputStream?.available()}")
|
||||||
|
|
||||||
if (!response.isSuccessful || inputStream == null) {
|
if (!response.isSuccessful || inputStream == null) {
|
||||||
return@flatMap Try.Failure(IOException())
|
return@flatMap Try.Failure(IOException())
|
||||||
}
|
}
|
||||||
|
@ -103,7 +111,7 @@ internal class DefaultFileService @Inject constructor(
|
||||||
private fun copyFile(file: File, downloadMode: FileService.DownloadMode): File {
|
private fun copyFile(file: File, downloadMode: FileService.DownloadMode): File {
|
||||||
return when (downloadMode) {
|
return when (downloadMode) {
|
||||||
FileService.DownloadMode.TO_EXPORT ->
|
FileService.DownloadMode.TO_EXPORT ->
|
||||||
file.copyTo(File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), file.name), true)
|
file.copyTo(File(externalFilesDirectory, file.name), true)
|
||||||
FileService.DownloadMode.FOR_EXTERNAL_SHARE ->
|
FileService.DownloadMode.FOR_EXTERNAL_SHARE ->
|
||||||
file.copyTo(File(File(cacheDirectory, "ext_share"), file.name), true)
|
file.copyTo(File(File(cacheDirectory, "ext_share"), file.name), true)
|
||||||
FileService.DownloadMode.FOR_INTERNAL_USE ->
|
FileService.DownloadMode.FOR_INTERNAL_USE ->
|
||||||
|
|
|
@ -193,6 +193,7 @@ internal class DefaultSession @Inject constructor(
|
||||||
stopAnyBackgroundSync()
|
stopAnyBackgroundSync()
|
||||||
liveEntityObservers.forEach { it.cancelProcess() }
|
liveEntityObservers.forEach { it.cancelProcess() }
|
||||||
cacheService.get().clearCache(callback)
|
cacheService.get().clearCache(callback)
|
||||||
|
workManagerProvider.cancelAllWorks()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Subscribe(threadMode = ThreadMode.MAIN)
|
@Subscribe(threadMode = ThreadMode.MAIN)
|
||||||
|
|
|
@ -20,7 +20,10 @@ import dagger.BindsInstance
|
||||||
import dagger.Component
|
import dagger.Component
|
||||||
import im.vector.matrix.android.api.auth.data.SessionParams
|
import im.vector.matrix.android.api.auth.data.SessionParams
|
||||||
import im.vector.matrix.android.api.session.Session
|
import im.vector.matrix.android.api.session.Session
|
||||||
|
import im.vector.matrix.android.internal.crypto.CancelGossipRequestWorker
|
||||||
import im.vector.matrix.android.internal.crypto.CryptoModule
|
import im.vector.matrix.android.internal.crypto.CryptoModule
|
||||||
|
import im.vector.matrix.android.internal.crypto.SendGossipRequestWorker
|
||||||
|
import im.vector.matrix.android.internal.crypto.SendGossipWorker
|
||||||
import im.vector.matrix.android.internal.crypto.verification.SendVerificationMessageWorker
|
import im.vector.matrix.android.internal.crypto.verification.SendVerificationMessageWorker
|
||||||
import im.vector.matrix.android.internal.di.MatrixComponent
|
import im.vector.matrix.android.internal.di.MatrixComponent
|
||||||
import im.vector.matrix.android.internal.di.SessionAssistedInjectModule
|
import im.vector.matrix.android.internal.di.SessionAssistedInjectModule
|
||||||
|
@ -106,6 +109,12 @@ internal interface SessionComponent {
|
||||||
|
|
||||||
fun inject(worker: SendVerificationMessageWorker)
|
fun inject(worker: SendVerificationMessageWorker)
|
||||||
|
|
||||||
|
fun inject(worker: SendGossipRequestWorker)
|
||||||
|
|
||||||
|
fun inject(worker: CancelGossipRequestWorker)
|
||||||
|
|
||||||
|
fun inject(worker: SendGossipWorker)
|
||||||
|
|
||||||
@Component.Factory
|
@Component.Factory
|
||||||
interface Factory {
|
interface Factory {
|
||||||
fun create(
|
fun create(
|
||||||
|
|
|
@ -53,9 +53,9 @@ internal class FileUploader @Inject constructor(@Authenticated
|
||||||
|
|
||||||
suspend fun uploadByteArray(byteArray: ByteArray,
|
suspend fun uploadByteArray(byteArray: ByteArray,
|
||||||
filename: String?,
|
filename: String?,
|
||||||
mimeType: String,
|
mimeType: String?,
|
||||||
progressListener: ProgressRequestBody.Listener? = null): ContentUploadResponse {
|
progressListener: ProgressRequestBody.Listener? = null): ContentUploadResponse {
|
||||||
val uploadBody = byteArray.toRequestBody(mimeType.toMediaTypeOrNull())
|
val uploadBody = byteArray.toRequestBody(mimeType?.toMediaTypeOrNull())
|
||||||
return upload(uploadBody, filename, progressListener)
|
return upload(uploadBody, filename, progressListener)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -16,12 +16,12 @@
|
||||||
|
|
||||||
package im.vector.matrix.android.internal.session.content
|
package im.vector.matrix.android.internal.session.content
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.media.ThumbnailUtils
|
import android.media.MediaMetadataRetriever
|
||||||
import android.provider.MediaStore
|
|
||||||
import im.vector.matrix.android.api.session.content.ContentAttachmentData
|
import im.vector.matrix.android.api.session.content.ContentAttachmentData
|
||||||
|
import timber.log.Timber
|
||||||
import java.io.ByteArrayOutputStream
|
import java.io.ByteArrayOutputStream
|
||||||
import java.io.File
|
|
||||||
|
|
||||||
internal object ThumbnailExtractor {
|
internal object ThumbnailExtractor {
|
||||||
|
|
||||||
|
@ -33,34 +33,40 @@ internal object ThumbnailExtractor {
|
||||||
val mimeType: String
|
val mimeType: String
|
||||||
)
|
)
|
||||||
|
|
||||||
fun extractThumbnail(attachment: ContentAttachmentData): ThumbnailData? {
|
fun extractThumbnail(context: Context, attachment: ContentAttachmentData): ThumbnailData? {
|
||||||
val file = File(attachment.path)
|
|
||||||
if (!file.exists() || !file.isFile) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
return if (attachment.type == ContentAttachmentData.Type.VIDEO) {
|
return if (attachment.type == ContentAttachmentData.Type.VIDEO) {
|
||||||
extractVideoThumbnail(attachment)
|
extractVideoThumbnail(context, attachment)
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun extractVideoThumbnail(attachment: ContentAttachmentData): ThumbnailData? {
|
private fun extractVideoThumbnail(context: Context, attachment: ContentAttachmentData): ThumbnailData? {
|
||||||
val thumbnail = ThumbnailUtils.createVideoThumbnail(attachment.path, MediaStore.Video.Thumbnails.MINI_KIND) ?: return null
|
var thumbnailData: ThumbnailData? = null
|
||||||
val outputStream = ByteArrayOutputStream()
|
val mediaMetadataRetriever = MediaMetadataRetriever()
|
||||||
thumbnail.compress(Bitmap.CompressFormat.JPEG, 100, outputStream)
|
try {
|
||||||
val thumbnailWidth = thumbnail.width
|
mediaMetadataRetriever.setDataSource(context, attachment.queryUri)
|
||||||
val thumbnailHeight = thumbnail.height
|
val thumbnail = mediaMetadataRetriever.frameAtTime
|
||||||
val thumbnailSize = outputStream.size()
|
|
||||||
val thumbnailData = ThumbnailData(
|
val outputStream = ByteArrayOutputStream()
|
||||||
width = thumbnailWidth,
|
thumbnail.compress(Bitmap.CompressFormat.JPEG, 100, outputStream)
|
||||||
height = thumbnailHeight,
|
val thumbnailWidth = thumbnail.width
|
||||||
size = thumbnailSize.toLong(),
|
val thumbnailHeight = thumbnail.height
|
||||||
bytes = outputStream.toByteArray(),
|
val thumbnailSize = outputStream.size()
|
||||||
mimeType = "image/jpeg"
|
thumbnailData = ThumbnailData(
|
||||||
)
|
width = thumbnailWidth,
|
||||||
thumbnail.recycle()
|
height = thumbnailHeight,
|
||||||
outputStream.reset()
|
size = thumbnailSize.toLong(),
|
||||||
|
bytes = outputStream.toByteArray(),
|
||||||
|
mimeType = "image/jpeg"
|
||||||
|
)
|
||||||
|
thumbnail.recycle()
|
||||||
|
outputStream.reset()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.e(e, "Cannot extract video thumbnail")
|
||||||
|
} finally {
|
||||||
|
mediaMetadataRetriever.release()
|
||||||
|
}
|
||||||
return thumbnailData
|
return thumbnailData
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,12 +17,9 @@
|
||||||
package im.vector.matrix.android.internal.session.content
|
package im.vector.matrix.android.internal.session.content
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.BitmapFactory
|
|
||||||
import androidx.work.CoroutineWorker
|
import androidx.work.CoroutineWorker
|
||||||
import androidx.work.WorkerParameters
|
import androidx.work.WorkerParameters
|
||||||
import com.squareup.moshi.JsonClass
|
import com.squareup.moshi.JsonClass
|
||||||
import id.zelory.compressor.Compressor
|
|
||||||
import id.zelory.compressor.constraint.default
|
|
||||||
import im.vector.matrix.android.api.session.content.ContentAttachmentData
|
import im.vector.matrix.android.api.session.content.ContentAttachmentData
|
||||||
import im.vector.matrix.android.api.session.events.model.Event
|
import im.vector.matrix.android.api.session.events.model.Event
|
||||||
import im.vector.matrix.android.api.session.events.model.toContent
|
import im.vector.matrix.android.api.session.events.model.toContent
|
||||||
|
@ -41,8 +38,6 @@ import im.vector.matrix.android.internal.worker.WorkerParamsFactory
|
||||||
import im.vector.matrix.android.internal.worker.getSessionComponent
|
import im.vector.matrix.android.internal.worker.getSessionComponent
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.io.ByteArrayInputStream
|
import java.io.ByteArrayInputStream
|
||||||
import java.io.File
|
|
||||||
import java.io.FileInputStream
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
private data class NewImageAttributes(
|
private data class NewImageAttributes(
|
||||||
|
@ -77,6 +72,16 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter
|
||||||
return Result.success(inputData)
|
return Result.success(inputData)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Just defensive code to ensure that we never have an uncaught exception that could break the queue
|
||||||
|
return try {
|
||||||
|
internalDoWork(params)
|
||||||
|
} catch (failure: Throwable) {
|
||||||
|
Timber.e(failure)
|
||||||
|
handleFailure(params, failure)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun internalDoWork(params: Params): Result {
|
||||||
val sessionComponent = getSessionComponent(params.sessionId) ?: return Result.success()
|
val sessionComponent = getSessionComponent(params.sessionId) ?: return Result.success()
|
||||||
sessionComponent.inject(this)
|
sessionComponent.inject(this)
|
||||||
|
|
||||||
|
@ -84,8 +89,90 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter
|
||||||
|
|
||||||
var newImageAttributes: NewImageAttributes? = null
|
var newImageAttributes: NewImageAttributes? = null
|
||||||
|
|
||||||
val attachmentFile = try {
|
try {
|
||||||
File(attachment.path)
|
val inputStream = context.contentResolver.openInputStream(attachment.queryUri)
|
||||||
|
?: return Result.success(
|
||||||
|
WorkerParamsFactory.toData(
|
||||||
|
params.copy(
|
||||||
|
lastFailureMessage = "Cannot openInputStream for file: " + attachment.queryUri.toString()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
inputStream.use {
|
||||||
|
var uploadedThumbnailUrl: String? = null
|
||||||
|
var uploadedThumbnailEncryptedFileInfo: EncryptedFileInfo? = null
|
||||||
|
|
||||||
|
ThumbnailExtractor.extractThumbnail(context, params.attachment)?.let { thumbnailData ->
|
||||||
|
val thumbnailProgressListener = object : ProgressRequestBody.Listener {
|
||||||
|
override fun onProgress(current: Long, total: Long) {
|
||||||
|
notifyTracker(params) { contentUploadStateTracker.setProgressThumbnail(it, current, total) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
val contentUploadResponse = if (params.isEncrypted) {
|
||||||
|
Timber.v("Encrypt thumbnail")
|
||||||
|
notifyTracker(params) { contentUploadStateTracker.setEncryptingThumbnail(it) }
|
||||||
|
val encryptionResult = MXEncryptedAttachments.encryptAttachment(ByteArrayInputStream(thumbnailData.bytes), thumbnailData.mimeType)
|
||||||
|
uploadedThumbnailEncryptedFileInfo = encryptionResult.encryptedFileInfo
|
||||||
|
fileUploader.uploadByteArray(encryptionResult.encryptedByteArray,
|
||||||
|
"thumb_${attachment.name}",
|
||||||
|
"application/octet-stream",
|
||||||
|
thumbnailProgressListener)
|
||||||
|
} else {
|
||||||
|
fileUploader.uploadByteArray(thumbnailData.bytes,
|
||||||
|
"thumb_${attachment.name}",
|
||||||
|
thumbnailData.mimeType,
|
||||||
|
thumbnailProgressListener)
|
||||||
|
}
|
||||||
|
|
||||||
|
uploadedThumbnailUrl = contentUploadResponse.contentUri
|
||||||
|
} catch (t: Throwable) {
|
||||||
|
Timber.e(t, "Thumbnail update failed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val progressListener = object : ProgressRequestBody.Listener {
|
||||||
|
override fun onProgress(current: Long, total: Long) {
|
||||||
|
notifyTracker(params) {
|
||||||
|
if (isStopped) {
|
||||||
|
contentUploadStateTracker.setFailure(it, Throwable("Cancelled"))
|
||||||
|
} else {
|
||||||
|
contentUploadStateTracker.setProgress(it, current, total)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var uploadedFileEncryptedFileInfo: EncryptedFileInfo? = null
|
||||||
|
|
||||||
|
return try {
|
||||||
|
val contentUploadResponse = if (params.isEncrypted) {
|
||||||
|
Timber.v("Encrypt file")
|
||||||
|
notifyTracker(params) { contentUploadStateTracker.setEncrypting(it) }
|
||||||
|
|
||||||
|
val encryptionResult = MXEncryptedAttachments.encryptAttachment(inputStream, attachment.getSafeMimeType())
|
||||||
|
uploadedFileEncryptedFileInfo = encryptionResult.encryptedFileInfo
|
||||||
|
|
||||||
|
fileUploader
|
||||||
|
.uploadByteArray(encryptionResult.encryptedByteArray, attachment.name, "application/octet-stream", progressListener)
|
||||||
|
} else {
|
||||||
|
fileUploader
|
||||||
|
.uploadByteArray(inputStream.readBytes(), attachment.name, attachment.getSafeMimeType(), progressListener)
|
||||||
|
}
|
||||||
|
|
||||||
|
handleSuccess(params,
|
||||||
|
contentUploadResponse.contentUri,
|
||||||
|
uploadedFileEncryptedFileInfo,
|
||||||
|
uploadedThumbnailUrl,
|
||||||
|
uploadedThumbnailEncryptedFileInfo,
|
||||||
|
newImageAttributes)
|
||||||
|
} catch (t: Throwable) {
|
||||||
|
Timber.e(t)
|
||||||
|
handleFailure(params, t)
|
||||||
|
}
|
||||||
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.e(e)
|
Timber.e(e)
|
||||||
notifyTracker(params) { contentUploadStateTracker.setFailure(it, e) }
|
notifyTracker(params) { contentUploadStateTracker.setFailure(it, e) }
|
||||||
|
@ -96,109 +183,6 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
|
||||||
.let { originalFile ->
|
|
||||||
if (attachment.type == ContentAttachmentData.Type.IMAGE) {
|
|
||||||
if (params.compressBeforeSending) {
|
|
||||||
Compressor.compress(context, originalFile) {
|
|
||||||
default(
|
|
||||||
width = MAX_IMAGE_SIZE,
|
|
||||||
height = MAX_IMAGE_SIZE
|
|
||||||
)
|
|
||||||
}.also { compressedFile ->
|
|
||||||
// Update the params
|
|
||||||
val options = BitmapFactory.Options().apply { inJustDecodeBounds = true }
|
|
||||||
BitmapFactory.decodeFile(compressedFile.absolutePath, options)
|
|
||||||
val fileSize = compressedFile.length().toInt()
|
|
||||||
|
|
||||||
newImageAttributes = NewImageAttributes(
|
|
||||||
options.outWidth,
|
|
||||||
options.outHeight,
|
|
||||||
fileSize
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// TODO Fix here the image rotation issue
|
|
||||||
originalFile
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Other type
|
|
||||||
originalFile
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var uploadedThumbnailUrl: String? = null
|
|
||||||
var uploadedThumbnailEncryptedFileInfo: EncryptedFileInfo? = null
|
|
||||||
|
|
||||||
ThumbnailExtractor.extractThumbnail(params.attachment)?.let { thumbnailData ->
|
|
||||||
val thumbnailProgressListener = object : ProgressRequestBody.Listener {
|
|
||||||
override fun onProgress(current: Long, total: Long) {
|
|
||||||
notifyTracker(params) { contentUploadStateTracker.setProgressThumbnail(it, current, total) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
val contentUploadResponse = if (params.isEncrypted) {
|
|
||||||
Timber.v("Encrypt thumbnail")
|
|
||||||
notifyTracker(params) { contentUploadStateTracker.setEncryptingThumbnail(it) }
|
|
||||||
val encryptionResult = MXEncryptedAttachments.encryptAttachment(ByteArrayInputStream(thumbnailData.bytes), thumbnailData.mimeType)
|
|
||||||
uploadedThumbnailEncryptedFileInfo = encryptionResult.encryptedFileInfo
|
|
||||||
fileUploader.uploadByteArray(encryptionResult.encryptedByteArray,
|
|
||||||
"thumb_${attachment.name}",
|
|
||||||
"application/octet-stream",
|
|
||||||
thumbnailProgressListener)
|
|
||||||
} else {
|
|
||||||
fileUploader.uploadByteArray(thumbnailData.bytes,
|
|
||||||
"thumb_${attachment.name}",
|
|
||||||
thumbnailData.mimeType,
|
|
||||||
thumbnailProgressListener)
|
|
||||||
}
|
|
||||||
|
|
||||||
uploadedThumbnailUrl = contentUploadResponse.contentUri
|
|
||||||
} catch (t: Throwable) {
|
|
||||||
Timber.e(t)
|
|
||||||
return handleFailure(params, t)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val progressListener = object : ProgressRequestBody.Listener {
|
|
||||||
override fun onProgress(current: Long, total: Long) {
|
|
||||||
notifyTracker(params) {
|
|
||||||
if (isStopped) {
|
|
||||||
contentUploadStateTracker.setFailure(it, Throwable("Cancelled"))
|
|
||||||
} else {
|
|
||||||
contentUploadStateTracker.setProgress(it, current, total)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var uploadedFileEncryptedFileInfo: EncryptedFileInfo? = null
|
|
||||||
|
|
||||||
return try {
|
|
||||||
val contentUploadResponse = if (params.isEncrypted) {
|
|
||||||
Timber.v("Encrypt file")
|
|
||||||
notifyTracker(params) { contentUploadStateTracker.setEncrypting(it) }
|
|
||||||
|
|
||||||
val encryptionResult = MXEncryptedAttachments.encryptAttachment(FileInputStream(attachmentFile), attachment.getSafeMimeType())
|
|
||||||
uploadedFileEncryptedFileInfo = encryptionResult.encryptedFileInfo
|
|
||||||
|
|
||||||
fileUploader
|
|
||||||
.uploadByteArray(encryptionResult.encryptedByteArray, attachment.name, "application/octet-stream", progressListener)
|
|
||||||
} else {
|
|
||||||
fileUploader
|
|
||||||
.uploadFile(attachmentFile, attachment.name, attachment.getSafeMimeType(), progressListener)
|
|
||||||
}
|
|
||||||
|
|
||||||
handleSuccess(params,
|
|
||||||
contentUploadResponse.contentUri,
|
|
||||||
uploadedFileEncryptedFileInfo,
|
|
||||||
uploadedThumbnailUrl,
|
|
||||||
uploadedThumbnailEncryptedFileInfo,
|
|
||||||
newImageAttributes)
|
|
||||||
} catch (t: Throwable) {
|
|
||||||
Timber.e(t)
|
|
||||||
handleFailure(params, t)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -230,7 +230,7 @@ internal class DefaultEventRelationsAggregationTask @Inject constructor(
|
||||||
private fun decryptIfNeeded(event: Event) {
|
private fun decryptIfNeeded(event: Event) {
|
||||||
if (event.mxDecryptionResult == null) {
|
if (event.mxDecryptionResult == null) {
|
||||||
try {
|
try {
|
||||||
val result = cryptoService.decryptEvent(event, event.roomId ?: "")
|
val result = cryptoService.decryptEvent(event, "")
|
||||||
event.mxDecryptionResult = OlmDecryptionResult(
|
event.mxDecryptionResult = OlmDecryptionResult(
|
||||||
payload = result.clearEvent,
|
payload = result.clearEvent,
|
||||||
senderKey = result.senderCurve25519Key,
|
senderKey = result.senderCurve25519Key,
|
||||||
|
@ -238,7 +238,7 @@ internal class DefaultEventRelationsAggregationTask @Inject constructor(
|
||||||
forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain
|
forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain
|
||||||
)
|
)
|
||||||
} catch (e: MXCryptoError) {
|
} catch (e: MXCryptoError) {
|
||||||
Timber.w("Failed to decrypt e2e replace")
|
Timber.v("Failed to decrypt e2e replace")
|
||||||
// TODO -> we should keep track of this and retry, or aggregation will be broken
|
// TODO -> we should keep track of this and retry, or aggregation will be broken
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -161,7 +161,15 @@ internal class RoomSummaryUpdater @Inject constructor(
|
||||||
roomSummaryEntity.otherMemberIds.clear()
|
roomSummaryEntity.otherMemberIds.clear()
|
||||||
roomSummaryEntity.otherMemberIds.addAll(otherRoomMembers)
|
roomSummaryEntity.otherMemberIds.addAll(otherRoomMembers)
|
||||||
if (roomSummaryEntity.isEncrypted) {
|
if (roomSummaryEntity.isEncrypted) {
|
||||||
eventBus.post(SessionToCryptoRoomMembersUpdate(roomId, roomSummaryEntity.otherMemberIds.toList() + userId))
|
// 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) {
|
||||||
|
roomSummaryEntity.otherMemberIds.toList()
|
||||||
|
} else {
|
||||||
|
roomSummaryEntity.otherMemberIds.toList() + userId
|
||||||
|
}
|
||||||
|
eventBus.post(SessionToCryptoRoomMembersUpdate(roomId, listToCheck))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
|
|
||||||
package im.vector.matrix.android.internal.session.room.send
|
package im.vector.matrix.android.internal.session.room.send
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.media.MediaMetadataRetriever
|
import android.media.MediaMetadataRetriever
|
||||||
import androidx.exifinterface.media.ExifInterface
|
import androidx.exifinterface.media.ExifInterface
|
||||||
|
@ -74,6 +75,7 @@ import javax.inject.Inject
|
||||||
* The transactionId is used as loc
|
* The transactionId is used as loc
|
||||||
*/
|
*/
|
||||||
internal class LocalEchoEventFactory @Inject constructor(
|
internal class LocalEchoEventFactory @Inject constructor(
|
||||||
|
private val context: Context,
|
||||||
@UserId private val userId: String,
|
@UserId private val userId: String,
|
||||||
private val stringProvider: StringProvider,
|
private val stringProvider: StringProvider,
|
||||||
private val textPillsUtils: TextPillsUtils,
|
private val textPillsUtils: TextPillsUtils,
|
||||||
|
@ -266,14 +268,14 @@ internal class LocalEchoEventFactory @Inject constructor(
|
||||||
height = height?.toInt() ?: 0,
|
height = height?.toInt() ?: 0,
|
||||||
size = attachment.size.toInt()
|
size = attachment.size.toInt()
|
||||||
),
|
),
|
||||||
url = attachment.path
|
url = attachment.queryUri.toString()
|
||||||
)
|
)
|
||||||
return createEvent(roomId, content)
|
return createEvent(roomId, content)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createVideoEvent(roomId: String, attachment: ContentAttachmentData): Event {
|
private fun createVideoEvent(roomId: String, attachment: ContentAttachmentData): Event {
|
||||||
val mediaDataRetriever = MediaMetadataRetriever()
|
val mediaDataRetriever = MediaMetadataRetriever()
|
||||||
mediaDataRetriever.setDataSource(attachment.path)
|
mediaDataRetriever.setDataSource(context, attachment.queryUri)
|
||||||
|
|
||||||
// Use frame to calculate height and width as we are sure to get the right ones
|
// Use frame to calculate height and width as we are sure to get the right ones
|
||||||
val firstFrame: Bitmap? = mediaDataRetriever.frameAtTime
|
val firstFrame: Bitmap? = mediaDataRetriever.frameAtTime
|
||||||
|
@ -281,7 +283,7 @@ internal class LocalEchoEventFactory @Inject constructor(
|
||||||
val width = firstFrame?.width ?: 0
|
val width = firstFrame?.width ?: 0
|
||||||
mediaDataRetriever.release()
|
mediaDataRetriever.release()
|
||||||
|
|
||||||
val thumbnailInfo = ThumbnailExtractor.extractThumbnail(attachment)?.let {
|
val thumbnailInfo = ThumbnailExtractor.extractThumbnail(context, attachment)?.let {
|
||||||
ThumbnailInfo(
|
ThumbnailInfo(
|
||||||
width = it.width,
|
width = it.width,
|
||||||
height = it.height,
|
height = it.height,
|
||||||
|
@ -299,10 +301,10 @@ internal class LocalEchoEventFactory @Inject constructor(
|
||||||
size = attachment.size,
|
size = attachment.size,
|
||||||
duration = attachment.duration?.toInt() ?: 0,
|
duration = attachment.duration?.toInt() ?: 0,
|
||||||
// Glide will be able to use the local path and extract a thumbnail.
|
// Glide will be able to use the local path and extract a thumbnail.
|
||||||
thumbnailUrl = attachment.path,
|
thumbnailUrl = attachment.queryUri.toString(),
|
||||||
thumbnailInfo = thumbnailInfo
|
thumbnailInfo = thumbnailInfo
|
||||||
),
|
),
|
||||||
url = attachment.path
|
url = attachment.queryUri.toString()
|
||||||
)
|
)
|
||||||
return createEvent(roomId, content)
|
return createEvent(roomId, content)
|
||||||
}
|
}
|
||||||
|
@ -315,7 +317,7 @@ internal class LocalEchoEventFactory @Inject constructor(
|
||||||
mimeType = attachment.getSafeMimeType()?.takeIf { it.isNotBlank() } ?: "audio/mpeg",
|
mimeType = attachment.getSafeMimeType()?.takeIf { it.isNotBlank() } ?: "audio/mpeg",
|
||||||
size = attachment.size
|
size = attachment.size
|
||||||
),
|
),
|
||||||
url = attachment.path
|
url = attachment.queryUri.toString()
|
||||||
)
|
)
|
||||||
return createEvent(roomId, content)
|
return createEvent(roomId, content)
|
||||||
}
|
}
|
||||||
|
@ -329,7 +331,7 @@ internal class LocalEchoEventFactory @Inject constructor(
|
||||||
?: "application/octet-stream",
|
?: "application/octet-stream",
|
||||||
size = attachment.size
|
size = attachment.size
|
||||||
),
|
),
|
||||||
url = attachment.path
|
url = attachment.queryUri.toString()
|
||||||
)
|
)
|
||||||
return createEvent(roomId, content)
|
return createEvent(roomId, content)
|
||||||
}
|
}
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue