Gossiping refactoring

This commit is contained in:
Valere 2020-03-06 18:29:15 +01:00
parent 3639007985
commit fc6225a7ac
62 changed files with 2811 additions and 984 deletions

View file

@ -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.2'
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"

View file

@ -0,0 +1,210 @@
/*
* 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())
// @Before
// fun setup() {
// mockkStatic(Log::class)
// every { Log.v(any(), any()) } returns 0
// every { Log.d(any(), any()) } returns 0
// every { Log.i(any(), any()) } returns 0
// every { Log.e(any(), any()) } returns 0
//// every { Log.println(any(), any(), any()) } returns 0
//// every { Log.wtf(any(), any(), any()) } returns 0
// }
@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)
}
}

View file

@ -16,6 +16,7 @@
package im.vector.matrix.android.internal.crypto.keysbackup
import android.util.Log
import androidx.test.ext.junit.runners.AndroidJUnit4
import im.vector.matrix.android.InstrumentedTest
import im.vector.matrix.android.api.listeners.ProgressListener
@ -34,6 +35,7 @@ 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.ShareRequestState
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
@ -41,12 +43,15 @@ import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.KeysVersio
import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.KeysVersionResult
import im.vector.matrix.android.internal.crypto.model.ImportRoomKeysResult
import im.vector.matrix.android.internal.crypto.model.OlmInboundGroupSessionWrapper
import io.mockk.every
import io.mockk.mockkStatic
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Assert.fail
import org.junit.BeforeClass
import org.junit.FixMethodOrder
import org.junit.Test
import org.junit.runner.RunWith
@ -325,46 +330,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(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)
}
// @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

View file

@ -18,6 +18,7 @@ package im.vector.matrix.android.internal.crypto.verification.qrcode
import androidx.test.ext.junit.runners.AndroidJUnit4
import im.vector.matrix.android.InstrumentedTest
import im.vector.matrix.android.internal.crypto.crosssigning.fromBase64NoPadding
import org.amshove.kluent.shouldBeNull
import org.amshove.kluent.shouldEqual
import org.amshove.kluent.shouldEqualTo

View file

@ -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
)

View file

@ -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
}
}

View file

@ -26,6 +26,7 @@ import im.vector.matrix.android.api.session.crypto.keyshare.GossipingRequestList
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
@ -87,6 +88,8 @@ interface CryptoService {
fun getDeviceInfo(userId: String, deviceId: String?): CryptoDeviceInfo?
fun requestRoomKeyForEvent(event: Event)
fun reRequestRoomKeyForEvent(event: Event)
fun cancelRoomKeyRequest(requestBody: RoomKeyRequestBody)
@ -132,4 +135,6 @@ interface CryptoService {
fun removeSessionListener(listener: NewSessionListener)
fun getOutgoingRoomKeyRequest(): List<OutgoingRoomKeyRequest>
fun getIncomingRoomKeyRequest(): List<IncomingRoomKeyRequest>
fun getGossipingEventsTrail(): List<Event>
}

View file

@ -68,4 +68,7 @@ interface CrossSigningService {
fun checkDeviceTrust(otherUserId: String,
otherDeviceId: String,
locallyTrusted: Boolean?): DeviceTrustResult
fun onSecretSSKGossip(sskPrivateKey: String): Boolean
fun onSecretUSKGossip(uskPrivateKey: String): Boolean
}

View file

@ -111,6 +111,9 @@ 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?

View file

@ -0,0 +1,119 @@
/*
* 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)
}
}
}
}

View file

@ -33,6 +33,8 @@ 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.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
@ -47,6 +49,7 @@ import im.vector.matrix.android.internal.crypto.actions.SetDeviceVerificationAct
import im.vector.matrix.android.internal.crypto.algorithms.IMXEncrypting
import im.vector.matrix.android.internal.crypto.algorithms.megolm.MXMegolmEncryptionFactory
import im.vector.matrix.android.internal.crypto.algorithms.olm.MXOlmEncryptionFactory
import im.vector.matrix.android.internal.crypto.algorithms.olm.OlmDecryptionResult
import im.vector.matrix.android.internal.crypto.crosssigning.DefaultCrossSigningService
import im.vector.matrix.android.internal.crypto.crosssigning.DeviceTrustLevel
import im.vector.matrix.android.internal.crypto.keysbackup.DefaultKeysBackupService
@ -55,7 +58,9 @@ import im.vector.matrix.android.internal.crypto.model.ImportRoomKeysResult
import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo
import im.vector.matrix.android.internal.crypto.model.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 +85,7 @@ import im.vector.matrix.android.internal.session.room.membership.LoadRoomMembers
import im.vector.matrix.android.internal.session.room.membership.RoomMemberHelper
import im.vector.matrix.android.internal.session.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
@ -137,7 +143,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,
@ -189,6 +195,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
@ -207,6 +214,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)
@ -215,6 +223,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)
@ -231,6 +240,7 @@ internal class DefaultCryptoService @Inject constructor(
override fun getDevicesList(callback: MatrixCallback<DevicesListResponse>) {
getDevicesTask
.configureWith {
this.executionThread = TaskThread.CRYPTO
this.callback = callback
}
.executeBy(taskExecutor)
@ -239,6 +249,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)
@ -301,7 +312,6 @@ internal class DefaultCryptoService @Inject constructor(
runCatching {
uploadDeviceKeys()
oneTimeKeysUploader.maybeUploadOneTimeKeys()
outgoingRoomKeyRequestManager.start()
keysBackupService.checkAndStartKeysBackup()
if (isInitialSync) {
// refresh the devices list for each known room members
@ -329,8 +339,6 @@ internal class DefaultCryptoService @Inject constructor(
fun close() = runBlocking(coroutineDispatchers.crypto) {
cryptoCoroutineScope.coroutineContext.cancelChildren(CancellationException("Closing crypto module"))
outgoingRoomKeyRequestManager.stop()
olmDevice.release()
cryptoStore.close()
}
@ -689,18 +697,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 -> {
// 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 -> {
// incomingRoomKeyRequestManager.onGossipingRequestEvent(event)
cryptoStore.saveGossipingEvent(event)
onSecretSendReceived(event)
}
else -> {
// ignore
@ -716,6 +730,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
@ -728,6 +743,78 @@ internal class DefaultCryptoService @Inject constructor(
alg.onRoomKeyEvent(event, keysBackupService)
}
private fun onSecretSendReceived(event: Event) {
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 encryptedEventContent = event.getClearContent().toModel<EncryptedEventContent>()
?: return Unit.also {
Timber.e("## onSecretSend() :Received malformed secret send event")
}
val senderDevice = encryptedEventContent.senderKey
val device = senderDevice?.let { cryptoStore.getUserDevice(event.senderId, it) }
?: return Unit.also {
Timber.e("## processIncomingSecretShareRequest() : Received secret share request from unknown device ${senderDevice}")
}
try {
val result = decryptEvent(event, "gossip")
event.mxDecryptionResult = OlmDecryptionResult(
payload = result.clearEvent,
senderKey = result.senderCurve25519Key,
keysClaimed = result.claimedEd25519Key?.let { k -> mapOf("ed25519" to k) },
forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain
)
} catch (failure: Throwable) {
Timber.i("## onSecretSend() :Failed to decrypt secret share: $device")
}
val secretContent = event.getClearContent().toModel<SecretSendEventContent>() ?: return
val existingRequest = cryptoStore
.getPendingIncomingGossipingRequests()
.firstOrNull { it.requestId == secretContent.requestId } as? IncomingSecretShareRequest
if (existingRequest == null) {
Timber.i("## onSecretSend() :Received secret from unknown request id: ${secretContent.requestId} from device ")
return
}
if (device.isBlocked || !device.isVerified) {
// Ignore secrets from this
Timber.i("## onSecretSend() :Received secret from untrusted/blocked device: ${device}")
return
}
when (existingRequest.secretName) {
SELF_SIGNING_KEY_SSSS_NAME -> {
if (device.trustLevel?.isLocallyVerified() == true) {
crossSigningService.onSecretSSKGossip(secretContent.secretValue)
return
}
}
USER_SIGNING_KEY_SSSS_NAME -> {
if (device.trustLevel?.isLocallyVerified() == true) {
cryptoStore.storePrivateKeysInfo(null, null, secretContent.secretValue)
}
}
else -> {
// Ask to application layer?
}
}
}
/**
* Handle an m.room.encryption event.
*
@ -1003,14 +1090,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)
}
/**
@ -1019,20 +1106,36 @@ 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}")
}
}
}
/**
@ -1090,9 +1193,9 @@ 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 + ")"
@ -1101,4 +1204,12 @@ internal class DefaultCryptoService @Inject constructor(
override fun getOutgoingRoomKeyRequest(): List<OutgoingRoomKeyRequest> {
return cryptoStore.getOutgoingRoomKeyRequests()
}
override fun getIncomingRoomKeyRequest(): List<IncomingRoomKeyRequest> {
return cryptoStore.getIncomingRoomKeyRequests()
}
override fun getGossipingEventsTrail(): List<Event> {
return cryptoStore.getGossipingEventsTrail()
}
}

View file

@ -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
}

View file

@ -37,7 +37,8 @@ data class IncomingRequestCancellation(
/**
* The request id
*/
override val requestId: String? = null
override val requestId: String? = null,
override val localCreationTimestamp: Long?
) : IncomingShareRequestCommon {
companion object {
/**
@ -52,7 +53,8 @@ data class IncomingRequestCancellation(
IncomingRequestCancellation(
userId = event.senderId,
deviceId = it.requestingDeviceId,
requestId = it.requestId
requestId = it.requestId,
localCreationTimestamp = event.ageLocalTs ?: System.currentTimeMillis()
)
}
}

View file

@ -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,7 +58,8 @@ data class IncomingRoomKeyRequest(
* The runnable to call to ignore the key share request.
*/
@Transient
var ignore: Runnable? = null
var ignore: Runnable? = null,
override val localCreationTimestamp: Long?
) : IncomingShareRequestCommon {
companion object {
/**
@ -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()
)
}
}

View file

@ -17,7 +17,7 @@
package im.vector.matrix.android.internal.crypto
import im.vector.matrix.android.api.auth.data.Credentials
import im.vector.matrix.android.api.session.crypto.crosssigning.CrossSigningService
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
@ -35,8 +35,8 @@ import javax.inject.Inject
internal class IncomingRoomKeyRequestManager @Inject constructor(
private val credentials: Credentials,
private val cryptoStore: IMXCryptoStore,
private val crossSigningService: CrossSigningService,
private val secrSecretCryptoProvider: ShareSecretCryptoProvider,
private val cryptoConfig: MXCryptoConfig,
private val secretSecretCryptoProvider: ShareSecretCryptoProvider,
private val roomDecryptorProvider: RoomDecryptorProvider) {
// list of IncomingRoomKeyRequests/IncomingRoomKeyRequestCancellations
@ -48,8 +48,7 @@ internal class IncomingRoomKeyRequestManager @Inject constructor(
private val gossipingRequestListeners: MutableSet<GossipingRequestListener> = HashSet()
init {
receiveGossipingRequests.addAll(cryptoStore.getPendingIncomingSecretShareRequests())
receiveGossipingRequests.addAll(cryptoStore.getPendingIncomingRoomKeyRequests())
receiveGossipingRequests.addAll(cryptoStore.getPendingIncomingGossipingRequests())
}
/**
@ -59,21 +58,37 @@ internal class IncomingRoomKeyRequestManager @Inject constructor(
* @param event the announcement event.
*/
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 {
// save in DB
cryptoStore.storeIncomingGossipingRequest(it, ageLocalTs)
receiveGossipingRequests.add(it)
}
} else if (event.getClearType() == EventType.ROOM_KEY_REQUEST) {
IncomingRoomKeyRequest.fromEvent(event)?.let {
receiveGossipingRequests.add(it)
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)
receiveGossipingRequests.add(it)
}
}
}
}
GossipingToDeviceObject.ACTION_SHARE_CANCELLATION -> IncomingRequestCancellation.fromEvent(event)?.let { receivedRequestCancellations.add(it) }
else -> Timber.e("## onGossipingRequestEvent() : unsupported action ${roomKeyShare?.action}")
GossipingToDeviceObject.ACTION_SHARE_CANCELLATION -> {
IncomingRequestCancellation.fromEvent(event)?.let {
receivedRequestCancellations.add(it)
}
}
else -> {
Timber.e("## onGossipingRequestEvent() : unsupported action ${roomKeyShare?.action}")
}
}
}
@ -83,6 +98,8 @@ internal class IncomingRoomKeyRequestManager @Inject constructor(
* It must be called on CryptoThread
*/
fun processReceivedGossipingRequests() {
Timber.v("## processReceivedGossipingRequests()")
val roomKeyRequestsToProcess = receiveGossipingRequests.toList()
receiveGossipingRequests.clear()
for (request in roomKeyRequestsToProcess) {
@ -102,16 +119,25 @@ internal class IncomingRoomKeyRequestManager @Inject constructor(
}
}
if (null != receivedRequestCancellations) {
for (request in receivedRequestCancellations!!) {
Timber.v("## ## processReceivedGossipingRequests() : 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)
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)
}
}
}
}
@ -123,11 +149,11 @@ internal class IncomingRoomKeyRequestManager @Inject constructor(
val roomId = body!!.roomId
val alg = body.algorithm
Timber.v("m.room_key_request from $userId:$deviceId for $roomId / ${body.sessionId} id ${request.requestId}")
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.deleteIncomingRoomKeyRequest(request)
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?
@ -136,89 +162,110 @@ internal class IncomingRoomKeyRequestManager @Inject constructor(
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.deleteIncomingRoomKeyRequest(request)
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED)
return
}
if (credentials.deviceId == deviceId && credentials.userId == userId) {
Timber.v("## processReceivedGossipingRequests() : oneself device - ignored")
cryptoStore.deleteIncomingRoomKeyRequest(request)
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED)
return
}
request.share = Runnable {
decryptor.shareKeysWithDevice(request)
cryptoStore.deleteIncomingRoomKeyRequest(request)
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.ACCEPTED)
}
request.ignore = Runnable {
cryptoStore.deleteIncomingRoomKeyRequest(request)
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")
cryptoStore.deleteIncomingRoomKeyRequest(request)
request.share?.run()
return
}
if (device.isBlocked) {
Timber.v("## processReceivedGossipingRequests() : device is blocked -> ignored")
cryptoStore.deleteIncomingRoomKeyRequest(request)
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED)
return
}
}
// If cross signing is available on account we automatically discard untrust devices request
if (cryptoStore.getMyCrossSigningInfo() != null) {
// 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.deleteIncomingRoomKeyRequest(request)
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED)
return
}
cryptoStore.storeIncomingRoomKeyRequest(request)
// Legacy, pass to application layer to decide what to do
// Pass to application layer to decide what to do
onRoomKeyRequest(request)
}
private fun processIncomingSecretShareRequest(request: IncomingSecretShareRequest) {
val secretName = request.secretName ?: return Unit.also {
cryptoStore.deleteIncomingSecretRequest(request)
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 user")
cryptoStore.deleteIncomingRoomKeyRequest(request)
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)
}
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 (!device.isVerified || device.isBlocked) {
Timber.v("## processIncomingSecretShareRequest() : Ignoring secret share request from untrusted/blocked session $device")
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED)
return
}
val isDeviceLocallyVerified = cryptoStore.getUserDevice(userId, deviceId)?.trustLevel?.isLocallyVerified()
//Should SDK always Silently reject any request for the master key?
when (secretName) {
SELF_SIGNING_KEY_SSSS_NAME -> cryptoStore.getCrossSigningPrivateKeys()?.selfSigned
USER_SIGNING_KEY_SSSS_NAME -> cryptoStore.getCrossSigningPrivateKeys()?.user
else -> null
}?.let { secretValue ->
// TODO check if locally trusted and not outdated
if (cryptoStore.getUserDevice(userId, request.deviceId ?: "")?.trustLevel?.isLocallyVerified() == true) {
secrSecretCryptoProvider.shareSecretWithDevice(request, secretValue)
cryptoStore.deleteIncomingRoomKeyRequest(request)
Timber.i("## processIncomingSecretShareRequest() : Sharing secret $secretName with $device locally trusted")
if (isDeviceLocallyVerified == true) {
secretSecretCryptoProvider.shareSecretWithDevice(request, secretValue)
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.ACCEPTED)
}
return
}
request.ignore = Runnable {
cryptoStore.deleteIncomingRoomKeyRequest(request)
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED)
}
request.share = { secretValue ->
secrSecretCryptoProvider.shareSecretWithDevice(request, secretValue)
cryptoStore.deleteIncomingRoomKeyRequest(request)
secretSecretCryptoProvider.shareSecretWithDevice(request, secretValue)
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.ACCEPTED)
}
onShareRequest(request)

