mirror of
https://github.com/vector-im/element-android.git
synced 2024-11-16 02:05:06 +08:00
Merge pull request #1140 from vector-im/feature/gossiping_work
Feature/gossiping work
This commit is contained in:
commit
56c241c9bd
@ -2,7 +2,7 @@ Changes in RiotX 0.19.0 (2020-XX-XX)
|
||||
===================================================
|
||||
|
||||
Features ✨:
|
||||
-
|
||||
- Cross-Signing | Support SSSS secret sharing (#944)
|
||||
|
||||
Improvements 🙌:
|
||||
-
|
||||
|
@ -97,6 +97,7 @@ dependencies {
|
||||
def coroutines_version = "1.3.2"
|
||||
def markwon_version = '3.1.0'
|
||||
def daggerVersion = '2.25.4'
|
||||
def work_version = '2.3.3'
|
||||
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
|
||||
@ -126,7 +127,7 @@ dependencies {
|
||||
kapt 'dk.ilios:realmfieldnameshelper:1.1.1'
|
||||
|
||||
// Work
|
||||
implementation "androidx.work:work-runtime-ktx:2.3.3"
|
||||
implementation "androidx.work:work-runtime-ktx:$work_version"
|
||||
|
||||
// FP
|
||||
implementation "io.arrow-kt:arrow-core:$arrow_version"
|
||||
|
@ -0,0 +1,198 @@
|
||||
/*
|
||||
* 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.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.model.event.EncryptedEventContent
|
||||
import junit.framework.TestCase.assertNotNull
|
||||
import junit.framework.TestCase.assertTrue
|
||||
import junit.framework.TestCase.fail
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
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
|
||||
|
||||
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
|
||||
waitWithLatch { latch ->
|
||||
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)
|
||||
|
||||
waitWithLatch { latch ->
|
||||
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)
|
||||
waitWithLatch { latch ->
|
||||
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)
|
||||
}
|
||||
|
||||
fun retryPeriodicallyWithLatch(latch: CountDownLatch, condition: (() -> Boolean)) {
|
||||
GlobalScope.launch {
|
||||
while (true) {
|
||||
delay(1000)
|
||||
if (condition()) {
|
||||
latch.countDown()
|
||||
return@launch
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun waitWithLatch(block: (CountDownLatch) -> Unit) {
|
||||
val latch = CountDownLatch(1)
|
||||
block(latch)
|
||||
mTestHelper.await(latch)
|
||||
}
|
||||
}
|
@ -34,7 +34,6 @@ import im.vector.matrix.android.common.assertDictEquals
|
||||
import im.vector.matrix.android.common.assertListEquals
|
||||
import im.vector.matrix.android.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM_BACKUP
|
||||
import im.vector.matrix.android.internal.crypto.MegolmSessionData
|
||||
import im.vector.matrix.android.internal.crypto.OutgoingRoomKeyRequest
|
||||
import im.vector.matrix.android.internal.crypto.crosssigning.DeviceTrustLevel
|
||||
import im.vector.matrix.android.internal.crypto.keysbackup.model.KeysBackupVersionTrust
|
||||
import im.vector.matrix.android.internal.crypto.keysbackup.model.MegolmBackupCreationInfo
|
||||
@ -326,46 +325,46 @@ class KeysBackupTest : InstrumentedTest {
|
||||
* - Restore must be successful
|
||||
* - *** There must be no more pending key share requests
|
||||
*/
|
||||
@Test
|
||||
fun restoreKeysBackupAndKeyShareRequestTest() {
|
||||
fail("Check with Valere for this test. I think we do not send key share request")
|
||||
|
||||
val testData = createKeysBackupScenarioWithPassword(null)
|
||||
|
||||
// - Check the SDK sent key share requests
|
||||
val cryptoStore2 = (testData.aliceSession2.cryptoService().keysBackupService() as DefaultKeysBackupService).store
|
||||
val unsentRequest = cryptoStore2
|
||||
.getOutgoingRoomKeyRequestByState(setOf(OutgoingRoomKeyRequest.RequestState.UNSENT))
|
||||
val sentRequest = cryptoStore2
|
||||
.getOutgoingRoomKeyRequestByState(setOf(OutgoingRoomKeyRequest.RequestState.SENT))
|
||||
|
||||
// Request is either sent or unsent
|
||||
assertTrue(unsentRequest != null || sentRequest != null)
|
||||
|
||||
// - Restore the e2e backup from the homeserver
|
||||
val importRoomKeysResult = mTestHelper.doSync<ImportRoomKeysResult> {
|
||||
testData.aliceSession2.cryptoService().keysBackupService().restoreKeysWithRecoveryKey(testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion!!,
|
||||
testData.prepareKeysBackupDataResult.megolmBackupCreationInfo.recoveryKey,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
it
|
||||
)
|
||||
}
|
||||
|
||||
checkRestoreSuccess(testData, importRoomKeysResult.totalNumberOfKeys, importRoomKeysResult.successfullyNumberOfImportedKeys)
|
||||
|
||||
// - There must be no more pending key share requests
|
||||
val unsentRequestAfterRestoration = cryptoStore2
|
||||
.getOutgoingRoomKeyRequestByState(setOf(OutgoingRoomKeyRequest.RequestState.UNSENT))
|
||||
val sentRequestAfterRestoration = cryptoStore2
|
||||
.getOutgoingRoomKeyRequestByState(setOf(OutgoingRoomKeyRequest.RequestState.SENT))
|
||||
|
||||
// Request is either sent or unsent
|
||||
assertTrue(unsentRequestAfterRestoration == null && sentRequestAfterRestoration == null)
|
||||
|
||||
testData.cleanUp(mTestHelper)
|
||||
}
|
||||
// @Test
|
||||
// fun restoreKeysBackupAndKeyShareRequestTest() {
|
||||
// fail("Check with Valere for this test. I think we do not send key share request")
|
||||
//
|
||||
// val testData = createKeysBackupScenarioWithPassword(null)
|
||||
//
|
||||
// // - Check the SDK sent key share requests
|
||||
// val cryptoStore2 = (testData.aliceSession2.cryptoService().keysBackupService() as DefaultKeysBackupService).store
|
||||
// val unsentRequest = cryptoStore2
|
||||
// .getOutgoingRoomKeyRequestByState(setOf(ShareRequestState.UNSENT))
|
||||
// val sentRequest = cryptoStore2
|
||||
// .getOutgoingRoomKeyRequestByState(setOf(ShareRequestState.SENT))
|
||||
//
|
||||
// // Request is either sent or unsent
|
||||
// assertTrue(unsentRequest != null || sentRequest != null)
|
||||
//
|
||||
// // - Restore the e2e backup from the homeserver
|
||||
// val importRoomKeysResult = mTestHelper.doSync<ImportRoomKeysResult> {
|
||||
// testData.aliceSession2.cryptoService().keysBackupService().restoreKeysWithRecoveryKey(testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion!!,
|
||||
// testData.prepareKeysBackupDataResult.megolmBackupCreationInfo.recoveryKey,
|
||||
// null,
|
||||
// null,
|
||||
// null,
|
||||
// it
|
||||
// )
|
||||
// }
|
||||
//
|
||||
// checkRestoreSuccess(testData, importRoomKeysResult.totalNumberOfKeys, importRoomKeysResult.successfullyNumberOfImportedKeys)
|
||||
//
|
||||
// // - There must be no more pending key share requests
|
||||
// val unsentRequestAfterRestoration = cryptoStore2
|
||||
// .getOutgoingRoomKeyRequestByState(setOf(ShareRequestState.UNSENT))
|
||||
// val sentRequestAfterRestoration = cryptoStore2
|
||||
// .getOutgoingRoomKeyRequestByState(setOf(ShareRequestState.SENT))
|
||||
//
|
||||
// // Request is either sent or unsent
|
||||
// assertTrue(unsentRequestAfterRestoration == null && sentRequestAfterRestoration == null)
|
||||
//
|
||||
// testData.cleanUp(mTestHelper)
|
||||
// }
|
||||
|
||||
/**
|
||||
* - Do an e2e backup to the homeserver with a recovery key
|
||||
|
@ -23,5 +23,14 @@ data class MXCryptoConfig(
|
||||
// Tell whether the encryption of the event content is enabled for the invited members.
|
||||
// SDK clients can disable this by settings it to false.
|
||||
// Note that the encryption for the invited members will be blocked if the history visibility is "joined".
|
||||
var enableEncryptionForInvitedMembers: Boolean = true
|
||||
var enableEncryptionForInvitedMembers: Boolean = true,
|
||||
|
||||
/**
|
||||
* If set to true, the SDK will automatically ignore room key request (gossiping)
|
||||
* coming from your other untrusted sessions (or blocked).
|
||||
* If set to false, the request will be forwarded to the application layer; in this
|
||||
* case the application can decide to prompt the user.
|
||||
*/
|
||||
var discardRoomKeyRequestsFromUntrustedDevices : Boolean = true
|
||||
|
||||
)
|
||||
|
@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Copyright (c) 2020 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.api.extensions
|
||||
|
||||
inline fun <A> tryThis(operation: () -> A): A? {
|
||||
return try {
|
||||
operation()
|
||||
} catch (any: Throwable) {
|
||||
null
|
||||
}
|
||||
}
|
@ -22,12 +22,14 @@ import im.vector.matrix.android.api.MatrixCallback
|
||||
import im.vector.matrix.android.api.listeners.ProgressListener
|
||||
import im.vector.matrix.android.api.session.crypto.crosssigning.CrossSigningService
|
||||
import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupService
|
||||
import im.vector.matrix.android.api.session.crypto.keyshare.RoomKeysRequestListener
|
||||
import im.vector.matrix.android.api.session.crypto.keyshare.GossipingRequestListener
|
||||
import im.vector.matrix.android.api.session.crypto.verification.VerificationService
|
||||
import im.vector.matrix.android.api.session.events.model.Content
|
||||
import im.vector.matrix.android.api.session.events.model.Event
|
||||
import im.vector.matrix.android.internal.crypto.IncomingRoomKeyRequest
|
||||
import im.vector.matrix.android.internal.crypto.MXEventDecryptionResult
|
||||
import im.vector.matrix.android.internal.crypto.NewSessionListener
|
||||
import im.vector.matrix.android.internal.crypto.OutgoingRoomKeyRequest
|
||||
import im.vector.matrix.android.internal.crypto.crosssigning.DeviceTrustLevel
|
||||
import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
|
||||
import im.vector.matrix.android.internal.crypto.model.ImportRoomKeysResult
|
||||
@ -86,13 +88,15 @@ interface CryptoService {
|
||||
|
||||
fun getDeviceInfo(userId: String, deviceId: String?): CryptoDeviceInfo?
|
||||
|
||||
fun requestRoomKeyForEvent(event: Event)
|
||||
|
||||
fun reRequestRoomKeyForEvent(event: Event)
|
||||
|
||||
fun cancelRoomKeyRequest(requestBody: RoomKeyRequestBody)
|
||||
|
||||
fun addRoomKeysRequestListener(listener: RoomKeysRequestListener)
|
||||
fun addRoomKeysRequestListener(listener: GossipingRequestListener)
|
||||
|
||||
fun removeRoomKeysRequestListener(listener: RoomKeysRequestListener)
|
||||
fun removeRoomKeysRequestListener(listener: GossipingRequestListener)
|
||||
|
||||
fun getDevicesList(callback: MatrixCallback<DevicesListResponse>)
|
||||
|
||||
@ -129,4 +133,8 @@ interface CryptoService {
|
||||
fun addNewSessionListener(newSessionListener: NewSessionListener)
|
||||
|
||||
fun removeSessionListener(listener: NewSessionListener)
|
||||
|
||||
fun getOutgoingRoomKeyRequest(): List<OutgoingRoomKeyRequest>
|
||||
fun getIncomingRoomKeyRequest(): List<IncomingRoomKeyRequest>
|
||||
fun getGossipingEventsTrail(): List<Event>
|
||||
}
|
||||
|
@ -68,4 +68,7 @@ interface CrossSigningService {
|
||||
fun checkDeviceTrust(otherUserId: String,
|
||||
otherDeviceId: String,
|
||||
locallyTrusted: Boolean?): DeviceTrustResult
|
||||
|
||||
fun onSecretSSKGossip(sskPrivateKey: String)
|
||||
fun onSecretUSKGossip(uskPrivateKey: String)
|
||||
}
|
||||
|
@ -17,12 +17,13 @@
|
||||
package im.vector.matrix.android.api.session.crypto.keyshare
|
||||
|
||||
import im.vector.matrix.android.internal.crypto.IncomingRoomKeyRequest
|
||||
import im.vector.matrix.android.internal.crypto.IncomingRoomKeyRequestCancellation
|
||||
import im.vector.matrix.android.internal.crypto.IncomingRequestCancellation
|
||||
import im.vector.matrix.android.internal.crypto.IncomingSecretShareRequest
|
||||
|
||||
/**
|
||||
* Room keys events listener
|
||||
*/
|
||||
interface RoomKeysRequestListener {
|
||||
interface GossipingRequestListener {
|
||||
/**
|
||||
* An room key request has been received.
|
||||
*
|
||||
@ -30,10 +31,16 @@ interface RoomKeysRequestListener {
|
||||
*/
|
||||
fun onRoomKeyRequest(request: IncomingRoomKeyRequest)
|
||||
|
||||
/**
|
||||
* Returns the secret value to be shared
|
||||
* @return true if is handled
|
||||
*/
|
||||
fun onSecretShareRequest(request: IncomingSecretShareRequest) : Boolean
|
||||
|
||||
/**
|
||||
* A room key request cancellation has been received.
|
||||
*
|
||||
* @param request the cancellation request
|
||||
*/
|
||||
fun onRoomKeyRequestCancellation(request: IncomingRoomKeyRequestCancellation)
|
||||
fun onRoomKeyRequestCancellation(request: IncomingRequestCancellation)
|
||||
}
|
@ -17,6 +17,7 @@
|
||||
package im.vector.matrix.android.api.session.crypto.verification
|
||||
|
||||
import im.vector.matrix.android.api.MatrixCallback
|
||||
import im.vector.matrix.android.api.session.events.model.Event
|
||||
import im.vector.matrix.android.api.session.events.model.LocalEcho
|
||||
|
||||
/**
|
||||
@ -136,4 +137,6 @@ interface VerificationService {
|
||||
return age in tooInThePast..tooInTheFuture
|
||||
}
|
||||
}
|
||||
|
||||
fun onPotentiallyInterestingEventRoomFailToDecrypt(event: Event)
|
||||
}
|
||||
|
@ -142,12 +142,12 @@ data class Event(
|
||||
}
|
||||
|
||||
fun toContentStringWithIndent(): String {
|
||||
val contentMap = toContent().toMutableMap()
|
||||
val contentMap = toContent()
|
||||
return JSONObject(contentMap).toString(4)
|
||||
}
|
||||
|
||||
fun toClearContentStringWithIndent(): String? {
|
||||
val contentMap = this.mxDecryptionResult?.payload?.toMutableMap()
|
||||
val contentMap = this.mxDecryptionResult?.payload
|
||||
val adapter = MoshiProvider.providesMoshi().adapter(Map::class.java)
|
||||
return contentMap?.let { JSONObject(adapter.toJson(it)).toString(4) }
|
||||
}
|
||||
|
@ -111,6 +111,8 @@ interface SharedSecretStorageService {
|
||||
|
||||
fun checkShouldBeAbleToAccessSecrets(secretNames: List<String>, keyId: String?) : IntegrityResult
|
||||
|
||||
fun requestSecret(name: String, myOtherDeviceId: String)
|
||||
|
||||
data class KeyRef(
|
||||
val keyId: String?,
|
||||
val keySpec: SsssKeySpec?
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -33,7 +33,9 @@ import im.vector.matrix.android.api.failure.Failure
|
||||
import im.vector.matrix.android.api.listeners.ProgressListener
|
||||
import im.vector.matrix.android.api.session.crypto.CryptoService
|
||||
import im.vector.matrix.android.api.session.crypto.MXCryptoError
|
||||
import im.vector.matrix.android.api.session.crypto.keyshare.RoomKeysRequestListener
|
||||
import im.vector.matrix.android.api.session.crypto.crosssigning.SELF_SIGNING_KEY_SSSS_NAME
|
||||
import im.vector.matrix.android.api.session.crypto.crosssigning.USER_SIGNING_KEY_SSSS_NAME
|
||||
import im.vector.matrix.android.api.session.crypto.keyshare.GossipingRequestListener
|
||||
import im.vector.matrix.android.api.session.events.model.Content
|
||||
import im.vector.matrix.android.api.session.events.model.Event
|
||||
import im.vector.matrix.android.api.session.events.model.EventType
|
||||
@ -55,7 +57,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.MXEncryptEventContentResult
|
||||
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.SecretSendEventContent
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.DevicesListResponse
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.KeysUploadResponse
|
||||
@ -80,6 +84,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.sync.model.SyncResponse
|
||||
import im.vector.matrix.android.internal.task.TaskExecutor
|
||||
import im.vector.matrix.android.internal.task.TaskThread
|
||||
import im.vector.matrix.android.internal.task.configureWith
|
||||
import im.vector.matrix.android.internal.util.JsonCanonicalizer
|
||||
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
|
||||
@ -115,6 +120,7 @@ internal class DefaultCryptoService @Inject constructor(
|
||||
private val myDeviceInfoHolder: Lazy<MyDeviceInfoHolder>,
|
||||
// the crypto store
|
||||
private val cryptoStore: IMXCryptoStore,
|
||||
|
||||
// Olm device
|
||||
private val olmDevice: MXOlmDevice,
|
||||
// Set of parameters used to configure/customize the end-to-end crypto.
|
||||
@ -136,7 +142,7 @@ internal class DefaultCryptoService @Inject constructor(
|
||||
//
|
||||
private val incomingRoomKeyRequestManager: IncomingRoomKeyRequestManager,
|
||||
//
|
||||
private val outgoingRoomKeyRequestManager: OutgoingRoomKeyRequestManager,
|
||||
private val outgoingGossipingRequestManager: OutgoingGossipingRequestManager,
|
||||
// Actions
|
||||
private val setDeviceVerificationAction: SetDeviceVerificationAction,
|
||||
private val megolmSessionDataImporter: MegolmSessionDataImporter,
|
||||
@ -188,6 +194,7 @@ internal class DefaultCryptoService @Inject constructor(
|
||||
override fun setDeviceName(deviceId: String, deviceName: String, callback: MatrixCallback<Unit>) {
|
||||
setDeviceNameTask
|
||||
.configureWith(SetDeviceNameTask.Params(deviceId, deviceName)) {
|
||||
this.executionThread = TaskThread.CRYPTO
|
||||
this.callback = object : MatrixCallback<Unit> {
|
||||
override fun onSuccess(data: Unit) {
|
||||
// bg refresh of crypto device
|
||||
@ -206,6 +213,7 @@ internal class DefaultCryptoService @Inject constructor(
|
||||
override fun deleteDevice(deviceId: String, callback: MatrixCallback<Unit>) {
|
||||
deleteDeviceTask
|
||||
.configureWith(DeleteDeviceTask.Params(deviceId)) {
|
||||
this.executionThread = TaskThread.CRYPTO
|
||||
this.callback = callback
|
||||
}
|
||||
.executeBy(taskExecutor)
|
||||
@ -214,6 +222,7 @@ internal class DefaultCryptoService @Inject constructor(
|
||||
override fun deleteDeviceWithUserPassword(deviceId: String, authSession: String?, password: String, callback: MatrixCallback<Unit>) {
|
||||
deleteDeviceWithUserPasswordTask
|
||||
.configureWith(DeleteDeviceWithUserPasswordTask.Params(deviceId, authSession, password)) {
|
||||
this.executionThread = TaskThread.CRYPTO
|
||||
this.callback = callback
|
||||
}
|
||||
.executeBy(taskExecutor)
|
||||
@ -230,6 +239,7 @@ internal class DefaultCryptoService @Inject constructor(
|
||||
override fun getDevicesList(callback: MatrixCallback<DevicesListResponse>) {
|
||||
getDevicesTask
|
||||
.configureWith {
|
||||
this.executionThread = TaskThread.CRYPTO
|
||||
this.callback = callback
|
||||
}
|
||||
.executeBy(taskExecutor)
|
||||
@ -238,6 +248,7 @@ internal class DefaultCryptoService @Inject constructor(
|
||||
override fun getDeviceInfo(deviceId: String, callback: MatrixCallback<DeviceInfo>) {
|
||||
getDeviceInfoTask
|
||||
.configureWith(GetDeviceInfoTask.Params(deviceId)) {
|
||||
this.executionThread = TaskThread.CRYPTO
|
||||
this.callback = callback
|
||||
}
|
||||
.executeBy(taskExecutor)
|
||||
@ -300,14 +311,13 @@ internal class DefaultCryptoService @Inject constructor(
|
||||
runCatching {
|
||||
uploadDeviceKeys()
|
||||
oneTimeKeysUploader.maybeUploadOneTimeKeys()
|
||||
outgoingRoomKeyRequestManager.start()
|
||||
keysBackupService.checkAndStartKeysBackup()
|
||||
if (isInitialSync) {
|
||||
// refresh the devices list for each known room members
|
||||
deviceListManager.invalidateAllDeviceLists()
|
||||
deviceListManager.refreshOutdatedDeviceLists()
|
||||
} else {
|
||||
incomingRoomKeyRequestManager.processReceivedRoomKeyRequests()
|
||||
incomingRoomKeyRequestManager.processReceivedGossipingRequests()
|
||||
}
|
||||
}.fold(
|
||||
{
|
||||
@ -328,8 +338,6 @@ internal class DefaultCryptoService @Inject constructor(
|
||||
fun close() = runBlocking(coroutineDispatchers.crypto) {
|
||||
cryptoCoroutineScope.coroutineContext.cancelChildren(CancellationException("Closing crypto module"))
|
||||
|
||||
outgoingRoomKeyRequestManager.stop()
|
||||
|
||||
olmDevice.release()
|
||||
cryptoStore.close()
|
||||
}
|
||||
@ -368,7 +376,7 @@ internal class DefaultCryptoService @Inject constructor(
|
||||
// Make sure we process to-device messages before generating new one-time-keys #2782
|
||||
deviceListManager.refreshOutdatedDeviceLists()
|
||||
oneTimeKeysUploader.maybeUploadOneTimeKeys()
|
||||
incomingRoomKeyRequestManager.processReceivedRoomKeyRequests()
|
||||
incomingRoomKeyRequestManager.processReceivedGossipingRequests()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -688,13 +696,24 @@ internal class DefaultCryptoService @Inject constructor(
|
||||
* @param event the event
|
||||
*/
|
||||
fun onToDeviceEvent(event: Event) {
|
||||
// event have already been decrypted
|
||||
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
|
||||
when (event.getClearType()) {
|
||||
EventType.ROOM_KEY, EventType.FORWARDED_ROOM_KEY -> {
|
||||
cryptoStore.saveGossipingEvent(event)
|
||||
// Keys are imported directly, not waiting for end of sync
|
||||
onRoomKeyEvent(event)
|
||||
}
|
||||
EventType.REQUEST_SECRET,
|
||||
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)
|
||||
incomingRoomKeyRequestManager.onGossipingRequestEvent(event)
|
||||
}
|
||||
EventType.SEND_SECRET -> {
|
||||
cryptoStore.saveGossipingEvent(event)
|
||||
onSecretSendReceived(event)
|
||||
}
|
||||
else -> {
|
||||
// ignore
|
||||
@ -710,6 +729,7 @@ internal class DefaultCryptoService @Inject constructor(
|
||||
*/
|
||||
private fun onRoomKeyEvent(event: Event) {
|
||||
val roomKeyContent = event.getClearContent().toModel<RoomKeyContent>() ?: return
|
||||
Timber.v("## onRoomKeyEvent() : type<${event.type}> , sessionId<${roomKeyContent.sessionId}>")
|
||||
if (roomKeyContent.roomId.isNullOrEmpty() || roomKeyContent.algorithm.isNullOrEmpty()) {
|
||||
Timber.e("## onRoomKeyEvent() : missing fields")
|
||||
return
|
||||
@ -722,6 +742,46 @@ internal class DefaultCryptoService @Inject constructor(
|
||||
alg.onRoomKeyEvent(event, keysBackupService)
|
||||
}
|
||||
|
||||
private fun onSecretSendReceived(event: Event) {
|
||||
Timber.i("## onSecretSend() : onSecretSendReceived ${event.content?.get("sender_key")}")
|
||||
if (!event.isEncrypted()) {
|
||||
// secret send messages must be encrypted
|
||||
Timber.e("## onSecretSend() :Received unencrypted secret send event")
|
||||
return
|
||||
}
|
||||
|
||||
// Was that sent by us?
|
||||
if (event.senderId != credentials.userId) {
|
||||
Timber.e("## 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("## onSecretSend() : Ignore secret that was not requested: ${secretContent.requestId}")
|
||||
return
|
||||
}
|
||||
|
||||
when (existingRequest.secretName) {
|
||||
SELF_SIGNING_KEY_SSSS_NAME -> {
|
||||
crossSigningService.onSecretSSKGossip(secretContent.secretValue)
|
||||
return
|
||||
}
|
||||
USER_SIGNING_KEY_SSSS_NAME -> {
|
||||
crossSigningService.onSecretUSKGossip(secretContent.secretValue)
|
||||
return
|
||||
}
|
||||
else -> {
|
||||
// Ask to application layer?
|
||||
Timber.v("## onSecretSend() : secret not handled by SDK")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle an m.room.encryption event.
|
||||
*
|
||||
@ -735,7 +795,7 @@ internal class DefaultCryptoService @Inject constructor(
|
||||
val userIds = getRoomUserIds(roomId)
|
||||
setEncryptionInRoom(roomId, event.content?.get("algorithm")?.toString(), true, userIds)
|
||||
} catch (throwable: Throwable) {
|
||||
Timber.e(throwable)
|
||||
Timber.e(throwable, "## onRoomEncryptionEvent ERROR FAILED TO SETUP CRYPTO ")
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -997,14 +1057,14 @@ internal class DefaultCryptoService @Inject constructor(
|
||||
setRoomBlacklistUnverifiedDevices(roomId, false)
|
||||
}
|
||||
|
||||
// TODO Check if this method is still necessary
|
||||
// TODO Check if this method is still necessary
|
||||
/**
|
||||
* Cancel any earlier room key request
|
||||
*
|
||||
* @param requestBody requestBody
|
||||
*/
|
||||
override fun cancelRoomKeyRequest(requestBody: RoomKeyRequestBody) {
|
||||
outgoingRoomKeyRequestManager.cancelRoomKeyRequest(requestBody)
|
||||
outgoingGossipingRequestManager.cancelRoomKeyRequest(requestBody)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1013,37 +1073,53 @@ internal class DefaultCryptoService @Inject constructor(
|
||||
* @param event the event to decrypt again.
|
||||
*/
|
||||
override fun reRequestRoomKeyForEvent(event: Event) {
|
||||
val wireContent = event.content
|
||||
if (wireContent == null) {
|
||||
val wireContent = event.content.toModel<EncryptedEventContent>() ?: return Unit.also {
|
||||
Timber.e("## reRequestRoomKeyForEvent Failed to re-request key, null content")
|
||||
return
|
||||
}
|
||||
|
||||
val requestBody = RoomKeyRequestBody(
|
||||
algorithm = wireContent["algorithm"]?.toString(),
|
||||
algorithm = wireContent.algorithm,
|
||||
roomId = event.roomId,
|
||||
senderKey = wireContent["sender_key"]?.toString(),
|
||||
sessionId = wireContent["session_id"]?.toString()
|
||||
senderKey = wireContent.senderKey,
|
||||
sessionId = wireContent.sessionId
|
||||
)
|
||||
|
||||
outgoingRoomKeyRequestManager.resendRoomKeyRequest(requestBody)
|
||||
outgoingGossipingRequestManager.resendRoomKeyRequest(requestBody)
|
||||
}
|
||||
|
||||
override fun requestRoomKeyForEvent(event: Event) {
|
||||
val wireContent = event.content.toModel<EncryptedEventContent>() ?: return Unit.also {
|
||||
Timber.e("## requestRoomKeyForEvent Failed to request key, null content eventId: ${event.eventId}")
|
||||
}
|
||||
|
||||
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
|
||||
if (!isStarted()) {
|
||||
Timber.v("## requestRoomKeyForEvent() : wait after e2e init")
|
||||
internalStart(false)
|
||||
}
|
||||
roomDecryptorProvider
|
||||
.getOrCreateRoomDecryptor(event.roomId, wireContent.algorithm)
|
||||
?.requestKeysForEvent(event) ?: run {
|
||||
Timber.v("## requestRoomKeyForEvent() : No room decryptor for roomId:${event.roomId} algorithm:${wireContent.algorithm}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a RoomKeysRequestListener listener.
|
||||
* Add a GossipingRequestListener listener.
|
||||
*
|
||||
* @param listener listener
|
||||
*/
|
||||
override fun addRoomKeysRequestListener(listener: RoomKeysRequestListener) {
|
||||
override fun addRoomKeysRequestListener(listener: GossipingRequestListener) {
|
||||
incomingRoomKeyRequestManager.addRoomKeysRequestListener(listener)
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a RoomKeysRequestListener listener.
|
||||
* Add a GossipingRequestListener listener.
|
||||
*
|
||||
* @param listener listener
|
||||
*/
|
||||
override fun removeRoomKeysRequestListener(listener: RoomKeysRequestListener) {
|
||||
override fun removeRoomKeysRequestListener(listener: GossipingRequestListener) {
|
||||
incomingRoomKeyRequestManager.removeRoomKeysRequestListener(listener)
|
||||
}
|
||||
|
||||
@ -1084,11 +1160,23 @@ internal class DefaultCryptoService @Inject constructor(
|
||||
override fun removeSessionListener(listener: NewSessionListener) {
|
||||
roomDecryptorProvider.removeSessionListener(listener)
|
||||
}
|
||||
/* ==========================================================================================
|
||||
* DEBUG INFO
|
||||
* ========================================================================================== */
|
||||
/* ==========================================================================================
|
||||
* DEBUG INFO
|
||||
* ========================================================================================== */
|
||||
|
||||
override fun toString(): String {
|
||||
return "DefaultCryptoService of " + credentials.userId + " (" + credentials.deviceId + ")"
|
||||
}
|
||||
|
||||
override fun getOutgoingRoomKeyRequest(): List<OutgoingRoomKeyRequest> {
|
||||
return cryptoStore.getOutgoingRoomKeyRequests()
|
||||
}
|
||||
|
||||
override fun getIncomingRoomKeyRequest(): List<IncomingRoomKeyRequest> {
|
||||
return cryptoStore.getIncomingRoomKeyRequests()
|
||||
}
|
||||
|
||||
override fun getGossipingEventsTrail(): List<Event> {
|
||||
return cryptoStore.getGossipingEventsTrail()
|
||||
}
|
||||
}
|
||||
|
@ -361,13 +361,13 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
|
||||
|
||||
// Handle cross signing keys update
|
||||
val masterKey = response.masterKeys?.get(userId)?.toCryptoModel().also {
|
||||
Timber.d("## CrossSigning : Got keys for $userId : MSK ${it?.unpaddedBase64PublicKey}")
|
||||
Timber.v("## CrossSigning : Got keys for $userId : MSK ${it?.unpaddedBase64PublicKey}")
|
||||
}
|
||||
val selfSigningKey = response.selfSigningKeys?.get(userId)?.toCryptoModel()?.also {
|
||||
Timber.d("## CrossSigning : Got keys for $userId : SSK ${it.unpaddedBase64PublicKey}")
|
||||
Timber.v("## CrossSigning : Got keys for $userId : SSK ${it.unpaddedBase64PublicKey}")
|
||||
}
|
||||
val userSigningKey = response.userSigningKeys?.get(userId)?.toCryptoModel()?.also {
|
||||
Timber.d("## CrossSigning : Got keys for $userId : USK ${it.unpaddedBase64PublicKey}")
|
||||
Timber.v("## CrossSigning : Got keys for $userId : USK ${it.unpaddedBase64PublicKey}")
|
||||
}
|
||||
cryptoStore.storeUserCrossSigningKeys(
|
||||
userId,
|
||||
|
@ -0,0 +1,43 @@
|
||||
/*
|
||||
* 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,
|
||||
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
|
||||
}
|
@ -18,12 +18,12 @@ package im.vector.matrix.android.internal.crypto
|
||||
|
||||
import im.vector.matrix.android.api.session.events.model.Event
|
||||
import im.vector.matrix.android.api.session.events.model.toModel
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyShareCancellation
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.ShareRequestCancellation
|
||||
|
||||
/**
|
||||
* IncomingRoomKeyRequestCancellation describes the incoming room key cancellation.
|
||||
* IncomingRequestCancellation describes the incoming room key cancellation.
|
||||
*/
|
||||
data class IncomingRoomKeyRequestCancellation(
|
||||
data class IncomingRequestCancellation(
|
||||
/**
|
||||
* The user id
|
||||
*/
|
||||
@ -37,22 +37,24 @@ data class IncomingRoomKeyRequestCancellation(
|
||||
/**
|
||||
* The request id
|
||||
*/
|
||||
override val requestId: String? = null
|
||||
) : IncomingRoomKeyRequestCommon {
|
||||
override val requestId: String? = null,
|
||||
override val localCreationTimestamp: Long?
|
||||
) : IncomingShareRequestCommon {
|
||||
companion object {
|
||||
/**
|
||||
* Factory
|
||||
*
|
||||
* @param event the event
|
||||
*/
|
||||
fun fromEvent(event: Event): IncomingRoomKeyRequestCancellation? {
|
||||
fun fromEvent(event: Event): IncomingRequestCancellation? {
|
||||
return event.getClearContent()
|
||||
.toModel<RoomKeyShareCancellation>()
|
||||
.toModel<ShareRequestCancellation>()
|
||||
?.let {
|
||||
IncomingRoomKeyRequestCancellation(
|
||||
IncomingRequestCancellation(
|
||||
userId = event.senderId,
|
||||
deviceId = it.requestingDeviceId,
|
||||
requestId = it.requestId
|
||||
requestId = it.requestId,
|
||||
localCreationTimestamp = event.ageLocalTs ?: System.currentTimeMillis()
|
||||
)
|
||||
}
|
||||
}
|
@ -46,6 +46,8 @@ data class IncomingRoomKeyRequest(
|
||||
*/
|
||||
val requestBody: RoomKeyRequestBody? = null,
|
||||
|
||||
val state: GossipingRequestState = GossipingRequestState.NONE,
|
||||
|
||||
/**
|
||||
* The runnable to call to accept to share the keys
|
||||
*/
|
||||
@ -56,8 +58,9 @@ data class IncomingRoomKeyRequest(
|
||||
* The runnable to call to ignore the key share request.
|
||||
*/
|
||||
@Transient
|
||||
var ignore: Runnable? = null
|
||||
) : IncomingRoomKeyRequestCommon {
|
||||
var ignore: Runnable? = null,
|
||||
override val localCreationTimestamp: Long?
|
||||
) : IncomingShareRequestCommon {
|
||||
companion object {
|
||||
/**
|
||||
* Factory
|
||||
@ -72,7 +75,8 @@ data class IncomingRoomKeyRequest(
|
||||
userId = event.senderId,
|
||||
deviceId = it.requestingDeviceId,
|
||||
requestId = it.requestId,
|
||||
requestBody = it.body ?: RoomKeyRequestBody()
|
||||
requestBody = it.body ?: RoomKeyRequestBody(),
|
||||
localCreationTimestamp = event.ageLocalTs ?: System.currentTimeMillis()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -17,9 +17,15 @@
|
||||
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.crypto.MXCryptoConfig
|
||||
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.internal.crypto.model.rest.RoomKeyShare
|
||||
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.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.session.SessionScope
|
||||
import timber.log.Timber
|
||||
@ -29,18 +35,20 @@ import javax.inject.Inject
|
||||
internal class IncomingRoomKeyRequestManager @Inject constructor(
|
||||
private val credentials: Credentials,
|
||||
private val cryptoStore: IMXCryptoStore,
|
||||
private val cryptoConfig: MXCryptoConfig,
|
||||
private val secretSecretCryptoProvider: ShareSecretCryptoProvider,
|
||||
private val roomDecryptorProvider: RoomDecryptorProvider) {
|
||||
|
||||
// list of IncomingRoomKeyRequests/IncomingRoomKeyRequestCancellations
|
||||
// we received in the current sync.
|
||||
private val receivedRoomKeyRequests = ArrayList<IncomingRoomKeyRequest>()
|
||||
private val receivedRoomKeyRequestCancellations = ArrayList<IncomingRoomKeyRequestCancellation>()
|
||||
private val receivedGossipingRequests = ArrayList<IncomingShareRequestCommon>()
|
||||
private val receivedRequestCancellations = ArrayList<IncomingRequestCancellation>()
|
||||
|
||||
// the listeners
|
||||
private val roomKeysRequestListeners: MutableSet<RoomKeysRequestListener> = HashSet()
|
||||
private val gossipingRequestListeners: MutableSet<GossipingRequestListener> = HashSet()
|
||||
|
||||
init {
|
||||
receivedRoomKeyRequests.addAll(cryptoStore.getPendingIncomingRoomKeyRequests())
|
||||
receivedGossipingRequests.addAll(cryptoStore.getPendingIncomingGossipingRequests())
|
||||
}
|
||||
|
||||
/**
|
||||
@ -49,110 +57,223 @@ internal class IncomingRoomKeyRequestManager @Inject constructor(
|
||||
*
|
||||
* @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")
|
||||
fun onGossipingRequestEvent(event: Event) {
|
||||
Timber.v("## 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("## 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("## 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("## onGossipingRequestEvent() : unsupported action ${roomKeyShare?.action}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process any m.room_key_request events which were queued up during the
|
||||
* 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 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
|
||||
fun processReceivedGossipingRequests() {
|
||||
Timber.v("## processReceivedGossipingRequests()")
|
||||
|
||||
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")
|
||||
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("## 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("## 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("## 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("## processReceivedGossipingRequests() : room key request for unknown $alg in room $roomId")
|
||||
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED)
|
||||
return
|
||||
}
|
||||
if (!decryptor.hasKeysForKeyRequest(request)) {
|
||||
Timber.w("## processReceivedGossipingRequests() : room key request for unknown session ${body.sessionId!!}")
|
||||
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED)
|
||||
return
|
||||
}
|
||||
|
||||
if (credentials.deviceId == deviceId && credentials.userId == userId) {
|
||||
Timber.v("## 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("## processReceivedGossipingRequests() : device is already verified: sharing keys")
|
||||
request.share?.run()
|
||||
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
|
||||
if (device.isBlocked) {
|
||||
Timber.v("## processReceivedGossipingRequests() : device is blocked -> ignored")
|
||||
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED)
|
||||
return
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
// 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("## processIncomingSecretShareRequest() : Missing secret name")
|
||||
}
|
||||
|
||||
val userId = request.userId
|
||||
if (userId == null || credentials.userId != userId) {
|
||||
Timber.e("## processIncomingSecretShareRequest() : Ignoring secret share request from other users")
|
||||
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED)
|
||||
return
|
||||
}
|
||||
|
||||
val deviceId = request.deviceId
|
||||
?: return Unit.also {
|
||||
Timber.e("## processIncomingSecretShareRequest() : Malformed request, no ")
|
||||
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED)
|
||||
}
|
||||
|
||||
if (device.isBlocked) {
|
||||
Timber.v("## processReceivedRoomKeyRequests() : device is blocked -> ignored")
|
||||
cryptoStore.deleteIncomingRoomKeyRequest(request)
|
||||
continue
|
||||
val device = cryptoStore.getUserDevice(userId, deviceId)
|
||||
?: return Unit.also {
|
||||
Timber.e("## processIncomingSecretShareRequest() : Received secret share request from unknown device ${request.deviceId}")
|
||||
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED)
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
if (!device.isVerified || device.isBlocked) {
|
||||
Timber.v("## processIncomingSecretShareRequest() : Ignoring secret share request from untrusted/blocked session $device")
|
||||
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED)
|
||||
return
|
||||
}
|
||||
|
||||
var receivedRoomKeyRequestCancellations: List<IncomingRoomKeyRequestCancellation>? = null
|
||||
val isDeviceLocallyVerified = cryptoStore.getUserDevice(userId, deviceId)?.trustLevel?.isLocallyVerified()
|
||||
|
||||
synchronized(this.receivedRoomKeyRequestCancellations) {
|
||||
if (this.receivedRoomKeyRequestCancellations.isNotEmpty()) {
|
||||
receivedRoomKeyRequestCancellations = this.receivedRoomKeyRequestCancellations.toList()
|
||||
this.receivedRoomKeyRequestCancellations.clear()
|
||||
// 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
|
||||
else -> null
|
||||
}?.let { secretValue ->
|
||||
// TODO check if locally trusted and not outdated
|
||||
Timber.i("## processIncomingSecretShareRequest() : Sharing secret $secretName with $device locally trusted")
|
||||
if (isDeviceLocallyVerified == true) {
|
||||
secretSecretCryptoProvider.shareSecretWithDevice(request, secretValue)
|
||||
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.ACCEPTED)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
request.ignore = Runnable {
|
||||
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED)
|
||||
}
|
||||
|
||||
request.share = { secretValue ->
|
||||
secretSecretCryptoProvider.shareSecretWithDevice(request, secretValue)
|
||||
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.ACCEPTED)
|
||||
}
|
||||
|
||||
onShareRequest(request)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -161,8 +282,8 @@ internal class IncomingRoomKeyRequestManager @Inject constructor(
|
||||
* @param request the request
|
||||
*/
|
||||
private fun onRoomKeyRequest(request: IncomingRoomKeyRequest) {
|
||||
synchronized(roomKeysRequestListeners) {
|
||||
for (listener in roomKeysRequestListeners) {
|
||||
synchronized(gossipingRequestListeners) {
|
||||
for (listener in gossipingRequestListeners) {
|
||||
try {
|
||||
listener.onRoomKeyRequest(request)
|
||||
} catch (e: Exception) {
|
||||
@ -172,14 +293,33 @@ internal class IncomingRoomKeyRequestManager @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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, "## 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: IncomingRoomKeyRequestCancellation) {
|
||||
synchronized(roomKeysRequestListeners) {
|
||||
for (listener in roomKeysRequestListeners) {
|
||||
private fun onRoomKeyRequestCancellation(request: IncomingRequestCancellation) {
|
||||
synchronized(gossipingRequestListeners) {
|
||||
for (listener in gossipingRequestListeners) {
|
||||
try {
|
||||
listener.onRoomKeyRequestCancellation(request)
|
||||
} catch (e: Exception) {
|
||||
@ -189,15 +329,15 @@ internal class IncomingRoomKeyRequestManager @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
fun addRoomKeysRequestListener(listener: RoomKeysRequestListener) {
|
||||
synchronized(roomKeysRequestListeners) {
|
||||
roomKeysRequestListeners.add(listener)
|
||||
fun addRoomKeysRequestListener(listener: GossipingRequestListener) {
|
||||
synchronized(gossipingRequestListeners) {
|
||||
gossipingRequestListeners.add(listener)
|
||||
}
|
||||
}
|
||||
|
||||
fun removeRoomKeysRequestListener(listener: RoomKeysRequestListener) {
|
||||
synchronized(roomKeysRequestListeners) {
|
||||
roomKeysRequestListeners.remove(listener)
|
||||
fun removeRoomKeysRequestListener(listener: GossipingRequestListener) {
|
||||
synchronized(gossipingRequestListeners) {
|
||||
gossipingRequestListeners.remove(listener)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,82 @@
|
||||
/*
|
||||
* Copyright (c) 2020 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.internal.crypto
|
||||
|
||||
import im.vector.matrix.android.api.session.events.model.Event
|
||||
import im.vector.matrix.android.api.session.events.model.toModel
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.SecretShareRequest
|
||||
|
||||
/**
|
||||
* IncomingRoomKeyRequest class defines the incoming room keys request.
|
||||
*/
|
||||
data class IncomingSecretShareRequest(
|
||||
/**
|
||||
* The user id
|
||||
*/
|
||||
override val userId: String? = null,
|
||||
|
||||
/**
|
||||
* The device id
|
||||
*/
|
||||
override val deviceId: String? = null,
|
||||
|
||||
/**
|
||||
* The request id
|
||||
*/
|
||||
override val requestId: String? = null,
|
||||
|
||||
/**
|
||||
* The request body
|
||||
*/
|
||||
val secretName: String? = null,
|
||||
|
||||
/**
|
||||
* The runnable to call to accept to share the keys
|
||||
*/
|
||||
@Transient
|
||||
var share: ((String) -> Unit)? = null,
|
||||
|
||||
/**
|
||||
* The runnable to call to ignore the key share request.
|
||||
*/
|
||||
@Transient
|
||||
var ignore: Runnable? = null,
|
||||
|
||||
override val localCreationTimestamp: Long?
|
||||
|
||||
) : IncomingShareRequestCommon {
|
||||
companion object {
|
||||
/**
|
||||
* Factory
|
||||
*
|
||||
* @param event the event
|
||||
*/
|
||||
fun fromEvent(event: Event): IncomingSecretShareRequest? {
|
||||
return event.getClearContent()
|
||||
.toModel<SecretShareRequest>()
|
||||
?.let {
|
||||
IncomingSecretShareRequest(
|
||||
userId = event.senderId,
|
||||
deviceId = it.requestingDeviceId,
|
||||
requestId = it.requestId,
|
||||
secretName = it.secretName,
|
||||
localCreationTimestamp = event.ageLocalTs ?: System.currentTimeMillis()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -16,7 +16,7 @@
|
||||
|
||||
package im.vector.matrix.android.internal.crypto
|
||||
|
||||
interface IncomingRoomKeyRequestCommon {
|
||||
interface IncomingShareRequestCommon {
|
||||
/**
|
||||
* The user id
|
||||
*/
|
||||
@ -31,4 +31,6 @@ interface IncomingRoomKeyRequestCommon {
|
||||
* The request id
|
||||
*/
|
||||
val requestId: String?
|
||||
|
||||
val localCreationTimestamp: Long?
|
||||
}
|
@ -770,7 +770,7 @@ internal class MXOlmDevice @Inject constructor(
|
||||
return session
|
||||
}
|
||||
} else {
|
||||
Timber.w("## getInboundGroupSession() : Cannot retrieve inbound group session $sessionId")
|
||||
Timber.v("## getInboundGroupSession() : Cannot retrieve inbound group session $sessionId")
|
||||
throw MXCryptoError.Base(MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID, MXCryptoError.UNKNOWN_INBOUND_SESSION_ID_REASON)
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Copyright (c) 2020 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.internal.crypto
|
||||
|
||||
interface OutgoingGossipingRequest {
|
||||
var recipients: Map<String, List<String>>
|
||||
var requestId: String
|
||||
var state: OutgoingGossipingRequestState
|
||||
// transaction id for the cancellation, if any
|
||||
// var cancellationTxnId: String?
|
||||
}
|
@ -0,0 +1,189 @@
|
||||
/*
|
||||
* 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 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.session.events.model.LocalEcho
|
||||
import im.vector.matrix.android.api.util.Cancelable
|
||||
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.di.WorkManagerProvider
|
||||
import im.vector.matrix.android.internal.session.SessionScope
|
||||
import im.vector.matrix.android.internal.util.CancelableWork
|
||||
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
|
||||
import im.vector.matrix.android.internal.worker.WorkerParamsFactory
|
||||
import im.vector.matrix.android.internal.worker.startChain
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import java.util.concurrent.TimeUnit
|
||||
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 workManagerProvider: WorkManagerProvider) {
|
||||
|
||||
/**
|
||||
* 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("## 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("## sendOutgoingRoomKeyRequest() : 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("## cancelRoomKeyRequest() Unknown request")
|
||||
}
|
||||
|
||||
sendOutgoingRoomKeyRequestCancellation(req, andResend)
|
||||
}
|
||||
|
||||
/**
|
||||
* Send the outgoing key request.
|
||||
*
|
||||
* @param request the request
|
||||
*/
|
||||
private fun sendOutgoingGossipingRequest(request: OutgoingGossipingRequest) {
|
||||
Timber.v("## 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 = createWork<SendGossipRequestWorker>(WorkerParamsFactory.toData(params), true)
|
||||
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 = createWork<CancelGossipRequestWorker>(WorkerParamsFactory.toData(params), true)
|
||||
postWork(workRequest)
|
||||
|
||||
if (resend) {
|
||||
val reSendParams = SendGossipRequestWorker.Params(
|
||||
sessionId = sessionId,
|
||||
keyShareRequest = request.copy(requestId = LocalEcho.createLocalEchoId())
|
||||
)
|
||||
val reSendWorkRequest = createWork<SendGossipRequestWorker>(WorkerParamsFactory.toData(reSendParams), true)
|
||||
postWork(reSendWorkRequest)
|
||||
}
|
||||
}
|
||||
|
||||
private 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()
|
||||
}
|
||||
|
||||
private fun postWork(workRequest: OneTimeWorkRequest, policy: ExistingWorkPolicy = ExistingWorkPolicy.APPEND): Cancelable {
|
||||
workManagerProvider.workManager
|
||||
.beginUniqueWork(this::class.java.name, policy, workRequest)
|
||||
.enqueue()
|
||||
|
||||
return CancelableWork(workManagerProvider.workManager, workRequest.id)
|
||||
}
|
||||
}
|
@ -17,22 +17,26 @@
|
||||
|
||||
package im.vector.matrix.android.internal.crypto
|
||||
|
||||
import com.squareup.moshi.JsonClass
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody
|
||||
|
||||
/**
|
||||
* Represents an outgoing room key request
|
||||
*/
|
||||
class OutgoingRoomKeyRequest(
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class OutgoingRoomKeyRequest(
|
||||
// RequestBody
|
||||
var requestBody: RoomKeyRequestBody?, // list of recipients for the request
|
||||
var recipients: List<Map<String, String>>, // Unique id for this request. Used for both
|
||||
var requestBody: RoomKeyRequestBody?,
|
||||
// list of recipients for the request
|
||||
override var recipients: Map<String, List<String>>,
|
||||
// Unique id for this request. Used for both
|
||||
// an id within the request for later pairing with a cancellation, and for
|
||||
// the transaction id when sending the to_device messages to our local
|
||||
var requestId: String, // current state of this request
|
||||
var state: RequestState) {
|
||||
|
||||
// transaction id for the cancellation, if any
|
||||
var cancellationTxnId: String? = null
|
||||
override var requestId: String, // current state of this request
|
||||
override var state: OutgoingGossipingRequestState
|
||||
// transaction id for the cancellation, if any
|
||||
// override var cancellationTxnId: String? = null
|
||||
) : OutgoingGossipingRequest {
|
||||
|
||||
/**
|
||||
* Used only for log.
|
||||
@ -53,66 +57,4 @@ class OutgoingRoomKeyRequest(
|
||||
get() = if (null != requestBody) {
|
||||
requestBody!!.sessionId
|
||||
} else null
|
||||
|
||||
/**
|
||||
* possible states for a room key request
|
||||
*
|
||||
*
|
||||
* The state machine looks like:
|
||||
* <pre>
|
||||
*
|
||||
* |
|
||||
* V
|
||||
* UNSENT -----------------------------+
|
||||
* | |
|
||||
* | (send successful) | (cancellation requested)
|
||||
* V |
|
||||
* SENT |
|
||||
* |-------------------------------- | --------------+
|
||||
* | | |
|
||||
* | | | (cancellation requested with intent
|
||||
* | | | to resend a new request)
|
||||
* | (cancellation requested) | |
|
||||
* V | V
|
||||
* CANCELLATION_PENDING | CANCELLATION_PENDING_AND_WILL_RESEND
|
||||
* | | |
|
||||
* | (cancellation sent) | | (cancellation sent. Create new request
|
||||
* | | | in the UNSENT state)
|
||||
* V | |
|
||||
* (deleted) <---------------------------+----------------+
|
||||
* </pre>
|
||||
*/
|
||||
|
||||
enum class RequestState {
|
||||
/**
|
||||
* request not yet sent
|
||||
*/
|
||||
UNSENT,
|
||||
/**
|
||||
* request sent, awaiting reply
|
||||
*/
|
||||
SENT,
|
||||
/**
|
||||
* reply received, cancellation not yet sent
|
||||
*/
|
||||
CANCELLATION_PENDING,
|
||||
/**
|
||||
* Cancellation not yet sent, once sent, a new request will be done
|
||||
*/
|
||||
CANCELLATION_PENDING_AND_WILL_RESEND,
|
||||
/**
|
||||
* sending failed
|
||||
*/
|
||||
FAILED;
|
||||
|
||||
companion object {
|
||||
fun from(state: Int) = when (state) {
|
||||
0 -> UNSENT
|
||||
1 -> SENT
|
||||
2 -> CANCELLATION_PENDING
|
||||
3 -> CANCELLATION_PENDING_AND_WILL_RESEND
|
||||
else /*4*/ -> FAILED
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,320 +0,0 @@
|
||||
/*
|
||||
* Copyright 2016 OpenMarket Ltd
|
||||
* Copyright 2018 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.internal.crypto
|
||||
|
||||
import im.vector.matrix.android.api.MatrixCallback
|
||||
import im.vector.matrix.android.api.session.events.model.EventType
|
||||
import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyShareCancellation
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyShareRequest
|
||||
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
|
||||
import im.vector.matrix.android.internal.crypto.tasks.SendToDeviceTask
|
||||
import im.vector.matrix.android.internal.session.SessionScope
|
||||
import im.vector.matrix.android.internal.task.TaskExecutor
|
||||
import im.vector.matrix.android.internal.task.TaskThread
|
||||
import im.vector.matrix.android.internal.task.configureWith
|
||||
import im.vector.matrix.android.internal.util.createBackgroundHandler
|
||||
import timber.log.Timber
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import javax.inject.Inject
|
||||
|
||||
@SessionScope
|
||||
internal class OutgoingRoomKeyRequestManager @Inject constructor(
|
||||
private val cryptoStore: IMXCryptoStore,
|
||||
private val sendToDeviceTask: SendToDeviceTask,
|
||||
private val taskExecutor: TaskExecutor) {
|
||||
|
||||
// running
|
||||
private var isClientRunning: Boolean = false
|
||||
|
||||
// transaction counter
|
||||
private var txnCtr: Int = 0
|
||||
|
||||
// sanity check to ensure that we don't end up with two concurrent runs
|
||||
// of sendOutgoingRoomKeyRequestsTimer
|
||||
private val sendOutgoingRoomKeyRequestsRunning = AtomicBoolean(false)
|
||||
|
||||
/**
|
||||
* Called when the client is started. Sets background processes running.
|
||||
*/
|
||||
fun start() {
|
||||
isClientRunning = true
|
||||
startTimer()
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the client is stopped. Stops any running background processes.
|
||||
*/
|
||||
fun stop() {
|
||||
isClientRunning = false
|
||||
stopTimer()
|
||||
}
|
||||
|
||||
/**
|
||||
* Make up a new transaction id
|
||||
*
|
||||
* @return {string} a new, unique, transaction id
|
||||
*/
|
||||
private fun makeTxnId(): String {
|
||||
return "m" + System.currentTimeMillis() + "." + txnCtr++
|
||||
}
|
||||
|
||||
/**
|
||||
* Send off a room key request, if we haven't already done so.
|
||||
*
|
||||
*
|
||||
* The `requestBody` is compared (with a deep-equality check) against
|
||||
* previous queued or sent requests and if it matches, no change is made.
|
||||
* Otherwise, a request is added to the pending list, and a job is started
|
||||
* in the background to send it.
|
||||
*
|
||||
* @param requestBody requestBody
|
||||
* @param recipients recipients
|
||||
*/
|
||||
fun sendRoomKeyRequest(requestBody: RoomKeyRequestBody?, recipients: List<Map<String, String>>) {
|
||||
val req = cryptoStore.getOrAddOutgoingRoomKeyRequest(
|
||||
OutgoingRoomKeyRequest(requestBody, recipients, makeTxnId(), OutgoingRoomKeyRequest.RequestState.UNSENT))
|
||||
|
||||
if (req?.state == OutgoingRoomKeyRequest.RequestState.UNSENT) {
|
||||
startTimer()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel room key requests, if any match the given details
|
||||
*
|
||||
* @param requestBody requestBody
|
||||
*/
|
||||
fun cancelRoomKeyRequest(requestBody: RoomKeyRequestBody) {
|
||||
BACKGROUND_HANDLER.post {
|
||||
cancelRoomKeyRequest(requestBody, false)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel room key requests, if any match the given details, and resend
|
||||
*
|
||||
* @param requestBody requestBody
|
||||
*/
|
||||
fun resendRoomKeyRequest(requestBody: RoomKeyRequestBody) {
|
||||
BACKGROUND_HANDLER.post {
|
||||
cancelRoomKeyRequest(requestBody, true)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel room key requests, if any match the given details, and resend
|
||||
*
|
||||
* @param requestBody requestBody
|
||||
* @param andResend true to resend the key request
|
||||
*/
|
||||
private fun cancelRoomKeyRequest(requestBody: RoomKeyRequestBody, andResend: Boolean) {
|
||||
val req = cryptoStore.getOutgoingRoomKeyRequest(requestBody)
|
||||
?: // no request was made for this key
|
||||
return
|
||||
|
||||
Timber.v("cancelRoomKeyRequest: requestId: " + req.requestId + " state: " + req.state + " andResend: " + andResend)
|
||||
|
||||
when (req.state) {
|
||||
OutgoingRoomKeyRequest.RequestState.CANCELLATION_PENDING,
|
||||
OutgoingRoomKeyRequest.RequestState.CANCELLATION_PENDING_AND_WILL_RESEND -> {
|
||||
// nothing to do here
|
||||
}
|
||||
OutgoingRoomKeyRequest.RequestState.UNSENT,
|
||||
OutgoingRoomKeyRequest.RequestState.FAILED -> {
|
||||
Timber.v("## cancelRoomKeyRequest() : deleting unnecessary room key request for $requestBody")
|
||||
cryptoStore.deleteOutgoingRoomKeyRequest(req.requestId)
|
||||
}
|
||||
OutgoingRoomKeyRequest.RequestState.SENT -> {
|
||||
if (andResend) {
|
||||
req.state = OutgoingRoomKeyRequest.RequestState.CANCELLATION_PENDING_AND_WILL_RESEND
|
||||
} else {
|
||||
req.state = OutgoingRoomKeyRequest.RequestState.CANCELLATION_PENDING
|
||||
}
|
||||
req.cancellationTxnId = makeTxnId()
|
||||
cryptoStore.updateOutgoingRoomKeyRequest(req)
|
||||
sendOutgoingRoomKeyRequestCancellation(req)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the background timer to send queued requests, if the timer isn't already running.
|
||||
*/
|
||||
private fun startTimer() {
|
||||
if (sendOutgoingRoomKeyRequestsRunning.get()) {
|
||||
return
|
||||
}
|
||||
BACKGROUND_HANDLER.postDelayed(Runnable {
|
||||
if (sendOutgoingRoomKeyRequestsRunning.get()) {
|
||||
Timber.v("## startTimer() : RoomKeyRequestSend already in progress!")
|
||||
return@Runnable
|
||||
}
|
||||
|
||||
sendOutgoingRoomKeyRequestsRunning.set(true)
|
||||
sendOutgoingRoomKeyRequests()
|
||||
}, SEND_KEY_REQUESTS_DELAY_MS.toLong())
|
||||
}
|
||||
|
||||
private fun stopTimer() {
|
||||
BACKGROUND_HANDLER.removeCallbacksAndMessages(null)
|
||||
}
|
||||
|
||||
// look for and send any queued requests. Runs itself recursively until
|
||||
// there are no more requests, or there is an error (in which case, the
|
||||
// timer will be restarted before the promise resolves).
|
||||
private fun sendOutgoingRoomKeyRequests() {
|
||||
if (!isClientRunning) {
|
||||
sendOutgoingRoomKeyRequestsRunning.set(false)
|
||||
return
|
||||
}
|
||||
|
||||
Timber.v("## sendOutgoingRoomKeyRequests() : Looking for queued outgoing room key requests")
|
||||
val outgoingRoomKeyRequest = cryptoStore.getOutgoingRoomKeyRequestByState(
|
||||
setOf(OutgoingRoomKeyRequest.RequestState.UNSENT,
|
||||
OutgoingRoomKeyRequest.RequestState.CANCELLATION_PENDING,
|
||||
OutgoingRoomKeyRequest.RequestState.CANCELLATION_PENDING_AND_WILL_RESEND))
|
||||
|
||||
if (null == outgoingRoomKeyRequest) {
|
||||
Timber.v("## sendOutgoingRoomKeyRequests() : No more outgoing room key requests")
|
||||
sendOutgoingRoomKeyRequestsRunning.set(false)
|
||||
return
|
||||
}
|
||||
|
||||
if (OutgoingRoomKeyRequest.RequestState.UNSENT === outgoingRoomKeyRequest.state) {
|
||||
sendOutgoingRoomKeyRequest(outgoingRoomKeyRequest)
|
||||
} else {
|
||||
sendOutgoingRoomKeyRequestCancellation(outgoingRoomKeyRequest)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send the outgoing key request.
|
||||
*
|
||||
* @param request the request
|
||||
*/
|
||||
private fun sendOutgoingRoomKeyRequest(request: OutgoingRoomKeyRequest) {
|
||||
Timber.v("## sendOutgoingRoomKeyRequest() : Requesting keys " + request.requestBody
|
||||
+ " from " + request.recipients + " id " + request.requestId)
|
||||
|
||||
val requestMessage = RoomKeyShareRequest(
|
||||
requestingDeviceId = cryptoStore.getDeviceId(),
|
||||
requestId = request.requestId,
|
||||
body = request.requestBody
|
||||
)
|
||||
|
||||
sendMessageToDevices(requestMessage, request.recipients, request.requestId, object : MatrixCallback<Unit> {
|
||||
private fun onDone(state: OutgoingRoomKeyRequest.RequestState) {
|
||||
if (request.state !== OutgoingRoomKeyRequest.RequestState.UNSENT) {
|
||||
Timber.v("## sendOutgoingRoomKeyRequest() : Cannot update room key request from UNSENT as it was already updated to ${request.state}")
|
||||
} else {
|
||||
request.state = state
|
||||
cryptoStore.updateOutgoingRoomKeyRequest(request)
|
||||
}
|
||||
|
||||
sendOutgoingRoomKeyRequestsRunning.set(false)
|
||||
startTimer()
|
||||
}
|
||||
|
||||
override fun onSuccess(data: Unit) {
|
||||
Timber.v("## sendOutgoingRoomKeyRequest succeed")
|
||||
onDone(OutgoingRoomKeyRequest.RequestState.SENT)
|
||||
}
|
||||
|
||||
override fun onFailure(failure: Throwable) {
|
||||
Timber.e("## sendOutgoingRoomKeyRequest failed")
|
||||
onDone(OutgoingRoomKeyRequest.RequestState.FAILED)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a OutgoingRoomKeyRequest, cancel it and delete the request record
|
||||
*
|
||||
* @param request the request
|
||||
*/
|
||||
private fun sendOutgoingRoomKeyRequestCancellation(request: OutgoingRoomKeyRequest) {
|
||||
Timber.v("## sendOutgoingRoomKeyRequestCancellation() : Sending cancellation for key request for " + request.requestBody
|
||||
+ " to " + request.recipients
|
||||
+ " cancellation id " + request.cancellationTxnId)
|
||||
|
||||
val roomKeyShareCancellation = RoomKeyShareCancellation(
|
||||
requestingDeviceId = cryptoStore.getDeviceId(),
|
||||
requestId = request.cancellationTxnId
|
||||
)
|
||||
|
||||
sendMessageToDevices(roomKeyShareCancellation, request.recipients, request.cancellationTxnId, object : MatrixCallback<Unit> {
|
||||
private fun onDone() {
|
||||
cryptoStore.deleteOutgoingRoomKeyRequest(request.requestId)
|
||||
sendOutgoingRoomKeyRequestsRunning.set(false)
|
||||
startTimer()
|
||||
}
|
||||
|
||||
override fun onSuccess(data: Unit) {
|
||||
Timber.v("## sendOutgoingRoomKeyRequestCancellation() : done")
|
||||
val resend = request.state === OutgoingRoomKeyRequest.RequestState.CANCELLATION_PENDING_AND_WILL_RESEND
|
||||
|
||||
onDone()
|
||||
|
||||
// Resend the request with a new ID
|
||||
if (resend) {
|
||||
sendRoomKeyRequest(request.requestBody, request.recipients)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onFailure(failure: Throwable) {
|
||||
Timber.e("## sendOutgoingRoomKeyRequestCancellation failed")
|
||||
onDone()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a SendToDeviceObject to a list of recipients
|
||||
*
|
||||
* @param message the message
|
||||
* @param recipients the recipients.
|
||||
* @param transactionId the transaction id
|
||||
* @param callback the asynchronous callback.
|
||||
*/
|
||||
private fun sendMessageToDevices(message: Any,
|
||||
recipients: List<Map<String, String>>,
|
||||
transactionId: String?,
|
||||
callback: MatrixCallback<Unit>) {
|
||||
val contentMap = MXUsersDevicesMap<Any>()
|
||||
|
||||
for (recipient in recipients) {
|
||||
// TODO Change this two hard coded key to something better
|
||||
contentMap.setObject(recipient["userId"], recipient["deviceId"], message)
|
||||
}
|
||||
sendToDeviceTask
|
||||
.configureWith(SendToDeviceTask.Params(EventType.ROOM_KEY_REQUEST, contentMap, transactionId)) {
|
||||
this.callback = callback
|
||||
this.callbackThread = TaskThread.CALLER
|
||||
this.executionThread = TaskThread.CALLER
|
||||
}
|
||||
.executeBy(taskExecutor)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val SEND_KEY_REQUESTS_DELAY_MS = 500
|
||||
|
||||
private val BACKGROUND_HANDLER = createBackgroundHandler("OutgoingRoomKeyRequest")
|
||||
}
|
||||
}
|
@ -0,0 +1,38 @@
|
||||
/*
|
||||
* Copyright (c) 2020 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.internal.crypto
|
||||
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
/**
|
||||
* Represents an outgoing room key request
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
class OutgoingSecretRequest(
|
||||
// Secret Name
|
||||
val secretName: String?,
|
||||
// list of recipients for the request
|
||||
override var recipients: Map<String, List<String>>,
|
||||
// Unique id for this request. Used for both
|
||||
// an id within the request for later pairing with a cancellation, and for
|
||||
// the transaction id when sending the to_device messages to our local
|
||||
override var requestId: String,
|
||||
// current state of this request
|
||||
override var state: OutgoingGossipingRequestState) : OutgoingGossipingRequest {
|
||||
|
||||
// transaction id for the cancellation, if any
|
||||
}
|
@ -0,0 +1,148 @@
|
||||
/*
|
||||
* Copyright (c) 2020 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.internal.crypto
|
||||
|
||||
import android.content.Context
|
||||
import androidx.work.CoroutineWorker
|
||||
import androidx.work.Data
|
||||
import androidx.work.WorkerParameters
|
||||
import com.squareup.moshi.JsonClass
|
||||
import im.vector.matrix.android.api.auth.data.Credentials
|
||||
import im.vector.matrix.android.api.failure.shouldBeRetried
|
||||
import im.vector.matrix.android.api.session.events.model.Event
|
||||
import im.vector.matrix.android.api.session.events.model.EventType
|
||||
import im.vector.matrix.android.api.session.events.model.LocalEcho
|
||||
import im.vector.matrix.android.api.session.events.model.toContent
|
||||
import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.GossipingToDeviceObject
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyShareRequest
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.SecretShareRequest
|
||||
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
|
||||
import im.vector.matrix.android.internal.crypto.tasks.SendToDeviceTask
|
||||
import im.vector.matrix.android.internal.worker.WorkerParamsFactory
|
||||
import im.vector.matrix.android.internal.worker.getSessionComponent
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class SendGossipRequestWorker(context: Context,
|
||||
params: WorkerParameters)
|
||||
: CoroutineWorker(context, params) {
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
internal data class Params(
|
||||
val sessionId: String,
|
||||
val keyShareRequest: OutgoingRoomKeyRequest? = null,
|
||||
val secretShareRequest: OutgoingSecretRequest? = null
|
||||
)
|
||||
|
||||
@Inject lateinit var sendToDeviceTask: SendToDeviceTask
|
||||
@Inject lateinit var cryptoStore: IMXCryptoStore
|
||||
@Inject lateinit var eventBus: EventBus
|
||||
@Inject lateinit var credentials: Credentials
|
||||
|
||||
override suspend fun doWork(): Result {
|
||||
val errorOutputData = Data.Builder().putBoolean("failed", true).build()
|
||||
val params = WorkerParamsFactory.fromData<Params>(inputData)
|
||||
?: return Result.success(errorOutputData)
|
||||
|
||||
val sessionComponent = getSessionComponent(params.sessionId)
|
||||
?: return Result.success(errorOutputData).also {
|
||||
// TODO, can this happen? should I update local echo?
|
||||
Timber.e("Unknown Session, cannot send message, sessionId: ${params.sessionId}")
|
||||
}
|
||||
sessionComponent.inject(this)
|
||||
|
||||
val localId = LocalEcho.createLocalEchoId()
|
||||
val contentMap = MXUsersDevicesMap<Any>()
|
||||
val eventType: String
|
||||
val requestId: String
|
||||
when {
|
||||
params.keyShareRequest != null -> {
|
||||
eventType = EventType.ROOM_KEY_REQUEST
|
||||
requestId = params.keyShareRequest.requestId
|
||||
val toDeviceContent = RoomKeyShareRequest(
|
||||
requestingDeviceId = credentials.deviceId,
|
||||
requestId = params.keyShareRequest.requestId,
|
||||
action = GossipingToDeviceObject.ACTION_SHARE_REQUEST,
|
||||
body = params.keyShareRequest.requestBody
|
||||
)
|
||||
cryptoStore.saveGossipingEvent(Event(
|
||||
type = eventType,
|
||||
content = toDeviceContent.toContent(),
|
||||
senderId = credentials.userId
|
||||
).also {
|
||||
it.ageLocalTs = System.currentTimeMillis()
|
||||
})
|
||||
|
||||
params.keyShareRequest.recipients.forEach { userToDeviceMap ->
|
||||
userToDeviceMap.value.forEach { deviceId ->
|
||||
contentMap.setObject(userToDeviceMap.key, deviceId, toDeviceContent)
|
||||
}
|
||||
}
|
||||
}
|
||||
params.secretShareRequest != null -> {
|
||||
eventType = EventType.REQUEST_SECRET
|
||||
requestId = params.secretShareRequest.requestId
|
||||
val toDeviceContent = SecretShareRequest(
|
||||
requestingDeviceId = credentials.deviceId,
|
||||
requestId = params.secretShareRequest.requestId,
|
||||
action = GossipingToDeviceObject.ACTION_SHARE_REQUEST,
|
||||
secretName = params.secretShareRequest.secretName
|
||||
)
|
||||
|
||||
cryptoStore.saveGossipingEvent(Event(
|
||||
type = eventType,
|
||||
content = toDeviceContent.toContent(),
|
||||
senderId = credentials.userId
|
||||
).also {
|
||||
it.ageLocalTs = System.currentTimeMillis()
|
||||
})
|
||||
|
||||
params.secretShareRequest.recipients.forEach { userToDeviceMap ->
|
||||
userToDeviceMap.value.forEach { deviceId ->
|
||||
contentMap.setObject(userToDeviceMap.key, deviceId, toDeviceContent)
|
||||
}
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
return Result.success(errorOutputData).also {
|
||||
Timber.e("Unknown empty gossiping request: $params")
|
||||
}
|
||||
}
|
||||
}
|
||||
try {
|
||||
cryptoStore.updateOutgoingGossipingRequestState(requestId, OutgoingGossipingRequestState.SENDING)
|
||||
sendToDeviceTask.execute(
|
||||
SendToDeviceTask.Params(
|
||||
eventType = eventType,
|
||||
contentMap = contentMap,
|
||||
transactionId = localId
|
||||
)
|
||||
)
|
||||
cryptoStore.updateOutgoingGossipingRequestState(requestId, OutgoingGossipingRequestState.SENT)
|
||||
return Result.success()
|
||||
} catch (exception: Throwable) {
|
||||
return if (exception.shouldBeRetried()) {
|
||||
Result.retry()
|
||||
} else {
|
||||
cryptoStore.updateOutgoingGossipingRequestState(requestId, OutgoingGossipingRequestState.FAILED_TO_SEND)
|
||||
Result.success(errorOutputData)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,74 @@
|
||||
/*
|
||||
* 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.EventType
|
||||
import im.vector.matrix.android.internal.crypto.actions.MessageEncrypter
|
||||
import im.vector.matrix.android.internal.crypto.algorithms.olm.MXOlmDecryptionFactory
|
||||
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.util.MatrixCoroutineDispatchers
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class ShareSecretCryptoProvider @Inject constructor(
|
||||
val messageEncrypter: MessageEncrypter,
|
||||
val sendToDeviceTask: SendToDeviceTask,
|
||||
val deviceListManager: DeviceListManager,
|
||||
private val olmDecryptionFactory: MXOlmDecryptionFactory,
|
||||
val cryptoCoroutineScope: CoroutineScope,
|
||||
val cryptoStore: IMXCryptoStore,
|
||||
val coroutineDispatchers: MatrixCoroutineDispatchers
|
||||
) {
|
||||
fun shareSecretWithDevice(request: IncomingSecretShareRequest, secretValue: String) {
|
||||
val userId = request.userId ?: return
|
||||
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
|
||||
runCatching { deviceListManager.downloadKeys(listOf(userId), false) }
|
||||
.mapCatching {
|
||||
val deviceId = request.deviceId
|
||||
val deviceInfo = cryptoStore.getUserDevice(userId, deviceId ?: "") ?: throw RuntimeException()
|
||||
|
||||
Timber.i("## shareSecretWithDevice() : sharing secret ${request.secretName} with device $userId:$deviceId")
|
||||
|
||||
val payloadJson = mutableMapOf<String, Any>("type" to EventType.SEND_SECRET)
|
||||
payloadJson["content"] = SecretSendEventContent(
|
||||
requestId = request.requestId ?: "",
|
||||
secretValue = secretValue
|
||||
)
|
||||
|
||||
val encodedPayload = messageEncrypter.encryptMessage(payloadJson, listOf(deviceInfo))
|
||||
val sendToDeviceMap = MXUsersDevicesMap<Any>()
|
||||
sendToDeviceMap.setObject(userId, deviceId, encodedPayload)
|
||||
Timber.i("## shareSecretWithDevice() : sending to $userId:$deviceId")
|
||||
val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, sendToDeviceMap)
|
||||
sendToDeviceTask.execute(sendToDeviceParams)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun decryptEvent(event: Event): MXEventDecryptionResult {
|
||||
return runBlocking(coroutineDispatchers.crypto) {
|
||||
olmDecryptionFactory.create().decryptEvent(event, ShareSecretCryptoProvider::class.java.name ?: "")
|
||||
}
|
||||
}
|
||||
}
|
@ -20,7 +20,7 @@ import androidx.annotation.WorkerThread
|
||||
import im.vector.matrix.android.api.listeners.ProgressListener
|
||||
import im.vector.matrix.android.internal.crypto.MXOlmDevice
|
||||
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.model.ImportRoomKeysResult
|
||||
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,
|
||||
private val roomDecryptorProvider: RoomDecryptorProvider,
|
||||
private val outgoingRoomKeyRequestManager: OutgoingRoomKeyRequestManager,
|
||||
private val outgoingGossipingRequestManager: OutgoingGossipingRequestManager,
|
||||
private val cryptoStore: IMXCryptoStore) {
|
||||
|
||||
/**
|
||||
@ -73,7 +73,7 @@ internal class MegolmSessionDataImporter @Inject constructor(private val olmDevi
|
||||
sessionId = megolmSessionData.sessionId
|
||||
)
|
||||
|
||||
outgoingRoomKeyRequestManager.cancelRoomKeyRequest(roomKeyRequestBody)
|
||||
outgoingGossipingRequestManager.cancelRoomKeyRequest(roomKeyRequestBody)
|
||||
|
||||
// Have another go at decrypting events sent with this session
|
||||
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.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.keysbackup.DefaultKeysBackupService
|
||||
|
||||
@ -65,4 +66,8 @@ internal interface IMXDecrypting {
|
||||
* @param request keyRequest
|
||||
*/
|
||||
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.MXOlmDevice
|
||||
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.MessageEncrypter
|
||||
import im.vector.matrix.android.internal.crypto.algorithms.IMXDecrypting
|
||||
@ -46,7 +46,7 @@ import timber.log.Timber
|
||||
internal class MXMegolmDecryption(private val userId: String,
|
||||
private val olmDevice: MXOlmDevice,
|
||||
private val deviceListManager: DeviceListManager,
|
||||
private val outgoingRoomKeyRequestManager: OutgoingRoomKeyRequestManager,
|
||||
private val outgoingGossipingRequestManager: OutgoingGossipingRequestManager,
|
||||
private val messageEncrypter: MessageEncrypter,
|
||||
private val ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction,
|
||||
private val cryptoStore: IMXCryptoStore,
|
||||
@ -144,23 +144,23 @@ internal class MXMegolmDecryption(private val userId: String,
|
||||
*
|
||||
* @param event the event
|
||||
*/
|
||||
private fun requestKeysForEvent(event: Event) {
|
||||
val sender = event.senderId!!
|
||||
val encryptedEventContent = event.content.toModel<EncryptedEventContent>()!!
|
||||
override fun requestKeysForEvent(event: Event) {
|
||||
val sender = event.senderId ?: return
|
||||
val encryptedEventContent = event.content.toModel<EncryptedEventContent>()
|
||||
val senderDevice = encryptedEventContent?.deviceId ?: return
|
||||
|
||||
val recipients = ArrayList<Map<String, String>>()
|
||||
|
||||
val selfMap = HashMap<String, String>()
|
||||
// TODO Replace this hard coded keys (see OutgoingRoomKeyRequestManager)
|
||||
selfMap["userId"] = userId
|
||||
selfMap["deviceId"] = "*"
|
||||
recipients.add(selfMap)
|
||||
|
||||
if (sender != userId) {
|
||||
val senderMap = HashMap<String, String>()
|
||||
senderMap["userId"] = sender
|
||||
senderMap["deviceId"] = encryptedEventContent.deviceId!!
|
||||
recipients.add(senderMap)
|
||||
val recipients = if (event.senderId != userId) {
|
||||
mapOf(
|
||||
userId to listOf("*")
|
||||
)
|
||||
} else {
|
||||
// for the case where you share the key with a device that has a broken olm session
|
||||
// The other user might Re-shares a megolm session key with devices if the key has already been
|
||||
// sent to them.
|
||||
mapOf(
|
||||
userId to listOf("*"),
|
||||
sender to listOf(senderDevice)
|
||||
)
|
||||
}
|
||||
|
||||
val requestBody = RoomKeyRequestBody(
|
||||
@ -170,7 +170,7 @@ internal class MXMegolmDecryption(private val userId: String,
|
||||
sessionId = encryptedEventContent.sessionId
|
||||
)
|
||||
|
||||
outgoingRoomKeyRequestManager.sendRoomKeyRequest(requestBody, recipients)
|
||||
outgoingGossipingRequestManager.sendRoomKeyRequest(requestBody, recipients)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -271,7 +271,7 @@ internal class MXMegolmDecryption(private val userId: String,
|
||||
senderKey = senderKey
|
||||
)
|
||||
|
||||
outgoingRoomKeyRequestManager.cancelRoomKeyRequest(content)
|
||||
outgoingGossipingRequestManager.cancelRoomKeyRequest(content)
|
||||
|
||||
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.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.MessageEncrypter
|
||||
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
|
||||
@ -32,7 +32,7 @@ internal class MXMegolmDecryptionFactory @Inject constructor(
|
||||
@UserId private val userId: String,
|
||||
private val olmDevice: MXOlmDevice,
|
||||
private val deviceListManager: DeviceListManager,
|
||||
private val outgoingRoomKeyRequestManager: OutgoingRoomKeyRequestManager,
|
||||
private val outgoingGossipingRequestManager: OutgoingGossipingRequestManager,
|
||||
private val messageEncrypter: MessageEncrypter,
|
||||
private val ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction,
|
||||
private val cryptoStore: IMXCryptoStore,
|
||||
@ -46,7 +46,7 @@ internal class MXMegolmDecryptionFactory @Inject constructor(
|
||||
userId,
|
||||
olmDevice,
|
||||
deviceListManager,
|
||||
outgoingRoomKeyRequestManager,
|
||||
outgoingGossipingRequestManager,
|
||||
messageEncrypter,
|
||||
ensureOlmSessionsForDevicesAction,
|
||||
cryptoStore,
|
||||
|
@ -210,4 +210,8 @@ internal class MXOlmDecryption(
|
||||
|
||||
return res["payload"]
|
||||
}
|
||||
|
||||
override fun requestKeysForEvent(event: Event) {
|
||||
// nop
|
||||
}
|
||||
}
|
||||
|
@ -25,6 +25,7 @@ import im.vector.matrix.android.api.util.Optional
|
||||
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.OutgoingGossipingRequestManager
|
||||
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
|
||||
@ -61,6 +62,7 @@ internal class DefaultCrossSigningService @Inject constructor(
|
||||
private val taskExecutor: TaskExecutor,
|
||||
private val coroutineDispatchers: MatrixCoroutineDispatchers,
|
||||
private val cryptoCoroutineScope: CoroutineScope,
|
||||
private val outgoingGossipingRequestManager: OutgoingGossipingRequestManager,
|
||||
private val eventBus: EventBus) : CrossSigningService, DeviceListManager.UserDevicesUpdateListener {
|
||||
|
||||
private var olmUtility: OlmUtility? = null
|
||||
@ -297,6 +299,59 @@ internal class DefaultCrossSigningService @Inject constructor(
|
||||
cryptoStore.clearOtherUserTrust()
|
||||
}
|
||||
|
||||
override fun onSecretSSKGossip(sskPrivateKey: String) {
|
||||
Timber.i("## CrossSigning - onSecretSSKGossip")
|
||||
val mxCrossSigningInfo = getMyCrossSigningKeys() ?: return Unit.also {
|
||||
Timber.e("## CrossSigning - onSecretSSKGossip() received secret but public key is not known")
|
||||
}
|
||||
|
||||
sskPrivateKey.fromBase64()
|
||||
.let { privateKeySeed ->
|
||||
val pkSigning = OlmPkSigning()
|
||||
try {
|
||||
if (pkSigning.initWithSeed(privateKeySeed) == mxCrossSigningInfo.selfSigningKey()?.unpaddedBase64PublicKey) {
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSecretUSKGossip(uskPrivateKey: String) {
|
||||
Timber.i("## CrossSigning - onSecretUSKGossip")
|
||||
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?,
|
||||
uskKeyPrivateKey: String?,
|
||||
sskPrivateKey: String?
|
||||
@ -396,7 +451,7 @@ internal class DefaultCrossSigningService @Inject constructor(
|
||||
* Will not force a download of the key, but will verify signatures trust chain
|
||||
*/
|
||||
override fun checkUserTrust(otherUserId: String): UserTrustResult {
|
||||
Timber.d("## CrossSigning checkUserTrust for $otherUserId")
|
||||
Timber.v("## CrossSigning checkUserTrust for $otherUserId")
|
||||
if (otherUserId == userId) {
|
||||
return checkSelfTrust()
|
||||
}
|
||||
@ -547,97 +602,103 @@ internal class DefaultCrossSigningService @Inject constructor(
|
||||
}
|
||||
|
||||
override fun trustUser(otherUserId: String, callback: MatrixCallback<Unit>) {
|
||||
Timber.d("## CrossSigning - Mark user $userId as trusted ")
|
||||
// We should have this user keys
|
||||
val otherMasterKeys = getUserCrossSigningKeys(otherUserId)?.masterKey()
|
||||
if (otherMasterKeys == null) {
|
||||
callback.onFailure(Throwable("## CrossSigning - Other master signing key is not known"))
|
||||
return
|
||||
}
|
||||
val myKeys = getUserCrossSigningKeys(userId)
|
||||
if (myKeys == null) {
|
||||
callback.onFailure(Throwable("## CrossSigning - CrossSigning is not setup for this account"))
|
||||
return
|
||||
}
|
||||
val userPubKey = myKeys.userKey()?.unpaddedBase64PublicKey
|
||||
if (userPubKey == null || userPkSigning == null) {
|
||||
callback.onFailure(Throwable("## CrossSigning - Cannot sign from this account, privateKeyUnknown $userPubKey"))
|
||||
return
|
||||
}
|
||||
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
|
||||
Timber.d("## CrossSigning - Mark user $userId as trusted ")
|
||||
// We should have this user keys
|
||||
val otherMasterKeys = getUserCrossSigningKeys(otherUserId)?.masterKey()
|
||||
if (otherMasterKeys == null) {
|
||||
callback.onFailure(Throwable("## CrossSigning - Other master signing key is not known"))
|
||||
return@launch
|
||||
}
|
||||
val myKeys = getUserCrossSigningKeys(userId)
|
||||
if (myKeys == null) {
|
||||
callback.onFailure(Throwable("## CrossSigning - CrossSigning is not setup for this account"))
|
||||
return@launch
|
||||
}
|
||||
val userPubKey = myKeys.userKey()?.unpaddedBase64PublicKey
|
||||
if (userPubKey == null || userPkSigning == null) {
|
||||
callback.onFailure(Throwable("## CrossSigning - Cannot sign from this account, privateKeyUnknown $userPubKey"))
|
||||
return@launch
|
||||
}
|
||||
|
||||
// Sign the other MasterKey with our UserSigning key
|
||||
val newSignature = JsonCanonicalizer.getCanonicalJson(Map::class.java,
|
||||
otherMasterKeys.signalableJSONDictionary()).let { userPkSigning?.sign(it) }
|
||||
// Sign the other MasterKey with our UserSigning key
|
||||
val newSignature = JsonCanonicalizer.getCanonicalJson(Map::class.java,
|
||||
otherMasterKeys.signalableJSONDictionary()).let { userPkSigning?.sign(it) }
|
||||
|
||||
if (newSignature == null) {
|
||||
// race??
|
||||
callback.onFailure(Throwable("## CrossSigning - Failed to sign"))
|
||||
return
|
||||
if (newSignature == null) {
|
||||
// race??
|
||||
callback.onFailure(Throwable("## CrossSigning - Failed to sign"))
|
||||
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() {
|
||||
cryptoStore.markMyMasterKeyAsLocallyTrusted(true)
|
||||
checkSelfTrust()
|
||||
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
|
||||
cryptoStore.markMyMasterKeyAsLocallyTrusted(true)
|
||||
checkSelfTrust()
|
||||
}
|
||||
}
|
||||
|
||||
override fun trustDevice(deviceId: String, callback: MatrixCallback<Unit>) {
|
||||
// This device should be yours
|
||||
val device = cryptoStore.getUserDevice(userId, deviceId)
|
||||
if (device == null) {
|
||||
callback.onFailure(IllegalArgumentException("This device [$deviceId] is not known, or not yours"))
|
||||
return
|
||||
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
|
||||
// This device should be yours
|
||||
val device = cryptoStore.getUserDevice(userId, deviceId)
|
||||
if (device == null) {
|
||||
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 {
|
||||
@ -706,7 +767,7 @@ internal class DefaultCrossSigningService @Inject constructor(
|
||||
Timber.d("## CrossSigning - onUsersDeviceUpdate for ${userIds.size} users")
|
||||
userIds.forEach { otherUserId ->
|
||||
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())
|
||||
}
|
||||
|
||||
@ -714,7 +775,7 @@ internal class DefaultCrossSigningService @Inject constructor(
|
||||
val devices = cryptoStore.getUserDeviceList(otherUserId)
|
||||
devices?.forEach { device ->
|
||||
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())
|
||||
}
|
||||
|
||||
@ -736,6 +797,7 @@ internal class DefaultCrossSigningService @Inject constructor(
|
||||
// If it's me, recheck trust of all users and devices?
|
||||
val users = ArrayList<String>()
|
||||
if (otherUserId == userId && currentTrust != trusted) {
|
||||
// reRequestAllPendingRoomKeyRequest()
|
||||
cryptoStore.updateUsersTrust {
|
||||
users.add(it)
|
||||
checkUserTrust(it).isVerified()
|
||||
@ -744,11 +806,26 @@ internal class DefaultCrossSigningService @Inject constructor(
|
||||
users.forEach {
|
||||
cryptoStore.getUserDeviceList(it)?.forEach { device ->
|
||||
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())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
@ -33,7 +33,7 @@ data class CryptoDeviceInfo(
|
||||
) : CryptoInfo {
|
||||
|
||||
val isVerified: Boolean
|
||||
get() = trustLevel?.isVerified() ?: false
|
||||
get() = trustLevel?.isVerified() == true
|
||||
|
||||
val isUnknown: Boolean
|
||||
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
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
/**
|
||||
* Interface representing an room key action request
|
||||
* Note: this class cannot be abstract because of [org.matrix.androidsdk.core.JsonUtils.toRoomKeyShare]
|
||||
*/
|
||||
internal interface RoomKeyShare : SendToDeviceObject {
|
||||
interface GossipingToDeviceObject : SendToDeviceObject {
|
||||
|
||||
val action: String?
|
||||
|
||||
@ -32,3 +35,10 @@ internal interface RoomKeyShare : SendToDeviceObject {
|
||||
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
|
@ -18,6 +18,7 @@ package im.vector.matrix.android.internal.crypto.model.rest
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import im.vector.matrix.android.internal.di.MoshiProvider
|
||||
|
||||
/**
|
||||
* Class representing an room key request body content
|
||||
@ -35,4 +36,14 @@ data class RoomKeyRequestBody(
|
||||
|
||||
@Json(name = "session_id")
|
||||
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
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
internal data class RoomKeyShareRequest(
|
||||
data class RoomKeyShareRequest(
|
||||
@Json(name = "action")
|
||||
override val action: String? = RoomKeyShare.ACTION_SHARE_REQUEST,
|
||||
override val action: String? = GossipingToDeviceObject.ACTION_SHARE_REQUEST,
|
||||
|
||||
@Json(name = "requesting_device_id")
|
||||
override val requestingDeviceId: String? = null,
|
||||
@ -35,4 +35,4 @@ internal data class RoomKeyShareRequest(
|
||||
|
||||
@Json(name = "body")
|
||||
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.JsonClass
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.GossipingToDeviceObject.Companion.ACTION_SHARE_CANCELLATION
|
||||
|
||||
/**
|
||||
* Class representing a room key request cancellation content
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
internal data class RoomKeyShareCancellation(
|
||||
internal data class ShareRequestCancellation(
|
||||
@Json(name = "action")
|
||||
override val action: String? = RoomKeyShare.ACTION_SHARE_CANCELLATION,
|
||||
override val action: String? = ACTION_SHARE_CANCELLATION,
|
||||
|
||||
@Json(name = "requesting_device_id")
|
||||
override val requestingDeviceId: String? = null,
|
||||
|
||||
@Json(name = "request_id")
|
||||
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.SsssKeySpec
|
||||
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_CURVE25519_AES_SHA2
|
||||
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.tools.HkdfSha256
|
||||
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.util.MatrixCoroutineDispatchers
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
@ -55,7 +57,9 @@ import javax.inject.Inject
|
||||
import kotlin.experimental.and
|
||||
|
||||
internal class DefaultSharedSecretStorageService @Inject constructor(
|
||||
@UserId private val userId: String,
|
||||
private val accountDataService: AccountDataService,
|
||||
private val outgoingGossipingRequestManager: OutgoingGossipingRequestManager,
|
||||
private val coroutineDispatchers: MatrixCoroutineDispatchers,
|
||||
private val cryptoCoroutineScope: CoroutineScope
|
||||
) : SharedSecretStorageService {
|
||||
@ -429,4 +433,11 @@ internal class DefaultSharedSecretStorageService @Inject constructor(
|
||||
|
||||
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 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.internal.crypto.GossipingRequestState
|
||||
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.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.CryptoCrossSigningKey
|
||||
import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
|
||||
import im.vector.matrix.android.internal.crypto.model.OlmInboundGroupSessionWrapper
|
||||
@ -117,6 +121,10 @@ internal interface IMXCryptoStore {
|
||||
*/
|
||||
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.
|
||||
*
|
||||
@ -187,8 +195,8 @@ internal interface IMXCryptoStore {
|
||||
fun storeUserDevices(userId: String, devices: Map<String, CryptoDeviceInfo>?)
|
||||
|
||||
fun storeUserCrossSigningKeys(userId: String, masterKey: CryptoCrossSigningKey?,
|
||||
selfSigningKey: CryptoCrossSigningKey?,
|
||||
userSigningKey: CryptoCrossSigningKey?)
|
||||
selfSigningKey: CryptoCrossSigningKey?,
|
||||
userSigningKey: CryptoCrossSigningKey?)
|
||||
|
||||
/**
|
||||
* Retrieve the known devices for a user.
|
||||
@ -206,6 +214,7 @@ internal interface IMXCryptoStore {
|
||||
|
||||
// TODO temp
|
||||
fun getLiveDeviceList(): LiveData<List<CryptoDeviceInfo>>
|
||||
|
||||
/**
|
||||
* Store the crypto algorithm for a room.
|
||||
*
|
||||
@ -347,43 +356,13 @@ internal interface IMXCryptoStore {
|
||||
* @param request the request
|
||||
* @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?
|
||||
|
||||
/**
|
||||
* Look for room key requests by state.
|
||||
*
|
||||
* @param states the states
|
||||
* @return an OutgoingRoomKeyRequest or null
|
||||
*/
|
||||
fun getOutgoingRoomKeyRequestByState(states: Set<OutgoingRoomKeyRequest.RequestState>): OutgoingRoomKeyRequest?
|
||||
fun getOrAddOutgoingSecretShareRequest(secretName: String, recipients: Map<String, List<String>>): OutgoingSecretRequest?
|
||||
|
||||
/**
|
||||
* Update an existing outgoing request.
|
||||
*
|
||||
* @param request the request
|
||||
*/
|
||||
fun updateOutgoingRoomKeyRequest(request: OutgoingRoomKeyRequest)
|
||||
fun saveGossipingEvent(event: Event)
|
||||
|
||||
/**
|
||||
* 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)
|
||||
fun updateGossipingRequestState(request: IncomingShareRequestCommon, state: GossipingRequestState)
|
||||
|
||||
/**
|
||||
* Search an IncomingRoomKeyRequest
|
||||
@ -395,6 +374,8 @@ internal interface IMXCryptoStore {
|
||||
*/
|
||||
fun getIncomingRoomKeyRequest(userId: String, deviceId: String, requestId: String): IncomingRoomKeyRequest?
|
||||
|
||||
fun updateOutgoingGossipingRequestState(requestId: String, state: OutgoingGossipingRequestState)
|
||||
|
||||
fun addNewSessionListener(listener: NewSessionListener)
|
||||
|
||||
fun removeSessionListener(listener: NewSessionListener)
|
||||
@ -406,22 +387,34 @@ internal interface IMXCryptoStore {
|
||||
/**
|
||||
* Gets the current crosssigning info
|
||||
*/
|
||||
fun getMyCrossSigningInfo() : MXCrossSigningInfo?
|
||||
fun getMyCrossSigningInfo(): MXCrossSigningInfo?
|
||||
|
||||
fun setMyCrossSigningInfo(info: MXCrossSigningInfo?)
|
||||
|
||||
fun getCrossSigningInfo(userId: String) : MXCrossSigningInfo?
|
||||
fun getLiveCrossSigningInfo(userId: String) : LiveData<Optional<MXCrossSigningInfo>>
|
||||
fun getCrossSigningInfo(userId: String): MXCrossSigningInfo?
|
||||
fun getLiveCrossSigningInfo(userId: String): LiveData<Optional<MXCrossSigningInfo>>
|
||||
fun setCrossSigningInfo(userId: String, info: MXCrossSigningInfo?)
|
||||
|
||||
fun markMyMasterKeyAsLocallyTrusted(trusted: Boolean)
|
||||
|
||||
fun storePrivateKeysInfo(msk: String?, usk: String?, ssk: String?)
|
||||
fun getCrossSigningPrivateKeys() : PrivateKeysInfo?
|
||||
fun storeSSKPrivateKey(ssk: String?)
|
||||
fun storeUSKPrivateKey(usk: String?)
|
||||
|
||||
fun getCrossSigningPrivateKeys(): PrivateKeysInfo?
|
||||
|
||||
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 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>
|
||||
}
|
||||
|
@ -59,7 +59,12 @@ fun <T : RealmObject> doRealmQueryAndCopyList(realmConfiguration: RealmConfigura
|
||||
*/
|
||||
fun doRealmTransaction(realmConfiguration: RealmConfiguration, action: (Realm) -> Unit) {
|
||||
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 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.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.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.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.OutgoingGossipingRequestState
|
||||
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.model.CryptoCrossSigningKey
|
||||
import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
|
||||
@ -44,16 +53,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.DeviceInfoEntity
|
||||
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.IncomingRoomKeyRequestEntityFields
|
||||
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.IncomingGossipingRequestEntityFields
|
||||
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.OlmInboundGroupSessionEntity
|
||||
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.OlmSessionEntityFields
|
||||
import im.vector.matrix.android.internal.crypto.store.db.model.OutgoingRoomKeyRequestEntity
|
||||
import im.vector.matrix.android.internal.crypto.store.db.model.OutgoingRoomKeyRequestEntityFields
|
||||
import im.vector.matrix.android.internal.crypto.store.db.model.OutgoingGossipingRequestEntity
|
||||
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.UserEntity
|
||||
import im.vector.matrix.android.internal.crypto.store.db.model.UserEntityFields
|
||||
@ -62,7 +72,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.getById
|
||||
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.MoshiProvider
|
||||
import im.vector.matrix.android.internal.session.SessionScope
|
||||
import io.realm.Realm
|
||||
import io.realm.RealmConfiguration
|
||||
@ -359,7 +371,23 @@ internal class RealmCryptoStore @Inject constructor(
|
||||
doRealmTransaction(realmConfiguration) { realm ->
|
||||
realm.where<CryptoMetadataEntity>().findFirst()?.apply {
|
||||
xSignMasterPrivateKey = msk
|
||||
xSignUserPrivateKey = usk
|
||||
xSignSelfSignedPrivateKey = ssk
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
@ -797,131 +825,328 @@ internal class RealmCryptoStore @Inject constructor(
|
||||
}
|
||||
|
||||
override fun getOutgoingRoomKeyRequest(requestBody: RoomKeyRequestBody): OutgoingRoomKeyRequest? {
|
||||
return doRealmQueryAndCopy(realmConfiguration) {
|
||||
it.where<OutgoingRoomKeyRequestEntity>()
|
||||
.equalTo(OutgoingRoomKeyRequestEntityFields.REQUEST_BODY_ALGORITHM, requestBody.algorithm)
|
||||
.equalTo(OutgoingRoomKeyRequestEntityFields.REQUEST_BODY_ROOM_ID, requestBody.roomId)
|
||||
.equalTo(OutgoingRoomKeyRequestEntityFields.REQUEST_BODY_SENDER_KEY, requestBody.senderKey)
|
||||
.equalTo(OutgoingRoomKeyRequestEntityFields.REQUEST_BODY_SESSION_ID, requestBody.sessionId)
|
||||
.findFirst()
|
||||
return monarchy.fetchAllCopiedSync { realm ->
|
||||
realm.where<OutgoingGossipingRequestEntity>()
|
||||
.equalTo(OutgoingGossipingRequestEntityFields.TYPE_STR, GossipRequestType.KEY.name)
|
||||
}.mapNotNull {
|
||||
it.toOutgoingGossipingRequest() as? OutgoingRoomKeyRequest
|
||||
}.firstOrNull {
|
||||
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? {
|
||||
if (request.requestBody == null) {
|
||||
return null
|
||||
}
|
||||
override fun getOutgoingSecretRequest(secretName: String): OutgoingSecretRequest? {
|
||||
return monarchy.fetchAllCopiedSync { realm ->
|
||||
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!!)
|
||||
|
||||
if (existingOne != null) {
|
||||
return existingOne
|
||||
override fun getIncomingRoomKeyRequests(): List<IncomingRoomKeyRequest> {
|
||||
return monarchy.fetchAllCopiedSync { realm ->
|
||||
realm.where<IncomingGossipingRequestEntity>()
|
||||
.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
|
||||
doRealmTransaction(realmConfiguration) {
|
||||
it.createObject(OutgoingRoomKeyRequestEntity::class.java, request.requestId).apply {
|
||||
putRequestBody(request.requestBody)
|
||||
putRecipients(request.recipients)
|
||||
cancellationTxnId = request.cancellationTxnId
|
||||
state = request.state.ordinal
|
||||
doRealmTransaction(realmConfiguration) { realm ->
|
||||
val existing = realm.where<OutgoingGossipingRequestEntity>()
|
||||
.equalTo(OutgoingGossipingRequestEntityFields.TYPE_STR, GossipRequestType.SECRET.name)
|
||||
.equalTo(OutgoingGossipingRequestEntityFields.REQUESTED_INFO_STR, secretName)
|
||||
.findAll()
|
||||
.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
|
||||
}
|
||||
|
||||
override fun getOutgoingRoomKeyRequestByState(states: Set<OutgoingRoomKeyRequest.RequestState>): OutgoingRoomKeyRequest? {
|
||||
val statesIndex = states.map { it.ordinal }.toTypedArray()
|
||||
return doRealmQueryAndCopy(realmConfiguration) {
|
||||
it.where<OutgoingRoomKeyRequestEntity>()
|
||||
.`in`(OutgoingRoomKeyRequestEntityFields.STATE, statesIndex)
|
||||
.findFirst()
|
||||
override fun saveGossipingEvent(event: Event) {
|
||||
val now = System.currentTimeMillis()
|
||||
val ageLocalTs = event.unsignedData?.age?.let { now - it } ?: now
|
||||
val entity = GossipingEventEntity(
|
||||
type = event.type,
|
||||
sender = event.senderId,
|
||||
ageLocalTs = ageLocalTs,
|
||||
content = ContentMapper.map(event.content)
|
||||
).apply {
|
||||
sendState = SendState.SYNCED
|
||||
decryptionResultJson = MoshiProvider.providesMoshi().adapter<OlmDecryptionResult>(OlmDecryptionResult::class.java).toJson(event.mxDecryptionResult)
|
||||
decryptionErrorCode = event.mCryptoError?.name
|
||||
}
|
||||
?.toOutgoingRoomKeyRequest()
|
||||
}
|
||||
|
||||
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)
|
||||
doRealmTransaction(realmConfiguration) { realm ->
|
||||
realm.insertOrUpdate(entity)
|
||||
}
|
||||
}
|
||||
|
||||
override fun deleteOutgoingRoomKeyRequest(transactionId: String) {
|
||||
doRealmTransaction(realmConfiguration) {
|
||||
it.where<OutgoingRoomKeyRequestEntity>()
|
||||
.equalTo(OutgoingRoomKeyRequestEntityFields.REQUEST_ID, transactionId)
|
||||
.findFirst()
|
||||
?.deleteFromRealm()
|
||||
// override fun getOutgoingRoomKeyRequestByState(states: Set<ShareRequestState>): OutgoingRoomKeyRequest? {
|
||||
// val statesIndex = states.map { it.ordinal }.toTypedArray()
|
||||
// return doRealmQueryAndCopy(realmConfiguration) { realm ->
|
||||
// realm.where<GossipingEventEntity>()
|
||||
// .equalTo(GossipingEventEntityFields.SENDER, credentials.userId)
|
||||
// .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?) {
|
||||
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: 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 updateOutgoingGossipingRequestState(requestId: String, state: OutgoingGossipingRequestState) {
|
||||
doRealmTransaction(realmConfiguration) { realm ->
|
||||
realm.where<OutgoingGossipingRequestEntity>()
|
||||
.equalTo(OutgoingGossipingRequestEntityFields.REQUEST_ID, requestId)
|
||||
.findAll().forEach {
|
||||
it.requestState = state
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getIncomingRoomKeyRequest(userId: String, deviceId: String, requestId: String): IncomingRoomKeyRequest? {
|
||||
return doRealmQueryAndCopy(realmConfiguration) {
|
||||
it.where<IncomingRoomKeyRequestEntity>()
|
||||
.equalTo(IncomingRoomKeyRequestEntityFields.USER_ID, userId)
|
||||
.equalTo(IncomingRoomKeyRequestEntityFields.DEVICE_ID, deviceId)
|
||||
.equalTo(IncomingRoomKeyRequestEntityFields.REQUEST_ID, requestId)
|
||||
.findFirst()
|
||||
}
|
||||
?.toIncomingRoomKeyRequest()
|
||||
return doRealmQueryAndCopyList(realmConfiguration) { realm ->
|
||||
realm.where<IncomingGossipingRequestEntity>()
|
||||
.equalTo(IncomingGossipingRequestEntityFields.TYPE_STR, GossipRequestType.KEY.name)
|
||||
.equalTo(IncomingGossipingRequestEntityFields.OTHER_DEVICE_ID, deviceId)
|
||||
.equalTo(IncomingGossipingRequestEntityFields.OTHER_USER_ID, userId)
|
||||
.findAll()
|
||||
}.mapNotNull { entity ->
|
||||
entity.toIncomingGossipingRequest() as? IncomingRoomKeyRequest
|
||||
}.firstOrNull()
|
||||
}
|
||||
|
||||
override fun getPendingIncomingRoomKeyRequests(): MutableList<IncomingRoomKeyRequest> {
|
||||
override fun getPendingIncomingRoomKeyRequests(): List<IncomingRoomKeyRequest> {
|
||||
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()
|
||||
}
|
||||
.map {
|
||||
it.toIncomingRoomKeyRequest()
|
||||
.map { entity ->
|
||||
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
|
||||
* ========================================================================================== */
|
||||
@ -1024,6 +1249,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? {
|
||||
return doRealmQueryAndCopy(realmConfiguration) { realm ->
|
||||
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.CryptoMetadataEntityFields
|
||||
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.OutgoingGossipingRequestEntityFields
|
||||
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.di.SerializeNulls
|
||||
@ -34,102 +37,146 @@ import timber.log.Timber
|
||||
internal object RealmCryptoStoreMigration : RealmMigration {
|
||||
|
||||
// Version 1L added Cross Signing info persistence
|
||||
const val CRYPTO_STORE_SCHEMA_VERSION = 1L
|
||||
const val CRYPTO_STORE_SCHEMA_VERSION = 2L
|
||||
|
||||
override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) {
|
||||
Timber.v("Migrating Realm Crypto from $oldVersion to $newVersion")
|
||||
|
||||
if (oldVersion <= 0) {
|
||||
Timber.d("Step 0 -> 1")
|
||||
Timber.d("Create KeyInfoEntity")
|
||||
if (oldVersion <= 0) migrateTo1(realm)
|
||||
if (oldVersion <= 1) migrateTo2(realm)
|
||||
}
|
||||
|
||||
val trustLevelentityEntitySchema = realm.schema.create("TrustLevelEntity")
|
||||
.addField(TrustLevelEntityFields.CROSS_SIGNED_VERIFIED, Boolean::class.java)
|
||||
.setNullable(TrustLevelEntityFields.CROSS_SIGNED_VERIFIED, true)
|
||||
.addField(TrustLevelEntityFields.LOCALLY_VERIFIED, Boolean::class.java)
|
||||
.setNullable(TrustLevelEntityFields.LOCALLY_VERIFIED, true)
|
||||
private fun migrateTo1(realm: DynamicRealm) {
|
||||
Timber.d("Step 0 -> 1")
|
||||
Timber.d("Create KeyInfoEntity")
|
||||
|
||||
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 trustLevelentityEntitySchema = realm.schema.create("TrustLevelEntity")
|
||||
.addField(TrustLevelEntityFields.CROSS_SIGNED_VERIFIED, Boolean::class.java)
|
||||
.setNullable(TrustLevelEntityFields.CROSS_SIGNED_VERIFIED, true)
|
||||
.addField(TrustLevelEntityFields.LOCALLY_VERIFIED, Boolean::class.java)
|
||||
.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")
|
||||
.addField(CrossSigningInfoEntityFields.USER_ID, String::class.java)
|
||||
.addPrimaryKey(CrossSigningInfoEntityFields.USER_ID)
|
||||
.addRealmListField(CrossSigningInfoEntityFields.CROSS_SIGNING_KEYS.`$`, keyInfoEntitySchema)
|
||||
Timber.d("Create CrossSigningInfoEntity")
|
||||
|
||||
Timber.d("Updating UserEntity table")
|
||||
realm.schema.get("UserEntity")
|
||||
?.addRealmObjectField(UserEntityFields.CROSS_SIGNING_INFO_ENTITY.`$`, crossSigningInfoSchema)
|
||||
val crossSigningInfoSchema = realm.schema.create("CrossSigningInfoEntity")
|
||||
.addField(CrossSigningInfoEntityFields.USER_ID, String::class.java)
|
||||
.addPrimaryKey(CrossSigningInfoEntityFields.USER_ID)
|
||||
.addRealmListField(CrossSigningInfoEntityFields.CROSS_SIGNING_KEYS.`$`, keyInfoEntitySchema)
|
||||
|
||||
Timber.d("Updating CryptoMetadataEntity table")
|
||||
realm.schema.get("CryptoMetadataEntity")
|
||||
?.addField(CryptoMetadataEntityFields.X_SIGN_MASTER_PRIVATE_KEY, String::class.java)
|
||||
?.addField(CryptoMetadataEntityFields.X_SIGN_USER_PRIVATE_KEY, String::class.java)
|
||||
?.addField(CryptoMetadataEntityFields.X_SIGN_SELF_SIGNED_PRIVATE_KEY, String::class.java)
|
||||
Timber.d("Updating UserEntity table")
|
||||
realm.schema.get("UserEntity")
|
||||
?.addRealmObjectField(UserEntityFields.CROSS_SIGNING_INFO_ENTITY.`$`, crossSigningInfoSchema)
|
||||
|
||||
val moshi = Moshi.Builder().add(SerializeNulls.JSON_ADAPTER_FACTORY).build()
|
||||
val listMigrationAdapter = moshi.adapter<List<String>>(Types.newParameterizedType(
|
||||
List::class.java,
|
||||
String::class.java,
|
||||
Any::class.java
|
||||
))
|
||||
val mapMigrationAdapter = moshi.adapter<JsonDict>(Types.newParameterizedType(
|
||||
Map::class.java,
|
||||
String::class.java,
|
||||
Any::class.java
|
||||
))
|
||||
Timber.d("Updating CryptoMetadataEntity table")
|
||||
realm.schema.get("CryptoMetadataEntity")
|
||||
?.addField(CryptoMetadataEntityFields.X_SIGN_MASTER_PRIVATE_KEY, String::class.java)
|
||||
?.addField(CryptoMetadataEntityFields.X_SIGN_USER_PRIVATE_KEY, String::class.java)
|
||||
?.addField(CryptoMetadataEntityFields.X_SIGN_SELF_SIGNED_PRIVATE_KEY, String::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 ->
|
||||
val moshi = Moshi.Builder().add(SerializeNulls.JSON_ADAPTER_FACTORY).build()
|
||||
val listMigrationAdapter = moshi.adapter<List<String>>(Types.newParameterizedType(
|
||||
List::class.java,
|
||||
String::class.java,
|
||||
Any::class.java
|
||||
))
|
||||
val mapMigrationAdapter = moshi.adapter<JsonDict>(Types.newParameterizedType(
|
||||
Map::class.java,
|
||||
String::class.java,
|
||||
Any::class.java
|
||||
))
|
||||
|
||||
val oldSerializedData = obj.getString("deviceInfoData")
|
||||
deserializeFromRealm<MXDeviceInfo>(oldSerializedData)?.let { oldDevice ->
|
||||
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 ->
|
||||
|
||||
val trustLevel = realm.createObject("TrustLevelEntity")
|
||||
when (oldDevice.verified) {
|
||||
MXDeviceInfo.DEVICE_VERIFICATION_UNKNOWN -> {
|
||||
obj.setNull(DeviceInfoEntityFields.TRUST_LEVEL_ENTITY.`$`)
|
||||
}
|
||||
MXDeviceInfo.DEVICE_VERIFICATION_BLOCKED -> {
|
||||
trustLevel.setNull(TrustLevelEntityFields.LOCALLY_VERIFIED)
|
||||
trustLevel.setNull(TrustLevelEntityFields.CROSS_SIGNED_VERIFIED)
|
||||
obj.setBoolean(DeviceInfoEntityFields.IS_BLOCKED, oldDevice.isBlocked)
|
||||
obj.setObject(DeviceInfoEntityFields.TRUST_LEVEL_ENTITY.`$`, trustLevel)
|
||||
}
|
||||
MXDeviceInfo.DEVICE_VERIFICATION_UNVERIFIED -> {
|
||||
trustLevel.setBoolean(TrustLevelEntityFields.LOCALLY_VERIFIED, false)
|
||||
trustLevel.setBoolean(TrustLevelEntityFields.CROSS_SIGNED_VERIFIED, false)
|
||||
obj.setObject(DeviceInfoEntityFields.TRUST_LEVEL_ENTITY.`$`, trustLevel)
|
||||
}
|
||||
MXDeviceInfo.DEVICE_VERIFICATION_VERIFIED -> {
|
||||
trustLevel.setBoolean(TrustLevelEntityFields.LOCALLY_VERIFIED, true)
|
||||
trustLevel.setBoolean(TrustLevelEntityFields.CROSS_SIGNED_VERIFIED, false)
|
||||
obj.setObject(DeviceInfoEntityFields.TRUST_LEVEL_ENTITY.`$`, trustLevel)
|
||||
}
|
||||
val oldSerializedData = obj.getString("deviceInfoData")
|
||||
deserializeFromRealm<MXDeviceInfo>(oldSerializedData)?.let { oldDevice ->
|
||||
|
||||
val trustLevel = realm.createObject("TrustLevelEntity")
|
||||
when (oldDevice.verified) {
|
||||
MXDeviceInfo.DEVICE_VERIFICATION_UNKNOWN -> {
|
||||
obj.setNull(DeviceInfoEntityFields.TRUST_LEVEL_ENTITY.`$`)
|
||||
}
|
||||
MXDeviceInfo.DEVICE_VERIFICATION_BLOCKED -> {
|
||||
trustLevel.setNull(TrustLevelEntityFields.LOCALLY_VERIFIED)
|
||||
trustLevel.setNull(TrustLevelEntityFields.CROSS_SIGNED_VERIFIED)
|
||||
obj.setBoolean(DeviceInfoEntityFields.IS_BLOCKED, oldDevice.isBlocked)
|
||||
obj.setObject(DeviceInfoEntityFields.TRUST_LEVEL_ENTITY.`$`, trustLevel)
|
||||
}
|
||||
MXDeviceInfo.DEVICE_VERIFICATION_UNVERIFIED -> {
|
||||
trustLevel.setBoolean(TrustLevelEntityFields.LOCALLY_VERIFIED, false)
|
||||
trustLevel.setBoolean(TrustLevelEntityFields.CROSS_SIGNED_VERIFIED, false)
|
||||
obj.setObject(DeviceInfoEntityFields.TRUST_LEVEL_ENTITY.`$`, trustLevel)
|
||||
}
|
||||
MXDeviceInfo.DEVICE_VERIFICATION_VERIFIED -> {
|
||||
trustLevel.setBoolean(TrustLevelEntityFields.LOCALLY_VERIFIED, true)
|
||||
trustLevel.setBoolean(TrustLevelEntityFields.CROSS_SIGNED_VERIFIED, false)
|
||||
obj.setObject(DeviceInfoEntityFields.TRUST_LEVEL_ENTITY.`$`, trustLevel)
|
||||
}
|
||||
|
||||
obj.setString(DeviceInfoEntityFields.USER_ID, oldDevice.userId)
|
||||
obj.setString(DeviceInfoEntityFields.IDENTITY_KEY, oldDevice.identityKey())
|
||||
obj.setString(DeviceInfoEntityFields.ALGORITHM_LIST_JSON, listMigrationAdapter.toJson(oldDevice.algorithms))
|
||||
obj.setString(DeviceInfoEntityFields.KEYS_MAP_JSON, mapMigrationAdapter.toJson(oldDevice.keys))
|
||||
obj.setString(DeviceInfoEntityFields.SIGNATURE_MAP_JSON, mapMigrationAdapter.toJson(oldDevice.signatures))
|
||||
obj.setString(DeviceInfoEntityFields.UNSIGNED_MAP_JSON, mapMigrationAdapter.toJson(oldDevice.unsigned))
|
||||
}
|
||||
|
||||
obj.setString(DeviceInfoEntityFields.USER_ID, oldDevice.userId)
|
||||
obj.setString(DeviceInfoEntityFields.IDENTITY_KEY, oldDevice.identityKey())
|
||||
obj.setString(DeviceInfoEntityFields.ALGORITHM_LIST_JSON, listMigrationAdapter.toJson(oldDevice.algorithms))
|
||||
obj.setString(DeviceInfoEntityFields.KEYS_MAP_JSON, mapMigrationAdapter.toJson(oldDevice.keys))
|
||||
obj.setString(DeviceInfoEntityFields.SIGNATURE_MAP_JSON, mapMigrationAdapter.toJson(oldDevice.signatures))
|
||||
obj.setString(DeviceInfoEntityFields.UNSIGNED_MAP_JSON, mapMigrationAdapter.toJson(oldDevice.unsigned))
|
||||
}
|
||||
?.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)
|
||||
}
|
||||
}
|
||||
|
@ -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.CryptoRoomEntity
|
||||
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.KeysBackupDataEntity
|
||||
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.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.UserEntity
|
||||
import io.realm.annotations.RealmModule
|
||||
@ -38,14 +39,15 @@ import io.realm.annotations.RealmModule
|
||||
CryptoMetadataEntity::class,
|
||||
CryptoRoomEntity::class,
|
||||
DeviceInfoEntity::class,
|
||||
IncomingRoomKeyRequestEntity::class,
|
||||
KeysBackupDataEntity::class,
|
||||
OlmInboundGroupSessionEntity::class,
|
||||
OlmSessionEntity::class,
|
||||
OutgoingRoomKeyRequestEntity::class,
|
||||
UserEntity::class,
|
||||
KeyInfoEntity::class,
|
||||
CrossSigningInfoEntity::class,
|
||||
TrustLevelEntity::class
|
||||
TrustLevelEntity::class,
|
||||
GossipingEventEntity::class,
|
||||
IncomingGossipingRequestEntity::class,
|
||||
OutgoingGossipingRequestEntity::class
|
||||
])
|
||||
internal class RealmCryptoStoreModule
|
||||
|
@ -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 +1,56 @@
|
||||
/*
|
||||
* 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
|
||||
}
|
||||
}
|
||||
}
|
||||
// /*
|
||||
// * 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,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.store.db.model
|
||||
//
|
||||
// import im.vector.matrix.android.internal.crypto.IncomingSecretShareRequest
|
||||
// import io.realm.RealmObject
|
||||
//
|
||||
// internal open class IncomingSecretRequestEntity(
|
||||
// var requestId: String? = null,
|
||||
// var userId: String? = null,
|
||||
// var deviceId: String? = null,
|
||||
// var secretName: String? = null
|
||||
// ) : RealmObject() {
|
||||
//
|
||||
// fun toIncomingSecretShareRequest(): IncomingSecretShareRequest {
|
||||
// return IncomingSecretShareRequest(
|
||||
// requestId = requestId,
|
||||
// userId = userId,
|
||||
// deviceId = deviceId,
|
||||
// secretName = secretName
|
||||
// )
|
||||
// }
|
||||
// }
|
@ -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 +1,77 @@
|
||||
/*
|
||||
* 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
|
||||
}
|
||||
}
|
||||
}
|
||||
// /*
|
||||
// * 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.ShareRequestState
|
||||
// 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!!,
|
||||
// ShareRequestState.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,63 @@
|
||||
// /*
|
||||
// * 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.internal.crypto.OutgoingSecretRequest
|
||||
// import im.vector.matrix.android.internal.crypto.ShareRequestState
|
||||
// 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 OutgoingSecretRequestEntity(
|
||||
// @PrimaryKey var requestId: String? = null,
|
||||
// var cancellationTxnId: String? = null,
|
||||
// // Serialized Json
|
||||
// var recipientsData: String? = null,
|
||||
// // RoomKeyRequestBody fields
|
||||
// var secretName: String? = null,
|
||||
// // State
|
||||
// var state: Int = 0
|
||||
// ) : RealmObject() {
|
||||
//
|
||||
// /**
|
||||
// * Convert to OutgoingRoomKeyRequest
|
||||
// */
|
||||
// fun toOutgoingSecretRequest(): OutgoingSecretRequest {
|
||||
// val cancellationTxnId = this.cancellationTxnId
|
||||
// return OutgoingSecretRequest(
|
||||
// secretName,
|
||||
// getRecipients() ?: emptyList(),
|
||||
// requestId!!,
|
||||
// ShareRequestState.from(state)
|
||||
// ).apply {
|
||||
// this.cancellationTxnId = cancellationTxnId
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// private fun getRecipients(): List<Map<String, String>>? {
|
||||
// return try {
|
||||
// deserializeFromRealm(recipientsData)
|
||||
// } catch (failure: Throwable) {
|
||||
// null
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// fun putRecipients(recipients: List<Map<String, String>>?) {
|
||||
// recipientsData = serializeForRealm(recipients)
|
||||
// }
|
||||
// }
|
@ -60,8 +60,7 @@ internal class DefaultRoomVerificationUpdateTask @Inject constructor(
|
||||
// TODO ignore initial sync or back pagination?
|
||||
|
||||
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")
|
||||
Timber.v("## SAS Verification live observer: received msgId: ${event.eventId} msgtype: ${event.type} from ${event.senderId}")
|
||||
|
||||
// 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.
|
||||
@ -85,6 +84,7 @@ internal class DefaultRoomVerificationUpdateTask @Inject constructor(
|
||||
)
|
||||
} catch (e: MXCryptoError) {
|
||||
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()}")
|
||||
|
@ -23,6 +23,7 @@ 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.VerificationTxState
|
||||
import im.vector.matrix.android.api.session.events.model.EventType
|
||||
import im.vector.matrix.android.internal.crypto.OutgoingGossipingRequestManager
|
||||
import im.vector.matrix.android.internal.crypto.actions.SetDeviceVerificationAction
|
||||
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
|
||||
import timber.log.Timber
|
||||
@ -33,6 +34,7 @@ internal class DefaultIncomingSASDefaultVerificationTransaction(
|
||||
override val deviceId: String?,
|
||||
private val cryptoStore: IMXCryptoStore,
|
||||
crossSigningService: CrossSigningService,
|
||||
outgoingGossipingRequestManager: OutgoingGossipingRequestManager,
|
||||
deviceFingerprint: String,
|
||||
transactionId: String,
|
||||
otherUserID: String,
|
||||
@ -43,6 +45,7 @@ internal class DefaultIncomingSASDefaultVerificationTransaction(
|
||||
deviceId,
|
||||
cryptoStore,
|
||||
crossSigningService,
|
||||
outgoingGossipingRequestManager,
|
||||
deviceFingerprint,
|
||||
transactionId,
|
||||
otherUserID,
|
||||
|
@ -20,6 +20,7 @@ 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.VerificationTxState
|
||||
import im.vector.matrix.android.api.session.events.model.EventType
|
||||
import im.vector.matrix.android.internal.crypto.OutgoingGossipingRequestManager
|
||||
import im.vector.matrix.android.internal.crypto.actions.SetDeviceVerificationAction
|
||||
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
|
||||
import timber.log.Timber
|
||||
@ -30,6 +31,7 @@ internal class DefaultOutgoingSASDefaultVerificationTransaction(
|
||||
deviceId: String?,
|
||||
cryptoStore: IMXCryptoStore,
|
||||
crossSigningService: CrossSigningService,
|
||||
outgoingGossipingRequestManager: OutgoingGossipingRequestManager,
|
||||
deviceFingerprint: String,
|
||||
transactionId: String,
|
||||
otherUserId: String,
|
||||
@ -40,6 +42,7 @@ internal class DefaultOutgoingSASDefaultVerificationTransaction(
|
||||
deviceId,
|
||||
cryptoStore,
|
||||
crossSigningService,
|
||||
outgoingGossipingRequestManager,
|
||||
deviceFingerprint,
|
||||
transactionId,
|
||||
otherUserId,
|
||||
|
@ -35,6 +35,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.EventType
|
||||
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.room.model.message.MessageContent
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageRelationContent
|
||||
@ -50,10 +51,12 @@ import im.vector.matrix.android.api.session.room.model.message.MessageVerificati
|
||||
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.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.crosssigning.DeviceTrustLevel
|
||||
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.KeyVerificationAccept
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.KeyVerificationCancel
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.KeyVerificationKey
|
||||
@ -86,6 +89,7 @@ internal class DefaultVerificationService @Inject constructor(
|
||||
@UserId private val userId: String,
|
||||
@DeviceId private val deviceId: String?,
|
||||
private val cryptoStore: IMXCryptoStore,
|
||||
private val outgoingGossipingRequestManager: OutgoingGossipingRequestManager,
|
||||
private val myDeviceInfoHolder: Lazy<MyDeviceInfoHolder>,
|
||||
private val deviceListManager: DeviceListManager,
|
||||
private val setDeviceVerificationAction: SetDeviceVerificationAction,
|
||||
@ -354,6 +358,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) {
|
||||
val startReq = event.getClearContent().toModel<MessageVerificationStartContent>()
|
||||
?.copy(
|
||||
@ -482,6 +507,7 @@ internal class DefaultVerificationService @Inject constructor(
|
||||
deviceId,
|
||||
cryptoStore,
|
||||
crossSigningService,
|
||||
outgoingGossipingRequestManager,
|
||||
myDeviceInfoHolder.get().myDevice.fingerprint()!!,
|
||||
startReq.transactionId,
|
||||
otherUserId,
|
||||
@ -781,6 +807,7 @@ internal class DefaultVerificationService @Inject constructor(
|
||||
senderId,
|
||||
readyReq.fromDevice,
|
||||
crossSigningService,
|
||||
outgoingGossipingRequestManager,
|
||||
cryptoStore,
|
||||
qrCodeData,
|
||||
userId,
|
||||
@ -962,6 +989,7 @@ internal class DefaultVerificationService @Inject constructor(
|
||||
deviceId,
|
||||
cryptoStore,
|
||||
crossSigningService,
|
||||
outgoingGossipingRequestManager,
|
||||
myDeviceInfoHolder.get().myDevice.fingerprint()!!,
|
||||
txID,
|
||||
otherUserId,
|
||||
@ -1137,6 +1165,7 @@ internal class DefaultVerificationService @Inject constructor(
|
||||
deviceId,
|
||||
cryptoStore,
|
||||
crossSigningService,
|
||||
outgoingGossipingRequestManager,
|
||||
myDeviceInfoHolder.get().myDevice.fingerprint()!!,
|
||||
transactionId,
|
||||
otherUserId,
|
||||
@ -1273,6 +1302,7 @@ internal class DefaultVerificationService @Inject constructor(
|
||||
otherUserId,
|
||||
otherDeviceId,
|
||||
crossSigningService,
|
||||
outgoingGossipingRequestManager,
|
||||
cryptoStore,
|
||||
qrCodeData,
|
||||
userId,
|
||||
|
@ -17,8 +17,11 @@ package im.vector.matrix.android.internal.crypto.verification
|
||||
|
||||
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.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.VerificationTransaction
|
||||
import im.vector.matrix.android.api.session.crypto.verification.VerificationTxState
|
||||
import im.vector.matrix.android.internal.crypto.OutgoingGossipingRequestManager
|
||||
import im.vector.matrix.android.internal.crypto.actions.SetDeviceVerificationAction
|
||||
import im.vector.matrix.android.internal.crypto.crosssigning.DeviceTrustLevel
|
||||
import timber.log.Timber
|
||||
@ -29,6 +32,7 @@ import timber.log.Timber
|
||||
internal abstract class DefaultVerificationTransaction(
|
||||
private val setDeviceVerificationAction: SetDeviceVerificationAction,
|
||||
private val crossSigningService: CrossSigningService,
|
||||
private val outgoingGossipingRequestManager: OutgoingGossipingRequestManager,
|
||||
private val userId: String,
|
||||
override val transactionId: String,
|
||||
override val otherUserId: String,
|
||||
@ -54,6 +58,14 @@ internal abstract class DefaultVerificationTransaction(
|
||||
protected fun trust(canTrustOtherUserMasterKey: Boolean,
|
||||
toVerifyDeviceIds: List<String>,
|
||||
eventuallyMarkMyMasterKeyAsTrusted: Boolean) {
|
||||
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 (canTrustOtherUserMasterKey) {
|
||||
// we should trust this master key
|
||||
@ -83,11 +95,13 @@ internal abstract class DefaultVerificationTransaction(
|
||||
})
|
||||
}
|
||||
|
||||
// TODO what if the otherDevice is not in this list? and should we
|
||||
toVerifyDeviceIds.forEach {
|
||||
setDeviceVerified(otherUserId, it)
|
||||
transport.done(transactionId) {
|
||||
if (otherUserId == userId) {
|
||||
outgoingGossipingRequestManager.sendSecretShareRequest(SELF_SIGNING_KEY_SSSS_NAME, mapOf(userId to listOf(otherDeviceId ?: "*")))
|
||||
outgoingGossipingRequestManager.sendSecretShareRequest(USER_SIGNING_KEY_SSSS_NAME, mapOf(userId to listOf(otherDeviceId ?: "*")))
|
||||
}
|
||||
}
|
||||
transport.done(transactionId)
|
||||
|
||||
state = VerificationTxState.Verified
|
||||
}
|
||||
|
||||
|
@ -24,6 +24,7 @@ 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.VerificationTxState
|
||||
import im.vector.matrix.android.api.session.events.model.EventType
|
||||
import im.vector.matrix.android.internal.crypto.OutgoingGossipingRequestManager
|
||||
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.store.IMXCryptoStore
|
||||
@ -42,6 +43,7 @@ internal abstract class SASDefaultVerificationTransaction(
|
||||
open val deviceId: String?,
|
||||
private val cryptoStore: IMXCryptoStore,
|
||||
crossSigningService: CrossSigningService,
|
||||
outgoingGossipingRequestManager: OutgoingGossipingRequestManager,
|
||||
private val deviceFingerprint: String,
|
||||
transactionId: String,
|
||||
otherUserId: String,
|
||||
@ -50,6 +52,7 @@ internal abstract class SASDefaultVerificationTransaction(
|
||||
) : DefaultVerificationTransaction(
|
||||
setDeviceVerificationAction,
|
||||
crossSigningService,
|
||||
outgoingGossipingRequestManager,
|
||||
userId,
|
||||
transactionId,
|
||||
otherUserId,
|
||||
|
@ -46,7 +46,8 @@ internal interface VerificationTransport {
|
||||
otherUserDeviceId: String?,
|
||||
code: CancelCode)
|
||||
|
||||
fun done(transactionId: String)
|
||||
fun done(transactionId: String,
|
||||
onDone: (() -> Unit)?)
|
||||
|
||||
/**
|
||||
* Creates an accept message suitable for this transport
|
||||
|
@ -22,8 +22,8 @@ import androidx.work.ExistingWorkPolicy
|
||||
import androidx.work.Operation
|
||||
import androidx.work.WorkInfo
|
||||
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.ValidVerificationInfoRequest
|
||||
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.Event
|
||||
@ -107,7 +107,7 @@ internal class VerificationTransportRoomMessage(
|
||||
// }, listenerExecutor)
|
||||
|
||||
val workLiveData = workManagerProvider.workManager
|
||||
.getWorkInfosForUniqueWorkLiveData("${roomId}_VerificationWork")
|
||||
.getWorkInfosForUniqueWorkLiveData(uniqueQueueName())
|
||||
|
||||
val observer = object : Observer<List<WorkInfo>> {
|
||||
override fun onChanged(workInfoList: List<WorkInfo>?) {
|
||||
@ -228,7 +228,8 @@ internal class VerificationTransportRoomMessage(
|
||||
enqueueSendWork(workerParams)
|
||||
}
|
||||
|
||||
override fun done(transactionId: String) {
|
||||
override fun done(transactionId: String,
|
||||
onDone: (() -> Unit)?) {
|
||||
Timber.d("## SAS sending done for $transactionId")
|
||||
val event = createEventAndLocalEcho(
|
||||
type = EventType.KEY_VERIFICATION_DONE,
|
||||
@ -244,7 +245,26 @@ internal class VerificationTransportRoomMessage(
|
||||
sessionId = sessionId,
|
||||
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> {
|
||||
@ -254,10 +274,12 @@ internal class VerificationTransportRoomMessage(
|
||||
.setBackoffCriteria(BackoffPolicy.LINEAR, 2_000L, TimeUnit.MILLISECONDS)
|
||||
.build()
|
||||
return workManagerProvider.workManager
|
||||
.beginUniqueWork("${roomId}_VerificationWork", ExistingWorkPolicy.APPEND, workRequest)
|
||||
.beginUniqueWork(uniqueQueueName(), ExistingWorkPolicy.APPEND, workRequest)
|
||||
.enqueue() to workRequest.id
|
||||
}
|
||||
|
||||
private fun uniqueQueueName() = "${roomId}_VerificationWork"
|
||||
|
||||
override fun createAccept(tid: String,
|
||||
keyAgreementProtocol: String,
|
||||
hash: String,
|
||||
|
@ -145,7 +145,7 @@ internal class VerificationTransportToDevice(
|
||||
.executeBy(taskExecutor)
|
||||
}
|
||||
|
||||
override fun done(transactionId: String) {
|
||||
override fun done(transactionId: String, onDone: (() -> Unit)?) {
|
||||
val otherUserId = tx?.otherUserId ?: return
|
||||
val otherUserDeviceId = tx?.otherDeviceId ?: return
|
||||
val cancelMessage = KeyVerificationDone(transactionId)
|
||||
@ -155,6 +155,7 @@ internal class VerificationTransportToDevice(
|
||||
.configureWith(SendToDeviceTask.Params(EventType.KEY_VERIFICATION_DONE, contentMap, transactionId)) {
|
||||
this.callback = object : MatrixCallback<Unit> {
|
||||
override fun onSuccess(data: Unit) {
|
||||
onDone?.invoke()
|
||||
Timber.v("## SAS verification [$transactionId] done")
|
||||
}
|
||||
|
||||
|
@ -21,6 +21,7 @@ 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.VerificationTxState
|
||||
import im.vector.matrix.android.api.session.events.model.EventType
|
||||
import im.vector.matrix.android.internal.crypto.OutgoingGossipingRequestManager
|
||||
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.fromBase64Safe
|
||||
@ -36,6 +37,7 @@ internal class DefaultQrCodeVerificationTransaction(
|
||||
override val otherUserId: String,
|
||||
override var otherDeviceId: String?,
|
||||
private val crossSigningService: CrossSigningService,
|
||||
outgoingGossipingRequestManager: OutgoingGossipingRequestManager,
|
||||
private val cryptoStore: IMXCryptoStore,
|
||||
// Not null only if other user is able to scan QR code
|
||||
private val qrCodeData: QrCodeData?,
|
||||
@ -45,6 +47,7 @@ internal class DefaultQrCodeVerificationTransaction(
|
||||
) : DefaultVerificationTransaction(
|
||||
setDeviceVerificationAction,
|
||||
crossSigningService,
|
||||
outgoingGossipingRequestManager,
|
||||
userId,
|
||||
transactionId,
|
||||
otherUserId,
|
||||
@ -181,13 +184,12 @@ internal class DefaultQrCodeVerificationTransaction(
|
||||
// qrCodeData.sharedSecret will be used to send the start request
|
||||
start(otherQrCodeData.sharedSecret)
|
||||
|
||||
// Trust the other user
|
||||
trust(canTrustOtherUserMasterKey,
|
||||
toVerifyDeviceIds.distinct(),
|
||||
eventuallyMarkMyMasterKeyAsTrusted = true)
|
||||
}
|
||||
|
||||
private fun start(remoteSecret: String) {
|
||||
private fun start(remoteSecret: String, onDone: (() -> Unit)? = null) {
|
||||
if (state != VerificationTxState.None) {
|
||||
Timber.e("## Verification QR: start verification from invalid state")
|
||||
// should I cancel??
|
||||
@ -205,7 +207,7 @@ internal class DefaultQrCodeVerificationTransaction(
|
||||
startMessage,
|
||||
VerificationTxState.Started,
|
||||
CancelCode.User,
|
||||
null
|
||||
onDone
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -20,7 +20,9 @@ import dagger.BindsInstance
|
||||
import dagger.Component
|
||||
import im.vector.matrix.android.api.auth.data.SessionParams
|
||||
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.SendGossipRequestWorker
|
||||
import im.vector.matrix.android.internal.crypto.verification.SendVerificationMessageWorker
|
||||
import im.vector.matrix.android.internal.di.MatrixComponent
|
||||
import im.vector.matrix.android.internal.di.SessionAssistedInjectModule
|
||||
@ -106,6 +108,9 @@ internal interface SessionComponent {
|
||||
|
||||
fun inject(worker: SendVerificationMessageWorker)
|
||||
|
||||
fun inject(worker: SendGossipRequestWorker)
|
||||
fun inject(worker: CancelGossipRequestWorker)
|
||||
|
||||
@Component.Factory
|
||||
interface Factory {
|
||||
fun create(
|
||||
|
@ -238,7 +238,7 @@ internal class DefaultEventRelationsAggregationTask @Inject constructor(
|
||||
forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain
|
||||
)
|
||||
} 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
|
||||
}
|
||||
}
|
||||
|
@ -114,7 +114,7 @@ internal class TimelineEventDecryptor @Inject constructor(
|
||||
Timber.v("Successfully decrypted event $eventId")
|
||||
eventEntity.setDecryptionResult(result)
|
||||
} catch (e: MXCryptoError) {
|
||||
Timber.w(e, "Failed to decrypt event $eventId")
|
||||
Timber.v(e, "Failed to decrypt event $eventId")
|
||||
if (e is MXCryptoError.Base && e.errorType == MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID) {
|
||||
// Keep track of unknown sessions to automatically try to decrypt on new session
|
||||
eventEntity.decryptionErrorCode = e.errorType.name
|
||||
|
@ -249,6 +249,7 @@ dependencies {
|
||||
def moshi_version = '1.8.0'
|
||||
def daggerVersion = '2.25.4'
|
||||
def autofill_version = "1.0.0"
|
||||
def work_version = '2.3.3'
|
||||
|
||||
implementation project(":matrix-sdk-android")
|
||||
implementation project(":matrix-sdk-android-rx")
|
||||
@ -296,7 +297,7 @@ dependencies {
|
||||
implementation 'com.airbnb.android:mvrx:1.3.0'
|
||||
|
||||
// Work
|
||||
implementation "androidx.work:work-runtime-ktx:2.3.3"
|
||||
implementation "androidx.work:work-runtime-ktx:$work_version"
|
||||
|
||||
// Paging
|
||||
implementation "androidx.paging:paging-runtime-ktx:2.1.1"
|
||||
|
@ -74,6 +74,10 @@ import im.vector.riotx.features.settings.VectorSettingsSecurityPrivacyFragment
|
||||
import im.vector.riotx.features.settings.crosssigning.CrossSigningSettingsFragment
|
||||
import im.vector.riotx.features.settings.devices.VectorSettingsDevicesFragment
|
||||
import im.vector.riotx.features.settings.devtools.AccountDataFragment
|
||||
import im.vector.riotx.features.settings.devtools.GossipingEventsPaperTrailFragment
|
||||
import im.vector.riotx.features.settings.devtools.IncomingKeyRequestListFragment
|
||||
import im.vector.riotx.features.settings.devtools.OutgoingKeyRequestListFragment
|
||||
import im.vector.riotx.features.settings.devtools.KeyRequestsFragment
|
||||
import im.vector.riotx.features.settings.ignored.VectorSettingsIgnoredUsersFragment
|
||||
import im.vector.riotx.features.settings.push.PushGatewaysFragment
|
||||
import im.vector.riotx.features.share.IncomingShareFragment
|
||||
@ -366,4 +370,24 @@ interface FragmentModule {
|
||||
@IntoMap
|
||||
@FragmentKey(AccountDataFragment::class)
|
||||
fun bindAccountDataFragment(fragment: AccountDataFragment): Fragment
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@FragmentKey(OutgoingKeyRequestListFragment::class)
|
||||
fun bindOutgoingKeyRequestListFragment(fragment: OutgoingKeyRequestListFragment): Fragment
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@FragmentKey(IncomingKeyRequestListFragment::class)
|
||||
fun bindIncomingKeyRequestListFragment(fragment: IncomingKeyRequestListFragment): Fragment
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@FragmentKey(KeyRequestsFragment::class)
|
||||
fun bindKeyRequestsFragment(fragment: KeyRequestsFragment): Fragment
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@FragmentKey(GossipingEventsPaperTrailFragment::class)
|
||||
fun bindGossipingEventsPaperTrailFragment(fragment: GossipingEventsPaperTrailFragment): Fragment
|
||||
}
|
||||
|
@ -171,7 +171,7 @@ abstract class VectorBaseFragment : BaseMvRxFragment(), HasScreenInjector {
|
||||
|
||||
override fun invalidate() {
|
||||
// no-ops by default
|
||||
Timber.w("invalidate() method has not been implemented")
|
||||
Timber.v("invalidate() method has not been implemented")
|
||||
}
|
||||
|
||||
protected fun setArguments(args: Parcelable? = null) {
|
||||
|
@ -22,13 +22,14 @@ package im.vector.riotx.features.crypto.keysrequest
|
||||
import android.content.Context
|
||||
import im.vector.matrix.android.api.MatrixCallback
|
||||
import im.vector.matrix.android.api.session.Session
|
||||
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.SasVerificationTransaction
|
||||
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.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
|
||||
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.MXUsersDevicesMap
|
||||
@ -54,7 +55,7 @@ import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class KeyRequestHandler @Inject constructor(private val context: Context)
|
||||
: RoomKeysRequestListener,
|
||||
: GossipingRequestListener,
|
||||
VerificationService.Listener {
|
||||
|
||||
private val alertsToRequests = HashMap<String, ArrayList<IncomingRoomKeyRequest>>()
|
||||
@ -73,6 +74,13 @@ class KeyRequestHandler @Inject constructor(private val context: Context)
|
||||
session = null
|
||||
}
|
||||
|
||||
override fun onSecretShareRequest(request: IncomingSecretShareRequest) : Boolean {
|
||||
// By default riotX will not prompt if the SDK has decided that the request should not be fulfilled
|
||||
Timber.v("## onSecretShareRequest() : Ignoring $request")
|
||||
request.ignore?.run()
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle incoming key request.
|
||||
*
|
||||
@ -194,20 +202,6 @@ class KeyRequestHandler @Inject constructor(private val context: Context)
|
||||
denyAllRequests(mappingKey)
|
||||
}
|
||||
|
||||
// TODO send to the new profile page
|
||||
// alert.addButton(
|
||||
// context.getString(R.string.start_verification_short_label),
|
||||
// Runnable {
|
||||
// alert.weakCurrentActivity?.get()?.let {
|
||||
// val intent = SASVerificationActivity.outgoingIntent(it,
|
||||
// session?.myUserId ?: "",
|
||||
// userId, deviceId)
|
||||
// it.startActivity(intent)
|
||||
// }
|
||||
// },
|
||||
// false
|
||||
// )
|
||||
|
||||
alert.addButton(context.getString(R.string.share_without_verifying_short_label), Runnable {
|
||||
shareAllSessions(mappingKey)
|
||||
})
|
||||
@ -238,7 +232,7 @@ class KeyRequestHandler @Inject constructor(private val context: Context)
|
||||
*
|
||||
* @param request the cancellation request.
|
||||
*/
|
||||
override fun onRoomKeyRequestCancellation(request: IncomingRoomKeyRequestCancellation) {
|
||||
override fun onRoomKeyRequestCancellation(request: IncomingRequestCancellation) {
|
||||
// see if we can find the request in the queue
|
||||
val userId = request.userId
|
||||
val deviceId = request.deviceId
|
||||
|
@ -71,4 +71,5 @@ sealed class RoomDetailAction : VectorViewModelAction {
|
||||
data class DeclineVerificationRequest(val transactionId: String, val otherUserId: String) : RoomDetailAction()
|
||||
data class RequestVerification(val userId: String) : RoomDetailAction()
|
||||
data class ResumeVerification(val transactionId: String, val otherUserId: String?) : RoomDetailAction()
|
||||
data class ReRequestKeys(val eventId: String) : RoomDetailAction()
|
||||
}
|
||||
|
@ -124,6 +124,7 @@ import im.vector.riotx.features.attachments.preview.AttachmentsPreviewActivity
|
||||
import im.vector.riotx.features.attachments.preview.AttachmentsPreviewArgs
|
||||
import im.vector.riotx.features.attachments.toGroupedContentAttachmentData
|
||||
import im.vector.riotx.features.command.Command
|
||||
import im.vector.riotx.features.crypto.keysbackup.restore.KeysBackupRestoreActivity
|
||||
import im.vector.riotx.features.crypto.util.toImageRes
|
||||
import im.vector.riotx.features.crypto.verification.VerificationBottomSheet
|
||||
import im.vector.riotx.features.home.AvatarRenderer
|
||||
@ -1236,6 +1237,14 @@ class RoomDetailFragment @Inject constructor(
|
||||
is EventSharedAction.OnUrlLongClicked -> {
|
||||
onUrlLongClicked(action.url)
|
||||
}
|
||||
is EventSharedAction.ReRequestKey -> {
|
||||
roomDetailViewModel.handle(RoomDetailAction.ReRequestKeys(action.eventId))
|
||||
}
|
||||
is EventSharedAction.UseKeyBackup -> {
|
||||
context?.let {
|
||||
startActivity(KeysBackupRestoreActivity.intent(it))
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
Toast.makeText(context, "Action $action is not implemented yet", Toast.LENGTH_LONG).show()
|
||||
}
|
||||
|
@ -209,6 +209,7 @@ class RoomDetailViewModel @AssistedInject constructor(
|
||||
is RoomDetailAction.DeclineVerificationRequest -> handleDeclineVerification(action)
|
||||
is RoomDetailAction.RequestVerification -> handleRequestVerification(action)
|
||||
is RoomDetailAction.ResumeVerification -> handleResumeRequestVerification(action)
|
||||
is RoomDetailAction.ReRequestKeys -> handleReRequestKeys(action)
|
||||
}
|
||||
}
|
||||
|
||||
@ -886,6 +887,14 @@ class RoomDetailViewModel @AssistedInject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleReRequestKeys(action: RoomDetailAction.ReRequestKeys) {
|
||||
// Check if this request is still active and handled by me
|
||||
room.getTimeLineEvent(action.eventId)?.let {
|
||||
session.cryptoService().reRequestRoomKeyForEvent(it.root)
|
||||
_viewEvents.post(RoomDetailViewEvents.ShowMessage(stringProvider.getString(R.string.e2e_re_request_encryption_key_dialog_content)))
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleReplyToOptions(action: RoomDetailAction.ReplyToOptions) {
|
||||
room.sendOptionsReply(action.eventId, action.optionIndex, action.optionValue)
|
||||
}
|
||||
|
@ -102,4 +102,10 @@ sealed class EventSharedAction(@StringRes val titleRes: Int,
|
||||
// An url in the event preview has been long clicked
|
||||
data class OnUrlLongClicked(val url: String) :
|
||||
EventSharedAction(0, 0)
|
||||
|
||||
data class ReRequestKey(val eventId: String) :
|
||||
EventSharedAction(R.string.e2e_re_request_encryption_key, R.drawable.key_small)
|
||||
|
||||
object UseKeyBackup :
|
||||
EventSharedAction(R.string.e2e_use_keybackup, R.drawable.shield)
|
||||
}
|
||||
|
@ -25,6 +25,7 @@ import com.squareup.inject.assisted.Assisted
|
||||
import com.squareup.inject.assisted.AssistedInject
|
||||
import dagger.Lazy
|
||||
import im.vector.matrix.android.api.session.Session
|
||||
import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupState
|
||||
import im.vector.matrix.android.api.session.events.model.EventType
|
||||
import im.vector.matrix.android.api.session.events.model.isTextMessage
|
||||
import im.vector.matrix.android.api.session.events.model.toModel
|
||||
@ -209,6 +210,31 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
|
||||
} ?: ""
|
||||
}
|
||||
|
||||
private fun getRedactionReason(timelineEvent: TimelineEvent): String {
|
||||
return (timelineEvent
|
||||
.root
|
||||
.unsignedData
|
||||
?.redactedEvent
|
||||
?.content
|
||||
?.get("reason") as? String)
|
||||
?.takeIf { it.isNotBlank() }
|
||||
.let { reason ->
|
||||
if (reason == null) {
|
||||
if (timelineEvent.root.isRedactedBySameUser()) {
|
||||
stringProvider.getString(R.string.event_redacted_by_user_reason)
|
||||
} else {
|
||||
stringProvider.getString(R.string.event_redacted_by_admin_reason)
|
||||
}
|
||||
} else {
|
||||
if (timelineEvent.root.isRedactedBySameUser()) {
|
||||
stringProvider.getString(R.string.event_redacted_by_user_reason_with_reason, reason)
|
||||
} else {
|
||||
stringProvider.getString(R.string.event_redacted_by_admin_reason_with_reason, reason)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun actionsForEvent(timelineEvent: TimelineEvent): List<EventSharedAction> {
|
||||
val messageContent: MessageContent? = timelineEvent.annotations?.editSummary?.aggregatedContent.toModel()
|
||||
?: timelineEvent.root.getClearContent().toModel()
|
||||
@ -272,8 +298,21 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
|
||||
}
|
||||
|
||||
if (vectorPreferences.developerMode()) {
|
||||
if (timelineEvent.isEncrypted() && timelineEvent.root.mCryptoError != null) {
|
||||
val keysBackupService = session.cryptoService().keysBackupService()
|
||||
if (keysBackupService.state == KeysBackupState.NotTrusted
|
||||
|| (keysBackupService.state == KeysBackupState.ReadyToBackUp
|
||||
&& keysBackupService.canRestoreKeys())
|
||||
) {
|
||||
add(EventSharedAction.UseKeyBackup)
|
||||
}
|
||||
if (session.cryptoService().getCryptoDeviceInfo(session.myUserId).size > 1) {
|
||||
add(EventSharedAction.ReRequestKey(timelineEvent.eventId))
|
||||
}
|
||||
}
|
||||
|
||||
add(EventSharedAction.ViewSource(timelineEvent.root.toContentStringWithIndent()))
|
||||
if (timelineEvent.isEncrypted()) {
|
||||
if (timelineEvent.isEncrypted() && timelineEvent.root.mxDecryptionResult != null) {
|
||||
val decryptedContent = timelineEvent.root.toClearContentStringWithIndent()
|
||||
?: stringProvider.getString(R.string.encryption_information_decryption_error)
|
||||
add(EventSharedAction.ViewDecryptedSource(decryptedContent))
|
||||
|
@ -187,7 +187,7 @@ class NotificationDrawerManager @Inject constructor(private val context: Context
|
||||
|
||||
fun refreshNotificationDrawer() {
|
||||
// Implement last throttler
|
||||
Timber.w("refreshNotificationDrawer()")
|
||||
Timber.v("refreshNotificationDrawer()")
|
||||
backgroundHandler.removeCallbacksAndMessages(null)
|
||||
backgroundHandler.postDelayed(
|
||||
{
|
||||
@ -197,7 +197,7 @@ class NotificationDrawerManager @Inject constructor(private val context: Context
|
||||
|
||||
@WorkerThread
|
||||
private fun refreshNotificationDrawerBg() {
|
||||
Timber.w("refreshNotificationDrawerBg()")
|
||||
Timber.v("refreshNotificationDrawerBg()")
|
||||
|
||||
val session = activeSessionHolder.getSafeActiveSession() ?: return
|
||||
|
||||
|
@ -0,0 +1,236 @@
|
||||
/*
|
||||
* 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.riotx.features.settings.devtools
|
||||
|
||||
import com.airbnb.epoxy.TypedEpoxyController
|
||||
import com.airbnb.mvrx.Fail
|
||||
import com.airbnb.mvrx.Loading
|
||||
import com.airbnb.mvrx.Success
|
||||
import com.airbnb.mvrx.Uninitialized
|
||||
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.model.event.OlmEventContent
|
||||
import im.vector.matrix.android.internal.crypto.model.event.SecretSendEventContent
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.ForwardedRoomKeyContent
|
||||
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.riotx.R
|
||||
import im.vector.riotx.core.date.VectorDateFormatter
|
||||
import im.vector.riotx.core.epoxy.loadingItem
|
||||
import im.vector.riotx.core.extensions.exhaustive
|
||||
import im.vector.riotx.core.resources.ColorProvider
|
||||
import im.vector.riotx.core.resources.DateProvider
|
||||
import im.vector.riotx.core.resources.StringProvider
|
||||
import im.vector.riotx.core.ui.list.GenericItem
|
||||
import im.vector.riotx.core.ui.list.genericFooterItem
|
||||
import im.vector.riotx.core.ui.list.genericItem
|
||||
import im.vector.riotx.core.ui.list.genericItemHeader
|
||||
import me.gujun.android.span.span
|
||||
import javax.inject.Inject
|
||||
|
||||
class GossipingEventsEpoxyController @Inject constructor(
|
||||
private val stringProvider: StringProvider,
|
||||
private val vectorDateFormatter: VectorDateFormatter,
|
||||
private val colorProvider: ColorProvider
|
||||
) : TypedEpoxyController<GossipingEventsPaperTrailState>() {
|
||||
|
||||
interface InteractionListener {
|
||||
fun didTap(event: Event)
|
||||
}
|
||||
|
||||
var interactionListener: InteractionListener? = null
|
||||
|
||||
override fun buildModels(data: GossipingEventsPaperTrailState?) {
|
||||
when (val async = data?.events) {
|
||||
is Uninitialized,
|
||||
is Loading -> {
|
||||
loadingItem {
|
||||
id("loadingOutgoing")
|
||||
loadingText(stringProvider.getString(R.string.loading))
|
||||
}
|
||||
}
|
||||
is Fail -> {
|
||||
genericItem {
|
||||
id("failOutgoing")
|
||||
title(async.error.localizedMessage)
|
||||
}
|
||||
}
|
||||
is Success -> {
|
||||
val eventList = async.invoke()
|
||||
if (eventList.isEmpty()) {
|
||||
genericFooterItem {
|
||||
id("empty")
|
||||
text(stringProvider.getString(R.string.no_result_placeholder))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
eventList.forEachIndexed { _, event ->
|
||||
genericItem {
|
||||
id(event.hashCode())
|
||||
itemClickAction(GenericItem.Action("view").apply { perform = Runnable { interactionListener?.didTap(event) } })
|
||||
title(
|
||||
if (event.isEncrypted()) {
|
||||
"${event.getClearType()} [encrypted]"
|
||||
} else {
|
||||
event.type
|
||||
}
|
||||
)
|
||||
description(
|
||||
span {
|
||||
+vectorDateFormatter.formatMessageDay(DateProvider.toLocalDateTime(event.ageLocalTs))
|
||||
+" ${vectorDateFormatter.formatMessageHour(DateProvider.toLocalDateTime(event.ageLocalTs))}"
|
||||
span("\nfrom: ") {
|
||||
textStyle = "bold"
|
||||
}
|
||||
+"${event.senderId}"
|
||||
apply {
|
||||
if (event.getClearType() == EventType.ROOM_KEY_REQUEST) {
|
||||
val content = event.getClearContent().toModel<RoomKeyShareRequest>()
|
||||
span("\nreqId:") {
|
||||
textStyle = "bold"
|
||||
}
|
||||
+" ${content?.requestId}"
|
||||
span("\naction:") {
|
||||
textStyle = "bold"
|
||||
}
|
||||
+" ${content?.action}"
|
||||
if (content?.action == GossipingToDeviceObject.ACTION_SHARE_REQUEST) {
|
||||
span("\nsessionId:") {
|
||||
textStyle = "bold"
|
||||
}
|
||||
+" ${content.body?.sessionId}"
|
||||
}
|
||||
span("\nrequestedBy: ") {
|
||||
textStyle = "bold"
|
||||
}
|
||||
+"${content?.requestingDeviceId}"
|
||||
} else if (event.getClearType() == EventType.FORWARDED_ROOM_KEY) {
|
||||
val encryptedContent = event.content.toModel<OlmEventContent>()
|
||||
val content = event.getClearContent().toModel<ForwardedRoomKeyContent>()
|
||||
if (event.mxDecryptionResult == null) {
|
||||
span("**Failed to Decrypt** ${event.mCryptoError}") {
|
||||
textColor = colorProvider.getColor(R.color.vector_error_color)
|
||||
}
|
||||
}
|
||||
span("\nsessionId:") {
|
||||
textStyle = "bold"
|
||||
}
|
||||
+" ${content?.sessionId}"
|
||||
span("\nFrom Device (sender key):") {
|
||||
textStyle = "bold"
|
||||
}
|
||||
+" ${encryptedContent?.senderKey}"
|
||||
} else if (event.getClearType() == EventType.SEND_SECRET) {
|
||||
val content = event.getClearContent().toModel<SecretSendEventContent>()
|
||||
|
||||
span("\nrequestId:") {
|
||||
textStyle = "bold"
|
||||
}
|
||||
+" ${content?.requestId}"
|
||||
span("\nFrom Device:") {
|
||||
textStyle = "bold"
|
||||
}
|
||||
+" ${event.mxDecryptionResult?.payload?.get("sender_device")}"
|
||||
} else if (event.getClearType() == EventType.REQUEST_SECRET) {
|
||||
val content = event.getClearContent().toModel<SecretShareRequest>()
|
||||
span("\nreqId:") {
|
||||
textStyle = "bold"
|
||||
}
|
||||
+" ${content?.requestId}"
|
||||
span("\naction:") {
|
||||
textStyle = "bold"
|
||||
}
|
||||
+" ${content?.action}"
|
||||
if (content?.action == GossipingToDeviceObject.ACTION_SHARE_REQUEST) {
|
||||
span("\nsecretName:") {
|
||||
textStyle = "bold"
|
||||
}
|
||||
+" ${content.secretName}"
|
||||
}
|
||||
span("\nrequestedBy: ") {
|
||||
textStyle = "bold"
|
||||
}
|
||||
+"${content?.requestingDeviceId}"
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildOutgoing(data: KeyRequestListViewState?) {
|
||||
data?.outgoingRoomKeyRequest?.let { async ->
|
||||
when (async) {
|
||||
is Uninitialized,
|
||||
is Loading -> {
|
||||
loadingItem {
|
||||
id("loadingOutgoing")
|
||||
loadingText(stringProvider.getString(R.string.loading))
|
||||
}
|
||||
}
|
||||
is Fail -> {
|
||||
genericItem {
|
||||
id("failOutgoing")
|
||||
title(async.error.localizedMessage)
|
||||
}
|
||||
}
|
||||
is Success -> {
|
||||
if (async.invoke().isEmpty()) {
|
||||
genericFooterItem {
|
||||
id("empty")
|
||||
text(stringProvider.getString(R.string.no_result_placeholder))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
val requestList = async.invoke().groupBy { it.roomId }
|
||||
|
||||
requestList.forEach {
|
||||
genericItemHeader {
|
||||
id(it.key)
|
||||
text("roomId: ${it.key}")
|
||||
}
|
||||
it.value.forEach { roomKeyRequest ->
|
||||
genericItem {
|
||||
id(roomKeyRequest.requestId)
|
||||
title(roomKeyRequest.requestId)
|
||||
description(
|
||||
span {
|
||||
span("sessionId:\n") {
|
||||
textStyle = "bold"
|
||||
}
|
||||
+"${roomKeyRequest.sessionId}"
|
||||
span("\nstate:") {
|
||||
textStyle = "bold"
|
||||
}
|
||||
+"\n${roomKeyRequest.state.name}"
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}.exhaustive
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,73 @@
|
||||
/*
|
||||
* 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.riotx.features.settings.devtools
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import com.airbnb.mvrx.fragmentViewModel
|
||||
import com.airbnb.mvrx.withState
|
||||
import im.vector.matrix.android.api.session.events.model.Event
|
||||
import im.vector.riotx.R
|
||||
import im.vector.riotx.core.extensions.cleanup
|
||||
import im.vector.riotx.core.extensions.configureWith
|
||||
import im.vector.riotx.core.platform.VectorBaseFragment
|
||||
import im.vector.riotx.core.resources.ColorProvider
|
||||
import im.vector.riotx.core.utils.jsonViewerStyler
|
||||
import kotlinx.android.synthetic.main.fragment_generic_recycler.*
|
||||
import org.billcarsonfr.jsonviewer.JSonViewerDialog
|
||||
import javax.inject.Inject
|
||||
|
||||
class GossipingEventsPaperTrailFragment @Inject constructor(
|
||||
val viewModelFactory: GossipingEventsPaperTrailViewModel.Factory,
|
||||
private val epoxyController: GossipingEventsEpoxyController,
|
||||
private val colorProvider: ColorProvider
|
||||
) : VectorBaseFragment(), GossipingEventsEpoxyController.InteractionListener {
|
||||
|
||||
override fun getLayoutResId() = R.layout.fragment_generic_recycler
|
||||
|
||||
private val viewModel: GossipingEventsPaperTrailViewModel by fragmentViewModel(GossipingEventsPaperTrailViewModel::class)
|
||||
|
||||
override fun invalidate() = withState(viewModel) { state ->
|
||||
epoxyController.setData(state)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
recyclerView.configureWith(epoxyController, showDivider = true)
|
||||
epoxyController.interactionListener = this
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
super.onDestroyView()
|
||||
recyclerView.cleanup()
|
||||
epoxyController.interactionListener = null
|
||||
}
|
||||
|
||||
override fun didTap(event: Event) {
|
||||
if (event.isEncrypted()) {
|
||||
event.toClearContentStringWithIndent()
|
||||
} else {
|
||||
event.toContentStringWithIndent()
|
||||
}?.let {
|
||||
JSonViewerDialog.newInstance(
|
||||
it,
|
||||
-1,
|
||||
jsonViewerStyler(colorProvider)
|
||||
).show(childFragmentManager, "JSON_VIEWER")
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,79 @@
|
||||
/*
|
||||
* 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.riotx.features.settings.devtools
|
||||
|
||||
import com.airbnb.mvrx.Async
|
||||
import com.airbnb.mvrx.FragmentViewModelContext
|
||||
import com.airbnb.mvrx.Loading
|
||||
import com.airbnb.mvrx.MvRxState
|
||||
import com.airbnb.mvrx.MvRxViewModelFactory
|
||||
import com.airbnb.mvrx.Success
|
||||
import com.airbnb.mvrx.Uninitialized
|
||||
import com.airbnb.mvrx.ViewModelContext
|
||||
import com.squareup.inject.assisted.Assisted
|
||||
import com.squareup.inject.assisted.AssistedInject
|
||||
import im.vector.matrix.android.api.session.Session
|
||||
import im.vector.matrix.android.api.session.events.model.Event
|
||||
import im.vector.riotx.core.platform.EmptyAction
|
||||
import im.vector.riotx.core.platform.EmptyViewEvents
|
||||
import im.vector.riotx.core.platform.VectorViewModel
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
data class GossipingEventsPaperTrailState(
|
||||
val events: Async<List<Event>> = Uninitialized
|
||||
) : MvRxState
|
||||
|
||||
class GossipingEventsPaperTrailViewModel @AssistedInject constructor(@Assisted initialState: GossipingEventsPaperTrailState,
|
||||
private val session: Session)
|
||||
: VectorViewModel<GossipingEventsPaperTrailState, EmptyAction, EmptyViewEvents>(initialState) {
|
||||
|
||||
init {
|
||||
refresh()
|
||||
}
|
||||
|
||||
fun refresh() {
|
||||
setState {
|
||||
copy(events = Loading())
|
||||
}
|
||||
GlobalScope.launch {
|
||||
session.cryptoService().getGossipingEventsTrail().let {
|
||||
val sorted = it.sortedByDescending { it.ageLocalTs }
|
||||
setState {
|
||||
copy(events = Success(sorted))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun handle(action: EmptyAction) {}
|
||||
|
||||
@AssistedInject.Factory
|
||||
interface Factory {
|
||||
fun create(initialState: GossipingEventsPaperTrailState): GossipingEventsPaperTrailViewModel
|
||||
}
|
||||
|
||||
companion object : MvRxViewModelFactory<GossipingEventsPaperTrailViewModel, GossipingEventsPaperTrailState> {
|
||||
|
||||
@JvmStatic
|
||||
override fun create(viewModelContext: ViewModelContext, state: GossipingEventsPaperTrailState): GossipingEventsPaperTrailViewModel? {
|
||||
val fragment: GossipingEventsPaperTrailFragment = (viewModelContext as FragmentViewModelContext).fragment()
|
||||
|
||||
return fragment.viewModelFactory.create(state)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,55 @@
|
||||
/*
|
||||
* 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.riotx.features.settings.devtools
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import com.airbnb.mvrx.fragmentViewModel
|
||||
import com.airbnb.mvrx.withState
|
||||
import im.vector.riotx.R
|
||||
import im.vector.riotx.core.extensions.cleanup
|
||||
import im.vector.riotx.core.extensions.configureWith
|
||||
import im.vector.riotx.core.platform.VectorBaseFragment
|
||||
import im.vector.riotx.core.resources.ColorProvider
|
||||
import kotlinx.android.synthetic.main.fragment_generic_recycler.*
|
||||
import javax.inject.Inject
|
||||
|
||||
class IncomingKeyRequestListFragment @Inject constructor(
|
||||
val viewModelFactory: KeyRequestListViewModel.Factory,
|
||||
private val epoxyController: KeyRequestEpoxyController,
|
||||
private val colorProvider: ColorProvider
|
||||
) : VectorBaseFragment() {
|
||||
|
||||
override fun getLayoutResId() = R.layout.fragment_generic_recycler
|
||||
|
||||
private val viewModel: KeyRequestListViewModel by fragmentViewModel(KeyRequestListViewModel::class)
|
||||
|
||||
override fun invalidate() = withState(viewModel) { state ->
|
||||
epoxyController.outgoing = false
|
||||
epoxyController.setData(state)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
recyclerView.configureWith(epoxyController, showDivider = true)
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
super.onDestroyView()
|
||||
recyclerView.cleanup()
|
||||
}
|
||||
}
|
@ -0,0 +1,164 @@
|
||||
/*
|
||||
* 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.riotx.features.settings.devtools
|
||||
|
||||
import com.airbnb.epoxy.TypedEpoxyController
|
||||
import com.airbnb.mvrx.Fail
|
||||
import com.airbnb.mvrx.Loading
|
||||
import com.airbnb.mvrx.Success
|
||||
import com.airbnb.mvrx.Uninitialized
|
||||
import im.vector.riotx.R
|
||||
import im.vector.riotx.core.epoxy.loadingItem
|
||||
import im.vector.riotx.core.extensions.exhaustive
|
||||
import im.vector.riotx.core.resources.StringProvider
|
||||
import im.vector.riotx.core.ui.list.genericFooterItem
|
||||
import im.vector.riotx.core.ui.list.genericItem
|
||||
import im.vector.riotx.core.ui.list.genericItemHeader
|
||||
import me.gujun.android.span.span
|
||||
import javax.inject.Inject
|
||||
|
||||
class KeyRequestEpoxyController @Inject constructor(
|
||||
private val stringProvider: StringProvider
|
||||
) : TypedEpoxyController<KeyRequestListViewState>() {
|
||||
|
||||
interface InteractionListener {
|
||||
// fun didTap(data: UserAccountData)
|
||||
}
|
||||
|
||||
var outgoing = true
|
||||
|
||||
var interactionListener: InteractionListener? = null
|
||||
|
||||
override fun buildModels(data: KeyRequestListViewState?) {
|
||||
if (outgoing) {
|
||||
buildOutgoing(data)
|
||||
} else {
|
||||
buildIncoming(data)
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildIncoming(data: KeyRequestListViewState?) {
|
||||
data?.incomingRequests?.let { async ->
|
||||
when (async) {
|
||||
is Uninitialized,
|
||||
is Loading -> {
|
||||
loadingItem {
|
||||
id("loadingOutgoing")
|
||||
loadingText(stringProvider.getString(R.string.loading))
|
||||
}
|
||||
}
|
||||
is Fail -> {
|
||||
genericItem {
|
||||
id("failOutgoing")
|
||||
title(async.error.localizedMessage)
|
||||
}
|
||||
}
|
||||
is Success -> {
|
||||
if (async.invoke().isEmpty()) {
|
||||
genericFooterItem {
|
||||
id("empty")
|
||||
text(stringProvider.getString(R.string.no_result_placeholder))
|
||||
}
|
||||
return
|
||||
}
|
||||
val requestList = async.invoke().groupBy { it.userId }
|
||||
|
||||
requestList.forEach {
|
||||
genericItemHeader {
|
||||
id(it.key)
|
||||
text("From user: ${it.key}")
|
||||
}
|
||||
it.value.forEach { roomKeyRequest ->
|
||||
genericItem {
|
||||
id(roomKeyRequest.requestId)
|
||||
title(roomKeyRequest.requestId)
|
||||
description(
|
||||
span {
|
||||
span("sessionId:") {
|
||||
textStyle = "bold"
|
||||
}
|
||||
span("\nFrom device:") {
|
||||
textStyle = "bold"
|
||||
}
|
||||
+"${roomKeyRequest.deviceId}"
|
||||
+"\n${roomKeyRequest.state.name}"
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}.exhaustive
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildOutgoing(data: KeyRequestListViewState?) {
|
||||
data?.outgoingRoomKeyRequest?.let { async ->
|
||||
when (async) {
|
||||
is Uninitialized,
|
||||
is Loading -> {
|
||||
loadingItem {
|
||||
id("loadingOutgoing")
|
||||
loadingText(stringProvider.getString(R.string.loading))
|
||||
}
|
||||
}
|
||||
is Fail -> {
|
||||
genericItem {
|
||||
id("failOutgoing")
|
||||
title(async.error.localizedMessage)
|
||||
}
|
||||
}
|
||||
is Success -> {
|
||||
if (async.invoke().isEmpty()) {
|
||||
genericFooterItem {
|
||||
id("empty")
|
||||
text(stringProvider.getString(R.string.no_result_placeholder))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
val requestList = async.invoke().groupBy { it.roomId }
|
||||
|
||||
requestList.forEach {
|
||||
genericItemHeader {
|
||||
id(it.key)
|
||||
text("roomId: ${it.key}")
|
||||
}
|
||||
it.value.forEach { roomKeyRequest ->
|
||||
genericItem {
|
||||
id(roomKeyRequest.requestId)
|
||||
title(roomKeyRequest.requestId)
|
||||
description(
|
||||
span {
|
||||
span("sessionId:\n") {
|
||||
textStyle = "bold"
|
||||
}
|
||||
+"${roomKeyRequest.sessionId}"
|
||||
span("\nstate:") {
|
||||
textStyle = "bold"
|
||||
}
|
||||
+"\n${roomKeyRequest.state.name}"
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}.exhaustive
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,87 @@
|
||||
/*
|
||||
* 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.riotx.features.settings.devtools
|
||||
|
||||
import com.airbnb.mvrx.Async
|
||||
import com.airbnb.mvrx.FragmentViewModelContext
|
||||
import com.airbnb.mvrx.MvRxState
|
||||
import com.airbnb.mvrx.MvRxViewModelFactory
|
||||
import com.airbnb.mvrx.Success
|
||||
import com.airbnb.mvrx.Uninitialized
|
||||
import com.airbnb.mvrx.ViewModelContext
|
||||
import com.squareup.inject.assisted.Assisted
|
||||
import com.squareup.inject.assisted.AssistedInject
|
||||
import im.vector.matrix.android.api.session.Session
|
||||
import im.vector.matrix.android.internal.crypto.IncomingRoomKeyRequest
|
||||
import im.vector.matrix.android.internal.crypto.OutgoingRoomKeyRequest
|
||||
import im.vector.riotx.core.platform.EmptyAction
|
||||
import im.vector.riotx.core.platform.EmptyViewEvents
|
||||
import im.vector.riotx.core.platform.VectorViewModel
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
data class KeyRequestListViewState(
|
||||
val incomingRequests: Async<List<IncomingRoomKeyRequest>> = Uninitialized,
|
||||
val outgoingRoomKeyRequest: Async<List<OutgoingRoomKeyRequest>> = Uninitialized
|
||||
) : MvRxState
|
||||
|
||||
class KeyRequestListViewModel @AssistedInject constructor(@Assisted initialState: KeyRequestListViewState,
|
||||
private val session: Session)
|
||||
: VectorViewModel<KeyRequestListViewState, EmptyAction, EmptyViewEvents>(initialState) {
|
||||
|
||||
init {
|
||||
refresh()
|
||||
}
|
||||
|
||||
fun refresh() {
|
||||
GlobalScope.launch {
|
||||
session.cryptoService().getOutgoingRoomKeyRequest().let {
|
||||
setState {
|
||||
copy(
|
||||
outgoingRoomKeyRequest = Success(it)
|
||||
)
|
||||
}
|
||||
}
|
||||
session.cryptoService().getIncomingRoomKeyRequest().let {
|
||||
setState {
|
||||
copy(
|
||||
incomingRequests = Success(it)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun handle(action: EmptyAction) {}
|
||||
|
||||
@AssistedInject.Factory
|
||||
interface Factory {
|
||||
fun create(initialState: KeyRequestListViewState): KeyRequestListViewModel
|
||||
}
|
||||
|
||||
companion object : MvRxViewModelFactory<KeyRequestListViewModel, KeyRequestListViewState> {
|
||||
|
||||
@JvmStatic
|
||||
override fun create(viewModelContext: ViewModelContext, state: KeyRequestListViewState): KeyRequestListViewModel? {
|
||||
val context = viewModelContext as FragmentViewModelContext
|
||||
val factory = (context.fragment as? IncomingKeyRequestListFragment)?.viewModelFactory
|
||||
?: (context.fragment as? OutgoingKeyRequestListFragment)?.viewModelFactory
|
||||
|
||||
return factory?.create(state)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,105 @@
|
||||
/*
|
||||
* 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.riotx.features.settings.devtools
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.viewpager2.adapter.FragmentStateAdapter
|
||||
import androidx.viewpager2.widget.ViewPager2
|
||||
import androidx.viewpager2.widget.ViewPager2.SCROLL_STATE_IDLE
|
||||
import com.google.android.material.tabs.TabLayoutMediator
|
||||
import im.vector.riotx.R
|
||||
import im.vector.riotx.core.platform.VectorBaseActivity
|
||||
import im.vector.riotx.core.platform.VectorBaseFragment
|
||||
import kotlinx.android.synthetic.main.fragment_devtool_keyrequests.*
|
||||
import javax.inject.Inject
|
||||
|
||||
class KeyRequestsFragment @Inject constructor() : VectorBaseFragment() {
|
||||
|
||||
override fun getLayoutResId(): Int = R.layout.fragment_devtool_keyrequests
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
(activity as? VectorBaseActivity)?.supportActionBar?.setTitle(R.string.key_share_request)
|
||||
}
|
||||
|
||||
private var mPagerAdapter: KeyReqPagerAdapter? = null
|
||||
|
||||
private val pageAdapterListener = object : ViewPager2.OnPageChangeCallback() {
|
||||
override fun onPageSelected(position: Int) {
|
||||
invalidateOptionsMenu()
|
||||
}
|
||||
|
||||
override fun onPageScrollStateChanged(state: Int) {
|
||||
childFragmentManager.fragments.forEach {
|
||||
setHasOptionsMenu(state == SCROLL_STATE_IDLE)
|
||||
}
|
||||
invalidateOptionsMenu()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
invalidateOptionsMenu()
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
mPagerAdapter = KeyReqPagerAdapter(this)
|
||||
devToolKeyRequestPager.adapter = mPagerAdapter
|
||||
devToolKeyRequestPager.registerOnPageChangeCallback(pageAdapterListener)
|
||||
|
||||
TabLayoutMediator(devToolKeyRequestTabs, devToolKeyRequestPager) { tab, position ->
|
||||
when (position) {
|
||||
0 -> {
|
||||
tab.text = "Outgoing"
|
||||
}
|
||||
1 -> {
|
||||
tab.text = "Incoming"
|
||||
}
|
||||
2 -> {
|
||||
tab.text = "Audit Trail"
|
||||
}
|
||||
}
|
||||
}.attach()
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
devToolKeyRequestPager.unregisterOnPageChangeCallback(pageAdapterListener)
|
||||
mPagerAdapter = null
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
private inner class KeyReqPagerAdapter(fa: Fragment) : FragmentStateAdapter(fa) {
|
||||
override fun getItemCount(): Int = 3
|
||||
|
||||
override fun createFragment(position: Int): Fragment {
|
||||
return when (position) {
|
||||
0 -> {
|
||||
childFragmentManager.fragmentFactory.instantiate(requireContext().classLoader, OutgoingKeyRequestListFragment::class.java.name)
|
||||
}
|
||||
1 -> {
|
||||
childFragmentManager.fragmentFactory.instantiate(requireContext().classLoader, IncomingKeyRequestListFragment::class.java.name)
|
||||
}
|
||||
else -> {
|
||||
childFragmentManager.fragmentFactory.instantiate(requireContext().classLoader, GossipingEventsPaperTrailFragment::class.java.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,55 @@
|
||||
/*
|
||||
* 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.riotx.features.settings.devtools
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import com.airbnb.mvrx.fragmentViewModel
|
||||
import com.airbnb.mvrx.withState
|
||||
import im.vector.riotx.R
|
||||
import im.vector.riotx.core.extensions.cleanup
|
||||
import im.vector.riotx.core.extensions.configureWith
|
||||
import im.vector.riotx.core.platform.VectorBaseFragment
|
||||
import im.vector.riotx.core.resources.ColorProvider
|
||||
import kotlinx.android.synthetic.main.fragment_generic_recycler.*
|
||||
import javax.inject.Inject
|
||||
|
||||
class OutgoingKeyRequestListFragment @Inject constructor(
|
||||
val viewModelFactory: KeyRequestListViewModel.Factory,
|
||||
private val epoxyController: KeyRequestEpoxyController,
|
||||
private val colorProvider: ColorProvider
|
||||
) : VectorBaseFragment() {
|
||||
|
||||
override fun getLayoutResId() = R.layout.fragment_generic_recycler
|
||||
private val viewModel: KeyRequestListViewModel by fragmentViewModel(KeyRequestListViewModel::class)
|
||||
|
||||
override fun invalidate() = withState(viewModel) { state ->
|
||||
epoxyController.setData(state)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
recyclerView.configureWith(epoxyController, showDivider = true)
|
||||
// epoxyController.interactionListener = this
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
super.onDestroyView()
|
||||
recyclerView.cleanup()
|
||||
// epoxyController.interactionListener = null
|
||||
}
|
||||
}
|
22
vector/src/main/res/layout/fragment_devtool_keyrequests.xml
Normal file
22
vector/src/main/res/layout/fragment_devtool_keyrequests.xml
Normal file
@ -0,0 +1,22 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
tools:background="#FFFFFF">
|
||||
|
||||
<com.google.android.material.tabs.TabLayout
|
||||
android:id="@+id/devToolKeyRequestTabs"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:tabMode="scrollable" />
|
||||
|
||||
<androidx.viewpager2.widget.ViewPager2
|
||||
android:id="@+id/devToolKeyRequestPager"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1" />
|
||||
|
||||
</LinearLayout>
|
@ -301,7 +301,7 @@
|
||||
<string name="e2e_need_log_in_again">You need to log back in to generate end-to-end encryption keys for this session and submit the public key to your homeserver.\nThis is a once off.\nSorry for the inconvenience.</string>
|
||||
|
||||
<!-- request again e2e key -->
|
||||
<string name="e2e_re_request_encryption_key"><u>Re-request encryption keys</u> from your other sessions.</string>
|
||||
<string name="e2e_re_request_encryption_key">Re-request encryption keys from your other sessions.</string>
|
||||
|
||||
<string name="e2e_re_request_encryption_key_sent">Key request sent.</string>
|
||||
|
||||
|
@ -7,6 +7,13 @@
|
||||
|
||||
<!-- BEGIN Strings added by Valere -->
|
||||
|
||||
|
||||
<string name="settings_key_requests">Key Requests</string>
|
||||
|
||||
<string name="e2e_use_keybackup">Unlock encrypted messages history</string>
|
||||
|
||||
<string name="refresh">Refresh</string>
|
||||
|
||||
<!-- END Strings added by Valere -->
|
||||
|
||||
|
||||
@ -18,7 +25,6 @@
|
||||
<string name="settings_when_rooms_are_upgraded">When rooms are upgraded</string>
|
||||
<string name="settings_troubleshoot_title">Troubleshoot</string>
|
||||
<string name="settings_notification_advanced_summary_riotx">Set notification importance by event</string>
|
||||
<string name="refresh">Refresh</string>
|
||||
<!-- END Strings added by Benoit -->
|
||||
|
||||
|
||||
|
@ -65,13 +65,20 @@
|
||||
app:fragment="im.vector.riotx.features.settings.push.PushRulesFragment" />
|
||||
</im.vector.riotx.core.preference.VectorPreferenceCategory>
|
||||
|
||||
<im.vector.riotx.core.preference.VectorPreferenceCategory android:title="@string/settings_dev_tools">
|
||||
<im.vector.riotx.core.preference.VectorPreferenceCategory
|
||||
android:dependency="SETTINGS_DEVELOPER_MODE_PREFERENCE_KEY"
|
||||
android:title="@string/settings_dev_tools">
|
||||
|
||||
<im.vector.riotx.core.preference.VectorPreference
|
||||
android:persistent="false"
|
||||
android:title="@string/settings_account_data"
|
||||
app:fragment="im.vector.riotx.features.settings.devtools.AccountDataFragment" />
|
||||
|
||||
<im.vector.riotx.core.preference.VectorPreference
|
||||
android:persistent="false"
|
||||
android:title="@string/settings_key_requests"
|
||||
app:fragment="im.vector.riotx.features.settings.devtools.KeyRequestsFragment" />
|
||||
|
||||
</im.vector.riotx.core.preference.VectorPreferenceCategory>
|
||||
|
||||
</androidx.preference.PreferenceScreen>
|
Loading…
Reference in New Issue
Block a user