Initial commit

This commit is contained in:
Valere 2020-01-10 18:29:23 +01:00
parent ea9166e0c6
commit 859c75df98
55 changed files with 2134 additions and 99 deletions

2
.gitignore vendored
View file

@ -15,3 +15,5 @@
ktlint ktlint
.idea/copyright/New_vector.xml .idea/copyright/New_vector.xml
.idea/copyright/profiles_settings.xml .idea/copyright/profiles_settings.xml
.idea/copyright/New_Vector_Ltd.xml

View file

@ -19,8 +19,12 @@ package im.vector.matrix.android
import android.content.Context import android.content.Context
import androidx.test.core.app.ApplicationProvider import androidx.test.core.app.ApplicationProvider
import java.io.File import java.io.File
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import org.junit.Rule
interface InstrumentedTest { interface InstrumentedTest {
fun context(): Context { fun context(): Context {
return ApplicationProvider.getApplicationContext() return ApplicationProvider.getApplicationContext()
} }

View file

@ -19,6 +19,7 @@ package im.vector.matrix.android.common
import android.content.Context import android.content.Context
import android.net.Uri import android.net.Uri
import androidx.lifecycle.Observer
import im.vector.matrix.android.api.Matrix import im.vector.matrix.android.api.Matrix
import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.MatrixConfiguration import im.vector.matrix.android.api.MatrixConfiguration
@ -31,6 +32,11 @@ import im.vector.matrix.android.api.session.room.Room
import im.vector.matrix.android.api.session.room.timeline.Timeline import im.vector.matrix.android.api.session.room.timeline.Timeline
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.matrix.android.api.session.room.timeline.TimelineSettings import im.vector.matrix.android.api.session.room.timeline.TimelineSettings
import im.vector.matrix.android.api.session.sync.SyncState
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import org.junit.Assert.* import org.junit.Assert.*
import java.util.* import java.util.*
import java.util.concurrent.CountDownLatch import java.util.concurrent.CountDownLatch
@ -73,23 +79,26 @@ class CommonTestHelper(context: Context) {
* @param session the session to sync * @param session the session to sync
*/ */
fun syncSession(session: Session) { fun syncSession(session: Session) {
// val lock = CountDownLatch(1) val lock = CountDownLatch(1)
// val observer = androidx.lifecycle.Observer<SyncState> { syncState ->
// if (syncState is SyncState.Idle) {
// lock.countDown()
// }
// }
// TODO observe?
// while (session.syncState().value !is SyncState.Idle) {
// sleep(100)
// }
session.open() session.open()
session.startSync(true) session.startSync(true)
// await(lock)
// session.syncState().removeObserver(observer) val syncLiveData = runBlocking(Dispatchers.Main) {
session.getSyncStateLive()
}
val syncObserver = object : Observer<SyncState> {
override fun onChanged(t: SyncState?) {
if (session.hasAlreadySynced()) {
lock.countDown()
syncLiveData.removeObserver(this)
}
}
}
GlobalScope.launch(Dispatchers.Main) { syncLiveData.observeForever(syncObserver) }
await(lock)
} }
/** /**

View file

@ -17,11 +17,15 @@
package im.vector.matrix.android.common package im.vector.matrix.android.common
import android.os.SystemClock import android.os.SystemClock
import androidx.lifecycle.Observer
import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.events.model.toContent import im.vector.matrix.android.api.session.events.model.toContent
import im.vector.matrix.android.api.session.room.model.Membership
import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams
import im.vector.matrix.android.api.session.room.roomSummaryQueryParams
import im.vector.matrix.android.api.session.room.timeline.Timeline import im.vector.matrix.android.api.session.room.timeline.Timeline
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.matrix.android.api.session.room.timeline.TimelineSettings import im.vector.matrix.android.api.session.room.timeline.TimelineSettings
@ -29,6 +33,10 @@ import im.vector.matrix.android.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
import im.vector.matrix.android.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM_BACKUP import im.vector.matrix.android.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM_BACKUP
import im.vector.matrix.android.internal.crypto.keysbackup.model.MegolmBackupAuthData import im.vector.matrix.android.internal.crypto.keysbackup.model.MegolmBackupAuthData
import im.vector.matrix.android.internal.crypto.keysbackup.model.MegolmBackupCreationInfo import im.vector.matrix.android.internal.crypto.keysbackup.model.MegolmBackupCreationInfo
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import org.junit.Assert.* import org.junit.Assert.*
import java.util.* import java.util.*
import java.util.concurrent.CountDownLatch import java.util.concurrent.CountDownLatch
@ -78,26 +86,32 @@ class CryptoTestHelper(val mTestHelper: CommonTestHelper) {
val aliceSession = cryptoTestData.firstSession val aliceSession = cryptoTestData.firstSession
val aliceRoomId = cryptoTestData.roomId val aliceRoomId = cryptoTestData.roomId
val room = aliceSession.getRoom(aliceRoomId)!! val aliceRoom = aliceSession.getRoom(aliceRoomId)!!
val bobSession = mTestHelper.createAccount(TestConstants.USER_BOB, defaultSessionParams) val bobSession = mTestHelper.createAccount(TestConstants.USER_BOB, defaultSessionParams)
val lock1 = CountDownLatch(2) val lock1 = CountDownLatch(2)
// val bobEventListener = object : MXEventListener() {
// override fun onNewRoom(roomId: String) {
// if (TextUtils.equals(roomId, aliceRoomId)) {
// if (!statuses.containsKey("onNewRoom")) {
// statuses["onNewRoom"] = "onNewRoom"
// lock1.countDown()
// }
// }
// }
// }
//
// bobSession.dataHandler.addListener(bobEventListener)
room.invite(bobSession.myUserId, callback = object : TestMatrixCallback<Unit>(lock1) { val bobRoomSummariesLive = runBlocking(Dispatchers.Main) {
bobSession.getRoomSummariesLive(roomSummaryQueryParams { })
}
val newRoomObserver = object : Observer<List<RoomSummary>> {
override fun onChanged(t: List<RoomSummary>?) {
if (t?.isNotEmpty() == true) {
statuses["onNewRoom"] = "onNewRoom"
lock1.countDown()
bobRoomSummariesLive.removeObserver(this)
}
}
}
GlobalScope.launch(Dispatchers.Main) {
bobRoomSummariesLive.observeForever(newRoomObserver)
}
aliceRoom.invite(bobSession.myUserId, callback = object : TestMatrixCallback<Unit>(lock1) {
override fun onSuccess(data: Unit) { override fun onSuccess(data: Unit) {
statuses["invite"] = "invite" statuses["invite"] = "invite"
super.onSuccess(data) super.onSuccess(data)
@ -108,25 +122,27 @@ class CryptoTestHelper(val mTestHelper: CommonTestHelper) {
assertTrue(statuses.containsKey("invite") && statuses.containsKey("onNewRoom")) assertTrue(statuses.containsKey("invite") && statuses.containsKey("onNewRoom"))
// bobSession.dataHandler.removeListener(bobEventListener)
val lock2 = CountDownLatch(2) val lock2 = CountDownLatch(2)
bobSession.joinRoom(aliceRoomId, callback = TestMatrixCallback(lock2)) val roomJoinedObserver = object : Observer<List<RoomSummary>> {
override fun onChanged(t: List<RoomSummary>?) {
if (bobSession.getRoom(aliceRoomId)
?.getRoomMember(aliceSession.myUserId)
?.membership == Membership.JOIN) {
statuses["AliceJoin"] = "AliceJoin"
lock2.countDown()
bobRoomSummariesLive.removeObserver(this)
}
}
}
// room.addEventListener(object : MXEventListener() { GlobalScope.launch(Dispatchers.Main) {
// override fun onLiveEvent(event: Event, roomState: RoomState) { bobRoomSummariesLive.observeForever(roomJoinedObserver)
// if (TextUtils.equals(event.getType(), Event.EVENT_TYPE_STATE_ROOM_MEMBER)) { }
// val contentToConsider = event.contentAsJsonObject
// val member = JsonUtils.toRoomMember(contentToConsider)
//
// if (TextUtils.equals(member.membership, RoomMember.MEMBERSHIP_JOIN)) { bobSession.joinRoom(aliceRoomId, callback = TestMatrixCallback(lock2))
// statuses["AliceJoin"] = "AliceJoin"
// lock2.countDown()
// }
// }
// }
// })
mTestHelper.await(lock2) mTestHelper.await(lock2)

View file

@ -0,0 +1,214 @@
package im.vector.matrix.android.internal.crypto.crosssigning
import androidx.test.ext.junit.runners.AndroidJUnit4
import im.vector.matrix.android.InstrumentedTest
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.common.*
import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo
import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap
import im.vector.matrix.android.internal.crypto.model.rest.SignatureUploadResponse
import im.vector.matrix.android.internal.crypto.model.rest.UserPasswordAuth
import org.junit.Assert
import org.junit.Assert.assertNotNull
import org.junit.Assert.fail
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.NAME_ASCENDING)
class XSigningTest : InstrumentedTest {
private val mTestHelper = CommonTestHelper(context())
private val mCryptoTestHelper = CryptoTestHelper(mTestHelper)
@Test
fun test_InitializeAndStoreKeys() {
val aliceSession = mTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true))
val aliceLatch = CountDownLatch(1)
aliceSession.getCrossSigningService()
.initializeCrossSigning(UserPasswordAuth(
user = aliceSession.myUserId,
password = TestConstants.PASSWORD
), TestMatrixCallback(aliceLatch))
mTestHelper.await(aliceLatch)
val myCrossSigningKeys = aliceSession.getCrossSigningService().getMyCrossSigningKeys()
val masterPubKey = myCrossSigningKeys?.masterKey()
Assert.assertNotNull("Master key should be stored", masterPubKey?.unpaddedBase64PublicKey)
val selfSigningKey = myCrossSigningKeys?.selfSigningKey()
Assert.assertNotNull("SelfSigned key should be stored", selfSigningKey?.unpaddedBase64PublicKey)
val userKey = myCrossSigningKeys?.userKey()
Assert.assertNotNull("User key should be stored", userKey?.unpaddedBase64PublicKey)
Assert.assertTrue("Signing Keys should be trusted", myCrossSigningKeys?.isTrusted == true)
}
@Test
fun test_CrossSigningCheckBobSeesTheKeys() {
val cryptoTestData = mCryptoTestHelper.doE2ETestWithAliceAndBobInARoom()
val aliceSession = cryptoTestData.firstSession
val bobSession = cryptoTestData.secondSession
val aliceAuthParams = UserPasswordAuth(
user = aliceSession.myUserId,
password = TestConstants.PASSWORD
)
val bobAuthParams = UserPasswordAuth(
user = bobSession!!.myUserId,
password = TestConstants.PASSWORD
)
val aliceLatch = CountDownLatch(1)
val bobLatch = CountDownLatch(1)
aliceSession.getCrossSigningService().initializeCrossSigning(aliceAuthParams, TestMatrixCallback(aliceLatch))
bobSession.getCrossSigningService().initializeCrossSigning(bobAuthParams, TestMatrixCallback(bobLatch))
mTestHelper.await(aliceLatch)
mTestHelper.await(bobLatch)
//Check that alice can see bob keys
val downloadLatch = CountDownLatch(1)
aliceSession.downloadKeys(listOf(bobSession.myUserId), true, TestMatrixCallback(downloadLatch))
mTestHelper.await(downloadLatch)
val bobKeysFromAlicePOV = aliceSession.getCrossSigningService().getUserCrossSigningKeys(bobSession.myUserId)
Assert.assertNotNull("Alice can see bob Master key", bobKeysFromAlicePOV?.masterKey())
Assert.assertNull("Alice should not see bob User key", bobKeysFromAlicePOV?.userKey())
Assert.assertNotNull("Alice can see bob SelfSigned key", bobKeysFromAlicePOV?.selfSigningKey())
Assert.assertEquals("Bob keys from alice pov should match", bobKeysFromAlicePOV?.masterKey()?.unpaddedBase64PublicKey, bobSession.getCrossSigningService().getMyCrossSigningKeys()?.masterKey()?.unpaddedBase64PublicKey)
Assert.assertEquals("Bob keys from alice pov should match", bobKeysFromAlicePOV?.selfSigningKey()?.unpaddedBase64PublicKey, bobSession.getCrossSigningService().getMyCrossSigningKeys()?.selfSigningKey()?.unpaddedBase64PublicKey)
Assert.assertTrue("Bob keys from alice pov should not be trusted", bobKeysFromAlicePOV?.isTrusted == false)
}
@Test
fun test_CrossSigningTestAliceTrustBobNewDevice() {
val cryptoTestData = mCryptoTestHelper.doE2ETestWithAliceAndBobInARoom()
val aliceSession = cryptoTestData.firstSession
val bobSession = cryptoTestData.secondSession
val aliceAuthParams = UserPasswordAuth(
user = aliceSession.myUserId,
password = TestConstants.PASSWORD
)
val bobAuthParams = UserPasswordAuth(
user = bobSession!!.myUserId,
password = TestConstants.PASSWORD
)
val aliceLatch = CountDownLatch(1)
val bobLatch = CountDownLatch(1)
aliceSession.getCrossSigningService().initializeCrossSigning(aliceAuthParams, TestMatrixCallback(aliceLatch))
bobSession.getCrossSigningService().initializeCrossSigning(bobAuthParams, TestMatrixCallback(bobLatch))
mTestHelper.await(aliceLatch)
mTestHelper.await(bobLatch)
//Check that alice can see bob keys
val downloadLatch = CountDownLatch(1)
val bobUserId = bobSession.myUserId
aliceSession.downloadKeys(listOf(bobUserId), true, TestMatrixCallback(downloadLatch))
mTestHelper.await(downloadLatch)
val bobKeysFromAlicePOV = aliceSession.getCrossSigningService().getUserCrossSigningKeys(bobUserId)
Assert.assertTrue("Bob keys from alice pov should not be trusted", bobKeysFromAlicePOV?.isTrusted == false)
val trustLatch = CountDownLatch(1)
aliceSession.getCrossSigningService().trustUser(bobUserId, object : MatrixCallback<SignatureUploadResponse> {
override fun onSuccess(data: SignatureUploadResponse) {
trustLatch.countDown()
}
override fun onFailure(failure: Throwable) {
fail("Failed to trust bob")
}
})
mTestHelper.await(trustLatch)
// Now bobs logs in on a new device and verifies it
// We will want to test that in alice POV, this new device would be trusted by cross signing
val bobSession2 = mTestHelper.logIntoAccount(bobUserId, SessionTestParams(true))
val bobSecondDeviceId = bobSession2.sessionParams.credentials.deviceId
// Check that bob first session sees the new login
val bobKeysLatch = CountDownLatch(1)
bobSession.downloadKeys(listOf(bobUserId), true, object : MatrixCallback<MXUsersDevicesMap<MXDeviceInfo>> {
override fun onFailure(failure: Throwable) {
fail("Failed to get device")
}
override fun onSuccess(data: MXUsersDevicesMap<MXDeviceInfo>) {
if(data.getUserDeviceIds(bobUserId)?.contains(bobSecondDeviceId!!) == false) {
fail("Bob should see the new device")
}
bobKeysLatch.countDown()
}
})
mTestHelper.await(bobKeysLatch)
val bobSecondDevicePOVFirstDevice = bobSession.getDeviceInfo(bobUserId, bobSecondDeviceId)
assertNotNull("Bob Second device should be known and persisted from first", bobSecondDevicePOVFirstDevice)
// Manually mark it as trusted from first session
val bobSignLatch = CountDownLatch(1)
bobSession.getCrossSigningService().signDevice(bobSecondDeviceId!!, object : MatrixCallback<SignatureUploadResponse> {
override fun onSuccess(data: SignatureUploadResponse) {
bobSignLatch.countDown()
}
override fun onFailure(failure: Throwable) {
fail("Failed to trust bob ${failure.localizedMessage}")
}
})
mTestHelper.await(bobSignLatch)
// Now alice should cross trust bob's second device
val aliceKeysLatch = CountDownLatch(1)
aliceSession.downloadKeys(listOf(bobUserId), true, object : MatrixCallback<MXUsersDevicesMap<MXDeviceInfo>> {
override fun onFailure(failure: Throwable) {
fail("Failed to get device")
}
override fun onSuccess(data: MXUsersDevicesMap<MXDeviceInfo>) {
//check that the device is seen
if(data.getUserDeviceIds(bobUserId)?.contains(bobSecondDeviceId) == false) {
fail("Alice should see the new device")
}
aliceKeysLatch.countDown()
}
})
mTestHelper.await(aliceKeysLatch)
val secondDevicetrustLatch = CountDownLatch(1)
aliceSession.getCrossSigningService().checkDeviceTrust(bobUserId, bobSecondDeviceId,object : MatrixCallback<Unit> {
override fun onFailure(failure: Throwable) {
fail("Failed to check trust cause:${failure.localizedMessage}")
}
override fun onSuccess(data: Unit) {
secondDevicetrustLatch.countDown()
}
})
mTestHelper.await(secondDevicetrustLatch)
}
}

View file

@ -36,6 +36,10 @@ import org.junit.runner.RunWith
import org.junit.runners.MethodSorters import org.junit.runners.MethodSorters
import java.util.* import java.util.*
import java.util.concurrent.CountDownLatch import java.util.concurrent.CountDownLatch
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import androidx.test.annotation.UiThreadTest
import org.junit.Rule
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
@FixMethodOrder(MethodSorters.NAME_ASCENDING) @FixMethodOrder(MethodSorters.NAME_ASCENDING)
@ -135,8 +139,24 @@ class SASTest : InstrumentedTest {
val tid = "00000000" val tid = "00000000"
// Bob should receive a cancel // Bob should receive a cancel
var canceledToDeviceEvent: Event? = null var cancelReason: String? = null
val cancelLatch = CountDownLatch(1) val cancelLatch = CountDownLatch(1)
val bobListener = object : SasVerificationService.SasVerificationListener {
override fun transactionCreated(tx: SasVerificationTransaction) {}
override fun transactionUpdated(tx: SasVerificationTransaction) {
if (tx.transactionId == tid && tx.cancelledReason != null) {
cancelReason = tx.cancelledReason?.humanReadable
cancelLatch.countDown()
}
}
override fun markedAsManuallyVerified(userId: String, deviceId: String) {}
}
bobSession.getSasVerificationService().addListener(bobListener)
// TODO bobSession!!.dataHandler.addListener(object : MXEventListener() { // TODO bobSession!!.dataHandler.addListener(object : MXEventListener() {
// TODO override fun onToDeviceEvent(event: Event?) { // TODO override fun onToDeviceEvent(event: Event?) {
// TODO if (event!!.getType() == CryptoEvent.EVENT_TYPE_KEY_VERIFICATION_CANCEL) { // TODO if (event!!.getType() == CryptoEvent.EVENT_TYPE_KEY_VERIFICATION_CANCEL) {
@ -156,8 +176,8 @@ class SASTest : InstrumentedTest {
override fun transactionCreated(tx: SasVerificationTransaction) {} override fun transactionCreated(tx: SasVerificationTransaction) {}
override fun transactionUpdated(tx: SasVerificationTransaction) { override fun transactionUpdated(tx: SasVerificationTransaction) {
if ((tx as IncomingSASVerificationTransaction).uxState === IncomingSasVerificationTransaction.UxState.SHOW_ACCEPT) { if ((tx as IncomingSasVerificationTransaction).uxState === IncomingSasVerificationTransaction.UxState.SHOW_ACCEPT) {
(tx as IncomingSASVerificationTransaction).performAccept() (tx as IncomingSasVerificationTransaction).performAccept()
} }
} }
@ -169,8 +189,7 @@ class SASTest : InstrumentedTest {
mTestHelper.await(cancelLatch) mTestHelper.await(cancelLatch)
val cancelReq = canceledToDeviceEvent!!.content.toModel<KeyVerificationCancel>()!! assertEquals("Request should be cancelled with m.unknown_method", CancelCode.UnknownMethod.value, cancelReason)
assertEquals("Request should be cancelled with m.unknown_method", CancelCode.UnknownMethod.value, cancelReq.code)
cryptoTestData.close() cryptoTestData.close()
} }
@ -257,14 +276,15 @@ class SASTest : InstrumentedTest {
hashes: List<String> = SASVerificationTransaction.KNOWN_HASHES, hashes: List<String> = SASVerificationTransaction.KNOWN_HASHES,
mac: List<String> = SASVerificationTransaction.KNOWN_MACS, mac: List<String> = SASVerificationTransaction.KNOWN_MACS,
codes: List<String> = SASVerificationTransaction.KNOWN_SHORT_CODES) { codes: List<String> = SASVerificationTransaction.KNOWN_SHORT_CODES) {
val startMessage = KeyVerificationStart() val startMessage = KeyVerificationStart(
startMessage.fromDevice = bobSession.getMyDevice().deviceId fromDevice = bobSession.getMyDevice().deviceId,
startMessage.method = KeyVerificationStart.VERIF_METHOD_SAS method = KeyVerificationStart.VERIF_METHOD_SAS,
startMessage.transactionID = tid transactionID = tid,
startMessage.keyAgreementProtocols = protocols keyAgreementProtocols = protocols,
startMessage.hashes = hashes hashes = hashes,
startMessage.messageAuthenticationCodes = mac messageAuthenticationCodes = mac,
startMessage.shortAuthenticationStrings = codes shortAuthenticationStrings = codes
)
val contentMap = MXUsersDevicesMap<Any>() val contentMap = MXUsersDevicesMap<Any>()
contentMap.setObject(aliceUserID, aliceDevice, startMessage) contentMap.setObject(aliceUserID, aliceDevice, startMessage)
@ -344,8 +364,8 @@ class SASTest : InstrumentedTest {
override fun transactionUpdated(tx: SasVerificationTransaction) { override fun transactionUpdated(tx: SasVerificationTransaction) {
if ((tx as SASVerificationTransaction).state === SasVerificationTxState.OnAccepted) { if ((tx as SASVerificationTransaction).state === SasVerificationTxState.OnAccepted) {
val at = tx as SASVerificationTransaction val at = tx as SASVerificationTransaction
accepted = at.accepted accepted = at.accepted as? KeyVerificationAccept
startReq = at.startReq startReq = at.startReq as? KeyVerificationStart
aliceAcceptedLatch.countDown() aliceAcceptedLatch.countDown()
} }
} }
@ -356,8 +376,8 @@ class SASTest : InstrumentedTest {
override fun transactionCreated(tx: SasVerificationTransaction) {} override fun transactionCreated(tx: SasVerificationTransaction) {}
override fun transactionUpdated(tx: SasVerificationTransaction) { override fun transactionUpdated(tx: SasVerificationTransaction) {
if ((tx as IncomingSASVerificationTransaction).uxState === IncomingSasVerificationTransaction.UxState.SHOW_ACCEPT) { if ((tx as IncomingSasVerificationTransaction).uxState === IncomingSasVerificationTransaction.UxState.SHOW_ACCEPT) {
val at = tx as IncomingSASVerificationTransaction val at = tx as IncomingSasVerificationTransaction
at.performAccept() at.performAccept()
} }
} }
@ -401,7 +421,7 @@ class SASTest : InstrumentedTest {
override fun transactionCreated(tx: SasVerificationTransaction) {} override fun transactionCreated(tx: SasVerificationTransaction) {}
override fun transactionUpdated(tx: SasVerificationTransaction) { override fun transactionUpdated(tx: SasVerificationTransaction) {
val uxState = (tx as OutgoingSASVerificationRequest).uxState val uxState = (tx as OutgoingSasVerificationRequest).uxState
when (uxState) { when (uxState) {
OutgoingSasVerificationRequest.UxState.SHOW_SAS -> { OutgoingSasVerificationRequest.UxState.SHOW_SAS -> {
aliceSASLatch.countDown() aliceSASLatch.countDown()
@ -419,7 +439,7 @@ class SASTest : InstrumentedTest {
override fun transactionCreated(tx: SasVerificationTransaction) {} override fun transactionCreated(tx: SasVerificationTransaction) {}
override fun transactionUpdated(tx: SasVerificationTransaction) { override fun transactionUpdated(tx: SasVerificationTransaction) {
val uxState = (tx as IncomingSASVerificationTransaction).uxState val uxState = (tx as IncomingSasVerificationTransaction).uxState
when (uxState) { when (uxState) {
IncomingSasVerificationTransaction.UxState.SHOW_ACCEPT -> { IncomingSasVerificationTransaction.UxState.SHOW_ACCEPT -> {
tx.performAccept() tx.performAccept()
@ -465,7 +485,7 @@ class SASTest : InstrumentedTest {
override fun transactionCreated(tx: SasVerificationTransaction) {} override fun transactionCreated(tx: SasVerificationTransaction) {}
override fun transactionUpdated(tx: SasVerificationTransaction) { override fun transactionUpdated(tx: SasVerificationTransaction) {
val uxState = (tx as OutgoingSASVerificationRequest).uxState val uxState = (tx as OutgoingSasVerificationRequest).uxState
when (uxState) { when (uxState) {
OutgoingSasVerificationRequest.UxState.SHOW_SAS -> { OutgoingSasVerificationRequest.UxState.SHOW_SAS -> {
tx.userHasVerifiedShortCode() tx.userHasVerifiedShortCode()
@ -486,7 +506,7 @@ class SASTest : InstrumentedTest {
override fun transactionCreated(tx: SasVerificationTransaction) {} override fun transactionCreated(tx: SasVerificationTransaction) {}
override fun transactionUpdated(tx: SasVerificationTransaction) { override fun transactionUpdated(tx: SasVerificationTransaction) {
val uxState = (tx as IncomingSASVerificationTransaction).uxState val uxState = (tx as IncomingSasVerificationTransaction).uxState
when (uxState) { when (uxState) {
IncomingSasVerificationTransaction.UxState.SHOW_ACCEPT -> { IncomingSasVerificationTransaction.UxState.SHOW_ACCEPT -> {
tx.performAccept() tx.performAccept()

View file

@ -6,9 +6,10 @@
<uses-permission android:name="android.permission.VIBRATE" /> <uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<application> <application android:networkSecurityConfig="@xml/network_security_config">
<provider android:name="androidx.work.impl.WorkManagerInitializer" <provider
android:name="androidx.work.impl.WorkManagerInitializer"
android:authorities="${applicationId}.workmanager-init" android:authorities="${applicationId}.workmanager-init"
android:exported="false" android:exported="false"
tools:node="remove" /> tools:node="remove" />

View file

@ -19,6 +19,7 @@ package im.vector.matrix.android.api.session.crypto
import android.content.Context import android.content.Context
import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.listeners.ProgressListener import im.vector.matrix.android.api.listeners.ProgressListener
import im.vector.matrix.android.api.session.crypto.crosssigning.CrossSigningService
import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupService import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupService
import im.vector.matrix.android.api.session.crypto.keyshare.RoomKeysRequestListener import im.vector.matrix.android.api.session.crypto.keyshare.RoomKeysRequestListener
import im.vector.matrix.android.api.session.crypto.sas.SasVerificationService import im.vector.matrix.android.api.session.crypto.sas.SasVerificationService
@ -47,6 +48,8 @@ interface CryptoService {
fun isCryptoEnabled(): Boolean fun isCryptoEnabled(): Boolean
fun getSasVerificationService(): SasVerificationService fun getSasVerificationService(): SasVerificationService
fun getCrossSigningService(): CrossSigningService
fun getKeysBackupService(): KeysBackupService fun getKeysBackupService(): KeysBackupService

View file

@ -0,0 +1,47 @@
/*
* Copyright 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.session.crypto.crosssigning
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.internal.crypto.model.rest.SignatureUploadResponse
import im.vector.matrix.android.internal.crypto.model.rest.UserPasswordAuth
interface CrossSigningService {
fun isUserTrusted(userId: String) : Boolean
fun checkUserTrust(userId: String, callback: MatrixCallback<Boolean>? = null)
/**
* Initialize cross signing for this user.
* Users needs to enter credentials
*/
fun initializeCrossSigning(authParams: UserPasswordAuth?, callback: MatrixCallback<Unit>? = null)
fun getUserCrossSigningKeys(userId: String): MXCrossSigningInfo?
fun getMyCrossSigningKeys(): MXCrossSigningInfo?
fun trustUser(userId: String, callback: MatrixCallback<SignatureUploadResponse>)
/**
* Sign one of your devices and upload the signature
*/
fun signDevice(deviceId: String, callback: MatrixCallback<SignatureUploadResponse>)
fun checkDeviceTrust(userId: String, deviceId: String, callback: MatrixCallback<Unit>)
}

View file

@ -0,0 +1,38 @@
/*
* Copyright 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.session.crypto.crosssigning
/**
* Defines the account cross signing state.
*
*/
enum class CrossSigningState {
/** Current state is unknown, need to download user keys from server to resolve */
Unknown,
/** Currently dowloading user keys*/
CheckingState,
/** No Cross signing keys are defined on the server */
Disabled,
/** CrossSigning keys are beeing created and uploaded to the server */
Enabling,
/** Cross signing keys exists and are trusted*/
Trusted,
/** Cross signing keys exists but are not yet trusted*/
Untrusted,
/** The local cross signing keys do not match with the server keys*/
Conflicted
}

View file

@ -0,0 +1,47 @@
/*
* Copyright 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.session.crypto.crosssigning
import im.vector.matrix.android.internal.crypto.model.rest.CrossSigningKeyInfo
data class MXCrossSigningInfo(
/**
* the user id
*/
// @Json(name = "user_id")
var userId: String,
// @Json(name = "user_keys")
var crossSigningKeys: List<CrossSigningKeyInfo> = ArrayList(),
val isTrusted: Boolean = false
) {
fun masterKey(): CrossSigningKeyInfo? = crossSigningKeys
.firstOrNull { it.usages?.contains(CrossSigningKeyInfo.KeyUsage.MASTER.value) == true }
fun userKey(): CrossSigningKeyInfo? = crossSigningKeys
.firstOrNull { it.usages?.contains(CrossSigningKeyInfo.KeyUsage.USER_SIGNING.value) == true }
fun selfSigningKey(): CrossSigningKeyInfo? = crossSigningKeys
.firstOrNull { it.usages?.contains(CrossSigningKeyInfo.KeyUsage.SELF_SIGNING.value) == true }
}

View file

@ -0,0 +1,80 @@
///*
// * Copyright 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.session.crypto.crosssigning
//
//import com.squareup.moshi.Json
//import com.squareup.moshi.JsonClass
//import im.vector.matrix.android.internal.crypto.model.rest.CrossSigningKeyInfo
//
//
//@JsonClass(generateAdapter = true)
//data class MXKeyInfo(
//
// @Json(name = "public_key")
// val publicKeyBase64: String,
// val privateKeyBase64: String?,
//
// @Json(name = "is_trusted")
// val isTrusted: Boolean = false,
//
//
// @Json(name = "usage")
// val usage: List<String> = ArrayList(),
//
// /**
// * The signature of this MXDeviceInfo.
// * A map from "<userId>" to a map from "<key type>:<Publickey>" to "<signature>"
// */
// @Json(name = "signatures")
// var signatures: Map<String, Map<String, String>>? = null
//
//) {
//
// data class Builder(
// private val publicKeyBase64: String,
// private val usage: CrossSigningKeyInfo.KeyUsage,
// private var trusted: Boolean = false,
// private val signatures: ArrayList<Triple<String, String, String>> = ArrayList()
// ) {
//
// fun signature(userId: String, keyUsedToSignBase64: String, base64Signature: String) = apply {
// signatures.add(Triple(userId, keyUsedToSignBase64, base64Signature))
// }
//
// fun trusted(trusted: Boolean) = apply {
// this.trusted = trusted
// }
//
// fun build(): MXKeyInfo {
//
// val signMap = HashMap<String, HashMap<String, String>>()
// signatures.forEach { info ->
// val uMap = signMap[info.first]
// ?: HashMap<String, String>().also { signMap[info.first] = it }
// uMap["ed25519:${info.second}"] = info.third
// }
//
// return MXKeyInfo(
// publicKeyBase64 = publicKeyBase64,
// usage = listOf(usage.value),
// isTrusted = trusted,
// signatures = signMap
// )
// }
// }
//}
//