View file

@ -54,7 +54,9 @@ data class IncomingSecretShareRequest(
* The runnable to call to ignore the key share request.
*/
@Transient
var ignore: Runnable? = null
var ignore: Runnable? = null,
override val localCreationTimestamp: Long?
) : IncomingShareRequestCommon {
companion object {
@ -71,7 +73,8 @@ data class IncomingSecretShareRequest(
userId = event.senderId,
deviceId = it.requestingDeviceId,
requestId = it.requestId,
secretName = it.secretName
secretName = it.secretName,
localCreationTimestamp = event.ageLocalTs ?: System.currentTimeMillis()
)
}
}

View file

@ -31,4 +31,6 @@ interface IncomingShareRequestCommon {
* The request id
*/
val requestId: String?
val localCreationTimestamp: Long?
}

View file

@ -16,10 +16,10 @@
package im.vector.matrix.android.internal.crypto
interface OutgoingShareRequest {
var recipients: List<Map<String, String>>
interface OutgoingGossipingRequest {
var recipients: Map<String, List<String>>
var requestId: String
var state: ShareRequestState
var state: OutgoingGossipingRequestState
// transaction id for the cancellation, if any
var cancellationTxnId: String?
//var cancellationTxnId: String?
}

View file

@ -0,0 +1,185 @@
/*
* 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.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) {
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)
}
}

View file

@ -17,22 +17,27 @@
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
override 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
override var requestId: String, // current state of this request
override var state: ShareRequestState) : OutgoingShareRequest {
override var state: OutgoingGossipingRequestState
// transaction id for the cancellation, if any
// override var cancellationTxnId: String? = null
) : OutgoingGossipingRequest {
// transaction id for the cancellation, if any
override var cancellationTxnId: String? = null
/**
* Used only for log.

View file

@ -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.ShareRequestCancellation
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(), ShareRequestState.UNSENT))
if (req?.state == ShareRequestState.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) {
ShareRequestState.CANCELLATION_PENDING,
ShareRequestState.CANCELLATION_PENDING_AND_WILL_RESEND -> {
// nothing to do here
}
ShareRequestState.UNSENT,
ShareRequestState.FAILED -> {
Timber.v("## cancelRoomKeyRequest() : deleting unnecessary room key request for $requestBody")
cryptoStore.deleteOutgoingRoomKeyRequest(req.requestId)
}
ShareRequestState.SENT -> {
if (andResend) {
req.state = ShareRequestState.CANCELLATION_PENDING_AND_WILL_RESEND
} else {
req.state = ShareRequestState.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(ShareRequestState.UNSENT,
ShareRequestState.CANCELLATION_PENDING,
ShareRequestState.CANCELLATION_PENDING_AND_WILL_RESEND))
if (null == outgoingRoomKeyRequest) {
Timber.v("## sendOutgoingRoomKeyRequests() : No more outgoing room key requests")
sendOutgoingRoomKeyRequestsRunning.set(false)
return
}
if (ShareRequestState.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: ShareRequestState) {
if (request.state !== ShareRequestState.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(ShareRequestState.SENT)
}
override fun onFailure(failure: Throwable) {
Timber.e("## sendOutgoingRoomKeyRequest failed")
onDone(ShareRequestState.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 = ShareRequestCancellation(
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 === ShareRequestState.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")
}
}

View file

@ -16,21 +16,23 @@
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
var secretName: String?,
val secretName: String?,
// list of recipients for the request
override var recipients: List<Map<String, String>>,
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: ShareRequestState) : OutgoingShareRequest {
override var state: OutgoingGossipingRequestState) : OutgoingGossipingRequest {
// transaction id for the cancellation, if any
override var cancellationTxnId: String? = null
}

View file

@ -0,0 +1,150 @@
/*
* 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.database.mapper.ContentMapper
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)
}
}
}
}

View file

@ -16,8 +16,10 @@
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
@ -25,6 +27,7 @@ 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
@ -32,6 +35,7 @@ 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
@ -61,4 +65,10 @@ internal class ShareSecretCryptoProvider @Inject constructor(
}
}
}
fun decryptEvent(event: Event): MXEventDecryptionResult {
return runBlocking(coroutineDispatchers.crypto) {
olmDecryptionFactory.create().decryptEvent(event, ShareSecretCryptoProvider::class.java.name ?: "")
}
}
}

View file

@ -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!!)

View file

@ -68,4 +68,6 @@ internal interface IMXDecrypting {
fun shareKeysWithDevice(request: IncomingRoomKeyRequest) {}
fun shareSecretWithDevice(request: IncomingSecretShareRequest, secretValue : String) {}
fun requestKeysForEvent(event: Event)
}

View file

@ -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,25 +144,26 @@ 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(
roomId = event.roomId,
algorithm = encryptedEventContent.algorithm,
@ -170,7 +171,7 @@ internal class MXMegolmDecryption(private val userId: String,
sessionId = encryptedEventContent.sessionId
)
outgoingRoomKeyRequestManager.sendRoomKeyRequest(requestBody, recipients)
outgoingGossipingRequestManager.sendRoomKeyRequest(requestBody, recipients)
}
/**
@ -271,7 +272,7 @@ internal class MXMegolmDecryption(private val userId: String,
senderKey = senderKey
)
outgoingRoomKeyRequestManager.cancelRoomKeyRequest(content)
outgoingGossipingRequestManager.cancelRoomKeyRequest(content)
onNewSession(senderKey, roomKeyContent.sessionId)
}

View file

@ -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,

View file

@ -210,4 +210,8 @@ internal class MXOlmDecryption(
return res["payload"]
}
override fun requestKeysForEvent(event: Event) {
// nop
}
}

View file

@ -25,7 +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.OutgoingRoomKeyRequestManager
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
@ -43,6 +43,7 @@ import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
import im.vector.matrix.android.internal.util.withoutPrefix
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import org.greenrobot.eventbus.EventBus
import org.matrix.olm.OlmPkSigning
import org.matrix.olm.OlmUtility
@ -62,7 +63,7 @@ internal class DefaultCrossSigningService @Inject constructor(
private val taskExecutor: TaskExecutor,
private val coroutineDispatchers: MatrixCoroutineDispatchers,
private val cryptoCoroutineScope: CoroutineScope,
private val outgoingRoomKeyRequestManager: OutgoingRoomKeyRequestManager,
private val outgoingGossipingRequestManager: OutgoingGossipingRequestManager,
private val eventBus: EventBus) : CrossSigningService, DeviceListManager.UserDevicesUpdateListener {
private var olmUtility: OlmUtility? = null
@ -299,6 +300,58 @@ internal class DefaultCrossSigningService @Inject constructor(
cryptoStore.clearOtherUserTrust()
}
override fun onSecretSSKGossip(sskPrivateKey: String): Boolean {
Timber.i("## CrossSigning - onSecretSSKGossip")
return runBlocking(coroutineDispatchers.crypto) {
val mxCrossSigningInfo = getMyCrossSigningKeys() ?: return@runBlocking false
sskPrivateKey.fromBase64NoPadding()
.let { privateKeySeed ->
val pkSigning = OlmPkSigning()
try {
if (pkSigning.initWithSeed(privateKeySeed) == mxCrossSigningInfo.selfSigningKey()?.unpaddedBase64PublicKey) {
selfSigningPkSigning?.releaseSigning()
selfSigningPkSigning = pkSigning
Timber.i("## CrossSigning - Loading SSK success")
cryptoStore.storePrivateKeysInfo(null, null, sskPrivateKey)
return@runBlocking true
} else {
pkSigning.releaseSigning()
}
} catch (failure: Throwable) {
pkSigning.releaseSigning()
}
}
return@runBlocking false
}
}
override fun onSecretUSKGossip(uskPrivateKey: String): Boolean {
Timber.i("## CrossSigning - onSecretUSKGossip")
return runBlocking(coroutineDispatchers.crypto) {
val mxCrossSigningInfo = getMyCrossSigningKeys() ?: return@runBlocking false
uskPrivateKey.fromBase64NoPadding()
.let { privateKeySeed ->
val pkSigning = OlmPkSigning()
try {
if (pkSigning.initWithSeed(privateKeySeed) == mxCrossSigningInfo.selfSigningKey()?.unpaddedBase64PublicKey) {
userPkSigning?.releaseSigning()
userPkSigning = pkSigning
Timber.i("## CrossSigning - Loading USK success")
cryptoStore.storePrivateKeysInfo(null, uskPrivateKey, null)
return@runBlocking true
} else {
pkSigning.releaseSigning()
}
} catch (failure: Throwable) {
pkSigning.releaseSigning()
}
}
return@runBlocking false
}
}
override fun checkTrustFromPrivateKeys(masterKeyPrivateKey: String?,
uskKeyPrivateKey: String?,
sskPrivateKey: String?
@ -549,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 {
@ -738,7 +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()
// reRequestAllPendingRoomKeyRequest()
cryptoStore.updateUsersTrust {
users.add(it)
checkUserTrust(it).isVerified()
@ -755,16 +814,18 @@ internal class DefaultCrossSigningService @Inject constructor(
}
}
private fun reRequestAllPendingRoomKeyRequest() {
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)
}
}
}
}
// 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)
// }
// }
// }
// }
// }
}

View file

@ -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

View file

@ -22,7 +22,7 @@ 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 GossipingToDeviceObject : SendToDeviceObject {
interface GossipingToDeviceObject : SendToDeviceObject {
val action: String?

View file

@ -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,16 @@ 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) }
}
}
}

View file

@ -23,7 +23,7 @@ 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? = GossipingToDeviceObject.ACTION_SHARE_REQUEST,

View file

@ -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))
)
}
}

View file

@ -19,13 +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.IncomingShareRequestCommon
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.NewSessionListener
import im.vector.matrix.android.internal.crypto.OutgoingGossipingRequestState
import im.vector.matrix.android.internal.crypto.OutgoingRoomKeyRequest
import im.vector.matrix.android.internal.crypto.ShareRequestState
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
@ -118,7 +120,10 @@ internal interface IMXCryptoStore {
* @return the pending IncomingRoomKeyRequest requests
*/
fun getPendingIncomingRoomKeyRequests(): List<IncomingRoomKeyRequest>
fun getPendingIncomingSecretShareRequests(): List<IncomingSecretShareRequest>
fun getPendingIncomingGossipingRequests(): List<IncomingShareRequestCommon>
fun storeIncomingGossipingRequest(request: IncomingShareRequestCommon, ageLocalTS: Long?)
// fun getPendingIncomingSecretShareRequests(): List<IncomingSecretShareRequest>
/**
* Indicate if the store contains data for the passed account.
@ -190,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.
@ -209,6 +214,7 @@ internal interface IMXCryptoStore {
// TODO temp
fun getLiveDeviceList(): LiveData<List<CryptoDeviceInfo>>
/**
* Store the crypto algorithm for a room.
*
@ -350,45 +356,49 @@ 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?
fun getOrAddOutgoingSecretShareRequest(secretName: String, recipients: Map<String, List<String>>): OutgoingSecretRequest?
fun saveGossipingEvent(event: Event)
/**
* Look for room key requests by state.
*
* @param states the states
* @return an OutgoingRoomKeyRequest or null
*/
fun getOutgoingRoomKeyRequestByState(states: Set<ShareRequestState>): OutgoingRoomKeyRequest?
// fun getOutgoingRoomKeyRequestByState(states: Set<ShareRequestState>): OutgoingRoomKeyRequest?
// fun getOutgoingSecretShareRequestByState(states: Set<ShareRequestState>): OutgoingSecretRequest?
/**
* Update an existing outgoing request.
*
* @param request the request
*/
fun updateOutgoingRoomKeyRequest(request: OutgoingRoomKeyRequest)
// fun updateOutgoingRoomKeyRequest(request: OutgoingRoomKeyRequest)
/**
* Delete an outgoing room key request.
*
* @param transactionId the transaction id.
*/
fun deleteOutgoingRoomKeyRequest(transactionId: String)
// fun deleteOutgoingRoomKeyRequest(transactionId: String)
/**
* Store an incomingRoomKeyRequest instance
*
* @param incomingRoomKeyRequest the incoming key request
*/
fun storeIncomingRoomKeyRequest(incomingRoomKeyRequest: IncomingRoomKeyRequest?)
// fun storeIncomingRoomKeyRequest(incomingRoomKeyRequest: IncomingRoomKeyRequest?)
/**
* Delete an incomingRoomKeyRequest instance
*
* @param incomingRoomKeyRequest the incoming key request
*/
fun deleteIncomingRoomKeyRequest(incomingRoomKeyRequest: IncomingShareRequestCommon)
// fun deleteIncomingRoomKeyRequest(incomingRoomKeyRequest: IncomingShareRequestCommon)
fun deleteIncomingSecretRequest(request: IncomingSecretShareRequest)
fun updateGossipingRequestState(request: IncomingShareRequestCommon, state: GossipingRequestState)
/**
* Search an IncomingRoomKeyRequest
@ -400,6 +410,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)
@ -411,20 +423,21 @@ 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 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()
@ -432,5 +445,8 @@ internal interface IMXCryptoStore {
// Dev tools
fun getOutgoingRoomKeyRequests() : List<OutgoingRoomKeyRequest>
fun getOutgoingRoomKeyRequests(): List<OutgoingRoomKeyRequest>
fun getOutgoingSecretRequest(secretName: String): OutgoingSecretRequest?
fun getIncomingRoomKeyRequests(): List<IncomingRoomKeyRequest>
fun getGossipingEventsTrail(): List<Event>
}

View file

@ -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) }
}
}

View file

@ -21,14 +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.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.ShareRequestState
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
@ -46,18 +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.IncomingSecretRequestEntity
import im.vector.matrix.android.internal.crypto.store.db.model.IncomingSecretRequestEntityFields
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
@ -66,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
@ -801,152 +809,334 @@ 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(GossipingEventEntityFields.SENDER, credentials.userId)
//// }.mapNotNull {
//// ContentMapper.map(it.content)?.toModel<OutgoingSecretRequest>()
//// }.firstOrNull {
//// it.secretName == secretName
//// }
TODO("not implemented")
}
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<ShareRequestState>): 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: IncomingShareRequestCommon) {
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 deleteIncomingSecretRequest(request: IncomingSecretShareRequest) {
doRealmTransaction(realmConfiguration) {
it.where<IncomingSecretRequestEntity>()
.equalTo(IncomingSecretRequestEntityFields.USER_ID, request.userId)
.equalTo(IncomingSecretRequestEntityFields.DEVICE_ID, request.deviceId)
.equalTo(IncomingSecretRequestEntityFields.REQUEST_ID, request.requestId)
.equalTo(IncomingSecretRequestEntityFields.SECRET_NAME, request.secretName)
.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 getPendingIncomingSecretShareRequests(): List<IncomingSecretShareRequest> {
override fun getPendingIncomingGossipingRequests(): List<IncomingShareRequestCommon> {
return doRealmQueryAndCopyList(realmConfiguration) {
it.where<IncomingSecretRequestEntity>()
it.where<IncomingGossipingRequestEntity>()
.equalTo(IncomingGossipingRequestEntityFields.REQUEST_STATE_STR, GossipingRequestState.PENDING.name)
.findAll()
}.map {
it.toIncomingSecretShareRequest()
}
.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
* ========================================================================================== */
@ -1051,10 +1241,12 @@ internal class RealmCryptoStore @Inject constructor(
override fun getOutgoingRoomKeyRequests(): List<OutgoingRoomKeyRequest> {
return monarchy.fetchAllMappedSync({ realm ->
realm.where(OutgoingRoomKeyRequestEntity::class.java)
}, {
it.toOutgoingRoomKeyRequest()
})
realm
.where(OutgoingGossipingRequestEntity::class.java)
.equalTo(OutgoingGossipingRequestEntityFields.TYPE_STR, GossipRequestType.KEY.name)
}, { entity ->
entity.toOutgoingGossipingRequest() as? OutgoingRoomKeyRequest
}).filterNotNull()
}
override fun getCrossSigningInfo(userId: String): MXCrossSigningInfo? {

View file

@ -23,9 +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.IncomingSecretRequestEntityFields
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.OutgoingSecretRequestEntityFields
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
@ -140,21 +141,38 @@ internal object RealmCryptoStoreMigration : RealmMigration {
private fun migrateTo2(realm: DynamicRealm) {
Timber.d("Step 1 -> 2")
realm.schema.remove("OutgoingRoomKeyRequestEntity")
realm.schema.remove("IncomingRoomKeyRequestEntity")
realm.schema.create("IncomingSecretRequestEntity")
.addField(IncomingSecretRequestEntityFields.DEVICE_ID, String::class.java)
.addField(IncomingSecretRequestEntityFields.SECRET_NAME, String::class.java)
.addField(IncomingSecretRequestEntityFields.REQUEST_ID, String::class.java)
.addField(IncomingSecretRequestEntityFields.USER_ID, String::class.java)
//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)
.addField(GossipingEventEntityFields.DECRYPTION_RESULT_JSON, String::class.java)
.addField(GossipingEventEntityFields.DECRYPTION_ERROR_CODE, String::class.java)
.addField(GossipingEventEntityFields.AGE_LOCAL_TS, Long::class.java)
.addField(GossipingEventEntityFields.SEND_STATE_STR, Long::class.java)
realm.schema.create("OutgoingSecretRequestEntity")
.addField(OutgoingSecretRequestEntityFields.REQUEST_ID, String::class.java)
.addPrimaryKey(OutgoingSecretRequestEntityFields.REQUEST_ID)
.addField(OutgoingSecretRequestEntityFields.SECRET_NAME, String::class.java)
.addField(OutgoingSecretRequestEntityFields.CANCELLATION_TXN_ID, String::class.java)
.addField(OutgoingSecretRequestEntityFields.RECIPIENTS_DATA, String::class.java)
.addField(OutgoingSecretRequestEntityFields.STATE, Int::class.java)
realm.schema.create("IncomingGossipingRequestEntity")
.addField(IncomingGossipingRequestEntityFields.REQUEST_ID, String::class.java)
.addField(IncomingGossipingRequestEntityFields.TYPE_STR, String::class.java)
.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)
.addField(OutgoingGossipingRequestEntityFields.TYPE_STR, String::class.java)
.addField(OutgoingGossipingRequestEntityFields.REQUESTED_INFO_STR, String::class.java)
.addField(OutgoingGossipingRequestEntityFields.REQUEST_STATE_STR, String::class.java)
.addField(OutgoingGossipingRequestEntityFields.RECIPIENTS_DATA, String::class.java)
}
}

