Initial commit

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

2
.gitignore vendored
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,47 @@
/*
* Copyright 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.api.session.crypto.crosssigning
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.internal.crypto.model.rest.SignatureUploadResponse
import im.vector.matrix.android.internal.crypto.model.rest.UserPasswordAuth
interface CrossSigningService {
fun isUserTrusted(userId: String) : Boolean
fun checkUserTrust(userId: String, callback: MatrixCallback<Boolean>? = null)
/**
* Initialize cross signing for this user.
* Users needs to enter credentials
*/
fun initializeCrossSigning(authParams: UserPasswordAuth?, callback: MatrixCallback<Unit>? = null)
fun getUserCrossSigningKeys(userId: String): MXCrossSigningInfo?
fun getMyCrossSigningKeys(): MXCrossSigningInfo?
fun trustUser(userId: String, callback: MatrixCallback<SignatureUploadResponse>)
/**
* Sign one of your devices and upload the signature
*/
fun signDevice(deviceId: String, callback: MatrixCallback<SignatureUploadResponse>)
fun checkDeviceTrust(userId: String, deviceId: String, callback: MatrixCallback<Unit>)
}

View File

@ -0,0 +1,38 @@
/*
* Copyright 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.api.session.crypto.crosssigning
/**
* Defines the account cross signing state.
*
*/
enum class CrossSigningState {
/** Current state is unknown, need to download user keys from server to resolve */
Unknown,
/** Currently dowloading user keys*/
CheckingState,
/** No Cross signing keys are defined on the server */
Disabled,
/** CrossSigning keys are beeing created and uploaded to the server */
Enabling,
/** Cross signing keys exists and are trusted*/
Trusted,
/** Cross signing keys exists but are not yet trusted*/
Untrusted,
/** The local cross signing keys do not match with the server keys*/
Conflicted
}

View File

@ -0,0 +1,47 @@
/*
* Copyright 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.api.session.crypto.crosssigning
import im.vector.matrix.android.internal.crypto.model.rest.CrossSigningKeyInfo
data class MXCrossSigningInfo(
/**
* the user id
*/
// @Json(name = "user_id")
var userId: String,
// @Json(name = "user_keys")
var crossSigningKeys: List<CrossSigningKeyInfo> = ArrayList(),
val isTrusted: Boolean = false
) {
fun masterKey(): CrossSigningKeyInfo? = crossSigningKeys
.firstOrNull { it.usages?.contains(CrossSigningKeyInfo.KeyUsage.MASTER.value) == true }
fun userKey(): CrossSigningKeyInfo? = crossSigningKeys
.firstOrNull { it.usages?.contains(CrossSigningKeyInfo.KeyUsage.USER_SIGNING.value) == true }
fun selfSigningKey(): CrossSigningKeyInfo? = crossSigningKeys
.firstOrNull { it.usages?.contains(CrossSigningKeyInfo.KeyUsage.SELF_SIGNING.value) == true }
}

View File

@ -0,0 +1,80 @@
///*
// * Copyright 2020 New Vector Ltd
// *
// * Licensed under the Apache License, Version 2.0 (the "License");
// * you may not use this file except in compliance with the License.
// * You may obtain a copy of the License at
// *
// * http://www.apache.org/licenses/LICENSE-2.0
// *
// * Unless required by applicable law or agreed to in writing, software
// * distributed under the License is distributed on an "AS IS" BASIS,
// * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// * See the License for the specific language governing permissions and
// * limitations under the License.
// */
//
//package im.vector.matrix.android.api.session.crypto.crosssigning
//
//import com.squareup.moshi.Json
//import com.squareup.moshi.JsonClass
//import im.vector.matrix.android.internal.crypto.model.rest.CrossSigningKeyInfo
//
//
//@JsonClass(generateAdapter = true)
//data class MXKeyInfo(
//
// @Json(name = "public_key")
// val publicKeyBase64: String,
// val privateKeyBase64: String?,
//
// @Json(name = "is_trusted")
// val isTrusted: Boolean = false,
//
//
// @Json(name = "usage")
// val usage: List<String> = ArrayList(),
//
// /**
// * The signature of this MXDeviceInfo.
// * A map from "<userId>" to a map from "<key type>:<Publickey>" to "<signature>"
// */
// @Json(name = "signatures")
// var signatures: Map<String, Map<String, String>>? = null
//
//) {
//
// data class Builder(
// private val publicKeyBase64: String,
// private val usage: CrossSigningKeyInfo.KeyUsage,
// private var trusted: Boolean = false,
// private val signatures: ArrayList<Triple<String, String, String>> = ArrayList()
// ) {
//
// fun signature(userId: String, keyUsedToSignBase64: String, base64Signature: String) = apply {
// signatures.add(Triple(userId, keyUsedToSignBase64, base64Signature))
// }
//
// fun trusted(trusted: Boolean) = apply {
// this.trusted = trusted
// }
//
// fun build(): MXKeyInfo {
//
// val signMap = HashMap<String, HashMap<String, String>>()
// signatures.forEach { info ->
// val uMap = signMap[info.first]
// ?: HashMap<String, String>().also { signMap[info.first] = it }
// uMap["ed25519:${info.second}"] = info.third
// }
//
// return MXKeyInfo(
// publicKeyBase64 = publicKeyBase64,
// usage = listOf(usage.value),
// isTrusted = trusted,
// signatures = signMap
// )
// }
// }
//}
//