View file

@ -132,6 +132,12 @@ internal abstract class CryptoModule {
@Binds @Binds
abstract fun bindUploadKeysTask(uploadKeysTask: DefaultUploadKeysTask): UploadKeysTask abstract fun bindUploadKeysTask(uploadKeysTask: DefaultUploadKeysTask): UploadKeysTask
@Binds
abstract fun bindUploadSigningKeysTask(uploadKeysTask: DefaultUploadSigningKeysTask): UploadSigningKeysTask
@Binds
abstract fun bindUploadSignaturesTask(uploadSignaturesTask: DefaultUploadSignaturesTask): UploadSignaturesTask
@Binds @Binds
abstract fun bindDownloadKeysForUsersTask(downloadKeysForUsersTask: DefaultDownloadKeysForUsers): DownloadKeysForUsersTask abstract fun bindDownloadKeysForUsersTask(downloadKeysForUsersTask: DefaultDownloadKeysForUsers): DownloadKeysForUsersTask

View file

@ -44,6 +44,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.IMXEncrypting
import im.vector.matrix.android.internal.crypto.algorithms.megolm.MXMegolmEncryptionFactory 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.MXOlmEncryptionFactory
import im.vector.matrix.android.internal.crypto.crosssigning.DefaultCrossSigningService
import im.vector.matrix.android.internal.crypto.keysbackup.KeysBackup import im.vector.matrix.android.internal.crypto.keysbackup.KeysBackup
import im.vector.matrix.android.internal.crypto.model.ImportRoomKeysResult import im.vector.matrix.android.internal.crypto.model.ImportRoomKeysResult
import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo
@ -113,6 +114,8 @@ internal class DefaultCryptoService @Inject constructor(
private val roomDecryptorProvider: RoomDecryptorProvider, private val roomDecryptorProvider: RoomDecryptorProvider,
// The SAS verification service. // The SAS verification service.
private val sasVerificationService: DefaultSasVerificationService, private val sasVerificationService: DefaultSasVerificationService,
private val crossSigningService: DefaultCrossSigningService,
// //
private val incomingRoomKeyRequestManager: IncomingRoomKeyRequestManager, private val incomingRoomKeyRequestManager: IncomingRoomKeyRequestManager,
// //
@ -317,6 +320,8 @@ internal class DefaultCryptoService @Inject constructor(
*/ */
override fun getSasVerificationService() = sasVerificationService override fun getSasVerificationService() = sasVerificationService
override fun getCrossSigningService() = crossSigningService
/** /**
* A sync response has been received * A sync response has been received
* *
@ -364,7 +369,7 @@ internal class DefaultCryptoService @Inject constructor(
*/ */
override fun getDeviceInfo(userId: String, deviceId: String?): MXDeviceInfo? { override fun getDeviceInfo(userId: String, deviceId: String?): MXDeviceInfo? {
return if (userId.isNotEmpty() && !deviceId.isNullOrEmpty()) { return if (userId.isNotEmpty() && !deviceId.isNullOrEmpty()) {
cryptoStore.getUserDevice(deviceId, userId) cryptoStore.getUserDevice(userId, deviceId)
} else { } else {
null null
} }

View file

@ -171,8 +171,8 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
for ((k, value) in failures) { for ((k, value) in failures) {
val statusCode = when (val status = value["status"]) { val statusCode = when (val status = value["status"]) {
is Double -> status.toInt() is Double -> status.toInt()
is Int -> status.toInt() is Int -> status.toInt()
else -> 0 else -> 0
} }
if (statusCode == 503) { if (statusCode == 503) {
synchronized(notReadyToRetryHS) { synchronized(notReadyToRetryHS) {
@ -289,7 +289,7 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
val mutableDevices = devices.toMutableMap() val mutableDevices = devices.toMutableMap()
for ((deviceId, deviceInfo) in devices) { for ((deviceId, deviceInfo) in devices) {
// Get the potential previously store device keys for this device // Get the potential previously store device keys for this device
val previouslyStoredDeviceKeys = cryptoStore.getUserDevice(deviceId, userId) val previouslyStoredDeviceKeys = cryptoStore.getUserDevice(userId, deviceId)
// in some race conditions (like unit tests) // in some race conditions (like unit tests)
// the self device must be seen as verified // the self device must be seen as verified
@ -315,6 +315,25 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
// Note that devices which aren't in the response will be removed from the stores // Note that devices which aren't in the response will be removed from the stores
cryptoStore.storeUserDevices(userId, mutableDevices) cryptoStore.storeUserDevices(userId, mutableDevices)
} }
//Handle cross signing keys update
val masterKey = response.masterKeys?.get(userId)?.also {
Timber.d("## CrossSigning : Got keys for $userId : MSK ${it.unpaddedBase64PublicKey}")
}
val selfSigningKey = response.selfSigningKeys?.get(userId)?.also {
Timber.d("## CrossSigning : Got keys for $userId : SSK ${it.unpaddedBase64PublicKey}")
}
val userSigningKey = response.userSigningKeys?.get(userId)?.also {
Timber.d("## CrossSigning : Got keys for $userId : USK ${it.unpaddedBase64PublicKey}")
}
cryptoStore.storeUserCrossSigningKeys(
userId,
masterKey,
selfSigningKey,
userSigningKey
)
} }
return onKeysDownloadSucceed(filteredUsers, response.failures) return onKeysDownloadSucceed(filteredUsers, response.failures)
} }

View file

@ -107,7 +107,7 @@ internal class IncomingRoomKeyRequestManager @Inject constructor(
cryptoStore.deleteIncomingRoomKeyRequest(request) cryptoStore.deleteIncomingRoomKeyRequest(request)
} }
// if the device is verified already, share the keys // if the device is verified already, share the keys
val device = cryptoStore.getUserDevice(deviceId!!, userId) val device = cryptoStore.getUserDevice(userId, deviceId!!)
if (device != null) { if (device != null) {
if (device.isVerified) { if (device.isVerified) {
Timber.v("## processReceivedRoomKeyRequests() : device is already verified: sharing keys") Timber.v("## processReceivedRoomKeyRequests() : device is already verified: sharing keys")

View file

@ -141,6 +141,7 @@ internal class MXOlmDevice @Inject constructor(
*/ */
fun release() { fun release() {
olmAccount?.releaseAccount() olmAccount?.releaseAccount()
olmUtility?.releaseUtility()
} }
/** /**

View file

@ -28,7 +28,7 @@ internal class SetDeviceVerificationAction @Inject constructor(
private val keysBackup: KeysBackup) { private val keysBackup: KeysBackup) {
fun handle(verificationStatus: Int, deviceId: String, userId: String) { fun handle(verificationStatus: Int, deviceId: String, userId: String) {
val device = cryptoStore.getUserDevice(deviceId, userId) val device = cryptoStore.getUserDevice(userId, deviceId)
// Sanity check // Sanity check
if (null == device) { if (null == device) {

View file

@ -297,7 +297,7 @@ internal class MXMegolmDecryption(private val userId: String,
runCatching { deviceListManager.downloadKeys(listOf(userId), false) } runCatching { deviceListManager.downloadKeys(listOf(userId), false) }
.mapCatching { .mapCatching {
val deviceId = request.deviceId val deviceId = request.deviceId
val deviceInfo = cryptoStore.getUserDevice(deviceId ?: "", userId) val deviceInfo = cryptoStore.getUserDevice(userId, deviceId ?: "")
if (deviceInfo == null) { if (deviceInfo == null) {
throw RuntimeException() throw RuntimeException()
} else { } else {

View file

@ -16,6 +16,7 @@
*/ */
package im.vector.matrix.android.internal.crypto.api package im.vector.matrix.android.internal.crypto.api
import im.vector.matrix.android.internal.crypto.model.MXKeysObject
import im.vector.matrix.android.internal.crypto.model.rest.* import im.vector.matrix.android.internal.crypto.model.rest.*
import im.vector.matrix.android.internal.network.NetworkConstants import im.vector.matrix.android.internal.network.NetworkConstants
import retrofit2.Call import retrofit2.Call
@ -65,6 +66,36 @@ internal interface CryptoApi {
@POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "keys/query") @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "keys/query")
fun downloadKeysForUsers(@Body params: KeysQueryBody): Call<KeysQueryResponse> fun downloadKeysForUsers(@Body params: KeysQueryBody): Call<KeysQueryResponse>
/**
* CrossSigning - Uploading signing keys
* Public keys for the cross-signing keys are uploaded to the servers using /keys/device_signing/upload.
* This endpoint requires UI Auth.
*/
@POST(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "keys/device_signing/upload")
fun uploadSigningKeys(@Body params: UploadSigningKeysBody): Call<KeysQueryResponse>
/**
* CrossSigning - Uploading signatures
* Signatures of device keys can be up
* loaded using /keys/signatures/upload.
* For example, Alice signs one of her devices (HIJKLMN) (using her self-signing key),
* her own master key (using her HIJKLMN device), Bob's master key (using her user-signing key).
*
* The response contains a failures property, which is a map of user ID to device ID to failure reason, if any of the uploaded keys failed.
* The homeserver should verify that the signatures on the uploaded keys are valid.
* If a signature is not valid, the homeserver should set the corresponding entry in failures to a JSON object
* with the errcode property set to M_INVALID_SIGNATURE.
*
* After Alice uploads a signature for her own devices or master key,
* her signature will be included in the results of the /keys/query request when anyone requests her keys.
* However, signatures made for other users' keys, made by her user-signing key, will not be included.
*/
@POST(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "keys/signatures/upload")
fun uploadSignatures(@Body params: Map<String, @JvmSuppressWildcards Any>?): Call<SignatureUploadResponse>
/** /**
* Claim one-time keys. * Claim one-time keys.
* Doc: https://matrix.org/docs/spec/client_server/r0.4.0.html#post-matrix-client-r0-keys-claim * Doc: https://matrix.org/docs/spec/client_server/r0.4.0.html#post-matrix-client-r0-keys-claim

View file

@ -0,0 +1,511 @@
/*
* Copyright 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.crosssigning
import android.util.Base64
import dagger.Lazy
import im.vector.matrix.android.api.MatrixCallback
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.session.crypto.crosssigning.CrossSigningState
import im.vector.matrix.android.api.session.crypto.crosssigning.MXCrossSigningInfo
import im.vector.matrix.android.internal.crypto.DeviceListManager
import im.vector.matrix.android.internal.crypto.MXOlmDevice
import im.vector.matrix.android.internal.crypto.MyDeviceInfoHolder
import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo
import im.vector.matrix.android.internal.crypto.model.rest.*
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
import im.vector.matrix.android.internal.crypto.tasks.UploadSignaturesTask
import im.vector.matrix.android.internal.crypto.tasks.UploadSigningKeysTask
import im.vector.matrix.android.internal.di.MoshiProvider
import im.vector.matrix.android.internal.di.UserId
import im.vector.matrix.android.internal.task.TaskConstraints
import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.internal.task.configureWith
import im.vector.matrix.android.internal.util.JsonCanonicalizer
import org.matrix.olm.OlmPkSigning
import org.matrix.olm.OlmUtility
import timber.log.Timber
import javax.inject.Inject
internal class DefaultCrossSigningService @Inject constructor(
@UserId private val userId: String,
private val credentials: Credentials,
private val cryptoStore: IMXCryptoStore,
private val myDeviceInfoHolder: Lazy<MyDeviceInfoHolder>,
private val olmDevice: MXOlmDevice,
private val deviceListManager: DeviceListManager,
private val uploadSigningKeysTask: UploadSigningKeysTask,
private val uploadSignaturesTask: UploadSignaturesTask,
private val taskExecutor: TaskExecutor) : CrossSigningService {
private var olmUtility: OlmUtility? = null
private var crossSigningState: CrossSigningState = CrossSigningState.Unknown
private var masterPkSigning: OlmPkSigning? = null
private var userPkSigning: OlmPkSigning? = null
private var selfSigningPkSigning: OlmPkSigning? = null
init {
try {
olmUtility = OlmUtility()
//Try to get stored keys if they exist
cryptoStore.getMyCrossSigningInfo()?.let { mxCrossSigningInfo ->
Timber.i("## CrossSigning - Found Existing self signed keys")
Timber.i("## CrossSigning - Checking if private keys are known")
cryptoStore.getCrossSigningPrivateKeys()?.let { privateKeyinfo ->
privateKeyinfo.master?.let { privateKey ->
val keySeed = Base64.decode(privateKey, Base64.NO_PADDING)
val pkSigning = OlmPkSigning()
if (pkSigning.initWithSeed(keySeed) == mxCrossSigningInfo.masterKey()?.unpaddedBase64PublicKey) {
masterPkSigning = pkSigning
Timber.i("## CrossSigning - Loading master key success")
} else {
Timber.w("## CrossSigning - Public master key does not match the private key")
// TODO untrust
}
}
privateKeyinfo.user?.let { privateKey ->
val keySeed = Base64.decode(privateKey, Base64.NO_PADDING)
val pkSigning = OlmPkSigning()
if (pkSigning.initWithSeed(keySeed) == mxCrossSigningInfo.userKey()?.unpaddedBase64PublicKey) {
userPkSigning = pkSigning
Timber.i("## CrossSigning - Loading User Signing key success")
} else {
Timber.w("## CrossSigning - Public User key does not match the private key")
// TODO untrust
}
}
privateKeyinfo.selfSigned?.let { privateKey ->
val keySeed = Base64.decode(privateKey, Base64.NO_PADDING)
val pkSigning = OlmPkSigning()
if (pkSigning.initWithSeed(keySeed) == mxCrossSigningInfo.selfSigningKey()?.unpaddedBase64PublicKey) {
selfSigningPkSigning = pkSigning
Timber.i("## CrossSigning - Loading Self Signing key success")
} else {
Timber.w("## CrossSigning - Public Self Signing key does not match the private key")
// TODO untrust
}
}
}
}
} catch (e: Throwable) {
// Mmm this kind of a big issue
Timber.e(e, "Failed to initialize Cross Signing")
}
}
fun release() {
olmUtility?.releaseUtility()
listOf(masterPkSigning, userPkSigning, selfSigningPkSigning).forEach { it?.releaseSigning() }
}
/**
* - Make 3 key pairs (MSK, USK, SSK)
* - Save the private keys with proper security
* - Sign the keys and upload them
* - Sign the current device with SSK and sign MSK with device key (migration) and upload signatures
*/
override fun initializeCrossSigning(authParams: UserPasswordAuth?, callback: MatrixCallback<Unit>?) {
Timber.d("## CrossSigning initializeCrossSigning")
// TODO sync that
crossSigningState = CrossSigningState.Enabling
val myUserID = credentials.userId
//=================
// MASTER KEY
//=================
val masterPkOlm = OlmPkSigning()
val masterKeyPrivateKey = OlmPkSigning.generateSeed()
val masterPublicKey = masterPkOlm.initWithSeed(masterKeyPrivateKey)
Timber.v("## CrossSigning - masterPublicKey:$masterPublicKey")
//=================
// USER KEY
//=================
val userSigningPkOlm = OlmPkSigning()
val uskPrivateKey = OlmPkSigning.generateSeed()
val uskPublicKey = userSigningPkOlm.initWithSeed(uskPrivateKey)
Timber.v("## CrossSigning - uskPublicKey:$uskPublicKey")
// Sign userSigningKey with master
val signedUSK = JsonCanonicalizer.getCanonicalJson(Map::class.java, CrossSigningKeyInfo.Builder(myUserID, CrossSigningKeyInfo.KeyUsage.USER_SIGNING)
.key(uskPublicKey)
.build().signalableJSONDictionary()).let { masterPkOlm.sign(it) }
//=================
// SELF SIGNING KEY
//=================
val selfSigningPkOlm = OlmPkSigning()
val sskPrivateKey = OlmPkSigning.generateSeed()
val sskPublicKey = selfSigningPkOlm.initWithSeed(sskPrivateKey)
Timber.v("## CrossSigning - sskPublicKey:$sskPublicKey")
// Sign userSigningKey with master
val signedSSK = JsonCanonicalizer.getCanonicalJson(Map::class.java, CrossSigningKeyInfo.Builder(myUserID, CrossSigningKeyInfo.KeyUsage.SELF_SIGNING)
.key(sskPublicKey)
.build().signalableJSONDictionary()).let { masterPkOlm.sign(it) }
// I need to upload the keys
val mskCrossSigningKeyInfo = CrossSigningKeyInfo.Builder(myUserID, CrossSigningKeyInfo.KeyUsage.MASTER)
.key(masterPublicKey)
.build()
val params = UploadSigningKeysTask.Params(
masterKey = mskCrossSigningKeyInfo,
userKey = CrossSigningKeyInfo.Builder(myUserID, CrossSigningKeyInfo.KeyUsage.USER_SIGNING)
.key(uskPublicKey)
.signature(myUserID, masterPublicKey, signedUSK)
.build(),
selfSignedKey = CrossSigningKeyInfo.Builder(myUserID, CrossSigningKeyInfo.KeyUsage.SELF_SIGNING)
.key(sskPublicKey)
.signature(myUserID, masterPublicKey, signedSSK)
.build(),
userPasswordAuth = authParams
)
this.masterPkSigning = masterPkOlm
this.userPkSigning = userSigningPkOlm
this.selfSigningPkSigning = selfSigningPkOlm
val crossSigningInfo = MXCrossSigningInfo(myUserID, listOf(params.masterKey, params.userKey, params.selfSignedKey))
cryptoStore.setMyCrossSigningInfo(crossSigningInfo)
cryptoStore.setUserKeysAsTrusted(myUserID)
// TODO we should ensure that they are sent
// TODO error handling?
uploadSigningKeysTask.configureWith(params) {
this.retryCount = 3
this.constraints = TaskConstraints(true)
this.callback = object : MatrixCallback<KeysQueryResponse> {
override fun onSuccess(data: KeysQueryResponse) {
Timber.i("## CrossSigning - Keys succesfully uploaded")
// Sign the current device with SSK
val uploadSignatureQueryBuilder = UploadSignatureQueryBuilder()
val myDevice = myDeviceInfoHolder.get().myDevice
val canonicalJson = JsonCanonicalizer.getCanonicalJson(Map::class.java, myDevice.signalableJSONDictionary())
val signedDevice = selfSigningPkOlm.sign(canonicalJson)
val updateSignatures = (myDevice.signatures?.toMutableMap() ?: HashMap()).also {
it[myUserID] = (it[myUserID]
?: HashMap()) + mapOf("ed25519:$sskPublicKey" to signedDevice)
}
myDevice.copy(signatures = updateSignatures).let {
uploadSignatureQueryBuilder.withDeviceInfo(it)
}
// sign MSK with device key (migration) and upload signatures
olmDevice.signMessage(JsonCanonicalizer.getCanonicalJson(Map::class.java, mskCrossSigningKeyInfo.signalableJSONDictionary()))?.let { sign ->
val mskUpdatedSignatures = (mskCrossSigningKeyInfo.signatures?.toMutableMap()
?: HashMap()).also {
it[myUserID] = (it[myUserID]
?: HashMap()) + mapOf("ed25519:${myDevice.deviceId}" to sign)
}
mskCrossSigningKeyInfo.copy(
signatures = mskUpdatedSignatures
).let {
uploadSignatureQueryBuilder.withSigningKeyInfo(it)
}
}
uploadSignaturesTask.configureWith(UploadSignaturesTask.Params(uploadSignatureQueryBuilder.build())) {
this.retryCount = 3
this.constraints = TaskConstraints(true)
this.callback = object : MatrixCallback<SignatureUploadResponse> {
override fun onSuccess(data: SignatureUploadResponse) {
Timber.i("## CrossSigning - signatures succesfuly uploaded")
}
override fun onFailure(failure: Throwable) {
Timber.e(failure, "## CrossSigning - Failed to upload signatures")
}
}
}.executeBy(taskExecutor)
callback?.onSuccess(Unit)
crossSigningState = CrossSigningState.Trusted
}
override fun onFailure(failure: Throwable) {
Timber.e(failure, "## CrossSigning - Failed to upload signing keys")
callback?.onFailure(failure)
}
}
}.executeBy(taskExecutor)
}
/**
*
*
* ALICE BOB
*
* MSK MSK
*
*
* SSK SSK
*
* USK
* USK (not visible by
* Alice)
*
*
* BOB's Device
*
*/
override fun isUserTrusted(userId: String): Boolean {
return cryptoStore.getCrossSigningInfo(userId)?.isTrusted == true
}
/**
* Will not force a download of the key, but will verify signatures trust chain
*/
override fun checkUserTrust(userId: String, callback: MatrixCallback<Boolean>?) {
Timber.d("## CrossSigning checkUserTrust for $userId")
// I trust a user if I trust his master key
// I can trust the master key if it is signed by my user key
// TODO what if the master key is signed by a device key that i have verified
// First let's get my user key
val myUserKey = cryptoStore.getCrossSigningInfo(credentials.userId)?.userKey()
if (myUserKey == null) {
Timber.d("## CrossSigning checkUserTrust false, CrossSigning is not enabled (userKey not defined)")
callback?.onSuccess(false)
return
}
// Let's get the other user master key
val masterKey = cryptoStore.getCrossSigningInfo(userId)?.masterKey()
if (masterKey == null) {
Timber.d("## CrossSigning checkUserTrust false for $userId, ")
callback?.onSuccess(false)
return
}
val masterKeySignaturesMadeByMyUserKey = masterKey.signatures
?.get(credentials.userId) // Signatures made by me
?.get("ed25519:${myUserKey.unpaddedBase64PublicKey}")
if (masterKeySignaturesMadeByMyUserKey.isNullOrBlank()) {
Timber.d("## CrossSigning checkUserTrust false for $userId, not signed by my UserSigningKey")
callback?.onSuccess(false)
return
}
// olmUtility?.verifyEd25519Signature(masterKeySignaturesMadeByMyUserKey,
// myUserKey.publicKeyBase64,
// masterKey.publicKeyBase64)
}
override fun getUserCrossSigningKeys(userId: String): MXCrossSigningInfo? {
return cryptoStore.getCrossSigningInfo(userId)
}
override fun getMyCrossSigningKeys(): MXCrossSigningInfo? {
return cryptoStore.getMyCrossSigningInfo()
}
override fun trustUser(userId: String, callback: MatrixCallback<SignatureUploadResponse>) {
//We should have this user keys
val otherMasterKeys = getUserCrossSigningKeys(userId)?.masterKey()
if (otherMasterKeys == null) {
callback.onFailure(Throwable("Other master signing key is not known"))
return
}
val myKeys = getUserCrossSigningKeys(credentials.userId)
if (myKeys == null) {
callback.onFailure(Throwable("CrossSigning is not setup for this account"))
return
}
val userPubKey = myKeys.userKey()?.unpaddedBase64PublicKey
if (userPubKey == null || userPkSigning == null) {
callback.onFailure(Throwable("Cannot sign from this account, privateKeyUnknown $userPubKey"))
return
}
// Sign the other MasterKey with our UserSiging key
val newSignature = JsonCanonicalizer.getCanonicalJson(Map::class.java,
otherMasterKeys.signalableJSONDictionary()).let { userPkSigning?.sign(it) }
if (newSignature == null) {
// race??
callback.onFailure(Throwable("Failed to sign"))
return
}
otherMasterKeys.addSignature(credentials.userId, userPubKey, newSignature)
cryptoStore.setUserKeysAsTrusted(userId, true)
// TODO update local copy with new signature directly here? kind of local echo of trust?
val uploadQuery = UploadSignatureQueryBuilder()
.withSigningKeyInfo(otherMasterKeys)
.build()
uploadSignaturesTask.configureWith(UploadSignaturesTask.Params(uploadQuery)) {
this.callback = callback
}.executeBy(taskExecutor)
}
override fun signDevice(deviceId: String, callback: MatrixCallback<SignatureUploadResponse>) {
// This device should be yours
val device = cryptoStore.getUserDevice(credentials.userId, deviceId)
if (device == null) {
callback.onFailure(IllegalArgumentException("This device [$deviceId] is not known, or not yours"))
return
}
val myKeys = getUserCrossSigningKeys(credentials.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 = JsonCanonicalizer.getCanonicalJson(Map::class.java, device.signalableJSONDictionary()).let { userPkSigning?.sign(it) }
val newSignature = selfSigningPkSigning?.sign(device.canonicalSignable())
if (newSignature == null) {
// race??
callback.onFailure(Throwable("Failed to sign"))
return
}
val toUpload = device.copy(
signatures = mapOf(
credentials.userId
to
mapOf(
"ed25519:${ssPubKey}" to newSignature
)
)
)
// device.addSignature(credentials.userId, ssPubKey, newSignature)
val uploadQuery = UploadSignatureQueryBuilder()
.withDeviceInfo(toUpload)
.build()
uploadSignaturesTask.configureWith(UploadSignaturesTask.Params(uploadQuery)) {
this.callback = object : MatrixCallback<SignatureUploadResponse> {
override fun onFailure(failure: Throwable) {
callback.onFailure(failure)
}
override fun onSuccess(data: SignatureUploadResponse) {
val watchedFailure = data.failures?.get(userId)?.get(deviceId)
if (watchedFailure == null) {
callback.onSuccess(data)
} else {
val failure = MoshiProvider.providesMoshi().adapter(UploadResponseFailure::class.java).fromJson(watchedFailure.toString())?.message
?: watchedFailure.toString()
callback.onFailure(Throwable(failure))
}
}
}
}.executeBy(taskExecutor)
}
override fun checkDeviceTrust(userId: String, deviceId: String, callback: MatrixCallback<Unit>) {
val otherDevice = cryptoStore.getUserDevice(userId, deviceId)
if (otherDevice == null) {
callback.onFailure(IllegalArgumentException("This device is not known, or not yours"))
return
}
val myKeys = getUserCrossSigningKeys(credentials.userId)
if (myKeys == null) {
callback.onFailure(Throwable("CrossSigning is not setup for this account"))
return
}
val otherKeys = getUserCrossSigningKeys(userId)
if (otherKeys == null) {
callback.onFailure(Throwable("CrossSigning is not setup for $userId"))
return
}
// TODO should we force verification ?
if (!otherKeys.isTrusted) {
callback.onFailure(Throwable("$userId is not trusted"))
return
}
// Check if the trust chain is valid
/*
*
* ALICE BOB
*
* MSK MSK
*
*
* SSK SSK
*
* USK
* USK (not visible by
* Alice)
*
*
* BOB's Device
*
*/
val otherSSKSignature = otherDevice.signatures?.get(userId)?.get("ed25519:${otherKeys.selfSigningKey()?.unpaddedBase64PublicKey}")
if (otherSSKSignature == null) {
callback.onFailure(Throwable("Device ${otherDevice.deviceId} is not signed by $userId self signed key"))
return
}
// Check bob's device is signed by bob's SSK
try {
olmUtility?.verifyEd25519Signature(otherSSKSignature, otherKeys.selfSigningKey()?.unpaddedBase64PublicKey, otherDevice.canonicalSignable())
} catch (e: Throwable) {
callback.onFailure(Throwable("Invalid self signed signature for Device ${otherDevice.deviceId}"))
}
callback.onSuccess(Unit)
}
}
fun MXDeviceInfo.canonicalSignable(): String {
return JsonCanonicalizer.getCanonicalJson(Map::class.java, signalableJSONDictionary())
}

View file

@ -402,7 +402,7 @@ internal class KeysBackup @Inject constructor(
} }
if (deviceId != null) { if (deviceId != null) {
val device = cryptoStore.getUserDevice(deviceId, userId) val device = cryptoStore.getUserDevice(userId, deviceId)
var isSignatureValid = false var isSignatureValid = false
if (device == null) { if (device == null) {

View file

@ -147,6 +147,14 @@ data class MXDeviceInfo(
return map return map
} }
fun addSignature(userId: String, signedWithNoPrefix: String, signature: String) = apply {
val updated = (signatures?.toMutableMap() ?: HashMap())
val userMap = updated[userId]?.toMutableMap()
?: HashMap<String, String>().also { updated[userId] = it }
userMap["ed25519:${signedWithNoPrefix}"] = signature
signatures = updated
}
/** /**
* @return a dictionary of the parameters * @return a dictionary of the parameters
*/ */

View file

@ -0,0 +1,29 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.crypto.model
interface MXKeysObject {
val userId: String
val keys: Map<String, String>?
val signatures: Map<String, Map<String, String>>?
// fun signalableJSONDictionary(): Map<String, Any>
}

View file

@ -0,0 +1,125 @@
package im.vector.matrix.android.internal.crypto.model.rest
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import im.vector.matrix.android.internal.crypto.model.MXKeysObject
/**
* "self_signing_key": {
* "user_id": "@alice:example.com",
* "usage": ["self_signing"],
* "keys": {
* "ed25519:base64+self+signing+public+key": "base64+self+signing+public+key"
* },
* "signatures": {
* "@alice:example.com": {
* "ed25519:base64+master+public+key": "base64+signature"
* }
* }
* }
*/
@JsonClass(generateAdapter = true)
data class CrossSigningKeyInfo(
/**
* The user who owns the key
*/
@Json(name = "user_id")
override var userId: String,
/**
* Allowed uses for the key.
* Must contain "master" for master keys, "self_signing" for self-signing keys, and "user_signing" for user-signing keys.
* See CrossSigningKeyInfo#KEY_USAGE_* constants
*/
@Json(name = "usage")
val usages: List<String>?,
/**
* An object that must have one entry,
* whose name is "ed25519:" followed by the unpadded base64 encoding of the public key,
* and whose value is the unpadded base64 encoding of the public key.
*/
@Json(name = "keys")
override var keys: Map<String, String>?,
/**
* Signatures of the key.
* A self-signing or user-signing key must be signed by the master key.
* A master key may be signed by a device.
*/
@Json(name = "signatures")
override var signatures: Map<String, Map<String, String>>? = null
) : MXKeysObject {
// Shortcut to get key as "keys" is an object that must have one entry
val unpaddedBase64PublicKey: String? = keys?.values?.firstOrNull()
val isMasterKey = usages?.contains(KeyUsage.MASTER.value) ?: false
val isSelfSigningKey = usages?.contains(KeyUsage.SELF_SIGNING.value) ?: false
val isUserKey = usages?.contains(KeyUsage.USER_SIGNING.value) ?: false
fun signalableJSONDictionary(): Map<String, Any> {
val map = HashMap<String, Any>()
userId.let { map["user_id"] = it }
usages?.let { map["usage"] = it }
keys?.let { map["keys"] = it }
return map
}
fun addSignature(userId: String, signedWithNoPrefix: String, signature: String) = apply {
val updated = (signatures?.toMutableMap() ?: HashMap())
val userMap = updated[userId]?.toMutableMap()
?: HashMap<String, String>().also { updated[userId] = it }
userMap["ed25519:${signedWithNoPrefix}"] = signature
signatures = updated
}
// fun toXSigningKeys(): XSigningKeys {
// return XSigningKeys(
// userId = userId,
// usage = usages ?: emptyList(),
// keys = keys ?: emptyMap(),
// signatures = signatures
//
// )
// }
data class Builder(
val userId: String,
val usage: KeyUsage,
private var base64Pkey: String? = null,
private val signatures: ArrayList<Triple<String, String, String>> = ArrayList()
) {
fun key(publicKeyBase64: String) = apply {
base64Pkey = publicKeyBase64
}
fun signature(userId: String, keySignedBase64: String, base64Signature: String) = apply {
signatures.add(Triple(userId, keySignedBase64, base64Signature))
}
fun build(): CrossSigningKeyInfo {
val b64key = base64Pkey ?: throw IllegalArgumentException("")
val signMap = HashMap<String, HashMap<String, String>>()
signatures.forEach { info ->
val uMap = signMap[info.first]
?: HashMap<String, String>().also { signMap[info.first] = it }
uMap["ed25519:${info.second}"] = info.third
}
return CrossSigningKeyInfo(
userId = userId,
usages = listOf(usage.value),
keys = mapOf("ed25519:$b64key" to b64key),
signatures = signMap
)
}
}
enum class KeyUsage(val value: String) {
MASTER("master"),
SELF_SIGNING("self_signing"),
USER_SIGNING("user_signing")
}
}

View file

@ -24,5 +24,5 @@ import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
internal data class DeleteDeviceParams( internal data class DeleteDeviceParams(
@Json(name = "auth") @Json(name = "auth")
var deleteDeviceAuth: DeleteDeviceAuth? = null var userPasswordAuth: UserPasswordAuth? = null
) )

View file

@ -18,12 +18,12 @@ package im.vector.matrix.android.internal.crypto.model.rest
import com.squareup.moshi.Json import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
import im.vector.matrix.android.api.util.JsonDict import im.vector.matrix.android.internal.crypto.model.MXKeysObject
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class DeviceKeys( data class DeviceKeys(
@Json(name = "user_id") @Json(name = "user_id")
val userId: String, override val userId: String,
@Json(name = "device_id") @Json(name = "device_id")
val deviceId: String, val deviceId: String,
@ -32,8 +32,8 @@ data class DeviceKeys(
val algorithms: List<String>, val algorithms: List<String>,
@Json(name = "keys") @Json(name = "keys")
val keys: Map<String, String>, override val keys: Map<String, String>,
@Json(name = "signatures") @Json(name = "signatures")
val signatures: JsonDict override val signatures: Map<String, Map<String, String>>?
) ) : MXKeysObject

View file

@ -1,6 +1,5 @@
/* /*
* Copyright 2016 OpenMarket Ltd
* Copyright 2017 Vector Creations Ltd
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -23,6 +22,11 @@ import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo
/** /**
* This class represents the response to /keys/query request made by downloadKeysForUsers * This class represents the response to /keys/query request made by downloadKeysForUsers
*
* After uploading cross-signing keys, they will be included under the /keys/query endpoint under the master_keys,
* self_signing_keys and user_signing_keys properties.
*
* The user_signing_keys property will only be included when a user requests their own keys.
*/ */
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class KeysQueryResponse( data class KeysQueryResponse(
@ -38,5 +42,16 @@ data class KeysQueryResponse(
* The failures sorted by homeservers. TODO Bad comment ? * The failures sorted by homeservers. TODO Bad comment ?
* TODO Use MXUsersDevicesMap? * TODO Use MXUsersDevicesMap?
*/ */
var failures: Map<String, Map<String, Any>>? = null var failures: Map<String, Map<String, Any>>? = null,
@Json(name = "master_keys")
var masterKeys: Map<String, CrossSigningKeyInfo?>? = null,
@Json(name = "self_signing_keys")
var selfSigningKeys: Map<String, CrossSigningKeyInfo?>? = null,
@Json(name = "user_signing_keys")
var userSigningKeys: Map<String, CrossSigningKeyInfo?>? = null
) )

View file

@ -0,0 +1,50 @@
/*
* Copyright 2016 OpenMarket Ltd
* Copyright 2017 Vector Creations Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.crypto.model.rest
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo
/**
* Upload Signature response
*/
@JsonClass(generateAdapter = true)
data class SignatureUploadResponse(
/**
* The response contains a failures property, which is a map of user ID to device ID to failure reason,
* if any of the uploaded keys failed.
* The homeserver should verify that the signatures on the uploaded keys are valid.
* If a signature is not valid, the homeserver should set the corresponding entry in failures to a JSON object
* with the errcode property set to M_INVALID_SIGNATURE.
*/
var failures: Map<String, Map<String, @JvmSuppressWildcards Any>>? = null
)
@JsonClass(generateAdapter = true)
data class UploadResponseFailure(
@Json(name = "status")
val status: Int,
@Json(name = "errCode")
val errCode: String,
@Json(name = "message")
val message: String
)

View file

@ -0,0 +1,58 @@
/*
* Copyright 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.crypto.model.rest
import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo
import im.vector.matrix.android.internal.crypto.model.MXKeysObject
/**
* Helper class to build CryptoApi#uploadSignatures params
*/
data class UploadSignatureQueryBuilder(
private val deviceInfoList: ArrayList<MXDeviceInfo> = ArrayList(),
private val signingKeyInfoList: ArrayList<CrossSigningKeyInfo> = ArrayList()
) {
fun withDeviceInfo(deviceInfo: MXDeviceInfo) = apply {
deviceInfoList.add(deviceInfo)
}
fun withSigningKeyInfo(info: CrossSigningKeyInfo) = apply {
signingKeyInfoList.add(info)
}
fun build(): Map<String, Map<String, MXKeysObject>> {
val map = HashMap<String, HashMap<String, MXKeysObject>>()
val usersList = (deviceInfoList.map { it.userId } + signingKeyInfoList.mapNotNull { it.userId }).distinct()
usersList.forEach { userID ->
val userMap = HashMap<String, MXKeysObject>()
deviceInfoList.filter { it.userId == userID }.forEach { deviceInfo ->
userMap[deviceInfo.deviceId] = deviceInfo.toDeviceKeys()
}
signingKeyInfoList.filter { it.userId == userID }.forEach { keyInfo ->
keyInfo.unpaddedBase64PublicKey?.let { base64Key ->
userMap[base64Key] = keyInfo
}
}
map[userID] = userMap
}
return map
}
}

View file

@ -0,0 +1,31 @@
/*
* Copyright 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.crypto.model.rest
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class UploadSignaturesBody(
@Json(name = "master_key")
val masterKey: CrossSigningKeyInfo? = null,
@Json(name = "self_signing_key")
val selfSigningKey: CrossSigningKeyInfo? = null,
@Json(name = "user_signing_key")
val userSigningKey: CrossSigningKeyInfo? = null
)

View file

@ -0,0 +1,35 @@
/*
* Copyright 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.crypto.model.rest
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import im.vector.matrix.android.internal.auth.registration.AuthParams
@JsonClass(generateAdapter = true)
internal data class UploadSigningKeysBody(
@Json(name = "master_key")
val masterKey: CrossSigningKeyInfo? = null,
@Json(name = "self_signing_key")
val selfSigningKey: CrossSigningKeyInfo? = null,
@Json(name = "user_signing_key")
val userSigningKey: CrossSigningKeyInfo? = null,
@Json(name = "auth")
val auth: UserPasswordAuth? = null
)

View file

@ -1,5 +1,6 @@
/* /*
* Copyright 2016 OpenMarket Ltd * Copyright 2016 OpenMarket Ltd
* Copyright 2020 New Vector Ltd
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -17,12 +18,13 @@ package im.vector.matrix.android.internal.crypto.model.rest
import com.squareup.moshi.Json import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
import im.vector.matrix.android.internal.auth.data.LoginFlowTypes
/** /**
* This class provides the authentication data to delete a device * This class provides the authentication data to delete a device
*/ */
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
internal data class DeleteDeviceAuth( data class UserPasswordAuth(
// device device session id // device device session id
@Json(name = "session") @Json(name = "session")
@ -30,7 +32,7 @@ internal data class DeleteDeviceAuth(
// registration information // registration information
@Json(name = "type") @Json(name = "type")
var type: String? = null, var type: String? = LoginFlowTypes.PASSWORD,
@Json(name = "user") @Json(name = "user")
var user: String? = null, var user: String? = null,

View file

@ -0,0 +1,20 @@
package im.vector.matrix.android.internal.crypto.model.rest
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import im.vector.matrix.android.internal.crypto.model.MXKeysObject
@JsonClass(generateAdapter = true)
data class XSigningKeys(
@Json(name = "user_id")
override val userId: String,
@Json(name = "usage")
val usage: List<String>,
@Json(name = "keys")
override val keys: Map<String, String>,
@Json(name = "signatures")
override val signatures: Map<String, Map<String, String>>?
) : MXKeysObject

View file

@ -17,12 +17,14 @@
package im.vector.matrix.android.internal.crypto.store package im.vector.matrix.android.internal.crypto.store
import im.vector.matrix.android.api.session.crypto.crosssigning.MXCrossSigningInfo
import im.vector.matrix.android.internal.crypto.IncomingRoomKeyRequest import im.vector.matrix.android.internal.crypto.IncomingRoomKeyRequest
import im.vector.matrix.android.internal.crypto.NewSessionListener import im.vector.matrix.android.internal.crypto.NewSessionListener
import im.vector.matrix.android.internal.crypto.OutgoingRoomKeyRequest import im.vector.matrix.android.internal.crypto.OutgoingRoomKeyRequest
import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo
import im.vector.matrix.android.internal.crypto.model.OlmInboundGroupSessionWrapper import im.vector.matrix.android.internal.crypto.model.OlmInboundGroupSessionWrapper
import im.vector.matrix.android.internal.crypto.model.OlmSessionWrapper import im.vector.matrix.android.internal.crypto.model.OlmSessionWrapper
import im.vector.matrix.android.internal.crypto.model.rest.CrossSigningKeyInfo
import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody
import im.vector.matrix.android.internal.crypto.store.db.model.KeysBackupDataEntity import im.vector.matrix.android.internal.crypto.store.db.model.KeysBackupDataEntity
import org.matrix.olm.OlmAccount import org.matrix.olm.OlmAccount
@ -163,7 +165,7 @@ internal interface IMXCryptoStore {
* @param userId the user's id. * @param userId the user's id.
* @return the device * @return the device
*/ */
fun getUserDevice(deviceId: String, userId: String): MXDeviceInfo? fun getUserDevice(userId: String, deviceId: String): MXDeviceInfo?
/** /**
* Retrieve a device by its identity key. * Retrieve a device by its identity key.
@ -181,6 +183,11 @@ internal interface IMXCryptoStore {
*/ */
fun storeUserDevices(userId: String, devices: Map<String, MXDeviceInfo>?) fun storeUserDevices(userId: String, devices: Map<String, MXDeviceInfo>?)
fun storeUserCrossSigningKeys(userId: String, masterKey: CrossSigningKeyInfo?,
selfSigningKey: CrossSigningKeyInfo?,
userSigningKey: CrossSigningKeyInfo?)
/** /**
* Retrieve the known devices for a user. * Retrieve the known devices for a user.
* *
@ -381,4 +388,24 @@ internal interface IMXCryptoStore {
fun addNewSessionListener(listener: NewSessionListener) fun addNewSessionListener(listener: NewSessionListener)
fun removeSessionListener(listener: NewSessionListener) fun removeSessionListener(listener: NewSessionListener)
//=============================================
// CROSS SIGNING
//=============================================
/**
* Gets the current crosssigning info
*/
fun getMyCrossSigningInfo() : MXCrossSigningInfo?
fun setMyCrossSigningInfo(info: MXCrossSigningInfo?)
fun getCrossSigningInfo(userId: String) : MXCrossSigningInfo?
fun setCrossSigningInfo(userId: String, info: MXCrossSigningInfo?)
fun getCrossSigningPrivateKeys() : PrivateKeysInfo?
fun setUserKeysAsTrusted(userId: String, trusted: Boolean = true)
} }

View file

@ -0,0 +1,7 @@
package im.vector.matrix.android.internal.crypto.store
data class PrivateKeysInfo(
val master: String? = null,
val selfSigned: String? = null,
val user: String? = null
)

View file

@ -17,21 +17,26 @@
package im.vector.matrix.android.internal.crypto.store.db package im.vector.matrix.android.internal.crypto.store.db
import im.vector.matrix.android.api.auth.data.Credentials import im.vector.matrix.android.api.auth.data.Credentials
import im.vector.matrix.android.api.session.crypto.crosssigning.MXCrossSigningInfo
import im.vector.matrix.android.internal.crypto.IncomingRoomKeyRequest import im.vector.matrix.android.internal.crypto.IncomingRoomKeyRequest
import im.vector.matrix.android.internal.crypto.NewSessionListener import im.vector.matrix.android.internal.crypto.NewSessionListener
import im.vector.matrix.android.internal.crypto.OutgoingRoomKeyRequest import im.vector.matrix.android.internal.crypto.OutgoingRoomKeyRequest
import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo
import im.vector.matrix.android.internal.crypto.model.OlmInboundGroupSessionWrapper import im.vector.matrix.android.internal.crypto.model.OlmInboundGroupSessionWrapper
import im.vector.matrix.android.internal.crypto.model.OlmSessionWrapper import im.vector.matrix.android.internal.crypto.model.OlmSessionWrapper
import im.vector.matrix.android.internal.crypto.model.rest.CrossSigningKeyInfo
import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
import im.vector.matrix.android.internal.crypto.store.PrivateKeysInfo
import im.vector.matrix.android.internal.crypto.store.db.model.* import im.vector.matrix.android.internal.crypto.store.db.model.*
import im.vector.matrix.android.internal.crypto.store.db.query.delete 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.getById
import im.vector.matrix.android.internal.crypto.store.db.query.getOrCreate import im.vector.matrix.android.internal.crypto.store.db.query.getOrCreate
import im.vector.matrix.android.internal.session.SessionScope import im.vector.matrix.android.internal.session.SessionScope
import io.realm.Realm import io.realm.Realm
import io.realm.RealmConfiguration import io.realm.RealmConfiguration
import io.realm.RealmList
import io.realm.Sort import io.realm.Sort
import io.realm.kotlin.where import io.realm.kotlin.where
import org.matrix.olm.OlmAccount import org.matrix.olm.OlmAccount
@ -187,7 +192,7 @@ internal class RealmCryptoStore(private val realmConfiguration: RealmConfigurati
} }
} }
override fun getUserDevice(deviceId: String, userId: String): MXDeviceInfo? { override fun getUserDevice(userId: String, deviceId: String): MXDeviceInfo? {
return doRealmQueryAndCopy(realmConfiguration) { return doRealmQueryAndCopy(realmConfiguration) {
it.where<DeviceInfoEntity>() it.where<DeviceInfoEntity>()
.equalTo(DeviceInfoEntityFields.PRIMARY_KEY, DeviceInfoEntity.createPrimaryKey(userId, deviceId)) .equalTo(DeviceInfoEntityFields.PRIMARY_KEY, DeviceInfoEntity.createPrimaryKey(userId, deviceId))
@ -231,6 +236,73 @@ internal class RealmCryptoStore(private val realmConfiguration: RealmConfigurati
} }
} }
override fun storeUserCrossSigningKeys(userId: String,
masterKey: CrossSigningKeyInfo?,
selfSigningKey: CrossSigningKeyInfo?,
userSigningKey: CrossSigningKeyInfo?) {
doRealmTransaction(realmConfiguration) { realm ->
UserEntity.getOrCreate(realm, userId)
.let { userEntity ->
if (masterKey == null || selfSigningKey == null) {
// The user has disabled cross signing?
userEntity.crossSigningInfoEntity?.deleteFromRealm()
userEntity.crossSigningInfoEntity = null
} else {
CrossSigningInfoEntity.getOrCreate(realm, userId).let { signingInfo ->
// What should we do if we detect a change of the keys?
val existingMaster = signingInfo.getMasterKey()
if (existingMaster != null && existingMaster.publicKeyBase64 == masterKey.unpaddedBase64PublicKey) {
//update signatures?
existingMaster.putSignatures(masterKey.signatures)
existingMaster.usages = masterKey.usages?.toTypedArray()?.let { RealmList(*it) }
?: RealmList()
} else {
val keyEntity = realm.createObject(KeyInfoEntity::class.java).apply {
this.publicKeyBase64 = masterKey.unpaddedBase64PublicKey
this.usages = masterKey.usages?.toTypedArray()?.let { RealmList(*it) }
?: RealmList()
this.putSignatures(masterKey.signatures)
}
signingInfo.setMasterKey(keyEntity)
}
val existingSelfSigned = signingInfo.getSelfSignedKey()
if (existingSelfSigned != null && existingSelfSigned.publicKeyBase64 == selfSigningKey.unpaddedBase64PublicKey) {
//update signatures?
existingSelfSigned.putSignatures(selfSigningKey.signatures)
existingSelfSigned.usages = selfSigningKey.usages?.toTypedArray()?.let { RealmList(*it) }
?: RealmList()
} else {
val keyEntity = realm.createObject(KeyInfoEntity::class.java).apply {
this.publicKeyBase64 = selfSigningKey.unpaddedBase64PublicKey
this.usages = selfSigningKey.usages?.toTypedArray()?.let { RealmList(*it) }
?: RealmList()
this.putSignatures(selfSigningKey.signatures)
}
signingInfo.setSelfSignedKey(keyEntity)
}
userEntity.crossSigningInfoEntity = signingInfo
}
}
}
}
}
override fun getCrossSigningPrivateKeys(): PrivateKeysInfo? {
return doRealmQueryAndCopy(realmConfiguration) { realm ->
realm.where<CryptoMetadataEntity>().findFirst()
}?.let {
PrivateKeysInfo(
master = it.xSignMasterPrivateKey,
selfSigned = it.xSignSelfSignedPrivateKey,
user = it.xSignUserPrivateKey
)
}
}
override fun getUserDevices(userId: String): Map<String, MXDeviceInfo>? { override fun getUserDevices(userId: String): Map<String, MXDeviceInfo>? {
return doRealmQueryAndCopy(realmConfiguration) { return doRealmQueryAndCopy(realmConfiguration) {
it.where<UserEntity>() it.where<UserEntity>()
@ -731,4 +803,82 @@ internal class RealmCryptoStore(private val realmConfiguration: RealmConfigurati
} }
.toMutableList() .toMutableList()
} }
/* ==========================================================================================
* Cross Signing
* ========================================================================================== */
override fun getMyCrossSigningInfo(): MXCrossSigningInfo? {
return doRealmQueryAndCopy(realmConfiguration) {
it.where<CryptoMetadataEntity>().findFirst()
}?.userId?.let {
getCrossSigningInfo(it)
}
}
override fun setMyCrossSigningInfo(info: MXCrossSigningInfo?) {
doRealmQueryAndCopy(realmConfiguration) {
it.where<CryptoMetadataEntity>().findFirst()
}?.userId?.let {
setCrossSigningInfo(it, info)
}
}
override fun setUserKeysAsTrusted(userId: String, trusted: Boolean) {
doRealmTransaction(realmConfiguration) { realm ->
realm.where(CrossSigningInfoEntity::class.java)
.equalTo(CrossSigningInfoEntityFields.USER_ID, userId)
.findFirst()
?.isTrusted = trusted
}
}
override fun getCrossSigningInfo(userId: String): MXCrossSigningInfo? {
return doRealmQueryAndCopy(realmConfiguration) { realm ->
realm.where(CrossSigningInfoEntity::class.java)
.equalTo(CrossSigningInfoEntityFields.USER_ID, userId)
.findFirst()
}?.let { xsignInfo ->
MXCrossSigningInfo(
userId = userId,
crossSigningKeys = xsignInfo.crossSigningKeys.mapNotNull {
val pubKey = it.publicKeyBase64 ?: return@mapNotNull null
CrossSigningKeyInfo(
userId = userId,
keys = mapOf("ed25519:$pubKey" to pubKey),
usages = it.usages.map { it },
signatures = it.getSignatures()
)
},
isTrusted = xsignInfo.isTrusted
)
}
}
override fun setCrossSigningInfo(userId: String, info: MXCrossSigningInfo?) {
doRealmTransaction(realmConfiguration) { realm ->
var existing = CrossSigningInfoEntity.get(realm, userId)
if (info == null) {
// Delete known if needed
existing?.deleteFromRealm()
// TODO notify, we might need to untrust things?
} else {
// Just override existing, caller should check and untrust id needed
existing = CrossSigningInfoEntity.getOrCreate(realm, userId)
val xkeys = RealmList<KeyInfoEntity>()
info.crossSigningKeys.forEach { info ->
xkeys.add(
realm.createObject(KeyInfoEntity::class.java).also { keyInfoEntity ->
keyInfoEntity.publicKeyBase64 = info.unpaddedBase64PublicKey
keyInfoEntity.usages = info.usages?.let { RealmList(*it.toTypedArray()) }
?: RealmList()
keyInfoEntity.putSignatures(info.signatures)
}
)
}
existing.crossSigningKeys = xkeys
}
}
}
} }

View file

@ -16,15 +16,50 @@
package im.vector.matrix.android.internal.crypto.store.db package im.vector.matrix.android.internal.crypto.store.db
import im.vector.matrix.android.internal.crypto.store.db.model.*
import im.vector.matrix.android.internal.crypto.store.db.model.KeyInfoEntity
import io.realm.DynamicRealm import io.realm.DynamicRealm
import io.realm.RealmMigration import io.realm.RealmMigration
import timber.log.Timber import timber.log.Timber
internal object RealmCryptoStoreMigration : RealmMigration { internal object RealmCryptoStoreMigration : RealmMigration {
const val CRYPTO_STORE_SCHEMA_VERSION = 0L // Version 1L added Cross Signing info persistence
const val CRYPTO_STORE_SCHEMA_VERSION = 1L
override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) { override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) {
Timber.v("Migrating Realm Crypto from $oldVersion to $newVersion") Timber.v("Migrating Realm Crypto from $oldVersion to $newVersion")
if (oldVersion <= 0) {
Timber.d("Step 0 -> 1")
Timber.d("Create KeyInfoEntity")
val keyInfoEntitySchema = realm.schema.create("KeyInfoEntity")
.addField(KeyInfoEntityFields.PUBLIC_KEY_BASE64, String::class.java)
.addField(KeyInfoEntityFields.SIGNATURES, String::class.java)
.addRealmListField(KeyInfoEntityFields.USAGES.`$`, String::class.java)
Timber.d("Create CrossSigningInfoEntity")
val crossSigningInfoSchema = realm.schema.create("CrossSigningInfoEntity")
.addField(CrossSigningInfoEntityFields.USER_ID, String::class.java)
.addField(CrossSigningInfoEntityFields.IS_TRUSTED, Boolean::class.java)
.addPrimaryKey(CrossSigningInfoEntityFields.USER_ID)
.addRealmListField(CrossSigningInfoEntityFields.CROSS_SIGNING_KEYS.`$`, keyInfoEntitySchema)
Timber.d("Updating UserEntity table")
realm.schema.get("UserEntity")
?.addRealmObjectField(UserEntityFields.CROSS_SIGNING_INFO_ENTITY.`$`, crossSigningInfoSchema)
Timber.d("Updating CryptoMetadataEntity table")
realm.schema.get("CryptoMetadataEntity")
?.addField(CryptoMetadataEntityFields.X_SIGN_MASTER_PRIVATE_KEY, String::class.java)
?.addField(CryptoMetadataEntityFields.X_SIGN_USER_PRIVATE_KEY, String::class.java)
?.addField(CryptoMetadataEntityFields.X_SIGN_SELF_SIGNED_PRIVATE_KEY, String::class.java)
}
} }
} }

View file

@ -32,6 +32,8 @@ import io.realm.annotations.RealmModule
OlmInboundGroupSessionEntity::class, OlmInboundGroupSessionEntity::class,
OlmSessionEntity::class, OlmSessionEntity::class,
OutgoingRoomKeyRequestEntity::class, OutgoingRoomKeyRequestEntity::class,
UserEntity::class UserEntity::class,
KeyInfoEntity::class,
CrossSigningInfoEntity::class
]) ])
internal class RealmCryptoStoreModule internal class RealmCryptoStoreModule

View file

@ -0,0 +1,51 @@
/*
* Copyright 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.model.rest.CrossSigningKeyInfo
import io.realm.RealmList
import io.realm.RealmObject
import io.realm.annotations.PrimaryKey
internal open class CrossSigningInfoEntity(
@PrimaryKey
var userId: String? = null,
var crossSigningKeys: RealmList<KeyInfoEntity> = RealmList(),
var isTrusted: Boolean = false
) : RealmObject() {
companion object
fun getMasterKey() = crossSigningKeys.firstOrNull { it.usages.contains(CrossSigningKeyInfo.KeyUsage.MASTER.value) }
fun setMasterKey(info: KeyInfoEntity?) {
crossSigningKeys
.filter { it.usages.contains(CrossSigningKeyInfo.KeyUsage.MASTER.value) }
.forEach { crossSigningKeys.remove(it) }
info?.let { crossSigningKeys.add(it) }
}
fun getSelfSignedKey() = crossSigningKeys.firstOrNull { it.usages.contains(CrossSigningKeyInfo.KeyUsage.SELF_SIGNING.value) }
fun setSelfSignedKey(info: KeyInfoEntity?) {
crossSigningKeys
.filter { it.usages.contains(CrossSigningKeyInfo.KeyUsage.SELF_SIGNING.value) }
.forEach { crossSigningKeys.remove(it) }
info?.let { crossSigningKeys.add(it) }
}
}

View file

@ -18,6 +18,7 @@ package im.vector.matrix.android.internal.crypto.store.db.model
import im.vector.matrix.android.internal.crypto.store.db.deserializeFromRealm import im.vector.matrix.android.internal.crypto.store.db.deserializeFromRealm
import im.vector.matrix.android.internal.crypto.store.db.serializeForRealm import im.vector.matrix.android.internal.crypto.store.db.serializeForRealm
import io.realm.Realm
import io.realm.RealmObject import io.realm.RealmObject
import io.realm.annotations.PrimaryKey import io.realm.annotations.PrimaryKey
import org.matrix.olm.OlmAccount import org.matrix.olm.OlmAccount
@ -34,7 +35,13 @@ internal open class CryptoMetadataEntity(
// Settings for blacklisting unverified devices. // Settings for blacklisting unverified devices.
var globalBlacklistUnverifiedDevices: Boolean = false, var globalBlacklistUnverifiedDevices: Boolean = false,
// The keys backup version currently used. Null means no backup. // The keys backup version currently used. Null means no backup.
var backupVersion: String? = null var backupVersion: String? = null,
var xSignMasterPrivateKey: String? = null,
var xSignUserPrivateKey: String? = null,
var xSignSelfSignedPrivateKey: String? = null
// var crossSigningInfoEntity: CrossSigningInfoEntity? = null
) : RealmObject() { ) : RealmObject() {
// Deserialize data // Deserialize data

View file

@ -0,0 +1,45 @@
/*
* Copyright 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.store.db.deserializeFromRealm
import im.vector.matrix.android.internal.crypto.store.db.serializeForRealm
import io.realm.RealmList
import io.realm.RealmObject
internal open class KeyInfoEntity(
var publicKeyBase64: String? = null,
// var isTrusted: Boolean = false,
var usages: RealmList<String> = RealmList(),
/**
* The signature of this MXDeviceInfo.
* A map from "<userId>" to a map from "<key type>:<Publickey>" to "<signature>"
*/
var signatures: String? = null
) : RealmObject() {
// Deserialize data
fun getSignatures(): Map<String, Map<String, String>>? {
return deserializeFromRealm(signatures)
}
// Serialize data
fun putSignatures(deviceInfo: Map<String, Map<String, String>>?) {
signatures = serializeForRealm(deviceInfo)
}
}

View file

@ -20,9 +20,11 @@ import io.realm.RealmList
import io.realm.RealmObject import io.realm.RealmObject
import io.realm.annotations.PrimaryKey import io.realm.annotations.PrimaryKey
internal open class UserEntity(@PrimaryKey var userId: String? = null, internal open class UserEntity(
var devices: RealmList<DeviceInfoEntity> = RealmList(), @PrimaryKey var userId: String? = null,
var deviceTrackingStatus: Int = 0) var devices: RealmList<DeviceInfoEntity> = RealmList(),
var crossSigningInfoEntity: CrossSigningInfoEntity? = null,
var deviceTrackingStatus: Int = 0)
: RealmObject() { : RealmObject() {
companion object companion object

View file

@ -0,0 +1,37 @@
/*
* Copyright 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.query
import im.vector.matrix.android.internal.crypto.store.db.model.CrossSigningInfoEntity
import im.vector.matrix.android.internal.crypto.store.db.model.UserEntityFields
import io.realm.Realm
import io.realm.kotlin.createObject
import io.realm.kotlin.where
internal fun CrossSigningInfoEntity.Companion.getOrCreate(realm: Realm, userId: String): CrossSigningInfoEntity {
return realm.where<CrossSigningInfoEntity>()
.equalTo(UserEntityFields.USER_ID, userId)
.findFirst()
?: realm.createObject(userId)
}
internal fun CrossSigningInfoEntity.Companion.get(realm: Realm, userId: String): CrossSigningInfoEntity? {
return realm.where<CrossSigningInfoEntity>()
.equalTo(UserEntityFields.USER_ID, userId)
.findFirst()
}

View file

@ -18,7 +18,7 @@ package im.vector.matrix.android.internal.crypto.tasks
import im.vector.matrix.android.internal.auth.data.LoginFlowTypes import im.vector.matrix.android.internal.auth.data.LoginFlowTypes
import im.vector.matrix.android.internal.crypto.api.CryptoApi import im.vector.matrix.android.internal.crypto.api.CryptoApi
import im.vector.matrix.android.internal.crypto.model.rest.DeleteDeviceAuth import im.vector.matrix.android.internal.crypto.model.rest.UserPasswordAuth
import im.vector.matrix.android.internal.crypto.model.rest.DeleteDeviceParams import im.vector.matrix.android.internal.crypto.model.rest.DeleteDeviceParams
import im.vector.matrix.android.internal.di.UserId import im.vector.matrix.android.internal.di.UserId
import im.vector.matrix.android.internal.network.executeRequest import im.vector.matrix.android.internal.network.executeRequest
@ -44,7 +44,7 @@ internal class DefaultDeleteDeviceWithUserPasswordTask @Inject constructor(
return executeRequest(eventBus) { return executeRequest(eventBus) {
apiCall = cryptoApi.deleteDevice(params.deviceId, DeleteDeviceParams() apiCall = cryptoApi.deleteDevice(params.deviceId, DeleteDeviceParams()
.apply { .apply {
deleteDeviceAuth = DeleteDeviceAuth() userPasswordAuth = UserPasswordAuth()
.apply { .apply {
type = LoginFlowTypes.PASSWORD type = LoginFlowTypes.PASSWORD
session = params.authSession session = params.authSession

View file

@ -0,0 +1,44 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.crypto.tasks
import im.vector.matrix.android.internal.crypto.api.CryptoApi
import im.vector.matrix.android.internal.crypto.model.MXKeysObject
import im.vector.matrix.android.internal.crypto.model.rest.SignatureUploadResponse
import im.vector.matrix.android.internal.network.executeRequest
import im.vector.matrix.android.internal.task.Task
import org.greenrobot.eventbus.EventBus
import javax.inject.Inject
internal interface UploadSignaturesTask : Task<UploadSignaturesTask.Params, SignatureUploadResponse> {
data class Params(
val signatures: Map<String, Map<String, MXKeysObject>>
)
}
internal class DefaultUploadSignaturesTask @Inject constructor(
private val cryptoApi: CryptoApi,
private val eventBus: EventBus
) : UploadSignaturesTask {
override suspend fun execute(params: UploadSignaturesTask.Params): SignatureUploadResponse {
return executeRequest(eventBus) {
apiCall = cryptoApi.uploadSignatures(params.signatures)
}
}
}

View file

@ -0,0 +1,92 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.crypto.tasks
import im.vector.matrix.android.api.failure.Failure
import im.vector.matrix.android.internal.auth.registration.RegistrationFlowResponse
import im.vector.matrix.android.internal.crypto.api.CryptoApi
import im.vector.matrix.android.internal.crypto.model.rest.CrossSigningKeyInfo
import im.vector.matrix.android.internal.crypto.model.rest.KeysQueryResponse
import im.vector.matrix.android.internal.crypto.model.rest.UploadSigningKeysBody
import im.vector.matrix.android.internal.crypto.model.rest.UserPasswordAuth
import im.vector.matrix.android.internal.di.MoshiProvider
import im.vector.matrix.android.internal.network.executeRequest
import im.vector.matrix.android.internal.task.Task
import org.greenrobot.eventbus.EventBus
import javax.inject.Inject
internal interface UploadSigningKeysTask : Task<UploadSigningKeysTask.Params, KeysQueryResponse> {
data class Params(
// the device keys to send.
val masterKey: CrossSigningKeyInfo,
// the one-time keys to send.
val userKey: CrossSigningKeyInfo,
// the explicit device_id to use for upload (default is to use the same as that used during auth).
val selfSignedKey: CrossSigningKeyInfo,
val userPasswordAuth: UserPasswordAuth?
)
}
internal class DefaultUploadSigningKeysTask @Inject constructor(
private val cryptoApi: CryptoApi,
private val eventBus: EventBus
) : UploadSigningKeysTask {
override suspend fun execute(params: UploadSigningKeysTask.Params): KeysQueryResponse {
val uploadQuery = UploadSigningKeysBody(
masterKey = params.masterKey,
userSigningKey = params.userKey,
selfSigningKey = params.selfSignedKey,
auth = params.userPasswordAuth.takeIf { params.userPasswordAuth?.session != null }
)
try {
// Make a first request to start user-interactive authentication
return executeRequest(eventBus) {
apiCall = cryptoApi.uploadSigningKeys(uploadQuery)
}
} catch (throwable: Throwable) {
if (throwable is Failure.OtherServerError
&& throwable.httpCode == 401
&& params.userPasswordAuth != null
/* Avoid infinite loop */
&& params.userPasswordAuth.session.isNullOrEmpty()
) {
try {
MoshiProvider.providesMoshi()
.adapter(RegistrationFlowResponse::class.java)
.fromJson(throwable.errorBody)
} catch (e: Exception) {
null
}?.let {
// Retry with authentication
return executeRequest(eventBus) {
apiCall = cryptoApi.uploadSigningKeys(
uploadQuery.copy(auth = params.userPasswordAuth.copy(session = it.session))
)
}
}
}
// Other error
throw throwable
}
}
}

View file

@ -108,7 +108,7 @@ internal class DefaultIncomingSASVerificationTransaction(
} }
// Bobs device ensures that it has a copy of Alices device key. // Bobs device ensures that it has a copy of Alices device key.
val mxDeviceInfo = cryptoStore.getUserDevice(deviceId = otherDeviceId!!, userId = otherUserId) val mxDeviceInfo = cryptoStore.getUserDevice(userId = otherUserId, deviceId = otherDeviceId!!)
if (mxDeviceInfo?.fingerprint() == null) { if (mxDeviceInfo?.fingerprint() == null) {
Timber.e("## SAS Failed to find device key ") Timber.e("## SAS Failed to find device key ")

View file

@ -21,6 +21,7 @@ import android.os.Looper
import dagger.Lazy import dagger.Lazy
import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.auth.data.Credentials import im.vector.matrix.android.api.auth.data.Credentials
import im.vector.matrix.android.api.auth.data.sessionId
import im.vector.matrix.android.api.session.crypto.CryptoService import im.vector.matrix.android.api.session.crypto.CryptoService
import im.vector.matrix.android.api.session.crypto.sas.* import im.vector.matrix.android.api.session.crypto.sas.*
import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.events.model.Event

View file

@ -20,6 +20,7 @@ import android.content.Context
import androidx.work.CoroutineWorker import androidx.work.CoroutineWorker
import androidx.work.WorkerParameters import androidx.work.WorkerParameters
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.failure.Failure import im.vector.matrix.android.api.failure.Failure
import im.vector.matrix.android.api.session.crypto.CryptoService import im.vector.matrix.android.api.session.crypto.CryptoService
import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.events.model.Event
@ -75,6 +76,10 @@ internal class EncryptEventWorker(context: Context, params: WorkerParameters)
localMutableContent.remove(it) localMutableContent.remove(it)
} }
crypto.downloadKeys(listOf("@testxsigningvfe:matrix.org"), true, object : MatrixCallback<Any> {
})
var error: Throwable? = null var error: Throwable? = null
var result: MXEncryptEventContentResult? = null var result: MXEncryptEventContentResult? = null
try { try {

View file

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<!-- Ref: https://developer.android.com/training/articles/security-config.html -->
<!-- By default, do not allow clearText traffic -->
<base-config cleartextTrafficPermitted="false" />
<!-- Allow clearText traffic on some specified host -->
<domain-config cleartextTrafficPermitted="true">
<!-- Localhost -->
<domain includeSubdomains="true">localhost</domain>
<domain includeSubdomains="true">127.0.0.1</domain>
<!-- Localhost for Android emulator -->
<domain includeSubdomains="true">10.0.2.2</domain>
</domain-config>
</network-security-config>

View file

@ -21,17 +21,37 @@ import android.app.NotificationManager
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Build import android.os.Build
import android.text.InputType
import android.widget.EditText
import androidx.appcompat.app.AlertDialog
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.core.app.Person import androidx.core.app.Person
import butterknife.OnClick import butterknife.OnClick
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.failure.Failure
import im.vector.matrix.android.internal.auth.data.LoginFlowTypes
import im.vector.matrix.android.internal.auth.registration.RegistrationFlowResponse
import im.vector.matrix.android.internal.crypto.model.rest.UserPasswordAuth
import im.vector.matrix.android.internal.di.MoshiProvider
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.di.ActiveSessionHolder
import im.vector.riotx.core.di.ScreenComponent
import im.vector.riotx.core.platform.VectorBaseActivity import im.vector.riotx.core.platform.VectorBaseActivity
import im.vector.riotx.features.debug.sas.DebugSasEmojiActivity import im.vector.riotx.features.debug.sas.DebugSasEmojiActivity
import javax.inject.Inject
class DebugMenuActivity : VectorBaseActivity() { class DebugMenuActivity : VectorBaseActivity() {
override fun getLayoutRes() = R.layout.activity_debug_menu override fun getLayoutRes() = R.layout.activity_debug_menu
@Inject
lateinit var activeSessionHolder: ActiveSessionHolder
override fun injectWith(injector: ScreenComponent) {
injector.inject(this)
}
@OnClick(R.id.debug_test_text_view_link) @OnClick(R.id.debug_test_text_view_link)
fun testTextViewLink() { fun testTextViewLink() {
startActivity(Intent(this, TestLinkifyActivity::class.java)) startActivity(Intent(this, TestLinkifyActivity::class.java))
@ -140,4 +160,60 @@ class DebugMenuActivity : VectorBaseActivity() {
fun testCrash() { fun testCrash() {
throw RuntimeException("Application crashed from user demand") throw RuntimeException("Application crashed from user demand")
} }
@OnClick(R.id.debug_initialise_xsigning)
fun testXSigning() {
activeSessionHolder.getActiveSession().getCrossSigningService().initializeCrossSigning(null, object : MatrixCallback<Unit> {
override fun onFailure(failure: Throwable) {
if (failure is Failure.OtherServerError
&& failure.httpCode == 401
) {
try {
MoshiProvider.providesMoshi()
.adapter(RegistrationFlowResponse::class.java)
.fromJson(failure.errorBody)
} catch (e: Exception) {
null
}?.let {
// Retry with authentication
if (it.flows?.any { it.stages?.contains(LoginFlowTypes.PASSWORD) == true } == true) {
// Ask for password
val inflater = this@DebugMenuActivity.layoutInflater
val layout = inflater.inflate(R.layout.dialog_base_edit_text, null)
val input = layout.findViewById<EditText>(R.id.edit_text).also {
it.inputType = InputType.TYPE_TEXT_VARIATION_PASSWORD
}
val activeSession = activeSessionHolder.getActiveSession()
AlertDialog.Builder(this@DebugMenuActivity)
.setTitle("Confirm password")
.setView(layout)
.setPositiveButton(R.string.ok) { _, _ ->
val pass = input.text.toString()
activeSession.getCrossSigningService().initializeCrossSigning(
UserPasswordAuth(
session = it.session,
user = activeSession.myUserId,
password = pass
)
)
}
.setNegativeButton(R.string.cancel, null)
.show()
} else {
// can't do this from here
AlertDialog.Builder(this@DebugMenuActivity)
.setTitle(R.string.dialog_title_error)
.setMessage("You cannot do that from mobile")
.setPositiveButton(R.string.ok, null)
.show()
}
}
}
}
})
}
} }

View file

@ -61,6 +61,13 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="Crash the app" /> android:text="Crash the app" />
<com.google.android.material.button.MaterialButton
android:id="@+id/debug_initialise_xsigning"
style="@style/VectorButtonStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Initialize XSigning" />
</LinearLayout> </LinearLayout>
</ScrollView> </ScrollView>

View file

@ -26,6 +26,7 @@ import im.vector.riotx.core.preference.UserAvatarPreference
import im.vector.riotx.features.MainActivity import im.vector.riotx.features.MainActivity
import im.vector.riotx.features.crypto.keysbackup.settings.KeysBackupManageActivity import im.vector.riotx.features.crypto.keysbackup.settings.KeysBackupManageActivity
import im.vector.riotx.features.crypto.verification.VerificationBottomSheet import im.vector.riotx.features.crypto.verification.VerificationBottomSheet
import im.vector.riotx.features.debug.DebugMenuActivity
import im.vector.riotx.features.home.HomeActivity import im.vector.riotx.features.home.HomeActivity
import im.vector.riotx.features.home.HomeModule import im.vector.riotx.features.home.HomeModule
import im.vector.riotx.features.createdirect.CreateDirectRoomActivity import im.vector.riotx.features.createdirect.CreateDirectRoomActivity
@ -139,6 +140,8 @@ interface ScreenComponent {
fun inject(permalinkHandlerActivity: PermalinkHandlerActivity) fun inject(permalinkHandlerActivity: PermalinkHandlerActivity)
fun inject(activity: DebugMenuActivity)
@Component.Factory @Component.Factory
interface Factory { interface Factory {
fun create(vectorComponent: VectorComponent, fun create(vectorComponent: VectorComponent,

View file

@ -50,6 +50,7 @@ import im.vector.matrix.android.internal.crypto.attachments.toElementToDecrypt
import im.vector.matrix.android.internal.crypto.model.event.EncryptedEventContent import im.vector.matrix.android.internal.crypto.model.event.EncryptedEventContent
import im.vector.matrix.rx.rx import im.vector.matrix.rx.rx
import im.vector.matrix.rx.unwrap import im.vector.matrix.rx.unwrap
import im.vector.riotx.BuildConfig
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.extensions.postLiveEvent import im.vector.riotx.core.extensions.postLiveEvent
import im.vector.riotx.core.platform.VectorViewModel import im.vector.riotx.core.platform.VectorViewModel
@ -188,7 +189,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
is RoomDetailAction.AcceptVerificationRequest -> handleAcceptVerification(action) is RoomDetailAction.AcceptVerificationRequest -> handleAcceptVerification(action)
is RoomDetailAction.DeclineVerificationRequest -> handleDeclineVerification(action) is RoomDetailAction.DeclineVerificationRequest -> handleDeclineVerification(action)
is RoomDetailAction.RequestVerification -> handleRequestVerification(action) is RoomDetailAction.RequestVerification -> handleRequestVerification(action)
is RoomDetailAction.ResumeVerification -> handleResumeRequestVerification(action) is RoomDetailAction.ResumeVerification -> handleResumeRequestVerification(action)
} }
} }
@ -809,7 +810,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
private fun handleAcceptVerification(action: RoomDetailAction.AcceptVerificationRequest) { private fun handleAcceptVerification(action: RoomDetailAction.AcceptVerificationRequest) {
Timber.v("## SAS handleAcceptVerification ${action.otherUserId}, roomId:${room.roomId}, txId:${action.transactionId}") Timber.v("## SAS handleAcceptVerification ${action.otherUserId}, roomId:${room.roomId}, txId:${action.transactionId}")
if (session.getSasVerificationService().readyPendingVerificationInDMs(action.otherUserId, room.roomId, if (session.getSasVerificationService().readyPendingVerificationInDMs(action.otherUserId, room.roomId,
action.transactionId)) { action.transactionId)) {
_requestLiveData.postValue(LiveEvent(Success(action))) _requestLiveData.postValue(LiveEvent(Success(action)))
} }
} }