View file

@ -20,14 +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.IncomingSecretRequestEntity
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.OutgoingSecretRequestEntity
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
@ -40,16 +39,19 @@ import io.realm.annotations.RealmModule
CryptoMetadataEntity::class,
CryptoRoomEntity::class,
DeviceInfoEntity::class,
IncomingRoomKeyRequestEntity::class,
// IncomingRoomKeyRequestEntity::class,
KeysBackupDataEntity::class,
OlmInboundGroupSessionEntity::class,
OlmSessionEntity::class,
OutgoingRoomKeyRequestEntity::class,
// OutgoingRoomKeyRequestEntity::class,
UserEntity::class,
KeyInfoEntity::class,
CrossSigningInfoEntity::class,
TrustLevelEntity::class,
IncomingSecretRequestEntity::class,
OutgoingSecretRequestEntity::class
// IncomingSecretRequestEntity::class,
// OutgoingSecretRequestEntity::class,
GossipingEventEntity::class,
IncomingGossipingRequestEntity::class,
OutgoingGossipingRequestEntity::class
])
internal class RealmCryptoStoreModule

View file

@ -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)
}
}
}
}

View file

@ -0,0 +1,90 @@
/*
* 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.RealmList
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
)
}
}
}
}

View file

@ -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
// }
// }
//}

View file

@ -1,37 +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
)
}
}
///*
// * 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
// )
// }
//}

View file

@ -0,0 +1,107 @@
/*
* 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.api.session.events.model.toContent
import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.internal.crypto.GossipRequestType
import im.vector.matrix.android.internal.crypto.GossipingRequestState
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.RealmList
import io.realm.RealmObject
import io.realm.annotations.Index
internal open class OutgoingGossipingRequestEntity(
@Index var requestId: String? = null,
// var cancellationTxnId: String? = null,
// Serialized Json
var recipientsData: String? = null,
var requestedInfoStr: String? = null,
@Index var typeStr: String? = null,
var sourceEvents: RealmList<String> = RealmList()
) : 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)
}
}

View file

@ -1,77 +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.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
}
}
}
///*
// * 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
// }
// }
//}

View file

@ -1,63 +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)
}
}
///*
// * 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)
// }
//}

View file

@ -61,7 +61,6 @@ internal class DefaultRoomVerificationUpdateTask @Inject constructor(
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")
// 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.

View file

@ -22,6 +22,7 @@ import im.vector.matrix.android.api.session.crypto.verification.QrCodeVerificati
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.actions.SetDeviceVerificationAction
import im.vector.matrix.android.internal.crypto.crosssigning.DeviceTrustLevel
import im.vector.matrix.android.internal.crypto.crosssigning.fromBase64
import im.vector.matrix.android.internal.crypto.crosssigning.fromBase64Safe
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore

View file

@ -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(

View file

@ -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.2'
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"

View file

@ -74,7 +74,9 @@ 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.KeyRequestListFragment
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
@ -371,11 +373,24 @@ interface FragmentModule {
@Binds
@IntoMap
@FragmentKey(KeyRequestListFragment::class)
fun bindKeyRequestListFragment(fragment: KeyRequestListFragment): Fragment
@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
}

View file

@ -0,0 +1,203 @@
/*
* 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.EncryptedEventContent
import im.vector.matrix.android.internal.crypto.model.event.OlmEventContent
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.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.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())
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}"
}
}
}
)
}
}
}
}
}
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
}
}
}

View file

@ -0,0 +1,68 @@
/*
* 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.MenuItem
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 GossipingEventsPaperTrailFragment @Inject constructor(
val viewModelFactory: GossipingEventsPaperTrailViewModel.Factory,
private val epoxyController: GossipingEventsEpoxyController
) : VectorBaseFragment() {
override fun getLayoutResId() = R.layout.fragment_generic_recycler
override fun getMenuRes(): Int = R.menu.menu_common_gossiping
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 onOptionsItemSelected(item: MenuItem): Boolean {
if (item.itemId == R.id.refresh) {
viewModel.refresh()
return true
} else {
return super.onOptionsItemSelected(item)
}
}
}

View file

@ -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)
}
}
}

View file

@ -0,0 +1,57 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.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)
// epoxyController.interactionListener = this
}
override fun onDestroyView() {
super.onDestroyView()
recyclerView.cleanup()
// epoxyController.interactionListener = null
}
}

View file

@ -25,6 +25,7 @@ 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
@ -38,9 +39,75 @@ class KeyRequestEpoxyController @Inject constructor(
// 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,
@ -57,6 +124,15 @@ class KeyRequestEpoxyController @Inject constructor(
}
}
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 {
@ -70,7 +146,7 @@ class KeyRequestEpoxyController @Inject constructor(
title(roomKeyRequest.requestId)
description(
span {
span("sessionId:") {
span("sessionId:\n") {
textStyle = "bold"
}
+"${roomKeyRequest.sessionId}"

View file

@ -31,6 +31,8 @@ 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,
@ -42,11 +44,24 @@ class KeyRequestListViewModel @AssistedInject constructor(@Assisted initialState
: VectorViewModel<KeyRequestListViewState, EmptyAction, EmptyViewEvents>(initialState) {
init {
session.cryptoService().getOutgoingRoomKeyRequest().let {
setState {
copy(
outgoingRoomKeyRequest = Success(it)
)
refresh()
}
fun refresh() {
GlobalScope.launch {
session.cryptoService().getOutgoingRoomKeyRequest().let {
setState {
copy(
outgoingRoomKeyRequest = Success(it)
)
}
}
session.cryptoService().getIncomingRoomKeyRequest().let {
setState {
copy(
incomingRequests = Success(it)
)
}
}
}
}
@ -58,12 +73,16 @@ class KeyRequestListViewModel @AssistedInject constructor(@Assisted initialState
fun create(initialState: KeyRequestListViewState): KeyRequestListViewModel
}
companion object : MvRxViewModelFactory<KeyRequestListViewModel, KeyRequestListViewState> {
@JvmStatic
override fun create(viewModelContext: ViewModelContext, state: KeyRequestListViewState): KeyRequestListViewModel? {
val fragment: KeyRequestListFragment = (viewModelContext as FragmentViewModelContext).fragment()
return fragment.viewModelFactory.create(state)
val context = viewModelContext as FragmentViewModelContext
val factory = (context.fragment as? IncomingKeyRequestListFragment)?.viewModelFactory
?: (context.fragment as? OutgoingKeyRequestListFragment)?.viewModelFactory
return factory?.create(state)
}
}
}

View file

@ -16,11 +16,14 @@
package im.vector.riotx.features.settings.devtools
import android.content.Context
import android.os.Bundle
import android.view.MenuItem
import android.view.View
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
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
@ -31,26 +34,79 @@ import javax.inject.Inject
class KeyRequestsFragment @Inject constructor() : VectorBaseFragment() {
override fun getLayoutResId(): Int = R.layout.fragment_devtool_keyrequests
override fun getMenuRes(): Int = R.menu.menu_common_gossiping
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)
devToolKeyRequestPager.adapter = KeyReqPagerAdapter(requireActivity())
mPagerAdapter = KeyReqPagerAdapter(this)
devToolKeyRequestPager.adapter = mPagerAdapter
devToolKeyRequestPager.registerOnPageChangeCallback(pageAdapterListener)
TabLayoutMediator(devToolKeyRequestTabs, devToolKeyRequestPager) { tab, _ ->
tab.text = "Outgoing"
TabLayoutMediator(devToolKeyRequestTabs, devToolKeyRequestPager) { tab, position ->
when (position) {
0 -> {
tab.text = "Outgoing"
}
1 -> {
tab.text = "Incoming"
}
2 -> {
tab.text = "Audit Trail"
}
}
}.attach()
}
private inner class KeyReqPagerAdapter(fa: FragmentActivity) : FragmentStateAdapter(fa) {
override fun getItemCount(): Int = 1
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 childFragmentManager.fragmentFactory.instantiate(requireContext().classLoader, KeyRequestListFragment::class.java.name)
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)
}
}
}
}
}

View file

@ -17,6 +17,7 @@
package im.vector.riotx.features.settings.devtools
import android.os.Bundle
import android.view.MenuItem
import android.view.View
import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState
@ -28,14 +29,13 @@ import im.vector.riotx.core.resources.ColorProvider
import kotlinx.android.synthetic.main.fragment_generic_recycler.*
import javax.inject.Inject
class KeyRequestListFragment @Inject constructor(
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 ->
@ -53,4 +53,5 @@ class KeyRequestListFragment @Inject constructor(
recyclerView.cleanup()
// epoxyController.interactionListener = null
}
}

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/refresh"
android:icon="@drawable/ic_refresh_cw"
app:iconTint="?riotx_text_primary"
android:title="@string/refresh"
app:showAsAction="collapseActionView|ifRoom" />
</menu>

View file

@ -12,6 +12,8 @@
<string name="e2e_use_keybackup">Unlock encrypted messages history</string>
<string name="refresh">Refresh</string>
<!-- END Strings added by Valere -->