View File

@ -132,6 +132,12 @@ internal abstract class CryptoModule {
@Binds
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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +1,6 @@
/*
* Copyright 2016 OpenMarket Ltd
* Copyright 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,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,51 @@
/*
* Copyright 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.crypto.store.db.model
import im.vector.matrix.android.internal.crypto.model.rest.CrossSigningKeyInfo
import io.realm.RealmList
import io.realm.RealmObject
import io.realm.annotations.PrimaryKey
internal open class CrossSigningInfoEntity(
@PrimaryKey
var userId: String? = null,
var crossSigningKeys: RealmList<KeyInfoEntity> = RealmList(),
var isTrusted: Boolean = false
) : RealmObject() {
companion object
fun getMasterKey() = crossSigningKeys.firstOrNull { it.usages.contains(CrossSigningKeyInfo.KeyUsage.MASTER.value) }
fun setMasterKey(info: KeyInfoEntity?) {
crossSigningKeys
.filter { it.usages.contains(CrossSigningKeyInfo.KeyUsage.MASTER.value) }
.forEach { crossSigningKeys.remove(it) }
info?.let { crossSigningKeys.add(it) }
}
fun getSelfSignedKey() = crossSigningKeys.firstOrNull { it.usages.contains(CrossSigningKeyInfo.KeyUsage.SELF_SIGNING.value) }
fun setSelfSignedKey(info: KeyInfoEntity?) {
crossSigningKeys
.filter { it.usages.contains(CrossSigningKeyInfo.KeyUsage.SELF_SIGNING.value) }
.forEach { crossSigningKeys.remove(it) }
info?.let { crossSigningKeys.add(it) }
}
}

View File

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

View File

@ -0,0 +1,45 @@
/*
* Copyright 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.crypto.store.db.model
import im.vector.matrix.android.internal.crypto.store.db.deserializeFromRealm
import im.vector.matrix.android.internal.crypto.store.db.serializeForRealm
import io.realm.RealmList
import io.realm.RealmObject
internal open class KeyInfoEntity(
var publicKeyBase64: String? = null,
// var isTrusted: Boolean = false,
var usages: RealmList<String> = RealmList(),
/**
* The signature of this MXDeviceInfo.
* A map from "<userId>" to a map from "<key type>:<Publickey>" to "<signature>"
*/
var signatures: String? = null
) : RealmObject() {
// Deserialize data
fun getSignatures(): Map<String, Map<String, String>>? {
return deserializeFromRealm(signatures)
}
// Serialize data
fun putSignatures(deviceInfo: Map<String, Map<String, String>>?) {
signatures = serializeForRealm(deviceInfo)
}
}

View File

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

View File

@ -0,0 +1,37 @@
/*
* Copyright 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.crypto.store.db.query
import im.vector.matrix.android.internal.crypto.store.db.model.CrossSigningInfoEntity
import im.vector.matrix.android.internal.crypto.store.db.model.UserEntityFields
import io.realm.Realm
import io.realm.kotlin.createObject
import io.realm.kotlin.where
internal fun CrossSigningInfoEntity.Companion.getOrCreate(realm: Realm, userId: String): CrossSigningInfoEntity {
return realm.where<CrossSigningInfoEntity>()
.equalTo(UserEntityFields.USER_ID, userId)
.findFirst()
?: realm.createObject(userId)
}
internal fun CrossSigningInfoEntity.Companion.get(realm: Realm, userId: String): CrossSigningInfoEntity? {
return realm.where<CrossSigningInfoEntity>()
.equalTo(UserEntityFields.USER_ID, userId)
.findFirst()
}

View File

@ -18,7 +18,7 @@ package im.vector.matrix.android.internal.crypto.tasks
import im.vector.matrix.android.internal.auth.data.LoginFlowTypes
import im.vector.matrix.android.internal.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

View File

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

View File

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

View File

@ -108,7 +108,7 @@ internal class DefaultIncomingSASVerificationTransaction(
}
// Bobs device ensures that it has a copy of Alices device key.
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 ")

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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