Initial commit
This commit is contained in:
parent
ea9166e0c6
commit
859c75df98
|
@ -15,3 +15,5 @@
|
||||||
ktlint
|
ktlint
|
||||||
.idea/copyright/New_vector.xml
|
.idea/copyright/New_vector.xml
|
||||||
.idea/copyright/profiles_settings.xml
|
.idea/copyright/profiles_settings.xml
|
||||||
|
|
||||||
|
.idea/copyright/New_Vector_Ltd.xml
|
||||||
|
|
|
@ -19,8 +19,12 @@ package im.vector.matrix.android
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.test.core.app.ApplicationProvider
|
import androidx.test.core.app.ApplicationProvider
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
|
||||||
|
import org.junit.Rule
|
||||||
|
|
||||||
|
|
||||||
interface InstrumentedTest {
|
interface InstrumentedTest {
|
||||||
|
|
||||||
fun context(): Context {
|
fun context(): Context {
|
||||||
return ApplicationProvider.getApplicationContext()
|
return ApplicationProvider.getApplicationContext()
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,6 +19,7 @@ package im.vector.matrix.android.common
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import androidx.lifecycle.Observer
|
||||||
import im.vector.matrix.android.api.Matrix
|
import im.vector.matrix.android.api.Matrix
|
||||||
import im.vector.matrix.android.api.MatrixCallback
|
import im.vector.matrix.android.api.MatrixCallback
|
||||||
import im.vector.matrix.android.api.MatrixConfiguration
|
import im.vector.matrix.android.api.MatrixConfiguration
|
||||||
|
@ -31,6 +32,11 @@ import im.vector.matrix.android.api.session.room.Room
|
||||||
import im.vector.matrix.android.api.session.room.timeline.Timeline
|
import im.vector.matrix.android.api.session.room.timeline.Timeline
|
||||||
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
||||||
import im.vector.matrix.android.api.session.room.timeline.TimelineSettings
|
import im.vector.matrix.android.api.session.room.timeline.TimelineSettings
|
||||||
|
import im.vector.matrix.android.api.session.sync.SyncState
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.GlobalScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
import org.junit.Assert.*
|
import org.junit.Assert.*
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import java.util.concurrent.CountDownLatch
|
import java.util.concurrent.CountDownLatch
|
||||||
|
@ -73,23 +79,26 @@ class CommonTestHelper(context: Context) {
|
||||||
* @param session the session to sync
|
* @param session the session to sync
|
||||||
*/
|
*/
|
||||||
fun syncSession(session: Session) {
|
fun syncSession(session: Session) {
|
||||||
// val lock = CountDownLatch(1)
|
val lock = CountDownLatch(1)
|
||||||
|
|
||||||
// val observer = androidx.lifecycle.Observer<SyncState> { syncState ->
|
|
||||||
// if (syncState is SyncState.Idle) {
|
|
||||||
// lock.countDown()
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// TODO observe?
|
|
||||||
// while (session.syncState().value !is SyncState.Idle) {
|
|
||||||
// sleep(100)
|
|
||||||
// }
|
|
||||||
|
|
||||||
session.open()
|
session.open()
|
||||||
session.startSync(true)
|
session.startSync(true)
|
||||||
// await(lock)
|
|
||||||
// session.syncState().removeObserver(observer)
|
val syncLiveData = runBlocking(Dispatchers.Main) {
|
||||||
|
session.getSyncStateLive()
|
||||||
|
}
|
||||||
|
val syncObserver = object : Observer<SyncState> {
|
||||||
|
override fun onChanged(t: SyncState?) {
|
||||||
|
if (session.hasAlreadySynced()) {
|
||||||
|
lock.countDown()
|
||||||
|
syncLiveData.removeObserver(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
GlobalScope.launch(Dispatchers.Main) { syncLiveData.observeForever(syncObserver) }
|
||||||
|
|
||||||
|
await(lock)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -17,11 +17,15 @@
|
||||||
package im.vector.matrix.android.common
|
package im.vector.matrix.android.common
|
||||||
|
|
||||||
import android.os.SystemClock
|
import android.os.SystemClock
|
||||||
|
import androidx.lifecycle.Observer
|
||||||
import im.vector.matrix.android.api.session.Session
|
import im.vector.matrix.android.api.session.Session
|
||||||
import im.vector.matrix.android.api.session.events.model.Event
|
import im.vector.matrix.android.api.session.events.model.Event
|
||||||
import im.vector.matrix.android.api.session.events.model.EventType
|
import im.vector.matrix.android.api.session.events.model.EventType
|
||||||
import im.vector.matrix.android.api.session.events.model.toContent
|
import im.vector.matrix.android.api.session.events.model.toContent
|
||||||
|
import im.vector.matrix.android.api.session.room.model.Membership
|
||||||
|
import im.vector.matrix.android.api.session.room.model.RoomSummary
|
||||||
import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams
|
import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams
|
||||||
|
import im.vector.matrix.android.api.session.room.roomSummaryQueryParams
|
||||||
import im.vector.matrix.android.api.session.room.timeline.Timeline
|
import im.vector.matrix.android.api.session.room.timeline.Timeline
|
||||||
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
||||||
import im.vector.matrix.android.api.session.room.timeline.TimelineSettings
|
import im.vector.matrix.android.api.session.room.timeline.TimelineSettings
|
||||||
|
@ -29,6 +33,10 @@ import im.vector.matrix.android.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
|
||||||
import im.vector.matrix.android.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM_BACKUP
|
import im.vector.matrix.android.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM_BACKUP
|
||||||
import im.vector.matrix.android.internal.crypto.keysbackup.model.MegolmBackupAuthData
|
import im.vector.matrix.android.internal.crypto.keysbackup.model.MegolmBackupAuthData
|
||||||
import im.vector.matrix.android.internal.crypto.keysbackup.model.MegolmBackupCreationInfo
|
import im.vector.matrix.android.internal.crypto.keysbackup.model.MegolmBackupCreationInfo
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.GlobalScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
import org.junit.Assert.*
|
import org.junit.Assert.*
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import java.util.concurrent.CountDownLatch
|
import java.util.concurrent.CountDownLatch
|
||||||
|
@ -78,26 +86,32 @@ class CryptoTestHelper(val mTestHelper: CommonTestHelper) {
|
||||||
val aliceSession = cryptoTestData.firstSession
|
val aliceSession = cryptoTestData.firstSession
|
||||||
val aliceRoomId = cryptoTestData.roomId
|
val aliceRoomId = cryptoTestData.roomId
|
||||||
|
|
||||||
val room = aliceSession.getRoom(aliceRoomId)!!
|
val aliceRoom = aliceSession.getRoom(aliceRoomId)!!
|
||||||
|
|
||||||
val bobSession = mTestHelper.createAccount(TestConstants.USER_BOB, defaultSessionParams)
|
val bobSession = mTestHelper.createAccount(TestConstants.USER_BOB, defaultSessionParams)
|
||||||
|
|
||||||
val lock1 = CountDownLatch(2)
|
val lock1 = CountDownLatch(2)
|
||||||
|
|
||||||
// val bobEventListener = object : MXEventListener() {
|
|
||||||
// override fun onNewRoom(roomId: String) {
|
|
||||||
// if (TextUtils.equals(roomId, aliceRoomId)) {
|
|
||||||
// if (!statuses.containsKey("onNewRoom")) {
|
|
||||||
// statuses["onNewRoom"] = "onNewRoom"
|
|
||||||
// lock1.countDown()
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// bobSession.dataHandler.addListener(bobEventListener)
|
|
||||||
|
|
||||||
room.invite(bobSession.myUserId, callback = object : TestMatrixCallback<Unit>(lock1) {
|
val bobRoomSummariesLive = runBlocking(Dispatchers.Main) {
|
||||||
|
bobSession.getRoomSummariesLive(roomSummaryQueryParams { })
|
||||||
|
}
|
||||||
|
|
||||||
|
val newRoomObserver = object : Observer<List<RoomSummary>> {
|
||||||
|
override fun onChanged(t: List<RoomSummary>?) {
|
||||||
|
if (t?.isNotEmpty() == true) {
|
||||||
|
statuses["onNewRoom"] = "onNewRoom"
|
||||||
|
lock1.countDown()
|
||||||
|
bobRoomSummariesLive.removeObserver(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
GlobalScope.launch(Dispatchers.Main) {
|
||||||
|
bobRoomSummariesLive.observeForever(newRoomObserver)
|
||||||
|
}
|
||||||
|
|
||||||
|
aliceRoom.invite(bobSession.myUserId, callback = object : TestMatrixCallback<Unit>(lock1) {
|
||||||
override fun onSuccess(data: Unit) {
|
override fun onSuccess(data: Unit) {
|
||||||
statuses["invite"] = "invite"
|
statuses["invite"] = "invite"
|
||||||
super.onSuccess(data)
|
super.onSuccess(data)
|
||||||
|
@ -108,25 +122,27 @@ class CryptoTestHelper(val mTestHelper: CommonTestHelper) {
|
||||||
|
|
||||||
assertTrue(statuses.containsKey("invite") && statuses.containsKey("onNewRoom"))
|
assertTrue(statuses.containsKey("invite") && statuses.containsKey("onNewRoom"))
|
||||||
|
|
||||||
// bobSession.dataHandler.removeListener(bobEventListener)
|
|
||||||
|
|
||||||
val lock2 = CountDownLatch(2)
|
val lock2 = CountDownLatch(2)
|
||||||
|
|
||||||
bobSession.joinRoom(aliceRoomId, callback = TestMatrixCallback(lock2))
|
val roomJoinedObserver = object : Observer<List<RoomSummary>> {
|
||||||
|
override fun onChanged(t: List<RoomSummary>?) {
|
||||||
|
if (bobSession.getRoom(aliceRoomId)
|
||||||
|
?.getRoomMember(aliceSession.myUserId)
|
||||||
|
?.membership == Membership.JOIN) {
|
||||||
|
statuses["AliceJoin"] = "AliceJoin"
|
||||||
|
lock2.countDown()
|
||||||
|
bobRoomSummariesLive.removeObserver(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// room.addEventListener(object : MXEventListener() {
|
GlobalScope.launch(Dispatchers.Main) {
|
||||||
// override fun onLiveEvent(event: Event, roomState: RoomState) {
|
bobRoomSummariesLive.observeForever(roomJoinedObserver)
|
||||||
// if (TextUtils.equals(event.getType(), Event.EVENT_TYPE_STATE_ROOM_MEMBER)) {
|
}
|
||||||
// val contentToConsider = event.contentAsJsonObject
|
|
||||||
// val member = JsonUtils.toRoomMember(contentToConsider)
|
|
||||||
//
|
|
||||||
// if (TextUtils.equals(member.membership, RoomMember.MEMBERSHIP_JOIN)) {
|
bobSession.joinRoom(aliceRoomId, callback = TestMatrixCallback(lock2))
|
||||||
// statuses["AliceJoin"] = "AliceJoin"
|
|
||||||
// lock2.countDown()
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// })
|
|
||||||
|
|
||||||
mTestHelper.await(lock2)
|
mTestHelper.await(lock2)
|
||||||
|
|
||||||
|
|
|
@ -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 org.junit.runners.MethodSorters
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import java.util.concurrent.CountDownLatch
|
import java.util.concurrent.CountDownLatch
|
||||||
|
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
|
||||||
|
import androidx.test.annotation.UiThreadTest
|
||||||
|
import org.junit.Rule
|
||||||
|
|
||||||
|
|
||||||
@RunWith(AndroidJUnit4::class)
|
@RunWith(AndroidJUnit4::class)
|
||||||
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
|
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
|
||||||
|
@ -135,8 +139,24 @@ class SASTest : InstrumentedTest {
|
||||||
val tid = "00000000"
|
val tid = "00000000"
|
||||||
|
|
||||||
// Bob should receive a cancel
|
// Bob should receive a cancel
|
||||||
var canceledToDeviceEvent: Event? = null
|
var cancelReason: String? = null
|
||||||
val cancelLatch = CountDownLatch(1)
|
val cancelLatch = CountDownLatch(1)
|
||||||
|
|
||||||
|
|
||||||
|
val bobListener = object : SasVerificationService.SasVerificationListener {
|
||||||
|
override fun transactionCreated(tx: SasVerificationTransaction) {}
|
||||||
|
|
||||||
|
override fun transactionUpdated(tx: SasVerificationTransaction) {
|
||||||
|
if (tx.transactionId == tid && tx.cancelledReason != null) {
|
||||||
|
cancelReason = tx.cancelledReason?.humanReadable
|
||||||
|
cancelLatch.countDown()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun markedAsManuallyVerified(userId: String, deviceId: String) {}
|
||||||
|
}
|
||||||
|
bobSession.getSasVerificationService().addListener(bobListener)
|
||||||
|
|
||||||
// TODO bobSession!!.dataHandler.addListener(object : MXEventListener() {
|
// TODO bobSession!!.dataHandler.addListener(object : MXEventListener() {
|
||||||
// TODO override fun onToDeviceEvent(event: Event?) {
|
// TODO override fun onToDeviceEvent(event: Event?) {
|
||||||
// TODO if (event!!.getType() == CryptoEvent.EVENT_TYPE_KEY_VERIFICATION_CANCEL) {
|
// TODO if (event!!.getType() == CryptoEvent.EVENT_TYPE_KEY_VERIFICATION_CANCEL) {
|
||||||
|
@ -156,8 +176,8 @@ class SASTest : InstrumentedTest {
|
||||||
override fun transactionCreated(tx: SasVerificationTransaction) {}
|
override fun transactionCreated(tx: SasVerificationTransaction) {}
|
||||||
|
|
||||||
override fun transactionUpdated(tx: SasVerificationTransaction) {
|
override fun transactionUpdated(tx: SasVerificationTransaction) {
|
||||||
if ((tx as IncomingSASVerificationTransaction).uxState === IncomingSasVerificationTransaction.UxState.SHOW_ACCEPT) {
|
if ((tx as IncomingSasVerificationTransaction).uxState === IncomingSasVerificationTransaction.UxState.SHOW_ACCEPT) {
|
||||||
(tx as IncomingSASVerificationTransaction).performAccept()
|
(tx as IncomingSasVerificationTransaction).performAccept()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -169,8 +189,7 @@ class SASTest : InstrumentedTest {
|
||||||
|
|
||||||
mTestHelper.await(cancelLatch)
|
mTestHelper.await(cancelLatch)
|
||||||
|
|
||||||
val cancelReq = canceledToDeviceEvent!!.content.toModel<KeyVerificationCancel>()!!
|
assertEquals("Request should be cancelled with m.unknown_method", CancelCode.UnknownMethod.value, cancelReason)
|
||||||
assertEquals("Request should be cancelled with m.unknown_method", CancelCode.UnknownMethod.value, cancelReq.code)
|
|
||||||
|
|
||||||
cryptoTestData.close()
|
cryptoTestData.close()
|
||||||
}
|
}
|
||||||
|
@ -257,14 +276,15 @@ class SASTest : InstrumentedTest {
|
||||||
hashes: List<String> = SASVerificationTransaction.KNOWN_HASHES,
|
hashes: List<String> = SASVerificationTransaction.KNOWN_HASHES,
|
||||||
mac: List<String> = SASVerificationTransaction.KNOWN_MACS,
|
mac: List<String> = SASVerificationTransaction.KNOWN_MACS,
|
||||||
codes: List<String> = SASVerificationTransaction.KNOWN_SHORT_CODES) {
|
codes: List<String> = SASVerificationTransaction.KNOWN_SHORT_CODES) {
|
||||||
val startMessage = KeyVerificationStart()
|
val startMessage = KeyVerificationStart(
|
||||||
startMessage.fromDevice = bobSession.getMyDevice().deviceId
|
fromDevice = bobSession.getMyDevice().deviceId,
|
||||||
startMessage.method = KeyVerificationStart.VERIF_METHOD_SAS
|
method = KeyVerificationStart.VERIF_METHOD_SAS,
|
||||||
startMessage.transactionID = tid
|
transactionID = tid,
|
||||||
startMessage.keyAgreementProtocols = protocols
|
keyAgreementProtocols = protocols,
|
||||||
startMessage.hashes = hashes
|
hashes = hashes,
|
||||||
startMessage.messageAuthenticationCodes = mac
|
messageAuthenticationCodes = mac,
|
||||||
startMessage.shortAuthenticationStrings = codes
|
shortAuthenticationStrings = codes
|
||||||
|
)
|
||||||
|
|
||||||
val contentMap = MXUsersDevicesMap<Any>()
|
val contentMap = MXUsersDevicesMap<Any>()
|
||||||
contentMap.setObject(aliceUserID, aliceDevice, startMessage)
|
contentMap.setObject(aliceUserID, aliceDevice, startMessage)
|
||||||
|
@ -344,8 +364,8 @@ class SASTest : InstrumentedTest {
|
||||||
override fun transactionUpdated(tx: SasVerificationTransaction) {
|
override fun transactionUpdated(tx: SasVerificationTransaction) {
|
||||||
if ((tx as SASVerificationTransaction).state === SasVerificationTxState.OnAccepted) {
|
if ((tx as SASVerificationTransaction).state === SasVerificationTxState.OnAccepted) {
|
||||||
val at = tx as SASVerificationTransaction
|
val at = tx as SASVerificationTransaction
|
||||||
accepted = at.accepted
|
accepted = at.accepted as? KeyVerificationAccept
|
||||||
startReq = at.startReq
|
startReq = at.startReq as? KeyVerificationStart
|
||||||
aliceAcceptedLatch.countDown()
|
aliceAcceptedLatch.countDown()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -356,8 +376,8 @@ class SASTest : InstrumentedTest {
|
||||||
override fun transactionCreated(tx: SasVerificationTransaction) {}
|
override fun transactionCreated(tx: SasVerificationTransaction) {}
|
||||||
|
|
||||||
override fun transactionUpdated(tx: SasVerificationTransaction) {
|
override fun transactionUpdated(tx: SasVerificationTransaction) {
|
||||||
if ((tx as IncomingSASVerificationTransaction).uxState === IncomingSasVerificationTransaction.UxState.SHOW_ACCEPT) {
|
if ((tx as IncomingSasVerificationTransaction).uxState === IncomingSasVerificationTransaction.UxState.SHOW_ACCEPT) {
|
||||||
val at = tx as IncomingSASVerificationTransaction
|
val at = tx as IncomingSasVerificationTransaction
|
||||||
at.performAccept()
|
at.performAccept()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -401,7 +421,7 @@ class SASTest : InstrumentedTest {
|
||||||
override fun transactionCreated(tx: SasVerificationTransaction) {}
|
override fun transactionCreated(tx: SasVerificationTransaction) {}
|
||||||
|
|
||||||
override fun transactionUpdated(tx: SasVerificationTransaction) {
|
override fun transactionUpdated(tx: SasVerificationTransaction) {
|
||||||
val uxState = (tx as OutgoingSASVerificationRequest).uxState
|
val uxState = (tx as OutgoingSasVerificationRequest).uxState
|
||||||
when (uxState) {
|
when (uxState) {
|
||||||
OutgoingSasVerificationRequest.UxState.SHOW_SAS -> {
|
OutgoingSasVerificationRequest.UxState.SHOW_SAS -> {
|
||||||
aliceSASLatch.countDown()
|
aliceSASLatch.countDown()
|
||||||
|
@ -419,7 +439,7 @@ class SASTest : InstrumentedTest {
|
||||||
override fun transactionCreated(tx: SasVerificationTransaction) {}
|
override fun transactionCreated(tx: SasVerificationTransaction) {}
|
||||||
|
|
||||||
override fun transactionUpdated(tx: SasVerificationTransaction) {
|
override fun transactionUpdated(tx: SasVerificationTransaction) {
|
||||||
val uxState = (tx as IncomingSASVerificationTransaction).uxState
|
val uxState = (tx as IncomingSasVerificationTransaction).uxState
|
||||||
when (uxState) {
|
when (uxState) {
|
||||||
IncomingSasVerificationTransaction.UxState.SHOW_ACCEPT -> {
|
IncomingSasVerificationTransaction.UxState.SHOW_ACCEPT -> {
|
||||||
tx.performAccept()
|
tx.performAccept()
|
||||||
|
@ -465,7 +485,7 @@ class SASTest : InstrumentedTest {
|
||||||
override fun transactionCreated(tx: SasVerificationTransaction) {}
|
override fun transactionCreated(tx: SasVerificationTransaction) {}
|
||||||
|
|
||||||
override fun transactionUpdated(tx: SasVerificationTransaction) {
|
override fun transactionUpdated(tx: SasVerificationTransaction) {
|
||||||
val uxState = (tx as OutgoingSASVerificationRequest).uxState
|
val uxState = (tx as OutgoingSasVerificationRequest).uxState
|
||||||
when (uxState) {
|
when (uxState) {
|
||||||
OutgoingSasVerificationRequest.UxState.SHOW_SAS -> {
|
OutgoingSasVerificationRequest.UxState.SHOW_SAS -> {
|
||||||
tx.userHasVerifiedShortCode()
|
tx.userHasVerifiedShortCode()
|
||||||
|
@ -486,7 +506,7 @@ class SASTest : InstrumentedTest {
|
||||||
override fun transactionCreated(tx: SasVerificationTransaction) {}
|
override fun transactionCreated(tx: SasVerificationTransaction) {}
|
||||||
|
|
||||||
override fun transactionUpdated(tx: SasVerificationTransaction) {
|
override fun transactionUpdated(tx: SasVerificationTransaction) {
|
||||||
val uxState = (tx as IncomingSASVerificationTransaction).uxState
|
val uxState = (tx as IncomingSasVerificationTransaction).uxState
|
||||||
when (uxState) {
|
when (uxState) {
|
||||||
IncomingSasVerificationTransaction.UxState.SHOW_ACCEPT -> {
|
IncomingSasVerificationTransaction.UxState.SHOW_ACCEPT -> {
|
||||||
tx.performAccept()
|
tx.performAccept()
|
||||||
|
|
|
@ -6,9 +6,10 @@
|
||||||
<uses-permission android:name="android.permission.VIBRATE" />
|
<uses-permission android:name="android.permission.VIBRATE" />
|
||||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||||
|
|
||||||
<application>
|
<application android:networkSecurityConfig="@xml/network_security_config">
|
||||||
|
|
||||||
<provider android:name="androidx.work.impl.WorkManagerInitializer"
|
<provider
|
||||||
|
android:name="androidx.work.impl.WorkManagerInitializer"
|
||||||
android:authorities="${applicationId}.workmanager-init"
|
android:authorities="${applicationId}.workmanager-init"
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
tools:node="remove" />
|
tools:node="remove" />
|
||||||
|
|
|
@ -19,6 +19,7 @@ package im.vector.matrix.android.api.session.crypto
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import im.vector.matrix.android.api.MatrixCallback
|
import im.vector.matrix.android.api.MatrixCallback
|
||||||
import im.vector.matrix.android.api.listeners.ProgressListener
|
import im.vector.matrix.android.api.listeners.ProgressListener
|
||||||
|
import im.vector.matrix.android.api.session.crypto.crosssigning.CrossSigningService
|
||||||
import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupService
|
import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupService
|
||||||
import im.vector.matrix.android.api.session.crypto.keyshare.RoomKeysRequestListener
|
import im.vector.matrix.android.api.session.crypto.keyshare.RoomKeysRequestListener
|
||||||
import im.vector.matrix.android.api.session.crypto.sas.SasVerificationService
|
import im.vector.matrix.android.api.session.crypto.sas.SasVerificationService
|
||||||
|
@ -48,6 +49,8 @@ interface CryptoService {
|
||||||
|
|
||||||
fun getSasVerificationService(): SasVerificationService
|
fun getSasVerificationService(): SasVerificationService
|
||||||
|
|
||||||
|
fun getCrossSigningService(): CrossSigningService
|
||||||
|
|
||||||
fun getKeysBackupService(): KeysBackupService
|
fun getKeysBackupService(): KeysBackupService
|
||||||
|
|
||||||
fun isRoomBlacklistUnverifiedDevices(roomId: String?): Boolean
|
fun isRoomBlacklistUnverifiedDevices(roomId: String?): Boolean
|
||||||
|
|
|
@ -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
|
@Binds
|
||||||
abstract fun bindUploadKeysTask(uploadKeysTask: DefaultUploadKeysTask): UploadKeysTask
|
abstract fun bindUploadKeysTask(uploadKeysTask: DefaultUploadKeysTask): UploadKeysTask
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
abstract fun bindUploadSigningKeysTask(uploadKeysTask: DefaultUploadSigningKeysTask): UploadSigningKeysTask
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
abstract fun bindUploadSignaturesTask(uploadSignaturesTask: DefaultUploadSignaturesTask): UploadSignaturesTask
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
abstract fun bindDownloadKeysForUsersTask(downloadKeysForUsersTask: DefaultDownloadKeysForUsers): DownloadKeysForUsersTask
|
abstract fun bindDownloadKeysForUsersTask(downloadKeysForUsersTask: DefaultDownloadKeysForUsers): DownloadKeysForUsersTask
|
||||||
|
|
||||||
|
|
|
@ -44,6 +44,7 @@ import im.vector.matrix.android.internal.crypto.actions.SetDeviceVerificationAct
|
||||||
import im.vector.matrix.android.internal.crypto.algorithms.IMXEncrypting
|
import im.vector.matrix.android.internal.crypto.algorithms.IMXEncrypting
|
||||||
import im.vector.matrix.android.internal.crypto.algorithms.megolm.MXMegolmEncryptionFactory
|
import im.vector.matrix.android.internal.crypto.algorithms.megolm.MXMegolmEncryptionFactory
|
||||||
import im.vector.matrix.android.internal.crypto.algorithms.olm.MXOlmEncryptionFactory
|
import im.vector.matrix.android.internal.crypto.algorithms.olm.MXOlmEncryptionFactory
|
||||||
|
import im.vector.matrix.android.internal.crypto.crosssigning.DefaultCrossSigningService
|
||||||
import im.vector.matrix.android.internal.crypto.keysbackup.KeysBackup
|
import im.vector.matrix.android.internal.crypto.keysbackup.KeysBackup
|
||||||
import im.vector.matrix.android.internal.crypto.model.ImportRoomKeysResult
|
import im.vector.matrix.android.internal.crypto.model.ImportRoomKeysResult
|
||||||
import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo
|
import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo
|
||||||
|
@ -113,6 +114,8 @@ internal class DefaultCryptoService @Inject constructor(
|
||||||
private val roomDecryptorProvider: RoomDecryptorProvider,
|
private val roomDecryptorProvider: RoomDecryptorProvider,
|
||||||
// The SAS verification service.
|
// The SAS verification service.
|
||||||
private val sasVerificationService: DefaultSasVerificationService,
|
private val sasVerificationService: DefaultSasVerificationService,
|
||||||
|
|
||||||
|
private val crossSigningService: DefaultCrossSigningService,
|
||||||
//
|
//
|
||||||
private val incomingRoomKeyRequestManager: IncomingRoomKeyRequestManager,
|
private val incomingRoomKeyRequestManager: IncomingRoomKeyRequestManager,
|
||||||
//
|
//
|
||||||
|
@ -317,6 +320,8 @@ internal class DefaultCryptoService @Inject constructor(
|
||||||
*/
|
*/
|
||||||
override fun getSasVerificationService() = sasVerificationService
|
override fun getSasVerificationService() = sasVerificationService
|
||||||
|
|
||||||
|
override fun getCrossSigningService() = crossSigningService
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A sync response has been received
|
* A sync response has been received
|
||||||
*
|
*
|
||||||
|
@ -364,7 +369,7 @@ internal class DefaultCryptoService @Inject constructor(
|
||||||
*/
|
*/
|
||||||
override fun getDeviceInfo(userId: String, deviceId: String?): MXDeviceInfo? {
|
override fun getDeviceInfo(userId: String, deviceId: String?): MXDeviceInfo? {
|
||||||
return if (userId.isNotEmpty() && !deviceId.isNullOrEmpty()) {
|
return if (userId.isNotEmpty() && !deviceId.isNullOrEmpty()) {
|
||||||
cryptoStore.getUserDevice(deviceId, userId)
|
cryptoStore.getUserDevice(userId, deviceId)
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
|
@ -289,7 +289,7 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
|
||||||
val mutableDevices = devices.toMutableMap()
|
val mutableDevices = devices.toMutableMap()
|
||||||
for ((deviceId, deviceInfo) in devices) {
|
for ((deviceId, deviceInfo) in devices) {
|
||||||
// Get the potential previously store device keys for this device
|
// Get the potential previously store device keys for this device
|
||||||
val previouslyStoredDeviceKeys = cryptoStore.getUserDevice(deviceId, userId)
|
val previouslyStoredDeviceKeys = cryptoStore.getUserDevice(userId, deviceId)
|
||||||
|
|
||||||
// in some race conditions (like unit tests)
|
// in some race conditions (like unit tests)
|
||||||
// the self device must be seen as verified
|
// the self device must be seen as verified
|
||||||
|
@ -315,6 +315,25 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
|
||||||
// Note that devices which aren't in the response will be removed from the stores
|
// Note that devices which aren't in the response will be removed from the stores
|
||||||
cryptoStore.storeUserDevices(userId, mutableDevices)
|
cryptoStore.storeUserDevices(userId, mutableDevices)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
//Handle cross signing keys update
|
||||||
|
val masterKey = response.masterKeys?.get(userId)?.also {
|
||||||
|
Timber.d("## CrossSigning : Got keys for $userId : MSK ${it.unpaddedBase64PublicKey}")
|
||||||
|
}
|
||||||
|
val selfSigningKey = response.selfSigningKeys?.get(userId)?.also {
|
||||||
|
Timber.d("## CrossSigning : Got keys for $userId : SSK ${it.unpaddedBase64PublicKey}")
|
||||||
|
}
|
||||||
|
val userSigningKey = response.userSigningKeys?.get(userId)?.also {
|
||||||
|
Timber.d("## CrossSigning : Got keys for $userId : USK ${it.unpaddedBase64PublicKey}")
|
||||||
|
}
|
||||||
|
cryptoStore.storeUserCrossSigningKeys(
|
||||||
|
userId,
|
||||||
|
masterKey,
|
||||||
|
selfSigningKey,
|
||||||
|
userSigningKey
|
||||||
|
)
|
||||||
}
|
}
|
||||||
return onKeysDownloadSucceed(filteredUsers, response.failures)
|
return onKeysDownloadSucceed(filteredUsers, response.failures)
|
||||||
}
|
}
|
||||||
|
|
|
@ -107,7 +107,7 @@ internal class IncomingRoomKeyRequestManager @Inject constructor(
|
||||||
cryptoStore.deleteIncomingRoomKeyRequest(request)
|
cryptoStore.deleteIncomingRoomKeyRequest(request)
|
||||||
}
|
}
|
||||||
// if the device is verified already, share the keys
|
// if the device is verified already, share the keys
|
||||||
val device = cryptoStore.getUserDevice(deviceId!!, userId)
|
val device = cryptoStore.getUserDevice(userId, deviceId!!)
|
||||||
if (device != null) {
|
if (device != null) {
|
||||||
if (device.isVerified) {
|
if (device.isVerified) {
|
||||||
Timber.v("## processReceivedRoomKeyRequests() : device is already verified: sharing keys")
|
Timber.v("## processReceivedRoomKeyRequests() : device is already verified: sharing keys")
|
||||||
|
|
|
@ -141,6 +141,7 @@ internal class MXOlmDevice @Inject constructor(
|
||||||
*/
|
*/
|
||||||
fun release() {
|
fun release() {
|
||||||
olmAccount?.releaseAccount()
|
olmAccount?.releaseAccount()
|
||||||
|
olmUtility?.releaseUtility()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -28,7 +28,7 @@ internal class SetDeviceVerificationAction @Inject constructor(
|
||||||
private val keysBackup: KeysBackup) {
|
private val keysBackup: KeysBackup) {
|
||||||
|
|
||||||
fun handle(verificationStatus: Int, deviceId: String, userId: String) {
|
fun handle(verificationStatus: Int, deviceId: String, userId: String) {
|
||||||
val device = cryptoStore.getUserDevice(deviceId, userId)
|
val device = cryptoStore.getUserDevice(userId, deviceId)
|
||||||
|
|
||||||
// Sanity check
|
// Sanity check
|
||||||
if (null == device) {
|
if (null == device) {
|
||||||
|
|
|
@ -297,7 +297,7 @@ internal class MXMegolmDecryption(private val userId: String,
|
||||||
runCatching { deviceListManager.downloadKeys(listOf(userId), false) }
|
runCatching { deviceListManager.downloadKeys(listOf(userId), false) }
|
||||||
.mapCatching {
|
.mapCatching {
|
||||||
val deviceId = request.deviceId
|
val deviceId = request.deviceId
|
||||||
val deviceInfo = cryptoStore.getUserDevice(deviceId ?: "", userId)
|
val deviceInfo = cryptoStore.getUserDevice(userId, deviceId ?: "")
|
||||||
if (deviceInfo == null) {
|
if (deviceInfo == null) {
|
||||||
throw RuntimeException()
|
throw RuntimeException()
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
*/
|
*/
|
||||||
package im.vector.matrix.android.internal.crypto.api
|
package im.vector.matrix.android.internal.crypto.api
|
||||||
|
|
||||||
|
import im.vector.matrix.android.internal.crypto.model.MXKeysObject
|
||||||
import im.vector.matrix.android.internal.crypto.model.rest.*
|
import im.vector.matrix.android.internal.crypto.model.rest.*
|
||||||
import im.vector.matrix.android.internal.network.NetworkConstants
|
import im.vector.matrix.android.internal.network.NetworkConstants
|
||||||
import retrofit2.Call
|
import retrofit2.Call
|
||||||
|
@ -65,6 +66,36 @@ internal interface CryptoApi {
|
||||||
@POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "keys/query")
|
@POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "keys/query")
|
||||||
fun downloadKeysForUsers(@Body params: KeysQueryBody): Call<KeysQueryResponse>
|
fun downloadKeysForUsers(@Body params: KeysQueryBody): Call<KeysQueryResponse>
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CrossSigning - Uploading signing keys
|
||||||
|
* Public keys for the cross-signing keys are uploaded to the servers using /keys/device_signing/upload.
|
||||||
|
* This endpoint requires UI Auth.
|
||||||
|
*/
|
||||||
|
@POST(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "keys/device_signing/upload")
|
||||||
|
fun uploadSigningKeys(@Body params: UploadSigningKeysBody): Call<KeysQueryResponse>
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CrossSigning - Uploading signatures
|
||||||
|
* Signatures of device keys can be up
|
||||||
|
* loaded using /keys/signatures/upload.
|
||||||
|
* For example, Alice signs one of her devices (HIJKLMN) (using her self-signing key),
|
||||||
|
* her own master key (using her HIJKLMN device), Bob's master key (using her user-signing key).
|
||||||
|
*
|
||||||
|
* The response contains a failures property, which is a map of user ID to device ID to failure reason, if any of the uploaded keys failed.
|
||||||
|
* The homeserver should verify that the signatures on the uploaded keys are valid.
|
||||||
|
* If a signature is not valid, the homeserver should set the corresponding entry in failures to a JSON object
|
||||||
|
* with the errcode property set to M_INVALID_SIGNATURE.
|
||||||
|
*
|
||||||
|
* After Alice uploads a signature for her own devices or master key,
|
||||||
|
* her signature will be included in the results of the /keys/query request when anyone requests her keys.
|
||||||
|
* However, signatures made for other users' keys, made by her user-signing key, will not be included.
|
||||||
|
*/
|
||||||
|
@POST(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "keys/signatures/upload")
|
||||||
|
fun uploadSignatures(@Body params: Map<String, @JvmSuppressWildcards Any>?): Call<SignatureUploadResponse>
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Claim one-time keys.
|
* Claim one-time keys.
|
||||||
* Doc: https://matrix.org/docs/spec/client_server/r0.4.0.html#post-matrix-client-r0-keys-claim
|
* Doc: https://matrix.org/docs/spec/client_server/r0.4.0.html#post-matrix-client-r0-keys-claim
|
||||||
|
|
|
@ -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) {
|
if (deviceId != null) {
|
||||||
val device = cryptoStore.getUserDevice(deviceId, userId)
|
val device = cryptoStore.getUserDevice(userId, deviceId)
|
||||||
var isSignatureValid = false
|
var isSignatureValid = false
|
||||||
|
|
||||||
if (device == null) {
|
if (device == null) {
|
||||||
|
|
|
@ -147,6 +147,14 @@ data class MXDeviceInfo(
|
||||||
return map
|
return map
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun addSignature(userId: String, signedWithNoPrefix: String, signature: String) = apply {
|
||||||
|
val updated = (signatures?.toMutableMap() ?: HashMap())
|
||||||
|
val userMap = updated[userId]?.toMutableMap()
|
||||||
|
?: HashMap<String, String>().also { updated[userId] = it }
|
||||||
|
userMap["ed25519:${signedWithNoPrefix}"] = signature
|
||||||
|
signatures = updated
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return a dictionary of the parameters
|
* @return a dictionary of the parameters
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -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)
|
@JsonClass(generateAdapter = true)
|
||||||
internal data class DeleteDeviceParams(
|
internal data class DeleteDeviceParams(
|
||||||
@Json(name = "auth")
|
@Json(name = "auth")
|
||||||
var deleteDeviceAuth: DeleteDeviceAuth? = null
|
var userPasswordAuth: UserPasswordAuth? = null
|
||||||
)
|
)
|
||||||
|
|
|
@ -18,12 +18,12 @@ package im.vector.matrix.android.internal.crypto.model.rest
|
||||||
|
|
||||||
import com.squareup.moshi.Json
|
import com.squareup.moshi.Json
|
||||||
import com.squareup.moshi.JsonClass
|
import com.squareup.moshi.JsonClass
|
||||||
import im.vector.matrix.android.api.util.JsonDict
|
import im.vector.matrix.android.internal.crypto.model.MXKeysObject
|
||||||
|
|
||||||
@JsonClass(generateAdapter = true)
|
@JsonClass(generateAdapter = true)
|
||||||
data class DeviceKeys(
|
data class DeviceKeys(
|
||||||
@Json(name = "user_id")
|
@Json(name = "user_id")
|
||||||
val userId: String,
|
override val userId: String,
|
||||||
|
|
||||||
@Json(name = "device_id")
|
@Json(name = "device_id")
|
||||||
val deviceId: String,
|
val deviceId: String,
|
||||||
|
@ -32,8 +32,8 @@ data class DeviceKeys(
|
||||||
val algorithms: List<String>,
|
val algorithms: List<String>,
|
||||||
|
|
||||||
@Json(name = "keys")
|
@Json(name = "keys")
|
||||||
val keys: Map<String, String>,
|
override val keys: Map<String, String>,
|
||||||
|
|
||||||
@Json(name = "signatures")
|
@Json(name = "signatures")
|
||||||
val signatures: JsonDict
|
override val signatures: Map<String, Map<String, String>>?
|
||||||
)
|
) : MXKeysObject
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright 2016 OpenMarket Ltd
|
|
||||||
* Copyright 2017 Vector Creations Ltd
|
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@ -23,6 +22,11 @@ import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This class represents the response to /keys/query request made by downloadKeysForUsers
|
* This class represents the response to /keys/query request made by downloadKeysForUsers
|
||||||
|
*
|
||||||
|
* After uploading cross-signing keys, they will be included under the /keys/query endpoint under the master_keys,
|
||||||
|
* self_signing_keys and user_signing_keys properties.
|
||||||
|
*
|
||||||
|
* The user_signing_keys property will only be included when a user requests their own keys.
|
||||||
*/
|
*/
|
||||||
@JsonClass(generateAdapter = true)
|
@JsonClass(generateAdapter = true)
|
||||||
data class KeysQueryResponse(
|
data class KeysQueryResponse(
|
||||||
|
@ -38,5 +42,16 @@ data class KeysQueryResponse(
|
||||||
* The failures sorted by homeservers. TODO Bad comment ?
|
* The failures sorted by homeservers. TODO Bad comment ?
|
||||||
* TODO Use MXUsersDevicesMap?
|
* TODO Use MXUsersDevicesMap?
|
||||||
*/
|
*/
|
||||||
var failures: Map<String, Map<String, Any>>? = null
|
var failures: Map<String, Map<String, Any>>? = null,
|
||||||
|
|
||||||
|
@Json(name = "master_keys")
|
||||||
|
var masterKeys: Map<String, CrossSigningKeyInfo?>? = null,
|
||||||
|
|
||||||
|
@Json(name = "self_signing_keys")
|
||||||
|
var selfSigningKeys: Map<String, CrossSigningKeyInfo?>? = null,
|
||||||
|
|
||||||
|
@Json(name = "user_signing_keys")
|
||||||
|
var userSigningKeys: Map<String, CrossSigningKeyInfo?>? = null
|
||||||
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -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 2016 OpenMarket Ltd
|
||||||
|
* Copyright 2020 New Vector Ltd
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@ -17,12 +18,13 @@ package im.vector.matrix.android.internal.crypto.model.rest
|
||||||
|
|
||||||
import com.squareup.moshi.Json
|
import com.squareup.moshi.Json
|
||||||
import com.squareup.moshi.JsonClass
|
import com.squareup.moshi.JsonClass
|
||||||
|
import im.vector.matrix.android.internal.auth.data.LoginFlowTypes
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This class provides the authentication data to delete a device
|
* This class provides the authentication data to delete a device
|
||||||
*/
|
*/
|
||||||
@JsonClass(generateAdapter = true)
|
@JsonClass(generateAdapter = true)
|
||||||
internal data class DeleteDeviceAuth(
|
data class UserPasswordAuth(
|
||||||
|
|
||||||
// device device session id
|
// device device session id
|
||||||
@Json(name = "session")
|
@Json(name = "session")
|
||||||
|
@ -30,7 +32,7 @@ internal data class DeleteDeviceAuth(
|
||||||
|
|
||||||
// registration information
|
// registration information
|
||||||
@Json(name = "type")
|
@Json(name = "type")
|
||||||
var type: String? = null,
|
var type: String? = LoginFlowTypes.PASSWORD,
|
||||||
|
|
||||||
@Json(name = "user")
|
@Json(name = "user")
|
||||||
var user: String? = null,
|
var user: String? = null,
|
|
@ -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
|
package im.vector.matrix.android.internal.crypto.store
|
||||||
|
|
||||||
|
import im.vector.matrix.android.api.session.crypto.crosssigning.MXCrossSigningInfo
|
||||||
import im.vector.matrix.android.internal.crypto.IncomingRoomKeyRequest
|
import im.vector.matrix.android.internal.crypto.IncomingRoomKeyRequest
|
||||||
import im.vector.matrix.android.internal.crypto.NewSessionListener
|
import im.vector.matrix.android.internal.crypto.NewSessionListener
|
||||||
import im.vector.matrix.android.internal.crypto.OutgoingRoomKeyRequest
|
import im.vector.matrix.android.internal.crypto.OutgoingRoomKeyRequest
|
||||||
import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo
|
import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo
|
||||||
import im.vector.matrix.android.internal.crypto.model.OlmInboundGroupSessionWrapper
|
import im.vector.matrix.android.internal.crypto.model.OlmInboundGroupSessionWrapper
|
||||||
import im.vector.matrix.android.internal.crypto.model.OlmSessionWrapper
|
import im.vector.matrix.android.internal.crypto.model.OlmSessionWrapper
|
||||||
|
import im.vector.matrix.android.internal.crypto.model.rest.CrossSigningKeyInfo
|
||||||
import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody
|
import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody
|
||||||
import im.vector.matrix.android.internal.crypto.store.db.model.KeysBackupDataEntity
|
import im.vector.matrix.android.internal.crypto.store.db.model.KeysBackupDataEntity
|
||||||
import org.matrix.olm.OlmAccount
|
import org.matrix.olm.OlmAccount
|
||||||
|
@ -163,7 +165,7 @@ internal interface IMXCryptoStore {
|
||||||
* @param userId the user's id.
|
* @param userId the user's id.
|
||||||
* @return the device
|
* @return the device
|
||||||
*/
|
*/
|
||||||
fun getUserDevice(deviceId: String, userId: String): MXDeviceInfo?
|
fun getUserDevice(userId: String, deviceId: String): MXDeviceInfo?
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve a device by its identity key.
|
* Retrieve a device by its identity key.
|
||||||
|
@ -181,6 +183,11 @@ internal interface IMXCryptoStore {
|
||||||
*/
|
*/
|
||||||
fun storeUserDevices(userId: String, devices: Map<String, MXDeviceInfo>?)
|
fun storeUserDevices(userId: String, devices: Map<String, MXDeviceInfo>?)
|
||||||
|
|
||||||
|
|
||||||
|
fun storeUserCrossSigningKeys(userId: String, masterKey: CrossSigningKeyInfo?,
|
||||||
|
selfSigningKey: CrossSigningKeyInfo?,
|
||||||
|
userSigningKey: CrossSigningKeyInfo?)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve the known devices for a user.
|
* Retrieve the known devices for a user.
|
||||||
*
|
*
|
||||||
|
@ -381,4 +388,24 @@ internal interface IMXCryptoStore {
|
||||||
fun addNewSessionListener(listener: NewSessionListener)
|
fun addNewSessionListener(listener: NewSessionListener)
|
||||||
|
|
||||||
fun removeSessionListener(listener: NewSessionListener)
|
fun removeSessionListener(listener: NewSessionListener)
|
||||||
|
|
||||||
|
|
||||||
|
//=============================================
|
||||||
|
// CROSS SIGNING
|
||||||
|
//=============================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the current crosssigning info
|
||||||
|
*/
|
||||||
|
fun getMyCrossSigningInfo() : MXCrossSigningInfo?
|
||||||
|
fun setMyCrossSigningInfo(info: MXCrossSigningInfo?)
|
||||||
|
|
||||||
|
|
||||||
|
fun getCrossSigningInfo(userId: String) : MXCrossSigningInfo?
|
||||||
|
fun setCrossSigningInfo(userId: String, info: MXCrossSigningInfo?)
|
||||||
|
|
||||||
|
|
||||||
|
fun getCrossSigningPrivateKeys() : PrivateKeysInfo?
|
||||||
|
|
||||||
|
fun setUserKeysAsTrusted(userId: String, trusted: Boolean = true)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
package im.vector.matrix.android.internal.crypto.store.db
|
||||||
|
|
||||||
import im.vector.matrix.android.api.auth.data.Credentials
|
import im.vector.matrix.android.api.auth.data.Credentials
|
||||||
|
import im.vector.matrix.android.api.session.crypto.crosssigning.MXCrossSigningInfo
|
||||||
import im.vector.matrix.android.internal.crypto.IncomingRoomKeyRequest
|
import im.vector.matrix.android.internal.crypto.IncomingRoomKeyRequest
|
||||||
import im.vector.matrix.android.internal.crypto.NewSessionListener
|
import im.vector.matrix.android.internal.crypto.NewSessionListener
|
||||||
import im.vector.matrix.android.internal.crypto.OutgoingRoomKeyRequest
|
import im.vector.matrix.android.internal.crypto.OutgoingRoomKeyRequest
|
||||||
import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo
|
import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo
|
||||||
import im.vector.matrix.android.internal.crypto.model.OlmInboundGroupSessionWrapper
|
import im.vector.matrix.android.internal.crypto.model.OlmInboundGroupSessionWrapper
|
||||||
import im.vector.matrix.android.internal.crypto.model.OlmSessionWrapper
|
import im.vector.matrix.android.internal.crypto.model.OlmSessionWrapper
|
||||||
|
import im.vector.matrix.android.internal.crypto.model.rest.CrossSigningKeyInfo
|
||||||
import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody
|
import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody
|
||||||
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
|
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
|
||||||
|
import im.vector.matrix.android.internal.crypto.store.PrivateKeysInfo
|
||||||
import im.vector.matrix.android.internal.crypto.store.db.model.*
|
import im.vector.matrix.android.internal.crypto.store.db.model.*
|
||||||
import im.vector.matrix.android.internal.crypto.store.db.query.delete
|
import im.vector.matrix.android.internal.crypto.store.db.query.delete
|
||||||
|
import im.vector.matrix.android.internal.crypto.store.db.query.get
|
||||||
import im.vector.matrix.android.internal.crypto.store.db.query.getById
|
import im.vector.matrix.android.internal.crypto.store.db.query.getById
|
||||||
import im.vector.matrix.android.internal.crypto.store.db.query.getOrCreate
|
import im.vector.matrix.android.internal.crypto.store.db.query.getOrCreate
|
||||||
import im.vector.matrix.android.internal.session.SessionScope
|
import im.vector.matrix.android.internal.session.SessionScope
|
||||||
import io.realm.Realm
|
import io.realm.Realm
|
||||||
import io.realm.RealmConfiguration
|
import io.realm.RealmConfiguration
|
||||||
|
import io.realm.RealmList
|
||||||
import io.realm.Sort
|
import io.realm.Sort
|
||||||
import io.realm.kotlin.where
|
import io.realm.kotlin.where
|
||||||
import org.matrix.olm.OlmAccount
|
import org.matrix.olm.OlmAccount
|
||||||
|
@ -187,7 +192,7 @@ internal class RealmCryptoStore(private val realmConfiguration: RealmConfigurati
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getUserDevice(deviceId: String, userId: String): MXDeviceInfo? {
|
override fun getUserDevice(userId: String, deviceId: String): MXDeviceInfo? {
|
||||||
return doRealmQueryAndCopy(realmConfiguration) {
|
return doRealmQueryAndCopy(realmConfiguration) {
|
||||||
it.where<DeviceInfoEntity>()
|
it.where<DeviceInfoEntity>()
|
||||||
.equalTo(DeviceInfoEntityFields.PRIMARY_KEY, DeviceInfoEntity.createPrimaryKey(userId, deviceId))
|
.equalTo(DeviceInfoEntityFields.PRIMARY_KEY, DeviceInfoEntity.createPrimaryKey(userId, deviceId))
|
||||||
|
@ -231,6 +236,73 @@ internal class RealmCryptoStore(private val realmConfiguration: RealmConfigurati
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun storeUserCrossSigningKeys(userId: String,
|
||||||
|
masterKey: CrossSigningKeyInfo?,
|
||||||
|
selfSigningKey: CrossSigningKeyInfo?,
|
||||||
|
userSigningKey: CrossSigningKeyInfo?) {
|
||||||
|
doRealmTransaction(realmConfiguration) { realm ->
|
||||||
|
UserEntity.getOrCreate(realm, userId)
|
||||||
|
.let { userEntity ->
|
||||||
|
if (masterKey == null || selfSigningKey == null) {
|
||||||
|
// The user has disabled cross signing?
|
||||||
|
userEntity.crossSigningInfoEntity?.deleteFromRealm()
|
||||||
|
userEntity.crossSigningInfoEntity = null
|
||||||
|
} else {
|
||||||
|
CrossSigningInfoEntity.getOrCreate(realm, userId).let { signingInfo ->
|
||||||
|
// What should we do if we detect a change of the keys?
|
||||||
|
|
||||||
|
val existingMaster = signingInfo.getMasterKey()
|
||||||
|
if (existingMaster != null && existingMaster.publicKeyBase64 == masterKey.unpaddedBase64PublicKey) {
|
||||||
|
//update signatures?
|
||||||
|
existingMaster.putSignatures(masterKey.signatures)
|
||||||
|
existingMaster.usages = masterKey.usages?.toTypedArray()?.let { RealmList(*it) }
|
||||||
|
?: RealmList()
|
||||||
|
} else {
|
||||||
|
val keyEntity = realm.createObject(KeyInfoEntity::class.java).apply {
|
||||||
|
this.publicKeyBase64 = masterKey.unpaddedBase64PublicKey
|
||||||
|
this.usages = masterKey.usages?.toTypedArray()?.let { RealmList(*it) }
|
||||||
|
?: RealmList()
|
||||||
|
this.putSignatures(masterKey.signatures)
|
||||||
|
}
|
||||||
|
signingInfo.setMasterKey(keyEntity)
|
||||||
|
}
|
||||||
|
|
||||||
|
val existingSelfSigned = signingInfo.getSelfSignedKey()
|
||||||
|
if (existingSelfSigned != null && existingSelfSigned.publicKeyBase64 == selfSigningKey.unpaddedBase64PublicKey) {
|
||||||
|
//update signatures?
|
||||||
|
existingSelfSigned.putSignatures(selfSigningKey.signatures)
|
||||||
|
existingSelfSigned.usages = selfSigningKey.usages?.toTypedArray()?.let { RealmList(*it) }
|
||||||
|
?: RealmList()
|
||||||
|
} else {
|
||||||
|
val keyEntity = realm.createObject(KeyInfoEntity::class.java).apply {
|
||||||
|
this.publicKeyBase64 = selfSigningKey.unpaddedBase64PublicKey
|
||||||
|
this.usages = selfSigningKey.usages?.toTypedArray()?.let { RealmList(*it) }
|
||||||
|
?: RealmList()
|
||||||
|
this.putSignatures(selfSigningKey.signatures)
|
||||||
|
}
|
||||||
|
signingInfo.setSelfSignedKey(keyEntity)
|
||||||
|
}
|
||||||
|
|
||||||
|
userEntity.crossSigningInfoEntity = signingInfo
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getCrossSigningPrivateKeys(): PrivateKeysInfo? {
|
||||||
|
return doRealmQueryAndCopy(realmConfiguration) { realm ->
|
||||||
|
realm.where<CryptoMetadataEntity>().findFirst()
|
||||||
|
}?.let {
|
||||||
|
PrivateKeysInfo(
|
||||||
|
master = it.xSignMasterPrivateKey,
|
||||||
|
selfSigned = it.xSignSelfSignedPrivateKey,
|
||||||
|
user = it.xSignUserPrivateKey
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun getUserDevices(userId: String): Map<String, MXDeviceInfo>? {
|
override fun getUserDevices(userId: String): Map<String, MXDeviceInfo>? {
|
||||||
return doRealmQueryAndCopy(realmConfiguration) {
|
return doRealmQueryAndCopy(realmConfiguration) {
|
||||||
it.where<UserEntity>()
|
it.where<UserEntity>()
|
||||||
|
@ -731,4 +803,82 @@ internal class RealmCryptoStore(private val realmConfiguration: RealmConfigurati
|
||||||
}
|
}
|
||||||
.toMutableList()
|
.toMutableList()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ==========================================================================================
|
||||||
|
* Cross Signing
|
||||||
|
* ========================================================================================== */
|
||||||
|
override fun getMyCrossSigningInfo(): MXCrossSigningInfo? {
|
||||||
|
return doRealmQueryAndCopy(realmConfiguration) {
|
||||||
|
it.where<CryptoMetadataEntity>().findFirst()
|
||||||
|
}?.userId?.let {
|
||||||
|
getCrossSigningInfo(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setMyCrossSigningInfo(info: MXCrossSigningInfo?) {
|
||||||
|
doRealmQueryAndCopy(realmConfiguration) {
|
||||||
|
it.where<CryptoMetadataEntity>().findFirst()
|
||||||
|
}?.userId?.let {
|
||||||
|
setCrossSigningInfo(it, info)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setUserKeysAsTrusted(userId: String, trusted: Boolean) {
|
||||||
|
doRealmTransaction(realmConfiguration) { realm ->
|
||||||
|
realm.where(CrossSigningInfoEntity::class.java)
|
||||||
|
.equalTo(CrossSigningInfoEntityFields.USER_ID, userId)
|
||||||
|
.findFirst()
|
||||||
|
?.isTrusted = trusted
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getCrossSigningInfo(userId: String): MXCrossSigningInfo? {
|
||||||
|
return doRealmQueryAndCopy(realmConfiguration) { realm ->
|
||||||
|
realm.where(CrossSigningInfoEntity::class.java)
|
||||||
|
.equalTo(CrossSigningInfoEntityFields.USER_ID, userId)
|
||||||
|
.findFirst()
|
||||||
|
}?.let { xsignInfo ->
|
||||||
|
MXCrossSigningInfo(
|
||||||
|
userId = userId,
|
||||||
|
crossSigningKeys = xsignInfo.crossSigningKeys.mapNotNull {
|
||||||
|
val pubKey = it.publicKeyBase64 ?: return@mapNotNull null
|
||||||
|
CrossSigningKeyInfo(
|
||||||
|
userId = userId,
|
||||||
|
keys = mapOf("ed25519:$pubKey" to pubKey),
|
||||||
|
usages = it.usages.map { it },
|
||||||
|
signatures = it.getSignatures()
|
||||||
|
|
||||||
|
)
|
||||||
|
},
|
||||||
|
isTrusted = xsignInfo.isTrusted
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setCrossSigningInfo(userId: String, info: MXCrossSigningInfo?) {
|
||||||
|
doRealmTransaction(realmConfiguration) { realm ->
|
||||||
|
|
||||||
|
var existing = CrossSigningInfoEntity.get(realm, userId)
|
||||||
|
if (info == null) {
|
||||||
|
// Delete known if needed
|
||||||
|
existing?.deleteFromRealm()
|
||||||
|
// TODO notify, we might need to untrust things?
|
||||||
|
} else {
|
||||||
|
// Just override existing, caller should check and untrust id needed
|
||||||
|
existing = CrossSigningInfoEntity.getOrCreate(realm, userId)
|
||||||
|
val xkeys = RealmList<KeyInfoEntity>()
|
||||||
|
info.crossSigningKeys.forEach { info ->
|
||||||
|
xkeys.add(
|
||||||
|
realm.createObject(KeyInfoEntity::class.java).also { keyInfoEntity ->
|
||||||
|
keyInfoEntity.publicKeyBase64 = info.unpaddedBase64PublicKey
|
||||||
|
keyInfoEntity.usages = info.usages?.let { RealmList(*it.toTypedArray()) }
|
||||||
|
?: RealmList()
|
||||||
|
keyInfoEntity.putSignatures(info.signatures)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
existing.crossSigningKeys = xkeys
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,15 +16,50 @@
|
||||||
|
|
||||||
package im.vector.matrix.android.internal.crypto.store.db
|
package im.vector.matrix.android.internal.crypto.store.db
|
||||||
|
|
||||||
|
import im.vector.matrix.android.internal.crypto.store.db.model.*
|
||||||
|
import im.vector.matrix.android.internal.crypto.store.db.model.KeyInfoEntity
|
||||||
import io.realm.DynamicRealm
|
import io.realm.DynamicRealm
|
||||||
import io.realm.RealmMigration
|
import io.realm.RealmMigration
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
internal object RealmCryptoStoreMigration : RealmMigration {
|
internal object RealmCryptoStoreMigration : RealmMigration {
|
||||||
|
|
||||||
const val CRYPTO_STORE_SCHEMA_VERSION = 0L
|
// Version 1L added Cross Signing info persistence
|
||||||
|
const val CRYPTO_STORE_SCHEMA_VERSION = 1L
|
||||||
|
|
||||||
override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) {
|
override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) {
|
||||||
Timber.v("Migrating Realm Crypto from $oldVersion to $newVersion")
|
Timber.v("Migrating Realm Crypto from $oldVersion to $newVersion")
|
||||||
|
|
||||||
|
if (oldVersion <= 0) {
|
||||||
|
Timber.d("Step 0 -> 1")
|
||||||
|
Timber.d("Create KeyInfoEntity")
|
||||||
|
|
||||||
|
val keyInfoEntitySchema = realm.schema.create("KeyInfoEntity")
|
||||||
|
.addField(KeyInfoEntityFields.PUBLIC_KEY_BASE64, String::class.java)
|
||||||
|
.addField(KeyInfoEntityFields.SIGNATURES, String::class.java)
|
||||||
|
.addRealmListField(KeyInfoEntityFields.USAGES.`$`, String::class.java)
|
||||||
|
|
||||||
|
|
||||||
|
Timber.d("Create CrossSigningInfoEntity")
|
||||||
|
|
||||||
|
val crossSigningInfoSchema = realm.schema.create("CrossSigningInfoEntity")
|
||||||
|
.addField(CrossSigningInfoEntityFields.USER_ID, String::class.java)
|
||||||
|
.addField(CrossSigningInfoEntityFields.IS_TRUSTED, Boolean::class.java)
|
||||||
|
.addPrimaryKey(CrossSigningInfoEntityFields.USER_ID)
|
||||||
|
.addRealmListField(CrossSigningInfoEntityFields.CROSS_SIGNING_KEYS.`$`, keyInfoEntitySchema)
|
||||||
|
|
||||||
|
|
||||||
|
Timber.d("Updating UserEntity table")
|
||||||
|
realm.schema.get("UserEntity")
|
||||||
|
?.addRealmObjectField(UserEntityFields.CROSS_SIGNING_INFO_ENTITY.`$`, crossSigningInfoSchema)
|
||||||
|
|
||||||
|
|
||||||
|
Timber.d("Updating CryptoMetadataEntity table")
|
||||||
|
realm.schema.get("CryptoMetadataEntity")
|
||||||
|
?.addField(CryptoMetadataEntityFields.X_SIGN_MASTER_PRIVATE_KEY, String::class.java)
|
||||||
|
?.addField(CryptoMetadataEntityFields.X_SIGN_USER_PRIVATE_KEY, String::class.java)
|
||||||
|
?.addField(CryptoMetadataEntityFields.X_SIGN_SELF_SIGNED_PRIVATE_KEY, String::class.java)
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,6 +32,8 @@ import io.realm.annotations.RealmModule
|
||||||
OlmInboundGroupSessionEntity::class,
|
OlmInboundGroupSessionEntity::class,
|
||||||
OlmSessionEntity::class,
|
OlmSessionEntity::class,
|
||||||
OutgoingRoomKeyRequestEntity::class,
|
OutgoingRoomKeyRequestEntity::class,
|
||||||
UserEntity::class
|
UserEntity::class,
|
||||||
|
KeyInfoEntity::class,
|
||||||
|
CrossSigningInfoEntity::class
|
||||||
])
|
])
|
||||||
internal class RealmCryptoStoreModule
|
internal class RealmCryptoStoreModule
|
||||||
|
|
|
@ -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.deserializeFromRealm
|
||||||
import im.vector.matrix.android.internal.crypto.store.db.serializeForRealm
|
import im.vector.matrix.android.internal.crypto.store.db.serializeForRealm
|
||||||
|
import io.realm.Realm
|
||||||
import io.realm.RealmObject
|
import io.realm.RealmObject
|
||||||
import io.realm.annotations.PrimaryKey
|
import io.realm.annotations.PrimaryKey
|
||||||
import org.matrix.olm.OlmAccount
|
import org.matrix.olm.OlmAccount
|
||||||
|
@ -34,7 +35,13 @@ internal open class CryptoMetadataEntity(
|
||||||
// Settings for blacklisting unverified devices.
|
// Settings for blacklisting unverified devices.
|
||||||
var globalBlacklistUnverifiedDevices: Boolean = false,
|
var globalBlacklistUnverifiedDevices: Boolean = false,
|
||||||
// The keys backup version currently used. Null means no backup.
|
// The keys backup version currently used. Null means no backup.
|
||||||
var backupVersion: String? = null
|
var backupVersion: String? = null,
|
||||||
|
|
||||||
|
var xSignMasterPrivateKey: String? = null,
|
||||||
|
var xSignUserPrivateKey: String? = null,
|
||||||
|
var xSignSelfSignedPrivateKey: String? = null
|
||||||
|
|
||||||
|
// var crossSigningInfoEntity: CrossSigningInfoEntity? = null
|
||||||
) : RealmObject() {
|
) : RealmObject() {
|
||||||
|
|
||||||
// Deserialize data
|
// Deserialize data
|
||||||
|
|
|
@ -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,8 +20,10 @@ import io.realm.RealmList
|
||||||
import io.realm.RealmObject
|
import io.realm.RealmObject
|
||||||
import io.realm.annotations.PrimaryKey
|
import io.realm.annotations.PrimaryKey
|
||||||
|
|
||||||
internal open class UserEntity(@PrimaryKey var userId: String? = null,
|
internal open class UserEntity(
|
||||||
|
@PrimaryKey var userId: String? = null,
|
||||||
var devices: RealmList<DeviceInfoEntity> = RealmList(),
|
var devices: RealmList<DeviceInfoEntity> = RealmList(),
|
||||||
|
var crossSigningInfoEntity: CrossSigningInfoEntity? = null,
|
||||||
var deviceTrackingStatus: Int = 0)
|
var deviceTrackingStatus: Int = 0)
|
||||||
: RealmObject() {
|
: RealmObject() {
|
||||||
|
|
||||||
|
|
|
@ -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.auth.data.LoginFlowTypes
|
||||||
import im.vector.matrix.android.internal.crypto.api.CryptoApi
|
import im.vector.matrix.android.internal.crypto.api.CryptoApi
|
||||||
import im.vector.matrix.android.internal.crypto.model.rest.DeleteDeviceAuth
|
import im.vector.matrix.android.internal.crypto.model.rest.UserPasswordAuth
|
||||||
import im.vector.matrix.android.internal.crypto.model.rest.DeleteDeviceParams
|
import im.vector.matrix.android.internal.crypto.model.rest.DeleteDeviceParams
|
||||||
import im.vector.matrix.android.internal.di.UserId
|
import im.vector.matrix.android.internal.di.UserId
|
||||||
import im.vector.matrix.android.internal.network.executeRequest
|
import im.vector.matrix.android.internal.network.executeRequest
|
||||||
|
@ -44,7 +44,7 @@ internal class DefaultDeleteDeviceWithUserPasswordTask @Inject constructor(
|
||||||
return executeRequest(eventBus) {
|
return executeRequest(eventBus) {
|
||||||
apiCall = cryptoApi.deleteDevice(params.deviceId, DeleteDeviceParams()
|
apiCall = cryptoApi.deleteDevice(params.deviceId, DeleteDeviceParams()
|
||||||
.apply {
|
.apply {
|
||||||
deleteDeviceAuth = DeleteDeviceAuth()
|
userPasswordAuth = UserPasswordAuth()
|
||||||
.apply {
|
.apply {
|
||||||
type = LoginFlowTypes.PASSWORD
|
type = LoginFlowTypes.PASSWORD
|
||||||
session = params.authSession
|
session = params.authSession
|
||||||
|
|
|
@ -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.
|
// 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) {
|
if (mxDeviceInfo?.fingerprint() == null) {
|
||||||
Timber.e("## SAS Failed to find device key ")
|
Timber.e("## SAS Failed to find device key ")
|
||||||
|
|
|
@ -21,6 +21,7 @@ import android.os.Looper
|
||||||
import dagger.Lazy
|
import dagger.Lazy
|
||||||
import im.vector.matrix.android.api.MatrixCallback
|
import im.vector.matrix.android.api.MatrixCallback
|
||||||
import im.vector.matrix.android.api.auth.data.Credentials
|
import im.vector.matrix.android.api.auth.data.Credentials
|
||||||
|
import im.vector.matrix.android.api.auth.data.sessionId
|
||||||
import im.vector.matrix.android.api.session.crypto.CryptoService
|
import im.vector.matrix.android.api.session.crypto.CryptoService
|
||||||
import im.vector.matrix.android.api.session.crypto.sas.*
|
import im.vector.matrix.android.api.session.crypto.sas.*
|
||||||
import im.vector.matrix.android.api.session.events.model.Event
|
import im.vector.matrix.android.api.session.events.model.Event
|
||||||
|
|
|
@ -20,6 +20,7 @@ import android.content.Context
|
||||||
import androidx.work.CoroutineWorker
|
import androidx.work.CoroutineWorker
|
||||||
import androidx.work.WorkerParameters
|
import androidx.work.WorkerParameters
|
||||||
import com.squareup.moshi.JsonClass
|
import com.squareup.moshi.JsonClass
|
||||||
|
import im.vector.matrix.android.api.MatrixCallback
|
||||||
import im.vector.matrix.android.api.failure.Failure
|
import im.vector.matrix.android.api.failure.Failure
|
||||||
import im.vector.matrix.android.api.session.crypto.CryptoService
|
import im.vector.matrix.android.api.session.crypto.CryptoService
|
||||||
import im.vector.matrix.android.api.session.events.model.Event
|
import im.vector.matrix.android.api.session.events.model.Event
|
||||||
|
@ -75,6 +76,10 @@ internal class EncryptEventWorker(context: Context, params: WorkerParameters)
|
||||||
localMutableContent.remove(it)
|
localMutableContent.remove(it)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
crypto.downloadKeys(listOf("@testxsigningvfe:matrix.org"), true, object : MatrixCallback<Any> {
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
var error: Throwable? = null
|
var error: Throwable? = null
|
||||||
var result: MXEncryptEventContentResult? = null
|
var result: MXEncryptEventContentResult? = null
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -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.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
|
import android.text.InputType
|
||||||
|
import android.widget.EditText
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
import androidx.core.app.Person
|
import androidx.core.app.Person
|
||||||
import butterknife.OnClick
|
import butterknife.OnClick
|
||||||
|
import im.vector.matrix.android.api.MatrixCallback
|
||||||
|
import im.vector.matrix.android.api.failure.Failure
|
||||||
|
import im.vector.matrix.android.internal.auth.data.LoginFlowTypes
|
||||||
|
import im.vector.matrix.android.internal.auth.registration.RegistrationFlowResponse
|
||||||
|
import im.vector.matrix.android.internal.crypto.model.rest.UserPasswordAuth
|
||||||
|
import im.vector.matrix.android.internal.di.MoshiProvider
|
||||||
import im.vector.riotx.R
|
import im.vector.riotx.R
|
||||||
|
import im.vector.riotx.core.di.ActiveSessionHolder
|
||||||
|
import im.vector.riotx.core.di.ScreenComponent
|
||||||
import im.vector.riotx.core.platform.VectorBaseActivity
|
import im.vector.riotx.core.platform.VectorBaseActivity
|
||||||
import im.vector.riotx.features.debug.sas.DebugSasEmojiActivity
|
import im.vector.riotx.features.debug.sas.DebugSasEmojiActivity
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
class DebugMenuActivity : VectorBaseActivity() {
|
class DebugMenuActivity : VectorBaseActivity() {
|
||||||
|
|
||||||
override fun getLayoutRes() = R.layout.activity_debug_menu
|
override fun getLayoutRes() = R.layout.activity_debug_menu
|
||||||
|
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var activeSessionHolder: ActiveSessionHolder
|
||||||
|
|
||||||
|
override fun injectWith(injector: ScreenComponent) {
|
||||||
|
injector.inject(this)
|
||||||
|
}
|
||||||
|
|
||||||
@OnClick(R.id.debug_test_text_view_link)
|
@OnClick(R.id.debug_test_text_view_link)
|
||||||
fun testTextViewLink() {
|
fun testTextViewLink() {
|
||||||
startActivity(Intent(this, TestLinkifyActivity::class.java))
|
startActivity(Intent(this, TestLinkifyActivity::class.java))
|
||||||
|
@ -140,4 +160,60 @@ class DebugMenuActivity : VectorBaseActivity() {
|
||||||
fun testCrash() {
|
fun testCrash() {
|
||||||
throw RuntimeException("Application crashed from user demand")
|
throw RuntimeException("Application crashed from user demand")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OnClick(R.id.debug_initialise_xsigning)
|
||||||
|
fun testXSigning() {
|
||||||
|
activeSessionHolder.getActiveSession().getCrossSigningService().initializeCrossSigning(null, object : MatrixCallback<Unit> {
|
||||||
|
|
||||||
|
override fun onFailure(failure: Throwable) {
|
||||||
|
if (failure is Failure.OtherServerError
|
||||||
|
&& failure.httpCode == 401
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
MoshiProvider.providesMoshi()
|
||||||
|
.adapter(RegistrationFlowResponse::class.java)
|
||||||
|
.fromJson(failure.errorBody)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
null
|
||||||
|
}?.let {
|
||||||
|
// Retry with authentication
|
||||||
|
if (it.flows?.any { it.stages?.contains(LoginFlowTypes.PASSWORD) == true } == true) {
|
||||||
|
// Ask for password
|
||||||
|
val inflater = this@DebugMenuActivity.layoutInflater
|
||||||
|
val layout = inflater.inflate(R.layout.dialog_base_edit_text, null)
|
||||||
|
|
||||||
|
val input = layout.findViewById<EditText>(R.id.edit_text).also {
|
||||||
|
it.inputType = InputType.TYPE_TEXT_VARIATION_PASSWORD
|
||||||
|
}
|
||||||
|
|
||||||
|
val activeSession = activeSessionHolder.getActiveSession()
|
||||||
|
AlertDialog.Builder(this@DebugMenuActivity)
|
||||||
|
.setTitle("Confirm password")
|
||||||
|
.setView(layout)
|
||||||
|
.setPositiveButton(R.string.ok) { _, _ ->
|
||||||
|
val pass = input.text.toString()
|
||||||
|
|
||||||
|
activeSession.getCrossSigningService().initializeCrossSigning(
|
||||||
|
UserPasswordAuth(
|
||||||
|
session = it.session,
|
||||||
|
user = activeSession.myUserId,
|
||||||
|
password = pass
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.setNegativeButton(R.string.cancel, null)
|
||||||
|
.show()
|
||||||
|
} else {
|
||||||
|
// can't do this from here
|
||||||
|
AlertDialog.Builder(this@DebugMenuActivity)
|
||||||
|
.setTitle(R.string.dialog_title_error)
|
||||||
|
.setMessage("You cannot do that from mobile")
|
||||||
|
.setPositiveButton(R.string.ok, null)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -61,6 +61,13 @@
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="Crash the app" />
|
android:text="Crash the app" />
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/debug_initialise_xsigning"
|
||||||
|
style="@style/VectorButtonStyle"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Initialize XSigning" />
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
|
|
|
@ -26,6 +26,7 @@ import im.vector.riotx.core.preference.UserAvatarPreference
|
||||||
import im.vector.riotx.features.MainActivity
|
import im.vector.riotx.features.MainActivity
|
||||||
import im.vector.riotx.features.crypto.keysbackup.settings.KeysBackupManageActivity
|
import im.vector.riotx.features.crypto.keysbackup.settings.KeysBackupManageActivity
|
||||||
import im.vector.riotx.features.crypto.verification.VerificationBottomSheet
|
import im.vector.riotx.features.crypto.verification.VerificationBottomSheet
|
||||||
|
import im.vector.riotx.features.debug.DebugMenuActivity
|
||||||
import im.vector.riotx.features.home.HomeActivity
|
import im.vector.riotx.features.home.HomeActivity
|
||||||
import im.vector.riotx.features.home.HomeModule
|
import im.vector.riotx.features.home.HomeModule
|
||||||
import im.vector.riotx.features.createdirect.CreateDirectRoomActivity
|
import im.vector.riotx.features.createdirect.CreateDirectRoomActivity
|
||||||
|
@ -139,6 +140,8 @@ interface ScreenComponent {
|
||||||
|
|
||||||
fun inject(permalinkHandlerActivity: PermalinkHandlerActivity)
|
fun inject(permalinkHandlerActivity: PermalinkHandlerActivity)
|
||||||
|
|
||||||
|
fun inject(activity: DebugMenuActivity)
|
||||||
|
|
||||||
@Component.Factory
|
@Component.Factory
|
||||||
interface Factory {
|
interface Factory {
|
||||||
fun create(vectorComponent: VectorComponent,
|
fun create(vectorComponent: VectorComponent,
|
||||||
|
|
|
@ -50,6 +50,7 @@ import im.vector.matrix.android.internal.crypto.attachments.toElementToDecrypt
|
||||||
import im.vector.matrix.android.internal.crypto.model.event.EncryptedEventContent
|
import im.vector.matrix.android.internal.crypto.model.event.EncryptedEventContent
|
||||||
import im.vector.matrix.rx.rx
|
import im.vector.matrix.rx.rx
|
||||||
import im.vector.matrix.rx.unwrap
|
import im.vector.matrix.rx.unwrap
|
||||||
|
import im.vector.riotx.BuildConfig
|
||||||
import im.vector.riotx.R
|
import im.vector.riotx.R
|
||||||
import im.vector.riotx.core.extensions.postLiveEvent
|
import im.vector.riotx.core.extensions.postLiveEvent
|
||||||
import im.vector.riotx.core.platform.VectorViewModel
|
import im.vector.riotx.core.platform.VectorViewModel
|
||||||
|
|
Loading…
Reference in New Issue