mirror of
https://github.com/vector-im/element-android.git
synced 2024-11-15 01:35:07 +08:00
Initial commit
This commit is contained in:
parent
ea9166e0c6
commit
859c75df98
2
.gitignore
vendored
2
.gitignore
vendored
@ -15,3 +15,5 @@
|
||||
ktlint
|
||||
.idea/copyright/New_vector.xml
|
||||
.idea/copyright/profiles_settings.xml
|
||||
|
||||
.idea/copyright/New_Vector_Ltd.xml
|
||||
|
@ -19,8 +19,12 @@ package im.vector.matrix.android
|
||||
import android.content.Context
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
import java.io.File
|
||||
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
|
||||
import org.junit.Rule
|
||||
|
||||
|
||||
interface InstrumentedTest {
|
||||
|
||||
fun context(): Context {
|
||||
return ApplicationProvider.getApplicationContext()
|
||||
}
|
||||
|
@ -19,6 +19,7 @@ package im.vector.matrix.android.common
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import androidx.lifecycle.Observer
|
||||
import im.vector.matrix.android.api.Matrix
|
||||
import im.vector.matrix.android.api.MatrixCallback
|
||||
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.TimelineEvent
|
||||
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 java.util.*
|
||||
import java.util.concurrent.CountDownLatch
|
||||
@ -73,23 +79,26 @@ class CommonTestHelper(context: Context) {
|
||||
* @param session the session to sync
|
||||
*/
|
||||
fun syncSession(session: Session) {
|
||||
// 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)
|
||||
// }
|
||||
val lock = CountDownLatch(1)
|
||||
|
||||
session.open()
|
||||
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)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -17,11 +17,15 @@
|
||||
package im.vector.matrix.android.common
|
||||
|
||||
import android.os.SystemClock
|
||||
import androidx.lifecycle.Observer
|
||||
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.EventType
|
||||
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.roomSummaryQueryParams
|
||||
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.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.keysbackup.model.MegolmBackupAuthData
|
||||
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 java.util.*
|
||||
import java.util.concurrent.CountDownLatch
|
||||
@ -78,26 +86,32 @@ class CryptoTestHelper(val mTestHelper: CommonTestHelper) {
|
||||
val aliceSession = cryptoTestData.firstSession
|
||||
val aliceRoomId = cryptoTestData.roomId
|
||||
|
||||
val room = aliceSession.getRoom(aliceRoomId)!!
|
||||
val aliceRoom = aliceSession.getRoom(aliceRoomId)!!
|
||||
|
||||
val bobSession = mTestHelper.createAccount(TestConstants.USER_BOB, defaultSessionParams)
|
||||
|
||||
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) {
|
||||
statuses["invite"] = "invite"
|
||||
super.onSuccess(data)
|
||||
@ -108,25 +122,27 @@ class CryptoTestHelper(val mTestHelper: CommonTestHelper) {
|
||||
|
||||
assertTrue(statuses.containsKey("invite") && statuses.containsKey("onNewRoom"))
|
||||
|
||||
// bobSession.dataHandler.removeListener(bobEventListener)
|
||||
|
||||
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() {
|
||||
// override fun onLiveEvent(event: Event, roomState: RoomState) {
|
||||
// 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)) {
|
||||
// statuses["AliceJoin"] = "AliceJoin"
|
||||
// lock2.countDown()
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// })
|
||||
GlobalScope.launch(Dispatchers.Main) {
|
||||
bobRoomSummariesLive.observeForever(roomJoinedObserver)
|
||||
}
|
||||
|
||||
|
||||
|
||||
bobSession.joinRoom(aliceRoomId, callback = TestMatrixCallback(lock2))
|
||||
|
||||
mTestHelper.await(lock2)
|
||||
|
||||
|
@ -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)
|
||||
|
||||
}
|
||||
}
|
@ -36,6 +36,10 @@ import org.junit.runner.RunWith
|
||||
import org.junit.runners.MethodSorters
|
||||
import java.util.*
|
||||
import java.util.concurrent.CountDownLatch
|
||||
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
|
||||
import androidx.test.annotation.UiThreadTest
|
||||
import org.junit.Rule
|
||||
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
|
||||
@ -135,8 +139,24 @@ class SASTest : InstrumentedTest {
|
||||
val tid = "00000000"
|
||||
|
||||
// Bob should receive a cancel
|
||||
var canceledToDeviceEvent: Event? = null
|
||||
var cancelReason: String? = null
|
||||
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 override fun onToDeviceEvent(event: Event?) {
|
||||
// TODO if (event!!.getType() == CryptoEvent.EVENT_TYPE_KEY_VERIFICATION_CANCEL) {
|
||||
@ -156,8 +176,8 @@ class SASTest : InstrumentedTest {
|
||||
override fun transactionCreated(tx: SasVerificationTransaction) {}
|
||||
|
||||
override fun transactionUpdated(tx: SasVerificationTransaction) {
|
||||
if ((tx as IncomingSASVerificationTransaction).uxState === IncomingSasVerificationTransaction.UxState.SHOW_ACCEPT) {
|
||||
(tx as IncomingSASVerificationTransaction).performAccept()
|
||||
if ((tx as IncomingSasVerificationTransaction).uxState === IncomingSasVerificationTransaction.UxState.SHOW_ACCEPT) {
|
||||
(tx as IncomingSasVerificationTransaction).performAccept()
|
||||
}
|
||||
}
|
||||
|
||||
@ -169,8 +189,7 @@ class SASTest : InstrumentedTest {
|
||||
|
||||
mTestHelper.await(cancelLatch)
|
||||
|
||||
val cancelReq = canceledToDeviceEvent!!.content.toModel<KeyVerificationCancel>()!!
|
||||
assertEquals("Request should be cancelled with m.unknown_method", CancelCode.UnknownMethod.value, cancelReq.code)
|
||||
assertEquals("Request should be cancelled with m.unknown_method", CancelCode.UnknownMethod.value, cancelReason)
|
||||
|
||||
cryptoTestData.close()
|
||||
}
|
||||
@ -257,14 +276,15 @@ class SASTest : InstrumentedTest {
|
||||
hashes: List<String> = SASVerificationTransaction.KNOWN_HASHES,
|
||||
mac: List<String> = SASVerificationTransaction.KNOWN_MACS,
|
||||
codes: List<String> = SASVerificationTransaction.KNOWN_SHORT_CODES) {
|
||||
val startMessage = KeyVerificationStart()
|
||||
startMessage.fromDevice = bobSession.getMyDevice().deviceId
|
||||
startMessage.method = KeyVerificationStart.VERIF_METHOD_SAS
|
||||
startMessage.transactionID = tid
|
||||
startMessage.keyAgreementProtocols = protocols
|
||||
startMessage.hashes = hashes
|
||||
startMessage.messageAuthenticationCodes = mac
|
||||
startMessage.shortAuthenticationStrings = codes
|
||||
val startMessage = KeyVerificationStart(
|
||||
fromDevice = bobSession.getMyDevice().deviceId,
|
||||
method = KeyVerificationStart.VERIF_METHOD_SAS,
|
||||
transactionID = tid,
|
||||
keyAgreementProtocols = protocols,
|
||||
hashes = hashes,
|
||||
messageAuthenticationCodes = mac,
|
||||
shortAuthenticationStrings = codes
|
||||
)
|
||||
|
||||
val contentMap = MXUsersDevicesMap<Any>()
|
||||
contentMap.setObject(aliceUserID, aliceDevice, startMessage)
|
||||
@ -344,8 +364,8 @@ class SASTest : InstrumentedTest {
|
||||
override fun transactionUpdated(tx: SasVerificationTransaction) {
|
||||
if ((tx as SASVerificationTransaction).state === SasVerificationTxState.OnAccepted) {
|
||||
val at = tx as SASVerificationTransaction
|
||||
accepted = at.accepted
|
||||
startReq = at.startReq
|
||||
accepted = at.accepted as? KeyVerificationAccept
|
||||
startReq = at.startReq as? KeyVerificationStart
|
||||
aliceAcceptedLatch.countDown()
|
||||
}
|
||||
}
|
||||
@ -356,8 +376,8 @@ class SASTest : InstrumentedTest {
|
||||
override fun transactionCreated(tx: SasVerificationTransaction) {}
|
||||
|
||||
override fun transactionUpdated(tx: SasVerificationTransaction) {
|
||||
if ((tx as IncomingSASVerificationTransaction).uxState === IncomingSasVerificationTransaction.UxState.SHOW_ACCEPT) {
|
||||
val at = tx as IncomingSASVerificationTransaction
|
||||
if ((tx as IncomingSasVerificationTransaction).uxState === IncomingSasVerificationTransaction.UxState.SHOW_ACCEPT) {
|
||||
val at = tx as IncomingSasVerificationTransaction
|
||||
at.performAccept()
|
||||
}
|
||||
}
|
||||
@ -401,7 +421,7 @@ class SASTest : InstrumentedTest {
|
||||
override fun transactionCreated(tx: SasVerificationTransaction) {}
|
||||
|
||||
override fun transactionUpdated(tx: SasVerificationTransaction) {
|
||||
val uxState = (tx as OutgoingSASVerificationRequest).uxState
|
||||
val uxState = (tx as OutgoingSasVerificationRequest).uxState
|
||||
when (uxState) {
|
||||
OutgoingSasVerificationRequest.UxState.SHOW_SAS -> {
|
||||
aliceSASLatch.countDown()
|
||||
@ -419,7 +439,7 @@ class SASTest : InstrumentedTest {
|
||||
override fun transactionCreated(tx: SasVerificationTransaction) {}
|
||||
|
||||
override fun transactionUpdated(tx: SasVerificationTransaction) {
|
||||
val uxState = (tx as IncomingSASVerificationTransaction).uxState
|
||||
val uxState = (tx as IncomingSasVerificationTransaction).uxState
|
||||
when (uxState) {
|
||||
IncomingSasVerificationTransaction.UxState.SHOW_ACCEPT -> {
|
||||
tx.performAccept()
|
||||
@ -465,7 +485,7 @@ class SASTest : InstrumentedTest {
|
||||
override fun transactionCreated(tx: SasVerificationTransaction) {}
|
||||
|
||||
override fun transactionUpdated(tx: SasVerificationTransaction) {
|
||||
val uxState = (tx as OutgoingSASVerificationRequest).uxState
|
||||
val uxState = (tx as OutgoingSasVerificationRequest).uxState
|
||||
when (uxState) {
|
||||
OutgoingSasVerificationRequest.UxState.SHOW_SAS -> {
|
||||
tx.userHasVerifiedShortCode()
|
||||
@ -486,7 +506,7 @@ class SASTest : InstrumentedTest {
|
||||
override fun transactionCreated(tx: SasVerificationTransaction) {}
|
||||
|
||||
override fun transactionUpdated(tx: SasVerificationTransaction) {
|
||||
val uxState = (tx as IncomingSASVerificationTransaction).uxState
|
||||
val uxState = (tx as IncomingSasVerificationTransaction).uxState
|
||||
when (uxState) {
|
||||
IncomingSasVerificationTransaction.UxState.SHOW_ACCEPT -> {
|
||||
tx.performAccept()
|
||||
|
@ -6,9 +6,10 @@
|
||||
<uses-permission android:name="android.permission.VIBRATE" />
|
||||
<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:exported="false"
|
||||
tools:node="remove" />
|
||||
|
@ -19,6 +19,7 @@ package im.vector.matrix.android.api.session.crypto
|
||||
import android.content.Context
|
||||
import im.vector.matrix.android.api.MatrixCallback
|
||||
import im.vector.matrix.android.api.listeners.ProgressListener
|
||||
import im.vector.matrix.android.api.session.crypto.crosssigning.CrossSigningService
|
||||
import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupService
|
||||
import im.vector.matrix.android.api.session.crypto.keyshare.RoomKeysRequestListener
|
||||
import im.vector.matrix.android.api.session.crypto.sas.SasVerificationService
|
||||
@ -47,6 +48,8 @@ interface CryptoService {
|
||||
fun isCryptoEnabled(): Boolean
|
||||
|
||||
fun getSasVerificationService(): SasVerificationService
|
||||
|
||||
fun getCrossSigningService(): CrossSigningService
|
||||
|
||||
fun getKeysBackupService(): KeysBackupService
|
||||
|
||||
|
@ -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>)
|
||||
}
|
@ -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
|
||||
}
|
@ -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 }
|
||||
|
||||
}
|
@ -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
|
||||
// )
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
//
|
@ -132,6 +132,12 @@ internal abstract class CryptoModule {
|
||||
@Binds
|
||||
abstract fun bindUploadKeysTask(uploadKeysTask: DefaultUploadKeysTask): UploadKeysTask
|
||||
|
||||
@Binds
|
||||
abstract fun bindUploadSigningKeysTask(uploadKeysTask: DefaultUploadSigningKeysTask): UploadSigningKeysTask
|
||||
|
||||
@Binds
|
||||
abstract fun bindUploadSignaturesTask(uploadSignaturesTask: DefaultUploadSignaturesTask): UploadSignaturesTask
|
||||
|
||||
@Binds
|
||||
abstract fun bindDownloadKeysForUsersTask(downloadKeysForUsersTask: DefaultDownloadKeysForUsers): DownloadKeysForUsersTask
|
||||
|
||||
|
@ -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.megolm.MXMegolmEncryptionFactory
|
||||
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.model.ImportRoomKeysResult
|
||||
import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo
|
||||
@ -113,6 +114,8 @@ internal class DefaultCryptoService @Inject constructor(
|
||||
private val roomDecryptorProvider: RoomDecryptorProvider,
|
||||
// The SAS verification service.
|
||||
private val sasVerificationService: DefaultSasVerificationService,
|
||||
|
||||
private val crossSigningService: DefaultCrossSigningService,
|
||||
//
|
||||
private val incomingRoomKeyRequestManager: IncomingRoomKeyRequestManager,
|
||||
//
|
||||
@ -317,6 +320,8 @@ internal class DefaultCryptoService @Inject constructor(
|
||||
*/
|
||||
override fun getSasVerificationService() = sasVerificationService
|
||||
|
||||
override fun getCrossSigningService() = crossSigningService
|
||||
|
||||
/**
|
||||
* A sync response has been received
|
||||
*
|
||||
@ -364,7 +369,7 @@ internal class DefaultCryptoService @Inject constructor(
|
||||
*/
|
||||
override fun getDeviceInfo(userId: String, deviceId: String?): MXDeviceInfo? {
|
||||
return if (userId.isNotEmpty() && !deviceId.isNullOrEmpty()) {
|
||||
cryptoStore.getUserDevice(deviceId, userId)
|
||||
cryptoStore.getUserDevice(userId, deviceId)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
@ -171,8 +171,8 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
|
||||
for ((k, value) in failures) {
|
||||
val statusCode = when (val status = value["status"]) {
|
||||
is Double -> status.toInt()
|
||||
is Int -> status.toInt()
|
||||
else -> 0
|
||||
is Int -> status.toInt()
|
||||
else -> 0
|
||||
}
|
||||
if (statusCode == 503) {
|
||||
synchronized(notReadyToRetryHS) {
|
||||
@ -289,7 +289,7 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
|
||||
val mutableDevices = devices.toMutableMap()
|
||||
for ((deviceId, deviceInfo) in devices) {
|
||||
// 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)
|
||||
// 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
|
||||
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)
|
||||
}
|
||||
|
@ -107,7 +107,7 @@ internal class IncomingRoomKeyRequestManager @Inject constructor(
|
||||
cryptoStore.deleteIncomingRoomKeyRequest(request)
|
||||
}
|
||||
// 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.isVerified) {
|
||||
Timber.v("## processReceivedRoomKeyRequests() : device is already verified: sharing keys")
|
||||
|
@ -141,6 +141,7 @@ internal class MXOlmDevice @Inject constructor(
|
||||
*/
|
||||
fun release() {
|
||||
olmAccount?.releaseAccount()
|
||||
olmUtility?.releaseUtility()
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -28,7 +28,7 @@ internal class SetDeviceVerificationAction @Inject constructor(
|
||||
private val keysBackup: KeysBackup) {
|
||||
|
||||
fun handle(verificationStatus: Int, deviceId: String, userId: String) {
|
||||
val device = cryptoStore.getUserDevice(deviceId, userId)
|
||||
val device = cryptoStore.getUserDevice(userId, deviceId)
|
||||
|
||||
// Sanity check
|
||||
if (null == device) {
|
||||
|
@ -297,7 +297,7 @@ internal class MXMegolmDecryption(private val userId: String,
|
||||
runCatching { deviceListManager.downloadKeys(listOf(userId), false) }
|
||||
.mapCatching {
|
||||
val deviceId = request.deviceId
|
||||
val deviceInfo = cryptoStore.getUserDevice(deviceId ?: "", userId)
|
||||
val deviceInfo = cryptoStore.getUserDevice(userId, deviceId ?: "")
|
||||
if (deviceInfo == null) {
|
||||
throw RuntimeException()
|
||||
} else {
|
||||
|
@ -16,6 +16,7 @@
|
||||
*/
|
||||
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.network.NetworkConstants
|
||||
import retrofit2.Call
|
||||
@ -65,6 +66,36 @@ internal interface CryptoApi {
|
||||
@POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "keys/query")
|
||||
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.
|
||||
* Doc: https://matrix.org/docs/spec/client_server/r0.4.0.html#post-matrix-client-r0-keys-claim
|
||||
|
@ -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())
|
||||
}
|
||||
|
@ -402,7 +402,7 @@ internal class KeysBackup @Inject constructor(
|
||||
}
|
||||
|
||||
if (deviceId != null) {
|
||||
val device = cryptoStore.getUserDevice(deviceId, userId)
|
||||
val device = cryptoStore.getUserDevice(userId, deviceId)
|
||||
var isSignatureValid = false
|
||||
|
||||
if (device == null) {
|
||||
|
@ -147,6 +147,14 @@ data class MXDeviceInfo(
|
||||
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
|
||||
*/
|
||||
|
@ -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>
|
||||
}
|
||||
|
@ -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")
|
||||
}
|
||||
|
||||
}
|
@ -24,5 +24,5 @@ import com.squareup.moshi.JsonClass
|
||||
@JsonClass(generateAdapter = true)
|
||||
internal data class DeleteDeviceParams(
|
||||
@Json(name = "auth")
|
||||
var deleteDeviceAuth: DeleteDeviceAuth? = null
|
||||
var userPasswordAuth: UserPasswordAuth? = null
|
||||
)
|
||||
|
@ -18,12 +18,12 @@ package im.vector.matrix.android.internal.crypto.model.rest
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
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)
|
||||
data class DeviceKeys(
|
||||
@Json(name = "user_id")
|
||||
val userId: String,
|
||||
override val userId: String,
|
||||
|
||||
@Json(name = "device_id")
|
||||
val deviceId: String,
|
||||
@ -32,8 +32,8 @@ data class DeviceKeys(
|
||||
val algorithms: List<String>,
|
||||
|
||||
@Json(name = "keys")
|
||||
val keys: Map<String, String>,
|
||||
override val keys: Map<String, String>,
|
||||
|
||||
@Json(name = "signatures")
|
||||
val signatures: JsonDict
|
||||
)
|
||||
override val signatures: Map<String, Map<String, String>>?
|
||||
) : MXKeysObject
|
||||
|
@ -1,6 +1,5 @@
|
||||
/*
|
||||
* 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.
|
||||
@ -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
|
||||
*
|
||||
* 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)
|
||||
data class KeysQueryResponse(
|
||||
@ -38,5 +42,16 @@ data class KeysQueryResponse(
|
||||
* The failures sorted by homeservers. TODO Bad comment ?
|
||||
* 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
|
||||
|
||||
)
|
||||
|
||||
|
@ -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
|
||||
)
|
@ -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
|
||||
}
|
||||
|
||||
}
|
@ -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
|
||||
)
|
@ -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
|
||||
)
|
@ -1,5 +1,6 @@
|
||||
/*
|
||||
* Copyright 2016 OpenMarket Ltd
|
||||
* 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.
|
||||
@ -17,12 +18,13 @@ 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.data.LoginFlowTypes
|
||||
|
||||
/**
|
||||
* This class provides the authentication data to delete a device
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
internal data class DeleteDeviceAuth(
|
||||
data class UserPasswordAuth(
|
||||
|
||||
// device device session id
|
||||
@Json(name = "session")
|
||||
@ -30,7 +32,7 @@ internal data class DeleteDeviceAuth(
|
||||
|
||||
// registration information
|
||||
@Json(name = "type")
|
||||
var type: String? = null,
|
||||
var type: String? = LoginFlowTypes.PASSWORD,
|
||||
|
||||
@Json(name = "user")
|
||||
var user: String? = null,
|
@ -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
|
@ -17,12 +17,14 @@
|
||||
|
||||
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.NewSessionListener
|
||||
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.OlmInboundGroupSessionWrapper
|
||||
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.store.db.model.KeysBackupDataEntity
|
||||
import org.matrix.olm.OlmAccount
|
||||
@ -163,7 +165,7 @@ internal interface IMXCryptoStore {
|
||||
* @param userId the user's id.
|
||||
* @return the device
|
||||
*/
|
||||
fun getUserDevice(deviceId: String, userId: String): MXDeviceInfo?
|
||||
fun getUserDevice(userId: String, deviceId: String): MXDeviceInfo?
|
||||
|
||||
/**
|
||||
* Retrieve a device by its identity key.
|
||||
@ -181,6 +183,11 @@ internal interface IMXCryptoStore {
|
||||
*/
|
||||
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.
|
||||
*
|
||||
@ -381,4 +388,24 @@ internal interface IMXCryptoStore {
|
||||
fun addNewSessionListener(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)
|
||||
}
|
||||
|
@ -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
|
||||
)
|
@ -17,21 +17,26 @@
|
||||
package im.vector.matrix.android.internal.crypto.store.db
|
||||
|
||||
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.NewSessionListener
|
||||
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.OlmInboundGroupSessionWrapper
|
||||
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.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.query.delete
|
||||
import im.vector.matrix.android.internal.crypto.store.db.query.get
|
||||
import im.vector.matrix.android.internal.crypto.store.db.query.getById
|
||||
import im.vector.matrix.android.internal.crypto.store.db.query.getOrCreate
|
||||
import im.vector.matrix.android.internal.session.SessionScope
|
||||
import io.realm.Realm
|
||||
import io.realm.RealmConfiguration
|
||||
import io.realm.RealmList
|
||||
import io.realm.Sort
|
||||
import io.realm.kotlin.where
|
||||
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) {
|
||||
it.where<DeviceInfoEntity>()
|
||||
.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>? {
|
||||
return doRealmQueryAndCopy(realmConfiguration) {
|
||||
it.where<UserEntity>()
|
||||
@ -731,4 +803,82 @@ internal class RealmCryptoStore(private val realmConfiguration: RealmConfigurati
|
||||
}
|
||||
.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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -16,15 +16,50 @@
|
||||
|
||||
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.RealmMigration
|
||||
import timber.log.Timber
|
||||
|
||||
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) {
|
||||
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)
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -32,6 +32,8 @@ import io.realm.annotations.RealmModule
|
||||
OlmInboundGroupSessionEntity::class,
|
||||
OlmSessionEntity::class,
|
||||
OutgoingRoomKeyRequestEntity::class,
|
||||
UserEntity::class
|
||||
UserEntity::class,
|
||||
KeyInfoEntity::class,
|
||||
CrossSigningInfoEntity::class
|
||||
])
|
||||
internal class RealmCryptoStoreModule
|
||||
|
@ -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) }
|
||||
}
|
||||
|
||||
}
|
@ -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.serializeForRealm
|
||||
import io.realm.Realm
|
||||
import io.realm.RealmObject
|
||||
import io.realm.annotations.PrimaryKey
|
||||
import org.matrix.olm.OlmAccount
|
||||
@ -34,7 +35,13 @@ internal open class CryptoMetadataEntity(
|
||||
// Settings for blacklisting unverified devices.
|
||||
var globalBlacklistUnverifiedDevices: Boolean = false,
|
||||
// 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() {
|
||||
|
||||
// Deserialize data
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
@ -20,9 +20,11 @@ import io.realm.RealmList
|
||||
import io.realm.RealmObject
|
||||
import io.realm.annotations.PrimaryKey
|
||||
|
||||
internal open class UserEntity(@PrimaryKey var userId: String? = null,
|
||||
var devices: RealmList<DeviceInfoEntity> = RealmList(),
|
||||
var deviceTrackingStatus: Int = 0)
|
||||
internal open class UserEntity(
|
||||
@PrimaryKey var userId: String? = null,
|
||||
var devices: RealmList<DeviceInfoEntity> = RealmList(),
|
||||
var crossSigningInfoEntity: CrossSigningInfoEntity? = null,
|
||||
var deviceTrackingStatus: Int = 0)
|
||||
: RealmObject() {
|
||||
|
||||
companion object
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -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.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.di.UserId
|
||||
import im.vector.matrix.android.internal.network.executeRequest
|
||||
@ -44,7 +44,7 @@ internal class DefaultDeleteDeviceWithUserPasswordTask @Inject constructor(
|
||||
return executeRequest(eventBus) {
|
||||
apiCall = cryptoApi.deleteDevice(params.deviceId, DeleteDeviceParams()
|
||||
.apply {
|
||||
deleteDeviceAuth = DeleteDeviceAuth()
|
||||
userPasswordAuth = UserPasswordAuth()
|
||||
.apply {
|
||||
type = LoginFlowTypes.PASSWORD
|
||||
session = params.authSession
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -108,7 +108,7 @@ internal class DefaultIncomingSASVerificationTransaction(
|
||||
}
|
||||
|
||||
// Bob’s device ensures that it has a copy of Alice’s device key.
|
||||
val mxDeviceInfo = cryptoStore.getUserDevice(deviceId = otherDeviceId!!, userId = otherUserId)
|
||||
val mxDeviceInfo = cryptoStore.getUserDevice(userId = otherUserId, deviceId = otherDeviceId!!)
|
||||
|
||||
if (mxDeviceInfo?.fingerprint() == null) {
|
||||
Timber.e("## SAS Failed to find device key ")
|
||||
|
@ -21,6 +21,7 @@ import android.os.Looper
|
||||
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.auth.data.sessionId
|
||||
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.events.model.Event
|
||||
|
@ -20,6 +20,7 @@ import android.content.Context
|
||||
import androidx.work.CoroutineWorker
|
||||
import androidx.work.WorkerParameters
|
||||
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.session.crypto.CryptoService
|
||||
import im.vector.matrix.android.api.session.events.model.Event
|
||||
@ -75,6 +76,10 @@ internal class EncryptEventWorker(context: Context, params: WorkerParameters)
|
||||
localMutableContent.remove(it)
|
||||
}
|
||||
|
||||
crypto.downloadKeys(listOf("@testxsigningvfe:matrix.org"), true, object : MatrixCallback<Any> {
|
||||
|
||||
})
|
||||
|
||||
var error: Throwable? = null
|
||||
var result: MXEncryptEventContentResult? = null
|
||||
try {
|
||||
|
@ -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>
|
@ -21,17 +21,37 @@ import android.app.NotificationManager
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
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.Person
|
||||
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.core.di.ActiveSessionHolder
|
||||
import im.vector.riotx.core.di.ScreenComponent
|
||||
import im.vector.riotx.core.platform.VectorBaseActivity
|
||||
import im.vector.riotx.features.debug.sas.DebugSasEmojiActivity
|
||||
import javax.inject.Inject
|
||||
|
||||
class DebugMenuActivity : VectorBaseActivity() {
|
||||
|
||||
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)
|
||||
fun testTextViewLink() {
|
||||
startActivity(Intent(this, TestLinkifyActivity::class.java))
|
||||
@ -140,4 +160,60 @@ class DebugMenuActivity : VectorBaseActivity() {
|
||||
fun testCrash() {
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -61,6 +61,13 @@
|
||||
android:layout_height="wrap_content"
|
||||
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>
|
||||
|
||||
</ScrollView>
|
||||
|
@ -26,6 +26,7 @@ import im.vector.riotx.core.preference.UserAvatarPreference
|
||||
import im.vector.riotx.features.MainActivity
|
||||
import im.vector.riotx.features.crypto.keysbackup.settings.KeysBackupManageActivity
|
||||
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.HomeModule
|
||||
import im.vector.riotx.features.createdirect.CreateDirectRoomActivity
|
||||
@ -139,6 +140,8 @@ interface ScreenComponent {
|
||||
|
||||
fun inject(permalinkHandlerActivity: PermalinkHandlerActivity)
|
||||
|
||||
fun inject(activity: DebugMenuActivity)
|
||||
|
||||
@Component.Factory
|
||||
interface Factory {
|
||||
fun create(vectorComponent: VectorComponent,
|
||||
|
@ -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.rx.rx
|
||||
import im.vector.matrix.rx.unwrap
|
||||
import im.vector.riotx.BuildConfig
|
||||
import im.vector.riotx.R
|
||||
import im.vector.riotx.core.extensions.postLiveEvent
|
||||
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.DeclineVerificationRequest -> handleDeclineVerification(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) {
|
||||
Timber.v("## SAS handleAcceptVerification ${action.otherUserId}, roomId:${room.roomId}, txId:${action.transactionId}")
|
||||
if (session.getSasVerificationService().readyPendingVerificationInDMs(action.otherUserId, room.roomId,
|
||||
action.transactionId)) {
|
||||
action.transactionId)) {
|
||||
_requestLiveData.postValue(LiveEvent(Success(action)))
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user