mirror of
https://github.com/SchildiChat/SchildiChat-android.git
synced 2024-11-23 01:45:52 +03:00
commit
1d84ccd64a
185 changed files with 7904 additions and 1378 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -15,3 +15,5 @@
|
|||
ktlint
|
||||
.idea/copyright/New_vector.xml
|
||||
.idea/copyright/profiles_settings.xml
|
||||
|
||||
.idea/copyright/New_Vector_Ltd.xml
|
||||
|
|
|
@ -18,6 +18,7 @@ package im.vector.matrix.rx
|
|||
|
||||
import androidx.paging.PagedList
|
||||
import im.vector.matrix.android.api.session.Session
|
||||
import im.vector.matrix.android.api.session.crypto.crosssigning.MXCrossSigningInfo
|
||||
import im.vector.matrix.android.api.session.group.GroupSummaryQueryParams
|
||||
import im.vector.matrix.android.api.session.group.model.GroupSummary
|
||||
import im.vector.matrix.android.api.session.pushers.Pusher
|
||||
|
@ -29,6 +30,7 @@ import im.vector.matrix.android.api.session.user.model.User
|
|||
import im.vector.matrix.android.api.util.JsonDict
|
||||
import im.vector.matrix.android.api.util.Optional
|
||||
import im.vector.matrix.android.api.util.toOptional
|
||||
import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
|
||||
import io.reactivex.Observable
|
||||
import io.reactivex.Single
|
||||
|
||||
|
@ -98,6 +100,15 @@ class RxSession(private val session: Session) {
|
|||
fun getProfileInfo(userId: String): Single<JsonDict> = singleBuilder {
|
||||
session.getProfile(userId, it)
|
||||
}
|
||||
|
||||
fun liveUserCryptoDevices(userId: String): Observable<List<CryptoDeviceInfo>> {
|
||||
return session.getLiveCryptoDeviceInfo(userId).asObservable()
|
||||
}
|
||||
|
||||
fun liveCrossSigningInfo(userId: String): Observable<Optional<MXCrossSigningInfo>> {
|
||||
return session.getCrossSigningService().getLiveCrossSigningKeys(userId).asObservable()
|
||||
.startWith(session.getCrossSigningService().getUserCrossSigningKeys(userId).toOptional())
|
||||
}
|
||||
}
|
||||
|
||||
fun Session.rx(): RxSession {
|
||||
|
|
|
@ -21,6 +21,7 @@ import androidx.test.core.app.ApplicationProvider
|
|||
import java.io.File
|
||||
|
||||
interface InstrumentedTest {
|
||||
|
||||
fun context(): Context {
|
||||
return ApplicationProvider.getApplicationContext()
|
||||
}
|
||||
|
|
|
@ -19,6 +19,7 @@ package im.vector.matrix.android.common
|
|||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import androidx.lifecycle.Observer
|
||||
import im.vector.matrix.android.api.Matrix
|
||||
import im.vector.matrix.android.api.MatrixCallback
|
||||
import im.vector.matrix.android.api.MatrixConfiguration
|
||||
|
@ -31,6 +32,11 @@ import im.vector.matrix.android.api.session.room.Room
|
|||
import im.vector.matrix.android.api.session.room.timeline.Timeline
|
||||
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
||||
import im.vector.matrix.android.api.session.room.timeline.TimelineSettings
|
||||
import im.vector.matrix.android.api.session.sync.SyncState
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.junit.Assert.*
|
||||
import java.util.*
|
||||
import java.util.concurrent.CountDownLatch
|
||||
|
@ -73,23 +79,25 @@ class CommonTestHelper(context: Context) {
|
|||
* @param session the session to sync
|
||||
*/
|
||||
fun syncSession(session: Session) {
|
||||
// val lock = CountDownLatch(1)
|
||||
|
||||
// val observer = androidx.lifecycle.Observer<SyncState> { syncState ->
|
||||
// if (syncState is SyncState.Idle) {
|
||||
// lock.countDown()
|
||||
// }
|
||||
// }
|
||||
|
||||
// TODO observe?
|
||||
// while (session.syncState().value !is SyncState.Idle) {
|
||||
// sleep(100)
|
||||
// }
|
||||
val lock = CountDownLatch(1)
|
||||
|
||||
session.open()
|
||||
session.startSync(true)
|
||||
// await(lock)
|
||||
// session.syncState().removeObserver(observer)
|
||||
|
||||
val syncLiveData = runBlocking(Dispatchers.Main) {
|
||||
session.getSyncStateLive()
|
||||
}
|
||||
val syncObserver = object : Observer<SyncState> {
|
||||
override fun onChanged(t: SyncState?) {
|
||||
if (session.hasAlreadySynced()) {
|
||||
lock.countDown()
|
||||
syncLiveData.removeObserver(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
GlobalScope.launch(Dispatchers.Main) { syncLiveData.observeForever(syncObserver) }
|
||||
|
||||
await(lock)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -17,11 +17,15 @@
|
|||
package im.vector.matrix.android.common
|
||||
|
||||
import android.os.SystemClock
|
||||
import androidx.lifecycle.Observer
|
||||
import im.vector.matrix.android.api.session.Session
|
||||
import im.vector.matrix.android.api.session.events.model.Event
|
||||
import im.vector.matrix.android.api.session.events.model.EventType
|
||||
import im.vector.matrix.android.api.session.events.model.toContent
|
||||
import im.vector.matrix.android.api.session.room.model.Membership
|
||||
import im.vector.matrix.android.api.session.room.model.RoomSummary
|
||||
import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams
|
||||
import im.vector.matrix.android.api.session.room.roomSummaryQueryParams
|
||||
import im.vector.matrix.android.api.session.room.timeline.Timeline
|
||||
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
||||
import im.vector.matrix.android.api.session.room.timeline.TimelineSettings
|
||||
|
@ -29,6 +33,10 @@ import im.vector.matrix.android.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
|
|||
import im.vector.matrix.android.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM_BACKUP
|
||||
import im.vector.matrix.android.internal.crypto.keysbackup.model.MegolmBackupAuthData
|
||||
import im.vector.matrix.android.internal.crypto.keysbackup.model.MegolmBackupCreationInfo
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.junit.Assert.*
|
||||
import java.util.*
|
||||
import java.util.concurrent.CountDownLatch
|
||||
|
@ -78,26 +86,31 @@ class CryptoTestHelper(val mTestHelper: CommonTestHelper) {
|
|||
val aliceSession = cryptoTestData.firstSession
|
||||
val aliceRoomId = cryptoTestData.roomId
|
||||
|
||||
val room = aliceSession.getRoom(aliceRoomId)!!
|
||||
val aliceRoom = aliceSession.getRoom(aliceRoomId)!!
|
||||
|
||||
val bobSession = mTestHelper.createAccount(TestConstants.USER_BOB, defaultSessionParams)
|
||||
|
||||
val lock1 = CountDownLatch(2)
|
||||
|
||||
// val bobEventListener = object : MXEventListener() {
|
||||
// override fun onNewRoom(roomId: String) {
|
||||
// if (TextUtils.equals(roomId, aliceRoomId)) {
|
||||
// if (!statuses.containsKey("onNewRoom")) {
|
||||
// statuses["onNewRoom"] = "onNewRoom"
|
||||
// lock1.countDown()
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// bobSession.dataHandler.addListener(bobEventListener)
|
||||
val bobRoomSummariesLive = runBlocking(Dispatchers.Main) {
|
||||
bobSession.getRoomSummariesLive(roomSummaryQueryParams { })
|
||||
}
|
||||
|
||||
room.invite(bobSession.myUserId, callback = object : TestMatrixCallback<Unit>(lock1) {
|
||||
val newRoomObserver = object : Observer<List<RoomSummary>> {
|
||||
override fun onChanged(t: List<RoomSummary>?) {
|
||||
if (t?.isNotEmpty() == true) {
|
||||
statuses["onNewRoom"] = "onNewRoom"
|
||||
lock1.countDown()
|
||||
bobRoomSummariesLive.removeObserver(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
GlobalScope.launch(Dispatchers.Main) {
|
||||
bobRoomSummariesLive.observeForever(newRoomObserver)
|
||||
}
|
||||
|
||||
aliceRoom.invite(bobSession.myUserId, callback = object : TestMatrixCallback<Unit>(lock1) {
|
||||
override fun onSuccess(data: Unit) {
|
||||
statuses["invite"] = "invite"
|
||||
super.onSuccess(data)
|
||||
|
@ -108,25 +121,25 @@ class CryptoTestHelper(val mTestHelper: CommonTestHelper) {
|
|||
|
||||
assertTrue(statuses.containsKey("invite") && statuses.containsKey("onNewRoom"))
|
||||
|
||||
// bobSession.dataHandler.removeListener(bobEventListener)
|
||||
|
||||
val lock2 = CountDownLatch(2)
|
||||
|
||||
bobSession.joinRoom(aliceRoomId, callback = TestMatrixCallback(lock2))
|
||||
val roomJoinedObserver = object : Observer<List<RoomSummary>> {
|
||||
override fun onChanged(t: List<RoomSummary>?) {
|
||||
if (bobSession.getRoom(aliceRoomId)
|
||||
?.getRoomMember(aliceSession.myUserId)
|
||||
?.membership == Membership.JOIN) {
|
||||
statuses["AliceJoin"] = "AliceJoin"
|
||||
lock2.countDown()
|
||||
bobRoomSummariesLive.removeObserver(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// room.addEventListener(object : MXEventListener() {
|
||||
// override fun onLiveEvent(event: Event, roomState: RoomState) {
|
||||
// if (TextUtils.equals(event.getType(), Event.EVENT_TYPE_STATE_ROOM_MEMBER)) {
|
||||
// val contentToConsider = event.contentAsJsonObject
|
||||
// val member = JsonUtils.toRoomMember(contentToConsider)
|
||||
//
|
||||
// if (TextUtils.equals(member.membership, RoomMember.MEMBERSHIP_JOIN)) {
|
||||
// statuses["AliceJoin"] = "AliceJoin"
|
||||
// lock2.countDown()
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// })
|
||||
GlobalScope.launch(Dispatchers.Main) {
|
||||
bobRoomSummariesLive.observeForever(roomJoinedObserver)
|
||||
}
|
||||
|
||||
bobSession.joinRoom(aliceRoomId, callback = TestMatrixCallback(lock2))
|
||||
|
||||
mTestHelper.await(lock2)
|
||||
|
||||
|
|
|
@ -0,0 +1,212 @@
|
|||
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.CommonTestHelper
|
||||
import im.vector.matrix.android.common.CryptoTestHelper
|
||||
import im.vector.matrix.android.common.SessionTestParams
|
||||
import im.vector.matrix.android.common.TestConstants
|
||||
import im.vector.matrix.android.common.TestMatrixCallback
|
||||
import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
|
||||
import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.UserPasswordAuth
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNotNull
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Assert.assertTrue
|
||||
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()
|
||||
assertNotNull("Master key should be stored", masterPubKey?.unpaddedBase64PublicKey)
|
||||
val selfSigningKey = myCrossSigningKeys?.selfSigningKey()
|
||||
assertNotNull("SelfSigned key should be stored", selfSigningKey?.unpaddedBase64PublicKey)
|
||||
val userKey = myCrossSigningKeys?.userKey()
|
||||
assertNotNull("User key should be stored", userKey?.unpaddedBase64PublicKey)
|
||||
|
||||
assertTrue("Signing Keys should be trusted", myCrossSigningKeys?.isTrusted() == true)
|
||||
|
||||
assertTrue("Signing Keys should be trusted", aliceSession.getCrossSigningService().checkUserTrust(aliceSession.myUserId).isVerified())
|
||||
|
||||
mTestHelper.signout(aliceSession)
|
||||
}
|
||||
|
||||
@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)
|
||||
assertNotNull("Alice can see bob Master key", bobKeysFromAlicePOV?.masterKey())
|
||||
assertNull("Alice should not see bob User key", bobKeysFromAlicePOV?.userKey())
|
||||
assertNotNull("Alice can see bob SelfSigned key", bobKeysFromAlicePOV?.selfSigningKey())
|
||||
|
||||
assertEquals("Bob keys from alice pov should match", bobKeysFromAlicePOV?.masterKey()?.unpaddedBase64PublicKey, bobSession.getCrossSigningService().getMyCrossSigningKeys()?.masterKey()?.unpaddedBase64PublicKey)
|
||||
assertEquals("Bob keys from alice pov should match", bobKeysFromAlicePOV?.selfSigningKey()?.unpaddedBase64PublicKey, bobSession.getCrossSigningService().getMyCrossSigningKeys()?.selfSigningKey()?.unpaddedBase64PublicKey)
|
||||
|
||||
assertTrue("Bob keys from alice pov should not be trusted", bobKeysFromAlicePOV?.isTrusted() == false)
|
||||
|
||||
mTestHelper.signout(aliceSession)
|
||||
mTestHelper.signout(bobSession)
|
||||
}
|
||||
|
||||
@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)
|
||||
assertTrue("Bob keys from alice pov should not be trusted", bobKeysFromAlicePOV?.isTrusted() == false)
|
||||
|
||||
val trustLatch = CountDownLatch(1)
|
||||
aliceSession.getCrossSigningService().trustUser(bobUserId, object : MatrixCallback<Unit> {
|
||||
override fun onSuccess(data: Unit) {
|
||||
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<CryptoDeviceInfo>> {
|
||||
override fun onFailure(failure: Throwable) {
|
||||
fail("Failed to get device")
|
||||
}
|
||||
|
||||
override fun onSuccess(data: MXUsersDevicesMap<CryptoDeviceInfo>) {
|
||||
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<Unit> {
|
||||
override fun onSuccess(data: Unit) {
|
||||
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<CryptoDeviceInfo>> {
|
||||
override fun onFailure(failure: Throwable) {
|
||||
fail("Failed to get device")
|
||||
}
|
||||
|
||||
override fun onSuccess(data: MXUsersDevicesMap<CryptoDeviceInfo>) {
|
||||
// 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 result = aliceSession.getCrossSigningService().checkDeviceTrust(bobUserId, bobSecondDeviceId, null)
|
||||
assertTrue("Bob second device should be trusted from alice POV", result.isCrossSignedVerified())
|
||||
|
||||
mTestHelper.signout(aliceSession)
|
||||
mTestHelper.signout(bobSession)
|
||||
mTestHelper.signout(bobSession2)
|
||||
}
|
||||
}
|
|
@ -25,23 +25,36 @@ import im.vector.matrix.android.api.session.Session
|
|||
import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupService
|
||||
import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupState
|
||||
import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupStateListener
|
||||
import im.vector.matrix.android.common.*
|
||||
import im.vector.matrix.android.common.CommonTestHelper
|
||||
import im.vector.matrix.android.common.CryptoTestData
|
||||
import im.vector.matrix.android.common.CryptoTestHelper
|
||||
import im.vector.matrix.android.common.SessionTestParams
|
||||
import im.vector.matrix.android.common.TestConstants
|
||||
import im.vector.matrix.android.common.TestMatrixCallback
|
||||
import im.vector.matrix.android.common.assertDictEquals
|
||||
import im.vector.matrix.android.common.assertListEquals
|
||||
import im.vector.matrix.android.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM_BACKUP
|
||||
import im.vector.matrix.android.internal.crypto.MegolmSessionData
|
||||
import im.vector.matrix.android.internal.crypto.OutgoingRoomKeyRequest
|
||||
import im.vector.matrix.android.internal.crypto.crosssigning.DeviceTrustLevel
|
||||
import im.vector.matrix.android.internal.crypto.keysbackup.model.KeysBackupVersionTrust
|
||||
import im.vector.matrix.android.internal.crypto.keysbackup.model.MegolmBackupCreationInfo
|
||||
import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.KeysVersion
|
||||
import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.KeysVersionResult
|
||||
import im.vector.matrix.android.internal.crypto.model.ImportRoomKeysResult
|
||||
import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo
|
||||
import im.vector.matrix.android.internal.crypto.model.OlmInboundGroupSessionWrapper
|
||||
import org.junit.Assert.*
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertNotNull
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Assert.fail
|
||||
import org.junit.FixMethodOrder
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.junit.runners.MethodSorters
|
||||
import java.util.*
|
||||
import java.util.ArrayList
|
||||
import java.util.Collections
|
||||
import java.util.concurrent.CountDownLatch
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
|
@ -298,7 +311,10 @@ class KeysBackupTest : InstrumentedTest {
|
|||
val decryption = keysBackup.pkDecryptionFromRecoveryKey(keyBackupCreationInfo.recoveryKey)
|
||||
assertNotNull(decryption)
|
||||
// - Check decryptKeyBackupData() returns stg
|
||||
val sessionData = keysBackup.decryptKeyBackupData(keyBackupData, session.olmInboundGroupSession!!.sessionIdentifier(), cryptoTestData.roomId, decryption!!)
|
||||
val sessionData = keysBackup
|
||||
.decryptKeyBackupData(keyBackupData,
|
||||
session.olmInboundGroupSession!!.sessionIdentifier(),
|
||||
cryptoTestData.roomId, decryption!!)
|
||||
assertNotNull(sessionData)
|
||||
// - Compare the decrypted megolm key with the original one
|
||||
assertKeysEquals(session.exportKeys(), sessionData)
|
||||
|
@ -1161,7 +1177,7 @@ class KeysBackupTest : InstrumentedTest {
|
|||
assertFalse(keysBackup2.isEnabled)
|
||||
|
||||
// - Validate the old device from the new one
|
||||
aliceSession2.setDeviceVerification(MXDeviceInfo.DEVICE_VERIFICATION_VERIFIED, oldDeviceId, aliceSession2.myUserId)
|
||||
aliceSession2.setDeviceVerification(DeviceTrustLevel(false, true), aliceSession2.myUserId, oldDeviceId)
|
||||
|
||||
// -> Backup should automatically enable on the new device
|
||||
val latch4 = CountDownLatch(1)
|
||||
|
|
|
@ -24,11 +24,12 @@ import im.vector.matrix.android.api.session.events.model.Event
|
|||
import im.vector.matrix.android.api.session.events.model.toModel
|
||||
import im.vector.matrix.android.common.CommonTestHelper
|
||||
import im.vector.matrix.android.common.CryptoTestHelper
|
||||
import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo
|
||||
import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
|
||||
import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.KeyVerificationAccept
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.KeyVerificationCancel
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.KeyVerificationStart
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.toValue
|
||||
import org.junit.Assert.*
|
||||
import org.junit.FixMethodOrder
|
||||
import org.junit.Test
|
||||
|
@ -50,53 +51,53 @@ class SASTest : InstrumentedTest {
|
|||
val aliceSession = cryptoTestData.firstSession
|
||||
val bobSession = cryptoTestData.secondSession
|
||||
|
||||
val aliceSasMgr = aliceSession.getSasVerificationService()
|
||||
val bobSasMgr = bobSession!!.getSasVerificationService()
|
||||
val aliceVerificationService = aliceSession.getVerificationService()
|
||||
val bobVerificationService = bobSession!!.getVerificationService()
|
||||
|
||||
val bobTxCreatedLatch = CountDownLatch(1)
|
||||
val bobListener = object : SasVerificationService.SasVerificationListener {
|
||||
override fun transactionCreated(tx: SasVerificationTransaction) {}
|
||||
val bobListener = object : VerificationService.VerificationListener {
|
||||
override fun transactionCreated(tx: VerificationTransaction) {}
|
||||
|
||||
override fun transactionUpdated(tx: SasVerificationTransaction) {
|
||||
override fun transactionUpdated(tx: VerificationTransaction) {
|
||||
bobTxCreatedLatch.countDown()
|
||||
}
|
||||
|
||||
override fun markedAsManuallyVerified(userId: String, deviceId: String) {}
|
||||
}
|
||||
bobSasMgr.addListener(bobListener)
|
||||
bobVerificationService.addListener(bobListener)
|
||||
|
||||
val txID = aliceSasMgr.beginKeyVerificationSAS(bobSession.myUserId, bobSession.getMyDevice().deviceId)
|
||||
val txID = aliceVerificationService.beginKeyVerification(VerificationMethod.SAS, bobSession.myUserId, bobSession.getMyDevice().deviceId)
|
||||
assertNotNull("Alice should have a started transaction", txID)
|
||||
|
||||
val aliceKeyTx = aliceSasMgr.getExistingTransaction(bobSession.myUserId, txID!!)
|
||||
val aliceKeyTx = aliceVerificationService.getExistingTransaction(bobSession.myUserId, txID!!)
|
||||
assertNotNull("Alice should have a started transaction", aliceKeyTx)
|
||||
|
||||
mTestHelper.await(bobTxCreatedLatch)
|
||||
bobSasMgr.removeListener(bobListener)
|
||||
bobVerificationService.removeListener(bobListener)
|
||||
|
||||
val bobKeyTx = bobSasMgr.getExistingTransaction(aliceSession.myUserId, txID)
|
||||
val bobKeyTx = bobVerificationService.getExistingTransaction(aliceSession.myUserId, txID)
|
||||
|
||||
assertNotNull("Bob should have started verif transaction", bobKeyTx)
|
||||
assertTrue(bobKeyTx is SASVerificationTransaction)
|
||||
assertTrue(bobKeyTx is SASDefaultVerificationTransaction)
|
||||
assertNotNull("Bob should have starting a SAS transaction", bobKeyTx)
|
||||
assertTrue(aliceKeyTx is SASVerificationTransaction)
|
||||
assertTrue(aliceKeyTx is SASDefaultVerificationTransaction)
|
||||
assertEquals("Alice and Bob have same transaction id", aliceKeyTx!!.transactionId, bobKeyTx!!.transactionId)
|
||||
|
||||
val aliceSasTx = aliceKeyTx as SASVerificationTransaction?
|
||||
val bobSasTx = bobKeyTx as SASVerificationTransaction?
|
||||
val aliceSasTx = aliceKeyTx as SASDefaultVerificationTransaction?
|
||||
val bobSasTx = bobKeyTx as SASDefaultVerificationTransaction?
|
||||
|
||||
assertEquals("Alice state should be started", SasVerificationTxState.Started, aliceSasTx!!.state)
|
||||
assertEquals("Bob state should be started by alice", SasVerificationTxState.OnStarted, bobSasTx!!.state)
|
||||
assertEquals("Alice state should be started", VerificationTxState.Started, aliceSasTx!!.state)
|
||||
assertEquals("Bob state should be started by alice", VerificationTxState.OnStarted, bobSasTx!!.state)
|
||||
|
||||
// Let's cancel from alice side
|
||||
val cancelLatch = CountDownLatch(1)
|
||||
|
||||
val bobListener2 = object : SasVerificationService.SasVerificationListener {
|
||||
override fun transactionCreated(tx: SasVerificationTransaction) {}
|
||||
val bobListener2 = object : VerificationService.VerificationListener {
|
||||
override fun transactionCreated(tx: VerificationTransaction) {}
|
||||
|
||||
override fun transactionUpdated(tx: SasVerificationTransaction) {
|
||||
override fun transactionUpdated(tx: VerificationTransaction) {
|
||||
if (tx.transactionId == txID) {
|
||||
if ((tx as SASVerificationTransaction).state === SasVerificationTxState.OnCancelled) {
|
||||
if ((tx as SASDefaultVerificationTransaction).state === VerificationTxState.OnCancelled) {
|
||||
cancelLatch.countDown()
|
||||
}
|
||||
}
|
||||
|
@ -104,23 +105,23 @@ class SASTest : InstrumentedTest {
|
|||
|
||||
override fun markedAsManuallyVerified(userId: String, deviceId: String) {}
|
||||
}
|
||||
bobSasMgr.addListener(bobListener2)
|
||||
bobVerificationService.addListener(bobListener2)
|
||||
|
||||
aliceSasTx.cancel(CancelCode.User)
|
||||
mTestHelper.await(cancelLatch)
|
||||
|
||||
assertEquals("Should be cancelled on alice side",
|
||||
SasVerificationTxState.Cancelled, aliceSasTx.state)
|
||||
VerificationTxState.Cancelled, aliceSasTx.state)
|
||||
assertEquals("Should be cancelled on bob side",
|
||||
SasVerificationTxState.OnCancelled, bobSasTx.state)
|
||||
VerificationTxState.OnCancelled, bobSasTx.state)
|
||||
|
||||
assertEquals("Should be User cancelled on alice side",
|
||||
CancelCode.User, aliceSasTx.cancelledReason)
|
||||
assertEquals("Should be User cancelled on bob side",
|
||||
CancelCode.User, aliceSasTx.cancelledReason)
|
||||
|
||||
assertNull(bobSasMgr.getExistingTransaction(aliceSession.myUserId, txID))
|
||||
assertNull(aliceSasMgr.getExistingTransaction(bobSession.myUserId, txID))
|
||||
assertNull(bobVerificationService.getExistingTransaction(aliceSession.myUserId, txID))
|
||||
assertNull(aliceVerificationService.getExistingTransaction(bobSession.myUserId, txID))
|
||||
|
||||
cryptoTestData.close()
|
||||
}
|
||||
|
@ -135,8 +136,23 @@ class SASTest : InstrumentedTest {
|
|||
val tid = "00000000"
|
||||
|
||||
// Bob should receive a cancel
|
||||
var canceledToDeviceEvent: Event? = null
|
||||
var cancelReason: String? = null
|
||||
val cancelLatch = CountDownLatch(1)
|
||||
|
||||
val bobListener = object : VerificationService.VerificationListener {
|
||||
override fun transactionCreated(tx: VerificationTransaction) {}
|
||||
|
||||
override fun transactionUpdated(tx: VerificationTransaction) {
|
||||
if (tx.transactionId == tid && tx.cancelledReason != null) {
|
||||
cancelReason = tx.cancelledReason?.humanReadable
|
||||
cancelLatch.countDown()
|
||||
}
|
||||
}
|
||||
|
||||
override fun markedAsManuallyVerified(userId: String, deviceId: String) {}
|
||||
}
|
||||
bobSession.getVerificationService().addListener(bobListener)
|
||||
|
||||
// TODO bobSession!!.dataHandler.addListener(object : MXEventListener() {
|
||||
// TODO override fun onToDeviceEvent(event: Event?) {
|
||||
// TODO if (event!!.getType() == CryptoEvent.EVENT_TYPE_KEY_VERIFICATION_CANCEL) {
|
||||
|
@ -152,25 +168,24 @@ class SASTest : InstrumentedTest {
|
|||
val aliceUserID = aliceSession.myUserId
|
||||
val aliceDevice = aliceSession.getMyDevice().deviceId
|
||||
|
||||
val aliceListener = object : SasVerificationService.SasVerificationListener {
|
||||
override fun transactionCreated(tx: SasVerificationTransaction) {}
|
||||
val aliceListener = object : VerificationService.VerificationListener {
|
||||
override fun transactionCreated(tx: VerificationTransaction) {}
|
||||
|
||||
override fun transactionUpdated(tx: SasVerificationTransaction) {
|
||||
if ((tx as IncomingSASVerificationTransaction).uxState === IncomingSasVerificationTransaction.UxState.SHOW_ACCEPT) {
|
||||
(tx as IncomingSASVerificationTransaction).performAccept()
|
||||
override fun transactionUpdated(tx: VerificationTransaction) {
|
||||
if ((tx as IncomingSasVerificationTransaction).uxState === IncomingSasVerificationTransaction.UxState.SHOW_ACCEPT) {
|
||||
(tx as IncomingSasVerificationTransaction).performAccept()
|
||||
}
|
||||
}
|
||||
|
||||
override fun markedAsManuallyVerified(userId: String, deviceId: String) {}
|
||||
}
|
||||
aliceSession.getSasVerificationService().addListener(aliceListener)
|
||||
aliceSession.getVerificationService().addListener(aliceListener)
|
||||
|
||||
fakeBobStart(bobSession, aliceUserID, aliceDevice, tid, protocols = protocols)
|
||||
|
||||
mTestHelper.await(cancelLatch)
|
||||
|
||||
val cancelReq = canceledToDeviceEvent!!.content.toModel<KeyVerificationCancel>()!!
|
||||
assertEquals("Request should be cancelled with m.unknown_method", CancelCode.UnknownMethod.value, cancelReq.code)
|
||||
assertEquals("Request should be cancelled with m.unknown_method", CancelCode.UnknownMethod.value, cancelReason)
|
||||
|
||||
cryptoTestData.close()
|
||||
}
|
||||
|
@ -253,18 +268,19 @@ class SASTest : InstrumentedTest {
|
|||
aliceUserID: String?,
|
||||
aliceDevice: String?,
|
||||
tid: String,
|
||||
protocols: List<String> = SASVerificationTransaction.KNOWN_AGREEMENT_PROTOCOLS,
|
||||
hashes: List<String> = SASVerificationTransaction.KNOWN_HASHES,
|
||||
mac: List<String> = SASVerificationTransaction.KNOWN_MACS,
|
||||
codes: List<String> = SASVerificationTransaction.KNOWN_SHORT_CODES) {
|
||||
val startMessage = KeyVerificationStart()
|
||||
startMessage.fromDevice = bobSession.getMyDevice().deviceId
|
||||
startMessage.method = KeyVerificationStart.VERIF_METHOD_SAS
|
||||
startMessage.transactionID = tid
|
||||
startMessage.keyAgreementProtocols = protocols
|
||||
startMessage.hashes = hashes
|
||||
startMessage.messageAuthenticationCodes = mac
|
||||
startMessage.shortAuthenticationStrings = codes
|
||||
protocols: List<String> = SASDefaultVerificationTransaction.KNOWN_AGREEMENT_PROTOCOLS,
|
||||
hashes: List<String> = SASDefaultVerificationTransaction.KNOWN_HASHES,
|
||||
mac: List<String> = SASDefaultVerificationTransaction.KNOWN_MACS,
|
||||
codes: List<String> = SASDefaultVerificationTransaction.KNOWN_SHORT_CODES) {
|
||||
val startMessage = KeyVerificationStart(
|
||||
fromDevice = bobSession.getMyDevice().deviceId,
|
||||
method = VerificationMethod.SAS.toValue(),
|
||||
transactionID = tid,
|
||||
keyAgreementProtocols = protocols,
|
||||
hashes = hashes,
|
||||
messageAuthenticationCodes = mac,
|
||||
shortAuthenticationStrings = codes
|
||||
)
|
||||
|
||||
val contentMap = MXUsersDevicesMap<Any>()
|
||||
contentMap.setObject(aliceUserID, aliceDevice, startMessage)
|
||||
|
@ -287,31 +303,31 @@ class SASTest : InstrumentedTest {
|
|||
val aliceSession = cryptoTestData.firstSession
|
||||
val bobSession = cryptoTestData.secondSession
|
||||
|
||||
val aliceSasMgr = aliceSession.getSasVerificationService()
|
||||
val aliceVerificationService = aliceSession.getVerificationService()
|
||||
|
||||
val aliceCreatedLatch = CountDownLatch(2)
|
||||
val aliceCancelledLatch = CountDownLatch(2)
|
||||
val createdTx = ArrayList<SASVerificationTransaction>()
|
||||
val aliceListener = object : SasVerificationService.SasVerificationListener {
|
||||
override fun transactionCreated(tx: SasVerificationTransaction) {
|
||||
createdTx.add(tx as SASVerificationTransaction)
|
||||
val createdTx = ArrayList<SASDefaultVerificationTransaction>()
|
||||
val aliceListener = object : VerificationService.VerificationListener {
|
||||
override fun transactionCreated(tx: VerificationTransaction) {
|
||||
createdTx.add(tx as SASDefaultVerificationTransaction)
|
||||
aliceCreatedLatch.countDown()
|
||||
}
|
||||
|
||||
override fun transactionUpdated(tx: SasVerificationTransaction) {
|
||||
if ((tx as SASVerificationTransaction).state === SasVerificationTxState.OnCancelled) {
|
||||
override fun transactionUpdated(tx: VerificationTransaction) {
|
||||
if ((tx as SASDefaultVerificationTransaction).state === VerificationTxState.OnCancelled) {
|
||||
aliceCancelledLatch.countDown()
|
||||
}
|
||||
}
|
||||
|
||||
override fun markedAsManuallyVerified(userId: String, deviceId: String) {}
|
||||
}
|
||||
aliceSasMgr.addListener(aliceListener)
|
||||
aliceVerificationService.addListener(aliceListener)
|
||||
|
||||
val bobUserId = bobSession!!.myUserId
|
||||
val bobDeviceId = bobSession.getMyDevice().deviceId
|
||||
aliceSasMgr.beginKeyVerificationSAS(bobUserId, bobDeviceId)
|
||||
aliceSasMgr.beginKeyVerificationSAS(bobUserId, bobDeviceId)
|
||||
aliceVerificationService.beginKeyVerification(VerificationMethod.SAS, bobUserId, bobDeviceId)
|
||||
aliceVerificationService.beginKeyVerification(VerificationMethod.SAS, bobUserId, bobDeviceId)
|
||||
|
||||
mTestHelper.await(aliceCreatedLatch)
|
||||
mTestHelper.await(aliceCancelledLatch)
|
||||
|
@ -329,46 +345,46 @@ class SASTest : InstrumentedTest {
|
|||
val aliceSession = cryptoTestData.firstSession
|
||||
val bobSession = cryptoTestData.secondSession
|
||||
|
||||
val aliceSasMgr = aliceSession.getSasVerificationService()
|
||||
val bobSasMgr = bobSession!!.getSasVerificationService()
|
||||
val aliceVerificationService = aliceSession.getVerificationService()
|
||||
val bobVerificationService = bobSession!!.getVerificationService()
|
||||
|
||||
var accepted: KeyVerificationAccept? = null
|
||||
var startReq: KeyVerificationStart? = null
|
||||
|
||||
val aliceAcceptedLatch = CountDownLatch(1)
|
||||
val aliceListener = object : SasVerificationService.SasVerificationListener {
|
||||
val aliceListener = object : VerificationService.VerificationListener {
|
||||
override fun markedAsManuallyVerified(userId: String, deviceId: String) {}
|
||||
|
||||
override fun transactionCreated(tx: SasVerificationTransaction) {}
|
||||
override fun transactionCreated(tx: VerificationTransaction) {}
|
||||
|
||||
override fun transactionUpdated(tx: SasVerificationTransaction) {
|
||||
if ((tx as SASVerificationTransaction).state === SasVerificationTxState.OnAccepted) {
|
||||
val at = tx as SASVerificationTransaction
|
||||
accepted = at.accepted
|
||||
startReq = at.startReq
|
||||
override fun transactionUpdated(tx: VerificationTransaction) {
|
||||
if ((tx as SASDefaultVerificationTransaction).state === VerificationTxState.OnAccepted) {
|
||||
val at = tx as SASDefaultVerificationTransaction
|
||||
accepted = at.accepted as? KeyVerificationAccept
|
||||
startReq = at.startReq as? KeyVerificationStart
|
||||
aliceAcceptedLatch.countDown()
|
||||
}
|
||||
}
|
||||
}
|
||||
aliceSasMgr.addListener(aliceListener)
|
||||
aliceVerificationService.addListener(aliceListener)
|
||||
|
||||
val bobListener = object : SasVerificationService.SasVerificationListener {
|
||||
override fun transactionCreated(tx: SasVerificationTransaction) {}
|
||||
val bobListener = object : VerificationService.VerificationListener {
|
||||
override fun transactionCreated(tx: VerificationTransaction) {}
|
||||
|
||||
override fun transactionUpdated(tx: SasVerificationTransaction) {
|
||||
if ((tx as IncomingSASVerificationTransaction).uxState === IncomingSasVerificationTransaction.UxState.SHOW_ACCEPT) {
|
||||
val at = tx as IncomingSASVerificationTransaction
|
||||
override fun transactionUpdated(tx: VerificationTransaction) {
|
||||
if ((tx as IncomingSasVerificationTransaction).uxState === IncomingSasVerificationTransaction.UxState.SHOW_ACCEPT) {
|
||||
val at = tx as IncomingSasVerificationTransaction
|
||||
at.performAccept()
|
||||
}
|
||||
}
|
||||
|
||||
override fun markedAsManuallyVerified(userId: String, deviceId: String) {}
|
||||
}
|
||||
bobSasMgr.addListener(bobListener)
|
||||
bobVerificationService.addListener(bobListener)
|
||||
|
||||
val bobUserId = bobSession.myUserId
|
||||
val bobDeviceId = bobSession.getMyDevice().deviceId
|
||||
aliceSasMgr.beginKeyVerificationSAS(bobUserId, bobDeviceId)
|
||||
aliceVerificationService.beginKeyVerification(VerificationMethod.SAS, bobUserId, bobDeviceId)
|
||||
mTestHelper.await(aliceAcceptedLatch)
|
||||
|
||||
assertTrue("Should have receive a commitment", accepted!!.commitment?.trim()?.isEmpty() == false)
|
||||
|
@ -393,17 +409,17 @@ class SASTest : InstrumentedTest {
|
|||
val aliceSession = cryptoTestData.firstSession
|
||||
val bobSession = cryptoTestData.secondSession
|
||||
|
||||
val aliceSasMgr = aliceSession.getSasVerificationService()
|
||||
val bobSasMgr = bobSession!!.getSasVerificationService()
|
||||
val aliceVerificationService = aliceSession.getVerificationService()
|
||||
val bobVerificationService = bobSession!!.getVerificationService()
|
||||
|
||||
val aliceSASLatch = CountDownLatch(1)
|
||||
val aliceListener = object : SasVerificationService.SasVerificationListener {
|
||||
override fun transactionCreated(tx: SasVerificationTransaction) {}
|
||||
val aliceListener = object : VerificationService.VerificationListener {
|
||||
override fun transactionCreated(tx: VerificationTransaction) {}
|
||||
|
||||
override fun transactionUpdated(tx: SasVerificationTransaction) {
|
||||
val uxState = (tx as OutgoingSASVerificationRequest).uxState
|
||||
override fun transactionUpdated(tx: VerificationTransaction) {
|
||||
val uxState = (tx as OutgoingSasVerificationTransaction).uxState
|
||||
when (uxState) {
|
||||
OutgoingSasVerificationRequest.UxState.SHOW_SAS -> {
|
||||
OutgoingSasVerificationTransaction.UxState.SHOW_SAS -> {
|
||||
aliceSASLatch.countDown()
|
||||
}
|
||||
else -> Unit
|
||||
|
@ -412,14 +428,14 @@ class SASTest : InstrumentedTest {
|
|||
|
||||
override fun markedAsManuallyVerified(userId: String, deviceId: String) {}
|
||||
}
|
||||
aliceSasMgr.addListener(aliceListener)
|
||||
aliceVerificationService.addListener(aliceListener)
|
||||
|
||||
val bobSASLatch = CountDownLatch(1)
|
||||
val bobListener = object : SasVerificationService.SasVerificationListener {
|
||||
override fun transactionCreated(tx: SasVerificationTransaction) {}
|
||||
val bobListener = object : VerificationService.VerificationListener {
|
||||
override fun transactionCreated(tx: VerificationTransaction) {}
|
||||
|
||||
override fun transactionUpdated(tx: SasVerificationTransaction) {
|
||||
val uxState = (tx as IncomingSASVerificationTransaction).uxState
|
||||
override fun transactionUpdated(tx: VerificationTransaction) {
|
||||
val uxState = (tx as IncomingSasVerificationTransaction).uxState
|
||||
when (uxState) {
|
||||
IncomingSasVerificationTransaction.UxState.SHOW_ACCEPT -> {
|
||||
tx.performAccept()
|
||||
|
@ -433,16 +449,16 @@ class SASTest : InstrumentedTest {
|
|||
|
||||
override fun markedAsManuallyVerified(userId: String, deviceId: String) {}
|
||||
}
|
||||
bobSasMgr.addListener(bobListener)
|
||||
bobVerificationService.addListener(bobListener)
|
||||
|
||||
val bobUserId = bobSession.myUserId
|
||||
val bobDeviceId = bobSession.getMyDevice().deviceId
|
||||
val verificationSAS = aliceSasMgr.beginKeyVerificationSAS(bobUserId, bobDeviceId)
|
||||
val verificationSAS = aliceVerificationService.beginKeyVerification(VerificationMethod.SAS, bobUserId, bobDeviceId)
|
||||
mTestHelper.await(aliceSASLatch)
|
||||
mTestHelper.await(bobSASLatch)
|
||||
|
||||
val aliceTx = aliceSasMgr.getExistingTransaction(bobUserId, verificationSAS!!) as SASVerificationTransaction
|
||||
val bobTx = bobSasMgr.getExistingTransaction(aliceSession.myUserId, verificationSAS) as SASVerificationTransaction
|
||||
val aliceTx = aliceVerificationService.getExistingTransaction(bobUserId, verificationSAS!!) as SASDefaultVerificationTransaction
|
||||
val bobTx = bobVerificationService.getExistingTransaction(aliceSession.myUserId, verificationSAS) as SASDefaultVerificationTransaction
|
||||
|
||||
assertEquals("Should have same SAS", aliceTx.getShortCodeRepresentation(SasMode.DECIMAL),
|
||||
bobTx.getShortCodeRepresentation(SasMode.DECIMAL))
|
||||
|
@ -457,20 +473,20 @@ class SASTest : InstrumentedTest {
|
|||
val aliceSession = cryptoTestData.firstSession
|
||||
val bobSession = cryptoTestData.secondSession
|
||||
|
||||
val aliceSasMgr = aliceSession.getSasVerificationService()
|
||||
val bobSasMgr = bobSession!!.getSasVerificationService()
|
||||
val aliceVerificationService = aliceSession.getVerificationService()
|
||||
val bobVerificationService = bobSession!!.getVerificationService()
|
||||
|
||||
val aliceSASLatch = CountDownLatch(1)
|
||||
val aliceListener = object : SasVerificationService.SasVerificationListener {
|
||||
override fun transactionCreated(tx: SasVerificationTransaction) {}
|
||||
val aliceListener = object : VerificationService.VerificationListener {
|
||||
override fun transactionCreated(tx: VerificationTransaction) {}
|
||||
|
||||
override fun transactionUpdated(tx: SasVerificationTransaction) {
|
||||
val uxState = (tx as OutgoingSASVerificationRequest).uxState
|
||||
override fun transactionUpdated(tx: VerificationTransaction) {
|
||||
val uxState = (tx as OutgoingSasVerificationTransaction).uxState
|
||||
when (uxState) {
|
||||
OutgoingSasVerificationRequest.UxState.SHOW_SAS -> {
|
||||
OutgoingSasVerificationTransaction.UxState.SHOW_SAS -> {
|
||||
tx.userHasVerifiedShortCode()
|
||||
}
|
||||
OutgoingSasVerificationRequest.UxState.VERIFIED -> {
|
||||
OutgoingSasVerificationTransaction.UxState.VERIFIED -> {
|
||||
aliceSASLatch.countDown()
|
||||
}
|
||||
else -> Unit
|
||||
|
@ -479,14 +495,14 @@ class SASTest : InstrumentedTest {
|
|||
|
||||
override fun markedAsManuallyVerified(userId: String, deviceId: String) {}
|
||||
}
|
||||
aliceSasMgr.addListener(aliceListener)
|
||||
aliceVerificationService.addListener(aliceListener)
|
||||
|
||||
val bobSASLatch = CountDownLatch(1)
|
||||
val bobListener = object : SasVerificationService.SasVerificationListener {
|
||||
override fun transactionCreated(tx: SasVerificationTransaction) {}
|
||||
val bobListener = object : VerificationService.VerificationListener {
|
||||
override fun transactionCreated(tx: VerificationTransaction) {}
|
||||
|
||||
override fun transactionUpdated(tx: SasVerificationTransaction) {
|
||||
val uxState = (tx as IncomingSASVerificationTransaction).uxState
|
||||
override fun transactionUpdated(tx: VerificationTransaction) {
|
||||
val uxState = (tx as IncomingSasVerificationTransaction).uxState
|
||||
when (uxState) {
|
||||
IncomingSasVerificationTransaction.UxState.SHOW_ACCEPT -> {
|
||||
tx.performAccept()
|
||||
|
@ -503,17 +519,17 @@ class SASTest : InstrumentedTest {
|
|||
|
||||
override fun markedAsManuallyVerified(userId: String, deviceId: String) {}
|
||||
}
|
||||
bobSasMgr.addListener(bobListener)
|
||||
bobVerificationService.addListener(bobListener)
|
||||
|
||||
val bobUserId = bobSession.myUserId
|
||||
val bobDeviceId = bobSession.getMyDevice().deviceId
|
||||
aliceSasMgr.beginKeyVerificationSAS(bobUserId, bobDeviceId)
|
||||
aliceVerificationService.beginKeyVerification(VerificationMethod.SAS, bobUserId, bobDeviceId)
|
||||
mTestHelper.await(aliceSASLatch)
|
||||
mTestHelper.await(bobSASLatch)
|
||||
|
||||
// Assert that devices are verified
|
||||
val bobDeviceInfoFromAlicePOV: MXDeviceInfo? = aliceSession.getDeviceInfo(bobUserId, bobDeviceId)
|
||||
val aliceDeviceInfoFromBobPOV: MXDeviceInfo? = bobSession.getDeviceInfo(aliceSession.myUserId, aliceSession.getMyDevice().deviceId)
|
||||
val bobDeviceInfoFromAlicePOV: CryptoDeviceInfo? = aliceSession.getDeviceInfo(bobUserId, bobDeviceId)
|
||||
val aliceDeviceInfoFromBobPOV: CryptoDeviceInfo? = bobSession.getDeviceInfo(aliceSession.myUserId, aliceSession.getMyDevice().deviceId)
|
||||
|
||||
// latch wait a bit again
|
||||
Thread.sleep(1000)
|
||||
|
|
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
* 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.verification.qrcode
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import im.vector.matrix.android.InstrumentedTest
|
||||
import org.amshove.kluent.shouldBe
|
||||
import org.amshove.kluent.shouldNotBeEqualTo
|
||||
import org.junit.FixMethodOrder
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.junit.runners.MethodSorters
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
@FixMethodOrder(MethodSorters.JVM)
|
||||
class SharedSecretTest : InstrumentedTest {
|
||||
|
||||
@Test
|
||||
fun testSharedSecretLengthCase() {
|
||||
repeat(100) {
|
||||
generateSharedSecret().length shouldBe 43
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testSharedDiffCase() {
|
||||
val sharedSecret1 = generateSharedSecret()
|
||||
val sharedSecret2 = generateSharedSecret()
|
||||
|
||||
sharedSecret1 shouldNotBeEqualTo sharedSecret2
|
||||
}
|
||||
}
|
|
@ -6,9 +6,10 @@
|
|||
<uses-permission android:name="android.permission.VIBRATE" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
|
||||
<application>
|
||||
<application android:networkSecurityConfig="@xml/network_security_config">
|
||||
|
||||
<provider android:name="androidx.work.impl.WorkManagerInitializer"
|
||||
<provider
|
||||
android:name="androidx.work.impl.WorkManagerInitializer"
|
||||
android:authorities="${applicationId}.workmanager-init"
|
||||
android:exported="false"
|
||||
tools:node="remove" />
|
||||
|
|
|
@ -17,14 +17,14 @@
|
|||
package im.vector.matrix.android.api.extensions
|
||||
|
||||
import im.vector.matrix.android.api.comparators.DatedObjectComparators
|
||||
import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo
|
||||
import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
|
||||
|
||||
/* ==========================================================================================
|
||||
* MXDeviceInfo
|
||||
* ========================================================================================== */
|
||||
|
||||
fun MXDeviceInfo.getFingerprintHumanReadable() = fingerprint()
|
||||
fun CryptoDeviceInfo.getFingerprintHumanReadable() = fingerprint()
|
||||
?.chunked(4)
|
||||
?.joinToString(separator = " ")
|
||||
|
||||
|
|
|
@ -19,7 +19,7 @@ package im.vector.matrix.android.api.permalinks
|
|||
import im.vector.matrix.android.api.session.events.model.Event
|
||||
|
||||
/**
|
||||
* Useful methods to create Matrix permalink.
|
||||
* Useful methods to create Matrix permalink (matrix.to links).
|
||||
*/
|
||||
object PermalinkFactory {
|
||||
|
||||
|
@ -84,7 +84,17 @@ object PermalinkFactory {
|
|||
* @param id the id to escape
|
||||
* @return the escaped id
|
||||
*/
|
||||
private fun escape(id: String): String {
|
||||
internal fun escape(id: String): String {
|
||||
return id.replace("/", "%2F")
|
||||
}
|
||||
|
||||
/**
|
||||
* Unescape '/' in id
|
||||
*
|
||||
* @param id the id to escape
|
||||
* @return the escaped id
|
||||
*/
|
||||
internal fun unescape(id: String): String {
|
||||
return id.replace("%2F", "/")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,15 +17,19 @@
|
|||
package im.vector.matrix.android.api.session.crypto
|
||||
|
||||
import android.content.Context
|
||||
import androidx.lifecycle.LiveData
|
||||
import im.vector.matrix.android.api.MatrixCallback
|
||||
import im.vector.matrix.android.api.listeners.ProgressListener
|
||||
import im.vector.matrix.android.api.session.crypto.crosssigning.CrossSigningService
|
||||
import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupService
|
||||
import im.vector.matrix.android.api.session.crypto.keyshare.RoomKeysRequestListener
|
||||
import im.vector.matrix.android.api.session.crypto.sas.SasVerificationService
|
||||
import im.vector.matrix.android.api.session.crypto.sas.VerificationService
|
||||
import im.vector.matrix.android.api.session.events.model.Content
|
||||
import im.vector.matrix.android.api.session.events.model.Event
|
||||
import im.vector.matrix.android.internal.crypto.MXEventDecryptionResult
|
||||
import im.vector.matrix.android.internal.crypto.NewSessionListener
|
||||
import im.vector.matrix.android.internal.crypto.crosssigning.DeviceTrustLevel
|
||||
import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
|
||||
import im.vector.matrix.android.internal.crypto.model.ImportRoomKeysResult
|
||||
import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo
|
||||
import im.vector.matrix.android.internal.crypto.model.MXEncryptEventContentResult
|
||||
|
@ -46,7 +50,9 @@ interface CryptoService {
|
|||
|
||||
fun isCryptoEnabled(): Boolean
|
||||
|
||||
fun getSasVerificationService(): SasVerificationService
|
||||
fun getVerificationService(): VerificationService
|
||||
|
||||
fun getCrossSigningService(): CrossSigningService
|
||||
|
||||
fun getKeysBackupService(): KeysBackupService
|
||||
|
||||
|
@ -54,15 +60,15 @@ interface CryptoService {
|
|||
|
||||
fun setWarnOnUnknownDevices(warn: Boolean)
|
||||
|
||||
fun setDeviceVerification(verificationStatus: Int, deviceId: String, userId: String)
|
||||
fun setDeviceVerification(trustLevel: DeviceTrustLevel, userId: String, deviceId: String)
|
||||
|
||||
fun getUserDevices(userId: String): MutableList<MXDeviceInfo>
|
||||
fun getUserDevices(userId: String): MutableList<CryptoDeviceInfo>
|
||||
|
||||
fun setDevicesKnown(devices: List<MXDeviceInfo>, callback: MatrixCallback<Unit>?)
|
||||
|
||||
fun deviceWithIdentityKey(senderKey: String, algorithm: String): MXDeviceInfo?
|
||||
fun deviceWithIdentityKey(senderKey: String, algorithm: String): CryptoDeviceInfo?
|
||||
|
||||
fun getMyDevice(): MXDeviceInfo
|
||||
fun getMyDevice(): CryptoDeviceInfo
|
||||
|
||||
fun getGlobalBlacklistUnverifiedDevices(): Boolean
|
||||
|
||||
|
@ -78,7 +84,7 @@ interface CryptoService {
|
|||
|
||||
fun setRoomBlacklistUnverifiedDevices(roomId: String)
|
||||
|
||||
fun getDeviceInfo(userId: String, deviceId: String?): MXDeviceInfo?
|
||||
fun getDeviceInfo(userId: String, deviceId: String?): CryptoDeviceInfo?
|
||||
|
||||
fun reRequestRoomKeyForEvent(event: Event)
|
||||
|
||||
|
@ -110,7 +116,11 @@ interface CryptoService {
|
|||
|
||||
fun shouldEncryptForInvitedMembers(roomId: String): Boolean
|
||||
|
||||
fun downloadKeys(userIds: List<String>, forceDownload: Boolean, callback: MatrixCallback<MXUsersDevicesMap<MXDeviceInfo>>)
|
||||
fun downloadKeys(userIds: List<String>, forceDownload: Boolean, callback: MatrixCallback<MXUsersDevicesMap<CryptoDeviceInfo>>)
|
||||
|
||||
fun getCryptoDeviceInfo(userId: String): List<CryptoDeviceInfo>
|
||||
|
||||
fun getLiveCryptoDeviceInfo(userId: String): LiveData<List<CryptoDeviceInfo>>
|
||||
|
||||
fun addNewSessionListener(newSessionListener: NewSessionListener)
|
||||
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
|
||||
package im.vector.matrix.android.api.session.crypto
|
||||
|
||||
import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo
|
||||
import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
|
||||
import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap
|
||||
import org.matrix.olm.OlmException
|
||||
|
||||
|
@ -36,7 +36,7 @@ sealed class MXCryptoError : Throwable() {
|
|||
|
||||
data class OlmError(val olmException: OlmException) : MXCryptoError()
|
||||
|
||||
data class UnknownDevice(val deviceList: MXUsersDevicesMap<MXDeviceInfo>) : MXCryptoError()
|
||||
data class UnknownDevice(val deviceList: MXUsersDevicesMap<CryptoDeviceInfo>) : MXCryptoError()
|
||||
|
||||
enum class ErrorType {
|
||||
ENCRYPTING_NOT_ENABLED,
|
||||
|
|
|
@ -0,0 +1,65 @@
|
|||
/*
|
||||
* 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 androidx.lifecycle.LiveData
|
||||
import im.vector.matrix.android.api.MatrixCallback
|
||||
import im.vector.matrix.android.api.util.Optional
|
||||
import im.vector.matrix.android.internal.crypto.crosssigning.DeviceTrustResult
|
||||
import im.vector.matrix.android.internal.crypto.crosssigning.UserTrustResult
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.UserPasswordAuth
|
||||
|
||||
interface CrossSigningService {
|
||||
|
||||
fun isCrossSigningEnabled(): Boolean
|
||||
|
||||
fun isUserTrusted(otherUserId: String): Boolean
|
||||
|
||||
/**
|
||||
* Will not force a download of the key, but will verify signatures trust chain.
|
||||
* Checks that my trusted user key has signed the other user UserKey
|
||||
*/
|
||||
fun checkUserTrust(otherUserId: String): UserTrustResult
|
||||
|
||||
/**
|
||||
* Initialize cross signing for this user.
|
||||
* Users needs to enter credentials
|
||||
*/
|
||||
fun initializeCrossSigning(authParams: UserPasswordAuth?,
|
||||
callback: MatrixCallback<Unit>? = null)
|
||||
|
||||
fun getUserCrossSigningKeys(otherUserId: String): MXCrossSigningInfo?
|
||||
|
||||
fun getLiveCrossSigningKeys(userId: String): LiveData<Optional<MXCrossSigningInfo>>
|
||||
|
||||
fun getMyCrossSigningKeys(): MXCrossSigningInfo?
|
||||
|
||||
fun canCrossSign(): Boolean
|
||||
|
||||
fun trustUser(otherUserId: String,
|
||||
callback: MatrixCallback<Unit>)
|
||||
|
||||
/**
|
||||
* Sign one of your devices and upload the signature
|
||||
*/
|
||||
fun signDevice(deviceId: String,
|
||||
callback: MatrixCallback<Unit>)
|
||||
|
||||
fun checkDeviceTrust(otherUserId: String,
|
||||
otherDeviceId: String,
|
||||
locallyTrusted: Boolean?): DeviceTrustResult
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
* 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.CryptoCrossSigningKey
|
||||
import im.vector.matrix.android.internal.crypto.model.KeyUsage
|
||||
|
||||
data class MXCrossSigningInfo(
|
||||
|
||||
var userId: String,
|
||||
|
||||
var crossSigningKeys: List<CryptoCrossSigningKey> = ArrayList()
|
||||
|
||||
) {
|
||||
|
||||
fun isTrusted() : Boolean = masterKey()?.trustLevel?.isVerified() == true
|
||||
&& selfSigningKey()?.trustLevel?.isVerified() == true
|
||||
|
||||
fun masterKey(): CryptoCrossSigningKey? = crossSigningKeys
|
||||
.firstOrNull { it.usages?.contains(KeyUsage.MASTER.value) == true }
|
||||
|
||||
fun userKey(): CryptoCrossSigningKey? = crossSigningKeys
|
||||
.firstOrNull { it.usages?.contains(KeyUsage.USER_SIGNING.value) == true }
|
||||
|
||||
fun selfSigningKey(): CryptoCrossSigningKey? = crossSigningKeys
|
||||
.firstOrNull { it.usages?.contains(KeyUsage.SELF_SIGNING.value) == true }
|
||||
}
|
|
@ -13,6 +13,8 @@
|
|||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
// TODO Rename package
|
||||
package im.vector.matrix.android.api.session.crypto.sas
|
||||
|
||||
enum class CancelCode(val value: String, val humanReadable: String) {
|
||||
|
@ -25,7 +27,9 @@ enum class CancelCode(val value: String, val humanReadable: String) {
|
|||
UnexpectedMessage("m.unexpected_message", "the device received an unexpected message"),
|
||||
InvalidMessage("m.invalid_message", "an invalid message was received"),
|
||||
MismatchedKeys("m.key_mismatch", "Key mismatch"),
|
||||
UserMismatchError("m.user_error", "User mismatch")
|
||||
UserError("m.user_error", "User error"),
|
||||
MismatchedUser("m.user_mismatch", "User mismatch"),
|
||||
QrCodeInvalid("m.qr_code.invalid", "Invalid QR code")
|
||||
}
|
||||
|
||||
fun safeValueOf(code: String?): CancelCode {
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
|
||||
package im.vector.matrix.android.api.session.crypto.sas
|
||||
|
||||
interface IncomingSasVerificationTransaction {
|
||||
interface IncomingSasVerificationTransaction : SasVerificationTransaction {
|
||||
val uxState: UxState
|
||||
|
||||
fun performAccept()
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
|
||||
package im.vector.matrix.android.api.session.crypto.sas
|
||||
|
||||
interface OutgoingSasVerificationRequest {
|
||||
interface OutgoingSasVerificationTransaction : SasVerificationTransaction {
|
||||
val uxState: UxState
|
||||
|
||||
enum class UxState {
|
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* 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.sas
|
||||
|
||||
interface QrCodeVerificationTransaction : VerificationTransaction {
|
||||
|
||||
/**
|
||||
* To use to display a qr code, for the other user to scan it
|
||||
*/
|
||||
val qrCodeText: String?
|
||||
|
||||
/**
|
||||
* Call when you have scan the other user QR code
|
||||
*/
|
||||
fun userHasScannedOtherQrCode(otherQrCodeText: String)
|
||||
}
|
|
@ -16,18 +16,7 @@
|
|||
|
||||
package im.vector.matrix.android.api.session.crypto.sas
|
||||
|
||||
interface SasVerificationTransaction {
|
||||
var state: SasVerificationTxState
|
||||
|
||||
val cancelledReason: CancelCode?
|
||||
|
||||
val transactionId: String
|
||||
|
||||
val otherUserId: String
|
||||
|
||||
var otherDeviceId: String?
|
||||
|
||||
val isIncoming: Boolean
|
||||
interface SasVerificationTransaction : VerificationTransaction {
|
||||
|
||||
fun supportsEmoji(): Boolean
|
||||
|
||||
|
@ -37,11 +26,6 @@ interface SasVerificationTransaction {
|
|||
|
||||
fun getDecimalCodeRepresentation(): String
|
||||
|
||||
/**
|
||||
* User wants to cancel the transaction
|
||||
*/
|
||||
fun cancel()
|
||||
|
||||
/**
|
||||
* To be called by the client when the user has verified that
|
||||
* both short codes do match
|
||||
|
|
|
@ -17,10 +17,13 @@
|
|||
package im.vector.matrix.android.api.session.crypto.sas
|
||||
|
||||
/**
|
||||
* Verification methods supported (or to be supported) by the matrix SDK
|
||||
* Verification methods
|
||||
*/
|
||||
enum class VerificationMethod {
|
||||
// Use it when your application supports the SAS verification method
|
||||
SAS,
|
||||
// Not supported yet
|
||||
SCAN
|
||||
// Use it if your application is able to display QR codes
|
||||
QR_CODE_SHOW,
|
||||
// Use it if your application is able to scan QR codes
|
||||
QR_CODE_SCAN
|
||||
}
|
||||
|
|
|
@ -17,44 +17,50 @@
|
|||
package im.vector.matrix.android.api.session.crypto.sas
|
||||
|
||||
import im.vector.matrix.android.api.MatrixCallback
|
||||
import im.vector.matrix.android.api.session.events.model.LocalEcho
|
||||
import im.vector.matrix.android.internal.crypto.verification.PendingVerificationRequest
|
||||
|
||||
/**
|
||||
* https://matrix.org/docs/spec/client_server/r0.5.0#key-verification-framework
|
||||
*
|
||||
* Verifying keys manually by reading out the Ed25519 key is not very user friendly, and can lead to errors.
|
||||
* SAS verification is a user-friendly key verification process.
|
||||
* SAS verification is intended to be a highly interactive process for users,
|
||||
* Verification is a user-friendly key verification process.
|
||||
* Verification is intended to be a highly interactive process for users,
|
||||
* and as such exposes verification methods which are easier for users to use.
|
||||
*/
|
||||
interface SasVerificationService {
|
||||
interface VerificationService {
|
||||
|
||||
fun addListener(listener: SasVerificationListener)
|
||||
fun addListener(listener: VerificationListener)
|
||||
|
||||
fun removeListener(listener: SasVerificationListener)
|
||||
fun removeListener(listener: VerificationListener)
|
||||
|
||||
/**
|
||||
* Mark this device as verified manually
|
||||
*/
|
||||
fun markedLocallyAsManuallyVerified(userId: String, deviceID: String)
|
||||
|
||||
fun getExistingTransaction(otherUser: String, tid: String): SasVerificationTransaction?
|
||||
fun getExistingTransaction(otherUserId: String, tid: String): VerificationTransaction?
|
||||
|
||||
fun getExistingVerificationRequest(otherUser: String): List<PendingVerificationRequest>?
|
||||
fun getExistingVerificationRequest(otherUserId: String): List<PendingVerificationRequest>?
|
||||
|
||||
fun getExistingVerificationRequest(otherUser: String, tid: String?): PendingVerificationRequest?
|
||||
fun getExistingVerificationRequest(otherUserId: String, tid: String?): PendingVerificationRequest?
|
||||
|
||||
fun getExistingVerificationRequestInRoom(roomId: String, tid: String?): PendingVerificationRequest?
|
||||
|
||||
fun beginKeyVerification(method: VerificationMethod, userId: String, deviceID: String): String?
|
||||
fun beginKeyVerification(method: VerificationMethod, otherUserId: String, otherDeviceID: String): String?
|
||||
|
||||
/**
|
||||
* Request a key verification from another user using toDevice events.
|
||||
*/
|
||||
fun requestKeyVerificationInDMs(methods: List<VerificationMethod>, userId: String, roomId: String): PendingVerificationRequest
|
||||
fun requestKeyVerificationInDMs(methods: List<VerificationMethod>,
|
||||
otherUserId: String,
|
||||
roomId: String,
|
||||
localId: String? = LocalEcho.createLocalEchoId()
|
||||
): PendingVerificationRequest
|
||||
|
||||
fun declineVerificationRequestInDMs(otherUserId: String, otherDeviceId: String, transactionId: String, roomId: String)
|
||||
|
||||
// Only SAS method is supported for the moment
|
||||
fun beginKeyVerificationInDMs(method: VerificationMethod,
|
||||
transactionId: String,
|
||||
roomId: String,
|
||||
|
@ -65,13 +71,16 @@ interface SasVerificationService {
|
|||
/**
|
||||
* Returns false if the request is unknown
|
||||
*/
|
||||
fun readyPendingVerificationInDMs(otherUserId: String, roomId: String, transactionId: String): Boolean
|
||||
fun readyPendingVerificationInDMs(methods: List<VerificationMethod>,
|
||||
otherUserId: String,
|
||||
roomId: String,
|
||||
transactionId: String): Boolean
|
||||
|
||||
// fun transactionUpdated(tx: SasVerificationTransaction)
|
||||
|
||||
interface SasVerificationListener {
|
||||
fun transactionCreated(tx: SasVerificationTransaction)
|
||||
fun transactionUpdated(tx: SasVerificationTransaction)
|
||||
interface VerificationListener {
|
||||
fun transactionCreated(tx: VerificationTransaction)
|
||||
fun transactionUpdated(tx: VerificationTransaction)
|
||||
fun markedAsManuallyVerified(userId: String, deviceId: String) {}
|
||||
|
||||
fun verificationRequestCreated(pr: PendingVerificationRequest) {}
|
|
@ -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.sas
|
||||
|
||||
interface VerificationTransaction {
|
||||
|
||||
var state: VerificationTxState
|
||||
|
||||
val cancelledReason: CancelCode?
|
||||
val transactionId: String
|
||||
val otherUserId: String
|
||||
var otherDeviceId: String?
|
||||
|
||||
// TODO Not used. Remove?
|
||||
val isIncoming: Boolean
|
||||
/**
|
||||
* User wants to cancel the transaction
|
||||
*/
|
||||
fun cancel()
|
||||
|
||||
fun cancel(code: CancelCode)
|
||||
|
||||
fun isToDeviceTransport(): Boolean
|
||||
}
|
|
@ -16,7 +16,7 @@
|
|||
|
||||
package im.vector.matrix.android.api.session.crypto.sas
|
||||
|
||||
enum class SasVerificationTxState {
|
||||
enum class VerificationTxState {
|
||||
None,
|
||||
// I have started a verification request
|
||||
SendingStart,
|
||||
|
@ -44,6 +44,8 @@ enum class SasVerificationTxState {
|
|||
Verified,
|
||||
|
||||
// Global: The verification has been cancelled (by me or other), see cancelReason for details
|
||||
// When I do the cancel
|
||||
Cancelled,
|
||||
// When the other user do a cancel
|
||||
OnCancelled
|
||||
}
|
|
@ -20,8 +20,9 @@ import com.squareup.moshi.JsonClass
|
|||
import im.vector.matrix.android.api.session.crypto.sas.SasMode
|
||||
import im.vector.matrix.android.api.session.events.model.toContent
|
||||
import im.vector.matrix.android.api.session.room.model.relation.RelationDefaultContent
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.supportedVerificationMethods
|
||||
import im.vector.matrix.android.internal.crypto.verification.SASVerificationTransaction
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.VERIFICATION_METHOD_RECIPROCATE
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.VERIFICATION_METHOD_SAS
|
||||
import im.vector.matrix.android.internal.crypto.verification.SASDefaultVerificationTransaction
|
||||
import im.vector.matrix.android.internal.crypto.verification.VerificationInfoStart
|
||||
import im.vector.matrix.android.internal.util.JsonCanonicalizer
|
||||
import timber.log.Timber
|
||||
|
@ -34,7 +35,8 @@ internal data class MessageVerificationStartContent(
|
|||
@Json(name = "message_authentication_codes") override val messageAuthenticationCodes: List<String>?,
|
||||
@Json(name = "short_authentication_string") override val shortAuthenticationStrings: List<String>?,
|
||||
@Json(name = "method") override val method: String?,
|
||||
@Json(name = "m.relates_to") val relatesTo: RelationDefaultContent?
|
||||
@Json(name = "m.relates_to") val relatesTo: RelationDefaultContent?,
|
||||
@Json(name = "secret") override val sharedSecret: String?
|
||||
) : VerificationInfoStart {
|
||||
|
||||
override fun toCanonicalJson(): String? {
|
||||
|
@ -44,22 +46,39 @@ internal data class MessageVerificationStartContent(
|
|||
override val transactionID: String?
|
||||
get() = relatesTo?.eventId
|
||||
|
||||
// TODO Move those method to the interface?
|
||||
override fun isValid(): Boolean {
|
||||
if (transactionID.isNullOrBlank()
|
||||
|| fromDevice.isNullOrBlank()
|
||||
|| method !in supportedVerificationMethods
|
||||
|| keyAgreementProtocols.isNullOrEmpty()
|
||||
|| hashes.isNullOrEmpty()
|
||||
|| !hashes.contains("sha256") || messageAuthenticationCodes.isNullOrEmpty()
|
||||
|| (!messageAuthenticationCodes.contains(SASVerificationTransaction.SAS_MAC_SHA256)
|
||||
&& !messageAuthenticationCodes.contains(SASVerificationTransaction.SAS_MAC_SHA256_LONGKDF))
|
||||
|| shortAuthenticationStrings.isNullOrEmpty()
|
||||
|| !shortAuthenticationStrings.contains(SasMode.DECIMAL)) {
|
||||
|| (method == VERIFICATION_METHOD_SAS && !isValidSas())
|
||||
|| (method == VERIFICATION_METHOD_RECIPROCATE && !isValidReciprocate())) {
|
||||
Timber.e("## received invalid verification request")
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private fun isValidSas(): Boolean {
|
||||
if (keyAgreementProtocols.isNullOrEmpty()
|
||||
|| hashes.isNullOrEmpty()
|
||||
|| !hashes.contains("sha256") || messageAuthenticationCodes.isNullOrEmpty()
|
||||
|| (!messageAuthenticationCodes.contains(SASDefaultVerificationTransaction.SAS_MAC_SHA256)
|
||||
&& !messageAuthenticationCodes.contains(SASDefaultVerificationTransaction.SAS_MAC_SHA256_LONGKDF))
|
||||
|| shortAuthenticationStrings.isNullOrEmpty()
|
||||
|| !shortAuthenticationStrings.contains(SasMode.DECIMAL)) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
private fun isValidReciprocate(): Boolean {
|
||||
if (sharedSecret.isNullOrBlank()) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun toEventContent() = toContent()
|
||||
}
|
||||
|
|
|
@ -30,3 +30,7 @@ const val MXCRYPTO_ALGORITHM_MEGOLM = "m.megolm.v1.aes-sha2"
|
|||
* Matrix algorithm value for megolm keys backup.
|
||||
*/
|
||||
const val MXCRYPTO_ALGORITHM_MEGOLM_BACKUP = "m.megolm_backup.v1.curve25519-aes-sha2"
|
||||
|
||||
// TODO Refacto: use this constants everywhere
|
||||
const val ed25519 = "ed25519"
|
||||
const val curve25519 = "curve25519"
|
||||
|
|
|
@ -21,14 +21,68 @@ import dagger.Module
|
|||
import dagger.Provides
|
||||
import im.vector.matrix.android.api.auth.data.Credentials
|
||||
import im.vector.matrix.android.api.session.crypto.CryptoService
|
||||
import im.vector.matrix.android.api.session.crypto.crosssigning.CrossSigningService
|
||||
import im.vector.matrix.android.internal.crypto.api.CryptoApi
|
||||
import im.vector.matrix.android.internal.crypto.crosssigning.DefaultCrossSigningService
|
||||
import im.vector.matrix.android.internal.crypto.keysbackup.api.RoomKeysApi
|
||||
import im.vector.matrix.android.internal.crypto.keysbackup.tasks.*
|
||||
import im.vector.matrix.android.internal.crypto.keysbackup.tasks.CreateKeysBackupVersionTask
|
||||
import im.vector.matrix.android.internal.crypto.keysbackup.tasks.DefaultCreateKeysBackupVersionTask
|
||||
import im.vector.matrix.android.internal.crypto.keysbackup.tasks.DefaultDeleteBackupTask
|
||||
import im.vector.matrix.android.internal.crypto.keysbackup.tasks.DefaultDeleteRoomSessionDataTask
|
||||
import im.vector.matrix.android.internal.crypto.keysbackup.tasks.DefaultDeleteRoomSessionsDataTask
|
||||
import im.vector.matrix.android.internal.crypto.keysbackup.tasks.DefaultDeleteSessionsDataTask
|
||||
import im.vector.matrix.android.internal.crypto.keysbackup.tasks.DefaultGetKeysBackupLastVersionTask
|
||||
import im.vector.matrix.android.internal.crypto.keysbackup.tasks.DefaultGetKeysBackupVersionTask
|
||||
import im.vector.matrix.android.internal.crypto.keysbackup.tasks.DefaultGetRoomSessionDataTask
|
||||
import im.vector.matrix.android.internal.crypto.keysbackup.tasks.DefaultGetRoomSessionsDataTask
|
||||
import im.vector.matrix.android.internal.crypto.keysbackup.tasks.DefaultGetSessionsDataTask
|
||||
import im.vector.matrix.android.internal.crypto.keysbackup.tasks.DefaultStoreRoomSessionDataTask
|
||||
import im.vector.matrix.android.internal.crypto.keysbackup.tasks.DefaultStoreRoomSessionsDataTask
|
||||
import im.vector.matrix.android.internal.crypto.keysbackup.tasks.DefaultStoreSessionsDataTask
|
||||
import im.vector.matrix.android.internal.crypto.keysbackup.tasks.DefaultUpdateKeysBackupVersionTask
|
||||
import im.vector.matrix.android.internal.crypto.keysbackup.tasks.DeleteBackupTask
|
||||
import im.vector.matrix.android.internal.crypto.keysbackup.tasks.DeleteRoomSessionDataTask
|
||||
import im.vector.matrix.android.internal.crypto.keysbackup.tasks.DeleteRoomSessionsDataTask
|
||||
import im.vector.matrix.android.internal.crypto.keysbackup.tasks.DeleteSessionsDataTask
|
||||
import im.vector.matrix.android.internal.crypto.keysbackup.tasks.GetKeysBackupLastVersionTask
|
||||
import im.vector.matrix.android.internal.crypto.keysbackup.tasks.GetKeysBackupVersionTask
|
||||
import im.vector.matrix.android.internal.crypto.keysbackup.tasks.GetRoomSessionDataTask
|
||||
import im.vector.matrix.android.internal.crypto.keysbackup.tasks.GetRoomSessionsDataTask
|
||||
import im.vector.matrix.android.internal.crypto.keysbackup.tasks.GetSessionsDataTask
|
||||
import im.vector.matrix.android.internal.crypto.keysbackup.tasks.StoreRoomSessionDataTask
|
||||
import im.vector.matrix.android.internal.crypto.keysbackup.tasks.StoreRoomSessionsDataTask
|
||||
import im.vector.matrix.android.internal.crypto.keysbackup.tasks.StoreSessionsDataTask
|
||||
import im.vector.matrix.android.internal.crypto.keysbackup.tasks.UpdateKeysBackupVersionTask
|
||||
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
|
||||
import im.vector.matrix.android.internal.crypto.store.db.RealmCryptoStore
|
||||
import im.vector.matrix.android.internal.crypto.store.db.RealmCryptoStoreMigration
|
||||
import im.vector.matrix.android.internal.crypto.store.db.RealmCryptoStoreModule
|
||||
import im.vector.matrix.android.internal.crypto.tasks.*
|
||||
import im.vector.matrix.android.internal.crypto.tasks.ClaimOneTimeKeysForUsersDeviceTask
|
||||
import im.vector.matrix.android.internal.crypto.tasks.DefaultClaimOneTimeKeysForUsersDevice
|
||||
import im.vector.matrix.android.internal.crypto.tasks.DefaultDeleteDeviceTask
|
||||
import im.vector.matrix.android.internal.crypto.tasks.DefaultDeleteDeviceWithUserPasswordTask
|
||||
import im.vector.matrix.android.internal.crypto.tasks.DefaultDownloadKeysForUsers
|
||||
import im.vector.matrix.android.internal.crypto.tasks.DefaultEncryptEventTask
|
||||
import im.vector.matrix.android.internal.crypto.tasks.DefaultGetDeviceInfoTask
|
||||
import im.vector.matrix.android.internal.crypto.tasks.DefaultGetDevicesTask
|
||||
import im.vector.matrix.android.internal.crypto.tasks.DefaultSendToDeviceTask
|
||||
import im.vector.matrix.android.internal.crypto.tasks.DefaultSendVerificationMessageTask
|
||||
import im.vector.matrix.android.internal.crypto.tasks.DefaultSetDeviceNameTask
|
||||
import im.vector.matrix.android.internal.crypto.tasks.DefaultUploadKeysTask
|
||||
import im.vector.matrix.android.internal.crypto.tasks.DefaultUploadSignaturesTask
|
||||
import im.vector.matrix.android.internal.crypto.tasks.DefaultUploadSigningKeysTask
|
||||
import im.vector.matrix.android.internal.crypto.tasks.DeleteDeviceTask
|
||||
import im.vector.matrix.android.internal.crypto.tasks.DeleteDeviceWithUserPasswordTask
|
||||
import im.vector.matrix.android.internal.crypto.tasks.DownloadKeysForUsersTask
|
||||
import im.vector.matrix.android.internal.crypto.tasks.EncryptEventTask
|
||||
import im.vector.matrix.android.internal.crypto.tasks.GetDeviceInfoTask
|
||||
import im.vector.matrix.android.internal.crypto.tasks.GetDevicesTask
|
||||
import im.vector.matrix.android.internal.crypto.tasks.SendToDeviceTask
|
||||
import im.vector.matrix.android.internal.crypto.tasks.SendVerificationMessageTask
|
||||
import im.vector.matrix.android.internal.crypto.tasks.SetDeviceNameTask
|
||||
import im.vector.matrix.android.internal.crypto.tasks.UploadKeysTask
|
||||
import im.vector.matrix.android.internal.crypto.tasks.UploadSignaturesTask
|
||||
import im.vector.matrix.android.internal.crypto.tasks.UploadSigningKeysTask
|
||||
import im.vector.matrix.android.internal.database.RealmKeysUtils
|
||||
import im.vector.matrix.android.internal.di.CryptoDatabase
|
||||
import im.vector.matrix.android.internal.di.SessionFilesDirectory
|
||||
|
@ -132,6 +186,12 @@ internal abstract class CryptoModule {
|
|||
@Binds
|
||||
abstract fun bindUploadKeysTask(uploadKeysTask: DefaultUploadKeysTask): UploadKeysTask
|
||||
|
||||
@Binds
|
||||
abstract fun bindUploadSigningKeysTask(uploadKeysTask: DefaultUploadSigningKeysTask): UploadSigningKeysTask
|
||||
|
||||
@Binds
|
||||
abstract fun bindUploadSignaturesTask(uploadSignaturesTask: DefaultUploadSignaturesTask): UploadSignaturesTask
|
||||
|
||||
@Binds
|
||||
abstract fun bindDownloadKeysForUsersTask(downloadKeysForUsersTask: DefaultDownloadKeysForUsers): DownloadKeysForUsersTask
|
||||
|
||||
|
@ -193,4 +253,7 @@ internal abstract class CryptoModule {
|
|||
@Binds
|
||||
abstract fun bindDeleteDeviceWithUserPasswordTask(deleteDeviceWithUserPasswordTask: DefaultDeleteDeviceWithUserPasswordTask)
|
||||
: DeleteDeviceWithUserPasswordTask
|
||||
|
||||
@Binds
|
||||
abstract fun bindCrossSigningService(crossSigningService: DefaultCrossSigningService): CrossSigningService
|
||||
}
|
||||
|
|
|
@ -21,6 +21,7 @@ package im.vector.matrix.android.internal.crypto
|
|||
import android.content.Context
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import androidx.lifecycle.LiveData
|
||||
import com.squareup.moshi.Types
|
||||
import com.zhuinden.monarchy.Monarchy
|
||||
import dagger.Lazy
|
||||
|
@ -44,7 +45,10 @@ import im.vector.matrix.android.internal.crypto.actions.SetDeviceVerificationAct
|
|||
import im.vector.matrix.android.internal.crypto.algorithms.IMXEncrypting
|
||||
import im.vector.matrix.android.internal.crypto.algorithms.megolm.MXMegolmEncryptionFactory
|
||||
import im.vector.matrix.android.internal.crypto.algorithms.olm.MXOlmEncryptionFactory
|
||||
import im.vector.matrix.android.internal.crypto.crosssigning.DefaultCrossSigningService
|
||||
import im.vector.matrix.android.internal.crypto.crosssigning.DeviceTrustLevel
|
||||
import im.vector.matrix.android.internal.crypto.keysbackup.KeysBackup
|
||||
import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
|
||||
import im.vector.matrix.android.internal.crypto.model.ImportRoomKeysResult
|
||||
import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo
|
||||
import im.vector.matrix.android.internal.crypto.model.MXEncryptEventContentResult
|
||||
|
@ -54,10 +58,16 @@ import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
|
|||
import im.vector.matrix.android.internal.crypto.model.rest.DevicesListResponse
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.KeysUploadResponse
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody
|
||||
import im.vector.matrix.android.internal.crypto.model.toRest
|
||||
import im.vector.matrix.android.internal.crypto.repository.WarnOnUnknownDeviceRepository
|
||||
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
|
||||
import im.vector.matrix.android.internal.crypto.tasks.*
|
||||
import im.vector.matrix.android.internal.crypto.verification.DefaultSasVerificationService
|
||||
import im.vector.matrix.android.internal.crypto.tasks.DeleteDeviceTask
|
||||
import im.vector.matrix.android.internal.crypto.tasks.DeleteDeviceWithUserPasswordTask
|
||||
import im.vector.matrix.android.internal.crypto.tasks.GetDeviceInfoTask
|
||||
import im.vector.matrix.android.internal.crypto.tasks.GetDevicesTask
|
||||
import im.vector.matrix.android.internal.crypto.tasks.SetDeviceNameTask
|
||||
import im.vector.matrix.android.internal.crypto.tasks.UploadKeysTask
|
||||
import im.vector.matrix.android.internal.crypto.verification.DefaultVerificationService
|
||||
import im.vector.matrix.android.internal.database.model.EventEntity
|
||||
import im.vector.matrix.android.internal.database.query.where
|
||||
import im.vector.matrix.android.internal.di.MoshiProvider
|
||||
|
@ -71,7 +81,12 @@ import im.vector.matrix.android.internal.task.configureWith
|
|||
import im.vector.matrix.android.internal.util.JsonCanonicalizer
|
||||
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
|
||||
import im.vector.matrix.android.internal.util.fetchCopied
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.cancelChildren
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.matrix.olm.OlmManager
|
||||
import timber.log.Timber
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
|
@ -111,8 +126,10 @@ internal class DefaultCryptoService @Inject constructor(
|
|||
private val oneTimeKeysUploader: OneTimeKeysUploader,
|
||||
//
|
||||
private val roomDecryptorProvider: RoomDecryptorProvider,
|
||||
// The SAS verification service.
|
||||
private val sasVerificationService: DefaultSasVerificationService,
|
||||
// The verification service.
|
||||
private val verificationService: DefaultVerificationService,
|
||||
|
||||
private val crossSigningService: DefaultCrossSigningService,
|
||||
//
|
||||
private val incomingRoomKeyRequestManager: IncomingRoomKeyRequestManager,
|
||||
//
|
||||
|
@ -139,7 +156,7 @@ internal class DefaultCryptoService @Inject constructor(
|
|||
) : CryptoService {
|
||||
|
||||
init {
|
||||
sasVerificationService.cryptoService = this
|
||||
verificationService.cryptoService = this
|
||||
}
|
||||
|
||||
private val uiHandler = Handler(Looper.getMainLooper())
|
||||
|
@ -168,7 +185,17 @@ internal class DefaultCryptoService @Inject constructor(
|
|||
override fun setDeviceName(deviceId: String, deviceName: String, callback: MatrixCallback<Unit>) {
|
||||
setDeviceNameTask
|
||||
.configureWith(SetDeviceNameTask.Params(deviceId, deviceName)) {
|
||||
this.callback = callback
|
||||
this.callback = object : MatrixCallback<Unit> {
|
||||
override fun onSuccess(data: Unit) {
|
||||
// bg refresh of crypto device
|
||||
downloadKeys(listOf(credentials.userId), true, object : MatrixCallback<MXUsersDevicesMap<CryptoDeviceInfo>> {})
|
||||
callback.onSuccess(data)
|
||||
}
|
||||
|
||||
override fun onFailure(failure: Throwable) {
|
||||
callback.onFailure(failure)
|
||||
}
|
||||
}
|
||||
}
|
||||
.executeBy(taskExecutor)
|
||||
}
|
||||
|
@ -193,7 +220,7 @@ internal class DefaultCryptoService @Inject constructor(
|
|||
return if (longFormat) olmManager.getDetailedVersion(context) else olmManager.version
|
||||
}
|
||||
|
||||
override fun getMyDevice(): MXDeviceInfo {
|
||||
override fun getMyDevice(): CryptoDeviceInfo {
|
||||
return myDeviceInfoHolder.get().myDevice
|
||||
}
|
||||
|
||||
|
@ -313,9 +340,11 @@ internal class DefaultCryptoService @Inject constructor(
|
|||
override fun getKeysBackupService() = keysBackup
|
||||
|
||||
/**
|
||||
* @return the SasVerificationService
|
||||
* @return the VerificationService
|
||||
*/
|
||||
override fun getSasVerificationService() = sasVerificationService
|
||||
override fun getVerificationService() = verificationService
|
||||
|
||||
override fun getCrossSigningService() = crossSigningService
|
||||
|
||||
/**
|
||||
* A sync response has been received
|
||||
|
@ -349,7 +378,7 @@ internal class DefaultCryptoService @Inject constructor(
|
|||
* @param algorithm the encryption algorithm.
|
||||
* @return the device info, or null if not found / unsupported algorithm / crypto released
|
||||
*/
|
||||
override fun deviceWithIdentityKey(senderKey: String, algorithm: String): MXDeviceInfo? {
|
||||
override fun deviceWithIdentityKey(senderKey: String, algorithm: String): CryptoDeviceInfo? {
|
||||
return if (algorithm != MXCRYPTO_ALGORITHM_MEGOLM && algorithm != MXCRYPTO_ALGORITHM_OLM) {
|
||||
// We only deal in olm keys
|
||||
null
|
||||
|
@ -362,13 +391,20 @@ internal class DefaultCryptoService @Inject constructor(
|
|||
* @param userId the user id
|
||||
* @param deviceId the device id
|
||||
*/
|
||||
override fun getDeviceInfo(userId: String, deviceId: String?): MXDeviceInfo? {
|
||||
override fun getDeviceInfo(userId: String, deviceId: String?): CryptoDeviceInfo? {
|
||||
return if (userId.isNotEmpty() && !deviceId.isNullOrEmpty()) {
|
||||
cryptoStore.getUserDevice(deviceId, userId)
|
||||
cryptoStore.getUserDevice(userId, deviceId)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
override fun getCryptoDeviceInfo(userId: String): List<CryptoDeviceInfo> {
|
||||
return cryptoStore.getUserDevices(userId)?.map { it.value } ?: emptyList()
|
||||
}
|
||||
|
||||
override fun getLiveCryptoDeviceInfo(userId: String): LiveData<List<CryptoDeviceInfo>> {
|
||||
return cryptoStore.getLiveDeviceList(userId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the devices as known
|
||||
|
@ -393,7 +429,7 @@ internal class DefaultCryptoService @Inject constructor(
|
|||
// assume if the device is either verified or blocked
|
||||
// it means that the device is known
|
||||
if (device?.isUnknown == true) {
|
||||
device.verified = MXDeviceInfo.DEVICE_VERIFICATION_UNVERIFIED
|
||||
device.trustLevel = DeviceTrustLevel(crossSigningVerified = false, locallyVerified = false)
|
||||
isUpdated = true
|
||||
}
|
||||
}
|
||||
|
@ -410,12 +446,12 @@ internal class DefaultCryptoService @Inject constructor(
|
|||
/**
|
||||
* Update the blocked/verified state of the given device.
|
||||
*
|
||||
* @param verificationStatus the new verification status
|
||||
* @param deviceId the unique identifier for the device.
|
||||
* @param trustLevel the new trust level
|
||||
* @param userId the owner of the device
|
||||
* @param deviceId the unique identifier for the device.
|
||||
*/
|
||||
override fun setDeviceVerification(verificationStatus: Int, deviceId: String, userId: String) {
|
||||
setDeviceVerificationAction.handle(verificationStatus, deviceId, userId)
|
||||
override fun setDeviceVerification(trustLevel: DeviceTrustLevel, userId: String, deviceId: String) {
|
||||
setDeviceVerificationAction.handle(trustLevel, userId, deviceId)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -494,9 +530,8 @@ internal class DefaultCryptoService @Inject constructor(
|
|||
/**
|
||||
* @return the stored device keys for a user.
|
||||
*/
|
||||
override fun getUserDevices(userId: String): MutableList<MXDeviceInfo> {
|
||||
val map = cryptoStore.getUserDevices(userId)
|
||||
return if (null != map) ArrayList(map.values) else ArrayList()
|
||||
override fun getUserDevices(userId: String): MutableList<CryptoDeviceInfo> {
|
||||
return cryptoStore.getUserDevices(userId)?.values?.toMutableList() ?: ArrayList()
|
||||
}
|
||||
|
||||
fun isEncryptionEnabledForInvitedUser(): Boolean {
|
||||
|
@ -758,11 +793,15 @@ internal class DefaultCryptoService @Inject constructor(
|
|||
// Prepare the device keys data to send
|
||||
// Sign it
|
||||
val canonicalJson = JsonCanonicalizer.getCanonicalJson(Map::class.java, getMyDevice().signalableJSONDictionary())
|
||||
getMyDevice().signatures = objectSigner.signObject(canonicalJson)
|
||||
var rest = getMyDevice().toRest()
|
||||
|
||||
rest = rest.copy(
|
||||
signatures = objectSigner.signObject(canonicalJson)
|
||||
)
|
||||
|
||||
// For now, we set the device id explicitly, as we may not be using the
|
||||
// same one as used in login.
|
||||
val uploadDeviceKeysParams = UploadKeysTask.Params(getMyDevice().toDeviceKeys(), null, getMyDevice().deviceId)
|
||||
val uploadDeviceKeysParams = UploadKeysTask.Params(rest, null, getMyDevice().deviceId)
|
||||
return uploadKeysTask.execute(uploadDeviceKeysParams)
|
||||
}
|
||||
|
||||
|
@ -1002,8 +1041,8 @@ internal class DefaultCryptoService @Inject constructor(
|
|||
* @param devicesInRoom the devices map
|
||||
* @return the unknown devices map
|
||||
*/
|
||||
private fun getUnknownDevices(devicesInRoom: MXUsersDevicesMap<MXDeviceInfo>): MXUsersDevicesMap<MXDeviceInfo> {
|
||||
val unknownDevices = MXUsersDevicesMap<MXDeviceInfo>()
|
||||
private fun getUnknownDevices(devicesInRoom: MXUsersDevicesMap<CryptoDeviceInfo>): MXUsersDevicesMap<CryptoDeviceInfo> {
|
||||
val unknownDevices = MXUsersDevicesMap<CryptoDeviceInfo>()
|
||||
val userIds = devicesInRoom.userIds
|
||||
for (userId in userIds) {
|
||||
devicesInRoom.getUserDeviceIds(userId)?.forEach { deviceId ->
|
||||
|
@ -1018,7 +1057,7 @@ internal class DefaultCryptoService @Inject constructor(
|
|||
return unknownDevices
|
||||
}
|
||||
|
||||
override fun downloadKeys(userIds: List<String>, forceDownload: Boolean, callback: MatrixCallback<MXUsersDevicesMap<MXDeviceInfo>>) {
|
||||
override fun downloadKeys(userIds: List<String>, forceDownload: Boolean, callback: MatrixCallback<MXUsersDevicesMap<CryptoDeviceInfo>>) {
|
||||
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
|
||||
runCatching {
|
||||
deviceListManager.downloadKeys(userIds, forceDownload)
|
||||
|
|
|
@ -19,12 +19,15 @@ package im.vector.matrix.android.internal.crypto
|
|||
|
||||
import im.vector.matrix.android.api.MatrixPatterns
|
||||
import im.vector.matrix.android.api.auth.data.Credentials
|
||||
import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo
|
||||
import im.vector.matrix.android.internal.crypto.crosssigning.DeviceTrustLevel
|
||||
import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
|
||||
import im.vector.matrix.android.internal.crypto.model.CryptoInfoMapper
|
||||
import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap
|
||||
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
|
||||
import im.vector.matrix.android.internal.crypto.tasks.DownloadKeysForUsersTask
|
||||
import im.vector.matrix.android.internal.session.SessionScope
|
||||
import im.vector.matrix.android.internal.session.sync.SyncTokenStore
|
||||
import okhttp3.internal.toImmutableList
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
|
@ -36,6 +39,30 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
|
|||
private val credentials: Credentials,
|
||||
private val downloadKeysForUsersTask: DownloadKeysForUsersTask) {
|
||||
|
||||
interface UserDevicesUpdateListener {
|
||||
fun onUsersDeviceUpdate(users: List<String>)
|
||||
}
|
||||
|
||||
private val deviceChangeListeners = ArrayList<UserDevicesUpdateListener>()
|
||||
|
||||
fun addListener(listener: UserDevicesUpdateListener) {
|
||||
deviceChangeListeners.add(listener)
|
||||
}
|
||||
|
||||
fun removeListener(listener: UserDevicesUpdateListener) {
|
||||
deviceChangeListeners.remove(listener)
|
||||
}
|
||||
|
||||
fun dispatchDeviceChange(users: List<String>) {
|
||||
deviceChangeListeners.forEach {
|
||||
try {
|
||||
it.onUsersDeviceUpdate(users)
|
||||
} catch (failure: Throwable) {
|
||||
Timber.e(failure, "Failed to dispatch device chande")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// HS not ready for retry
|
||||
private val notReadyToRetryHS = mutableSetOf<String>()
|
||||
|
||||
|
@ -166,7 +193,7 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
|
|||
* @param userIds the userIds list
|
||||
* @param failures the failure map.
|
||||
*/
|
||||
private fun onKeysDownloadSucceed(userIds: List<String>, failures: Map<String, Map<String, Any>>?): MXUsersDevicesMap<MXDeviceInfo> {
|
||||
private fun onKeysDownloadSucceed(userIds: List<String>, failures: Map<String, Map<String, Any>>?): MXUsersDevicesMap<CryptoDeviceInfo> {
|
||||
if (failures != null) {
|
||||
for ((k, value) in failures) {
|
||||
val statusCode = when (val status = value["status"]) {
|
||||
|
@ -182,7 +209,7 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
|
|||
}
|
||||
}
|
||||
val deviceTrackingStatuses = cryptoStore.getDeviceTrackingStatuses().toMutableMap()
|
||||
val usersDevicesInfoMap = MXUsersDevicesMap<MXDeviceInfo>()
|
||||
val usersDevicesInfoMap = MXUsersDevicesMap<CryptoDeviceInfo>()
|
||||
for (userId in userIds) {
|
||||
val devices = cryptoStore.getUserDevices(userId)
|
||||
if (null == devices) {
|
||||
|
@ -207,6 +234,8 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
|
|||
}
|
||||
}
|
||||
cryptoStore.saveDeviceTrackingStatuses(deviceTrackingStatuses)
|
||||
|
||||
dispatchDeviceChange(userIds.toImmutableList())
|
||||
return usersDevicesInfoMap
|
||||
}
|
||||
|
||||
|
@ -217,10 +246,10 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
|
|||
* @param userIds The users to fetch.
|
||||
* @param forceDownload Always download the keys even if cached.
|
||||
*/
|
||||
suspend fun downloadKeys(userIds: List<String>?, forceDownload: Boolean): MXUsersDevicesMap<MXDeviceInfo> {
|
||||
suspend fun downloadKeys(userIds: List<String>?, forceDownload: Boolean): MXUsersDevicesMap<CryptoDeviceInfo> {
|
||||
Timber.v("## downloadKeys() : forceDownload $forceDownload : $userIds")
|
||||
// Map from userId -> deviceId -> MXDeviceInfo
|
||||
val stored = MXUsersDevicesMap<MXDeviceInfo>()
|
||||
val stored = MXUsersDevicesMap<CryptoDeviceInfo>()
|
||||
|
||||
// List of user ids we need to download keys for
|
||||
val downloadUsers = ArrayList<String>()
|
||||
|
@ -265,7 +294,7 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
|
|||
*
|
||||
* @param downloadUsers the user ids list
|
||||
*/
|
||||
private suspend fun doKeyDownloadForUsers(downloadUsers: MutableList<String>): MXUsersDevicesMap<MXDeviceInfo> {
|
||||
private suspend fun doKeyDownloadForUsers(downloadUsers: MutableList<String>): MXUsersDevicesMap<CryptoDeviceInfo> {
|
||||
Timber.v("## doKeyDownloadForUsers() : doKeyDownloadForUsers $downloadUsers")
|
||||
// get the user ids which did not already trigger a keys download
|
||||
val filteredUsers = downloadUsers.filter { MatrixPatterns.isUserId(it) }
|
||||
|
@ -283,39 +312,62 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
|
|||
}
|
||||
Timber.v("## doKeyDownloadForUsers() : Got keys for " + filteredUsers.size + " users")
|
||||
for (userId in filteredUsers) {
|
||||
val devices = response.deviceKeys?.get(userId)
|
||||
Timber.v("## doKeyDownloadForUsers() : Got keys for $userId : $devices")
|
||||
if (devices != null) {
|
||||
val mutableDevices = devices.toMutableMap()
|
||||
for ((deviceId, deviceInfo) in devices) {
|
||||
// al devices =
|
||||
val models = response.deviceKeys?.get(userId)?.mapValues { entry -> CryptoInfoMapper.map(entry.value) }
|
||||
?.toMutableMap()
|
||||
|
||||
Timber.v("## doKeyDownloadForUsers() : Got keys for $userId : $models")
|
||||
if (!models.isNullOrEmpty()) {
|
||||
for ((deviceId, deviceInfo) in models) {
|
||||
// Get the potential previously store device keys for this device
|
||||
val previouslyStoredDeviceKeys = cryptoStore.getUserDevice(deviceId, userId)
|
||||
val previouslyStoredDeviceKeys = cryptoStore.getUserDevice(userId, deviceId)
|
||||
|
||||
// in some race conditions (like unit tests)
|
||||
// the self device must be seen as verified
|
||||
if (deviceInfo.deviceId == credentials.deviceId && userId == credentials.userId) {
|
||||
deviceInfo.verified = MXDeviceInfo.DEVICE_VERIFICATION_VERIFIED
|
||||
deviceInfo.trustLevel = DeviceTrustLevel(previouslyStoredDeviceKeys?.trustLevel?.crossSigningVerified ?: false, true)
|
||||
}
|
||||
// Validate received keys
|
||||
if (!validateDeviceKeys(deviceInfo, userId, deviceId, previouslyStoredDeviceKeys)) {
|
||||
// New device keys are not valid. Do not store them
|
||||
mutableDevices.remove(deviceId)
|
||||
models.remove(deviceId)
|
||||
if (null != previouslyStoredDeviceKeys) {
|
||||
// But keep old validated ones if any
|
||||
mutableDevices[deviceId] = previouslyStoredDeviceKeys
|
||||
models[deviceId] = previouslyStoredDeviceKeys
|
||||
}
|
||||
} else if (null != previouslyStoredDeviceKeys) {
|
||||
// The verified status is not sync'ed with hs.
|
||||
// This is a client side information, valid only for this client.
|
||||
// So, transfer its previous value
|
||||
mutableDevices[deviceId]!!.verified = previouslyStoredDeviceKeys.verified
|
||||
models[deviceId]!!.trustLevel = previouslyStoredDeviceKeys.trustLevel
|
||||
}
|
||||
}
|
||||
// Update the store
|
||||
// Note that devices which aren't in the response will be removed from the stores
|
||||
cryptoStore.storeUserDevices(userId, mutableDevices)
|
||||
cryptoStore.storeUserDevices(userId, models)
|
||||
}
|
||||
|
||||
// Handle cross signing keys update
|
||||
val masterKey = response.masterKeys?.get(userId)?.toCryptoModel().also {
|
||||
Timber.d("## CrossSigning : Got keys for $userId : MSK ${it?.unpaddedBase64PublicKey}")
|
||||
}
|
||||
val selfSigningKey = response.selfSigningKeys?.get(userId)?.toCryptoModel()?.also {
|
||||
Timber.d("## CrossSigning : Got keys for $userId : SSK ${it.unpaddedBase64PublicKey}")
|
||||
}
|
||||
val userSigningKey = response.userSigningKeys?.get(userId)?.toCryptoModel()?.also {
|
||||
Timber.d("## CrossSigning : Got keys for $userId : USK ${it.unpaddedBase64PublicKey}")
|
||||
}
|
||||
cryptoStore.storeUserCrossSigningKeys(
|
||||
userId,
|
||||
masterKey,
|
||||
selfSigningKey,
|
||||
userSigningKey
|
||||
)
|
||||
}
|
||||
|
||||
// Update devices trust for these users
|
||||
dispatchDeviceChange(downloadUsers)
|
||||
|
||||
return onKeysDownloadSucceed(filteredUsers, response.failures)
|
||||
}
|
||||
|
||||
|
@ -329,7 +381,7 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
|
|||
* @param previouslyStoredDeviceKeys the device keys we received before for this device
|
||||
* @return true if succeeds
|
||||
*/
|
||||
private fun validateDeviceKeys(deviceKeys: MXDeviceInfo?, userId: String, deviceId: String, previouslyStoredDeviceKeys: MXDeviceInfo?): Boolean {
|
||||
private fun validateDeviceKeys(deviceKeys: CryptoDeviceInfo?, userId: String, deviceId: String, previouslyStoredDeviceKeys: CryptoDeviceInfo?): Boolean {
|
||||
if (null == deviceKeys) {
|
||||
Timber.e("## validateDeviceKeys() : deviceKeys is null from $userId:$deviceId")
|
||||
return false
|
||||
|
@ -357,14 +409,14 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
|
|||
}
|
||||
|
||||
val signKeyId = "ed25519:" + deviceKeys.deviceId
|
||||
val signKey = deviceKeys.keys?.get(signKeyId)
|
||||
val signKey = deviceKeys.keys[signKeyId]
|
||||
|
||||
if (null == signKey) {
|
||||
Timber.e("## validateDeviceKeys() : Device $userId:${deviceKeys.deviceId} has no ed25519 key")
|
||||
return false
|
||||
}
|
||||
|
||||
val signatureMap = deviceKeys.signatures?.get(userId)
|
||||
val signatureMap = deviceKeys.signatures[userId]
|
||||
|
||||
if (null == signatureMap) {
|
||||
Timber.e("## validateDeviceKeys() : Device $userId:${deviceKeys.deviceId} has no map for $userId")
|
||||
|
|
|
@ -107,7 +107,7 @@ internal class IncomingRoomKeyRequestManager @Inject constructor(
|
|||
cryptoStore.deleteIncomingRoomKeyRequest(request)
|
||||
}
|
||||
// if the device is verified already, share the keys
|
||||
val device = cryptoStore.getUserDevice(deviceId!!, userId)
|
||||
val device = cryptoStore.getUserDevice(userId, deviceId!!)
|
||||
if (device != null) {
|
||||
if (device.isVerified) {
|
||||
Timber.v("## processReceivedRoomKeyRequests() : device is already verified: sharing keys")
|
||||
|
|
|
@ -141,6 +141,7 @@ internal class MXOlmDevice @Inject constructor(
|
|||
*/
|
||||
fun release() {
|
||||
olmAccount?.releaseAccount()
|
||||
olmUtility?.releaseUtility()
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -17,7 +17,8 @@
|
|||
package im.vector.matrix.android.internal.crypto
|
||||
|
||||
import im.vector.matrix.android.api.auth.data.Credentials
|
||||
import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo
|
||||
import im.vector.matrix.android.internal.crypto.crosssigning.DeviceTrustLevel
|
||||
import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
|
||||
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
|
||||
import im.vector.matrix.android.internal.session.SessionScope
|
||||
import javax.inject.Inject
|
||||
|
@ -35,11 +36,13 @@ internal class MyDeviceInfoHolder @Inject constructor(
|
|||
/**
|
||||
* my device info
|
||||
*/
|
||||
val myDevice: MXDeviceInfo = MXDeviceInfo(credentials.deviceId!!, credentials.userId)
|
||||
val myDevice: CryptoDeviceInfo
|
||||
|
||||
init {
|
||||
|
||||
val keys = HashMap<String, String>()
|
||||
|
||||
// TODO it's a bit strange, why not load from DB?
|
||||
if (!olmDevice.deviceEd25519Key.isNullOrEmpty()) {
|
||||
keys["ed25519:" + credentials.deviceId] = olmDevice.deviceEd25519Key!!
|
||||
}
|
||||
|
@ -48,10 +51,22 @@ internal class MyDeviceInfoHolder @Inject constructor(
|
|||
keys["curve25519:" + credentials.deviceId] = olmDevice.deviceCurve25519Key!!
|
||||
}
|
||||
|
||||
myDevice.keys = keys
|
||||
// myDevice.keys = keys
|
||||
//
|
||||
// myDevice.algorithms = MXCryptoAlgorithms.supportedAlgorithms()
|
||||
|
||||
myDevice.algorithms = MXCryptoAlgorithms.supportedAlgorithms()
|
||||
myDevice.verified = MXDeviceInfo.DEVICE_VERIFICATION_VERIFIED
|
||||
// TODO hwo to really check cross signed status?
|
||||
//
|
||||
val crossSigned = cryptoStore.getMyCrossSigningInfo()?.masterKey()?.trustLevel?.locallyVerified ?: false
|
||||
// myDevice.trustLevel = DeviceTrustLevel(crossSigned, true)
|
||||
|
||||
myDevice = CryptoDeviceInfo(
|
||||
credentials.deviceId!!,
|
||||
credentials.userId,
|
||||
keys = keys,
|
||||
algorithms = MXCryptoAlgorithms.supportedAlgorithms(),
|
||||
trustLevel = DeviceTrustLevel(crossSigned, true)
|
||||
)
|
||||
|
||||
// Add our own deviceinfo to the store
|
||||
val endToEndDevicesForUser = cryptoStore.getUserDevices(credentials.userId)
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
package im.vector.matrix.android.internal.crypto.actions
|
||||
|
||||
import im.vector.matrix.android.internal.crypto.MXOlmDevice
|
||||
import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo
|
||||
import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
|
||||
import im.vector.matrix.android.internal.crypto.model.MXKey
|
||||
import im.vector.matrix.android.internal.crypto.model.MXOlmSessionResult
|
||||
import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap
|
||||
|
@ -28,8 +28,8 @@ import javax.inject.Inject
|
|||
internal class EnsureOlmSessionsForDevicesAction @Inject constructor(private val olmDevice: MXOlmDevice,
|
||||
private val oneTimeKeysForUsersDeviceTask: ClaimOneTimeKeysForUsersDeviceTask) {
|
||||
|
||||
suspend fun handle(devicesByUser: Map<String, List<MXDeviceInfo>>): MXUsersDevicesMap<MXOlmSessionResult> {
|
||||
val devicesWithoutSession = ArrayList<MXDeviceInfo>()
|
||||
suspend fun handle(devicesByUser: Map<String, List<CryptoDeviceInfo>>): MXUsersDevicesMap<MXOlmSessionResult> {
|
||||
val devicesWithoutSession = ArrayList<CryptoDeviceInfo>()
|
||||
|
||||
val results = MXUsersDevicesMap<MXOlmSessionResult>()
|
||||
|
||||
|
@ -102,7 +102,7 @@ internal class EnsureOlmSessionsForDevicesAction @Inject constructor(private val
|
|||
return results
|
||||
}
|
||||
|
||||
private fun verifyKeyAndStartSession(oneTimeKey: MXKey, userId: String, deviceInfo: MXDeviceInfo): String? {
|
||||
private fun verifyKeyAndStartSession(oneTimeKey: MXKey, userId: String, deviceInfo: CryptoDeviceInfo): String? {
|
||||
var sessionId: String? = null
|
||||
|
||||
val deviceId = deviceInfo.deviceId
|
||||
|
|
|
@ -40,7 +40,7 @@ internal class EnsureOlmSessionsForUsersAction @Inject constructor(private val o
|
|||
// Don't bother setting up session to ourself
|
||||
it.identityKey() != olmDevice.deviceCurve25519Key
|
||||
// Don't bother setting up sessions with blocked users
|
||||
&& !it.isVerified
|
||||
&& !(it.trustLevel?.isVerified() ?: false)
|
||||
}
|
||||
}
|
||||
return ensureOlmSessionsForDevicesAction.handle(devicesByUser)
|
||||
|
|
|
@ -19,7 +19,7 @@ package im.vector.matrix.android.internal.crypto.actions
|
|||
import im.vector.matrix.android.api.auth.data.Credentials
|
||||
import im.vector.matrix.android.internal.crypto.MXCRYPTO_ALGORITHM_OLM
|
||||
import im.vector.matrix.android.internal.crypto.MXOlmDevice
|
||||
import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo
|
||||
import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.EncryptedMessage
|
||||
import im.vector.matrix.android.internal.util.JsonCanonicalizer
|
||||
import im.vector.matrix.android.internal.util.convertToUTF8
|
||||
|
@ -37,7 +37,7 @@ internal class MessageEncrypter @Inject constructor(private val credentials: Cre
|
|||
* @param deviceInfos list of device infos to encrypt for.
|
||||
* @return the content for an m.room.encrypted event.
|
||||
*/
|
||||
fun encryptMessage(payloadFields: Map<String, Any>, deviceInfos: List<MXDeviceInfo>): EncryptedMessage {
|
||||
fun encryptMessage(payloadFields: Map<String, Any>, deviceInfos: List<CryptoDeviceInfo>): EncryptedMessage {
|
||||
val deviceInfoParticipantKey = deviceInfos.associateBy { it.identityKey()!! }
|
||||
|
||||
val payloadJson = payloadFields.toMutableMap()
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
|
||||
package im.vector.matrix.android.internal.crypto.actions
|
||||
|
||||
import im.vector.matrix.android.internal.crypto.crosssigning.DeviceTrustLevel
|
||||
import im.vector.matrix.android.internal.crypto.keysbackup.KeysBackup
|
||||
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
|
||||
import im.vector.matrix.android.internal.di.UserId
|
||||
|
@ -27,8 +28,8 @@ internal class SetDeviceVerificationAction @Inject constructor(
|
|||
@UserId private val userId: String,
|
||||
private val keysBackup: KeysBackup) {
|
||||
|
||||
fun handle(verificationStatus: Int, deviceId: String, userId: String) {
|
||||
val device = cryptoStore.getUserDevice(deviceId, userId)
|
||||
fun handle(trustLevel: DeviceTrustLevel, userId: String, deviceId: String) {
|
||||
val device = cryptoStore.getUserDevice(userId, deviceId)
|
||||
|
||||
// Sanity check
|
||||
if (null == device) {
|
||||
|
@ -36,10 +37,7 @@ internal class SetDeviceVerificationAction @Inject constructor(
|
|||
return
|
||||
}
|
||||
|
||||
if (device.verified != verificationStatus) {
|
||||
device.verified = verificationStatus
|
||||
cryptoStore.storeUserDevice(userId, device)
|
||||
|
||||
if (device.isVerified != trustLevel.isVerified()) {
|
||||
if (userId == this.userId) {
|
||||
// If one of the user's own devices is being marked as verified / unverified,
|
||||
// check the key backup status, since whether or not we use this depends on
|
||||
|
@ -47,5 +45,10 @@ internal class SetDeviceVerificationAction @Inject constructor(
|
|||
keysBackup.checkAndStartKeysBackup()
|
||||
}
|
||||
}
|
||||
|
||||
if (device.trustLevel != trustLevel) {
|
||||
device.trustLevel = trustLevel
|
||||
cryptoStore.storeUserDevice(userId, device)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -297,7 +297,7 @@ internal class MXMegolmDecryption(private val userId: String,
|
|||
runCatching { deviceListManager.downloadKeys(listOf(userId), false) }
|
||||
.mapCatching {
|
||||
val deviceId = request.deviceId
|
||||
val deviceInfo = cryptoStore.getUserDevice(deviceId ?: "", userId)
|
||||
val deviceInfo = cryptoStore.getUserDevice(userId, deviceId ?: "")
|
||||
if (deviceInfo == null) {
|
||||
throw RuntimeException()
|
||||
} else {
|
||||
|
|
|
@ -29,7 +29,7 @@ import im.vector.matrix.android.internal.crypto.actions.EnsureOlmSessionsForDevi
|
|||
import im.vector.matrix.android.internal.crypto.actions.MessageEncrypter
|
||||
import im.vector.matrix.android.internal.crypto.algorithms.IMXEncrypting
|
||||
import im.vector.matrix.android.internal.crypto.keysbackup.KeysBackup
|
||||
import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo
|
||||
import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
|
||||
import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap
|
||||
import im.vector.matrix.android.internal.crypto.repository.WarnOnUnknownDeviceRepository
|
||||
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
|
||||
|
@ -95,7 +95,7 @@ internal class MXMegolmEncryption(
|
|||
*
|
||||
* @param devicesInRoom the devices list
|
||||
*/
|
||||
private suspend fun ensureOutboundSession(devicesInRoom: MXUsersDevicesMap<MXDeviceInfo>): MXOutboundSessionInfo {
|
||||
private suspend fun ensureOutboundSession(devicesInRoom: MXUsersDevicesMap<CryptoDeviceInfo>): MXOutboundSessionInfo {
|
||||
var session = outboundSession
|
||||
if (session == null
|
||||
// Need to make a brand new session?
|
||||
|
@ -106,7 +106,7 @@ internal class MXMegolmEncryption(
|
|||
outboundSession = session
|
||||
}
|
||||
val safeSession = session
|
||||
val shareMap = HashMap<String, MutableList<MXDeviceInfo>>()/* userId */
|
||||
val shareMap = HashMap<String, MutableList<CryptoDeviceInfo>>()/* userId */
|
||||
val userIds = devicesInRoom.userIds
|
||||
for (userId in userIds) {
|
||||
val deviceIds = devicesInRoom.getUserDeviceIds(userId)
|
||||
|
@ -129,14 +129,14 @@ internal class MXMegolmEncryption(
|
|||
* @param devicesByUsers the devices map
|
||||
*/
|
||||
private suspend fun shareKey(session: MXOutboundSessionInfo,
|
||||
devicesByUsers: Map<String, List<MXDeviceInfo>>) {
|
||||
devicesByUsers: Map<String, List<CryptoDeviceInfo>>) {
|
||||
// nothing to send, the task is done
|
||||
if (devicesByUsers.isEmpty()) {
|
||||
Timber.v("## shareKey() : nothing more to do")
|
||||
return
|
||||
}
|
||||
// reduce the map size to avoid request timeout when there are too many devices (Users size * devices per user)
|
||||
val subMap = HashMap<String, List<MXDeviceInfo>>()
|
||||
val subMap = HashMap<String, List<CryptoDeviceInfo>>()
|
||||
var devicesCount = 0
|
||||
for ((userId, devices) in devicesByUsers) {
|
||||
subMap[userId] = devices
|
||||
|
@ -158,7 +158,7 @@ internal class MXMegolmEncryption(
|
|||
* @param devicesByUser the devices map
|
||||
*/
|
||||
private suspend fun shareUserDevicesKey(session: MXOutboundSessionInfo,
|
||||
devicesByUser: Map<String, List<MXDeviceInfo>>) {
|
||||
devicesByUser: Map<String, List<CryptoDeviceInfo>>) {
|
||||
val sessionKey = olmDevice.getSessionKey(session.sessionId)
|
||||
val chainIndex = olmDevice.getMessageIndex(session.sessionId)
|
||||
|
||||
|
@ -262,7 +262,7 @@ internal class MXMegolmEncryption(
|
|||
*
|
||||
* @param userIds the user ids whose devices must be checked.
|
||||
*/
|
||||
private suspend fun getDevicesInRoom(userIds: List<String>): MXUsersDevicesMap<MXDeviceInfo> {
|
||||
private suspend fun getDevicesInRoom(userIds: List<String>): MXUsersDevicesMap<CryptoDeviceInfo> {
|
||||
// We are happy to use a cached version here: we assume that if we already
|
||||
// have a list of the user's devices, then we already share an e2e room
|
||||
// with them, which means that they will have announced any new devices via
|
||||
|
@ -271,8 +271,8 @@ internal class MXMegolmEncryption(
|
|||
val encryptToVerifiedDevicesOnly = cryptoStore.getGlobalBlacklistUnverifiedDevices()
|
||||
|| cryptoStore.getRoomsListBlacklistUnverifiedDevices().contains(roomId)
|
||||
|
||||
val devicesInRoom = MXUsersDevicesMap<MXDeviceInfo>()
|
||||
val unknownDevices = MXUsersDevicesMap<MXDeviceInfo>()
|
||||
val devicesInRoom = MXUsersDevicesMap<CryptoDeviceInfo>()
|
||||
val unknownDevices = MXUsersDevicesMap<CryptoDeviceInfo>()
|
||||
|
||||
for (userId in keys.userIds) {
|
||||
val deviceIds = keys.getUserDeviceIds(userId) ?: continue
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
|
||||
package im.vector.matrix.android.internal.crypto.algorithms.megolm
|
||||
|
||||
import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo
|
||||
import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
|
||||
import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap
|
||||
import timber.log.Timber
|
||||
|
||||
|
@ -52,7 +52,7 @@ internal class MXOutboundSessionInfo(
|
|||
* @param devicesInRoom the devices map
|
||||
* @return true if we have shared the session with devices which aren't in devicesInRoom.
|
||||
*/
|
||||
fun sharedWithTooManyDevices(devicesInRoom: MXUsersDevicesMap<MXDeviceInfo>): Boolean {
|
||||
fun sharedWithTooManyDevices(devicesInRoom: MXUsersDevicesMap<CryptoDeviceInfo>): Boolean {
|
||||
val userIds = sharedWithDevices.userIds
|
||||
|
||||
for (userId in userIds) {
|
||||
|
|
|
@ -25,7 +25,7 @@ import im.vector.matrix.android.internal.crypto.MXOlmDevice
|
|||
import im.vector.matrix.android.internal.crypto.actions.EnsureOlmSessionsForUsersAction
|
||||
import im.vector.matrix.android.internal.crypto.actions.MessageEncrypter
|
||||
import im.vector.matrix.android.internal.crypto.algorithms.IMXEncrypting
|
||||
import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo
|
||||
import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
|
||||
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
|
||||
|
||||
internal class MXOlmEncryption(
|
||||
|
@ -42,7 +42,7 @@ internal class MXOlmEncryption(
|
|||
//
|
||||
// TODO: there is a race condition here! What if a new user turns up
|
||||
ensureSession(userIds)
|
||||
val deviceInfos = ArrayList<MXDeviceInfo>()
|
||||
val deviceInfos = ArrayList<CryptoDeviceInfo>()
|
||||
for (userId in userIds) {
|
||||
val devices = cryptoStore.getUserDevices(userId)?.values ?: emptyList()
|
||||
for (device in devices) {
|
||||
|
|
|
@ -65,6 +65,33 @@ internal interface CryptoApi {
|
|||
@POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "keys/query")
|
||||
fun downloadKeysForUsers(@Body params: KeysQueryBody): Call<KeysQueryResponse>
|
||||
|
||||
/**
|
||||
* CrossSigning - Uploading signing keys
|
||||
* Public keys for the cross-signing keys are uploaded to the servers using /keys/device_signing/upload.
|
||||
* This endpoint requires UI Auth.
|
||||
*/
|
||||
@POST(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "keys/device_signing/upload")
|
||||
fun uploadSigningKeys(@Body params: UploadSigningKeysBody): Call<KeysQueryResponse>
|
||||
|
||||
/**
|
||||
* CrossSigning - Uploading signatures
|
||||
* Signatures of device keys can be up
|
||||
* loaded using /keys/signatures/upload.
|
||||
* For example, Alice signs one of her devices (HIJKLMN) (using her self-signing key),
|
||||
* her own master key (using her HIJKLMN device), Bob's master key (using her user-signing key).
|
||||
*
|
||||
* The response contains a failures property, which is a map of user ID to device ID to failure reason, if any of the uploaded keys failed.
|
||||
* The homeserver should verify that the signatures on the uploaded keys are valid.
|
||||
* If a signature is not valid, the homeserver should set the corresponding entry in failures to a JSON object
|
||||
* with the errcode property set to M_INVALID_SIGNATURE.
|
||||
*
|
||||
* After Alice uploads a signature for her own devices or master key,
|
||||
* her signature will be included in the results of the /keys/query request when anyone requests her keys.
|
||||
* However, signatures made for other users' keys, made by her user-signing key, will not be included.
|
||||
*/
|
||||
@POST(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "keys/signatures/upload")
|
||||
fun uploadSignatures(@Body params: Map<String, @JvmSuppressWildcards Any>?): Call<SignatureUploadResponse>
|
||||
|
||||
/**
|
||||
* Claim one-time keys.
|
||||
* Doc: https://matrix.org/docs/spec/client_server/r0.4.0.html#post-matrix-client-r0-keys-claim
|
||||
|
|
|
@ -0,0 +1,630 @@
|
|||
/*
|
||||
* 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 androidx.lifecycle.LiveData
|
||||
import dagger.Lazy
|
||||
import im.vector.matrix.android.api.MatrixCallback
|
||||
import im.vector.matrix.android.api.session.crypto.crosssigning.CrossSigningService
|
||||
import im.vector.matrix.android.api.session.crypto.crosssigning.MXCrossSigningInfo
|
||||
import im.vector.matrix.android.api.util.Optional
|
||||
import im.vector.matrix.android.internal.crypto.DeviceListManager
|
||||
import im.vector.matrix.android.internal.crypto.MXOlmDevice
|
||||
import im.vector.matrix.android.internal.crypto.MyDeviceInfoHolder
|
||||
import im.vector.matrix.android.internal.crypto.model.CryptoCrossSigningKey
|
||||
import im.vector.matrix.android.internal.crypto.model.KeyUsage
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.UploadSignatureQueryBuilder
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.UserPasswordAuth
|
||||
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.UserId
|
||||
import im.vector.matrix.android.internal.session.SessionScope
|
||||
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 im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
|
||||
import im.vector.matrix.android.internal.util.withoutPrefix
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import org.matrix.olm.OlmPkSigning
|
||||
import org.matrix.olm.OlmUtility
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
@SessionScope
|
||||
internal class DefaultCrossSigningService @Inject constructor(
|
||||
@UserId private val userId: String,
|
||||
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 cryptoCoroutineScope: CoroutineScope,
|
||||
private val coroutineDispatchers: MatrixCoroutineDispatchers,
|
||||
private val taskExecutor: TaskExecutor) : CrossSigningService, DeviceListManager.UserDevicesUpdateListener {
|
||||
|
||||
private var olmUtility: OlmUtility? = null
|
||||
|
||||
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 { privateKeysInfo ->
|
||||
privateKeysInfo.master
|
||||
?.fromBase64NoPadding()
|
||||
?.let { privateKeySeed ->
|
||||
val pkSigning = OlmPkSigning()
|
||||
if (pkSigning.initWithSeed(privateKeySeed) == 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
|
||||
}
|
||||
}
|
||||
privateKeysInfo.user
|
||||
?.fromBase64NoPadding()
|
||||
?.let { privateKeySeed ->
|
||||
val pkSigning = OlmPkSigning()
|
||||
if (pkSigning.initWithSeed(privateKeySeed) == 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
|
||||
}
|
||||
}
|
||||
privateKeysInfo.selfSigned
|
||||
?.fromBase64NoPadding()
|
||||
?.let { privateKeySeed ->
|
||||
val pkSigning = OlmPkSigning()
|
||||
if (pkSigning.initWithSeed(privateKeySeed) == 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Recover local trust in case private key are there?
|
||||
cryptoStore.setUserKeysAsTrusted(userId, checkUserTrust(userId).isVerified())
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
// Mmm this kind of a big issue
|
||||
Timber.e(e, "Failed to initialize Cross Signing")
|
||||
}
|
||||
|
||||
deviceListManager.addListener(this)
|
||||
}
|
||||
|
||||
fun release() {
|
||||
olmUtility?.releaseUtility()
|
||||
listOf(masterPkSigning, userPkSigning, selfSigningPkSigning).forEach { it?.releaseSigning() }
|
||||
deviceListManager.removeListener(this)
|
||||
}
|
||||
|
||||
protected fun finalize() {
|
||||
release()
|
||||
}
|
||||
|
||||
/**
|
||||
* - 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")
|
||||
|
||||
// =================
|
||||
// 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 = CryptoCrossSigningKey.Builder(userId, KeyUsage.USER_SIGNING)
|
||||
.key(uskPublicKey)
|
||||
.build()
|
||||
.canonicalSignable()
|
||||
.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, CryptoCrossSigningKey.Builder(userId, KeyUsage.SELF_SIGNING)
|
||||
.key(sskPublicKey)
|
||||
.build().signalableJSONDictionary()).let { masterPkOlm.sign(it) }
|
||||
|
||||
// I need to upload the keys
|
||||
val mskCrossSigningKeyInfo = CryptoCrossSigningKey.Builder(userId, KeyUsage.MASTER)
|
||||
.key(masterPublicKey)
|
||||
.build()
|
||||
val params = UploadSigningKeysTask.Params(
|
||||
masterKey = mskCrossSigningKeyInfo,
|
||||
userKey = CryptoCrossSigningKey.Builder(userId, KeyUsage.USER_SIGNING)
|
||||
.key(uskPublicKey)
|
||||
.signature(userId, masterPublicKey, signedUSK)
|
||||
.build(),
|
||||
selfSignedKey = CryptoCrossSigningKey.Builder(userId, KeyUsage.SELF_SIGNING)
|
||||
.key(sskPublicKey)
|
||||
.signature(userId, masterPublicKey, signedSSK)
|
||||
.build(),
|
||||
userPasswordAuth = authParams
|
||||
)
|
||||
|
||||
this.masterPkSigning = masterPkOlm
|
||||
this.userPkSigning = userSigningPkOlm
|
||||
this.selfSigningPkSigning = selfSigningPkOlm
|
||||
|
||||
val crossSigningInfo = MXCrossSigningInfo(userId, listOf(params.masterKey, params.userKey, params.selfSignedKey))
|
||||
cryptoStore.setMyCrossSigningInfo(crossSigningInfo)
|
||||
cryptoStore.setUserKeysAsTrusted(userId)
|
||||
cryptoStore.storePrivateKeysInfo(masterKeyPrivateKey?.toBase64NoPadding(), uskPrivateKey?.toBase64NoPadding(), sskPrivateKey?.toBase64NoPadding())
|
||||
|
||||
uploadSigningKeysTask.configureWith(params) {
|
||||
this.constraints = TaskConstraints(true)
|
||||
this.callback = object : MatrixCallback<Unit> {
|
||||
override fun onSuccess(data: Unit) {
|
||||
Timber.i("## CrossSigning - Keys successfully 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[userId] = (it[userId]
|
||||
?: 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[userId] = (it[userId]
|
||||
?: HashMap()) + mapOf("ed25519:${myDevice.deviceId}" to sign)
|
||||
}
|
||||
mskCrossSigningKeyInfo.copy(
|
||||
signatures = mskUpdatedSignatures
|
||||
).let {
|
||||
uploadSignatureQueryBuilder.withSigningKeyInfo(it)
|
||||
}
|
||||
}
|
||||
|
||||
resetTrustOnKeyChange()
|
||||
uploadSignaturesTask.configureWith(UploadSignaturesTask.Params(uploadSignatureQueryBuilder.build())) {
|
||||
// this.retryCount = 3
|
||||
this.constraints = TaskConstraints(true)
|
||||
this.callback = object : MatrixCallback<Unit> {
|
||||
override fun onSuccess(data: Unit) {
|
||||
Timber.i("## CrossSigning - signatures successfully uploaded")
|
||||
callback?.onSuccess(Unit)
|
||||
}
|
||||
|
||||
override fun onFailure(failure: Throwable) {
|
||||
// Clear
|
||||
Timber.e(failure, "## CrossSigning - Failed to upload signatures")
|
||||
clearSigningKeys()
|
||||
}
|
||||
}
|
||||
}.executeBy(taskExecutor)
|
||||
}
|
||||
|
||||
override fun onFailure(failure: Throwable) {
|
||||
Timber.e(failure, "## CrossSigning - Failed to upload signing keys")
|
||||
clearSigningKeys()
|
||||
callback?.onFailure(failure)
|
||||
}
|
||||
}
|
||||
}.executeBy(taskExecutor)
|
||||
}
|
||||
|
||||
private fun clearSigningKeys() {
|
||||
this@DefaultCrossSigningService.masterPkSigning?.releaseSigning()
|
||||
this@DefaultCrossSigningService.userPkSigning?.releaseSigning()
|
||||
this@DefaultCrossSigningService.selfSigningPkSigning?.releaseSigning()
|
||||
|
||||
this@DefaultCrossSigningService.masterPkSigning = null
|
||||
this@DefaultCrossSigningService.userPkSigning = null
|
||||
this@DefaultCrossSigningService.selfSigningPkSigning = null
|
||||
|
||||
cryptoStore.setMyCrossSigningInfo(null)
|
||||
cryptoStore.storePrivateKeysInfo(null, null, null)
|
||||
}
|
||||
|
||||
private fun resetTrustOnKeyChange() {
|
||||
Timber.i("## CrossSigning - Clear all other user trust")
|
||||
cryptoStore.clearOtherUserTrust()
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* ┏━━━━━━━━┓ ┏━━━━━━━━┓
|
||||
* ┃ ALICE ┃ ┃ BOB ┃
|
||||
* ┗━━━━━━━━┛ ┗━━━━━━━━┛
|
||||
* MSK ┌────────────▶ MSK
|
||||
* │
|
||||
* │ │
|
||||
* │ SSK │
|
||||
* │ │
|
||||
* │ │
|
||||
* └──▶ USK ────────────┘
|
||||
*/
|
||||
override fun isUserTrusted(otherUserId: String): Boolean {
|
||||
return cryptoStore.getCrossSigningInfo(userId)?.isTrusted() == true
|
||||
}
|
||||
|
||||
override fun isCrossSigningEnabled(): Boolean {
|
||||
return checkSelfTrust().isVerified()
|
||||
}
|
||||
|
||||
/**
|
||||
* Will not force a download of the key, but will verify signatures trust chain
|
||||
*/
|
||||
override fun checkUserTrust(otherUserId: String): UserTrustResult {
|
||||
Timber.d("## CrossSigning checkUserTrust for $otherUserId")
|
||||
if (otherUserId == userId) {
|
||||
return checkSelfTrust()
|
||||
}
|
||||
// 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 myCrossSigningInfo = cryptoStore.getCrossSigningInfo(userId)
|
||||
|
||||
val myUserKey = myCrossSigningInfo?.userKey()
|
||||
?: return UserTrustResult.CrossSigningNotConfigured(userId)
|
||||
|
||||
if (!myCrossSigningInfo.isTrusted()) {
|
||||
return UserTrustResult.KeysNotTrusted(myCrossSigningInfo)
|
||||
}
|
||||
|
||||
// Let's get the other user master key
|
||||
val otherMasterKey = cryptoStore.getCrossSigningInfo(otherUserId)?.masterKey()
|
||||
?: return UserTrustResult.UnknownCrossSignatureInfo(otherUserId)
|
||||
|
||||
val masterKeySignaturesMadeByMyUserKey = otherMasterKey.signatures
|
||||
?.get(userId) // Signatures made by me
|
||||
?.get("ed25519:${myUserKey.unpaddedBase64PublicKey}")
|
||||
|
||||
if (masterKeySignaturesMadeByMyUserKey.isNullOrBlank()) {
|
||||
Timber.d("## CrossSigning checkUserTrust false for $otherUserId, not signed by my UserSigningKey")
|
||||
return UserTrustResult.KeyNotSigned(otherMasterKey)
|
||||
}
|
||||
|
||||
// Check that Alice USK signature of Bob MSK is valid
|
||||
try {
|
||||
olmUtility!!.verifyEd25519Signature(masterKeySignaturesMadeByMyUserKey, myUserKey.unpaddedBase64PublicKey, otherMasterKey.canonicalSignable())
|
||||
} catch (failure: Throwable) {
|
||||
return UserTrustResult.InvalidSignature(myUserKey, masterKeySignaturesMadeByMyUserKey)
|
||||
}
|
||||
|
||||
return UserTrustResult.Success
|
||||
}
|
||||
|
||||
private fun checkSelfTrust(): UserTrustResult {
|
||||
// Special case when it's me,
|
||||
// I have to check that MSK -> USK -> SSK
|
||||
// and that MSK is trusted (i know the private key, or is signed by a trusted device)
|
||||
val myCrossSigningInfo = cryptoStore.getCrossSigningInfo(userId)
|
||||
|
||||
val myMasterKey = myCrossSigningInfo?.masterKey()
|
||||
?: return UserTrustResult.CrossSigningNotConfigured(userId)
|
||||
|
||||
// Is the master key trusted
|
||||
// 1) check if I know the private key
|
||||
val masterPrivateKey = cryptoStore.getCrossSigningPrivateKeys()
|
||||
?.master
|
||||
?.fromBase64NoPadding()
|
||||
|
||||
var isMaterKeyTrusted = false
|
||||
if (masterPrivateKey != null) {
|
||||
// Check if private match public
|
||||
var olmPkSigning: OlmPkSigning? = null
|
||||
try {
|
||||
olmPkSigning = OlmPkSigning()
|
||||
val expectedPK = olmPkSigning.initWithSeed(masterPrivateKey)
|
||||
isMaterKeyTrusted = myMasterKey.unpaddedBase64PublicKey == expectedPK
|
||||
} catch (failure: Throwable) {
|
||||
Timber.e(failure)
|
||||
}
|
||||
olmPkSigning?.releaseSigning()
|
||||
} else {
|
||||
// Maybe it's signed by a locally trusted device?
|
||||
myMasterKey.signatures?.get(userId)?.forEach { (key, value) ->
|
||||
val potentialDeviceId = key.withoutPrefix("ed25519:")
|
||||
val potentialDevice = cryptoStore.getUserDevice(userId, potentialDeviceId)
|
||||
if (potentialDevice != null && potentialDevice.isVerified) {
|
||||
// Check signature validity?
|
||||
try {
|
||||
olmUtility?.verifyEd25519Signature(value, potentialDevice.fingerprint(), myMasterKey.canonicalSignable())
|
||||
isMaterKeyTrusted = true
|
||||
return@forEach
|
||||
} catch (failure: Throwable) {
|
||||
// log
|
||||
Timber.v(failure)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!isMaterKeyTrusted) {
|
||||
return UserTrustResult.KeysNotTrusted(myCrossSigningInfo)
|
||||
}
|
||||
|
||||
val myUserKey = myCrossSigningInfo.userKey()
|
||||
?: return UserTrustResult.CrossSigningNotConfigured(userId)
|
||||
|
||||
val userKeySignaturesMadeByMyMasterKey = myUserKey.signatures
|
||||
?.get(userId) // Signatures made by me
|
||||
?.get("ed25519:${myMasterKey.unpaddedBase64PublicKey}")
|
||||
|
||||
if (userKeySignaturesMadeByMyMasterKey.isNullOrBlank()) {
|
||||
Timber.d("## CrossSigning checkUserTrust false for $userId, USK not signed by MSK")
|
||||
return UserTrustResult.KeyNotSigned(myUserKey)
|
||||
}
|
||||
|
||||
// Check that Alice USK signature of Alice MSK is valid
|
||||
try {
|
||||
olmUtility!!.verifyEd25519Signature(userKeySignaturesMadeByMyMasterKey, myMasterKey.unpaddedBase64PublicKey, myUserKey.canonicalSignable())
|
||||
} catch (failure: Throwable) {
|
||||
return UserTrustResult.InvalidSignature(myUserKey, userKeySignaturesMadeByMyMasterKey)
|
||||
}
|
||||
|
||||
val mySSKey = myCrossSigningInfo.selfSigningKey()
|
||||
?: return UserTrustResult.CrossSigningNotConfigured(userId)
|
||||
|
||||
val ssKeySignaturesMadeByMyMasterKey = mySSKey.signatures
|
||||
?.get(userId) // Signatures made by me
|
||||
?.get("ed25519:${myMasterKey.unpaddedBase64PublicKey}")
|
||||
|
||||
if (ssKeySignaturesMadeByMyMasterKey.isNullOrBlank()) {
|
||||
Timber.d("## CrossSigning checkUserTrust false for $userId, SSK not signed by MSK")
|
||||
return UserTrustResult.KeyNotSigned(mySSKey)
|
||||
}
|
||||
|
||||
// Check that Alice USK signature of Alice MSK is valid
|
||||
try {
|
||||
olmUtility!!.verifyEd25519Signature(ssKeySignaturesMadeByMyMasterKey, myMasterKey.unpaddedBase64PublicKey, mySSKey.canonicalSignable())
|
||||
} catch (failure: Throwable) {
|
||||
return UserTrustResult.InvalidSignature(mySSKey, ssKeySignaturesMadeByMyMasterKey)
|
||||
}
|
||||
|
||||
return UserTrustResult.Success
|
||||
}
|
||||
|
||||
override fun getUserCrossSigningKeys(otherUserId: String): MXCrossSigningInfo? {
|
||||
return cryptoStore.getCrossSigningInfo(otherUserId)
|
||||
}
|
||||
|
||||
override fun getLiveCrossSigningKeys(userId: String): LiveData<Optional<MXCrossSigningInfo>> {
|
||||
return cryptoStore.getLiveCrossSigningInfo(userId)
|
||||
}
|
||||
|
||||
override fun getMyCrossSigningKeys(): MXCrossSigningInfo? {
|
||||
return cryptoStore.getMyCrossSigningInfo()
|
||||
}
|
||||
|
||||
override fun canCrossSign(): Boolean {
|
||||
return checkSelfTrust().isVerified() && cryptoStore.getCrossSigningPrivateKeys()?.selfSigned != null
|
||||
}
|
||||
|
||||
override fun trustUser(otherUserId: String, callback: MatrixCallback<Unit>) {
|
||||
Timber.d("## CrossSigning - Mark user $userId as trusted ")
|
||||
// We should have this user keys
|
||||
val otherMasterKeys = getUserCrossSigningKeys(otherUserId)?.masterKey()
|
||||
if (otherMasterKeys == null) {
|
||||
callback.onFailure(Throwable("## CrossSigning - Other master signing key is not known"))
|
||||
return
|
||||
}
|
||||
val myKeys = getUserCrossSigningKeys(userId)
|
||||
if (myKeys == null) {
|
||||
callback.onFailure(Throwable("## CrossSigning - CrossSigning is not setup for this account"))
|
||||
return
|
||||
}
|
||||
val userPubKey = myKeys.userKey()?.unpaddedBase64PublicKey
|
||||
if (userPubKey == null || userPkSigning == null) {
|
||||
callback.onFailure(Throwable("## CrossSigning - Cannot sign from this account, privateKeyUnknown $userPubKey"))
|
||||
return
|
||||
}
|
||||
|
||||
// Sign the other MasterKey with our UserSigning key
|
||||
val newSignature = JsonCanonicalizer.getCanonicalJson(Map::class.java,
|
||||
otherMasterKeys.signalableJSONDictionary()).let { userPkSigning?.sign(it) }
|
||||
|
||||
if (newSignature == null) {
|
||||
// race??
|
||||
callback.onFailure(Throwable("## CrossSigning - Failed to sign"))
|
||||
return
|
||||
}
|
||||
|
||||
cryptoStore.setUserKeysAsTrusted(otherUserId, true)
|
||||
// TODO update local copy with new signature directly here? kind of local echo of trust?
|
||||
|
||||
Timber.d("## CrossSigning - Upload signature of $userId MSK signed by USK")
|
||||
val uploadQuery = UploadSignatureQueryBuilder()
|
||||
.withSigningKeyInfo(otherMasterKeys.copyForSignature(userId, userPubKey, newSignature))
|
||||
.build()
|
||||
uploadSignaturesTask.configureWith(UploadSignaturesTask.Params(uploadQuery)) {
|
||||
this.callback = callback
|
||||
}.executeBy(taskExecutor)
|
||||
}
|
||||
|
||||
override fun signDevice(deviceId: String, callback: MatrixCallback<Unit>) {
|
||||
// This device should be yours
|
||||
val device = cryptoStore.getUserDevice(userId, deviceId)
|
||||
if (device == null) {
|
||||
callback.onFailure(IllegalArgumentException("This device [$deviceId] is not known, or not yours"))
|
||||
return
|
||||
}
|
||||
|
||||
val myKeys = getUserCrossSigningKeys(userId)
|
||||
if (myKeys == null) {
|
||||
callback.onFailure(Throwable("CrossSigning is not setup for this account"))
|
||||
return
|
||||
}
|
||||
|
||||
val ssPubKey = myKeys.selfSigningKey()?.unpaddedBase64PublicKey
|
||||
if (ssPubKey == null || selfSigningPkSigning == null) {
|
||||
callback.onFailure(Throwable("Cannot sign from this account, public and/or privateKey Unknown $ssPubKey"))
|
||||
return
|
||||
}
|
||||
|
||||
// Sign with self signing
|
||||
val newSignature = selfSigningPkSigning?.sign(device.canonicalSignable())
|
||||
|
||||
if (newSignature == null) {
|
||||
// race??
|
||||
callback.onFailure(Throwable("Failed to sign"))
|
||||
return
|
||||
}
|
||||
val toUpload = device.copy(
|
||||
signatures = mapOf(
|
||||
userId
|
||||
to
|
||||
mapOf(
|
||||
"ed25519:$ssPubKey" to newSignature
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
val uploadQuery = UploadSignatureQueryBuilder()
|
||||
.withDeviceInfo(toUpload)
|
||||
.build()
|
||||
uploadSignaturesTask.configureWith(UploadSignaturesTask.Params(uploadQuery)) {
|
||||
this.callback = callback
|
||||
}.executeBy(taskExecutor)
|
||||
}
|
||||
|
||||
override fun checkDeviceTrust(otherUserId: String, otherDeviceId: String, locallyTrusted: Boolean?): DeviceTrustResult {
|
||||
val otherDevice = cryptoStore.getUserDevice(otherUserId, otherDeviceId)
|
||||
?: return DeviceTrustResult.UnknownDevice(otherDeviceId)
|
||||
|
||||
val myKeys = getUserCrossSigningKeys(userId)
|
||||
?: return legacyFallbackTrust(locallyTrusted, DeviceTrustResult.CrossSigningNotConfigured(userId))
|
||||
|
||||
if (!myKeys.isTrusted()) return legacyFallbackTrust(locallyTrusted, DeviceTrustResult.KeysNotTrusted(myKeys))
|
||||
|
||||
val otherKeys = getUserCrossSigningKeys(otherUserId)
|
||||
?: return legacyFallbackTrust(locallyTrusted, DeviceTrustResult.CrossSigningNotConfigured(otherUserId))
|
||||
|
||||
// TODO should we force verification ?
|
||||
if (!otherKeys.isTrusted()) return legacyFallbackTrust(locallyTrusted, DeviceTrustResult.KeysNotTrusted(otherKeys))
|
||||
|
||||
// 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(otherUserId)?.get("ed25519:${otherKeys.selfSigningKey()?.unpaddedBase64PublicKey}")
|
||||
?: return legacyFallbackTrust(
|
||||
locallyTrusted,
|
||||
DeviceTrustResult.MissingDeviceSignature(otherDeviceId, otherKeys.selfSigningKey()
|
||||
?.unpaddedBase64PublicKey
|
||||
?: ""
|
||||
)
|
||||
)
|
||||
|
||||
// Check bob's device is signed by bob's SSK
|
||||
try {
|
||||
olmUtility!!.verifyEd25519Signature(otherSSKSignature, otherKeys.selfSigningKey()?.unpaddedBase64PublicKey, otherDevice.canonicalSignable())
|
||||
} catch (e: Throwable) {
|
||||
return legacyFallbackTrust(locallyTrusted, DeviceTrustResult.InvalidDeviceSignature(otherDeviceId, otherSSKSignature, e))
|
||||
}
|
||||
|
||||
return DeviceTrustResult.Success(DeviceTrustLevel(crossSigningVerified = true, locallyVerified = locallyTrusted))
|
||||
}
|
||||
|
||||
private fun legacyFallbackTrust(locallyTrusted: Boolean?, crossSignTrustFail: DeviceTrustResult): DeviceTrustResult {
|
||||
return if (locallyTrusted == true) {
|
||||
DeviceTrustResult.Success(DeviceTrustLevel(crossSigningVerified = false, locallyVerified = true))
|
||||
} else {
|
||||
crossSignTrustFail
|
||||
}
|
||||
}
|
||||
|
||||
override fun onUsersDeviceUpdate(users: List<String>) {
|
||||
Timber.d("## CrossSigning - onUsersDeviceUpdate for ${users.size} users")
|
||||
users.forEach { otherUserId ->
|
||||
|
||||
checkUserTrust(otherUserId).let {
|
||||
Timber.d("## CrossSigning - update trust for $otherUserId , verified=${it.isVerified()}")
|
||||
cryptoStore.setUserKeysAsTrusted(otherUserId, it.isVerified())
|
||||
}
|
||||
|
||||
// TODO if my keys have changes, i should recheck all devices of all users?
|
||||
val devices = cryptoStore.getUserDeviceList(otherUserId)
|
||||
devices?.forEach { device ->
|
||||
val updatedTrust = checkDeviceTrust(otherUserId, device.deviceId, device.trustLevel?.isLocallyVerified() ?: false)
|
||||
Timber.d("## CrossSigning - update trust for device ${device.deviceId} of user $otherUserId , verified=$updatedTrust")
|
||||
cryptoStore.setDeviceTrust(otherUserId, device.deviceId, updatedTrust.isCrossSignedVerified(), updatedTrust.isLocallyVerified())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* 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
|
||||
|
||||
data class DeviceTrustLevel(val crossSigningVerified: Boolean, val locallyVerified: Boolean?) {
|
||||
|
||||
fun isVerified() = crossSigningVerified || locallyVerified == true
|
||||
fun isCrossSigningVerified() = crossSigningVerified
|
||||
fun isLocallyVerified() = locallyVerified
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* 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 im.vector.matrix.android.api.session.crypto.crosssigning.MXCrossSigningInfo
|
||||
|
||||
sealed class DeviceTrustResult {
|
||||
|
||||
data class Success(val level: DeviceTrustLevel) : DeviceTrustResult()
|
||||
data class UnknownDevice(val deviceID: String) : DeviceTrustResult()
|
||||
data class CrossSigningNotConfigured(val userID: String) : DeviceTrustResult()
|
||||
data class KeysNotTrusted(val key: MXCrossSigningInfo) : DeviceTrustResult()
|
||||
data class MissingDeviceSignature(val deviceId: String, val signingKey: String) : DeviceTrustResult()
|
||||
data class InvalidDeviceSignature(val deviceId: String, val signingKey: String, val throwable: Throwable?) : DeviceTrustResult()
|
||||
}
|
||||
|
||||
fun DeviceTrustResult.isSuccess(): Boolean = this is DeviceTrustResult.Success
|
||||
fun DeviceTrustResult.isCrossSignedVerified(): Boolean = (this as? DeviceTrustResult.Success)?.level?.isCrossSigningVerified() == true
|
||||
fun DeviceTrustResult.isLocallyVerified(): Boolean = (this as? DeviceTrustResult.Success)?.level?.isLocallyVerified() == true
|
|
@ -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.crosssigning
|
||||
|
||||
import android.util.Base64
|
||||
import im.vector.matrix.android.internal.crypto.model.CryptoCrossSigningKey
|
||||
import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
|
||||
import im.vector.matrix.android.internal.util.JsonCanonicalizer
|
||||
|
||||
fun CryptoDeviceInfo.canonicalSignable(): String {
|
||||
return JsonCanonicalizer.getCanonicalJson(Map::class.java, signalableJSONDictionary())
|
||||
}
|
||||
|
||||
fun CryptoCrossSigningKey.canonicalSignable(): String {
|
||||
return JsonCanonicalizer.getCanonicalJson(Map::class.java, signalableJSONDictionary())
|
||||
}
|
||||
|
||||
fun ByteArray.toBase64NoPadding(): String {
|
||||
return Base64.encodeToString(this, Base64.NO_PADDING or Base64.NO_WRAP)
|
||||
}
|
||||
|
||||
fun String.fromBase64NoPadding(): ByteArray {
|
||||
return Base64.decode(this, Base64.NO_PADDING or Base64.NO_WRAP)
|
||||
}
|
|
@ -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.crosssigning
|
||||
|
||||
import im.vector.matrix.android.api.session.crypto.crosssigning.MXCrossSigningInfo
|
||||
import im.vector.matrix.android.internal.crypto.model.CryptoCrossSigningKey
|
||||
|
||||
sealed class UserTrustResult {
|
||||
|
||||
object Success : UserTrustResult()
|
||||
|
||||
// data class Success(val deviceID: String, val crossSigned: Boolean) : UserTrustResult()
|
||||
//
|
||||
// data class UnknownDevice(val deviceID: String) : UserTrustResult()
|
||||
data class CrossSigningNotConfigured(val userID: String) : UserTrustResult()
|
||||
data class UnknownCrossSignatureInfo(val userID: String) : UserTrustResult()
|
||||
data class KeysNotTrusted(val key: MXCrossSigningInfo) : UserTrustResult()
|
||||
data class KeyNotSigned(val key: CryptoCrossSigningKey) : UserTrustResult()
|
||||
data class InvalidSignature(val key: CryptoCrossSigningKey, val signature: String) : UserTrustResult()
|
||||
}
|
||||
|
||||
fun UserTrustResult.isVerified() = this is UserTrustResult.Success
|
|
@ -402,7 +402,7 @@ internal class KeysBackup @Inject constructor(
|
|||
}
|
||||
|
||||
if (deviceId != null) {
|
||||
val device = cryptoStore.getUserDevice(deviceId, userId)
|
||||
val device = cryptoStore.getUserDevice(userId, deviceId)
|
||||
var isSignatureValid = false
|
||||
|
||||
if (device == null) {
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
|
||||
package im.vector.matrix.android.internal.crypto.keysbackup.model
|
||||
|
||||
import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo
|
||||
import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
|
||||
|
||||
/**
|
||||
* A signature in a the `KeyBackupVersionTrust` object.
|
||||
|
@ -26,7 +26,7 @@ class KeyBackupVersionTrustSignature {
|
|||
/**
|
||||
* The device that signed the backup version.
|
||||
*/
|
||||
var device: MXDeviceInfo? = null
|
||||
var device: CryptoDeviceInfo? = null
|
||||
|
||||
/**
|
||||
*Flag to indicate the signature from this device is valid.
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
|
||||
package im.vector.matrix.android.internal.crypto.keysbackup.model
|
||||
|
||||
import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo
|
||||
import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
|
||||
|
||||
/**
|
||||
* A signature in a `KeysBackupVersionTrust` object.
|
||||
|
@ -32,7 +32,7 @@ class KeysBackupVersionTrustSignature {
|
|||
* The device that signed the backup version.
|
||||
* Can be null if the device is not known.
|
||||
*/
|
||||
var device: MXDeviceInfo? = null
|
||||
var device: CryptoDeviceInfo? = null
|
||||
|
||||
/**
|
||||
* Flag to indicate the signature from this device is valid.
|
||||
|
|
|
@ -0,0 +1,92 @@
|
|||
package im.vector.matrix.android.internal.crypto.model
|
||||
|
||||
import im.vector.matrix.android.internal.crypto.crosssigning.DeviceTrustLevel
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.RestKeyInfo
|
||||
|
||||
data class CryptoCrossSigningKey(
|
||||
override val userId: String,
|
||||
|
||||
val usages: List<String>?,
|
||||
|
||||
override val keys: Map<String, String>,
|
||||
|
||||
override val signatures: Map<String, Map<String, String>>?,
|
||||
|
||||
var trustLevel: DeviceTrustLevel? = null
|
||||
) : CryptoInfo {
|
||||
|
||||
override 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
|
||||
}
|
||||
|
||||
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 addSignatureAndCopy(userId: String, signedWithNoPrefix: String, signature: String): CryptoCrossSigningKey {
|
||||
val updated = (signatures?.toMutableMap() ?: HashMap())
|
||||
val userMap = updated[userId]?.toMutableMap()
|
||||
?: HashMap<String, String>().also { updated[userId] = it }
|
||||
userMap["ed25519:$signedWithNoPrefix"] = signature
|
||||
|
||||
return this.copy(
|
||||
signatures = updated
|
||||
)
|
||||
}
|
||||
|
||||
fun copyForSignature(userId: String, signedWithNoPrefix: String, signature: String): CryptoCrossSigningKey {
|
||||
return this.copy(
|
||||
signatures = mapOf(userId to mapOf("ed25519:$signedWithNoPrefix" to signature))
|
||||
)
|
||||
}
|
||||
|
||||
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(): CryptoCrossSigningKey {
|
||||
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 CryptoCrossSigningKey(
|
||||
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")
|
||||
}
|
||||
|
||||
fun CryptoCrossSigningKey.toRest(): RestKeyInfo {
|
||||
return CryptoInfoMapper.map(this)
|
||||
}
|
|
@ -0,0 +1,102 @@
|
|||
/*
|
||||
* 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
|
||||
|
||||
import im.vector.matrix.android.api.util.JsonDict
|
||||
import im.vector.matrix.android.internal.crypto.crosssigning.DeviceTrustLevel
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.RestDeviceInfo
|
||||
import im.vector.matrix.android.internal.crypto.store.db.model.CryptoMapper
|
||||
import im.vector.matrix.android.internal.crypto.store.db.model.DeviceInfoEntity
|
||||
|
||||
data class CryptoDeviceInfo(
|
||||
val deviceId: String,
|
||||
override val userId: String,
|
||||
var algorithms: List<String>? = null,
|
||||
override val keys: Map<String, String>? = null,
|
||||
override val signatures: Map<String, Map<String, String>>? = null,
|
||||
val unsigned: JsonDict? = null,
|
||||
|
||||
// TODO how to store if this device is verified by a user SSK, or is legacy trusted?
|
||||
// I need to know if it is trusted via cross signing (Trusted because bob verified it)
|
||||
|
||||
var trustLevel: DeviceTrustLevel? = null,
|
||||
var isBlocked: Boolean = false
|
||||
|
||||
)
|
||||
|
||||
: CryptoInfo {
|
||||
|
||||
val isVerified: Boolean
|
||||
get() = trustLevel?.isVerified() ?: false
|
||||
|
||||
val isUnknown: Boolean
|
||||
get() = trustLevel == null
|
||||
|
||||
/**
|
||||
* @return the fingerprint
|
||||
*/
|
||||
fun fingerprint(): String? {
|
||||
return keys
|
||||
?.takeIf { !deviceId.isBlank() }
|
||||
?.get("ed25519:$deviceId")
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the identity key
|
||||
*/
|
||||
fun identityKey(): String? {
|
||||
return keys
|
||||
?.takeIf { !deviceId.isBlank() }
|
||||
?.get("curve25519:$deviceId")
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the display name
|
||||
*/
|
||||
fun displayName(): String? {
|
||||
return unsigned?.get("device_display_name") as? String
|
||||
}
|
||||
|
||||
override fun signalableJSONDictionary(): Map<String, Any> {
|
||||
val map = HashMap<String, Any>()
|
||||
map["device_id"] = deviceId
|
||||
map["user_id"] = userId
|
||||
algorithms?.let { map["algorithms"] = it }
|
||||
keys?.let { map["keys"] = it }
|
||||
return map
|
||||
}
|
||||
//
|
||||
// /**
|
||||
// * @return a dictionary of the parameters
|
||||
// */
|
||||
// fun toDeviceKeys(): DeviceKeys {
|
||||
// return DeviceKeys(
|
||||
// userId = userId,
|
||||
// deviceId = deviceId,
|
||||
// algorithms = algorithms!!,
|
||||
// keys = keys!!,
|
||||
// signatures = signatures!!
|
||||
// )
|
||||
// }
|
||||
}
|
||||
|
||||
fun CryptoDeviceInfo.toRest(): RestDeviceInfo {
|
||||
return CryptoInfoMapper.map(this)
|
||||
}
|
||||
|
||||
internal fun CryptoDeviceInfo.toEntity(): DeviceInfoEntity {
|
||||
return CryptoMapper.mapToEntity(this)
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* 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
|
||||
|
||||
/**
|
||||
* Generic crypto info.
|
||||
* Can be a device (CryptoDeviceInfo), as well as a CryptoCrossSigningInfo (can be seen as a kind of virtual device)
|
||||
*/
|
||||
interface CryptoInfo {
|
||||
|
||||
val userId: String
|
||||
|
||||
val keys: Map<String, String>?
|
||||
|
||||
val signatures: Map<String, Map<String, String>>?
|
||||
|
||||
fun signalableJSONDictionary(): Map<String, Any>
|
||||
}
|
|
@ -0,0 +1,84 @@
|
|||
/*
|
||||
* 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
|
||||
|
||||
import com.squareup.moshi.Moshi
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.RestDeviceInfo
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.RestKeyInfo
|
||||
import im.vector.matrix.android.internal.di.SerializeNulls
|
||||
|
||||
object CryptoInfoMapper {
|
||||
|
||||
private val moshi = Moshi.Builder().add(SerializeNulls.JSON_ADAPTER_FACTORY).build()
|
||||
|
||||
fun map(restDeviceInfo: RestDeviceInfo): CryptoDeviceInfo {
|
||||
return CryptoDeviceInfo(
|
||||
deviceId = restDeviceInfo.deviceId,
|
||||
userId = restDeviceInfo.userId,
|
||||
algorithms = restDeviceInfo.algorithms,
|
||||
keys = restDeviceInfo.keys,
|
||||
signatures = restDeviceInfo.signatures,
|
||||
unsigned = restDeviceInfo.unsigned,
|
||||
trustLevel = null
|
||||
)
|
||||
}
|
||||
|
||||
fun map(cryptoDeviceInfo: CryptoDeviceInfo): RestDeviceInfo {
|
||||
return RestDeviceInfo(
|
||||
deviceId = cryptoDeviceInfo.deviceId,
|
||||
algorithms = cryptoDeviceInfo.algorithms,
|
||||
keys = cryptoDeviceInfo.keys,
|
||||
signatures = cryptoDeviceInfo.signatures,
|
||||
unsigned = cryptoDeviceInfo.unsigned,
|
||||
userId = cryptoDeviceInfo.userId
|
||||
)
|
||||
}
|
||||
|
||||
fun map(keyInfo: RestKeyInfo): CryptoCrossSigningKey {
|
||||
return CryptoCrossSigningKey(
|
||||
userId = keyInfo.userId,
|
||||
usages = keyInfo.usages,
|
||||
keys = keyInfo.keys ?: emptyMap(),
|
||||
signatures = keyInfo.signatures,
|
||||
trustLevel = null
|
||||
)
|
||||
}
|
||||
|
||||
fun map(keyInfo: CryptoCrossSigningKey): RestKeyInfo {
|
||||
return RestKeyInfo(
|
||||
userId = keyInfo.userId,
|
||||
usages = keyInfo.usages,
|
||||
keys = keyInfo.keys,
|
||||
signatures = keyInfo.signatures
|
||||
)
|
||||
}
|
||||
|
||||
fun RestDeviceInfo.toCryptoModel(): CryptoDeviceInfo {
|
||||
return map(this)
|
||||
}
|
||||
|
||||
fun CryptoDeviceInfo.toRest(): RestDeviceInfo {
|
||||
return map(this)
|
||||
}
|
||||
|
||||
// fun RestKeyInfo.toCryptoModel(): CryptoCrossSigningKey {
|
||||
// return map(this)
|
||||
// }
|
||||
|
||||
fun CryptoCrossSigningKey.toRest(): RestKeyInfo {
|
||||
return map(this)
|
||||
}
|
||||
}
|
|
@ -22,7 +22,7 @@ data class MXOlmSessionResult(
|
|||
/**
|
||||
* the device
|
||||
*/
|
||||
val deviceInfo: MXDeviceInfo,
|
||||
val deviceInfo: CryptoDeviceInfo,
|
||||
/**
|
||||
* Base64 olm session id.
|
||||
* null if no session could be established.
|
||||
|
|
|
@ -24,5 +24,5 @@ import com.squareup.moshi.JsonClass
|
|||
@JsonClass(generateAdapter = true)
|
||||
internal data class DeleteDeviceParams(
|
||||
@Json(name = "auth")
|
||||
var deleteDeviceAuth: DeleteDeviceAuth? = null
|
||||
var userPasswordAuth: UserPasswordAuth? = null
|
||||
)
|
||||
|
|
|
@ -18,22 +18,25 @@ package im.vector.matrix.android.internal.crypto.model.rest
|
|||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import im.vector.matrix.android.api.util.JsonDict
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class DeviceKeys(
|
||||
@Json(name = "user_id")
|
||||
val userId: String,
|
||||
val userId: String?,
|
||||
|
||||
@Json(name = "device_id")
|
||||
val deviceId: String,
|
||||
val deviceId: String?,
|
||||
|
||||
@Json(name = "algorithms")
|
||||
val algorithms: List<String>,
|
||||
val algorithms: List<String>?,
|
||||
|
||||
@Json(name = "keys")
|
||||
val keys: Map<String, String>,
|
||||
val keys: Map<String, String>?,
|
||||
|
||||
@Json(name = "signatures")
|
||||
val signatures: JsonDict
|
||||
val signatures: Map<String, Map<String, String>>?,
|
||||
|
||||
@Json(name = "usage")
|
||||
val usage: List<String>? = null
|
||||
|
||||
)
|
||||
|
|
|
@ -43,6 +43,8 @@ internal data class KeyVerificationKey(
|
|||
}
|
||||
}
|
||||
|
||||
override fun toSendToDeviceObject() = this
|
||||
|
||||
override fun isValid(): Boolean {
|
||||
if (transactionID.isNullOrBlank() || key.isNullOrBlank()) {
|
||||
return false
|
||||
|
|
|
@ -27,7 +27,7 @@ import im.vector.matrix.android.internal.crypto.verification.VerificationInfoMac
|
|||
internal data class KeyVerificationMac(
|
||||
@Json(name = "transaction_id") override val transactionID: String? = null,
|
||||
@Json(name = "mac") override val mac: Map<String, String>? = null,
|
||||
@Json(name = "key") override val keys: String? = null
|
||||
@Json(name = "keys") override val keys: String? = null
|
||||
|
||||
) : SendToDeviceObject, VerificationInfoMac {
|
||||
|
||||
|
|
|
@ -18,7 +18,7 @@ package im.vector.matrix.android.internal.crypto.model.rest
|
|||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import im.vector.matrix.android.api.session.crypto.sas.SasMode
|
||||
import im.vector.matrix.android.internal.crypto.verification.SASVerificationTransaction
|
||||
import im.vector.matrix.android.internal.crypto.verification.SASDefaultVerificationTransaction
|
||||
import im.vector.matrix.android.internal.crypto.verification.VerificationInfoStart
|
||||
import im.vector.matrix.android.internal.util.JsonCanonicalizer
|
||||
import timber.log.Timber
|
||||
|
@ -34,30 +34,48 @@ internal data class KeyVerificationStart(
|
|||
@Json(name = "key_agreement_protocols") override val keyAgreementProtocols: List<String>? = null,
|
||||
@Json(name = "hashes") override val hashes: List<String>? = null,
|
||||
@Json(name = "message_authentication_codes") override val messageAuthenticationCodes: List<String>? = null,
|
||||
@Json(name = "short_authentication_string") override val shortAuthenticationStrings: List<String>? = null
|
||||
@Json(name = "short_authentication_string") override val shortAuthenticationStrings: List<String>? = null,
|
||||
// For QR code verification
|
||||
@Json(name = "secret") override val sharedSecret: String? = null
|
||||
) : SendToDeviceObject, VerificationInfoStart {
|
||||
|
||||
override fun toCanonicalJson(): String? {
|
||||
return JsonCanonicalizer.getCanonicalJson(KeyVerificationStart::class.java, this)
|
||||
}
|
||||
|
||||
// TODO Move those method to the interface?
|
||||
override fun isValid(): Boolean {
|
||||
if (transactionID.isNullOrBlank()
|
||||
|| fromDevice.isNullOrBlank()
|
||||
|| method !in supportedVerificationMethods
|
||||
|| keyAgreementProtocols.isNullOrEmpty()
|
||||
|| hashes.isNullOrEmpty()
|
||||
|| !hashes.contains("sha256")
|
||||
|| messageAuthenticationCodes.isNullOrEmpty()
|
||||
|| (!messageAuthenticationCodes.contains(SASVerificationTransaction.SAS_MAC_SHA256)
|
||||
&& !messageAuthenticationCodes.contains(SASVerificationTransaction.SAS_MAC_SHA256_LONGKDF))
|
||||
|| shortAuthenticationStrings.isNullOrEmpty()
|
||||
|| !shortAuthenticationStrings.contains(SasMode.DECIMAL)) {
|
||||
|| (method == VERIFICATION_METHOD_SAS && !isValidSas())
|
||||
|| (method == VERIFICATION_METHOD_RECIPROCATE && !isValidReciprocate())) {
|
||||
Timber.e("## received invalid verification request")
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private fun isValidSas(): Boolean {
|
||||
if (keyAgreementProtocols.isNullOrEmpty()
|
||||
|| hashes.isNullOrEmpty()
|
||||
|| !hashes.contains("sha256") || messageAuthenticationCodes.isNullOrEmpty()
|
||||
|| (!messageAuthenticationCodes.contains(SASDefaultVerificationTransaction.SAS_MAC_SHA256)
|
||||
&& !messageAuthenticationCodes.contains(SASDefaultVerificationTransaction.SAS_MAC_SHA256_LONGKDF))
|
||||
|| shortAuthenticationStrings.isNullOrEmpty()
|
||||
|| !shortAuthenticationStrings.contains(SasMode.DECIMAL)) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
private fun isValidReciprocate(): Boolean {
|
||||
if (sharedSecret.isNullOrBlank()) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun toSendToDeviceObject() = this
|
||||
}
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
/*
|
||||
* Copyright 2016 OpenMarket Ltd
|
||||
* Copyright 2017 Vector Creations Ltd
|
||||
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
|
@ -19,10 +18,14 @@ 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
|
||||
|
||||
/**
|
||||
* This class represents the response to /keys/query request made by downloadKeysForUsers
|
||||
*
|
||||
* After uploading cross-signing keys, they will be included under the /keys/query endpoint under the master_keys,
|
||||
* self_signing_keys and user_signing_keys properties.
|
||||
*
|
||||
* The user_signing_keys property will only be included when a user requests their own keys.
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class KeysQueryResponse(
|
||||
|
@ -32,11 +35,21 @@ data class KeysQueryResponse(
|
|||
* TODO Use MXUsersDevicesMap?
|
||||
*/
|
||||
@Json(name = "device_keys")
|
||||
var deviceKeys: Map<String, Map<String, MXDeviceInfo>>? = null,
|
||||
var deviceKeys: Map<String, Map<String, RestDeviceInfo>>? = null,
|
||||
|
||||
/**
|
||||
* The failures sorted by homeservers. TODO Bad comment ?
|
||||
* TODO Use MXUsersDevicesMap?
|
||||
*/
|
||||
var failures: Map<String, Map<String, Any>>? = null
|
||||
var failures: Map<String, Map<String, Any>>? = null,
|
||||
|
||||
@Json(name = "master_keys")
|
||||
var masterKeys: Map<String, RestKeyInfo?>? = null,
|
||||
|
||||
@Json(name = "self_signing_keys")
|
||||
var selfSigningKeys: Map<String, RestKeyInfo?>? = null,
|
||||
|
||||
@Json(name = "user_signing_keys")
|
||||
var userSigningKeys: Map<String, RestKeyInfo?>? = null
|
||||
|
||||
)
|
||||
|
|
|
@ -23,7 +23,7 @@ import im.vector.matrix.android.api.util.JsonDict
|
|||
@JsonClass(generateAdapter = true)
|
||||
data class KeysUploadBody(
|
||||
@Json(name = "device_keys")
|
||||
var deviceKeys: DeviceKeys? = null,
|
||||
var deviceKeys: RestDeviceInfo? = null,
|
||||
|
||||
@Json(name = "one_time_keys")
|
||||
var oneTimeKeys: JsonDict? = null
|
||||
|
|
|
@ -0,0 +1,59 @@
|
|||
/*
|
||||
* 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.api.util.JsonDict
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class RestDeviceInfo(
|
||||
/**
|
||||
* The id of this device.
|
||||
*/
|
||||
@Json(name = "device_id")
|
||||
var deviceId: String,
|
||||
|
||||
/**
|
||||
* the user id
|
||||
*/
|
||||
@Json(name = "user_id")
|
||||
var userId: String,
|
||||
|
||||
/**
|
||||
* The list of algorithms supported by this device.
|
||||
*/
|
||||
@Json(name = "algorithms")
|
||||
var algorithms: List<String>? = null,
|
||||
|
||||
/**
|
||||
* A map from "<key type>:<deviceId>" to "<base64-encoded key>".
|
||||
*/
|
||||
@Json(name = "keys")
|
||||
var keys: Map<String, String>? = null,
|
||||
|
||||
/**
|
||||
* The signature of this MXDeviceInfo.
|
||||
* A map from "<userId>" to a map from "<key type>:<deviceId>" to "<signature>"
|
||||
*/
|
||||
@Json(name = "signatures")
|
||||
var signatures: Map<String, Map<String, String>>? = null,
|
||||
|
||||
/*
|
||||
* Additional data from the home server.
|
||||
*/
|
||||
@Json(name = "unsigned")
|
||||
var unsigned: JsonDict? = null)
|
|
@ -0,0 +1,57 @@
|
|||
/*
|
||||
* 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.crypto.model.CryptoCrossSigningKey
|
||||
import im.vector.matrix.android.internal.crypto.model.CryptoInfoMapper
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class RestKeyInfo(
|
||||
/**
|
||||
* The user who owns the key
|
||||
*/
|
||||
@Json(name = "user_id")
|
||||
val 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")
|
||||
val 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")
|
||||
val signatures: Map<String, Map<String, String>>? = null
|
||||
) {
|
||||
fun toCryptoModel(): CryptoCrossSigningKey {
|
||||
return CryptoInfoMapper.map(this)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
/*
|
||||
* 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
|
||||
|
||||
/**
|
||||
* 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,62 @@
|
|||
/*
|
||||
* 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.CryptoCrossSigningKey
|
||||
import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
|
||||
import im.vector.matrix.android.internal.crypto.model.toRest
|
||||
|
||||
/**
|
||||
* Helper class to build CryptoApi#uploadSignatures params
|
||||
*/
|
||||
data class UploadSignatureQueryBuilder(
|
||||
private val deviceInfoList: ArrayList<CryptoDeviceInfo> = ArrayList(),
|
||||
private val signingKeyInfoList: ArrayList<CryptoCrossSigningKey> = ArrayList()
|
||||
) {
|
||||
|
||||
fun withDeviceInfo(deviceInfo: CryptoDeviceInfo) = apply {
|
||||
deviceInfoList.add(deviceInfo)
|
||||
}
|
||||
|
||||
fun withSigningKeyInfo(info: CryptoCrossSigningKey) = apply {
|
||||
signingKeyInfoList.add(info)
|
||||
}
|
||||
|
||||
fun build(): Map<String, Map<String, @JvmSuppressWildcards Any>> {
|
||||
val map = HashMap<String, HashMap<String, Any>>()
|
||||
|
||||
val usersList = (
|
||||
deviceInfoList.map { it.userId }
|
||||
+ signingKeyInfoList
|
||||
.map { it.userId }
|
||||
).distinct()
|
||||
|
||||
usersList.forEach { userID ->
|
||||
val userMap = HashMap<String, Any>()
|
||||
deviceInfoList.filter { it.userId == userID }.forEach { deviceInfo ->
|
||||
userMap[deviceInfo.deviceId] = deviceInfo.toRest()
|
||||
}
|
||||
signingKeyInfoList.filter { it.userId == userID }.forEach { keyInfo ->
|
||||
keyInfo.unpaddedBase64PublicKey?.let { base64Key ->
|
||||
userMap[base64Key] = keyInfo.toRest()
|
||||
}
|
||||
}
|
||||
map[userID] = userMap
|
||||
}
|
||||
|
||||
return map
|
||||
}
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
* 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)
|
||||
internal data class UploadSigningKeysBody(
|
||||
@Json(name = "master_key")
|
||||
val masterKey: RestKeyInfo? = null,
|
||||
|
||||
@Json(name = "self_signing_key")
|
||||
val selfSigningKey: RestKeyInfo? = null,
|
||||
|
||||
@Json(name = "user_signing_key")
|
||||
val userSigningKey: RestKeyInfo? = null,
|
||||
|
||||
@Json(name = "auth")
|
||||
val auth: UserPasswordAuth? = null
|
||||
)
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
* Copyright 2016 OpenMarket Ltd
|
||||
* Copyright 2020 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
|
@ -17,12 +18,13 @@ package im.vector.matrix.android.internal.crypto.model.rest
|
|||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import im.vector.matrix.android.internal.auth.data.LoginFlowTypes
|
||||
|
||||
/**
|
||||
* This class provides the authentication data to delete a device
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
internal data class DeleteDeviceAuth(
|
||||
data class UserPasswordAuth(
|
||||
|
||||
// device device session id
|
||||
@Json(name = "session")
|
||||
|
@ -30,7 +32,7 @@ internal data class DeleteDeviceAuth(
|
|||
|
||||
// registration information
|
||||
@Json(name = "type")
|
||||
var type: String? = null,
|
||||
var type: String? = LoginFlowTypes.PASSWORD,
|
||||
|
||||
@Json(name = "user")
|
||||
var user: String? = null,
|
|
@ -19,14 +19,17 @@ package im.vector.matrix.android.internal.crypto.model.rest
|
|||
import im.vector.matrix.android.api.session.crypto.sas.VerificationMethod
|
||||
|
||||
internal const val VERIFICATION_METHOD_SAS = "m.sas.v1"
|
||||
internal const val VERIFICATION_METHOD_SCAN = "m.qr_code.scan.v1"
|
||||
|
||||
// Qr code
|
||||
// Ref: https://github.com/uhoreg/matrix-doc/blob/qr_key_verification/proposals/1543-qr_code_key_verification.md#verification-methods
|
||||
internal const val VERIFICATION_METHOD_QR_CODE_SHOW = "m.qr_code.show.v1"
|
||||
internal const val VERIFICATION_METHOD_QR_CODE_SCAN = "m.qr_code.scan.v1"
|
||||
internal const val VERIFICATION_METHOD_RECIPROCATE = "m.reciprocate.v1"
|
||||
|
||||
internal fun VerificationMethod.toValue(): String {
|
||||
return when (this) {
|
||||
VerificationMethod.SAS -> VERIFICATION_METHOD_SAS
|
||||
VerificationMethod.SCAN -> VERIFICATION_METHOD_SCAN
|
||||
VerificationMethod.QR_CODE_SCAN -> VERIFICATION_METHOD_QR_CODE_SCAN
|
||||
VerificationMethod.QR_CODE_SHOW -> VERIFICATION_METHOD_QR_CODE_SHOW
|
||||
}
|
||||
}
|
||||
|
||||
// TODO Add SCAN
|
||||
internal val supportedVerificationMethods = listOf(VERIFICATION_METHOD_SAS)
|
||||
|
|
|
@ -17,10 +17,14 @@
|
|||
|
||||
package im.vector.matrix.android.internal.crypto.store
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import im.vector.matrix.android.api.session.crypto.crosssigning.MXCrossSigningInfo
|
||||
import im.vector.matrix.android.api.util.Optional
|
||||
import im.vector.matrix.android.internal.crypto.IncomingRoomKeyRequest
|
||||
import im.vector.matrix.android.internal.crypto.NewSessionListener
|
||||
import im.vector.matrix.android.internal.crypto.OutgoingRoomKeyRequest
|
||||
import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo
|
||||
import im.vector.matrix.android.internal.crypto.model.CryptoCrossSigningKey
|
||||
import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
|
||||
import im.vector.matrix.android.internal.crypto.model.OlmInboundGroupSessionWrapper
|
||||
import im.vector.matrix.android.internal.crypto.model.OlmSessionWrapper
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody
|
||||
|
@ -154,7 +158,7 @@ internal interface IMXCryptoStore {
|
|||
* @param userId the user's id.
|
||||
* @param device the device to store.
|
||||
*/
|
||||
fun storeUserDevice(userId: String?, deviceInfo: MXDeviceInfo?)
|
||||
fun storeUserDevice(userId: String?, deviceInfo: CryptoDeviceInfo?)
|
||||
|
||||
/**
|
||||
* Retrieve a device for a user.
|
||||
|
@ -163,7 +167,7 @@ internal interface IMXCryptoStore {
|
|||
* @param userId the user's id.
|
||||
* @return the device
|
||||
*/
|
||||
fun getUserDevice(deviceId: String, userId: String): MXDeviceInfo?
|
||||
fun getUserDevice(userId: String, deviceId: String): CryptoDeviceInfo?
|
||||
|
||||
/**
|
||||
* Retrieve a device by its identity key.
|
||||
|
@ -171,7 +175,7 @@ internal interface IMXCryptoStore {
|
|||
* @param identityKey the device identity key (`MXDeviceInfo.identityKey`)
|
||||
* @return the device or null if not found
|
||||
*/
|
||||
fun deviceWithIdentityKey(identityKey: String): MXDeviceInfo?
|
||||
fun deviceWithIdentityKey(identityKey: String): CryptoDeviceInfo?
|
||||
|
||||
/**
|
||||
* Store the known devices for a user.
|
||||
|
@ -179,7 +183,11 @@ internal interface IMXCryptoStore {
|
|||
* @param userId The user's id.
|
||||
* @param devices A map from device id to 'MXDevice' object for the device.
|
||||
*/
|
||||
fun storeUserDevices(userId: String, devices: Map<String, MXDeviceInfo>?)
|
||||
fun storeUserDevices(userId: String, devices: Map<String, CryptoDeviceInfo>?)
|
||||
|
||||
fun storeUserCrossSigningKeys(userId: String, masterKey: CryptoCrossSigningKey?,
|
||||
selfSigningKey: CryptoCrossSigningKey?,
|
||||
userSigningKey: CryptoCrossSigningKey?)
|
||||
|
||||
/**
|
||||
* Retrieve the known devices for a user.
|
||||
|
@ -187,8 +195,11 @@ internal interface IMXCryptoStore {
|
|||
* @param userId The user's id.
|
||||
* @return The devices map if some devices are known, else null
|
||||
*/
|
||||
fun getUserDevices(userId: String): Map<String, MXDeviceInfo>?
|
||||
fun getUserDevices(userId: String): Map<String, CryptoDeviceInfo>?
|
||||
|
||||
fun getUserDeviceList(userId: String): List<CryptoDeviceInfo>?
|
||||
|
||||
fun getLiveDeviceList(userId: String): LiveData<List<CryptoDeviceInfo>>
|
||||
/**
|
||||
* Store the crypto algorithm for a room.
|
||||
*
|
||||
|
@ -381,4 +392,26 @@ internal interface IMXCryptoStore {
|
|||
fun addNewSessionListener(listener: NewSessionListener)
|
||||
|
||||
fun removeSessionListener(listener: NewSessionListener)
|
||||
|
||||
// =============================================
|
||||
// CROSS SIGNING
|
||||
// =============================================
|
||||
|
||||
/**
|
||||
* Gets the current crosssigning info
|
||||
*/
|
||||
fun getMyCrossSigningInfo() : MXCrossSigningInfo?
|
||||
fun setMyCrossSigningInfo(info: MXCrossSigningInfo?)
|
||||
|
||||
fun getCrossSigningInfo(userId: String) : MXCrossSigningInfo?
|
||||
fun getLiveCrossSigningInfo(userId: String) : LiveData<Optional<MXCrossSigningInfo>>
|
||||
fun setCrossSigningInfo(userId: String, info: MXCrossSigningInfo?)
|
||||
|
||||
fun storePrivateKeysInfo(msk: String?, usk: String?, ssk: String?)
|
||||
fun getCrossSigningPrivateKeys() : PrivateKeysInfo?
|
||||
|
||||
fun setUserKeysAsTrusted(userId: String, trusted: Boolean = true)
|
||||
fun setDeviceTrust(userId: String, deviceId: String, crossSignedVerified: Boolean, locallyVerified : Boolean)
|
||||
|
||||
fun clearOtherUserTrust()
|
||||
}
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* 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
|
||||
|
||||
data class PrivateKeysInfo(
|
||||
val master: String? = null,
|
||||
val selfSigned: String? = null,
|
||||
val user: String? = null
|
||||
)
|
|
@ -16,22 +16,55 @@
|
|||
|
||||
package im.vector.matrix.android.internal.crypto.store.db
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.Transformations
|
||||
import com.zhuinden.monarchy.Monarchy
|
||||
import im.vector.matrix.android.api.auth.data.Credentials
|
||||
import im.vector.matrix.android.api.session.crypto.crosssigning.MXCrossSigningInfo
|
||||
import im.vector.matrix.android.api.util.Optional
|
||||
import im.vector.matrix.android.api.util.toOptional
|
||||
import im.vector.matrix.android.internal.crypto.IncomingRoomKeyRequest
|
||||
import im.vector.matrix.android.internal.crypto.NewSessionListener
|
||||
import im.vector.matrix.android.internal.crypto.OutgoingRoomKeyRequest
|
||||
import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo
|
||||
import im.vector.matrix.android.internal.crypto.crosssigning.DeviceTrustLevel
|
||||
import im.vector.matrix.android.internal.crypto.model.CryptoCrossSigningKey
|
||||
import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
|
||||
import im.vector.matrix.android.internal.crypto.model.OlmInboundGroupSessionWrapper
|
||||
import im.vector.matrix.android.internal.crypto.model.OlmSessionWrapper
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody
|
||||
import im.vector.matrix.android.internal.crypto.model.toEntity
|
||||
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
|
||||
import im.vector.matrix.android.internal.crypto.store.db.model.*
|
||||
import im.vector.matrix.android.internal.crypto.store.PrivateKeysInfo
|
||||
import im.vector.matrix.android.internal.crypto.store.db.model.CrossSigningInfoEntity
|
||||
import im.vector.matrix.android.internal.crypto.store.db.model.CrossSigningInfoEntityFields
|
||||
import im.vector.matrix.android.internal.crypto.store.db.model.CryptoMapper
|
||||
import im.vector.matrix.android.internal.crypto.store.db.model.CryptoMetadataEntity
|
||||
import im.vector.matrix.android.internal.crypto.store.db.model.CryptoRoomEntity
|
||||
import im.vector.matrix.android.internal.crypto.store.db.model.CryptoRoomEntityFields
|
||||
import im.vector.matrix.android.internal.crypto.store.db.model.DeviceInfoEntity
|
||||
import im.vector.matrix.android.internal.crypto.store.db.model.DeviceInfoEntityFields
|
||||
import im.vector.matrix.android.internal.crypto.store.db.model.IncomingRoomKeyRequestEntity
|
||||
import im.vector.matrix.android.internal.crypto.store.db.model.IncomingRoomKeyRequestEntityFields
|
||||
import im.vector.matrix.android.internal.crypto.store.db.model.KeyInfoEntity
|
||||
import im.vector.matrix.android.internal.crypto.store.db.model.KeysBackupDataEntity
|
||||
import im.vector.matrix.android.internal.crypto.store.db.model.OlmInboundGroupSessionEntity
|
||||
import im.vector.matrix.android.internal.crypto.store.db.model.OlmInboundGroupSessionEntityFields
|
||||
import im.vector.matrix.android.internal.crypto.store.db.model.OlmSessionEntity
|
||||
import im.vector.matrix.android.internal.crypto.store.db.model.OlmSessionEntityFields
|
||||
import im.vector.matrix.android.internal.crypto.store.db.model.OutgoingRoomKeyRequestEntity
|
||||
import im.vector.matrix.android.internal.crypto.store.db.model.OutgoingRoomKeyRequestEntityFields
|
||||
import im.vector.matrix.android.internal.crypto.store.db.model.TrustLevelEntity
|
||||
import im.vector.matrix.android.internal.crypto.store.db.model.UserEntity
|
||||
import im.vector.matrix.android.internal.crypto.store.db.model.UserEntityFields
|
||||
import im.vector.matrix.android.internal.crypto.store.db.model.createPrimaryKey
|
||||
import im.vector.matrix.android.internal.crypto.store.db.query.delete
|
||||
import im.vector.matrix.android.internal.crypto.store.db.query.get
|
||||
import im.vector.matrix.android.internal.crypto.store.db.query.getById
|
||||
import im.vector.matrix.android.internal.crypto.store.db.query.getOrCreate
|
||||
import im.vector.matrix.android.internal.session.SessionScope
|
||||
import io.realm.Realm
|
||||
import io.realm.RealmConfiguration
|
||||
import io.realm.RealmList
|
||||
import io.realm.Sort
|
||||
import io.realm.kotlin.where
|
||||
import org.matrix.olm.OlmAccount
|
||||
|
@ -69,6 +102,10 @@ internal class RealmCryptoStore(private val realmConfiguration: RealmConfigurati
|
|||
newSessionListeners.remove(listener)
|
||||
}
|
||||
|
||||
private val monarchy = Monarchy.Builder()
|
||||
.setRealmConfiguration(realmConfiguration)
|
||||
.build()
|
||||
|
||||
/* ==========================================================================================
|
||||
* Other data
|
||||
* ========================================================================================== */
|
||||
|
@ -166,20 +203,22 @@ internal class RealmCryptoStore(private val realmConfiguration: RealmConfigurati
|
|||
return olmAccount
|
||||
}
|
||||
|
||||
override fun storeUserDevice(userId: String?, deviceInfo: MXDeviceInfo?) {
|
||||
override fun storeUserDevice(userId: String?, deviceInfo: CryptoDeviceInfo?) {
|
||||
if (userId == null || deviceInfo == null) {
|
||||
return
|
||||
}
|
||||
|
||||
doRealmTransaction(realmConfiguration) {
|
||||
val user = UserEntity.getOrCreate(it, userId)
|
||||
doRealmTransaction(realmConfiguration) { realm ->
|
||||
val user = UserEntity.getOrCreate(realm, userId)
|
||||
|
||||
// Create device info
|
||||
val deviceInfoEntity = DeviceInfoEntity.getOrCreate(it, userId, deviceInfo.deviceId).apply {
|
||||
deviceId = deviceInfo.deviceId
|
||||
identityKey = deviceInfo.identityKey()
|
||||
putDeviceInfo(deviceInfo)
|
||||
}
|
||||
val deviceInfoEntity = CryptoMapper.mapToEntity(deviceInfo)
|
||||
realm.insertOrUpdate(deviceInfoEntity)
|
||||
// val deviceInfoEntity = DeviceInfoEntity.getOrCreate(it, userId, deviceInfo.deviceId).apply {
|
||||
// deviceId = deviceInfo.deviceId
|
||||
// identityKey = deviceInfo.identityKey()
|
||||
// putDeviceInfo(deviceInfo)
|
||||
// }
|
||||
|
||||
if (!user.devices.contains(deviceInfoEntity)) {
|
||||
user.devices.add(deviceInfoEntity)
|
||||
|
@ -187,25 +226,28 @@ internal class RealmCryptoStore(private val realmConfiguration: RealmConfigurati
|
|||
}
|
||||
}
|
||||
|
||||
override fun getUserDevice(deviceId: String, userId: String): MXDeviceInfo? {
|
||||
override fun getUserDevice(userId: String, deviceId: String): CryptoDeviceInfo? {
|
||||
return doRealmQueryAndCopy(realmConfiguration) {
|
||||
it.where<DeviceInfoEntity>()
|
||||
.equalTo(DeviceInfoEntityFields.PRIMARY_KEY, DeviceInfoEntity.createPrimaryKey(userId, deviceId))
|
||||
.findFirst()
|
||||
}?.let {
|
||||
CryptoMapper.mapToModel(it)
|
||||
}
|
||||
?.getDeviceInfo()
|
||||
}
|
||||
|
||||
override fun deviceWithIdentityKey(identityKey: String): MXDeviceInfo? {
|
||||
override fun deviceWithIdentityKey(identityKey: String): CryptoDeviceInfo? {
|
||||
return doRealmQueryAndCopy(realmConfiguration) {
|
||||
it.where<DeviceInfoEntity>()
|
||||
.equalTo(DeviceInfoEntityFields.IDENTITY_KEY, identityKey)
|
||||
.findFirst()
|
||||
}
|
||||
?.getDeviceInfo()
|
||||
?.let {
|
||||
CryptoMapper.mapToModel(it)
|
||||
}
|
||||
}
|
||||
|
||||
override fun storeUserDevices(userId: String, devices: Map<String, MXDeviceInfo>?) {
|
||||
override fun storeUserDevices(userId: String, devices: Map<String, CryptoDeviceInfo>?) {
|
||||
doRealmTransaction(realmConfiguration) { realm ->
|
||||
if (devices == null) {
|
||||
// Remove the user
|
||||
|
@ -216,32 +258,127 @@ internal class RealmCryptoStore(private val realmConfiguration: RealmConfigurati
|
|||
// Add the devices
|
||||
// Ensure all other devices are deleted
|
||||
u.devices.deleteAllFromRealm()
|
||||
val new = devices.map { entry -> entry.value.toEntity() }
|
||||
new.forEach { realm.insertOrUpdate(it) }
|
||||
u.devices.addAll(new)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
u.devices.addAll(
|
||||
devices.map {
|
||||
DeviceInfoEntity.getOrCreate(realm, userId, it.value.deviceId).apply {
|
||||
deviceId = it.value.deviceId
|
||||
identityKey = it.value.identityKey()
|
||||
putDeviceInfo(it.value)
|
||||
override fun storeUserCrossSigningKeys(userId: String,
|
||||
masterKey: CryptoCrossSigningKey?,
|
||||
selfSigningKey: CryptoCrossSigningKey?,
|
||||
userSigningKey: CryptoCrossSigningKey?) {
|
||||
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 storePrivateKeysInfo(msk: String?, usk: String?, ssk: String?) {
|
||||
doRealmTransaction(realmConfiguration) { realm ->
|
||||
realm.where<CryptoMetadataEntity>().findFirst()?.apply {
|
||||
xSignMasterPrivateKey = msk
|
||||
xSignSelfSignedPrivateKey = ssk
|
||||
xSignUserPrivateKey = usk
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getUserDevices(userId: String): Map<String, MXDeviceInfo>? {
|
||||
override fun getUserDevices(userId: String): Map<String, CryptoDeviceInfo>? {
|
||||
return doRealmQueryAndCopy(realmConfiguration) {
|
||||
it.where<UserEntity>()
|
||||
.equalTo(UserEntityFields.USER_ID, userId)
|
||||
.findFirst()
|
||||
}
|
||||
?.devices
|
||||
?.mapNotNull { it.getDeviceInfo() }
|
||||
?.map { CryptoMapper.mapToModel(it) }
|
||||
?.associateBy { it.deviceId }
|
||||
}
|
||||
|
||||
override fun getUserDeviceList(userId: String): List<CryptoDeviceInfo>? {
|
||||
return doRealmQueryAndCopy(realmConfiguration) {
|
||||
it.where<UserEntity>()
|
||||
.equalTo(UserEntityFields.USER_ID, userId)
|
||||
.findFirst()
|
||||
}
|
||||
?.devices
|
||||
?.map { CryptoMapper.mapToModel(it) }
|
||||
}
|
||||
|
||||
override fun getLiveDeviceList(userId: String): LiveData<List<CryptoDeviceInfo>> {
|
||||
val liveData = monarchy.findAllMappedWithChanges(
|
||||
{ realm: Realm ->
|
||||
realm
|
||||
.where<UserEntity>()
|
||||
.equalTo(UserEntityFields.USER_ID, userId)
|
||||
},
|
||||
{ entity ->
|
||||
entity.devices.map { CryptoMapper.mapToModel(it) }
|
||||
}
|
||||
)
|
||||
return Transformations.map(liveData) {
|
||||
it.firstOrNull() ?: emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
override fun storeRoomAlgorithm(roomId: String, algorithm: String) {
|
||||
doRealmTransaction(realmConfiguration) {
|
||||
CryptoRoomEntity.getOrCreate(it, roomId).algorithm = algorithm
|
||||
|
@ -731,4 +868,173 @@ internal class RealmCryptoStore(private val realmConfiguration: RealmConfigurati
|
|||
}
|
||||
.toMutableList()
|
||||
}
|
||||
|
||||
/* ==========================================================================================
|
||||
* Cross Signing
|
||||
* ========================================================================================== */
|
||||
override fun getMyCrossSigningInfo(): MXCrossSigningInfo? {
|
||||
return doRealmQueryAndCopy(realmConfiguration) {
|
||||
it.where<CryptoMetadataEntity>().findFirst()
|
||||
}?.userId?.let {
|
||||
getCrossSigningInfo(it)
|
||||
}
|
||||
}
|
||||
|
||||
override fun setMyCrossSigningInfo(info: MXCrossSigningInfo?) {
|
||||
doRealmTransaction(realmConfiguration) { realm ->
|
||||
realm.where<CryptoMetadataEntity>().findFirst()?.userId?.let { userId ->
|
||||
addOrUpdateCrossSigningInfo(realm, userId, info)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun setUserKeysAsTrusted(userId: String, trusted: Boolean) {
|
||||
doRealmTransaction(realmConfiguration) { realm ->
|
||||
val xInfoEntity = realm.where(CrossSigningInfoEntity::class.java)
|
||||
.equalTo(CrossSigningInfoEntityFields.USER_ID, userId)
|
||||
.findFirst()
|
||||
xInfoEntity?.crossSigningKeys?.forEach { info ->
|
||||
val level = info.trustLevelEntity
|
||||
if (level == null) {
|
||||
val newLevel = realm.createObject(TrustLevelEntity::class.java)
|
||||
newLevel.locallyVerified = trusted
|
||||
newLevel.crossSignedVerified = trusted
|
||||
info.trustLevelEntity = newLevel
|
||||
} else {
|
||||
level.locallyVerified = trusted
|
||||
level.crossSignedVerified = trusted
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun setDeviceTrust(userId: String, deviceId: String, crossSignedVerified: Boolean, locallyVerified: Boolean) {
|
||||
doRealmTransaction(realmConfiguration) { realm ->
|
||||
realm.where(DeviceInfoEntity::class.java)
|
||||
.equalTo(DeviceInfoEntityFields.PRIMARY_KEY, DeviceInfoEntity.createPrimaryKey(userId, deviceId))
|
||||
.findFirst()?.let { deviceInfoEntity ->
|
||||
val trustEntity = deviceInfoEntity.trustLevelEntity
|
||||
if (trustEntity == null) {
|
||||
realm.createObject(TrustLevelEntity::class.java).let {
|
||||
it.locallyVerified = locallyVerified
|
||||
it.crossSignedVerified = crossSignedVerified
|
||||
deviceInfoEntity.trustLevelEntity = it
|
||||
}
|
||||
} else {
|
||||
trustEntity.locallyVerified = locallyVerified
|
||||
trustEntity.crossSignedVerified = crossSignedVerified
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun clearOtherUserTrust() {
|
||||
doRealmTransaction(realmConfiguration) { realm ->
|
||||
val xInfoEntities = realm.where(CrossSigningInfoEntity::class.java)
|
||||
.findAll()
|
||||
xInfoEntities?.forEach { info ->
|
||||
// Need to ignore mine
|
||||
if (info.userId != credentials.userId) {
|
||||
info.crossSigningKeys.forEach {
|
||||
it.trustLevelEntity = null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
CryptoCrossSigningKey(
|
||||
userId = userId,
|
||||
keys = mapOf("ed25519:$pubKey" to pubKey),
|
||||
usages = it.usages.map { it },
|
||||
signatures = it.getSignatures(),
|
||||
trustLevel = it.trustLevelEntity?.let {
|
||||
DeviceTrustLevel(
|
||||
crossSigningVerified = it.crossSignedVerified ?: false,
|
||||
locallyVerified = it.locallyVerified ?: false
|
||||
)
|
||||
}
|
||||
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getLiveCrossSigningInfo(userId: String): LiveData<Optional<MXCrossSigningInfo>> {
|
||||
val liveData = monarchy.findAllMappedWithChanges(
|
||||
{ realm: Realm ->
|
||||
realm.where<CrossSigningInfoEntity>()
|
||||
.equalTo(UserEntityFields.USER_ID, userId)
|
||||
},
|
||||
{ entity ->
|
||||
MXCrossSigningInfo(
|
||||
userId = userId,
|
||||
crossSigningKeys = entity.crossSigningKeys.mapNotNull {
|
||||
val pubKey = it.publicKeyBase64 ?: return@mapNotNull null
|
||||
CryptoCrossSigningKey(
|
||||
userId = userId,
|
||||
keys = mapOf("ed25519:$pubKey" to pubKey),
|
||||
usages = it.usages.map { it },
|
||||
signatures = it.getSignatures(),
|
||||
trustLevel = it.trustLevelEntity?.let {
|
||||
DeviceTrustLevel(
|
||||
crossSigningVerified = it.crossSignedVerified ?: false,
|
||||
locallyVerified = it.locallyVerified ?: false
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
return Transformations.map(liveData) {
|
||||
it.firstOrNull().toOptional()
|
||||
}
|
||||
}
|
||||
|
||||
override fun setCrossSigningInfo(userId: String, info: MXCrossSigningInfo?) {
|
||||
doRealmTransaction(realmConfiguration) { realm ->
|
||||
addOrUpdateCrossSigningInfo(realm, userId, info)
|
||||
}
|
||||
}
|
||||
|
||||
private fun addOrUpdateCrossSigningInfo(realm: Realm, userId: String, info: MXCrossSigningInfo?): CrossSigningInfoEntity? {
|
||||
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)
|
||||
// existing.crossSigningKeys.forEach { it.deleteFromRealm() }
|
||||
val xkeys = RealmList<KeyInfoEntity>()
|
||||
info.crossSigningKeys.forEach { cryptoCrossSigningKey ->
|
||||
xkeys.add(
|
||||
realm.createObject(KeyInfoEntity::class.java).also { keyInfoEntity ->
|
||||
keyInfoEntity.publicKeyBase64 = cryptoCrossSigningKey.unpaddedBase64PublicKey
|
||||
keyInfoEntity.usages = cryptoCrossSigningKey.usages?.let { RealmList(*it.toTypedArray()) }
|
||||
?: RealmList()
|
||||
keyInfoEntity.putSignatures(cryptoCrossSigningKey.signatures)
|
||||
// TODO how to handle better, check if same keys?
|
||||
// reset trust
|
||||
keyInfoEntity.trustLevelEntity = null
|
||||
}
|
||||
)
|
||||
}
|
||||
existing.crossSigningKeys = xkeys
|
||||
}
|
||||
return existing
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,15 +16,120 @@
|
|||
|
||||
package im.vector.matrix.android.internal.crypto.store.db
|
||||
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.squareup.moshi.Types
|
||||
import im.vector.matrix.android.api.util.JsonDict
|
||||
import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo
|
||||
import im.vector.matrix.android.internal.crypto.store.db.model.CrossSigningInfoEntityFields
|
||||
import im.vector.matrix.android.internal.crypto.store.db.model.CryptoMetadataEntityFields
|
||||
import im.vector.matrix.android.internal.crypto.store.db.model.DeviceInfoEntityFields
|
||||
import im.vector.matrix.android.internal.crypto.store.db.model.KeyInfoEntityFields
|
||||
import im.vector.matrix.android.internal.crypto.store.db.model.TrustLevelEntityFields
|
||||
import im.vector.matrix.android.internal.crypto.store.db.model.UserEntityFields
|
||||
import im.vector.matrix.android.internal.di.SerializeNulls
|
||||
import io.realm.DynamicRealm
|
||||
import io.realm.RealmMigration
|
||||
import timber.log.Timber
|
||||
|
||||
internal object RealmCryptoStoreMigration : RealmMigration {
|
||||
|
||||
const val CRYPTO_STORE_SCHEMA_VERSION = 0L
|
||||
// Version 1L added Cross Signing info persistence
|
||||
const val CRYPTO_STORE_SCHEMA_VERSION = 1L
|
||||
|
||||
override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) {
|
||||
Timber.v("Migrating Realm Crypto from $oldVersion to $newVersion")
|
||||
|
||||
if (oldVersion <= 0) {
|
||||
Timber.d("Step 0 -> 1")
|
||||
Timber.d("Create KeyInfoEntity")
|
||||
|
||||
val trustLevelentityEntitySchema = realm.schema.create("TrustLevelEntity")
|
||||
.addField(TrustLevelEntityFields.CROSS_SIGNED_VERIFIED, Boolean::class.java)
|
||||
.setNullable(TrustLevelEntityFields.CROSS_SIGNED_VERIFIED, true)
|
||||
.addField(TrustLevelEntityFields.LOCALLY_VERIFIED, Boolean::class.java)
|
||||
.setNullable(TrustLevelEntityFields.LOCALLY_VERIFIED, true)
|
||||
|
||||
val keyInfoEntitySchema = realm.schema.create("KeyInfoEntity")
|
||||
.addField(KeyInfoEntityFields.PUBLIC_KEY_BASE64, String::class.java)
|
||||
.addField(KeyInfoEntityFields.SIGNATURES, String::class.java)
|
||||
.addRealmListField(KeyInfoEntityFields.USAGES.`$`, String::class.java)
|
||||
.addRealmObjectField(KeyInfoEntityFields.TRUST_LEVEL_ENTITY.`$`, trustLevelentityEntitySchema)
|
||||
|
||||
Timber.d("Create CrossSigningInfoEntity")
|
||||
|
||||
val crossSigningInfoSchema = realm.schema.create("CrossSigningInfoEntity")
|
||||
.addField(CrossSigningInfoEntityFields.USER_ID, String::class.java)
|
||||
.addPrimaryKey(CrossSigningInfoEntityFields.USER_ID)
|
||||
.addRealmListField(CrossSigningInfoEntityFields.CROSS_SIGNING_KEYS.`$`, keyInfoEntitySchema)
|
||||
|
||||
Timber.d("Updating 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)
|
||||
|
||||
val moshi = Moshi.Builder().add(SerializeNulls.JSON_ADAPTER_FACTORY).build()
|
||||
val listMigrationAdapter = moshi.adapter<List<String>>(Types.newParameterizedType(
|
||||
List::class.java,
|
||||
String::class.java,
|
||||
Any::class.java
|
||||
))
|
||||
val mapMigrationAdapter = moshi.adapter<JsonDict>(Types.newParameterizedType(
|
||||
Map::class.java,
|
||||
String::class.java,
|
||||
Any::class.java
|
||||
))
|
||||
|
||||
realm.schema.get("DeviceInfoEntity")
|
||||
?.addField(DeviceInfoEntityFields.USER_ID, String::class.java)
|
||||
?.addField(DeviceInfoEntityFields.ALGORITHM_LIST_JSON, String::class.java)
|
||||
?.addField(DeviceInfoEntityFields.KEYS_MAP_JSON, String::class.java)
|
||||
?.addField(DeviceInfoEntityFields.SIGNATURE_MAP_JSON, String::class.java)
|
||||
?.addField(DeviceInfoEntityFields.UNSIGNED_MAP_JSON, String::class.java)
|
||||
?.addField(DeviceInfoEntityFields.IS_BLOCKED, Boolean::class.java)
|
||||
?.setNullable(DeviceInfoEntityFields.IS_BLOCKED, true)
|
||||
?.addRealmObjectField(DeviceInfoEntityFields.TRUST_LEVEL_ENTITY.`$`, trustLevelentityEntitySchema)
|
||||
?.transform { obj ->
|
||||
|
||||
val oldSerializedData = obj.getString("deviceInfoData")
|
||||
deserializeFromRealm<MXDeviceInfo>(oldSerializedData)?.let { oldDevice ->
|
||||
|
||||
val trustLevel = realm.createObject("TrustLevelEntity")
|
||||
when (oldDevice.verified) {
|
||||
MXDeviceInfo.DEVICE_VERIFICATION_UNKNOWN -> {
|
||||
obj.setNull(DeviceInfoEntityFields.TRUST_LEVEL_ENTITY.`$`)
|
||||
}
|
||||
MXDeviceInfo.DEVICE_VERIFICATION_BLOCKED -> {
|
||||
trustLevel.setNull(TrustLevelEntityFields.LOCALLY_VERIFIED)
|
||||
trustLevel.setNull(TrustLevelEntityFields.CROSS_SIGNED_VERIFIED)
|
||||
obj.setBoolean(DeviceInfoEntityFields.IS_BLOCKED, oldDevice.isBlocked)
|
||||
obj.setObject(DeviceInfoEntityFields.TRUST_LEVEL_ENTITY.`$`, trustLevel)
|
||||
}
|
||||
MXDeviceInfo.DEVICE_VERIFICATION_UNVERIFIED -> {
|
||||
trustLevel.setBoolean(TrustLevelEntityFields.LOCALLY_VERIFIED, false)
|
||||
trustLevel.setBoolean(TrustLevelEntityFields.CROSS_SIGNED_VERIFIED, false)
|
||||
obj.setObject(DeviceInfoEntityFields.TRUST_LEVEL_ENTITY.`$`, trustLevel)
|
||||
}
|
||||
MXDeviceInfo.DEVICE_VERIFICATION_VERIFIED -> {
|
||||
trustLevel.setBoolean(TrustLevelEntityFields.LOCALLY_VERIFIED, true)
|
||||
trustLevel.setBoolean(TrustLevelEntityFields.CROSS_SIGNED_VERIFIED, false)
|
||||
obj.setObject(DeviceInfoEntityFields.TRUST_LEVEL_ENTITY.`$`, trustLevel)
|
||||
}
|
||||
}
|
||||
|
||||
obj.setString(DeviceInfoEntityFields.USER_ID, oldDevice.userId)
|
||||
obj.setString(DeviceInfoEntityFields.IDENTITY_KEY, oldDevice.identityKey())
|
||||
obj.setString(DeviceInfoEntityFields.ALGORITHM_LIST_JSON, listMigrationAdapter.toJson(oldDevice.algorithms))
|
||||
obj.setString(DeviceInfoEntityFields.KEYS_MAP_JSON, mapMigrationAdapter.toJson(oldDevice.keys))
|
||||
obj.setString(DeviceInfoEntityFields.SIGNATURE_MAP_JSON, mapMigrationAdapter.toJson(oldDevice.signatures))
|
||||
obj.setString(DeviceInfoEntityFields.UNSIGNED_MAP_JSON, mapMigrationAdapter.toJson(oldDevice.unsigned))
|
||||
}
|
||||
}
|
||||
?.removeField("deviceInfoData")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -32,6 +32,9 @@ import io.realm.annotations.RealmModule
|
|||
OlmInboundGroupSessionEntity::class,
|
||||
OlmSessionEntity::class,
|
||||
OutgoingRoomKeyRequestEntity::class,
|
||||
UserEntity::class
|
||||
UserEntity::class,
|
||||
KeyInfoEntity::class,
|
||||
CrossSigningInfoEntity::class,
|
||||
TrustLevelEntity::class
|
||||
])
|
||||
internal class RealmCryptoStoreModule
|
||||
|
|
|
@ -0,0 +1,49 @@
|
|||
/*
|
||||
* 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.KeyUsage
|
||||
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()
|
||||
) : RealmObject() {
|
||||
|
||||
companion object
|
||||
|
||||
fun getMasterKey() = crossSigningKeys.firstOrNull { it.usages.contains(KeyUsage.MASTER.value) }
|
||||
|
||||
fun setMasterKey(info: KeyInfoEntity?) {
|
||||
crossSigningKeys
|
||||
.filter { it.usages.contains(KeyUsage.MASTER.value) }
|
||||
.forEach { crossSigningKeys.remove(it) }
|
||||
info?.let { crossSigningKeys.add(it) }
|
||||
}
|
||||
|
||||
fun getSelfSignedKey() = crossSigningKeys.firstOrNull { it.usages.contains(KeyUsage.SELF_SIGNING.value) }
|
||||
|
||||
fun setSelfSignedKey(info: KeyInfoEntity?) {
|
||||
crossSigningKeys
|
||||
.filter { it.usages.contains(KeyUsage.SELF_SIGNING.value) }
|
||||
.forEach { crossSigningKeys.remove(it) }
|
||||
info?.let { crossSigningKeys.add(it) }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,110 @@
|
|||
/*
|
||||
* 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 com.squareup.moshi.Moshi
|
||||
import com.squareup.moshi.Types
|
||||
import im.vector.matrix.android.api.util.JsonDict
|
||||
import im.vector.matrix.android.internal.crypto.crosssigning.DeviceTrustLevel
|
||||
import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
|
||||
import im.vector.matrix.android.internal.di.SerializeNulls
|
||||
import timber.log.Timber
|
||||
|
||||
object CryptoMapper {
|
||||
|
||||
private val moshi = Moshi.Builder().add(SerializeNulls.JSON_ADAPTER_FACTORY).build()
|
||||
private val listMigrationAdapter = moshi.adapter<List<String>>(Types.newParameterizedType(
|
||||
List::class.java,
|
||||
String::class.java,
|
||||
Any::class.java
|
||||
))
|
||||
private val mapMigrationAdapter = moshi.adapter<JsonDict>(Types.newParameterizedType(
|
||||
Map::class.java,
|
||||
String::class.java,
|
||||
Any::class.java
|
||||
))
|
||||
private val mapOfStringMigrationAdapter = moshi.adapter<Map<String, Map<String, String>>>(Types.newParameterizedType(
|
||||
Map::class.java,
|
||||
String::class.java,
|
||||
Any::class.java
|
||||
))
|
||||
|
||||
internal fun mapToEntity(deviceInfo: CryptoDeviceInfo): DeviceInfoEntity {
|
||||
return DeviceInfoEntity(
|
||||
primaryKey = DeviceInfoEntity.createPrimaryKey(deviceInfo.userId, deviceInfo.deviceId),
|
||||
userId = deviceInfo.userId,
|
||||
deviceId = deviceInfo.deviceId,
|
||||
algorithmListJson = listMigrationAdapter.toJson(deviceInfo.algorithms),
|
||||
keysMapJson = mapMigrationAdapter.toJson(deviceInfo.keys),
|
||||
signatureMapJson = mapMigrationAdapter.toJson(deviceInfo.signatures),
|
||||
isBlocked = deviceInfo.isBlocked,
|
||||
trustLevelEntity = deviceInfo.trustLevel?.let {
|
||||
TrustLevelEntity(
|
||||
crossSignedVerified = it.crossSigningVerified,
|
||||
locallyVerified = it.locallyVerified
|
||||
)
|
||||
},
|
||||
unsignedMapJson = mapMigrationAdapter.toJson(deviceInfo.unsigned)
|
||||
)
|
||||
}
|
||||
|
||||
internal fun mapToModel(deviceInfoEntity: DeviceInfoEntity): CryptoDeviceInfo {
|
||||
return CryptoDeviceInfo(
|
||||
userId = deviceInfoEntity.userId ?: "",
|
||||
deviceId = deviceInfoEntity.deviceId ?: "",
|
||||
isBlocked = deviceInfoEntity.isBlocked ?: false,
|
||||
trustLevel = deviceInfoEntity.trustLevelEntity?.let {
|
||||
DeviceTrustLevel(it.crossSignedVerified ?: false, it.locallyVerified)
|
||||
},
|
||||
unsigned = deviceInfoEntity.unsignedMapJson?.let {
|
||||
try {
|
||||
mapMigrationAdapter.fromJson(it)
|
||||
} catch (failure: Throwable) {
|
||||
Timber.e(failure)
|
||||
null
|
||||
}
|
||||
},
|
||||
signatures = deviceInfoEntity.signatureMapJson?.let {
|
||||
try {
|
||||
mapOfStringMigrationAdapter.fromJson(it)
|
||||
} catch (failure: Throwable) {
|
||||
Timber.e(failure)
|
||||
null
|
||||
}
|
||||
},
|
||||
keys = deviceInfoEntity.keysMapJson?.let {
|
||||
try {
|
||||
moshi.adapter<Map<String, String>>(Types.newParameterizedType(
|
||||
Map::class.java,
|
||||
String::class.java,
|
||||
Any::class.java
|
||||
)).fromJson(it)
|
||||
} catch (failure: Throwable) {
|
||||
Timber.e(failure)
|
||||
null
|
||||
}
|
||||
},
|
||||
algorithms = deviceInfoEntity.algorithmListJson?.let {
|
||||
try {
|
||||
listMigrationAdapter.fromJson(it)
|
||||
} catch (failure: Throwable) {
|
||||
Timber.e(failure)
|
||||
null
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
|
@ -34,7 +34,13 @@ internal open class CryptoMetadataEntity(
|
|||
// Settings for blacklisting unverified devices.
|
||||
var globalBlacklistUnverifiedDevices: Boolean = false,
|
||||
// The keys backup version currently used. Null means no backup.
|
||||
var backupVersion: String? = null
|
||||
var backupVersion: String? = null,
|
||||
|
||||
var xSignMasterPrivateKey: String? = null,
|
||||
var xSignUserPrivateKey: String? = null,
|
||||
var xSignSelfSignedPrivateKey: String? = null
|
||||
|
||||
// var crossSigningInfoEntity: CrossSigningInfoEntity? = null
|
||||
) : RealmObject() {
|
||||
|
||||
// Deserialize data
|
||||
|
|
|
@ -16,9 +16,6 @@
|
|||
|
||||
package im.vector.matrix.android.internal.crypto.store.db.model
|
||||
|
||||
import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo
|
||||
import im.vector.matrix.android.internal.crypto.store.db.deserializeFromRealm
|
||||
import im.vector.matrix.android.internal.crypto.store.db.serializeForRealm
|
||||
import io.realm.RealmObject
|
||||
import io.realm.RealmResults
|
||||
import io.realm.annotations.LinkingObjects
|
||||
|
@ -30,18 +27,26 @@ internal fun DeviceInfoEntity.Companion.createPrimaryKey(userId: String, deviceI
|
|||
internal open class DeviceInfoEntity(@PrimaryKey var primaryKey: String = "",
|
||||
var deviceId: String? = null,
|
||||
var identityKey: String? = null,
|
||||
var deviceInfoData: String? = null)
|
||||
// var deviceInfoData: String? = null,
|
||||
var userId: String? = null,
|
||||
var isBlocked: Boolean? = null,
|
||||
var algorithmListJson: String? = null,
|
||||
var keysMapJson: String? = null,
|
||||
var signatureMapJson: String? = null,
|
||||
var unsignedMapJson: String? = null,
|
||||
var trustLevelEntity: TrustLevelEntity? = null
|
||||
)
|
||||
: RealmObject() {
|
||||
|
||||
// Deserialize data
|
||||
fun getDeviceInfo(): MXDeviceInfo? {
|
||||
return deserializeFromRealm(deviceInfoData)
|
||||
}
|
||||
|
||||
// Serialize data
|
||||
fun putDeviceInfo(deviceInfo: MXDeviceInfo?) {
|
||||
deviceInfoData = serializeForRealm(deviceInfo)
|
||||
}
|
||||
// // Deserialize data
|
||||
// fun getDeviceInfo(): MXDeviceInfo? {
|
||||
// return deserializeFromRealm(deviceInfoData)
|
||||
// }
|
||||
//
|
||||
// // Serialize data
|
||||
// fun putDeviceInfo(deviceInfo: MXDeviceInfo?) {
|
||||
// deviceInfoData = serializeForRealm(deviceInfo)
|
||||
// }
|
||||
|
||||
@LinkingObjects("devices")
|
||||
val users: RealmResults<UserEntity>? = null
|
||||
|
|
|
@ -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,
|
||||
var trustLevelEntity: TrustLevelEntity? = 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)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
package im.vector.matrix.android.internal.crypto.store.db.model
|
||||
|
||||
import io.realm.RealmObject
|
||||
|
||||
internal open class TrustLevelEntity(
|
||||
var crossSignedVerified: Boolean? = null,
|
||||
var locallyVerified: Boolean? = null
|
||||
) : RealmObject() {
|
||||
|
||||
companion object
|
||||
|
||||
fun isVerified() : Boolean = crossSignedVerified == true || locallyVerified == true
|
||||
}
|
|
@ -20,8 +20,10 @@ import io.realm.RealmList
|
|||
import io.realm.RealmObject
|
||||
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 crossSigningInfoEntity: CrossSigningInfoEntity? = null,
|
||||
var deviceTrackingStatus: Int = 0)
|
||||
: RealmObject() {
|
||||
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
/*
|
||||
* Copyright 2020 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.internal.crypto.store.db.query
|
||||
|
||||
import im.vector.matrix.android.internal.crypto.store.db.model.CrossSigningInfoEntity
|
||||
import im.vector.matrix.android.internal.crypto.store.db.model.UserEntityFields
|
||||
import io.realm.Realm
|
||||
import io.realm.kotlin.createObject
|
||||
import io.realm.kotlin.where
|
||||
|
||||
internal fun CrossSigningInfoEntity.Companion.getOrCreate(realm: Realm, userId: String): CrossSigningInfoEntity {
|
||||
return realm.where<CrossSigningInfoEntity>()
|
||||
.equalTo(UserEntityFields.USER_ID, userId)
|
||||
.findFirst()
|
||||
?: realm.createObject(userId)
|
||||
}
|
||||
|
||||
internal fun CrossSigningInfoEntity.Companion.get(realm: Realm, userId: String): CrossSigningInfoEntity? {
|
||||
return realm.where<CrossSigningInfoEntity>()
|
||||
.equalTo(UserEntityFields.USER_ID, userId)
|
||||
.findFirst()
|
||||
}
|
|
@ -18,7 +18,7 @@ package im.vector.matrix.android.internal.crypto.tasks
|
|||
|
||||
import im.vector.matrix.android.internal.auth.data.LoginFlowTypes
|
||||
import im.vector.matrix.android.internal.crypto.api.CryptoApi
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.DeleteDeviceAuth
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.UserPasswordAuth
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.DeleteDeviceParams
|
||||
import im.vector.matrix.android.internal.di.UserId
|
||||
import im.vector.matrix.android.internal.network.executeRequest
|
||||
|
@ -44,7 +44,7 @@ internal class DefaultDeleteDeviceWithUserPasswordTask @Inject constructor(
|
|||
return executeRequest(eventBus) {
|
||||
apiCall = cryptoApi.deleteDevice(params.deviceId, DeleteDeviceParams()
|
||||
.apply {
|
||||
deleteDeviceAuth = DeleteDeviceAuth()
|
||||
userPasswordAuth = UserPasswordAuth()
|
||||
.apply {
|
||||
type = LoginFlowTypes.PASSWORD
|
||||
session = params.authSession
|
||||
|
|
|
@ -18,13 +18,13 @@ package im.vector.matrix.android.internal.crypto.tasks
|
|||
|
||||
import im.vector.matrix.android.api.session.crypto.CryptoService
|
||||
import im.vector.matrix.android.api.session.crypto.MXCryptoError
|
||||
import im.vector.matrix.android.api.session.crypto.sas.SasVerificationService
|
||||
import im.vector.matrix.android.api.session.crypto.sas.VerificationService
|
||||
import im.vector.matrix.android.api.session.events.model.Event
|
||||
import im.vector.matrix.android.api.session.events.model.EventType
|
||||
import im.vector.matrix.android.api.session.events.model.toModel
|
||||
import im.vector.matrix.android.api.session.room.model.message.*
|
||||
import im.vector.matrix.android.internal.crypto.algorithms.olm.OlmDecryptionResult
|
||||
import im.vector.matrix.android.internal.crypto.verification.DefaultSasVerificationService
|
||||
import im.vector.matrix.android.internal.crypto.verification.DefaultVerificationService
|
||||
import im.vector.matrix.android.internal.di.DeviceId
|
||||
import im.vector.matrix.android.internal.di.UserId
|
||||
import im.vector.matrix.android.internal.task.Task
|
||||
|
@ -35,7 +35,7 @@ import javax.inject.Inject
|
|||
internal interface RoomVerificationUpdateTask : Task<RoomVerificationUpdateTask.Params, Unit> {
|
||||
data class Params(
|
||||
val events: List<Event>,
|
||||
val sasVerificationService: DefaultSasVerificationService,
|
||||
val sasVerificationService: DefaultVerificationService,
|
||||
val cryptoService: CryptoService
|
||||
)
|
||||
}
|
||||
|
@ -60,7 +60,7 @@ internal class DefaultRoomVerificationUpdateTask @Inject constructor(
|
|||
// If the request is in the future by more than 5 minutes or more than 10 minutes in the past,
|
||||
// the message should be ignored by the receiver.
|
||||
|
||||
if (!SasVerificationService.isValidRequest(event.ageLocalTs
|
||||
if (!VerificationService.isValidRequest(event.ageLocalTs
|
||||
?: event.originServerTs)) return@forEach Unit.also {
|
||||
Timber.d("## SAS Verification live observer: msgId: ${event.eventId} is outdated")
|
||||
}
|
||||
|
|
|
@ -18,9 +18,9 @@ package im.vector.matrix.android.internal.crypto.tasks
|
|||
|
||||
import im.vector.matrix.android.api.util.JsonDict
|
||||
import im.vector.matrix.android.internal.crypto.api.CryptoApi
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.DeviceKeys
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.KeysUploadBody
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.KeysUploadResponse
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.RestDeviceInfo
|
||||
import im.vector.matrix.android.internal.network.executeRequest
|
||||
import im.vector.matrix.android.internal.task.Task
|
||||
import im.vector.matrix.android.internal.util.convertToUTF8
|
||||
|
@ -30,7 +30,7 @@ import javax.inject.Inject
|
|||
internal interface UploadKeysTask : Task<UploadKeysTask.Params, KeysUploadResponse> {
|
||||
data class Params(
|
||||
// the device keys to send.
|
||||
val deviceKeys: DeviceKeys?,
|
||||
val deviceKeys: RestDeviceInfo?,
|
||||
// the one-time keys to send.
|
||||
val oneTimeKeys: JsonDict?,
|
||||
// the explicit device_id to use for upload (default is to use the same as that used during auth).
|
||||
|
|
|
@ -0,0 +1,50 @@
|
|||
/*
|
||||
* 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.crypto.api.CryptoApi
|
||||
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, Unit> {
|
||||
data class Params(
|
||||
val signatures: Map<String, Map<String, Any>>
|
||||
)
|
||||
}
|
||||
|
||||
internal class DefaultUploadSignaturesTask @Inject constructor(
|
||||
private val cryptoApi: CryptoApi,
|
||||
private val eventBus: EventBus
|
||||
) : UploadSignaturesTask {
|
||||
|
||||
override suspend fun execute(params: UploadSignaturesTask.Params) {
|
||||
try {
|
||||
val response = executeRequest<SignatureUploadResponse>(eventBus) {
|
||||
apiCall = cryptoApi.uploadSignatures(params.signatures)
|
||||
}
|
||||
if (response.failures?.isNotEmpty() == true) {
|
||||
throw Throwable(response.failures.toString())
|
||||
}
|
||||
return
|
||||
} catch (f: Failure) {
|
||||
throw f
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,101 @@
|
|||
/*
|
||||
* 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.CryptoCrossSigningKey
|
||||
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.crypto.model.toRest
|
||||
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, Unit> {
|
||||
data class Params(
|
||||
// the device keys to send.
|
||||
val masterKey: CryptoCrossSigningKey,
|
||||
// the one-time keys to send.
|
||||
val userKey: CryptoCrossSigningKey,
|
||||
// the explicit device_id to use for upload (default is to use the same as that used during auth).
|
||||
val selfSignedKey: CryptoCrossSigningKey,
|
||||
val userPasswordAuth: UserPasswordAuth?
|
||||
)
|
||||
}
|
||||
|
||||
data class UploadSigningKeys(val failures: Map<String, Any>?) : Failure.FeatureFailure()
|
||||
|
||||
internal class DefaultUploadSigningKeysTask @Inject constructor(
|
||||
private val cryptoApi: CryptoApi,
|
||||
private val eventBus: EventBus
|
||||
) : UploadSigningKeysTask {
|
||||
override suspend fun execute(params: UploadSigningKeysTask.Params) {
|
||||
val uploadQuery = UploadSigningKeysBody(
|
||||
masterKey = params.masterKey.toRest(),
|
||||
userSigningKey = params.userKey.toRest(),
|
||||
selfSigningKey = params.selfSignedKey.toRest(),
|
||||
auth = params.userPasswordAuth.takeIf { params.userPasswordAuth?.session != null }
|
||||
)
|
||||
try {
|
||||
// Make a first request to start user-interactive authentication
|
||||
val request = executeRequest<KeysQueryResponse>(eventBus) {
|
||||
apiCall = cryptoApi.uploadSigningKeys(uploadQuery)
|
||||
}
|
||||
if (request.failures?.isNotEmpty() == true) {
|
||||
throw UploadSigningKeys(request.failures)
|
||||
}
|
||||
return
|
||||
} 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
|
||||
try {
|
||||
val req = executeRequest<KeysQueryResponse>(eventBus) {
|
||||
apiCall = cryptoApi.uploadSigningKeys(
|
||||
uploadQuery.copy(auth = params.userPasswordAuth.copy(session = it.session))
|
||||
)
|
||||
}
|
||||
if (req.failures?.isNotEmpty() == true) {
|
||||
throw UploadSigningKeys(req.failures)
|
||||
}
|
||||
return
|
||||
} catch (failure: Throwable) {
|
||||
throw failure
|
||||
}
|
||||
}
|
||||
}
|
||||
// Other error
|
||||
throw throwable
|
||||
}
|
||||
}
|
||||
}
|
|
@ -17,65 +17,69 @@ package im.vector.matrix.android.internal.crypto.verification
|
|||
|
||||
import android.util.Base64
|
||||
import im.vector.matrix.android.BuildConfig
|
||||
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.sas.CancelCode
|
||||
import im.vector.matrix.android.api.session.crypto.sas.IncomingSasVerificationTransaction
|
||||
import im.vector.matrix.android.api.session.crypto.sas.SasMode
|
||||
import im.vector.matrix.android.api.session.crypto.sas.SasVerificationTxState
|
||||
import im.vector.matrix.android.api.session.crypto.sas.VerificationTxState
|
||||
import im.vector.matrix.android.api.session.events.model.EventType
|
||||
import im.vector.matrix.android.internal.crypto.actions.SetDeviceVerificationAction
|
||||
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
|
||||
import timber.log.Timber
|
||||
|
||||
internal class DefaultIncomingSASVerificationTransaction(
|
||||
internal class DefaultIncomingSASDefaultVerificationTransaction(
|
||||
setDeviceVerificationAction: SetDeviceVerificationAction,
|
||||
override val credentials: Credentials,
|
||||
override val userId: String,
|
||||
override val deviceId: String?,
|
||||
private val cryptoStore: IMXCryptoStore,
|
||||
crossSigningService: CrossSigningService,
|
||||
deviceFingerprint: String,
|
||||
transactionId: String,
|
||||
otherUserID: String,
|
||||
val autoAccept: Boolean = false
|
||||
) : SASVerificationTransaction(
|
||||
private val autoAccept: Boolean = false
|
||||
) : SASDefaultVerificationTransaction(
|
||||
setDeviceVerificationAction,
|
||||
credentials,
|
||||
userId,
|
||||
deviceId,
|
||||
cryptoStore,
|
||||
crossSigningService,
|
||||
deviceFingerprint,
|
||||
transactionId,
|
||||
otherUserID,
|
||||
null,
|
||||
true),
|
||||
isIncoming = true),
|
||||
IncomingSasVerificationTransaction {
|
||||
|
||||
override val uxState: IncomingSasVerificationTransaction.UxState
|
||||
get() {
|
||||
return when (state) {
|
||||
SasVerificationTxState.OnStarted -> IncomingSasVerificationTransaction.UxState.SHOW_ACCEPT
|
||||
SasVerificationTxState.SendingAccept,
|
||||
SasVerificationTxState.Accepted,
|
||||
SasVerificationTxState.OnKeyReceived,
|
||||
SasVerificationTxState.SendingKey,
|
||||
SasVerificationTxState.KeySent -> IncomingSasVerificationTransaction.UxState.WAIT_FOR_KEY_AGREEMENT
|
||||
SasVerificationTxState.ShortCodeReady -> IncomingSasVerificationTransaction.UxState.SHOW_SAS
|
||||
SasVerificationTxState.ShortCodeAccepted,
|
||||
SasVerificationTxState.SendingMac,
|
||||
SasVerificationTxState.MacSent,
|
||||
SasVerificationTxState.Verifying -> IncomingSasVerificationTransaction.UxState.WAIT_FOR_VERIFICATION
|
||||
SasVerificationTxState.Verified -> IncomingSasVerificationTransaction.UxState.VERIFIED
|
||||
SasVerificationTxState.Cancelled -> IncomingSasVerificationTransaction.UxState.CANCELLED_BY_ME
|
||||
SasVerificationTxState.OnCancelled -> IncomingSasVerificationTransaction.UxState.CANCELLED_BY_OTHER
|
||||
VerificationTxState.OnStarted -> IncomingSasVerificationTransaction.UxState.SHOW_ACCEPT
|
||||
VerificationTxState.SendingAccept,
|
||||
VerificationTxState.Accepted,
|
||||
VerificationTxState.OnKeyReceived,
|
||||
VerificationTxState.SendingKey,
|
||||
VerificationTxState.KeySent -> IncomingSasVerificationTransaction.UxState.WAIT_FOR_KEY_AGREEMENT
|
||||
VerificationTxState.ShortCodeReady -> IncomingSasVerificationTransaction.UxState.SHOW_SAS
|
||||
VerificationTxState.ShortCodeAccepted,
|
||||
VerificationTxState.SendingMac,
|
||||
VerificationTxState.MacSent,
|
||||
VerificationTxState.Verifying -> IncomingSasVerificationTransaction.UxState.WAIT_FOR_VERIFICATION
|
||||
VerificationTxState.Verified -> IncomingSasVerificationTransaction.UxState.VERIFIED
|
||||
VerificationTxState.Cancelled -> IncomingSasVerificationTransaction.UxState.CANCELLED_BY_ME
|
||||
VerificationTxState.OnCancelled -> IncomingSasVerificationTransaction.UxState.CANCELLED_BY_OTHER
|
||||
else -> IncomingSasVerificationTransaction.UxState.UNKNOWN
|
||||
}
|
||||
}
|
||||
|
||||
override fun onVerificationStart(startReq: VerificationInfoStart) {
|
||||
Timber.v("## SAS I: received verification request from state $state")
|
||||
if (state != SasVerificationTxState.None) {
|
||||
if (state != VerificationTxState.None) {
|
||||
Timber.e("## SAS I: received verification request from invalid state")
|
||||
// should I cancel??
|
||||
throw IllegalStateException("Interactive Key verification already started")
|
||||
}
|
||||
this.startReq = startReq
|
||||
state = SasVerificationTxState.OnStarted
|
||||
state = VerificationTxState.OnStarted
|
||||
this.otherDeviceId = startReq.fromDevice
|
||||
|
||||
if (autoAccept) {
|
||||
|
@ -84,7 +88,7 @@ internal class DefaultIncomingSASVerificationTransaction(
|
|||
}
|
||||
|
||||
override fun performAccept() {
|
||||
if (state != SasVerificationTxState.OnStarted) {
|
||||
if (state != VerificationTxState.OnStarted) {
|
||||
Timber.e("## SAS Cannot perform accept from state $state")
|
||||
return
|
||||
}
|
||||
|
@ -108,7 +112,7 @@ internal class DefaultIncomingSASVerificationTransaction(
|
|||
}
|
||||
|
||||
// Bob’s device ensures that it has a copy of Alice’s device key.
|
||||
val mxDeviceInfo = cryptoStore.getUserDevice(deviceId = otherDeviceId!!, userId = otherUserId)
|
||||
val mxDeviceInfo = cryptoStore.getUserDevice(userId = otherUserId, deviceId = otherDeviceId!!)
|
||||
|
||||
if (mxDeviceInfo?.fingerprint() == null) {
|
||||
Timber.e("## SAS Failed to find device key ")
|
||||
|
@ -140,11 +144,11 @@ internal class DefaultIncomingSASVerificationTransaction(
|
|||
val concat = getSAS().publicKey + startReq!!.toCanonicalJson()
|
||||
accept.commitment = hashUsingAgreedHashMethod(concat) ?: ""
|
||||
// we need to send this to other device now
|
||||
state = SasVerificationTxState.SendingAccept
|
||||
sendToOther(EventType.KEY_VERIFICATION_ACCEPT, accept, SasVerificationTxState.Accepted, CancelCode.User) {
|
||||
if (state == SasVerificationTxState.SendingAccept) {
|
||||
state = VerificationTxState.SendingAccept
|
||||
sendToOther(EventType.KEY_VERIFICATION_ACCEPT, accept, VerificationTxState.Accepted, CancelCode.User) {
|
||||
if (state == VerificationTxState.SendingAccept) {
|
||||
// It is possible that we receive the next event before this one :/, in this case we should keep state
|
||||
state = SasVerificationTxState.Accepted
|
||||
state = VerificationTxState.Accepted
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -154,9 +158,9 @@ internal class DefaultIncomingSASVerificationTransaction(
|
|||
cancel(CancelCode.UnexpectedMessage)
|
||||
}
|
||||
|
||||
override fun onKeyVerificationKey(userId: String, vKey: VerificationInfoKey) {
|
||||
override fun onKeyVerificationKey(vKey: VerificationInfoKey) {
|
||||
Timber.v("## SAS received key for request id:$transactionId")
|
||||
if (state != SasVerificationTxState.SendingAccept && state != SasVerificationTxState.Accepted) {
|
||||
if (state != VerificationTxState.SendingAccept && state != VerificationTxState.Accepted) {
|
||||
Timber.e("## SAS received key from invalid state $state")
|
||||
cancel(CancelCode.UnexpectedMessage)
|
||||
return
|
||||
|
@ -170,11 +174,11 @@ internal class DefaultIncomingSASVerificationTransaction(
|
|||
|
||||
val keyToDevice = transport.createKey(transactionId, pubKey)
|
||||
// we need to send this to other device now
|
||||
state = SasVerificationTxState.SendingKey
|
||||
this.sendToOther(EventType.KEY_VERIFICATION_KEY, keyToDevice, SasVerificationTxState.KeySent, CancelCode.User) {
|
||||
if (state == SasVerificationTxState.SendingKey) {
|
||||
state = VerificationTxState.SendingKey
|
||||
this.sendToOther(EventType.KEY_VERIFICATION_KEY, keyToDevice, VerificationTxState.KeySent, CancelCode.User) {
|
||||
if (state == VerificationTxState.SendingKey) {
|
||||
// It is possible that we receive the next event before this one :/, in this case we should keep state
|
||||
state = SasVerificationTxState.KeySent
|
||||
state = VerificationTxState.KeySent
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -191,10 +195,7 @@ internal class DefaultIncomingSASVerificationTransaction(
|
|||
// - the Matrix ID of the user who sent the m.key.verification.accept message,
|
||||
// - he device ID of the device that sent the m.key.verification.accept message
|
||||
// - the transaction ID.
|
||||
val sasInfo = "MATRIX_KEY_VERIFICATION_SAS" +
|
||||
"$otherUserId$otherDeviceId" +
|
||||
"${credentials.userId}${credentials.deviceId}" +
|
||||
transactionId
|
||||
val sasInfo = "MATRIX_KEY_VERIFICATION_SAS$otherUserId$otherDeviceId$userId$deviceId$transactionId"
|
||||
// decimal: generate five bytes by using HKDF.
|
||||
// emoji: generate six bytes by using HKDF.
|
||||
shortCodeBytes = getSAS().generateShortCode(sasInfo, 6)
|
||||
|
@ -204,18 +205,18 @@ internal class DefaultIncomingSASVerificationTransaction(
|
|||
Timber.v("************ BOB EMOJI CODE ${getShortCodeRepresentation(SasMode.EMOJI)}")
|
||||
}
|
||||
|
||||
state = SasVerificationTxState.ShortCodeReady
|
||||
state = VerificationTxState.ShortCodeReady
|
||||
}
|
||||
|
||||
override fun onKeyVerificationMac(vKey: VerificationInfoMac) {
|
||||
Timber.v("## SAS I: received mac for request id:$transactionId")
|
||||
// Check for state?
|
||||
if (state != SasVerificationTxState.SendingKey
|
||||
&& state != SasVerificationTxState.KeySent
|
||||
&& state != SasVerificationTxState.ShortCodeReady
|
||||
&& state != SasVerificationTxState.ShortCodeAccepted
|
||||
&& state != SasVerificationTxState.SendingMac
|
||||
&& state != SasVerificationTxState.MacSent) {
|
||||
if (state != VerificationTxState.SendingKey
|
||||
&& state != VerificationTxState.KeySent
|
||||
&& state != VerificationTxState.ShortCodeReady
|
||||
&& state != VerificationTxState.ShortCodeAccepted
|
||||
&& state != VerificationTxState.SendingMac
|
||||
&& state != VerificationTxState.MacSent) {
|
||||
Timber.e("## SAS I: received key from invalid state $state")
|
||||
cancel(CancelCode.UnexpectedMessage)
|
||||
return
|
|
@ -15,55 +15,57 @@
|
|||
*/
|
||||
package im.vector.matrix.android.internal.crypto.verification
|
||||
|
||||
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.sas.CancelCode
|
||||
import im.vector.matrix.android.api.session.crypto.sas.OutgoingSasVerificationRequest
|
||||
import im.vector.matrix.android.api.session.crypto.sas.SasVerificationTxState
|
||||
import im.vector.matrix.android.api.session.crypto.sas.VerificationMethod
|
||||
import im.vector.matrix.android.api.session.crypto.sas.OutgoingSasVerificationTransaction
|
||||
import im.vector.matrix.android.api.session.crypto.sas.VerificationTxState
|
||||
import im.vector.matrix.android.api.session.events.model.EventType
|
||||
import im.vector.matrix.android.internal.crypto.actions.SetDeviceVerificationAction
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.toValue
|
||||
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
|
||||
import timber.log.Timber
|
||||
|
||||
internal class DefaultOutgoingSASVerificationRequest(
|
||||
internal class DefaultOutgoingSASDefaultVerificationTransaction(
|
||||
setDeviceVerificationAction: SetDeviceVerificationAction,
|
||||
credentials: Credentials,
|
||||
userId: String,
|
||||
deviceId: String?,
|
||||
cryptoStore: IMXCryptoStore,
|
||||
crossSigningService: CrossSigningService,
|
||||
deviceFingerprint: String,
|
||||
transactionId: String,
|
||||
otherUserId: String,
|
||||
otherDeviceId: String
|
||||
) : SASVerificationTransaction(
|
||||
) : SASDefaultVerificationTransaction(
|
||||
setDeviceVerificationAction,
|
||||
credentials,
|
||||
userId,
|
||||
deviceId,
|
||||
cryptoStore,
|
||||
crossSigningService,
|
||||
deviceFingerprint,
|
||||
transactionId,
|
||||
otherUserId,
|
||||
otherDeviceId,
|
||||
isIncoming = false),
|
||||
OutgoingSasVerificationRequest {
|
||||
OutgoingSasVerificationTransaction {
|
||||
|
||||
override val uxState: OutgoingSasVerificationRequest.UxState
|
||||
override val uxState: OutgoingSasVerificationTransaction.UxState
|
||||
get() {
|
||||
return when (state) {
|
||||
SasVerificationTxState.None -> OutgoingSasVerificationRequest.UxState.WAIT_FOR_START
|
||||
SasVerificationTxState.SendingStart,
|
||||
SasVerificationTxState.Started,
|
||||
SasVerificationTxState.OnAccepted,
|
||||
SasVerificationTxState.SendingKey,
|
||||
SasVerificationTxState.KeySent,
|
||||
SasVerificationTxState.OnKeyReceived -> OutgoingSasVerificationRequest.UxState.WAIT_FOR_KEY_AGREEMENT
|
||||
SasVerificationTxState.ShortCodeReady -> OutgoingSasVerificationRequest.UxState.SHOW_SAS
|
||||
SasVerificationTxState.ShortCodeAccepted,
|
||||
SasVerificationTxState.SendingMac,
|
||||
SasVerificationTxState.MacSent,
|
||||
SasVerificationTxState.Verifying -> OutgoingSasVerificationRequest.UxState.WAIT_FOR_VERIFICATION
|
||||
SasVerificationTxState.Verified -> OutgoingSasVerificationRequest.UxState.VERIFIED
|
||||
SasVerificationTxState.OnCancelled -> OutgoingSasVerificationRequest.UxState.CANCELLED_BY_ME
|
||||
SasVerificationTxState.Cancelled -> OutgoingSasVerificationRequest.UxState.CANCELLED_BY_OTHER
|
||||
else -> OutgoingSasVerificationRequest.UxState.UNKNOWN
|
||||
VerificationTxState.None -> OutgoingSasVerificationTransaction.UxState.WAIT_FOR_START
|
||||
VerificationTxState.SendingStart,
|
||||
VerificationTxState.Started,
|
||||
VerificationTxState.OnAccepted,
|
||||
VerificationTxState.SendingKey,
|
||||
VerificationTxState.KeySent,
|
||||
VerificationTxState.OnKeyReceived -> OutgoingSasVerificationTransaction.UxState.WAIT_FOR_KEY_AGREEMENT
|
||||
VerificationTxState.ShortCodeReady -> OutgoingSasVerificationTransaction.UxState.SHOW_SAS
|
||||
VerificationTxState.ShortCodeAccepted,
|
||||
VerificationTxState.SendingMac,
|
||||
VerificationTxState.MacSent,
|
||||
VerificationTxState.Verifying -> OutgoingSasVerificationTransaction.UxState.WAIT_FOR_VERIFICATION
|
||||
VerificationTxState.Verified -> OutgoingSasVerificationTransaction.UxState.VERIFIED
|
||||
VerificationTxState.OnCancelled -> OutgoingSasVerificationTransaction.UxState.CANCELLED_BY_ME
|
||||
VerificationTxState.Cancelled -> OutgoingSasVerificationTransaction.UxState.CANCELLED_BY_OTHER
|
||||
else -> OutgoingSasVerificationTransaction.UxState.UNKNOWN
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -72,16 +74,15 @@ internal class DefaultOutgoingSASVerificationRequest(
|
|||
cancel(CancelCode.UnexpectedMessage)
|
||||
}
|
||||
|
||||
fun start(method: VerificationMethod) {
|
||||
if (state != SasVerificationTxState.None) {
|
||||
fun start() {
|
||||
if (state != VerificationTxState.None) {
|
||||
Timber.e("## SAS O: start verification from invalid state")
|
||||
// should I cancel??
|
||||
throw IllegalStateException("Interactive Key verification already started")
|
||||
}
|
||||
|
||||
val startMessage = transport.createStart(
|
||||
credentials.deviceId ?: "",
|
||||
method.toValue(),
|
||||
val startMessage = transport.createStartForSas(
|
||||
deviceId ?: "",
|
||||
transactionId,
|
||||
KNOWN_AGREEMENT_PROTOCOLS,
|
||||
KNOWN_HASHES,
|
||||
|
@ -90,19 +91,19 @@ internal class DefaultOutgoingSASVerificationRequest(
|
|||
)
|
||||
|
||||
startReq = startMessage
|
||||
state = SasVerificationTxState.SendingStart
|
||||
state = VerificationTxState.SendingStart
|
||||
|
||||
sendToOther(
|
||||
EventType.KEY_VERIFICATION_START,
|
||||
startMessage,
|
||||
SasVerificationTxState.Started,
|
||||
VerificationTxState.Started,
|
||||
CancelCode.User,
|
||||
null
|
||||
)
|
||||
}
|
||||
|
||||
// fun request() {
|
||||
// if (state != SasVerificationTxState.None) {
|
||||
// if (state != VerificationTxState.None) {
|
||||
// Timber.e("## start verification from invalid state")
|
||||
// // should I cancel??
|
||||
// throw IllegalStateException("Interactive Key verification already started")
|
||||
|
@ -118,7 +119,7 @@ internal class DefaultOutgoingSASVerificationRequest(
|
|||
// sendToOther(
|
||||
// EventType.KEY_VERIFICATION_REQUEST,
|
||||
// requestMessage,
|
||||
// SasVerificationTxState.None,
|
||||
// VerificationTxState.None,
|
||||
// CancelCode.User,
|
||||
// null
|
||||
// )
|
||||
|
@ -126,7 +127,7 @@ internal class DefaultOutgoingSASVerificationRequest(
|
|||
|
||||
override fun onVerificationAccept(accept: VerificationInfoAccept) {
|
||||
Timber.v("## SAS O: onVerificationAccept id:$transactionId")
|
||||
if (state != SasVerificationTxState.Started) {
|
||||
if (state != VerificationTxState.Started) {
|
||||
Timber.e("## SAS O: received accept request from invalid state $state")
|
||||
cancel(CancelCode.UnexpectedMessage)
|
||||
return
|
||||
|
@ -144,7 +145,7 @@ internal class DefaultOutgoingSASVerificationRequest(
|
|||
// Upon receipt of the m.key.verification.accept message from Bob’s device,
|
||||
// Alice’s device stores the commitment value for later use.
|
||||
accepted = accept
|
||||
state = SasVerificationTxState.OnAccepted
|
||||
state = VerificationTxState.OnAccepted
|
||||
|
||||
// Alice’s device creates an ephemeral Curve25519 key pair (dA,QA),
|
||||
// and replies with a to_device message with type set to “m.key.verification.key”, sending Alice’s public key QA
|
||||
|
@ -152,18 +153,18 @@ internal class DefaultOutgoingSASVerificationRequest(
|
|||
|
||||
val keyToDevice = transport.createKey(transactionId, pubKey)
|
||||
// we need to send this to other device now
|
||||
state = SasVerificationTxState.SendingKey
|
||||
sendToOther(EventType.KEY_VERIFICATION_KEY, keyToDevice, SasVerificationTxState.KeySent, CancelCode.User) {
|
||||
state = VerificationTxState.SendingKey
|
||||
sendToOther(EventType.KEY_VERIFICATION_KEY, keyToDevice, VerificationTxState.KeySent, CancelCode.User) {
|
||||
// It is possible that we receive the next event before this one :/, in this case we should keep state
|
||||
if (state == SasVerificationTxState.SendingKey) {
|
||||
state = SasVerificationTxState.KeySent
|
||||
if (state == VerificationTxState.SendingKey) {
|
||||
state = VerificationTxState.KeySent
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onKeyVerificationKey(userId: String, vKey: VerificationInfoKey) {
|
||||
override fun onKeyVerificationKey(vKey: VerificationInfoKey) {
|
||||
Timber.v("## SAS O: onKeyVerificationKey id:$transactionId")
|
||||
if (state != SasVerificationTxState.SendingKey && state != SasVerificationTxState.KeySent) {
|
||||
if (state != VerificationTxState.SendingKey && state != VerificationTxState.KeySent) {
|
||||
Timber.e("## received key from invalid state $state")
|
||||
cancel(CancelCode.UnexpectedMessage)
|
||||
return
|
||||
|
@ -189,27 +190,24 @@ internal class DefaultOutgoingSASVerificationRequest(
|
|||
// - the Matrix ID of the user who sent the m.key.verification.accept message,
|
||||
// - he device ID of the device that sent the m.key.verification.accept message
|
||||
// - the transaction ID.
|
||||
val sasInfo = "MATRIX_KEY_VERIFICATION_SAS" +
|
||||
"${credentials.userId}${credentials.deviceId}" +
|
||||
"$otherUserId$otherDeviceId" +
|
||||
transactionId
|
||||
val sasInfo = "MATRIX_KEY_VERIFICATION_SAS$userId$deviceId$otherUserId$otherDeviceId$transactionId"
|
||||
// decimal: generate five bytes by using HKDF.
|
||||
// emoji: generate six bytes by using HKDF.
|
||||
shortCodeBytes = getSAS().generateShortCode(sasInfo, 6)
|
||||
state = SasVerificationTxState.ShortCodeReady
|
||||
state = VerificationTxState.ShortCodeReady
|
||||
} else {
|
||||
// bad commitement
|
||||
// bad commitment
|
||||
cancel(CancelCode.MismatchedCommitment)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onKeyVerificationMac(vKey: VerificationInfoMac) {
|
||||
Timber.v("## SAS O: onKeyVerificationMac id:$transactionId")
|
||||
if (state != SasVerificationTxState.OnKeyReceived
|
||||
&& state != SasVerificationTxState.ShortCodeReady
|
||||
&& state != SasVerificationTxState.ShortCodeAccepted
|
||||
&& state != SasVerificationTxState.SendingMac
|
||||
&& state != SasVerificationTxState.MacSent) {
|
||||
if (state != VerificationTxState.OnKeyReceived
|
||||
&& state != VerificationTxState.ShortCodeReady
|
||||
&& state != VerificationTxState.ShortCodeAccepted
|
||||
&& state != VerificationTxState.SendingMac
|
||||
&& state != VerificationTxState.MacSent) {
|
||||
Timber.e("## SAS O: received key from invalid state $state")
|
||||
cancel(CancelCode.UnexpectedMessage)
|
||||
return
|
|
@ -20,43 +20,76 @@ import android.os.Handler
|
|||
import android.os.Looper
|
||||
import dagger.Lazy
|
||||
import im.vector.matrix.android.api.MatrixCallback
|
||||
import im.vector.matrix.android.api.auth.data.Credentials
|
||||
import im.vector.matrix.android.api.session.crypto.CryptoService
|
||||
import im.vector.matrix.android.api.session.crypto.sas.*
|
||||
import im.vector.matrix.android.api.session.crypto.crosssigning.CrossSigningService
|
||||
import im.vector.matrix.android.api.session.crypto.sas.CancelCode
|
||||
import im.vector.matrix.android.api.session.crypto.sas.QrCodeVerificationTransaction
|
||||
import im.vector.matrix.android.api.session.crypto.sas.SasVerificationTransaction
|
||||
import im.vector.matrix.android.api.session.crypto.sas.VerificationMethod
|
||||
import im.vector.matrix.android.api.session.crypto.sas.VerificationService
|
||||
import im.vector.matrix.android.api.session.crypto.sas.VerificationTransaction
|
||||
import im.vector.matrix.android.api.session.crypto.sas.VerificationTxState
|
||||
import im.vector.matrix.android.api.session.crypto.sas.safeValueOf
|
||||
import im.vector.matrix.android.api.session.events.model.Event
|
||||
import im.vector.matrix.android.api.session.events.model.EventType
|
||||
import im.vector.matrix.android.api.session.events.model.LocalEcho
|
||||
import im.vector.matrix.android.api.session.events.model.toModel
|
||||
import im.vector.matrix.android.api.session.room.model.message.*
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageContent
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageRelationContent
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageType
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageVerificationAcceptContent
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageVerificationCancelContent
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageVerificationDoneContent
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageVerificationKeyContent
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageVerificationMacContent
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageVerificationReadyContent
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageVerificationRequestContent
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageVerificationStartContent
|
||||
import im.vector.matrix.android.internal.crypto.DeviceListManager
|
||||
import im.vector.matrix.android.internal.crypto.MyDeviceInfoHolder
|
||||
import im.vector.matrix.android.internal.crypto.actions.SetDeviceVerificationAction
|
||||
import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo
|
||||
import im.vector.matrix.android.internal.crypto.crosssigning.DeviceTrustLevel
|
||||
import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
|
||||
import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.*
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.KeyVerificationAccept
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.KeyVerificationCancel
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.KeyVerificationKey
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.KeyVerificationMac
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.KeyVerificationStart
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.VERIFICATION_METHOD_QR_CODE_SCAN
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.VERIFICATION_METHOD_QR_CODE_SHOW
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.VERIFICATION_METHOD_RECIPROCATE
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.VERIFICATION_METHOD_SAS
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.toValue
|
||||
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
|
||||
import im.vector.matrix.android.internal.crypto.verification.qrcode.DefaultQrCodeVerificationTransaction
|
||||
import im.vector.matrix.android.internal.crypto.verification.qrcode.QrCodeData
|
||||
import im.vector.matrix.android.internal.crypto.verification.qrcode.generateSharedSecret
|
||||
import im.vector.matrix.android.internal.di.DeviceId
|
||||
import im.vector.matrix.android.internal.di.UserId
|
||||
import im.vector.matrix.android.internal.session.SessionScope
|
||||
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import okhttp3.internal.toImmutableList
|
||||
import timber.log.Timber
|
||||
import java.util.*
|
||||
import java.util.UUID
|
||||
import javax.inject.Inject
|
||||
import kotlin.collections.ArrayList
|
||||
import kotlin.collections.HashMap
|
||||
import kotlin.collections.set
|
||||
|
||||
@SessionScope
|
||||
internal class DefaultSasVerificationService @Inject constructor(
|
||||
private val credentials: Credentials,
|
||||
internal class DefaultVerificationService @Inject constructor(
|
||||
@UserId private val userId: String,
|
||||
@DeviceId private val deviceId: String?,
|
||||
private val cryptoStore: IMXCryptoStore,
|
||||
private val myDeviceInfoHolder: Lazy<MyDeviceInfoHolder>,
|
||||
private val deviceListManager: DeviceListManager,
|
||||
private val setDeviceVerificationAction: SetDeviceVerificationAction,
|
||||
private val coroutineDispatchers: MatrixCoroutineDispatchers,
|
||||
private val sasTransportRoomMessageFactory: SasTransportRoomMessageFactory,
|
||||
private val sasTransportToDeviceFactory: SasTransportToDeviceFactory
|
||||
) : VerificationTransaction.Listener, SasVerificationService {
|
||||
private val verificationTransportRoomMessageFactory: VerificationTransportRoomMessageFactory,
|
||||
private val verificationTransportToDeviceFactory: VerificationTransportToDeviceFactory,
|
||||
private val crossSigningService: CrossSigningService
|
||||
) : DefaultVerificationTransaction.Listener, VerificationService {
|
||||
|
||||
private val uiHandler = Handler(Looper.getMainLooper())
|
||||
|
||||
|
@ -64,7 +97,7 @@ internal class DefaultSasVerificationService @Inject constructor(
|
|||
lateinit var cryptoService: CryptoService
|
||||
|
||||
// map [sender : [transaction]]
|
||||
private val txMap = HashMap<String, HashMap<String, VerificationTransaction>>()
|
||||
private val txMap = HashMap<String, HashMap<String, DefaultVerificationTransaction>>()
|
||||
|
||||
/**
|
||||
* Map [sender: [PendingVerificationRequest]]
|
||||
|
@ -135,9 +168,9 @@ internal class DefaultSasVerificationService @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private var listeners = ArrayList<SasVerificationService.SasVerificationListener>()
|
||||
private var listeners = ArrayList<VerificationService.VerificationListener>()
|
||||
|
||||
override fun addListener(listener: SasVerificationService.SasVerificationListener) {
|
||||
override fun addListener(listener: VerificationService.VerificationListener) {
|
||||
uiHandler.post {
|
||||
if (!listeners.contains(listener)) {
|
||||
listeners.add(listener)
|
||||
|
@ -145,7 +178,7 @@ internal class DefaultSasVerificationService @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
override fun removeListener(listener: SasVerificationService.SasVerificationListener) {
|
||||
override fun removeListener(listener: VerificationService.VerificationListener) {
|
||||
uiHandler.post {
|
||||
listeners.remove(listener)
|
||||
}
|
||||
|
@ -200,9 +233,9 @@ internal class DefaultSasVerificationService @Inject constructor(
|
|||
}
|
||||
|
||||
override fun markedLocallyAsManuallyVerified(userId: String, deviceID: String) {
|
||||
setDeviceVerificationAction.handle(MXDeviceInfo.DEVICE_VERIFICATION_VERIFIED,
|
||||
deviceID,
|
||||
userId)
|
||||
setDeviceVerificationAction.handle(DeviceTrustLevel(false, true),
|
||||
userId,
|
||||
deviceID)
|
||||
|
||||
listeners.forEach {
|
||||
try {
|
||||
|
@ -232,7 +265,7 @@ internal class DefaultSasVerificationService @Inject constructor(
|
|||
?: return
|
||||
val senderId = event.senderId ?: return
|
||||
|
||||
if (requestInfo.toUserId != credentials.userId) {
|
||||
if (requestInfo.toUserId != userId) {
|
||||
// I should ignore this, it's not for me
|
||||
Timber.w("## SAS Verification ignoring request from ${event.senderId}, not sent to me")
|
||||
return
|
||||
|
@ -285,7 +318,7 @@ internal class DefaultSasVerificationService @Inject constructor(
|
|||
if (startReq?.isValid()?.not() == true) {
|
||||
Timber.e("## received invalid verification request")
|
||||
if (startReq.transactionID != null) {
|
||||
sasTransportRoomMessageFactory.createTransport(event.roomId ?: "", null)
|
||||
verificationTransportRoomMessageFactory.createTransport(event.roomId ?: "", null)
|
||||
.cancelTransaction(
|
||||
startReq.transactionID ?: "",
|
||||
otherUserId!!,
|
||||
|
@ -297,9 +330,9 @@ internal class DefaultSasVerificationService @Inject constructor(
|
|||
}
|
||||
|
||||
handleStart(otherUserId, startReq as VerificationInfoStart) {
|
||||
it.transport = sasTransportRoomMessageFactory.createTransport(event.roomId ?: "", it)
|
||||
it.transport = verificationTransportRoomMessageFactory.createTransport(event.roomId ?: "", it)
|
||||
}?.let {
|
||||
sasTransportRoomMessageFactory.createTransport(event.roomId ?: "", null)
|
||||
verificationTransportRoomMessageFactory.createTransport(event.roomId ?: "", null)
|
||||
.cancelTransaction(
|
||||
startReq.transactionID ?: "",
|
||||
otherUserId!!,
|
||||
|
@ -318,7 +351,7 @@ internal class DefaultSasVerificationService @Inject constructor(
|
|||
if (!startReq.isValid()) {
|
||||
Timber.e("## SAS received invalid verification request")
|
||||
if (startReq.transactionID != null) {
|
||||
sasTransportToDeviceFactory.createTransport(null).cancelTransaction(
|
||||
verificationTransportToDeviceFactory.createTransport(null).cancelTransaction(
|
||||
startReq.transactionID,
|
||||
otherUserId!!,
|
||||
startReq.fromDevice ?: event.getSenderKey()!!,
|
||||
|
@ -329,9 +362,9 @@ internal class DefaultSasVerificationService @Inject constructor(
|
|||
}
|
||||
// Download device keys prior to everything
|
||||
handleStart(otherUserId, startReq) {
|
||||
it.transport = sasTransportToDeviceFactory.createTransport(it)
|
||||
it.transport = verificationTransportToDeviceFactory.createTransport(it)
|
||||
}?.let {
|
||||
sasTransportToDeviceFactory.createTransport(null).cancelTransaction(
|
||||
verificationTransportToDeviceFactory.createTransport(null).cancelTransaction(
|
||||
startReq.transactionID ?: "",
|
||||
otherUserId!!,
|
||||
startReq.fromDevice ?: event.getSenderKey()!!,
|
||||
|
@ -340,70 +373,98 @@ internal class DefaultSasVerificationService @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private suspend fun handleStart(otherUserId: String?, startReq: VerificationInfoStart, txConfigure: (SASVerificationTransaction) -> Unit): CancelCode? {
|
||||
Timber.d("## SAS onStartRequestReceived ${startReq.transactionID!!}")
|
||||
/**
|
||||
* Return a CancelCode to make the caller cancel the verification. Else return null
|
||||
*/
|
||||
private suspend fun handleStart(otherUserId: String?, startReq: VerificationInfoStart, txConfigure: (DefaultVerificationTransaction) -> Unit): CancelCode? {
|
||||
Timber.d("## SAS onStartRequestReceived ${startReq.transactionID}")
|
||||
if (checkKeysAreDownloaded(otherUserId!!, startReq.fromDevice ?: "") != null) {
|
||||
Timber.v("## SAS onStartRequestReceived $startReq")
|
||||
val tid = startReq.transactionID!!
|
||||
val existing = getExistingTransaction(otherUserId, tid)
|
||||
val existingTxs = getExistingTransactionsForUser(otherUserId)
|
||||
if (existing != null) {
|
||||
|
||||
when (startReq.method) {
|
||||
VERIFICATION_METHOD_SAS -> {
|
||||
when (existing) {
|
||||
is SasVerificationTransaction -> {
|
||||
// should cancel both!
|
||||
Timber.v("## SAS onStartRequestReceived - Request exist with same if ${startReq.transactionID!!}")
|
||||
Timber.v("## SAS onStartRequestReceived - Request exist with same id ${startReq.transactionID}")
|
||||
existing.cancel(CancelCode.UnexpectedMessage)
|
||||
return CancelCode.UnexpectedMessage
|
||||
// cancelTransaction(tid, otherUserId, startReq.fromDevice!!, CancelCode.UnexpectedMessage)
|
||||
} else if (existingTxs?.isEmpty() == false) {
|
||||
Timber.v("## SAS onStartRequestReceived - There is already a transaction with this user ${startReq.transactionID!!}")
|
||||
// Multiple keyshares between two devices: any two devices may only have at most one key verification in flight at a time.
|
||||
existingTxs.forEach {
|
||||
// Already cancelled, so return null
|
||||
return null
|
||||
}
|
||||
is QrCodeVerificationTransaction -> {
|
||||
// Nothing to do?
|
||||
}
|
||||
null -> {
|
||||
getExistingTransactionsForUser(otherUserId)
|
||||
?.filterIsInstance(SasVerificationTransaction::class.java)
|
||||
?.takeIf { it.isNotEmpty() }
|
||||
?.also {
|
||||
// Multiple keyshares between two devices:
|
||||
// any two devices may only have at most one key verification in flight at a time.
|
||||
Timber.v("## SAS onStartRequestReceived - Already a transaction with this user ${startReq.transactionID}")
|
||||
}
|
||||
?.forEach {
|
||||
it.cancel(CancelCode.UnexpectedMessage)
|
||||
}
|
||||
?.also {
|
||||
return CancelCode.UnexpectedMessage
|
||||
// cancelTransaction(tid, otherUserId, startReq.fromDevice!!, CancelCode.UnexpectedMessage)
|
||||
} else {
|
||||
// Ok we can create
|
||||
if (startReq.method == VERIFICATION_METHOD_SAS) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Ok we can create a SAS transaction
|
||||
Timber.v("## SAS onStartRequestReceived - request accepted ${startReq.transactionID!!}")
|
||||
// If there is a corresponding request, we can auto accept
|
||||
// as we are the one requesting in first place (or we accepted the request)
|
||||
val autoAccept = getExistingVerificationRequest(otherUserId)?.any { it.transactionId == startReq.transactionID }
|
||||
?: false
|
||||
val tx = DefaultIncomingSASVerificationTransaction(
|
||||
val tx = DefaultIncomingSASDefaultVerificationTransaction(
|
||||
// this,
|
||||
setDeviceVerificationAction,
|
||||
credentials,
|
||||
userId,
|
||||
deviceId,
|
||||
cryptoStore,
|
||||
crossSigningService,
|
||||
myDeviceInfoHolder.get().myDevice.fingerprint()!!,
|
||||
startReq.transactionID!!,
|
||||
otherUserId,
|
||||
autoAccept).also { txConfigure(it) }
|
||||
addTransaction(tx)
|
||||
tx.acceptVerificationEvent(otherUserId, startReq)
|
||||
return null
|
||||
}
|
||||
VERIFICATION_METHOD_RECIPROCATE -> {
|
||||
// Other user has scanned my QR code
|
||||
if (existing is DefaultQrCodeVerificationTransaction) {
|
||||
existing.onStartReceived(startReq)
|
||||
return null
|
||||
} else {
|
||||
Timber.w("## SAS onStartRequestReceived - unexpected message ${startReq.transactionID}")
|
||||
return CancelCode.UnexpectedMessage
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
Timber.e("## SAS onStartRequestReceived - unknown method ${startReq.method}")
|
||||
return CancelCode.UnknownMethod
|
||||
// cancelTransaction(tid, otherUserId, startReq.fromDevice
|
||||
// ?: event.getSenderKey()!!, CancelCode.UnknownMethod)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return CancelCode.UnexpectedMessage
|
||||
// cancelTransaction(startReq.transactionID!!, otherUserId, startReq.fromDevice!!, CancelCode.UnexpectedMessage)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
// TODO Refacto: It could just return a boolean
|
||||
private suspend fun checkKeysAreDownloaded(otherUserId: String,
|
||||
fromDevice: String): MXUsersDevicesMap<MXDeviceInfo>? {
|
||||
otherDeviceId: String): MXUsersDevicesMap<CryptoDeviceInfo>? {
|
||||
return try {
|
||||
var keys = deviceListManager.downloadKeys(listOf(otherUserId), false)
|
||||
if (keys.getUserDeviceIds(otherUserId)?.contains(fromDevice) == true) {
|
||||
if (keys.getUserDeviceIds(otherUserId)?.contains(otherDeviceId) == true) {
|
||||
return keys
|
||||
} else {
|
||||
// force download
|
||||
keys = deviceListManager.downloadKeys(listOf(otherUserId), true)
|
||||
return keys.takeIf { keys.getUserDeviceIds(otherUserId)?.contains(fromDevice) == true }
|
||||
return keys.takeIf { keys.getUserDeviceIds(otherUserId)?.contains(otherDeviceId) == true }
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
|
@ -456,9 +517,9 @@ internal class DefaultSasVerificationService @Inject constructor(
|
|||
))
|
||||
}
|
||||
|
||||
if (existingTransaction is SASVerificationTransaction) {
|
||||
if (existingTransaction is SASDefaultVerificationTransaction) {
|
||||
existingTransaction.cancelledReason = safeValueOf(cancelReq.code)
|
||||
existingTransaction.state = SasVerificationTxState.OnCancelled
|
||||
existingTransaction.state = VerificationTxState.OnCancelled
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -492,7 +553,7 @@ internal class DefaultSasVerificationService @Inject constructor(
|
|||
return
|
||||
}
|
||||
|
||||
if (existing is SASVerificationTransaction) {
|
||||
if (existing is SASDefaultVerificationTransaction) {
|
||||
existing.acceptVerificationEvent(otherUserId, acceptReq)
|
||||
} else {
|
||||
// not other types now
|
||||
|
@ -533,7 +594,7 @@ internal class DefaultSasVerificationService @Inject constructor(
|
|||
Timber.e("## SAS Received invalid key request")
|
||||
return
|
||||
}
|
||||
if (existing is SASVerificationTransaction) {
|
||||
if (existing is SASDefaultVerificationTransaction) {
|
||||
existing.acceptVerificationEvent(otherUserId, keyReq)
|
||||
} else {
|
||||
// not other types now
|
||||
|
@ -568,12 +629,12 @@ internal class DefaultSasVerificationService @Inject constructor(
|
|||
return
|
||||
}
|
||||
if (checkKeysAreDownloaded(event.senderId, readyReq.fromDevice ?: "") == null) {
|
||||
Timber.e("## SAS Verification device ${readyReq.fromDevice} is not knwown")
|
||||
Timber.e("## SAS Verification device ${readyReq.fromDevice} is not known")
|
||||
// TODO cancel?
|
||||
return
|
||||
}
|
||||
|
||||
handleReadyReceived(event.senderId, readyReq)
|
||||
handleReadyReceived(event.senderId, event.roomId!!, readyReq)
|
||||
}
|
||||
|
||||
private fun onRoomDoneReceived(event: Event) {
|
||||
|
@ -611,20 +672,94 @@ internal class DefaultSasVerificationService @Inject constructor(
|
|||
Timber.e("## SAS Received invalid Mac request")
|
||||
return
|
||||
}
|
||||
if (existing is SASVerificationTransaction) {
|
||||
if (existing is SASDefaultVerificationTransaction) {
|
||||
existing.acceptVerificationEvent(senderId, macReq)
|
||||
} else {
|
||||
// not other types known for now
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleReadyReceived(senderId: String, readyReq: VerificationInfoReady) {
|
||||
private fun handleReadyReceived(senderId: String, roomId: String, readyReq: VerificationInfoReady) {
|
||||
val existingRequest = getExistingVerificationRequest(senderId)?.find { it.transactionId == readyReq.transactionID }
|
||||
if (existingRequest == null) {
|
||||
Timber.e("## SAS Received Ready for unknown request txId:${readyReq.transactionID} fromDevice ${readyReq.fromDevice}")
|
||||
return
|
||||
}
|
||||
updatePendingRequest(existingRequest.copy(readyInfo = readyReq))
|
||||
|
||||
val qrCodeData = readyReq.methods
|
||||
// Check if other user is able to scan QR code
|
||||
?.takeIf { it.contains(VERIFICATION_METHOD_QR_CODE_SCAN) }
|
||||
?.let {
|
||||
createQrCodeData(existingRequest.transactionId, existingRequest.otherUserId)
|
||||
}
|
||||
|
||||
if (readyReq.methods?.orEmpty().orEmpty().contains(VERIFICATION_METHOD_RECIPROCATE)) {
|
||||
// Create the pending transaction
|
||||
val tx = DefaultQrCodeVerificationTransaction(
|
||||
setDeviceVerificationAction,
|
||||
readyReq.transactionID!!,
|
||||
senderId,
|
||||
readyReq.fromDevice,
|
||||
crossSigningService,
|
||||
cryptoStore,
|
||||
qrCodeData,
|
||||
userId,
|
||||
deviceId ?: "",
|
||||
false)
|
||||
|
||||
tx.transport = verificationTransportRoomMessageFactory.createTransport(roomId, tx)
|
||||
|
||||
addTransaction(tx)
|
||||
}
|
||||
|
||||
updatePendingRequest(existingRequest.copy(
|
||||
readyInfo = readyReq
|
||||
))
|
||||
}
|
||||
|
||||
private fun createQrCodeData(transactionId: String?, otherUserId: String): QrCodeData? {
|
||||
// Build the QR code URL
|
||||
val requestEventId = transactionId ?: run {
|
||||
Timber.w("## Unknown requestEventId")
|
||||
return null
|
||||
}
|
||||
|
||||
val myMasterKey = crossSigningService.getMyCrossSigningKeys()
|
||||
?.masterKey()
|
||||
?.unpaddedBase64PublicKey
|
||||
?: run {
|
||||
Timber.w("## Unable to get my master key")
|
||||
return null
|
||||
}
|
||||
|
||||
val otherUserMasterKey = crossSigningService.getUserCrossSigningKeys(otherUserId)
|
||||
?.masterKey()
|
||||
?.unpaddedBase64PublicKey
|
||||
?: run {
|
||||
Timber.w("## Unable to get other user master key")
|
||||
return null
|
||||
}
|
||||
|
||||
val myDeviceId = deviceId
|
||||
?: run {
|
||||
Timber.w("## Unable to get my deviceId")
|
||||
return null
|
||||
}
|
||||
|
||||
val myDeviceKey = myDeviceInfoHolder.get().myDevice.fingerprint()!!
|
||||
|
||||
val generatedSharedSecret = generateSharedSecret()
|
||||
return QrCodeData(
|
||||
userId = userId,
|
||||
requestEventId = requestEventId,
|
||||
action = QrCodeData.ACTION_VERIFY,
|
||||
keys = hashMapOf(
|
||||
myMasterKey to myMasterKey,
|
||||
myDeviceId to myDeviceKey
|
||||
),
|
||||
sharedSecret = generatedSharedSecret,
|
||||
otherUserKey = otherUserMasterKey
|
||||
)
|
||||
}
|
||||
|
||||
private fun handleDoneReceived(senderId: String, doneInfo: VerificationInfo) {
|
||||
|
@ -636,21 +771,22 @@ internal class DefaultSasVerificationService @Inject constructor(
|
|||
updatePendingRequest(existingRequest.copy(isSuccessful = true))
|
||||
}
|
||||
|
||||
override fun getExistingTransaction(otherUser: String, tid: String): VerificationTransaction? {
|
||||
// TODO All this methods should be delegated to a TransactionStore
|
||||
override fun getExistingTransaction(otherUserId: String, tid: String): VerificationTransaction? {
|
||||
synchronized(lock = txMap) {
|
||||
return txMap[otherUser]?.get(tid)
|
||||
return txMap[otherUserId]?.get(tid)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getExistingVerificationRequest(otherUser: String): List<PendingVerificationRequest>? {
|
||||
override fun getExistingVerificationRequest(otherUserId: String): List<PendingVerificationRequest>? {
|
||||
synchronized(lock = pendingRequests) {
|
||||
return pendingRequests[otherUser]
|
||||
return pendingRequests[otherUserId]
|
||||
}
|
||||
}
|
||||
|
||||
override fun getExistingVerificationRequest(otherUser: String, tid: String?): PendingVerificationRequest? {
|
||||
override fun getExistingVerificationRequest(otherUserId: String, tid: String?): PendingVerificationRequest? {
|
||||
synchronized(lock = pendingRequests) {
|
||||
return tid?.let { tid -> pendingRequests[otherUser]?.firstOrNull { it.transactionId == tid } }
|
||||
return tid?.let { tid -> pendingRequests[otherUserId]?.firstOrNull { it.transactionId == tid } }
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -676,7 +812,7 @@ internal class DefaultSasVerificationService @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun addTransaction(tx: VerificationTransaction) {
|
||||
private fun addTransaction(tx: DefaultVerificationTransaction) {
|
||||
tx.otherUserId.let { otherUserId ->
|
||||
synchronized(txMap) {
|
||||
val txInnerMap = txMap.getOrPut(otherUserId) { HashMap() }
|
||||
|
@ -687,60 +823,79 @@ internal class DefaultSasVerificationService @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
override fun beginKeyVerification(method: VerificationMethod, userId: String, deviceID: String): String? {
|
||||
val txID = createUniqueIDForTransaction(userId, deviceID)
|
||||
override fun beginKeyVerification(method: VerificationMethod, otherUserId: String, otherDeviceID: String): String? {
|
||||
val txID = createUniqueIDForTransaction(otherUserId, otherDeviceID)
|
||||
// should check if already one (and cancel it)
|
||||
if (method == VerificationMethod.SAS) {
|
||||
val tx = DefaultOutgoingSASVerificationRequest(
|
||||
val tx = DefaultOutgoingSASDefaultVerificationTransaction(
|
||||
setDeviceVerificationAction,
|
||||
credentials,
|
||||
userId,
|
||||
deviceId,
|
||||
cryptoStore,
|
||||
crossSigningService,
|
||||
myDeviceInfoHolder.get().myDevice.fingerprint()!!,
|
||||
txID,
|
||||
userId,
|
||||
deviceID)
|
||||
tx.transport = sasTransportToDeviceFactory.createTransport(tx)
|
||||
otherUserId,
|
||||
otherDeviceID)
|
||||
tx.transport = verificationTransportToDeviceFactory.createTransport(tx)
|
||||
addTransaction(tx)
|
||||
|
||||
tx.start(method)
|
||||
tx.start()
|
||||
return txID
|
||||
} else {
|
||||
throw IllegalArgumentException("Unknown verification method")
|
||||
}
|
||||
}
|
||||
|
||||
override fun requestKeyVerificationInDMs(methods: List<VerificationMethod>, userId: String, roomId: String)
|
||||
override fun requestKeyVerificationInDMs(methods: List<VerificationMethod>, otherUserId: String, roomId: String, localId: String?)
|
||||
: PendingVerificationRequest {
|
||||
Timber.i("## SAS Requesting verification to user: $userId in room $roomId")
|
||||
Timber.i("## SAS Requesting verification to user: $otherUserId in room $roomId")
|
||||
|
||||
val requestsForUser = pendingRequests[userId]
|
||||
val requestsForUser = pendingRequests[otherUserId]
|
||||
?: ArrayList<PendingVerificationRequest>().also {
|
||||
pendingRequests[userId] = it
|
||||
pendingRequests[otherUserId] = it
|
||||
}
|
||||
|
||||
val transport = sasTransportRoomMessageFactory.createTransport(roomId, null)
|
||||
val transport = verificationTransportRoomMessageFactory.createTransport(roomId, null)
|
||||
|
||||
// Cancel existing pending requests?
|
||||
requestsForUser.forEach { existingRequest ->
|
||||
requestsForUser.toImmutableList().forEach { existingRequest ->
|
||||
existingRequest.transactionId?.let { tid ->
|
||||
if (!existingRequest.isFinished) {
|
||||
Timber.d("## SAS, cancelling pending requests to start a new one")
|
||||
updatePendingRequest(existingRequest.copy(cancelConclusion = CancelCode.User))
|
||||
transport.cancelTransaction(tid, existingRequest.otherUserId, "", CancelCode.User)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val localID = LocalEcho.createLocalEchoId()
|
||||
val localID = localId ?: LocalEcho.createLocalEchoId()
|
||||
|
||||
val verificationRequest = PendingVerificationRequest(
|
||||
ageLocalTs = System.currentTimeMillis(),
|
||||
isIncoming = false,
|
||||
roomId = roomId,
|
||||
localID = localID,
|
||||
otherUserId = userId
|
||||
otherUserId = otherUserId
|
||||
)
|
||||
|
||||
transport.sendVerificationRequest(methods.map { it.toValue() }, localID, userId, roomId) { syncedId, info ->
|
||||
// We can SCAN or SHOW QR codes only if cross-signing is enabled
|
||||
val methodValues = if (crossSigningService.isCrossSigningEnabled()) {
|
||||
// Add reciprocate method if application declares it can scan or show QR codes
|
||||
// Not sure if it ok to do that (?)
|
||||
val reciprocateMethod = methods
|
||||
.firstOrNull { it == VerificationMethod.QR_CODE_SCAN || it == VerificationMethod.QR_CODE_SHOW }
|
||||
?.let { listOf(VERIFICATION_METHOD_RECIPROCATE) }.orEmpty()
|
||||
methods.map { it.toValue() } + reciprocateMethod
|
||||
} else {
|
||||
// Filter out SCAN and SHOW qr code method
|
||||
methods
|
||||
.filter { it != VerificationMethod.QR_CODE_SHOW && it != VerificationMethod.QR_CODE_SCAN }
|
||||
.map { it.toValue() }
|
||||
}
|
||||
.distinct()
|
||||
|
||||
transport.sendVerificationRequest(methodValues, localID, otherUserId, roomId) { syncedId, info ->
|
||||
// We need to update with the syncedID
|
||||
updatePendingRequest(verificationRequest.copy(
|
||||
transactionId = syncedId,
|
||||
|
@ -755,7 +910,7 @@ internal class DefaultSasVerificationService @Inject constructor(
|
|||
}
|
||||
|
||||
override fun declineVerificationRequestInDMs(otherUserId: String, otherDeviceId: String, transactionId: String, roomId: String) {
|
||||
sasTransportRoomMessageFactory.createTransport(roomId, null)
|
||||
verificationTransportRoomMessageFactory.createTransport(roomId, null)
|
||||
.cancelTransaction(transactionId, otherUserId, otherDeviceId, CancelCode.User)
|
||||
|
||||
getExistingVerificationRequest(otherUserId, transactionId)?.let {
|
||||
|
@ -788,41 +943,53 @@ internal class DefaultSasVerificationService @Inject constructor(
|
|||
otherDeviceId: String,
|
||||
callback: MatrixCallback<String>?): String? {
|
||||
if (method == VerificationMethod.SAS) {
|
||||
val tx = DefaultOutgoingSASVerificationRequest(
|
||||
val tx = DefaultOutgoingSASDefaultVerificationTransaction(
|
||||
setDeviceVerificationAction,
|
||||
credentials,
|
||||
userId,
|
||||
deviceId,
|
||||
cryptoStore,
|
||||
crossSigningService,
|
||||
myDeviceInfoHolder.get().myDevice.fingerprint()!!,
|
||||
transactionId,
|
||||
otherUserId,
|
||||
otherDeviceId)
|
||||
tx.transport = sasTransportRoomMessageFactory.createTransport(roomId, tx)
|
||||
tx.transport = verificationTransportRoomMessageFactory.createTransport(roomId, tx)
|
||||
addTransaction(tx)
|
||||
|
||||
tx.start(method)
|
||||
tx.start()
|
||||
return transactionId
|
||||
} else {
|
||||
throw IllegalArgumentException("Unknown verification method")
|
||||
}
|
||||
}
|
||||
|
||||
override fun readyPendingVerificationInDMs(otherUserId: String, roomId: String, transactionId: String): Boolean {
|
||||
override fun readyPendingVerificationInDMs(methods: List<VerificationMethod>,
|
||||
otherUserId: String,
|
||||
roomId: String,
|
||||
transactionId: String): Boolean {
|
||||
Timber.v("## SAS readyPendingVerificationInDMs $otherUserId room:$roomId tx:$transactionId")
|
||||
// Let's find the related request
|
||||
val existingRequest = getExistingVerificationRequest(otherUserId, transactionId)
|
||||
if (existingRequest != null) {
|
||||
// we need to send a ready event, with matching methods
|
||||
val transport = sasTransportRoomMessageFactory.createTransport(roomId, null)
|
||||
val methods = existingRequest.requestInfo?.methods?.intersect(supportedVerificationMethods)?.toList()
|
||||
val transport = verificationTransportRoomMessageFactory.createTransport(roomId, null)
|
||||
val computedMethods = computeReadyMethods(
|
||||
transactionId,
|
||||
otherUserId,
|
||||
existingRequest.requestInfo?.fromDevice ?: "",
|
||||
roomId,
|
||||
existingRequest.requestInfo?.methods,
|
||||
methods)
|
||||
if (methods.isNullOrEmpty()) {
|
||||
Timber.i("Cannot ready this request, no common methods found txId:$transactionId")
|
||||
// TODO buttons should not be shown in this case?
|
||||
return false
|
||||
}
|
||||
// TODO this is not yet related to a transaction, maybe we should use another method like for cancel?
|
||||
val readyMsg = transport.createReady(transactionId, credentials.deviceId ?: "", methods)
|
||||
transport.sendToOther(EventType.KEY_VERIFICATION_READY, readyMsg,
|
||||
SasVerificationTxState.None,
|
||||
val readyMsg = transport.createReady(transactionId, deviceId ?: "", computedMethods)
|
||||
transport.sendToOther(EventType.KEY_VERIFICATION_READY,
|
||||
readyMsg,
|
||||
VerificationTxState.None,
|
||||
CancelCode.User,
|
||||
null // TODO handle error?
|
||||
)
|
||||
|
@ -835,25 +1002,91 @@ internal class DefaultSasVerificationService @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun computeReadyMethods(
|
||||
transactionId: String,
|
||||
otherUserId: String,
|
||||
otherDeviceId: String,
|
||||
roomId: String,
|
||||
otherUserMethods: List<String>?,
|
||||
methods: List<VerificationMethod>): List<String> {
|
||||
if (otherUserMethods.isNullOrEmpty()) {
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
val result = mutableSetOf<String>()
|
||||
|
||||
if (VERIFICATION_METHOD_SAS in otherUserMethods && VerificationMethod.SAS in methods) {
|
||||
// Other can do SAS and so do I
|
||||
result.add(VERIFICATION_METHOD_SAS)
|
||||
}
|
||||
|
||||
if (VERIFICATION_METHOD_QR_CODE_SCAN in otherUserMethods || VERIFICATION_METHOD_QR_CODE_SHOW in otherUserMethods) {
|
||||
// Other user wants to verify using QR code. Cross-signing has to be setup
|
||||
val qrCodeData = createQrCodeData(transactionId, otherUserId)
|
||||
|
||||
if (qrCodeData != null) {
|
||||
if (VERIFICATION_METHOD_QR_CODE_SCAN in otherUserMethods && VerificationMethod.QR_CODE_SHOW in methods) {
|
||||
// Other can Scan and I can show QR code
|
||||
result.add(VERIFICATION_METHOD_QR_CODE_SHOW)
|
||||
result.add(VERIFICATION_METHOD_RECIPROCATE)
|
||||
}
|
||||
if (VERIFICATION_METHOD_QR_CODE_SHOW in otherUserMethods && VerificationMethod.QR_CODE_SCAN in methods) {
|
||||
// Other can show and I can scan QR code
|
||||
result.add(VERIFICATION_METHOD_QR_CODE_SCAN)
|
||||
result.add(VERIFICATION_METHOD_RECIPROCATE)
|
||||
}
|
||||
}
|
||||
|
||||
if (VERIFICATION_METHOD_RECIPROCATE in result) {
|
||||
// Create the pending transaction
|
||||
val tx = DefaultQrCodeVerificationTransaction(
|
||||
setDeviceVerificationAction,
|
||||
transactionId,
|
||||
otherUserId,
|
||||
otherDeviceId,
|
||||
crossSigningService,
|
||||
cryptoStore,
|
||||
qrCodeData,
|
||||
userId,
|
||||
deviceId ?: "",
|
||||
false)
|
||||
|
||||
tx.transport = verificationTransportRoomMessageFactory.createTransport(roomId, tx)
|
||||
|
||||
addTransaction(tx)
|
||||
}
|
||||
}
|
||||
|
||||
return result.toList()
|
||||
}
|
||||
|
||||
/**
|
||||
* This string must be unique for the pair of users performing verification for the duration that the transaction is valid
|
||||
*/
|
||||
private fun createUniqueIDForTransaction(userId: String, deviceID: String): String {
|
||||
private fun createUniqueIDForTransaction(otherUserId: String, otherDeviceID: String): String {
|
||||
return buildString {
|
||||
append(credentials.userId).append("|")
|
||||
append(credentials.deviceId).append("|")
|
||||
append(userId).append("|")
|
||||
append(deviceID).append("|")
|
||||
append(deviceId).append("|")
|
||||
append(otherUserId).append("|")
|
||||
append(otherDeviceID).append("|")
|
||||
append(UUID.randomUUID().toString())
|
||||
}
|
||||
}
|
||||
|
||||
override fun transactionUpdated(tx: VerificationTransaction) {
|
||||
dispatchTxUpdated(tx)
|
||||
if (tx is SASVerificationTransaction
|
||||
&& (tx.state == SasVerificationTxState.Cancelled
|
||||
|| tx.state == SasVerificationTxState.OnCancelled
|
||||
|| tx.state == SasVerificationTxState.Verified)
|
||||
if (tx is SASDefaultVerificationTransaction
|
||||
&& (tx.state == VerificationTxState.Cancelled
|
||||
|| tx.state == VerificationTxState.OnCancelled
|
||||
|| tx.state == VerificationTxState.Verified)
|
||||
) {
|
||||
// remove
|
||||
this.removeTransaction(tx.otherUserId, tx.transactionId)
|
||||
}
|
||||
if (tx is QrCodeVerificationTransaction
|
||||
&& (tx.state == VerificationTxState.Cancelled
|
||||
|| tx.state == VerificationTxState.OnCancelled
|
||||
|| tx.state == VerificationTxState.Verified)
|
||||
) {
|
||||
// remove
|
||||
this.removeTransaction(tx.otherUserId, tx.transactionId)
|
|
@ -15,17 +15,18 @@
|
|||
*/
|
||||
package im.vector.matrix.android.internal.crypto.verification
|
||||
|
||||
import im.vector.matrix.android.api.session.crypto.sas.CancelCode
|
||||
import im.vector.matrix.android.api.session.crypto.sas.SasVerificationTransaction
|
||||
import im.vector.matrix.android.api.session.crypto.sas.VerificationTransaction
|
||||
|
||||
/**
|
||||
* Generic interactive key verification transaction
|
||||
*/
|
||||
internal abstract class VerificationTransaction(
|
||||
internal abstract class DefaultVerificationTransaction(
|
||||
override val transactionId: String,
|
||||
override val otherUserId: String,
|
||||
override var otherDeviceId: String? = null,
|
||||
override val isIncoming: Boolean) : SasVerificationTransaction {
|
||||
override val isIncoming: Boolean) : VerificationTransaction {
|
||||
|
||||
lateinit var transport: VerificationTransport
|
||||
|
||||
interface Listener {
|
||||
fun transactionUpdated(tx: VerificationTransaction)
|
||||
|
@ -42,6 +43,4 @@ internal abstract class VerificationTransaction(
|
|||
}
|
||||
|
||||
abstract fun acceptVerificationEvent(senderId: String, info: VerificationInfo)
|
||||
|
||||
abstract fun cancel(code: CancelCode)
|
||||
}
|
|
@ -18,12 +18,14 @@ package im.vector.matrix.android.internal.crypto.verification
|
|||
import im.vector.matrix.android.api.session.crypto.sas.CancelCode
|
||||
import im.vector.matrix.android.api.session.crypto.sas.VerificationMethod
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageVerificationRequestContent
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.VERIFICATION_METHOD_QR_CODE_SCAN
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.VERIFICATION_METHOD_QR_CODE_SHOW
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.VERIFICATION_METHOD_SAS
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.VERIFICATION_METHOD_SCAN
|
||||
import java.util.*
|
||||
import java.util.UUID
|
||||
|
||||
/**
|
||||
* Stores current pending verification requests
|
||||
* TODO We should not expose this whole object to the app. Create an interface
|
||||
*/
|
||||
data class PendingVerificationRequest(
|
||||
val ageLocalTs: Long,
|
||||
|
@ -37,7 +39,6 @@ data class PendingVerificationRequest(
|
|||
val cancelConclusion: CancelCode? = null,
|
||||
val isSuccessful: Boolean = false,
|
||||
val handledByOtherSession: Boolean = false
|
||||
|
||||
) {
|
||||
val isReady: Boolean = readyInfo != null
|
||||
val isSent: Boolean = transactionId != null
|
||||
|
@ -47,7 +48,8 @@ data class PendingVerificationRequest(
|
|||
fun hasMethod(method: VerificationMethod): Boolean? {
|
||||
return when (method) {
|
||||
VerificationMethod.SAS -> readyInfo?.methods?.contains(VERIFICATION_METHOD_SAS)
|
||||
VerificationMethod.SCAN -> readyInfo?.methods?.contains(VERIFICATION_METHOD_SCAN)
|
||||
VerificationMethod.QR_CODE_SHOW -> readyInfo?.methods?.contains(VERIFICATION_METHOD_QR_CODE_SHOW)
|
||||
VerificationMethod.QR_CODE_SCAN -> readyInfo?.methods?.contains(VERIFICATION_METHOD_QR_CODE_SCAN)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,17 +16,20 @@
|
|||
package im.vector.matrix.android.internal.crypto.verification
|
||||
|
||||
import android.os.Build
|
||||
import im.vector.matrix.android.api.auth.data.Credentials
|
||||
import im.vector.matrix.android.api.MatrixCallback
|
||||
import im.vector.matrix.android.api.session.crypto.crosssigning.CrossSigningService
|
||||
import im.vector.matrix.android.api.session.crypto.sas.CancelCode
|
||||
import im.vector.matrix.android.api.session.crypto.sas.EmojiRepresentation
|
||||
import im.vector.matrix.android.api.session.crypto.sas.SasMode
|
||||
import im.vector.matrix.android.api.session.crypto.sas.SasVerificationTxState
|
||||
import im.vector.matrix.android.api.session.crypto.sas.SasVerificationTransaction
|
||||
import im.vector.matrix.android.api.session.crypto.sas.VerificationTxState
|
||||
import im.vector.matrix.android.api.session.events.model.EventType
|
||||
import im.vector.matrix.android.internal.crypto.actions.SetDeviceVerificationAction
|
||||
import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo
|
||||
import im.vector.matrix.android.internal.crypto.crosssigning.DeviceTrustLevel
|
||||
import im.vector.matrix.android.internal.crypto.model.MXKey
|
||||
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
|
||||
import im.vector.matrix.android.internal.extensions.toUnsignedInt
|
||||
import im.vector.matrix.android.internal.util.withoutPrefix
|
||||
import org.matrix.olm.OlmSAS
|
||||
import org.matrix.olm.OlmUtility
|
||||
import timber.log.Timber
|
||||
|
@ -35,18 +38,18 @@ import kotlin.properties.Delegates
|
|||
/**
|
||||
* Represents an ongoing short code interactive key verification between two devices.
|
||||
*/
|
||||
internal abstract class SASVerificationTransaction(
|
||||
internal abstract class SASDefaultVerificationTransaction(
|
||||
private val setDeviceVerificationAction: SetDeviceVerificationAction,
|
||||
open val credentials: Credentials,
|
||||
open val userId: String,
|
||||
open val deviceId: String?,
|
||||
private val cryptoStore: IMXCryptoStore,
|
||||
private val crossSigningService: CrossSigningService,
|
||||
private val deviceFingerprint: String,
|
||||
transactionId: String,
|
||||
otherUserId: String,
|
||||
otherDevice: String?,
|
||||
isIncoming: Boolean) :
|
||||
VerificationTransaction(transactionId, otherUserId, otherDevice, isIncoming) {
|
||||
|
||||
lateinit var transport: SasTransport
|
||||
otherDeviceId: String?,
|
||||
isIncoming: Boolean
|
||||
) : DefaultVerificationTransaction(transactionId, otherUserId, otherDeviceId, isIncoming), SasVerificationTransaction {
|
||||
|
||||
companion object {
|
||||
const val SAS_MAC_SHA256_LONGKDF = "hmac-sha256"
|
||||
|
@ -68,7 +71,7 @@ internal abstract class SASVerificationTransaction(
|
|||
}
|
||||
}
|
||||
|
||||
override var state by Delegates.observable(SasVerificationTxState.None) { _, _, new ->
|
||||
override var state by Delegates.observable(VerificationTxState.None) { _, _, new ->
|
||||
// println("$property has changed from $old to $new")
|
||||
listeners.forEach {
|
||||
try {
|
||||
|
@ -77,9 +80,9 @@ internal abstract class SASVerificationTransaction(
|
|||
Timber.e(e, "## Error while notifying listeners")
|
||||
}
|
||||
}
|
||||
if (new == SasVerificationTxState.Cancelled
|
||||
|| new == SasVerificationTxState.OnCancelled
|
||||
|| new == SasVerificationTxState.Verified) {
|
||||
if (new == VerificationTxState.Cancelled
|
||||
|| new == VerificationTxState.OnCancelled
|
||||
|| new == VerificationTxState.Verified) {
|
||||
releaseSAS()
|
||||
}
|
||||
}
|
||||
|
@ -118,14 +121,14 @@ internal abstract class SASVerificationTransaction(
|
|||
*/
|
||||
override fun userHasVerifiedShortCode() {
|
||||
Timber.v("## SAS short code verified by user for id:$transactionId")
|
||||
if (state != SasVerificationTxState.ShortCodeReady) {
|
||||
if (state != VerificationTxState.ShortCodeReady) {
|
||||
// ignore and cancel?
|
||||
Timber.e("## Accepted short code from invalid state $state")
|
||||
cancel(CancelCode.UnexpectedMessage)
|
||||
return
|
||||
}
|
||||
|
||||
state = SasVerificationTxState.ShortCodeAccepted
|
||||
state = VerificationTxState.ShortCodeAccepted
|
||||
// Alice and Bob’ devices calculate the HMAC of their own device keys and a comma-separated,
|
||||
// sorted list of the key IDs that they wish the other user to verify,
|
||||
// the shared secret as the input keying material, no salt, and with the input parameter set to the concatenation of:
|
||||
|
@ -136,15 +139,37 @@ internal abstract class SASVerificationTransaction(
|
|||
// - the device ID of the device receiving the MAC,
|
||||
// - the transaction ID, and
|
||||
// - the key ID of the key being MAC-ed, or the string “KEY_IDS” if the item being MAC-ed is the list of key IDs.
|
||||
val baseInfo = "MATRIX_KEY_VERIFICATION_MAC$userId$deviceId$otherUserId$otherDeviceId$transactionId"
|
||||
|
||||
val baseInfo = "MATRIX_KEY_VERIFICATION_MAC" +
|
||||
credentials.userId + credentials.deviceId +
|
||||
otherUserId + otherDeviceId +
|
||||
transactionId
|
||||
// Previously, with SAS verification, the m.key.verification.mac message only contained the user's device key.
|
||||
// It should now contain both the device key and the MSK.
|
||||
// So when Alice and Bob verify with SAS, the verification will verify the MSK.
|
||||
|
||||
val keyId = "ed25519:${credentials.deviceId}"
|
||||
val keyMap = HashMap<String, String>()
|
||||
|
||||
val keyId = "ed25519:$deviceId"
|
||||
val macString = macUsingAgreedMethod(deviceFingerprint, baseInfo + keyId)
|
||||
val keyStrings = macUsingAgreedMethod(keyId, baseInfo + "KEY_IDS")
|
||||
|
||||
if (macString.isNullOrBlank()) {
|
||||
// Should not happen
|
||||
Timber.e("## SAS verification [$transactionId] failed to send KeyMac, empty key hashes.")
|
||||
cancel(CancelCode.UnexpectedMessage)
|
||||
return
|
||||
}
|
||||
|
||||
keyMap[keyId] = macString
|
||||
|
||||
cryptoStore.getMyCrossSigningInfo()?.takeIf { it.isTrusted() }
|
||||
?.masterKey()
|
||||
?.unpaddedBase64PublicKey
|
||||
?.let { masterPublicKey ->
|
||||
val crossSigningKeyId = "ed25519:$masterPublicKey"
|
||||
macUsingAgreedMethod(masterPublicKey, baseInfo + crossSigningKeyId)?.let { MSKMacString ->
|
||||
keyMap[crossSigningKeyId] = MSKMacString
|
||||
}
|
||||
}
|
||||
|
||||
val keyStrings = macUsingAgreedMethod(keyMap.keys.sorted().joinToString(","), baseInfo + "KEY_IDS")
|
||||
|
||||
if (macString.isNullOrBlank() || keyStrings.isNullOrBlank()) {
|
||||
// Should not happen
|
||||
|
@ -153,13 +178,13 @@ internal abstract class SASVerificationTransaction(
|
|||
return
|
||||
}
|
||||
|
||||
val macMsg = transport.createMac(transactionId, mapOf(keyId to macString), keyStrings)
|
||||
val macMsg = transport.createMac(transactionId, keyMap, keyStrings)
|
||||
myMac = macMsg
|
||||
state = SasVerificationTxState.SendingMac
|
||||
sendToOther(EventType.KEY_VERIFICATION_MAC, macMsg, SasVerificationTxState.MacSent, CancelCode.User) {
|
||||
if (state == SasVerificationTxState.SendingMac) {
|
||||
state = VerificationTxState.SendingMac
|
||||
sendToOther(EventType.KEY_VERIFICATION_MAC, macMsg, VerificationTxState.MacSent, CancelCode.User) {
|
||||
if (state == VerificationTxState.SendingMac) {
|
||||
// It is possible that we receive the next event before this one :/, in this case we should keep state
|
||||
state = SasVerificationTxState.MacSent
|
||||
state = VerificationTxState.MacSent
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -174,11 +199,15 @@ internal abstract class SASVerificationTransaction(
|
|||
cancel(CancelCode.MismatchedSas)
|
||||
}
|
||||
|
||||
override fun isToDeviceTransport(): Boolean {
|
||||
return transport is VerificationTransportToDevice
|
||||
}
|
||||
|
||||
override fun acceptVerificationEvent(senderId: String, info: VerificationInfo) {
|
||||
when (info) {
|
||||
is VerificationInfoStart -> onVerificationStart(info)
|
||||
is VerificationInfoAccept -> onVerificationAccept(info)
|
||||
is VerificationInfoKey -> onKeyVerificationKey(senderId, info)
|
||||
is VerificationInfoKey -> onKeyVerificationKey(info)
|
||||
is VerificationInfoMac -> onKeyVerificationMac(info)
|
||||
else -> {
|
||||
// nop
|
||||
|
@ -190,13 +219,13 @@ internal abstract class SASVerificationTransaction(
|
|||
|
||||
abstract fun onVerificationAccept(accept: VerificationInfoAccept)
|
||||
|
||||
abstract fun onKeyVerificationKey(userId: String, vKey: VerificationInfoKey)
|
||||
abstract fun onKeyVerificationKey(vKey: VerificationInfoKey)
|
||||
|
||||
abstract fun onKeyVerificationMac(vKey: VerificationInfoMac)
|
||||
|
||||
protected fun verifyMacs() {
|
||||
Timber.v("## SAS verifying macs for id:$transactionId")
|
||||
state = SasVerificationTxState.Verifying
|
||||
state = VerificationTxState.Verifying
|
||||
|
||||
// Keys have been downloaded earlier in process
|
||||
val otherUserKnownDevices = cryptoStore.getUserDevices(otherUserId)
|
||||
|
@ -208,7 +237,7 @@ internal abstract class SASVerificationTransaction(
|
|||
|
||||
val baseInfo = "MATRIX_KEY_VERIFICATION_MAC" +
|
||||
otherUserId + otherDeviceId +
|
||||
credentials.userId + credentials.deviceId +
|
||||
userId + deviceId +
|
||||
transactionId
|
||||
|
||||
val commaSeparatedListOfKeyIds = theirMac!!.mac!!.keys.sorted().joinToString(",")
|
||||
|
@ -224,10 +253,10 @@ internal abstract class SASVerificationTransaction(
|
|||
|
||||
// cannot be empty because it has been validated
|
||||
theirMac!!.mac!!.keys.forEach {
|
||||
val keyIDNoPrefix = if (it.startsWith("ed25519:")) it.substring("ed25519:".length) else it
|
||||
val keyIDNoPrefix = it.withoutPrefix("ed25519:")
|
||||
val otherDeviceKey = otherUserKnownDevices?.get(keyIDNoPrefix)?.fingerprint()
|
||||
if (otherDeviceKey == null) {
|
||||
Timber.e("## SAS Verification: Could not find device $keyIDNoPrefix to verify")
|
||||
Timber.w("## SAS Verification: Could not find device $keyIDNoPrefix to verify")
|
||||
// just ignore and continue
|
||||
return@forEach
|
||||
}
|
||||
|
@ -241,26 +270,70 @@ internal abstract class SASVerificationTransaction(
|
|||
verifiedDevices.add(keyIDNoPrefix)
|
||||
}
|
||||
|
||||
var otherMasterKeyIsVerified = false
|
||||
val otherMasterKey = cryptoStore.getCrossSigningInfo(otherUserId)?.masterKey()
|
||||
val otherCrossSigningMasterKeyPublic = otherMasterKey?.unpaddedBase64PublicKey
|
||||
if (otherCrossSigningMasterKeyPublic != null) {
|
||||
// Did the user signed his master key
|
||||
theirMac!!.mac!!.keys.forEach {
|
||||
val keyIDNoPrefix = it.withoutPrefix("ed25519:")
|
||||
if (keyIDNoPrefix == otherCrossSigningMasterKeyPublic) {
|
||||
// Check the signature
|
||||
val mac = macUsingAgreedMethod(otherCrossSigningMasterKeyPublic, baseInfo + it)
|
||||
if (mac != theirMac?.mac?.get(it)) {
|
||||
// WRONG!
|
||||
Timber.e("## SAS Verification: mac mismatch for MasterKey with id $keyIDNoPrefix")
|
||||
cancel(CancelCode.MismatchedKeys)
|
||||
return
|
||||
} else {
|
||||
otherMasterKeyIsVerified = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// if none of the keys could be verified, then error because the app
|
||||
// should be informed about that
|
||||
if (verifiedDevices.isEmpty()) {
|
||||
if (verifiedDevices.isEmpty() && !otherMasterKeyIsVerified) {
|
||||
Timber.e("## SAS Verification: No devices verified")
|
||||
cancel(CancelCode.MismatchedKeys)
|
||||
return
|
||||
}
|
||||
|
||||
// TODO what if the otherDevice is not in this list? and should we
|
||||
verifiedDevices.forEach {
|
||||
setDeviceVerified(it, otherUserId)
|
||||
// If not me sign his MSK and upload the signature
|
||||
if (otherMasterKeyIsVerified && otherUserId != userId) {
|
||||
// we should trust this master key
|
||||
// And check verification MSK -> SSK?
|
||||
crossSigningService.trustUser(otherUserId, object : MatrixCallback<Unit> {
|
||||
override fun onFailure(failure: Throwable) {
|
||||
Timber.e(failure, "## SAS Verification: Failed to trust User $otherUserId")
|
||||
}
|
||||
transport.done(transactionId)
|
||||
state = SasVerificationTxState.Verified
|
||||
})
|
||||
}
|
||||
|
||||
private fun setDeviceVerified(deviceId: String, userId: String) {
|
||||
setDeviceVerificationAction.handle(MXDeviceInfo.DEVICE_VERIFICATION_VERIFIED,
|
||||
deviceId,
|
||||
userId)
|
||||
if (otherUserId == userId) {
|
||||
// If me it's reasonable to sign and upload the device signature
|
||||
// Notice that i might not have the private keys, so may not be able to do it
|
||||
crossSigningService.signDevice(otherDeviceId!!, object : MatrixCallback<Unit> {
|
||||
override fun onFailure(failure: Throwable) {
|
||||
Timber.w(failure, "## SAS Verification: Failed to sign new device $otherDeviceId")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TODO what if the otherDevice is not in this list? and should we
|
||||
verifiedDevices.forEach {
|
||||
setDeviceVerified(otherUserId, it)
|
||||
}
|
||||
transport.done(transactionId)
|
||||
state = VerificationTxState.Verified
|
||||
}
|
||||
|
||||
private fun setDeviceVerified(userId: String, deviceId: String) {
|
||||
// TODO should not override cross sign status
|
||||
setDeviceVerificationAction.handle(DeviceTrustLevel(false, true),
|
||||
userId,
|
||||
deviceId)
|
||||
}
|
||||
|
||||
override fun cancel() {
|
||||
|
@ -269,13 +342,13 @@ internal abstract class SASVerificationTransaction(
|
|||
|
||||
override fun cancel(code: CancelCode) {
|
||||
cancelledReason = code
|
||||
state = SasVerificationTxState.Cancelled
|
||||
state = VerificationTxState.Cancelled
|
||||
transport.cancelTransaction(transactionId, otherUserId, otherDeviceId ?: "", code)
|
||||
}
|
||||
|
||||
protected fun sendToOther(type: String,
|
||||
keyToDevice: VerificationInfo,
|
||||
nextState: SasVerificationTxState,
|
||||
nextState: VerificationTxState,
|
||||
onErrorReason: CancelCode,
|
||||
onDone: (() -> Unit)?) {
|
||||
transport.sendToOther(type, keyToDevice, nextState, onErrorReason, onDone)
|
|
@ -18,6 +18,7 @@ package im.vector.matrix.android.internal.crypto.verification
|
|||
internal interface VerificationInfoStart : VerificationInfo {
|
||||
|
||||
val method: String?
|
||||
|
||||
/**
|
||||
* Alice’s device ID
|
||||
*/
|
||||
|
@ -58,5 +59,10 @@ internal interface VerificationInfoStart : VerificationInfo {
|
|||
*/
|
||||
val shortAuthenticationStrings: List<String>?
|
||||
|
||||
/**
|
||||
* Shared secret, when starting verification with QR code
|
||||
*/
|
||||
val sharedSecret: String?
|
||||
|
||||
fun toCanonicalJson(): String?
|
||||
}
|
||||
|
|
|
@ -37,7 +37,7 @@ internal class VerificationMessageLiveObserver @Inject constructor(
|
|||
@SessionDatabase realmConfiguration: RealmConfiguration,
|
||||
private val roomVerificationUpdateTask: DefaultRoomVerificationUpdateTask,
|
||||
private val cryptoService: CryptoService,
|
||||
private val sasVerificationService: DefaultSasVerificationService,
|
||||
private val sasVerificationService: DefaultVerificationService,
|
||||
private val taskExecutor: TaskExecutor
|
||||
) : RealmLiveEntityObserver<EventEntity>(realmConfiguration) {
|
||||
|
||||
|
|
|
@ -16,29 +16,29 @@
|
|||
package im.vector.matrix.android.internal.crypto.verification
|
||||
|
||||
import im.vector.matrix.android.api.session.crypto.sas.CancelCode
|
||||
import im.vector.matrix.android.api.session.crypto.sas.SasVerificationTxState
|
||||
import im.vector.matrix.android.api.session.crypto.sas.VerificationTxState
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageVerificationRequestContent
|
||||
|
||||
/**
|
||||
* SAS verification can be performed using toDevice events or via DM.
|
||||
* This class abstracts the concept of transport for SAS
|
||||
* Verification can be performed using toDevice events or via DM.
|
||||
* This class abstracts the concept of transport for verification
|
||||
*/
|
||||
internal interface SasTransport {
|
||||
internal interface VerificationTransport {
|
||||
|
||||
/**
|
||||
* Sends a message
|
||||
*/
|
||||
fun sendToOther(type: String,
|
||||
verificationInfo: VerificationInfo,
|
||||
nextState: SasVerificationTxState,
|
||||
nextState: VerificationTxState,
|
||||
onErrorReason: CancelCode,
|
||||
onDone: (() -> Unit)?)
|
||||
|
||||
fun sendVerificationRequest(supportedMethods: List<String>,
|
||||
localID: String,
|
||||
otherUserId: String,
|
||||
roomId: String, callback:
|
||||
(String?, MessageVerificationRequestContent?) -> Unit)
|
||||
roomId: String,
|
||||
callback: (String?, MessageVerificationRequestContent?) -> Unit)
|
||||
|
||||
fun cancelTransaction(transactionId: String,
|
||||
otherUserId: String,
|
||||
|
@ -46,6 +46,7 @@ internal interface SasTransport {
|
|||
code: CancelCode)
|
||||
|
||||
fun done(transactionId: String)
|
||||
|
||||
/**
|
||||
* Creates an accept message suitable for this transport
|
||||
*/
|
||||
|
@ -59,14 +60,23 @@ internal interface SasTransport {
|
|||
fun createKey(tid: String,
|
||||
pubKey: String): VerificationInfoKey
|
||||
|
||||
fun createStart(fromDevice: String,
|
||||
method: String,
|
||||
/**
|
||||
* Create start for SAS verification
|
||||
*/
|
||||
fun createStartForSas(fromDevice: String,
|
||||
transactionID: String,
|
||||
keyAgreementProtocols: List<String>,
|
||||
hashes: List<String>,
|
||||
messageAuthenticationCodes: List<String>,
|
||||
shortAuthenticationStrings: List<String>): VerificationInfoStart
|
||||
|
||||
/**
|
||||
* Create start for QR code verification
|
||||
*/
|
||||
fun createStartForQrCode(fromDevice: String,
|
||||
transactionID: String,
|
||||
sharedSecret: String): VerificationInfoStart
|
||||
|
||||
fun createMac(tid: String, mac: Map<String, String>, keys: String): VerificationInfoMac
|
||||
|
||||
fun createReady(tid: String, fromDevice: String, methods: List<String>): VerificationInfoReady
|
|
@ -16,14 +16,33 @@
|
|||
package im.vector.matrix.android.internal.crypto.verification
|
||||
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.work.*
|
||||
import androidx.work.BackoffPolicy
|
||||
import androidx.work.Data
|
||||
import androidx.work.ExistingWorkPolicy
|
||||
import androidx.work.Operation
|
||||
import androidx.work.WorkInfo
|
||||
import com.zhuinden.monarchy.Monarchy
|
||||
import im.vector.matrix.android.R
|
||||
import im.vector.matrix.android.api.session.crypto.sas.CancelCode
|
||||
import im.vector.matrix.android.api.session.crypto.sas.SasVerificationTxState
|
||||
import im.vector.matrix.android.api.session.events.model.*
|
||||
import im.vector.matrix.android.api.session.room.model.message.*
|
||||
import im.vector.matrix.android.api.session.crypto.sas.VerificationTxState
|
||||
import im.vector.matrix.android.api.session.events.model.Content
|
||||
import im.vector.matrix.android.api.session.events.model.Event
|
||||
import im.vector.matrix.android.api.session.events.model.EventType
|
||||
import im.vector.matrix.android.api.session.events.model.LocalEcho
|
||||
import im.vector.matrix.android.api.session.events.model.RelationType
|
||||
import im.vector.matrix.android.api.session.events.model.UnsignedData
|
||||
import im.vector.matrix.android.api.session.events.model.toContent
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageVerificationAcceptContent
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageVerificationCancelContent
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageVerificationDoneContent
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageVerificationKeyContent
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageVerificationMacContent
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageVerificationReadyContent
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageVerificationRequestContent
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageVerificationStartContent
|
||||
import im.vector.matrix.android.api.session.room.model.relation.RelationDefaultContent
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.VERIFICATION_METHOD_RECIPROCATE
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.VERIFICATION_METHOD_SAS
|
||||
import im.vector.matrix.android.internal.di.DeviceId
|
||||
import im.vector.matrix.android.internal.di.SessionId
|
||||
import im.vector.matrix.android.internal.di.UserId
|
||||
|
@ -35,11 +54,11 @@ import kotlinx.coroutines.Dispatchers
|
|||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import java.util.*
|
||||
import java.util.UUID
|
||||
import java.util.concurrent.TimeUnit
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class SasTransportRoomMessage(
|
||||
internal class VerificationTransportRoomMessage(
|
||||
private val workManagerProvider: WorkManagerProvider,
|
||||
private val stringProvider: StringProvider,
|
||||
private val sessionId: String,
|
||||
|
@ -48,12 +67,12 @@ internal class SasTransportRoomMessage(
|
|||
private val roomId: String,
|
||||
private val monarchy: Monarchy,
|
||||
private val localEchoEventFactory: LocalEchoEventFactory,
|
||||
private val tx: SASVerificationTransaction?
|
||||
) : SasTransport {
|
||||
private val tx: DefaultVerificationTransaction?
|
||||
) : VerificationTransport {
|
||||
|
||||
override fun sendToOther(type: String,
|
||||
verificationInfo: VerificationInfo,
|
||||
nextState: SasVerificationTxState,
|
||||
nextState: VerificationTxState,
|
||||
onErrorReason: CancelCode,
|
||||
onDone: (() -> Unit)?) {
|
||||
Timber.d("## SAS sending msg type $type")
|
||||
|
@ -245,8 +264,7 @@ internal class SasTransportRoomMessage(
|
|||
|
||||
override fun createMac(tid: String, mac: Map<String, String>, keys: String) = MessageVerificationMacContent.create(tid, mac, keys)
|
||||
|
||||
override fun createStart(fromDevice: String,
|
||||
method: String,
|
||||
override fun createStartForSas(fromDevice: String,
|
||||
transactionID: String,
|
||||
keyAgreementProtocols: List<String>,
|
||||
hashes: List<String>,
|
||||
|
@ -258,11 +276,30 @@ internal class SasTransportRoomMessage(
|
|||
keyAgreementProtocols,
|
||||
messageAuthenticationCodes,
|
||||
shortAuthenticationStrings,
|
||||
method,
|
||||
VERIFICATION_METHOD_SAS,
|
||||
RelationDefaultContent(
|
||||
type = RelationType.REFERENCE,
|
||||
eventId = transactionID
|
||||
),
|
||||
null
|
||||
)
|
||||
}
|
||||
|
||||
override fun createStartForQrCode(fromDevice: String,
|
||||
transactionID: String,
|
||||
sharedSecret: String): VerificationInfoStart {
|
||||
return MessageVerificationStartContent(
|
||||
fromDevice,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
VERIFICATION_METHOD_RECIPROCATE,
|
||||
RelationDefaultContent(
|
||||
type = RelationType.REFERENCE,
|
||||
eventId = transactionID
|
||||
),
|
||||
sharedSecret
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -292,7 +329,7 @@ internal class SasTransportRoomMessage(
|
|||
}
|
||||
}
|
||||
|
||||
internal class SasTransportRoomMessageFactory @Inject constructor(
|
||||
internal class VerificationTransportRoomMessageFactory @Inject constructor(
|
||||
private val workManagerProvider: WorkManagerProvider,
|
||||
private val stringProvider: StringProvider,
|
||||
private val monarchy: Monarchy,
|
||||
|
@ -304,7 +341,7 @@ internal class SasTransportRoomMessageFactory @Inject constructor(
|
|||
private val deviceId: String?,
|
||||
private val localEchoEventFactory: LocalEchoEventFactory) {
|
||||
|
||||
fun createTransport(roomId: String, tx: SASVerificationTransaction?): SasTransportRoomMessage {
|
||||
return SasTransportRoomMessage(workManagerProvider, stringProvider, sessionId, userId, deviceId, roomId, monarchy, localEchoEventFactory, tx)
|
||||
fun createTransport(roomId: String, tx: DefaultVerificationTransaction?): VerificationTransportRoomMessage {
|
||||
return VerificationTransportRoomMessage(workManagerProvider, stringProvider, sessionId, userId, deviceId, roomId, monarchy, localEchoEventFactory, tx)
|
||||
}
|
||||
}
|
|
@ -17,22 +17,29 @@ package im.vector.matrix.android.internal.crypto.verification
|
|||
|
||||
import im.vector.matrix.android.api.MatrixCallback
|
||||
import im.vector.matrix.android.api.session.crypto.sas.CancelCode
|
||||
import im.vector.matrix.android.api.session.crypto.sas.SasVerificationTxState
|
||||
import im.vector.matrix.android.api.session.crypto.sas.VerificationTxState
|
||||
import im.vector.matrix.android.api.session.events.model.EventType
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageVerificationRequestContent
|
||||
import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.*
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.KeyVerificationAccept
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.KeyVerificationCancel
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.KeyVerificationKey
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.KeyVerificationMac
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.KeyVerificationReady
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.KeyVerificationStart
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.VERIFICATION_METHOD_RECIPROCATE
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.VERIFICATION_METHOD_SAS
|
||||
import im.vector.matrix.android.internal.crypto.tasks.SendToDeviceTask
|
||||
import im.vector.matrix.android.internal.task.TaskExecutor
|
||||
import im.vector.matrix.android.internal.task.configureWith
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class SasTransportToDevice(
|
||||
private var tx: SASVerificationTransaction?,
|
||||
internal class VerificationTransportToDevice(
|
||||
private var tx: DefaultVerificationTransaction?,
|
||||
private var sendToDeviceTask: SendToDeviceTask,
|
||||
private var taskExecutor: TaskExecutor
|
||||
) : SasTransport {
|
||||
) : VerificationTransport {
|
||||
|
||||
override fun sendVerificationRequest(supportedMethods: List<String>,
|
||||
localID: String,
|
||||
|
@ -44,7 +51,7 @@ internal class SasTransportToDevice(
|
|||
|
||||
override fun sendToOther(type: String,
|
||||
verificationInfo: VerificationInfo,
|
||||
nextState: SasVerificationTxState,
|
||||
nextState: VerificationTxState,
|
||||
onErrorReason: CancelCode,
|
||||
onDone: (() -> Unit)?) {
|
||||
Timber.d("## SAS sending msg type $type")
|
||||
|
@ -119,8 +126,7 @@ internal class SasTransportToDevice(
|
|||
|
||||
override fun createMac(tid: String, mac: Map<String, String>, keys: String) = KeyVerificationMac.create(tid, mac, keys)
|
||||
|
||||
override fun createStart(fromDevice: String,
|
||||
method: String,
|
||||
override fun createStartForSas(fromDevice: String,
|
||||
transactionID: String,
|
||||
keyAgreementProtocols: List<String>,
|
||||
hashes: List<String>,
|
||||
|
@ -128,12 +134,27 @@ internal class SasTransportToDevice(
|
|||
shortAuthenticationStrings: List<String>): VerificationInfoStart {
|
||||
return KeyVerificationStart(
|
||||
fromDevice,
|
||||
method,
|
||||
VERIFICATION_METHOD_SAS,
|
||||
transactionID,
|
||||
keyAgreementProtocols,
|
||||
hashes,
|
||||
messageAuthenticationCodes,
|
||||
shortAuthenticationStrings)
|
||||
shortAuthenticationStrings,
|
||||
null)
|
||||
}
|
||||
|
||||
override fun createStartForQrCode(fromDevice: String,
|
||||
transactionID: String,
|
||||
sharedSecret: String): VerificationInfoStart {
|
||||
return KeyVerificationStart(
|
||||
fromDevice,
|
||||
VERIFICATION_METHOD_RECIPROCATE,
|
||||
transactionID,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
sharedSecret)
|
||||
}
|
||||
|
||||
override fun createReady(tid: String, fromDevice: String, methods: List<String>): VerificationInfoReady {
|
||||
|
@ -145,11 +166,11 @@ internal class SasTransportToDevice(
|
|||
}
|
||||
}
|
||||
|
||||
internal class SasTransportToDeviceFactory @Inject constructor(
|
||||
internal class VerificationTransportToDeviceFactory @Inject constructor(
|
||||
private val sendToDeviceTask: SendToDeviceTask,
|
||||
private val taskExecutor: TaskExecutor) {
|
||||
|
||||
fun createTransport(tx: SASVerificationTransaction?): SasTransportToDevice {
|
||||
return SasTransportToDevice(tx, sendToDeviceTask, taskExecutor)
|
||||
fun createTransport(tx: DefaultVerificationTransaction?): VerificationTransportToDevice {
|
||||
return VerificationTransportToDevice(tx, sendToDeviceTask, taskExecutor)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,245 @@
|
|||
/*
|
||||
* 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.verification.qrcode
|
||||
|
||||
import im.vector.matrix.android.api.MatrixCallback
|
||||
import im.vector.matrix.android.api.session.crypto.crosssigning.CrossSigningService
|
||||
import im.vector.matrix.android.api.session.crypto.sas.CancelCode
|
||||
import im.vector.matrix.android.api.session.crypto.sas.QrCodeVerificationTransaction
|
||||
import im.vector.matrix.android.api.session.crypto.sas.VerificationTxState
|
||||
import im.vector.matrix.android.api.session.events.model.EventType
|
||||
import im.vector.matrix.android.internal.crypto.actions.SetDeviceVerificationAction
|
||||
import im.vector.matrix.android.internal.crypto.crosssigning.DeviceTrustLevel
|
||||
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
|
||||
import im.vector.matrix.android.internal.crypto.verification.DefaultVerificationTransaction
|
||||
import im.vector.matrix.android.internal.crypto.verification.VerificationInfo
|
||||
import im.vector.matrix.android.internal.crypto.verification.VerificationInfoStart
|
||||
import im.vector.matrix.android.internal.util.withoutPrefix
|
||||
import timber.log.Timber
|
||||
import kotlin.properties.Delegates
|
||||
|
||||
internal class DefaultQrCodeVerificationTransaction(
|
||||
private val setDeviceVerificationAction: SetDeviceVerificationAction,
|
||||
override val transactionId: String,
|
||||
override val otherUserId: String,
|
||||
override var otherDeviceId: String?,
|
||||
private val crossSigningService: CrossSigningService,
|
||||
private val cryptoStore: IMXCryptoStore,
|
||||
// Not null only if other user is able to scan QR code
|
||||
private val qrCodeData: QrCodeData?,
|
||||
val userId: String,
|
||||
val deviceId: String,
|
||||
override val isIncoming: Boolean
|
||||
) : DefaultVerificationTransaction(transactionId, otherUserId, otherDeviceId, isIncoming), QrCodeVerificationTransaction {
|
||||
|
||||
override var cancelledReason: CancelCode? = null
|
||||
|
||||
override val qrCodeText: String?
|
||||
get() = qrCodeData?.toUrl()
|
||||
|
||||
override var state by Delegates.observable(VerificationTxState.None) { _, _, _ ->
|
||||
listeners.forEach {
|
||||
try {
|
||||
it.transactionUpdated(this)
|
||||
} catch (e: Throwable) {
|
||||
Timber.e(e, "## Error while notifying listeners")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun userHasScannedOtherQrCode(otherQrCodeText: String) {
|
||||
val otherQrCodeData = otherQrCodeText.toQrCodeData() ?: run {
|
||||
Timber.d("## Verification QR: Invalid QR Code Data")
|
||||
cancel(CancelCode.QrCodeInvalid)
|
||||
return
|
||||
}
|
||||
|
||||
// Perform some checks
|
||||
if (otherQrCodeData.action != QrCodeData.ACTION_VERIFY) {
|
||||
Timber.d("## Verification QR: Invalid action ${otherQrCodeData.action}")
|
||||
cancel(CancelCode.QrCodeInvalid)
|
||||
return
|
||||
}
|
||||
|
||||
if (otherQrCodeData.userId != otherUserId) {
|
||||
Timber.d("## Verification QR: Mismatched user ${otherQrCodeData.userId}")
|
||||
cancel(CancelCode.MismatchedUser)
|
||||
return
|
||||
}
|
||||
|
||||
if (otherQrCodeData.requestEventId != transactionId) {
|
||||
Timber.d("## Verification QR: Invalid transaction actual ${otherQrCodeData.requestEventId} expected:$transactionId")
|
||||
cancel(CancelCode.QrCodeInvalid)
|
||||
return
|
||||
}
|
||||
|
||||
// check master key
|
||||
if (otherQrCodeData.otherUserKey != crossSigningService.getUserCrossSigningKeys(userId)?.masterKey()?.unpaddedBase64PublicKey) {
|
||||
Timber.d("## Verification QR: Invalid other master key ${otherQrCodeData.otherUserKey}")
|
||||
cancel(CancelCode.MismatchedKeys)
|
||||
return
|
||||
}
|
||||
|
||||
val toVerifyDeviceIds = mutableListOf<String>()
|
||||
var canTrustOtherUserMasterKey = false
|
||||
|
||||
val otherDevices = cryptoStore.getUserDevices(otherUserId)
|
||||
otherQrCodeData.keys.keys.forEach { key ->
|
||||
Timber.w("## Verification QR: Checking key $key")
|
||||
|
||||
when (val keyNoPrefix = key.withoutPrefix("ed25519:")) {
|
||||
otherQrCodeData.keys[key] -> {
|
||||
// Maybe master key?
|
||||
if (otherQrCodeData.keys[key] == crossSigningService.getUserCrossSigningKeys(otherUserId)?.masterKey()?.unpaddedBase64PublicKey) {
|
||||
canTrustOtherUserMasterKey = true
|
||||
} else {
|
||||
cancel(CancelCode.MismatchedKeys)
|
||||
return
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
when (val otherDevice = otherDevices?.get(keyNoPrefix)) {
|
||||
null -> {
|
||||
// Unknown device, ignore
|
||||
}
|
||||
else -> {
|
||||
when (otherDevice.fingerprint()) {
|
||||
null -> {
|
||||
// Ignore
|
||||
}
|
||||
otherQrCodeData.keys[key] -> {
|
||||
// Store the deviceId to verify after
|
||||
toVerifyDeviceIds.add(key)
|
||||
}
|
||||
else -> {
|
||||
cancel(CancelCode.MismatchedKeys)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!canTrustOtherUserMasterKey && toVerifyDeviceIds.isEmpty()) {
|
||||
// Nothing to verify
|
||||
cancel(CancelCode.MismatchedKeys)
|
||||
return
|
||||
}
|
||||
|
||||
// All checks are correct
|
||||
// Send the shared secret so that sender can trust me
|
||||
// qrCodeData.sharedSecret will be used to send the start request
|
||||
start(otherQrCodeData.sharedSecret)
|
||||
|
||||
// Trust the other user
|
||||
trust(canTrustOtherUserMasterKey, toVerifyDeviceIds)
|
||||
}
|
||||
|
||||
fun start(remoteSecret: String) {
|
||||
if (state != VerificationTxState.None) {
|
||||
Timber.e("## Verification QR: start verification from invalid state")
|
||||
// should I cancel??
|
||||
throw IllegalStateException("Interactive Key verification already started")
|
||||
}
|
||||
|
||||
val startMessage = transport.createStartForQrCode(
|
||||
deviceId,
|
||||
transactionId,
|
||||
remoteSecret
|
||||
)
|
||||
|
||||
transport.sendToOther(
|
||||
EventType.KEY_VERIFICATION_START,
|
||||
startMessage,
|
||||
VerificationTxState.Started,
|
||||
CancelCode.User,
|
||||
null
|
||||
)
|
||||
}
|
||||
|
||||
override fun acceptVerificationEvent(senderId: String, info: VerificationInfo) {
|
||||
}
|
||||
|
||||
override fun cancel() {
|
||||
cancel(CancelCode.User)
|
||||
}
|
||||
|
||||
override fun cancel(code: CancelCode) {
|
||||
cancelledReason = code
|
||||
state = VerificationTxState.Cancelled
|
||||
transport.cancelTransaction(transactionId, otherUserId, otherDeviceId ?: "", code)
|
||||
}
|
||||
|
||||
override fun isToDeviceTransport() = false
|
||||
|
||||
// Other user has scanned our QR code. check that the secret matched, so we can trust him
|
||||
fun onStartReceived(startReq: VerificationInfoStart) {
|
||||
if (qrCodeData == null) {
|
||||
// Should not happen
|
||||
cancel(CancelCode.UnexpectedMessage)
|
||||
return
|
||||
}
|
||||
|
||||
if (startReq.sharedSecret == qrCodeData.sharedSecret) {
|
||||
// Ok, we can trust the other user
|
||||
// We can only trust the master key in this case
|
||||
trust(true, emptyList())
|
||||
} else {
|
||||
// Display a warning
|
||||
cancel(CancelCode.MismatchedKeys)
|
||||
}
|
||||
}
|
||||
|
||||
private fun trust(canTrustOtherUserMasterKey: Boolean, toVerifyDeviceIds: List<String>) {
|
||||
// If not me sign his MSK and upload the signature
|
||||
if (otherUserId != userId && canTrustOtherUserMasterKey) {
|
||||
// we should trust this master key
|
||||
// And check verification MSK -> SSK?
|
||||
crossSigningService.trustUser(otherUserId, object : MatrixCallback<Unit> {
|
||||
override fun onFailure(failure: Throwable) {
|
||||
Timber.e(failure, "## QR Verification: Failed to trust User $otherUserId")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (otherUserId == userId) {
|
||||
// If me it's reasonable to sign and upload the device signature
|
||||
// Notice that i might not have the private keys, so may not be able to do it
|
||||
crossSigningService.signDevice(otherDeviceId!!, object : MatrixCallback<Unit> {
|
||||
override fun onFailure(failure: Throwable) {
|
||||
Timber.w(failure, "## QR Verification: Failed to sign new device $otherDeviceId")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TODO what if the otherDevice is not in this list? and should we
|
||||
toVerifyDeviceIds.forEach {
|
||||
setDeviceVerified(otherUserId, it)
|
||||
}
|
||||
transport.done(transactionId)
|
||||
state = VerificationTxState.Verified
|
||||
}
|
||||
|
||||
private fun setDeviceVerified(userId: String, deviceId: String) {
|
||||
// TODO should not override cross sign status
|
||||
setDeviceVerificationAction.handle(DeviceTrustLevel(crossSigningVerified = false, locallyVerified = true),
|
||||
userId,
|
||||
deviceId)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,120 @@
|
|||
/*
|
||||
* 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.verification.qrcode
|
||||
|
||||
import im.vector.matrix.android.api.MatrixPatterns
|
||||
import im.vector.matrix.android.api.permalinks.PermalinkFactory
|
||||
import java.net.URLDecoder
|
||||
import java.net.URLEncoder
|
||||
|
||||
private const val ENCODING = "utf-8"
|
||||
|
||||
/**
|
||||
* Generate an URL to generate a QR code of the form:
|
||||
* <pre>
|
||||
* https://matrix.to/#/<user-id>?
|
||||
* request=<event-id>
|
||||
* &action=verify
|
||||
* &key_<keyid>=<key-in-base64>...
|
||||
* &secret=<shared_secret>
|
||||
* &other_user_key=<master-key-in-base64>
|
||||
*
|
||||
* Example:
|
||||
* https://matrix.to/#/@user:matrix.org?
|
||||
* request=%24pBeIfm7REDACTEDSQJbgqvi-yYiwmPB8_H_W_O974
|
||||
* &action=verify
|
||||
* &key_VJEDVKUYTQ=DL7LWIw7Qp%2B4AREDACTEDOwy2BjygumSWAGfzaWY
|
||||
* &key_fsh%2FfQ08N3xvh4ySXsINB%2BJ2hREDACTEDVcVOG4qqo=fsh%2FfQ08N3xvh4ySXsINB%2BJ2hREDACTEDVcVOG4qqo
|
||||
* &secret=AjQqw51Fp6UBuPolZ2FAD5WnXc22ZhJG6iGslrVvIdw%3D
|
||||
* &other_user_key=WqSVLkBCS%2Fi5NqR%2F%2FymC8T7K9RPxBIuqK8Usl6Y3big
|
||||
* </pre>
|
||||
*/
|
||||
fun QrCodeData.toUrl(): String {
|
||||
return buildString {
|
||||
append(PermalinkFactory.createPermalink(userId))
|
||||
append("?request=")
|
||||
append(URLEncoder.encode(requestEventId, ENCODING))
|
||||
append("&action=")
|
||||
append(URLEncoder.encode(action, ENCODING))
|
||||
|
||||
for ((keyId, key) in keys) {
|
||||
append("&key_$keyId=")
|
||||
append(URLEncoder.encode(key, ENCODING))
|
||||
}
|
||||
|
||||
append("&secret=")
|
||||
append(URLEncoder.encode(sharedSecret, ENCODING))
|
||||
append("&other_user_key=")
|
||||
append(URLEncoder.encode(otherUserKey, ENCODING))
|
||||
}
|
||||
}
|
||||
|
||||
fun String.toQrCodeData(): QrCodeData? {
|
||||
if (!startsWith(PermalinkFactory.MATRIX_TO_URL_BASE)) {
|
||||
return null
|
||||
}
|
||||
|
||||
val fragment = substringAfter("#")
|
||||
if (fragment.isEmpty()) {
|
||||
return null
|
||||
}
|
||||
|
||||
val safeFragment = fragment.substringBefore("?")
|
||||
|
||||
// we are limiting to 2 params
|
||||
val params = safeFragment
|
||||
.split(MatrixPatterns.SEP_REGEX.toRegex())
|
||||
.filter { it.isNotEmpty() }
|
||||
|
||||
if (params.size != 1) {
|
||||
return null
|
||||
}
|
||||
|
||||
val userId = params.getOrNull(0)
|
||||
?.let { PermalinkFactory.unescape(it) }
|
||||
?.takeIf { MatrixPatterns.isUserId(it) } ?: return null
|
||||
|
||||
val urlParams = fragment.substringAfter("?")
|
||||
.split("&".toRegex())
|
||||
.filter { it.isNotEmpty() }
|
||||
|
||||
val keyValues = urlParams.map {
|
||||
(it.substringBefore("=") to it.substringAfter("=").let { value -> URLDecoder.decode(value, ENCODING) })
|
||||
}.toMap()
|
||||
|
||||
val action = keyValues["action"] ?: return null
|
||||
|
||||
val requestEventId = keyValues["request"]?.takeIf { MatrixPatterns.isEventId(it) } ?: return null
|
||||
val sharedSecret = keyValues["secret"] ?: return null
|
||||
val otherUserKey = keyValues["other_user_key"] ?: return null
|
||||
|
||||
val keys = keyValues.keys
|
||||
.filter { it.startsWith("key_") }
|
||||
.map {
|
||||
it.substringAfter("key_") to (keyValues[it] ?: return null)
|
||||
}
|
||||
.toMap()
|
||||
|
||||
return QrCodeData(
|
||||
userId,
|
||||
requestEventId,
|
||||
action,
|
||||
keys,
|
||||
sharedSecret,
|
||||
otherUserKey
|
||||
)
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue