Merge pull request #1162 from vector-im/feature/xs_detect_new_session

Feature/xs detect new session
This commit is contained in:
Valere 2020-03-26 12:06:18 +01:00 committed by GitHub
commit 535cdf0ef5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
65 changed files with 1479 additions and 318 deletions

View file

@ -3,6 +3,7 @@ Changes in RiotX 0.19.0 (2020-XX-XX)
Features ✨:
- Cross-Signing | Support SSSS secret sharing (#944)
- Cross-Signing | Verify new session from existing session (#1134)
Improvements 🙌:
- Verification DM / Handle concurrent .start after .ready (#794)

View file

@ -266,8 +266,8 @@ class CommonTestHelper(context: Context) {
* @param latch
* @throws InterruptedException
*/
fun await(latch: CountDownLatch) {
assertTrue(latch.await(TestConstants.timeOutMillis, TimeUnit.MILLISECONDS))
fun await(latch: CountDownLatch, timout: Long? = TestConstants.timeOutMillis) {
assertTrue(latch.await(timout ?: TestConstants.timeOutMillis, TimeUnit.MILLISECONDS))
}
fun retryPeriodicallyWithLatch(latch: CountDownLatch, condition: (() -> Boolean)) {
@ -282,10 +282,10 @@ class CommonTestHelper(context: Context) {
}
}
fun waitWithLatch(block: (CountDownLatch) -> Unit) {
fun waitWithLatch(timout: Long? = TestConstants.timeOutMillis, block: (CountDownLatch) -> Unit) {
val latch = CountDownLatch(1)
block(latch)
await(latch)
await(latch, timout)
}
// Transform a method with a MatrixCallback to a synchronous method

View file

@ -19,6 +19,12 @@ package im.vector.matrix.android.internal.crypto.gossiping
import android.util.Log
import androidx.test.ext.junit.runners.AndroidJUnit4
import im.vector.matrix.android.InstrumentedTest
import im.vector.matrix.android.api.session.crypto.verification.IncomingSasVerificationTransaction
import im.vector.matrix.android.api.session.crypto.verification.SasVerificationTransaction
import im.vector.matrix.android.api.session.crypto.verification.VerificationMethod
import im.vector.matrix.android.api.session.crypto.verification.VerificationService
import im.vector.matrix.android.api.session.crypto.verification.VerificationTransaction
import im.vector.matrix.android.api.session.crypto.verification.VerificationTxState
import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.model.RoomDirectoryVisibility
import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams
@ -28,7 +34,11 @@ import im.vector.matrix.android.common.TestConstants
import im.vector.matrix.android.internal.crypto.GossipingRequestState
import im.vector.matrix.android.internal.crypto.OutgoingGossipingRequestState
import im.vector.matrix.android.internal.crypto.crosssigning.DeviceTrustLevel
import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap
import im.vector.matrix.android.internal.crypto.model.event.EncryptedEventContent
import im.vector.matrix.android.internal.crypto.model.rest.UserPasswordAuth
import junit.framework.TestCase.assertEquals
import junit.framework.TestCase.assertNotNull
import junit.framework.TestCase.assertTrue
import junit.framework.TestCase.fail
@ -174,4 +184,85 @@ class KeyShareTests : InstrumentedTest {
mTestHelper.signOutAndClose(aliceSession)
mTestHelper.signOutAndClose(aliceSession2)
}
@Test
fun test_ShareSSSSSecret() {
val aliceSession1 = mTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true))
mTestHelper.doSync<Unit> {
aliceSession1.cryptoService().crossSigningService()
.initializeCrossSigning(UserPasswordAuth(
user = aliceSession1.myUserId,
password = TestConstants.PASSWORD
), it)
}
val aliceSession2 = mTestHelper.logIntoAccount(aliceSession1.myUserId, SessionTestParams(true))
val aliceVerificationService1 = aliceSession1.cryptoService().verificationService()
val aliceVerificationService2 = aliceSession2.cryptoService().verificationService()
// force keys download
mTestHelper.doSync<MXUsersDevicesMap<CryptoDeviceInfo>> {
aliceSession1.cryptoService().downloadKeys(listOf(aliceSession1.myUserId), true, it)
}
mTestHelper.doSync<MXUsersDevicesMap<CryptoDeviceInfo>> {
aliceSession2.cryptoService().downloadKeys(listOf(aliceSession2.myUserId), true, it)
}
var session1ShortCode: String? = null
var session2ShortCode: String? = null
aliceVerificationService1.addListener(object : VerificationService.Listener {
override fun transactionUpdated(tx: VerificationTransaction) {
Log.d("#TEST", "AA: tx incoming?:${tx.isIncoming} state ${tx.state}")
if (tx is SasVerificationTransaction) {
if (tx.state == VerificationTxState.OnStarted) {
(tx as IncomingSasVerificationTransaction).performAccept()
}
if (tx.state == VerificationTxState.ShortCodeReady) {
session1ShortCode = tx.getDecimalCodeRepresentation()
tx.userHasVerifiedShortCode()
}
}
}
})
aliceVerificationService2.addListener(object : VerificationService.Listener {
override fun transactionUpdated(tx: VerificationTransaction) {
Log.d("#TEST", "BB: tx incoming?:${tx.isIncoming} state ${tx.state}")
if (tx is SasVerificationTransaction) {
if (tx.state == VerificationTxState.ShortCodeReady) {
session2ShortCode = tx.getDecimalCodeRepresentation()
tx.userHasVerifiedShortCode()
}
}
}
})
val txId: String = "m.testVerif12"
aliceVerificationService2.beginKeyVerification(VerificationMethod.SAS, aliceSession1.myUserId, aliceSession1.sessionParams.credentials.deviceId
?: "", txId)
mTestHelper.waitWithLatch { latch ->
mTestHelper.retryPeriodicallyWithLatch(latch) {
aliceSession1.cryptoService().getDeviceInfo(aliceSession1.myUserId, aliceSession2.sessionParams.credentials.deviceId ?: "")?.isVerified == true
}
}
assertNotNull(session1ShortCode)
Log.d("#TEST", "session1ShortCode: $session1ShortCode")
assertNotNull(session2ShortCode)
Log.d("#TEST", "session2ShortCode: $session2ShortCode")
assertEquals(session1ShortCode, session2ShortCode)
// SSK and USK private keys should have been shared
mTestHelper.waitWithLatch(60_000) { latch ->
mTestHelper.retryPeriodicallyWithLatch(latch) {
Log.d("#TEST", "CAN XS :${ aliceSession2.cryptoService().crossSigningService().getMyCrossSigningKeys()}")
aliceSession2.cryptoService().crossSigningService().canCrossSign()
}
}
}
}

View file

@ -60,6 +60,8 @@ interface VerificationService {
roomId: String,
localId: String? = LocalEcho.createLocalEchoId()): PendingVerificationRequest
fun cancelVerificationRequest(request: PendingVerificationRequest)
/**
* Request a key verification from another user using toDevice events.
*/

View file

@ -140,7 +140,7 @@ internal class DefaultCryptoService @Inject constructor(
private val crossSigningService: DefaultCrossSigningService,
//
private val incomingRoomKeyRequestManager: IncomingRoomKeyRequestManager,
private val incomingGossipingRequestManager: IncomingGossipingRequestManager,
//
private val outgoingGossipingRequestManager: OutgoingGossipingRequestManager,
// Actions
@ -239,7 +239,7 @@ internal class DefaultCryptoService @Inject constructor(
override fun getDevicesList(callback: MatrixCallback<DevicesListResponse>) {
getDevicesTask
.configureWith {
this.executionThread = TaskThread.CRYPTO
// this.executionThread = TaskThread.CRYPTO
this.callback = callback
}
.executeBy(taskExecutor)
@ -317,7 +317,7 @@ internal class DefaultCryptoService @Inject constructor(
deviceListManager.invalidateAllDeviceLists()
deviceListManager.refreshOutdatedDeviceLists()
} else {
incomingRoomKeyRequestManager.processReceivedGossipingRequests()
incomingGossipingRequestManager.processReceivedGossipingRequests()
}
}.fold(
{
@ -376,7 +376,7 @@ internal class DefaultCryptoService @Inject constructor(
// Make sure we process to-device messages before generating new one-time-keys #2782
deviceListManager.refreshOutdatedDeviceLists()
oneTimeKeysUploader.maybeUploadOneTimeKeys()
incomingRoomKeyRequestManager.processReceivedGossipingRequests()
incomingGossipingRequestManager.processReceivedGossipingRequests()
}
}
}
@ -709,7 +709,7 @@ internal class DefaultCryptoService @Inject constructor(
// save audit trail
cryptoStore.saveGossipingEvent(event)
// Requests are stacked, and will be handled one by one at the end of the sync (onSyncComplete)
incomingRoomKeyRequestManager.onGossipingRequestEvent(event)
incomingGossipingRequestManager.onGossipingRequestEvent(event)
}
EventType.SEND_SECRET -> {
cryptoStore.saveGossipingEvent(event)
@ -729,30 +729,30 @@ internal class DefaultCryptoService @Inject constructor(
*/
private fun onRoomKeyEvent(event: Event) {
val roomKeyContent = event.getClearContent().toModel<RoomKeyContent>() ?: return
Timber.v("## onRoomKeyEvent() : type<${event.type}> , sessionId<${roomKeyContent.sessionId}>")
Timber.v("## GOSSIP onRoomKeyEvent() : type<${event.type}> , sessionId<${roomKeyContent.sessionId}>")
if (roomKeyContent.roomId.isNullOrEmpty() || roomKeyContent.algorithm.isNullOrEmpty()) {
Timber.e("## onRoomKeyEvent() : missing fields")
Timber.e("## GOSSIP onRoomKeyEvent() : missing fields")
return
}
val alg = roomDecryptorProvider.getOrCreateRoomDecryptor(roomKeyContent.roomId, roomKeyContent.algorithm)
if (alg == null) {
Timber.e("## onRoomKeyEvent() : Unable to handle keys for ${roomKeyContent.algorithm}")
Timber.e("## GOSSIP onRoomKeyEvent() : Unable to handle keys for ${roomKeyContent.algorithm}")
return
}
alg.onRoomKeyEvent(event, keysBackupService)
}
private fun onSecretSendReceived(event: Event) {
Timber.i("## onSecretSend() : onSecretSendReceived ${event.content?.get("sender_key")}")
Timber.i("## GOSSIP onSecretSend() : onSecretSendReceived ${event.content?.get("sender_key")}")
if (!event.isEncrypted()) {
// secret send messages must be encrypted
Timber.e("## onSecretSend() :Received unencrypted secret send event")
Timber.e("## GOSSIP onSecretSend() :Received unencrypted secret send event")
return
}
// Was that sent by us?
if (event.senderId != credentials.userId) {
Timber.e("## onSecretSend() : Ignore secret from other user ${event.senderId}")
Timber.e("## GOSSIP onSecretSend() : Ignore secret from other user ${event.senderId}")
return
}
@ -762,7 +762,7 @@ internal class DefaultCryptoService @Inject constructor(
.getOutgoingSecretKeyRequests().firstOrNull { it.requestId == secretContent.requestId }
if (existingRequest == null) {
Timber.i("## onSecretSend() : Ignore secret that was not requested: ${secretContent.requestId}")
Timber.i("## GOSSIP onSecretSend() : Ignore secret that was not requested: ${secretContent.requestId}")
return
}
@ -1111,7 +1111,7 @@ internal class DefaultCryptoService @Inject constructor(
* @param listener listener
*/
override fun addRoomKeysRequestListener(listener: GossipingRequestListener) {
incomingRoomKeyRequestManager.addRoomKeysRequestListener(listener)
incomingGossipingRequestManager.addRoomKeysRequestListener(listener)
}
/**
@ -1120,7 +1120,7 @@ internal class DefaultCryptoService @Inject constructor(
* @param listener listener
*/
override fun removeRoomKeysRequestListener(listener: GossipingRequestListener) {
incomingRoomKeyRequestManager.removeRoomKeysRequestListener(listener)
incomingGossipingRequestManager.removeRoomKeysRequestListener(listener)
}
/**

View file

@ -25,7 +25,9 @@ enum class GossipingRequestState {
NONE,
PENDING,
REJECTED,
ACCEPTING,
ACCEPTED,
FAILED_TO_ACCEPTED,
// USER_REJECTED,
UNABLE_TO_PROCESS,
CANCELLED_BY_REQUESTER,

View file

@ -0,0 +1,57 @@
/*
* Copyright (c) 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
import androidx.work.BackoffPolicy
import androidx.work.Data
import androidx.work.ExistingWorkPolicy
import androidx.work.ListenableWorker
import androidx.work.OneTimeWorkRequest
import im.vector.matrix.android.api.util.Cancelable
import im.vector.matrix.android.internal.di.WorkManagerProvider
import im.vector.matrix.android.internal.session.SessionScope
import im.vector.matrix.android.internal.util.CancelableWork
import im.vector.matrix.android.internal.worker.startChain
import java.util.concurrent.TimeUnit
import javax.inject.Inject
@SessionScope
internal class GossipingWorkManager @Inject constructor(
private val workManagerProvider: WorkManagerProvider
) {
inline fun <reified W : ListenableWorker> createWork(data: Data, startChain: Boolean): OneTimeWorkRequest {
return workManagerProvider.matrixOneTimeWorkRequestBuilder<W>()
.setConstraints(WorkManagerProvider.workConstraints)
.startChain(startChain)
.setInputData(data)
.setBackoffCriteria(BackoffPolicy.LINEAR, 10_000L, TimeUnit.MILLISECONDS)
.build()
}
// Prevent sending queue to stay broken after app restart
// The unique queue id will stay the same as long as this object is instanciated
val queueSuffixApp = System.currentTimeMillis()
fun postWork(workRequest: OneTimeWorkRequest, policy: ExistingWorkPolicy = ExistingWorkPolicy.APPEND): Cancelable {
workManagerProvider.workManager
.beginUniqueWork(this::class.java.name + "_$queueSuffixApp", policy, workRequest)
.enqueue()
return CancelableWork(workManagerProvider.workManager, workRequest.id)
}
}

View file

@ -17,6 +17,7 @@
package im.vector.matrix.android.internal.crypto
import im.vector.matrix.android.api.auth.data.Credentials
import im.vector.matrix.android.api.auth.data.sessionId
import im.vector.matrix.android.api.crypto.MXCryptoConfig
import im.vector.matrix.android.api.session.crypto.crosssigning.SELF_SIGNING_KEY_SSSS_NAME
import im.vector.matrix.android.api.session.crypto.crosssigning.USER_SIGNING_KEY_SSSS_NAME
@ -27,16 +28,19 @@ import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.internal.crypto.model.rest.GossipingDefaultContent
import im.vector.matrix.android.internal.crypto.model.rest.GossipingToDeviceObject
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
import im.vector.matrix.android.internal.di.SessionId
import im.vector.matrix.android.internal.session.SessionScope
import im.vector.matrix.android.internal.worker.WorkerParamsFactory
import timber.log.Timber
import javax.inject.Inject
@SessionScope
internal class IncomingRoomKeyRequestManager @Inject constructor(
internal class IncomingGossipingRequestManager @Inject constructor(
@SessionId private val sessionId: String,
private val credentials: Credentials,
private val cryptoStore: IMXCryptoStore,
private val cryptoConfig: MXCryptoConfig,
private val secretSecretCryptoProvider: ShareSecretCryptoProvider,
private val gossipingWorkManager: GossipingWorkManager,
private val roomDecryptorProvider: RoomDecryptorProvider) {
// list of IncomingRoomKeyRequests/IncomingRoomKeyRequestCancellations
@ -51,6 +55,32 @@ internal class IncomingRoomKeyRequestManager @Inject constructor(
receivedGossipingRequests.addAll(cryptoStore.getPendingIncomingGossipingRequests())
}
// Recently verified devices (map of deviceId and timestamp)
private val recentlyVerifiedDevices = HashMap<String, Long>()
/**
* Called when a session has been verified.
* This information can be used by the manager to decide whether or not to fullfil gossiping requests
*/
fun onVerificationCompleteForDevice(deviceId: String) {
// For now we just keep an in memory cache
synchronized(recentlyVerifiedDevices) {
recentlyVerifiedDevices[deviceId] = System.currentTimeMillis()
}
}
private fun hasBeenVerifiedLessThanFiveMinutesFromNow(deviceId: String): Boolean {
val verifTimestamp: Long?
synchronized(recentlyVerifiedDevices) {
verifTimestamp = recentlyVerifiedDevices[deviceId]
}
if (verifTimestamp == null) return false
val age = System.currentTimeMillis() - verifTimestamp
return age < FIVE_MINUTES_IN_MILLIS
}
/**
* Called when we get an m.room_key_request event
* It must be called on CryptoThread
@ -58,7 +88,7 @@ internal class IncomingRoomKeyRequestManager @Inject constructor(
* @param event the announcement event.
*/
fun onGossipingRequestEvent(event: Event) {
Timber.v("## onGossipingRequestEvent type ${event.type} from user ${event.senderId}")
Timber.v("## GOSSIP onGossipingRequestEvent type ${event.type} from user ${event.senderId}")
val roomKeyShare = event.getClearContent().toModel<GossipingDefaultContent>()
val ageLocalTs = event.unsignedData?.age?.let { System.currentTimeMillis() - it }
when (roomKeyShare?.action) {
@ -67,7 +97,7 @@ internal class IncomingRoomKeyRequestManager @Inject constructor(
IncomingSecretShareRequest.fromEvent(event)?.let {
if (event.senderId == credentials.userId && it.deviceId == credentials.deviceId) {
// ignore, it was sent by me as *
Timber.v("## onGossipingRequestEvent type ${event.type} ignore remote echo")
Timber.v("## GOSSIP onGossipingRequestEvent type ${event.type} ignore remote echo")
} else {
// save in DB
cryptoStore.storeIncomingGossipingRequest(it, ageLocalTs)
@ -78,7 +108,7 @@ internal class IncomingRoomKeyRequestManager @Inject constructor(
IncomingRoomKeyRequest.fromEvent(event)?.let {
if (event.senderId == credentials.userId && it.deviceId == credentials.deviceId) {
// ignore, it was sent by me as *
Timber.v("## onGossipingRequestEvent type ${event.type} ignore remote echo")
Timber.v("## GOSSIP onGossipingRequestEvent type ${event.type} ignore remote echo")
} else {
cryptoStore.storeIncomingGossipingRequest(it, ageLocalTs)
receivedGossipingRequests.add(it)
@ -92,7 +122,7 @@ internal class IncomingRoomKeyRequestManager @Inject constructor(
}
}
else -> {
Timber.e("## onGossipingRequestEvent() : unsupported action ${roomKeyShare?.action}")
Timber.e("## GOSSIP onGossipingRequestEvent() : unsupported action ${roomKeyShare?.action}")
}
}
}
@ -103,8 +133,6 @@ internal class IncomingRoomKeyRequestManager @Inject constructor(
* It must be called on CryptoThread
*/
fun processReceivedGossipingRequests() {
Timber.v("## processReceivedGossipingRequests()")
val roomKeyRequestsToProcess = receivedGossipingRequests.toList()
receivedGossipingRequests.clear()
for (request in roomKeyRequestsToProcess) {
@ -125,7 +153,7 @@ internal class IncomingRoomKeyRequestManager @Inject constructor(
}
receivedRequestCancellations?.forEach { request ->
Timber.v("## processReceivedGossipingRequests() : m.room_key_request cancellation $request")
Timber.v("## GOSSIP processReceivedGossipingRequests() : m.room_key_request cancellation $request")
// we should probably only notify the app of cancellations we told it
// about, but we don't currently have a record of that, so we just pass
// everything through.
@ -154,10 +182,10 @@ internal class IncomingRoomKeyRequestManager @Inject constructor(
val roomId = body!!.roomId
val alg = body.algorithm
Timber.v("## processIncomingRoomKeyRequest from $userId:$deviceId for $roomId / ${body.sessionId} id ${request.requestId}")
Timber.v("## GOSSIP processIncomingRoomKeyRequest from $userId:$deviceId for $roomId / ${body.sessionId} id ${request.requestId}")
if (userId == null || credentials.userId != userId) {
// TODO: determine if we sent this device the keys already: in
Timber.w("## processReceivedGossipingRequests() : Ignoring room key request from other user for now")
Timber.w("## GOSSIP processReceivedGossipingRequests() : Ignoring room key request from other user for now")
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED)
return
}
@ -166,18 +194,18 @@ internal class IncomingRoomKeyRequestManager @Inject constructor(
// the keys for the requested events, and can drop the requests.
val decryptor = roomDecryptorProvider.getRoomDecryptor(roomId, alg)
if (null == decryptor) {
Timber.w("## processReceivedGossipingRequests() : room key request for unknown $alg in room $roomId")
Timber.w("## GOSSIP processReceivedGossipingRequests() : room key request for unknown $alg in room $roomId")
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED)
return
}
if (!decryptor.hasKeysForKeyRequest(request)) {
Timber.w("## processReceivedGossipingRequests() : room key request for unknown session ${body.sessionId!!}")
Timber.w("## GOSSIP processReceivedGossipingRequests() : room key request for unknown session ${body.sessionId!!}")
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED)
return
}
if (credentials.deviceId == deviceId && credentials.userId == userId) {
Timber.v("## processReceivedGossipingRequests() : oneself device - ignored")
Timber.v("## GOSSIP processReceivedGossipingRequests() : oneself device - ignored")
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED)
return
}
@ -192,13 +220,13 @@ internal class IncomingRoomKeyRequestManager @Inject constructor(
val device = cryptoStore.getUserDevice(userId, deviceId!!)
if (device != null) {
if (device.isVerified) {
Timber.v("## processReceivedGossipingRequests() : device is already verified: sharing keys")
Timber.v("## GOSSIP processReceivedGossipingRequests() : device is already verified: sharing keys")
request.share?.run()
return
}
if (device.isBlocked) {
Timber.v("## processReceivedGossipingRequests() : device is blocked -> ignored")
Timber.v("## GOSSIP processReceivedGossipingRequests() : device is blocked -> ignored")
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED)
return
}
@ -219,30 +247,30 @@ internal class IncomingRoomKeyRequestManager @Inject constructor(
private fun processIncomingSecretShareRequest(request: IncomingSecretShareRequest) {
val secretName = request.secretName ?: return Unit.also {
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED)
Timber.v("## processIncomingSecretShareRequest() : Missing secret name")
Timber.v("## GOSSIP processIncomingSecretShareRequest() : Missing secret name")
}
val userId = request.userId
if (userId == null || credentials.userId != userId) {
Timber.e("## processIncomingSecretShareRequest() : Ignoring secret share request from other users")
Timber.e("## GOSSIP processIncomingSecretShareRequest() : Ignoring secret share request from other users")
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED)
return
}
val deviceId = request.deviceId
?: return Unit.also {
Timber.e("## processIncomingSecretShareRequest() : Malformed request, no ")
Timber.e("## GOSSIP processIncomingSecretShareRequest() : Malformed request, no ")
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED)
}
val device = cryptoStore.getUserDevice(userId, deviceId)
?: return Unit.also {
Timber.e("## processIncomingSecretShareRequest() : Received secret share request from unknown device ${request.deviceId}")
Timber.e("## GOSSIP processIncomingSecretShareRequest() : Received secret share request from unknown device ${request.deviceId}")
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED)
}
if (!device.isVerified || device.isBlocked) {
Timber.v("## processIncomingSecretShareRequest() : Ignoring secret share request from untrusted/blocked session $device")
Timber.v("## GOSSIP processIncomingSecretShareRequest() : Ignoring secret share request from untrusted/blocked session $device")
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED)
return
}
@ -255,11 +283,20 @@ internal class IncomingRoomKeyRequestManager @Inject constructor(
USER_SIGNING_KEY_SSSS_NAME -> cryptoStore.getCrossSigningPrivateKeys()?.user
else -> null
}?.let { secretValue ->
// TODO check if locally trusted and not outdated
Timber.i("## processIncomingSecretShareRequest() : Sharing secret $secretName with $device locally trusted")
if (isDeviceLocallyVerified == true) {
secretSecretCryptoProvider.shareSecretWithDevice(request, secretValue)
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.ACCEPTED)
Timber.i("## GOSSIP processIncomingSecretShareRequest() : Sharing secret $secretName with $device locally trusted")
if (isDeviceLocallyVerified == true && hasBeenVerifiedLessThanFiveMinutesFromNow(deviceId)) {
val params = SendGossipWorker.Params(
sessionId = sessionId,
secretValue = secretValue,
request = request
)
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.ACCEPTING)
val workRequest = gossipingWorkManager.createWork<SendGossipWorker>(WorkerParamsFactory.toData(params), true)
gossipingWorkManager.postWork(workRequest)
} else {
Timber.v("## GOSSIP processIncomingSecretShareRequest() : Can't share secret $secretName with $device, verification too old")
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED)
}
return
}
@ -269,7 +306,16 @@ internal class IncomingRoomKeyRequestManager @Inject constructor(
}
request.share = { secretValue ->
secretSecretCryptoProvider.shareSecretWithDevice(request, secretValue)
val params = SendGossipWorker.Params(
sessionId = userId,
secretValue = secretValue,
request = request
)
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.ACCEPTING)
val workRequest = gossipingWorkManager.createWork<SendGossipWorker>(WorkerParamsFactory.toData(params), true)
gossipingWorkManager.postWork(workRequest)
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.ACCEPTED)
}
@ -304,7 +350,7 @@ internal class IncomingRoomKeyRequestManager @Inject constructor(
return
}
} catch (e: Exception) {
Timber.e(e, "## onRoomKeyRequest() failed")
Timber.e(e, "## GOSSIP onRoomKeyRequest() failed")
}
}
}
@ -323,7 +369,7 @@ internal class IncomingRoomKeyRequestManager @Inject constructor(
try {
listener.onRoomKeyRequestCancellation(request)
} catch (e: Exception) {
Timber.e(e, "## onRoomKeyRequestCancellation() failed")
Timber.e(e, "## GOSSIP onRoomKeyRequestCancellation() failed")
}
}
}
@ -340,4 +386,8 @@ internal class IncomingRoomKeyRequestManager @Inject constructor(
gossipingRequestListeners.remove(listener)
}
}
companion object {
private const val FIVE_MINUTES_IN_MILLIS = 5 * 60 * 1000
}
}

View file

@ -17,27 +17,17 @@
package im.vector.matrix.android.internal.crypto
import androidx.work.BackoffPolicy
import androidx.work.Data
import androidx.work.ExistingWorkPolicy
import androidx.work.ListenableWorker
import androidx.work.OneTimeWorkRequest
import im.vector.matrix.android.api.session.events.model.LocalEcho
import im.vector.matrix.android.api.util.Cancelable
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.di.SessionId
import im.vector.matrix.android.internal.di.WorkManagerProvider
import im.vector.matrix.android.internal.session.SessionScope
import im.vector.matrix.android.internal.util.CancelableWork
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
import im.vector.matrix.android.internal.worker.WorkerParamsFactory
import im.vector.matrix.android.internal.worker.startChain
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import timber.log.Timber
import java.util.concurrent.TimeUnit
import javax.inject.Inject
@SessionScope
@ -46,7 +36,7 @@ internal class OutgoingGossipingRequestManager @Inject constructor(
private val cryptoStore: IMXCryptoStore,
private val coroutineDispatchers: MatrixCoroutineDispatchers,
private val cryptoCoroutineScope: CoroutineScope,
private val workManagerProvider: WorkManagerProvider) {
private val gossipingWorkManager: GossipingWorkManager) {
/**
* Send off a room key request, if we haven't already done so.
@ -65,7 +55,7 @@ internal class OutgoingGossipingRequestManager @Inject constructor(
cryptoStore.getOrAddOutgoingRoomKeyRequest(requestBody, recipients)?.let {
// Don't resend if it's already done, you need to cancel first (reRequest)
if (it.state == OutgoingGossipingRequestState.SENDING || it.state == OutgoingGossipingRequestState.SENT) {
Timber.v("## sendOutgoingRoomKeyRequest() : we already request for that session: $it")
Timber.v("## GOSSIP sendOutgoingRoomKeyRequest() : we already request for that session: $it")
return@launch
}
@ -82,7 +72,7 @@ internal class OutgoingGossipingRequestManager @Inject constructor(
cryptoStore.getOrAddOutgoingSecretShareRequest(secretName, recipients)?.let {
// TODO check if there is already one that is being sent?
if (it.state == OutgoingGossipingRequestState.SENDING || it.state == OutgoingGossipingRequestState.SENT) {
Timber.v("## sendOutgoingRoomKeyRequest() : we already request for that session: $it")
Timber.v("## GOSSIP sendSecretShareRequest() : we already request for that session: $it")
return@launch
}
@ -123,7 +113,7 @@ internal class OutgoingGossipingRequestManager @Inject constructor(
val req = cryptoStore.getOutgoingRoomKeyRequest(requestBody)
?: // no request was made for this key
return Unit.also {
Timber.v("## cancelRoomKeyRequest() Unknown request")
Timber.v("## GOSSIP cancelRoomKeyRequest() Unknown request")
}
sendOutgoingRoomKeyRequestCancellation(req, andResend)
@ -135,7 +125,7 @@ internal class OutgoingGossipingRequestManager @Inject constructor(
* @param request the request
*/
private fun sendOutgoingGossipingRequest(request: OutgoingGossipingRequest) {
Timber.v("## sendOutgoingRoomKeyRequest() : Requesting keys $request")
Timber.v("## GOSSIP sendOutgoingRoomKeyRequest() : Requesting keys $request")
val params = SendGossipRequestWorker.Params(
sessionId = sessionId,
@ -143,8 +133,8 @@ internal class OutgoingGossipingRequestManager @Inject constructor(
secretShareRequest = request as? OutgoingSecretRequest
)
cryptoStore.updateOutgoingGossipingRequestState(request.requestId, OutgoingGossipingRequestState.SENDING)
val workRequest = createWork<SendGossipRequestWorker>(WorkerParamsFactory.toData(params), true)
postWork(workRequest)
val workRequest = gossipingWorkManager.createWork<SendGossipRequestWorker>(WorkerParamsFactory.toData(params), true)
gossipingWorkManager.postWork(workRequest)
}
/**
@ -157,33 +147,16 @@ internal class OutgoingGossipingRequestManager @Inject constructor(
val params = CancelGossipRequestWorker.Params.fromRequest(sessionId, request)
cryptoStore.updateOutgoingGossipingRequestState(request.requestId, OutgoingGossipingRequestState.CANCELLING)
val workRequest = createWork<CancelGossipRequestWorker>(WorkerParamsFactory.toData(params), true)
postWork(workRequest)
val workRequest = gossipingWorkManager.createWork<CancelGossipRequestWorker>(WorkerParamsFactory.toData(params), true)
gossipingWorkManager.postWork(workRequest)
if (resend) {
val reSendParams = SendGossipRequestWorker.Params(
sessionId = sessionId,
keyShareRequest = request.copy(requestId = LocalEcho.createLocalEchoId())
)
val reSendWorkRequest = createWork<SendGossipRequestWorker>(WorkerParamsFactory.toData(reSendParams), true)
postWork(reSendWorkRequest)
val reSendWorkRequest = gossipingWorkManager.createWork<SendGossipRequestWorker>(WorkerParamsFactory.toData(reSendParams), true)
gossipingWorkManager.postWork(reSendWorkRequest)
}
}
private inline fun <reified W : ListenableWorker> createWork(data: Data, startChain: Boolean): OneTimeWorkRequest {
return workManagerProvider.matrixOneTimeWorkRequestBuilder<W>()
.setConstraints(WorkManagerProvider.workConstraints)
.startChain(startChain)
.setInputData(data)
.setBackoffCriteria(BackoffPolicy.LINEAR, 10_000L, TimeUnit.MILLISECONDS)
.build()
}
private fun postWork(workRequest: OneTimeWorkRequest, policy: ExistingWorkPolicy = ExistingWorkPolicy.APPEND): Cancelable {
workManagerProvider.workManager
.beginUniqueWork(this::class.java.name, policy, workRequest)
.enqueue()
return CancelableWork(workManagerProvider.workManager, workRequest.id)
}
}

View file

@ -0,0 +1,141 @@
/*
* Copyright (c) 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
import android.content.Context
import androidx.work.CoroutineWorker
import androidx.work.Data
import androidx.work.WorkerParameters
import com.squareup.moshi.JsonClass
import im.vector.matrix.android.api.auth.data.Credentials
import im.vector.matrix.android.api.failure.shouldBeRetried
import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.events.model.LocalEcho
import im.vector.matrix.android.api.session.events.model.toContent
import im.vector.matrix.android.internal.crypto.actions.EnsureOlmSessionsForDevicesAction
import im.vector.matrix.android.internal.crypto.actions.MessageEncrypter
import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap
import im.vector.matrix.android.internal.crypto.model.event.SecretSendEventContent
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
import im.vector.matrix.android.internal.crypto.tasks.SendToDeviceTask
import im.vector.matrix.android.internal.worker.WorkerParamsFactory
import im.vector.matrix.android.internal.worker.getSessionComponent
import org.greenrobot.eventbus.EventBus
import timber.log.Timber
import javax.inject.Inject
internal class SendGossipWorker(context: Context,
params: WorkerParameters)
: CoroutineWorker(context, params) {
@JsonClass(generateAdapter = true)
internal data class Params(
val sessionId: String,
val secretValue: String,
val request: IncomingSecretShareRequest
)
@Inject lateinit var sendToDeviceTask: SendToDeviceTask
@Inject lateinit var cryptoStore: IMXCryptoStore
@Inject lateinit var eventBus: EventBus
@Inject lateinit var credentials: Credentials
@Inject lateinit var messageEncrypter: MessageEncrypter
@Inject lateinit var ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction
override suspend fun doWork(): Result {
val errorOutputData = Data.Builder().putBoolean("failed", true).build()
val params = WorkerParamsFactory.fromData<Params>(inputData)
?: return Result.success(errorOutputData)
val sessionComponent = getSessionComponent(params.sessionId)
?: return Result.success(errorOutputData).also {
// TODO, can this happen? should I update local echo?
Timber.e("Unknown Session, cannot send message, sessionId: ${params.sessionId}")
}
sessionComponent.inject(this)
val localId = LocalEcho.createLocalEchoId()
val eventType: String = EventType.SEND_SECRET
val toDeviceContent = SecretSendEventContent(
requestId = params.request.requestId ?: "",
secretValue = params.secretValue
)
val requestingUserId = params.request.userId ?: ""
val requestingDeviceId = params.request.deviceId ?: ""
val deviceInfo = cryptoStore.getUserDevice(requestingUserId, requestingDeviceId)
?: return Result.success(errorOutputData).also {
cryptoStore.updateGossipingRequestState(params.request, GossipingRequestState.FAILED_TO_ACCEPTED)
Timber.e("Unknown deviceInfo, cannot send message, sessionId: ${params.request.deviceId}")
}
val sendToDeviceMap = MXUsersDevicesMap<Any>()
val devicesByUser = mapOf(requestingUserId to listOf(deviceInfo))
val usersDeviceMap = ensureOlmSessionsForDevicesAction.handle(devicesByUser)
val olmSessionResult = usersDeviceMap.getObject(requestingUserId, requestingDeviceId)
if (olmSessionResult?.sessionId == null) {
// no session with this device, probably because there
// were no one-time keys.
return Result.success(errorOutputData).also {
cryptoStore.updateGossipingRequestState(params.request, GossipingRequestState.FAILED_TO_ACCEPTED)
Timber.e("no session with this device, probably because there were no one-time keys.")
}
}
val payloadJson = mapOf(
"type" to EventType.SEND_SECRET,
"content" to toDeviceContent.toContent()
)
try {
val encodedPayload = messageEncrypter.encryptMessage(payloadJson, listOf(deviceInfo))
sendToDeviceMap.setObject(requestingUserId, requestingDeviceId, encodedPayload)
} catch (failure: Throwable) {
Timber.e("## Fail to encrypt gossip + ${failure.localizedMessage}")
}
cryptoStore.saveGossipingEvent(Event(
type = eventType,
content = toDeviceContent.toContent(),
senderId = credentials.userId
).also {
it.ageLocalTs = System.currentTimeMillis()
})
try {
sendToDeviceTask.execute(
SendToDeviceTask.Params(
eventType = EventType.ENCRYPTED,
contentMap = sendToDeviceMap,
transactionId = localId
)
)
cryptoStore.updateGossipingRequestState(params.request, GossipingRequestState.ACCEPTED)
return Result.success()
} catch (exception: Throwable) {
return if (exception.shouldBeRetried()) {
Result.retry()
} else {
cryptoStore.updateGossipingRequestState(params.request, GossipingRequestState.FAILED_TO_ACCEPTED)
Result.success(errorOutputData)
}
}
}
}

View file

@ -1,74 +0,0 @@
/*
* Copyright (c) 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
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.internal.crypto.actions.MessageEncrypter
import im.vector.matrix.android.internal.crypto.algorithms.olm.MXOlmDecryptionFactory
import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap
import im.vector.matrix.android.internal.crypto.model.event.SecretSendEventContent
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
import im.vector.matrix.android.internal.crypto.tasks.SendToDeviceTask
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import timber.log.Timber
import javax.inject.Inject
internal class ShareSecretCryptoProvider @Inject constructor(
val messageEncrypter: MessageEncrypter,
val sendToDeviceTask: SendToDeviceTask,
val deviceListManager: DeviceListManager,
private val olmDecryptionFactory: MXOlmDecryptionFactory,
val cryptoCoroutineScope: CoroutineScope,
val cryptoStore: IMXCryptoStore,
val coroutineDispatchers: MatrixCoroutineDispatchers
) {
fun shareSecretWithDevice(request: IncomingSecretShareRequest, secretValue: String) {
val userId = request.userId ?: return
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
runCatching { deviceListManager.downloadKeys(listOf(userId), false) }
.mapCatching {
val deviceId = request.deviceId
val deviceInfo = cryptoStore.getUserDevice(userId, deviceId ?: "") ?: throw RuntimeException()
Timber.i("## shareSecretWithDevice() : sharing secret ${request.secretName} with device $userId:$deviceId")
val payloadJson = mutableMapOf<String, Any>("type" to EventType.SEND_SECRET)
payloadJson["content"] = SecretSendEventContent(
requestId = request.requestId ?: "",
secretValue = secretValue
)
val encodedPayload = messageEncrypter.encryptMessage(payloadJson, listOf(deviceInfo))
val sendToDeviceMap = MXUsersDevicesMap<Any>()
sendToDeviceMap.setObject(userId, deviceId, encodedPayload)
Timber.i("## shareSecretWithDevice() : sending to $userId:$deviceId")
val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, sendToDeviceMap)
sendToDeviceTask.execute(sendToDeviceParams)
}
}
}
fun decryptEvent(event: Event): MXEventDecryptionResult {
return runBlocking(coroutineDispatchers.crypto) {
olmDecryptionFactory.create().decryptEvent(event, ShareSecretCryptoProvider::class.java.name ?: "")
}
}
}

View file

@ -149,7 +149,7 @@ internal class MXMegolmDecryption(private val userId: String,
val encryptedEventContent = event.content.toModel<EncryptedEventContent>()
val senderDevice = encryptedEventContent?.deviceId ?: return
val recipients = if (event.senderId != userId) {
val recipients = if (event.senderId == userId) {
mapOf(
userId to listOf("*")
)

View file

@ -25,7 +25,6 @@ import im.vector.matrix.android.api.util.Optional
import im.vector.matrix.android.internal.crypto.DeviceListManager
import im.vector.matrix.android.internal.crypto.MXOlmDevice
import im.vector.matrix.android.internal.crypto.MyDeviceInfoHolder
import im.vector.matrix.android.internal.crypto.OutgoingGossipingRequestManager
import im.vector.matrix.android.internal.crypto.model.CryptoCrossSigningKey
import im.vector.matrix.android.internal.crypto.model.KeyUsage
import im.vector.matrix.android.internal.crypto.model.rest.UploadSignatureQueryBuilder
@ -62,7 +61,6 @@ internal class DefaultCrossSigningService @Inject constructor(
private val taskExecutor: TaskExecutor,
private val coroutineDispatchers: MatrixCoroutineDispatchers,
private val cryptoCoroutineScope: CoroutineScope,
private val outgoingGossipingRequestManager: OutgoingGossipingRequestManager,
private val eventBus: EventBus) : CrossSigningService, DeviceListManager.UserDevicesUpdateListener {
private var olmUtility: OlmUtility? = null
@ -599,6 +597,7 @@ internal class DefaultCrossSigningService @Inject constructor(
override fun canCrossSign(): Boolean {
return checkSelfTrust().isVerified() && cryptoStore.getCrossSigningPrivateKeys()?.selfSigned != null
&& cryptoStore.getCrossSigningPrivateKeys()?.user != null
}
override fun trustUser(otherUserId: String, callback: MatrixCallback<Unit>) {
@ -770,7 +769,12 @@ internal class DefaultCrossSigningService @Inject constructor(
Timber.v("## CrossSigning - update trust for $otherUserId , verified=${it.isVerified()}")
setUserKeysAsTrusted(otherUserId, it.isVerified())
}
}
}
// now check device trust
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
userIds.forEach { otherUserId ->
// TODO if my keys have changes, i should recheck all devices of all users?
val devices = cryptoStore.getUserDeviceList(otherUserId)
devices?.forEach { device ->
@ -791,24 +795,22 @@ internal class DefaultCrossSigningService @Inject constructor(
}
private fun setUserKeysAsTrusted(otherUserId: String, trusted: Boolean) {
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
val currentTrust = cryptoStore.getCrossSigningInfo(otherUserId)?.isTrusted()
cryptoStore.setUserKeysAsTrusted(otherUserId, trusted)
// If it's me, recheck trust of all users and devices?
val users = ArrayList<String>()
if (otherUserId == userId && currentTrust != trusted) {
val currentTrust = cryptoStore.getCrossSigningInfo(otherUserId)?.isTrusted()
cryptoStore.setUserKeysAsTrusted(otherUserId, trusted)
// If it's me, recheck trust of all users and devices?
val users = ArrayList<String>()
if (otherUserId == userId && currentTrust != trusted) {
// reRequestAllPendingRoomKeyRequest()
cryptoStore.updateUsersTrust {
users.add(it)
checkUserTrust(it).isVerified()
}
cryptoStore.updateUsersTrust {
users.add(it)
checkUserTrust(it).isVerified()
}
users.forEach {
cryptoStore.getUserDeviceList(it)?.forEach { device ->
val updatedTrust = checkDeviceTrust(it, device.deviceId, device.trustLevel?.isLocallyVerified() ?: false)
Timber.v("## CrossSigning - update trust for device ${device.deviceId} of user $otherUserId , verified=$updatedTrust")
cryptoStore.setDeviceTrust(it, device.deviceId, updatedTrust.isCrossSignedVerified(), updatedTrust.isLocallyVerified())
}
users.forEach {
cryptoStore.getUserDeviceList(it)?.forEach { device ->
val updatedTrust = checkDeviceTrust(it, device.deviceId, device.trustLevel?.isLocallyVerified() ?: false)
Timber.v("## CrossSigning - update trust for device ${device.deviceId} of user $otherUserId , verified=$updatedTrust")
cryptoStore.setDeviceTrust(it, device.deviceId, updatedTrust.isCrossSignedVerified(), updatedTrust.isLocallyVerified())
}
}
}

View file

@ -23,6 +23,7 @@ import im.vector.matrix.android.api.session.crypto.verification.IncomingSasVerif
import im.vector.matrix.android.api.session.crypto.verification.SasMode
import im.vector.matrix.android.api.session.crypto.verification.VerificationTxState
import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.internal.crypto.IncomingGossipingRequestManager
import im.vector.matrix.android.internal.crypto.OutgoingGossipingRequestManager
import im.vector.matrix.android.internal.crypto.actions.SetDeviceVerificationAction
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
@ -35,6 +36,7 @@ internal class DefaultIncomingSASDefaultVerificationTransaction(
private val cryptoStore: IMXCryptoStore,
crossSigningService: CrossSigningService,
outgoingGossipingRequestManager: OutgoingGossipingRequestManager,
incomingGossipingRequestManager: IncomingGossipingRequestManager,
deviceFingerprint: String,
transactionId: String,
otherUserID: String,
@ -46,6 +48,7 @@ internal class DefaultIncomingSASDefaultVerificationTransaction(
cryptoStore,
crossSigningService,
outgoingGossipingRequestManager,
incomingGossipingRequestManager,
deviceFingerprint,
transactionId,
otherUserID,

View file

@ -20,6 +20,7 @@ import im.vector.matrix.android.api.session.crypto.verification.CancelCode
import im.vector.matrix.android.api.session.crypto.verification.OutgoingSasVerificationTransaction
import im.vector.matrix.android.api.session.crypto.verification.VerificationTxState
import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.internal.crypto.IncomingGossipingRequestManager
import im.vector.matrix.android.internal.crypto.OutgoingGossipingRequestManager
import im.vector.matrix.android.internal.crypto.actions.SetDeviceVerificationAction
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
@ -32,6 +33,7 @@ internal class DefaultOutgoingSASDefaultVerificationTransaction(
cryptoStore: IMXCryptoStore,
crossSigningService: CrossSigningService,
outgoingGossipingRequestManager: OutgoingGossipingRequestManager,
incomingGossipingRequestManager: IncomingGossipingRequestManager,
deviceFingerprint: String,
transactionId: String,
otherUserId: String,
@ -43,6 +45,7 @@ internal class DefaultOutgoingSASDefaultVerificationTransaction(
cryptoStore,
crossSigningService,
outgoingGossipingRequestManager,
incomingGossipingRequestManager,
deviceFingerprint,
transactionId,
otherUserId,

View file

@ -50,6 +50,7 @@ import im.vector.matrix.android.api.session.room.model.message.MessageVerificati
import im.vector.matrix.android.api.session.room.model.message.MessageVerificationStartContent
import im.vector.matrix.android.api.session.room.model.message.ValidVerificationDone
import im.vector.matrix.android.internal.crypto.DeviceListManager
import im.vector.matrix.android.internal.crypto.IncomingGossipingRequestManager
import im.vector.matrix.android.internal.crypto.MyDeviceInfoHolder
import im.vector.matrix.android.internal.crypto.OutgoingGossipingRequestManager
import im.vector.matrix.android.internal.crypto.actions.SetDeviceVerificationAction
@ -90,6 +91,7 @@ internal class DefaultVerificationService @Inject constructor(
@DeviceId private val deviceId: String?,
private val cryptoStore: IMXCryptoStore,
private val outgoingGossipingRequestManager: OutgoingGossipingRequestManager,
private val incomingGossipingRequestManager: IncomingGossipingRequestManager,
private val myDeviceInfoHolder: Lazy<MyDeviceInfoHolder>,
private val deviceListManager: DeviceListManager,
private val setDeviceVerificationAction: SetDeviceVerificationAction,
@ -454,7 +456,7 @@ internal class DefaultVerificationService @Inject constructor(
private suspend fun handleStart(otherUserId: String?,
startReq: ValidVerificationInfoStart,
txConfigure: (DefaultVerificationTransaction) -> Unit): CancelCode? {
Timber.d("## SAS onStartRequestReceived ${startReq.transactionId}")
Timber.d("## SAS onStartRequestReceived $startReq")
if (otherUserId?.let { checkKeysAreDownloaded(it, startReq.fromDevice) } != null) {
val tid = startReq.transactionId
var existing = getExistingTransaction(otherUserId, tid)
@ -466,15 +468,17 @@ internal class DefaultVerificationService @Inject constructor(
// smallest is used, and the other m.key.verification.start event is ignored.
// In the case of a single user verifying two of their devices, the device ID is
// compared instead .
if (existing != null && !existing.isIncoming) {
if (existing is DefaultOutgoingSASDefaultVerificationTransaction) {
val readyRequest = getExistingVerificationRequest(otherUserId, tid)
if (readyRequest?.isReady == true) {
if (isOtherPrioritary(otherUserId, existing.otherDeviceId ?: "")) {
Timber.d("## SAS concurrent start isOtherPrioritary, clear")
// The other is prioritary!
// I should replace my outgoing with an incoming
removeTransaction(otherUserId, tid)
existing = null
} else {
Timber.d("## SAS concurrent start i am prioritary, ignore")
// i am prioritary, ignore this start event!
return null
}
@ -530,6 +534,7 @@ internal class DefaultVerificationService @Inject constructor(
cryptoStore,
crossSigningService,
outgoingGossipingRequestManager,
incomingGossipingRequestManager,
myDeviceInfoHolder.get().myDevice.fingerprint()!!,
startReq.transactionId,
otherUserId,
@ -544,7 +549,7 @@ internal class DefaultVerificationService @Inject constructor(
existing.onStartReceived(startReq)
return null
} else {
Timber.w("## SAS onStartRequestReceived - unexpected message ${startReq.transactionId}")
Timber.w("## SAS onStartRequestReceived - unexpected message ${startReq.transactionId} / $existing")
return CancelCode.UnexpectedMessage
}
}
@ -754,6 +759,7 @@ internal class DefaultVerificationService @Inject constructor(
private suspend fun onReadyReceived(event: Event) {
val readyReq = event.getClearContent().toModel<KeyVerificationReady>()?.asValidObject()
Timber.v("## SAS onReadyReceived $readyReq")
if (readyReq == null || event.senderId == null) {
// ignore
@ -834,17 +840,18 @@ internal class DefaultVerificationService @Inject constructor(
if (readyReq.methods.contains(VERIFICATION_METHOD_RECIPROCATE)) {
// Create the pending transaction
val tx = DefaultQrCodeVerificationTransaction(
setDeviceVerificationAction,
readyReq.transactionId,
senderId,
readyReq.fromDevice,
crossSigningService,
outgoingGossipingRequestManager,
cryptoStore,
qrCodeData,
userId,
deviceId ?: "",
false)
setDeviceVerificationAction = setDeviceVerificationAction,
transactionId = readyReq.transactionId,
otherUserId = senderId,
otherDeviceId = readyReq.fromDevice,
crossSigningService = crossSigningService,
outgoingGossipingRequestManager = outgoingGossipingRequestManager,
incomingGossipingRequestManager = incomingGossipingRequestManager,
cryptoStore = cryptoStore,
qrCodeData = qrCodeData,
userId = userId,
deviceId = deviceId ?: "",
isIncoming = false)
tx.transport = transportCreator.invoke(tx)
@ -1001,13 +1008,11 @@ internal class DefaultVerificationService @Inject constructor(
}
private fun addTransaction(tx: DefaultVerificationTransaction) {
tx.otherUserId.let { otherUserId ->
synchronized(txMap) {
val txInnerMap = txMap.getOrPut(otherUserId) { HashMap() }
txInnerMap[tx.transactionId] = tx
dispatchTxAdded(tx)
tx.addListener(this)
}
synchronized(txMap) {
val txInnerMap = txMap.getOrPut(tx.otherUserId) { HashMap() }
txInnerMap[tx.transactionId] = tx
dispatchTxAdded(tx)
tx.addListener(this)
}
}
@ -1022,6 +1027,7 @@ internal class DefaultVerificationService @Inject constructor(
cryptoStore,
crossSigningService,
outgoingGossipingRequestManager,
incomingGossipingRequestManager,
myDeviceInfoHolder.get().myDevice.fingerprint()!!,
txID,
otherUserId,
@ -1096,6 +1102,18 @@ internal class DefaultVerificationService @Inject constructor(
return verificationRequest
}
override fun cancelVerificationRequest(request: PendingVerificationRequest) {
if (request.roomId != null) {
val transport = verificationTransportRoomMessageFactory.createTransport(request.roomId, null)
transport.cancelTransaction(request.transactionId ?: "", request.otherUserId, null, CancelCode.User)
} else {
val transport = verificationTransportToDeviceFactory.createTransport(null)
request.targetDevices?.forEach { deviceId ->
transport.cancelTransaction(request.transactionId ?: "", request.otherUserId, deviceId, CancelCode.User)
}
}
}
override fun requestKeyVerification(methods: List<VerificationMethod>, otherUserId: String, otherDevices: List<String>?): PendingVerificationRequest {
// TODO refactor this with the DM one
Timber.i("## Requesting verification to user: $otherUserId with device list $otherDevices")
@ -1198,6 +1216,7 @@ internal class DefaultVerificationService @Inject constructor(
cryptoStore,
crossSigningService,
outgoingGossipingRequestManager,
incomingGossipingRequestManager,
myDeviceInfoHolder.get().myDevice.fingerprint()!!,
transactionId,
otherUserId,
@ -1329,17 +1348,18 @@ internal class DefaultVerificationService @Inject constructor(
if (VERIFICATION_METHOD_RECIPROCATE in result) {
// Create the pending transaction
val tx = DefaultQrCodeVerificationTransaction(
setDeviceVerificationAction,
transactionId,
otherUserId,
otherDeviceId,
crossSigningService,
outgoingGossipingRequestManager,
cryptoStore,
qrCodeData,
userId,
deviceId ?: "",
false)
setDeviceVerificationAction = setDeviceVerificationAction,
transactionId = transactionId,
otherUserId = otherUserId,
otherDeviceId = otherDeviceId,
crossSigningService = crossSigningService,
outgoingGossipingRequestManager = outgoingGossipingRequestManager,
incomingGossipingRequestManager = incomingGossipingRequestManager,
cryptoStore = cryptoStore,
qrCodeData = qrCodeData,
userId = userId,
deviceId = deviceId ?: "",
isIncoming = false)
tx.transport = transportCreator.invoke(tx)

View file

@ -21,6 +21,7 @@ import im.vector.matrix.android.api.session.crypto.crosssigning.SELF_SIGNING_KEY
import im.vector.matrix.android.api.session.crypto.crosssigning.USER_SIGNING_KEY_SSSS_NAME
import im.vector.matrix.android.api.session.crypto.verification.VerificationTransaction
import im.vector.matrix.android.api.session.crypto.verification.VerificationTxState
import im.vector.matrix.android.internal.crypto.IncomingGossipingRequestManager
import im.vector.matrix.android.internal.crypto.OutgoingGossipingRequestManager
import im.vector.matrix.android.internal.crypto.actions.SetDeviceVerificationAction
import im.vector.matrix.android.internal.crypto.crosssigning.DeviceTrustLevel
@ -33,6 +34,7 @@ internal abstract class DefaultVerificationTransaction(
private val setDeviceVerificationAction: SetDeviceVerificationAction,
private val crossSigningService: CrossSigningService,
private val outgoingGossipingRequestManager: OutgoingGossipingRequestManager,
private val incomingGossipingRequestManager: IncomingGossipingRequestManager,
private val userId: String,
override val transactionId: String,
override val otherUserId: String,
@ -86,6 +88,8 @@ internal abstract class DefaultVerificationTransaction(
}
if (otherUserId == userId) {
incomingGossipingRequestManager.onVerificationCompleteForDevice(otherDeviceId!!)
// If me it's reasonable to sign and upload the device signature
// Notice that i might not have the private keys, so may not be able to do it
crossSigningService.trustDevice(otherDeviceId!!, object : MatrixCallback<Unit> {
@ -96,7 +100,7 @@ internal abstract class DefaultVerificationTransaction(
}
transport.done(transactionId) {
if (otherUserId == userId) {
if (otherUserId == userId && !crossSigningService.canCrossSign()) {
outgoingGossipingRequestManager.sendSecretShareRequest(SELF_SIGNING_KEY_SSSS_NAME, mapOf(userId to listOf(otherDeviceId ?: "*")))
outgoingGossipingRequestManager.sendSecretShareRequest(USER_SIGNING_KEY_SSSS_NAME, mapOf(userId to listOf(otherDeviceId ?: "*")))
}

View file

@ -24,6 +24,7 @@ import im.vector.matrix.android.api.session.crypto.verification.SasMode
import im.vector.matrix.android.api.session.crypto.verification.SasVerificationTransaction
import im.vector.matrix.android.api.session.crypto.verification.VerificationTxState
import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.internal.crypto.IncomingGossipingRequestManager
import im.vector.matrix.android.internal.crypto.OutgoingGossipingRequestManager
import im.vector.matrix.android.internal.crypto.actions.SetDeviceVerificationAction
import im.vector.matrix.android.internal.crypto.model.MXKey
@ -44,6 +45,7 @@ internal abstract class SASDefaultVerificationTransaction(
private val cryptoStore: IMXCryptoStore,
crossSigningService: CrossSigningService,
outgoingGossipingRequestManager: OutgoingGossipingRequestManager,
incomingGossipingRequestManager: IncomingGossipingRequestManager,
private val deviceFingerprint: String,
transactionId: String,
otherUserId: String,
@ -53,6 +55,7 @@ internal abstract class SASDefaultVerificationTransaction(
setDeviceVerificationAction,
crossSigningService,
outgoingGossipingRequestManager,
incomingGossipingRequestManager,
userId,
transactionId,
otherUserId,

View file

@ -21,6 +21,7 @@ import im.vector.matrix.android.api.session.crypto.verification.CancelCode
import im.vector.matrix.android.api.session.crypto.verification.QrCodeVerificationTransaction
import im.vector.matrix.android.api.session.crypto.verification.VerificationTxState
import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.internal.crypto.IncomingGossipingRequestManager
import im.vector.matrix.android.internal.crypto.OutgoingGossipingRequestManager
import im.vector.matrix.android.internal.crypto.actions.SetDeviceVerificationAction
import im.vector.matrix.android.internal.crypto.crosssigning.fromBase64
@ -38,6 +39,7 @@ internal class DefaultQrCodeVerificationTransaction(
override var otherDeviceId: String?,
private val crossSigningService: CrossSigningService,
outgoingGossipingRequestManager: OutgoingGossipingRequestManager,
incomingGossipingRequestManager: IncomingGossipingRequestManager,
private val cryptoStore: IMXCryptoStore,
// Not null only if other user is able to scan QR code
private val qrCodeData: QrCodeData?,
@ -48,6 +50,7 @@ internal class DefaultQrCodeVerificationTransaction(
setDeviceVerificationAction,
crossSigningService,
outgoingGossipingRequestManager,
incomingGossipingRequestManager,
userId,
transactionId,
otherUserId,

View file

@ -23,6 +23,7 @@ import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.internal.crypto.CancelGossipRequestWorker
import im.vector.matrix.android.internal.crypto.CryptoModule
import im.vector.matrix.android.internal.crypto.SendGossipRequestWorker
import im.vector.matrix.android.internal.crypto.SendGossipWorker
import im.vector.matrix.android.internal.crypto.verification.SendVerificationMessageWorker
import im.vector.matrix.android.internal.di.MatrixComponent
import im.vector.matrix.android.internal.di.SessionAssistedInjectModule
@ -109,8 +110,11 @@ internal interface SessionComponent {
fun inject(worker: SendVerificationMessageWorker)
fun inject(worker: SendGossipRequestWorker)
fun inject(worker: CancelGossipRequestWorker)
fun inject(worker: SendGossipWorker)
@Component.Factory
interface Factory {
fun create(

View file

@ -128,7 +128,7 @@ internal class TimelineEventDecryptor @Inject constructor(
}
}
} catch (t: Throwable) {
Timber.e(t, "Failed to decrypt event $eventId")
Timber.e("Failed to decrypt event $eventId, ${t.localizedMessage}")
} finally {
synchronized(existingRequests) {
existingRequests.remove(request)

View file

@ -323,7 +323,7 @@ dependencies {
implementation 'com.nulab-inc:zxcvbn:1.2.7'
//Alerter
implementation 'com.tapadoo.android:alerter:4.0.3'
implementation 'com.tapadoo.android:alerter:5.1.2'
implementation 'com.otaliastudios:autocomplete:1.1.0'

View file

@ -48,6 +48,7 @@ import im.vector.riotx.features.lifecycle.VectorActivityLifecycleCallbacks
import im.vector.riotx.features.notifications.NotificationDrawerManager
import im.vector.riotx.features.notifications.NotificationUtils
import im.vector.riotx.features.notifications.PushRuleTriggerListener
import im.vector.riotx.features.popup.PopupAlertManager
import im.vector.riotx.features.rageshake.VectorUncaughtExceptionHandler
import im.vector.riotx.features.session.SessionListener
import im.vector.riotx.features.settings.VectorPreferences
@ -77,6 +78,7 @@ class VectorApplication : Application(), HasVectorInjector, MatrixConfiguration.
@Inject lateinit var notificationUtils: NotificationUtils
@Inject lateinit var appStateHandler: AppStateHandler
@Inject lateinit var rxConfig: RxConfig
@Inject lateinit var popupAlertManager: PopupAlertManager
lateinit var vectorComponent: VectorComponent
private var fontThreadHandler: Handler? = null
@ -102,7 +104,7 @@ class VectorApplication : Application(), HasVectorInjector, MatrixConfiguration.
BigImageViewer.initialize(GlideImageLoader.with(applicationContext))
EpoxyController.defaultDiffingHandler = EpoxyAsyncUtil.getAsyncBackgroundHandler()
EpoxyController.defaultModelBuildingHandler = EpoxyAsyncUtil.getAsyncBackgroundHandler()
registerActivityLifecycleCallbacks(VectorActivityLifecycleCallbacks())
registerActivityLifecycleCallbacks(VectorActivityLifecycleCallbacks(popupAlertManager))
val fontRequest = FontRequest(
"com.google.android.gms.fonts",
"com.google.android.gms",

View file

@ -26,6 +26,8 @@ import im.vector.riotx.features.attachments.preview.AttachmentsPreviewFragment
import im.vector.riotx.features.createdirect.CreateDirectRoomDirectoryUsersFragment
import im.vector.riotx.features.createdirect.CreateDirectRoomKnownUsersFragment
import im.vector.riotx.features.crypto.keysbackup.settings.KeysBackupSettingsFragment
import im.vector.riotx.features.crypto.verification.cancel.VerificationCancelFragment
import im.vector.riotx.features.crypto.verification.cancel.VerificationNotMeFragment
import im.vector.riotx.features.crypto.verification.choose.VerificationChooseMethodFragment
import im.vector.riotx.features.crypto.verification.conclusion.VerificationConclusionFragment
import im.vector.riotx.features.crypto.verification.emoji.VerificationEmojiCodeFragment
@ -336,6 +338,16 @@ interface FragmentModule {
@FragmentKey(VerificationConclusionFragment::class)
fun bindVerificationConclusionFragment(fragment: VerificationConclusionFragment): Fragment
@Binds
@IntoMap
@FragmentKey(VerificationCancelFragment::class)
fun bindVerificationCancelFragment(fragment: VerificationCancelFragment): Fragment
@Binds
@IntoMap
@FragmentKey(VerificationNotMeFragment::class)
fun bindVerificationNotMeFragment(fragment: VerificationNotMeFragment): Fragment
@Binds
@IntoMap
@FragmentKey(QrCodeScannerFragment::class)

View file

@ -45,6 +45,7 @@ import im.vector.riotx.features.notifications.NotificationBroadcastReceiver
import im.vector.riotx.features.notifications.NotificationDrawerManager
import im.vector.riotx.features.notifications.NotificationUtils
import im.vector.riotx.features.notifications.PushRuleTriggerListener
import im.vector.riotx.features.popup.PopupAlertManager
import im.vector.riotx.features.rageshake.BugReporter
import im.vector.riotx.features.rageshake.VectorFileLogger
import im.vector.riotx.features.rageshake.VectorUncaughtExceptionHandler
@ -128,6 +129,8 @@ interface VectorComponent {
fun emojiDataSource(): EmojiDataSource
fun alertManager() : PopupAlertManager
@Component.Factory
interface Factory {
fun create(@BindsInstance context: Context): VectorComponent

View file

@ -16,10 +16,13 @@
package im.vector.riotx.core.extensions
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.airbnb.epoxy.EpoxyController
import im.vector.riotx.R
import im.vector.riotx.features.themes.ThemeUtils
/**
* Apply a Vertical LinearLayout Manager to the recyclerView and set the adapter from the epoxy controller
@ -40,7 +43,13 @@ fun RecyclerView.configureWith(epoxyController: EpoxyController,
itemAnimator?.let { this.itemAnimator = it }
}
if (showDivider) {
addItemDecoration(DividerItemDecoration(context, DividerItemDecoration.VERTICAL))
addItemDecoration(
DividerItemDecoration(context, DividerItemDecoration.VERTICAL).apply {
ContextCompat.getDrawable(context, ThemeUtils.getResourceId(context, R.drawable.divider_horizontal_light))?.let {
setDrawable(it)
}
}
)
}
setHasFixedSize(hasFixedSize)
adapter = epoxyController.adapter

View file

@ -36,6 +36,7 @@ import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap
import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
import im.vector.matrix.android.internal.crypto.model.rest.DevicesListResponse
import im.vector.riotx.R
import im.vector.riotx.features.popup.DefaultVectorAlert
import im.vector.riotx.features.popup.PopupAlertManager
import timber.log.Timber
import java.text.DateFormat
@ -54,7 +55,7 @@ import javax.inject.Singleton
*/
@Singleton
class KeyRequestHandler @Inject constructor(private val context: Context)
class KeyRequestHandler @Inject constructor(private val context: Context, private val popupAlertManager: PopupAlertManager)
: GossipingRequestListener,
VerificationService.Listener {
@ -118,9 +119,9 @@ class KeyRequestHandler @Inject constructor(private val context: Context)
}
if (deviceInfo.isUnknown) {
session?.cryptoService()?.setDeviceVerification(DeviceTrustLevel(false, false), userId, deviceId)
session?.cryptoService()?.setDeviceVerification(DeviceTrustLevel(crossSigningVerified = false, locallyVerified = false), userId, deviceId)
deviceInfo.trustLevel = DeviceTrustLevel(false, false)
deviceInfo.trustLevel = DeviceTrustLevel(crossSigningVerified = false, locallyVerified = false)
// can we get more info on this device?
session?.cryptoService()?.getDevicesList(object : MatrixCallback<DevicesListResponse> {
@ -188,7 +189,7 @@ class KeyRequestHandler @Inject constructor(private val context: Context)
}
}
val alert = PopupAlertManager.VectorAlert(
val alert = DefaultVectorAlert(
alertManagerId(userId, deviceId),
context.getString(R.string.key_share_request),
dialogText,
@ -210,7 +211,7 @@ class KeyRequestHandler @Inject constructor(private val context: Context)
denyAllRequests(mappingKey)
})
PopupAlertManager.postVectorAlert(alert)
popupAlertManager.postVectorAlert(alert)
}
private fun denyAllRequests(mappingKey: String) {
@ -250,7 +251,7 @@ class KeyRequestHandler @Inject constructor(private val context: Context)
&& it.requestId == request.requestId
}
if (alertsToRequests[alertMgrUniqueKey]?.isEmpty() == true) {
PopupAlertManager.cancelAlert(alertMgrUniqueKey)
popupAlertManager.cancelAlert(alertMgrUniqueKey)
alertsToRequests.remove(keyForMap(userId, deviceId))
}
}
@ -261,7 +262,7 @@ class KeyRequestHandler @Inject constructor(private val context: Context)
if (state == VerificationTxState.Verified) {
// ok it's verified, see if we have key request for that
shareAllSessions("${tx.otherDeviceId}${tx.otherUserId}")
PopupAlertManager.cancelAlert("ikr_${tx.otherDeviceId}${tx.otherUserId}")
popupAlertManager.cancelAlert("ikr_${tx.otherDeviceId}${tx.otherUserId}")
}
}
// should do it with QR tx also
@ -271,7 +272,7 @@ class KeyRequestHandler @Inject constructor(private val context: Context)
override fun markedAsManuallyVerified(userId: String, deviceId: String) {
// accept related requests
shareAllSessions(keyForMap(userId, deviceId))
PopupAlertManager.cancelAlert(alertManagerId(userId, deviceId))
popupAlertManager.cancelAlert(alertManagerId(userId, deviceId))
}
private fun keyForMap(userId: String, deviceId: String) = "$deviceId$userId"

View file

@ -17,15 +17,17 @@ package im.vector.riotx.features.crypto.verification
import android.content.Context
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.crypto.verification.PendingVerificationRequest
import im.vector.matrix.android.api.session.crypto.verification.VerificationService
import im.vector.matrix.android.api.session.crypto.verification.VerificationTransaction
import im.vector.matrix.android.api.session.crypto.verification.VerificationTxState
import im.vector.matrix.android.api.session.crypto.verification.PendingVerificationRequest
import im.vector.matrix.android.api.util.toMatrixItem
import im.vector.riotx.R
import im.vector.riotx.core.platform.VectorBaseActivity
import im.vector.riotx.features.home.room.detail.RoomDetailActivity
import im.vector.riotx.features.home.room.detail.RoomDetailArgs
import im.vector.riotx.features.popup.PopupAlertManager
import im.vector.riotx.features.popup.VerificationVectorAlert
import im.vector.riotx.features.themes.ThemeUtils
import javax.inject.Inject
import javax.inject.Singleton
@ -34,7 +36,9 @@ import javax.inject.Singleton
* Listens to the VerificationManager and add a new notification when an incoming request is detected.
*/
@Singleton
class IncomingVerificationRequestHandler @Inject constructor(private val context: Context) : VerificationService.Listener {
class IncomingVerificationRequestHandler @Inject constructor(
private val context: Context,
private val popupAlertManager: PopupAlertManager) : VerificationService.Listener {
private var session: Session? = null
@ -58,7 +62,7 @@ class IncomingVerificationRequestHandler @Inject constructor(private val context
val name = session?.getUser(tx.otherUserId)?.displayName
?: tx.otherUserId
val alert = PopupAlertManager.VectorAlert(
val alert = VerificationVectorAlert(
uid,
context.getString(R.string.sas_incoming_request_notif_title),
context.getString(R.string.sas_incoming_request_notif_content, name),
@ -68,12 +72,14 @@ class IncomingVerificationRequestHandler @Inject constructor(private val context
// TODO a bit too hugly :/
activity.supportFragmentManager.findFragmentByTag(VerificationBottomSheet.WAITING_SELF_VERIF_TAG)?.let {
false.also {
PopupAlertManager.cancelAlert(uid)
popupAlertManager.cancelAlert(uid)
}
} ?: true
} else true
})
.apply {
matrixItem = session?.getUser(tx.otherUserId)?.toMatrixItem()
contentAction = Runnable {
(weakCurrentActivity?.get() as? VectorBaseActivity)?.let {
it.navigator.performDeviceVerification(it, tx.otherUserId, tx.transactionId)
@ -99,11 +105,11 @@ class IncomingVerificationRequestHandler @Inject constructor(private val context
// 10mn expiration
expirationTimestamp = System.currentTimeMillis() + (10 * 60 * 1000L)
}
PopupAlertManager.postVectorAlert(alert)
popupAlertManager.postVectorAlert(alert)
}
is VerificationTxState.TerminalTxState -> {
// cancel related notification
PopupAlertManager.cancelAlert(uid)
popupAlertManager.cancelAlert(uid)
}
else -> Unit
}
@ -115,7 +121,7 @@ class IncomingVerificationRequestHandler @Inject constructor(private val context
val name = session?.getUser(pr.otherUserId)?.displayName
?: pr.otherUserId
val alert = PopupAlertManager.VectorAlert(
val alert = VerificationVectorAlert(
uniqueIdForVerificationRequest(pr),
context.getString(R.string.sas_incoming_request_notif_title),
"$name(${pr.otherUserId})",
@ -128,6 +134,8 @@ class IncomingVerificationRequestHandler @Inject constructor(private val context
} else true
})
.apply {
matrixItem = session?.getUser(pr.otherUserId)?.toMatrixItem()
contentAction = Runnable {
(weakCurrentActivity?.get() as? VectorBaseActivity)?.let {
val roomId = pr.roomId
@ -148,14 +156,14 @@ class IncomingVerificationRequestHandler @Inject constructor(private val context
// 5mn expiration
expirationTimestamp = System.currentTimeMillis() + (5 * 60 * 1000L)
}
PopupAlertManager.postVectorAlert(alert)
popupAlertManager.postVectorAlert(alert)
}
}
override fun verificationRequestUpdated(pr: PendingVerificationRequest) {
// If an incoming request is readied (by another device?) we should discard the alert
if (pr.isIncoming && (pr.isReady || pr.handledByOtherSession)) {
PopupAlertManager.cancelAlert(uniqueIdForVerificationRequest(pr))
popupAlertManager.cancelAlert(uniqueIdForVerificationRequest(pr))
}
}

View file

@ -16,9 +16,11 @@
package im.vector.riotx.features.crypto.verification
import android.app.Activity
import android.app.Dialog
import android.content.Intent
import android.os.Bundle
import android.os.Parcelable
import android.view.KeyEvent
import android.view.View
import android.widget.ImageView
import android.widget.TextView
@ -34,19 +36,24 @@ import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.crypto.crosssigning.MASTER_KEY_SSSS_NAME
import im.vector.matrix.android.api.session.crypto.crosssigning.SELF_SIGNING_KEY_SSSS_NAME
import im.vector.matrix.android.api.session.crypto.crosssigning.USER_SIGNING_KEY_SSSS_NAME
import im.vector.matrix.android.api.session.crypto.verification.CancelCode
import im.vector.matrix.android.api.session.crypto.verification.VerificationTxState
import im.vector.riotx.R
import im.vector.riotx.core.di.ScreenComponent
import im.vector.riotx.core.extensions.commitTransaction
import im.vector.riotx.core.extensions.exhaustive
import im.vector.riotx.core.platform.VectorBaseActivity
import im.vector.riotx.core.platform.VectorBaseBottomSheetDialogFragment
import im.vector.riotx.features.crypto.quads.SharedSecureStorageActivity
import im.vector.riotx.features.crypto.verification.cancel.VerificationCancelFragment
import im.vector.riotx.features.crypto.verification.cancel.VerificationNotMeFragment
import im.vector.riotx.features.crypto.verification.choose.VerificationChooseMethodFragment
import im.vector.riotx.features.crypto.verification.conclusion.VerificationConclusionFragment
import im.vector.riotx.features.crypto.verification.emoji.VerificationEmojiCodeFragment
import im.vector.riotx.features.crypto.verification.qrconfirmation.VerificationQrScannedByOtherFragment
import im.vector.riotx.features.crypto.verification.request.VerificationRequestFragment
import im.vector.riotx.features.home.AvatarRenderer
import im.vector.riotx.features.settings.VectorSettingsActivity
import kotlinx.android.parcel.Parcelize
import timber.log.Timber
import javax.inject.Inject
@ -58,6 +65,7 @@ class VerificationBottomSheet : VectorBaseBottomSheetDialogFragment() {
data class VerificationArgs(
val otherUserId: String,
val verificationId: String? = null,
val verificationLocalId: String? = null,
val roomId: String? = null,
// Special mode where UX should show loading wheel until other session sends a request/tx
val selfVerificationMode: Boolean = false
@ -80,13 +88,17 @@ class VerificationBottomSheet : VectorBaseBottomSheetDialogFragment() {
lateinit var otherUserNameText: TextView
@BindView(R.id.verificationRequestShield)
lateinit var otherUserShield: View
lateinit var otherUserShield: ImageView
@BindView(R.id.verificationRequestAvatar)
lateinit var otherUserAvatarImageView: ImageView
override fun getLayoutResId() = R.layout.bottom_sheet_verification
init {
isCancelable = false
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
@ -110,10 +122,27 @@ class VerificationBottomSheet : VectorBaseBottomSheetDialogFragment() {
.show()
Unit
}
VerificationBottomSheetViewEvents.GoToSettings -> {
dismiss()
(activity as? VectorBaseActivity)?.navigator?.openSettings(requireContext(), VectorSettingsActivity.EXTRA_DIRECT_ACCESS_SECURITY_PRIVACY)
}
}.exhaustive
}
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
return super.onCreateDialog(savedInstanceState).apply {
setOnKeyListener { _, keyCode, keyEvent ->
if (keyCode == KeyEvent.KEYCODE_BACK && keyEvent.action == KeyEvent.ACTION_UP) {
viewModel.queryCancel()
true
} else {
false
}
}
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (resultCode == Activity.RESULT_OK && requestCode == SECRET_REQUEST_CODE) {
data?.getStringExtra(SharedSecureStorageActivity.EXTRA_DATA_RESULT)?.let {
@ -127,15 +156,16 @@ class VerificationBottomSheet : VectorBaseBottomSheetDialogFragment() {
state.otherUserMxItem?.let { matrixItem ->
if (state.isMe) {
avatarRenderer.render(matrixItem, otherUserAvatarImageView)
if (state.sasTransactionState == VerificationTxState.Verified
|| state.qrTransactionState == VerificationTxState.Verified
|| state.verifiedFromPrivateKeys) {
otherUserAvatarImageView.setImageResource(R.drawable.ic_shield_trusted)
otherUserShield.setImageResource(R.drawable.ic_shield_trusted)
} else {
otherUserAvatarImageView.setImageResource(R.drawable.ic_shield_warning)
otherUserShield.setImageResource(R.drawable.ic_shield_warning)
}
otherUserNameText.text = getString(R.string.complete_security)
otherUserShield.isVisible = false
otherUserShield.isVisible = true
} else {
avatarRenderer.render(matrixItem, otherUserAvatarImageView)
@ -149,6 +179,18 @@ class VerificationBottomSheet : VectorBaseBottomSheetDialogFragment() {
}
}
if (state.userThinkItsNotHim) {
otherUserNameText.text = getString(R.string.dialog_title_warning)
showFragment(VerificationNotMeFragment::class, Bundle())
return@withState
}
if (state.userWantsToCancel) {
otherUserNameText.text = getString(R.string.are_you_sure)
showFragment(VerificationCancelFragment::class, Bundle())
return@withState
}
if (state.selfVerificationMode && state.verifiedFromPrivateKeys) {
showFragment(VerificationConclusionFragment::class, Bundle().apply {
putParcelable(MvRx.KEY_ARG, VerificationConclusionFragment.Args(true, null, state.isMe))
@ -222,7 +264,14 @@ class VerificationBottomSheet : VectorBaseBottomSheetDialogFragment() {
// Transaction has not yet started
if (state.pendingRequest.invoke()?.cancelConclusion != null) {
// The request has been declined, we should dismiss
dismiss()
otherUserNameText.text = getString(R.string.verification_cancelled)
showFragment(VerificationConclusionFragment::class, Bundle().apply {
putParcelable(MvRx.KEY_ARG, VerificationConclusionFragment.Args(
false,
state.pendingRequest.invoke()?.cancelConclusion?.value ?: CancelCode.User.value,
state.isMe))
})
return@withState
}
// If it's an outgoing
@ -267,6 +316,10 @@ class VerificationBottomSheet : VectorBaseBottomSheetDialogFragment() {
}
}
override fun dismiss() {
super.dismiss()
}
companion object {
const val SECRET_REQUEST_CODE = 101

View file

@ -24,5 +24,6 @@ import im.vector.riotx.core.platform.VectorViewEvents
sealed class VerificationBottomSheetViewEvents : VectorViewEvents {
object Dismiss : VerificationBottomSheetViewEvents()
object AccessSecretStore : VerificationBottomSheetViewEvents()
object GoToSettings : VerificationBottomSheetViewEvents()
data class ModalError(val errorMessage: CharSequence) : VerificationBottomSheetViewEvents()
}

View file

@ -31,7 +31,9 @@ import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.crypto.crosssigning.MASTER_KEY_SSSS_NAME
import im.vector.matrix.android.api.session.crypto.crosssigning.SELF_SIGNING_KEY_SSSS_NAME
import im.vector.matrix.android.api.session.crypto.crosssigning.USER_SIGNING_KEY_SSSS_NAME
import im.vector.matrix.android.api.session.crypto.verification.CancelCode
import im.vector.matrix.android.api.session.crypto.verification.IncomingSasVerificationTransaction
import im.vector.matrix.android.api.session.crypto.verification.PendingVerificationRequest
import im.vector.matrix.android.api.session.crypto.verification.QrCodeVerificationTransaction
import im.vector.matrix.android.api.session.crypto.verification.SasVerificationTransaction
import im.vector.matrix.android.api.session.crypto.verification.VerificationMethod
@ -44,7 +46,6 @@ import im.vector.matrix.android.api.util.MatrixItem
import im.vector.matrix.android.api.util.toMatrixItem
import im.vector.matrix.android.internal.crypto.crosssigning.fromBase64
import im.vector.matrix.android.internal.crypto.crosssigning.isVerified
import im.vector.matrix.android.api.session.crypto.verification.PendingVerificationRequest
import im.vector.riotx.core.extensions.exhaustive
import im.vector.riotx.core.platform.VectorViewModel
import timber.log.Timber
@ -60,7 +61,10 @@ data class VerificationBottomSheetViewState(
// true when we display the loading and we wait for the other (incoming request)
val selfVerificationMode: Boolean = false,
val verifiedFromPrivateKeys: Boolean = false,
val isMe: Boolean = false
val isMe: Boolean = false,
val currentDeviceCanCrossSign: Boolean = false,
val userWantsToCancel: Boolean = false,
val userThinkItsNotHim: Boolean = false
) : MvRxState
class VerificationBottomSheetViewModel @AssistedInject constructor(
@ -111,7 +115,8 @@ class VerificationBottomSheetViewModel @AssistedInject constructor(
pendingRequest = if (pr != null) Success(pr) else Uninitialized,
selfVerificationMode = selfVerificationMode,
roomId = args.roomId,
isMe = args.otherUserId == session.myUserId
isMe = args.otherUserId == session.myUserId,
currentDeviceCanCrossSign = session.cryptoService().crossSigningService().canCrossSign()
)
}
@ -137,6 +142,57 @@ class VerificationBottomSheetViewModel @AssistedInject constructor(
args: VerificationBottomSheet.VerificationArgs): VerificationBottomSheetViewModel
}
fun queryCancel() = withState {
if (it.userThinkItsNotHim) {
setState {
copy(userThinkItsNotHim = false)
}
} else {
setState {
copy(userWantsToCancel = true)
}
}
}
fun confirmCancel() = withState { state ->
cancelAllPendingVerifications(state)
_viewEvents.post(VerificationBottomSheetViewEvents.Dismiss)
}
private fun cancelAllPendingVerifications(state: VerificationBottomSheetViewState) {
session.cryptoService()
.verificationService().getExistingVerificationRequest(state.otherUserMxItem?.id ?: "", state.transactionId)?.let {
session.cryptoService().verificationService().cancelVerificationRequest(it)
}
session.cryptoService()
.verificationService()
.getExistingTransaction(state.otherUserMxItem?.id ?: "", state.transactionId ?: "")
?.cancel(CancelCode.User)
}
fun continueFromCancel() {
setState {
copy(userWantsToCancel = false)
}
}
fun continueFromWasNotMe() {
setState {
copy(userThinkItsNotHim = false)
}
}
fun itWasNotMe() {
setState {
copy(userThinkItsNotHim = true)
}
}
fun goToSettings() = withState { state ->
cancelAllPendingVerifications(state)
_viewEvents.post(VerificationBottomSheetViewEvents.GoToSettings)
}
companion object : MvRxViewModelFactory<VerificationBottomSheetViewModel, VerificationBottomSheetViewState> {
override fun create(viewModelContext: ViewModelContext, state: VerificationBottomSheetViewState): VerificationBottomSheetViewModel? {

View file

@ -0,0 +1,96 @@
/*
* Copyright (c) 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.riotx.features.crypto.verification.cancel
import com.airbnb.epoxy.EpoxyController
import im.vector.riotx.R
import im.vector.riotx.core.epoxy.dividerItem
import im.vector.riotx.core.resources.ColorProvider
import im.vector.riotx.core.resources.StringProvider
import im.vector.riotx.features.crypto.verification.VerificationBottomSheetViewState
import im.vector.riotx.features.crypto.verification.epoxy.bottomSheetVerificationActionItem
import im.vector.riotx.features.crypto.verification.epoxy.bottomSheetVerificationNoticeItem
import javax.inject.Inject
class VerificationCancelController @Inject constructor(
private val stringProvider: StringProvider,
private val colorProvider: ColorProvider
) : EpoxyController() {
var listener: Listener? = null
private var viewState: VerificationBottomSheetViewState? = null
fun update(viewState: VerificationBottomSheetViewState) {
this.viewState = viewState
requestModelBuild()
}
override fun buildModels() {
val state = viewState ?: return
if (state.isMe) {
if (state.currentDeviceCanCrossSign) {
bottomSheetVerificationNoticeItem {
id("notice")
notice(stringProvider.getString(R.string.verify_cancel_self_verification_from_trusted))
}
} else {
bottomSheetVerificationNoticeItem {
id("notice")
notice(stringProvider.getString(R.string.verify_cancel_self_verification_from_untrusted))
}
}
} else {
bottomSheetVerificationNoticeItem {
id("notice")
notice(stringProvider.getString(R.string.verify_cancel_self_verification_from_untrusted))
}
}
dividerItem {
id("sep0")
}
bottomSheetVerificationActionItem {
id("cancel")
title(stringProvider.getString(R.string.cancel))
titleColor(colorProvider.getColor(R.color.riotx_destructive_accent))
iconRes(R.drawable.ic_arrow_right)
iconColor(colorProvider.getColor(R.color.riotx_destructive_accent))
listener { listener?.onTapCancel() }
}
dividerItem {
id("sep1")
}
bottomSheetVerificationActionItem {
id("continue")
title(stringProvider.getString(R.string._continue))
titleColor(colorProvider.getColor(R.color.riotx_positive_accent))
iconRes(R.drawable.ic_arrow_right)
iconColor(colorProvider.getColor(R.color.riotx_positive_accent))
listener { listener?.onTapContinue() }
}
}
interface Listener {
fun onTapCancel()
fun onTapContinue()
}
}

View file

@ -0,0 +1,66 @@
/*
* Copyright (c) 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.riotx.features.crypto.verification.cancel
import android.os.Bundle
import android.view.View
import com.airbnb.mvrx.parentFragmentViewModel
import com.airbnb.mvrx.withState
import im.vector.riotx.R
import im.vector.riotx.core.extensions.cleanup
import im.vector.riotx.core.extensions.configureWith
import im.vector.riotx.core.platform.VectorBaseFragment
import im.vector.riotx.features.crypto.verification.VerificationBottomSheetViewModel
import kotlinx.android.synthetic.main.bottom_sheet_verification_child_fragment.*
import javax.inject.Inject
class VerificationCancelFragment @Inject constructor(
val controller: VerificationCancelController
) : VectorBaseFragment(), VerificationCancelController.Listener {
private val viewModel by parentFragmentViewModel(VerificationBottomSheetViewModel::class)
override fun getLayoutResId() = R.layout.bottom_sheet_verification_child_fragment
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupRecyclerView()
}
override fun onDestroyView() {
bottomSheetVerificationRecyclerView.cleanup()
controller.listener = null
super.onDestroyView()
}
private fun setupRecyclerView() {
bottomSheetVerificationRecyclerView.configureWith(controller, hasFixedSize = false, disableItemAnimation = true)
controller.listener = this
}
override fun invalidate() = withState(viewModel) { state ->
controller.update(state)
}
override fun onTapCancel() {
viewModel.confirmCancel()
}
override fun onTapContinue() {
viewModel.continueFromCancel()
}
}

View file

@ -0,0 +1,82 @@
/*
* Copyright (c) 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.riotx.features.crypto.verification.cancel
import com.airbnb.epoxy.EpoxyController
import im.vector.riotx.R
import im.vector.riotx.core.epoxy.dividerItem
import im.vector.riotx.core.resources.ColorProvider
import im.vector.riotx.core.resources.StringProvider
import im.vector.riotx.features.crypto.verification.VerificationBottomSheetViewState
import im.vector.riotx.features.crypto.verification.epoxy.bottomSheetVerificationActionItem
import im.vector.riotx.features.crypto.verification.epoxy.bottomSheetVerificationNoticeItem
import im.vector.riotx.features.html.EventHtmlRenderer
import javax.inject.Inject
class VerificationNotMeController @Inject constructor(
private val stringProvider: StringProvider,
private val colorProvider: ColorProvider,
private val eventHtmlRenderer: EventHtmlRenderer
) : EpoxyController() {
var listener: Listener? = null
private var viewState: VerificationBottomSheetViewState? = null
fun update(viewState: VerificationBottomSheetViewState) {
this.viewState = viewState
requestModelBuild()
}
override fun buildModels() {
bottomSheetVerificationNoticeItem {
id("notice")
notice(eventHtmlRenderer.render(stringProvider.getString(R.string.verify_not_me_self_verification)))
}
dividerItem {
id("sep0")
}
bottomSheetVerificationActionItem {
id("skip")
title(stringProvider.getString(R.string.skip))
titleColor(colorProvider.getColorFromAttribute(R.attr.riotx_text_primary))
iconRes(R.drawable.ic_arrow_right)
iconColor(colorProvider.getColorFromAttribute(R.attr.riotx_text_primary))
listener { listener?.onTapSkip() }
}
dividerItem {
id("sep1")
}
bottomSheetVerificationActionItem {
id("settings")
title(stringProvider.getString(R.string.settings))
titleColor(colorProvider.getColor(R.color.riotx_positive_accent))
iconRes(R.drawable.ic_arrow_right)
iconColor(colorProvider.getColor(R.color.riotx_positive_accent))
listener { listener?.onTapSettings() }
}
}
interface Listener {
fun onTapSkip()
fun onTapSettings()
}
}

View file

@ -0,0 +1,66 @@
/*
* Copyright (c) 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.riotx.features.crypto.verification.cancel
import android.os.Bundle
import android.view.View
import com.airbnb.mvrx.parentFragmentViewModel
import com.airbnb.mvrx.withState
import im.vector.riotx.R
import im.vector.riotx.core.extensions.cleanup
import im.vector.riotx.core.extensions.configureWith
import im.vector.riotx.core.platform.VectorBaseFragment
import im.vector.riotx.features.crypto.verification.VerificationBottomSheetViewModel
import kotlinx.android.synthetic.main.bottom_sheet_verification_child_fragment.*
import javax.inject.Inject
class VerificationNotMeFragment @Inject constructor(
val controller: VerificationNotMeController
) : VectorBaseFragment(), VerificationNotMeController.Listener {
private val viewModel by parentFragmentViewModel(VerificationBottomSheetViewModel::class)
override fun getLayoutResId() = R.layout.bottom_sheet_verification_child_fragment
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupRecyclerView()
}
override fun onDestroyView() {
bottomSheetVerificationRecyclerView.cleanup()
controller.listener = null
super.onDestroyView()
}
private fun setupRecyclerView() {
bottomSheetVerificationRecyclerView.configureWith(controller, hasFixedSize = false, disableItemAnimation = true)
controller.listener = this
}
override fun invalidate() = withState(viewModel) { state ->
controller.update(state)
}
override fun onTapSkip() {
viewModel.continueFromWasNotMe()
}
override fun onTapSettings() {
viewModel.goToSettings()
}
}

View file

@ -95,10 +95,27 @@ class VerificationChooseMethodController @Inject constructor(
listener { listener?.doVerifyBySas() }
}
}
if (state.isMe && state.canCrossSign) {
dividerItem {
id("sep_notMe")
}
bottomSheetVerificationActionItem {
id("wasnote")
title(stringProvider.getString(R.string.verify_new_session_was_not_me))
titleColor(colorProvider.getColor(R.color.riotx_destructive_accent))
subTitle(stringProvider.getString(R.string.verify_new_session_compromized))
iconRes(R.drawable.ic_arrow_right)
iconColor(colorProvider.getColorFromAttribute(R.attr.riotx_text_primary))
listener { listener?.onClickOnWasNotMe() }
}
}
}
interface Listener {
fun openCamera()
fun doVerifyBySas()
fun onClickOnWasNotMe()
}
}

View file

@ -89,6 +89,10 @@ class VerificationChooseMethodFragment @Inject constructor(
}
}
override fun onClickOnWasNotMe() {
sharedViewModel.itWasNotMe()
}
private fun doOpenQRCodeScanner() {
QrCodeScannerActivity.startForResult(this)
}

View file

@ -39,7 +39,9 @@ data class VerificationChooseMethodViewState(
val otherCanShowQrCode: Boolean = false,
val otherCanScanQrCode: Boolean = false,
val qrCodeText: String? = null,
val SASModeAvailable: Boolean = false
val SASModeAvailable: Boolean = false,
val isMe: Boolean = false,
val canCrossSign: Boolean = false
) : MvRxState
class VerificationChooseMethodViewModel @AssistedInject constructor(
@ -61,6 +63,10 @@ class VerificationChooseMethodViewModel @AssistedInject constructor(
}
}
override fun verificationRequestCreated(pr: PendingVerificationRequest) {
verificationRequestUpdated(pr)
}
override fun verificationRequestUpdated(pr: PendingVerificationRequest) = withState { state ->
val pvr = session.cryptoService().verificationService().getExistingVerificationRequest(state.otherUserId, state.transactionId)
@ -103,6 +109,8 @@ class VerificationChooseMethodViewModel @AssistedInject constructor(
val qrCodeVerificationTransaction = verificationService.getExistingTransaction(args.otherUserId, args.verificationId ?: "")
return VerificationChooseMethodViewState(otherUserId = args.otherUserId,
isMe = session.myUserId == pvr?.otherUserId,
canCrossSign = session.cryptoService().crossSigningService().canCrossSign(),
transactionId = args.verificationId ?: "",
otherCanShowQrCode = pvr?.otherCanShowQrCode().orFalse(),
otherCanScanQrCode = pvr?.otherCanScanQrCode().orFalse(),

View file

@ -58,6 +58,8 @@ class VerificationConclusionController @Inject constructor(
id("image")
imageRes(R.drawable.ic_shield_trusted)
}
bottomDone()
}
ConclusionState.WARNING -> {
bottomSheetVerificationNoticeItem {
@ -74,10 +76,32 @@ class VerificationConclusionController @Inject constructor(
id("warning_notice")
notice(eventHtmlRenderer.render(stringProvider.getString(R.string.verification_conclusion_compromised)))
}
}
else -> Unit
}
bottomDone()
}
ConclusionState.CANCELLED -> {
bottomSheetVerificationNoticeItem {
id("notice_cancelled")
notice(stringProvider.getString(R.string.verify_cancelled_notice))
}
dividerItem {
id("sep0")
}
bottomSheetVerificationActionItem {
id("got_it")
title(stringProvider.getString(R.string.sas_got_it))
titleColor(colorProvider.getColor(R.color.riotx_accent))
iconRes(R.drawable.ic_arrow_right)
iconColor(colorProvider.getColor(R.color.riotx_accent))
listener { listener?.onButtonTapped() }
}
}
}
}
private fun bottomDone() {
dividerItem {
id("sep0")
}

View file

@ -66,12 +66,7 @@ class VerificationConclusionFragment @Inject constructor(
}
override fun invalidate() = withState(viewModel) { state ->
if (state.conclusionState == ConclusionState.CANCELLED) {
// Just dismiss in this case
sharedViewModel.handle(VerificationAction.GotItConclusion)
} else {
controller.update(state)
}
controller.update(state)
}
override fun onButtonTapped() {

View file

@ -84,11 +84,16 @@ class VerificationRequestController @Inject constructor(
listener { listener?.onClickDismiss() }
}
} else {
val styledText = matrixItem.let {
stringProvider.getString(R.string.verification_request_notice, it.id)
.toSpannable()
.colorizeMatchingText(it.id, colorProvider.getColorFromAttribute(R.attr.vctr_notice_text_color))
}
val styledText =
if (state.isMe) {
stringProvider.getString(R.string.verify_new_session_notice)
} else {
matrixItem.let {
stringProvider.getString(R.string.verification_request_notice, it.id)
.toSpannable()
.colorizeMatchingText(it.id, colorProvider.getColorFromAttribute(R.attr.vctr_notice_text_color))
}
}
bottomSheetVerificationNoticeItem {
id("notice")
@ -119,18 +124,42 @@ class VerificationRequestController @Inject constructor(
}
is Success -> {
if (!pr.invoke().isReady) {
bottomSheetVerificationWaitingItem {
id("waiting")
title(stringProvider.getString(R.string.verification_request_waiting_for, matrixItem.getBestName()))
if (state.isMe) {
bottomSheetVerificationWaitingItem {
id("waiting")
title(stringProvider.getString(R.string.verification_request_waiting))
}
} else {
bottomSheetVerificationWaitingItem {
id("waiting")
title(stringProvider.getString(R.string.verification_request_waiting_for, matrixItem.getBestName()))
}
}
}
}
}
}
if (state.isMe && state.currentDeviceCanCrossSign) {
dividerItem {
id("sep_notMe")
}
bottomSheetVerificationActionItem {
id("wasnote")
title(stringProvider.getString(R.string.verify_new_session_was_not_me))
titleColor(colorProvider.getColor(R.color.riotx_destructive_accent))
subTitle(stringProvider.getString(R.string.verify_new_session_compromized))
iconRes(R.drawable.ic_arrow_right)
iconColor(colorProvider.getColorFromAttribute(R.attr.riotx_text_primary))
listener { listener?.onClickOnWasNotMe() }
}
}
}
interface Listener {
fun onClickOnVerificationStart()
fun onClickOnWasNotMe()
fun onClickRecoverFromPassphrase()
fun onClickDismiss()
}

View file

@ -69,4 +69,8 @@ class VerificationRequestFragment @Inject constructor(
override fun onClickDismiss() {
viewModel.handle(VerificationAction.SkipVerification)
}
override fun onClickOnWasNotMe() {
viewModel.itWasNotMe()
}
}

View file

@ -96,8 +96,8 @@ class AvatarRenderer @Inject constructor(private val activeSessionHolder: Active
// PRIVATE API *********************************************************************************
private fun buildGlideRequest(glideRequest: GlideRequests, avatarUrl: String?): GlideRequest<Drawable> {
val resolvedUrl = activeSessionHolder.getActiveSession().contentUrlResolver()
.resolveThumbnail(avatarUrl, THUMBNAIL_SIZE, THUMBNAIL_SIZE, ContentUrlResolver.ThumbnailMethod.SCALE)
val resolvedUrl = activeSessionHolder.getSafeActiveSession()?.contentUrlResolver()
?.resolveThumbnail(avatarUrl, THUMBNAIL_SIZE, THUMBNAIL_SIZE, ContentUrlResolver.ThumbnailMethod.SCALE)
return glideRequest
.load(resolvedUrl)

View file

@ -41,6 +41,7 @@ import im.vector.riotx.core.platform.VectorBaseActivity
import im.vector.riotx.core.pushers.PushersManager
import im.vector.riotx.features.disclaimer.showDisclaimerDialog
import im.vector.riotx.features.notifications.NotificationDrawerManager
import im.vector.riotx.features.popup.DefaultVectorAlert
import im.vector.riotx.features.popup.PopupAlertManager
import im.vector.riotx.features.rageshake.VectorUncaughtExceptionHandler
import im.vector.riotx.features.settings.VectorPreferences
@ -60,6 +61,7 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable {
@Inject lateinit var pushManager: PushersManager
@Inject lateinit var notificationDrawerManager: NotificationDrawerManager
@Inject lateinit var vectorPreferences: VectorPreferences
@Inject lateinit var popupAlertManager: PopupAlertManager
private val drawerListener = object : DrawerLayout.SimpleDrawerListener() {
override fun onDrawerStateChanged(newState: Int) {
@ -149,8 +151,8 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable {
if (crossSigningEnabledOnAccount && myCrossSigningKeys?.isTrusted() == false) {
// We need to ask
sharedActionViewModel.hasDisplayedCompleteSecurityPrompt = true
PopupAlertManager.postVectorAlert(
PopupAlertManager.VectorAlert(
popupAlertManager.postVectorAlert(
DefaultVectorAlert(
uid = "completeSecurity",
title = getString(R.string.new_signin),
description = getString(R.string.complete_security),

View file

@ -1,3 +1,4 @@
/*
* Copyright 2019 New Vector Ltd
*
@ -19,8 +20,10 @@ package im.vector.riotx.features.home
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import androidx.core.content.ContextCompat
import androidx.core.view.forEachIndexed
import androidx.lifecycle.Observer
import com.airbnb.mvrx.activityViewModel
import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState
import com.google.android.material.bottomnavigation.BottomNavigationItemView
@ -32,11 +35,14 @@ import im.vector.riotx.R
import im.vector.riotx.core.extensions.commitTransactionNow
import im.vector.riotx.core.glide.GlideApp
import im.vector.riotx.core.platform.ToolbarConfigurable
import im.vector.riotx.core.platform.VectorBaseActivity
import im.vector.riotx.core.platform.VectorBaseFragment
import im.vector.riotx.core.ui.views.KeysBackupBanner
import im.vector.riotx.features.home.room.list.RoomListFragment
import im.vector.riotx.features.home.room.list.RoomListParams
import im.vector.riotx.features.home.room.list.UnreadCounterBadgeView
import im.vector.riotx.features.popup.PopupAlertManager
import im.vector.riotx.features.popup.VerificationVectorAlert
import im.vector.riotx.features.workers.signout.SignOutViewModel
import kotlinx.android.synthetic.main.fragment_home_detail.*
import timber.log.Timber
@ -48,12 +54,15 @@ private const val INDEX_ROOMS = 2
class HomeDetailFragment @Inject constructor(
val homeDetailViewModelFactory: HomeDetailViewModel.Factory,
private val avatarRenderer: AvatarRenderer
private val avatarRenderer: AvatarRenderer,
private val alertManager: PopupAlertManager
) : VectorBaseFragment(), KeysBackupBanner.Delegate {
private val unreadCounterBadgeViews = arrayListOf<UnreadCounterBadgeView>()
private val viewModel: HomeDetailViewModel by fragmentViewModel()
private val unknownDeviceDetectorSharedViewModel : UnknownDeviceDetectorSharedViewModel by activityViewModel()
private lateinit var sharedActionViewModel: HomeSharedActionViewModel
override fun getLayoutResId() = R.layout.fragment_home_detail
@ -77,6 +86,38 @@ class HomeDetailFragment @Inject constructor(
viewModel.selectSubscribe(this, HomeDetailViewState::displayMode) { displayMode ->
switchDisplayMode(displayMode)
}
unknownDeviceDetectorSharedViewModel.subscribe {
it.unknownSessions.invoke()?.let { unknownDevices ->
Timber.v("## Detector - ${unknownDevices.size} Unknown sessions")
unknownDevices.forEachIndexed { index, deviceInfo ->
Timber.v("## Detector - #$index deviceId:${deviceInfo.second.deviceId} lastSeenTs:${deviceInfo.second.lastSeenTs}")
}
val uid = "Newest_Device"
alertManager.cancelAlert(uid)
if (it.canCrossSign && unknownDevices.isNotEmpty()) {
val newest = unknownDevices.first().second
val user = unknownDevices.first().first
alertManager.postVectorAlert(
VerificationVectorAlert(
uid = uid,
title = getString(R.string.new_session),
description = getString(R.string.new_session_review),
iconId = R.drawable.ic_shield_warning
).apply {
matrixItem = user
colorInt = ContextCompat.getColor(requireActivity(), R.color.riotx_accent)
contentAction = Runnable {
(weakCurrentActivity?.get() as? VectorBaseActivity)
?.navigator
?.requestSessionVerification(requireContext(), newest.deviceId ?: "")
}
dismissedAction = Runnable {}
}
)
}
}
}
}
private fun onGroupChange(groupSummary: GroupSummary?) {

View file

@ -0,0 +1,86 @@
/*
* Copyright (c) 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.riotx.features.home
import com.airbnb.mvrx.Async
import com.airbnb.mvrx.MvRxState
import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.Uninitialized
import com.airbnb.mvrx.ViewModelContext
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.util.MatrixItem
import im.vector.matrix.android.api.util.NoOpCancellable
import im.vector.matrix.android.api.util.toMatrixItem
import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
import im.vector.matrix.android.internal.crypto.model.rest.DevicesListResponse
import im.vector.matrix.rx.rx
import im.vector.matrix.rx.singleBuilder
import im.vector.riotx.core.di.HasScreenInjector
import im.vector.riotx.core.platform.EmptyAction
import im.vector.riotx.core.platform.EmptyViewEvents
import im.vector.riotx.core.platform.VectorViewModel
import io.reactivex.android.schedulers.AndroidSchedulers
data class UnknownDevicesState(
val unknownSessions: Async<List<Pair<MatrixItem?, DeviceInfo>>> = Uninitialized,
val canCrossSign: Boolean = false
) : MvRxState
class UnknownDeviceDetectorSharedViewModel(session: Session, initialState: UnknownDevicesState)
: VectorViewModel<UnknownDevicesState, EmptyAction, EmptyViewEvents>(initialState) {
init {
session.rx().liveUserCryptoDevices(session.myUserId)
.observeOn(AndroidSchedulers.mainThread())
.switchMap { deviceList ->
// Timber.v("## Detector - ============================")
// Timber.v("## Detector - Crypto device update ${deviceList.map { "${it.deviceId} : ${it.isVerified}" }}")
singleBuilder<DevicesListResponse> {
session.cryptoService().getDevicesList(it)
NoOpCancellable
}.map { resp ->
// Timber.v("## Detector - Device Infos ${resp.devices?.map { "${it.deviceId} : lastSeen:${it.lastSeenTs}" }}")
resp.devices?.filter { info ->
deviceList.firstOrNull { info.deviceId == it.deviceId }?.let {
!it.isVerified
} ?: false
}?.sortedByDescending { it.lastSeenTs }
?.map {
session.getUser(it.user_id ?: "")?.toMatrixItem() to it
} ?: emptyList()
}
.toObservable()
}
.execute { async ->
copy(unknownSessions = async)
}
session.rx().liveCrossSigningInfo(session.myUserId)
.execute {
copy(canCrossSign = session.cryptoService().crossSigningService().canCrossSign())
}
}
override fun handle(action: EmptyAction) {}
companion object : MvRxViewModelFactory<UnknownDeviceDetectorSharedViewModel, UnknownDevicesState> {
override fun create(viewModelContext: ViewModelContext, state: UnknownDevicesState): UnknownDeviceDetectorSharedViewModel? {
val session = (viewModelContext.activity as HasScreenInjector).injector().activeSessionHolder().getActiveSession()
return UnknownDeviceDetectorSharedViewModel(session, state)
}
}
}

View file

@ -58,7 +58,7 @@ class ViewReactionsBottomSheet : VectorBaseBottomSheetDialogFragment(), ViewReac
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
sharedActionViewModel = activityViewModelProvider.get(MessageSharedActionViewModel::class.java)
recyclerView.configureWith(epoxyController, hasFixedSize = false)
recyclerView.configureWith(epoxyController, hasFixedSize = false, showDivider = true)
bottomSheetTitle.text = context?.getString(R.string.reactions)
epoxyController.listener = this
}

View file

@ -20,14 +20,13 @@ import android.app.Activity
import android.app.Application
import android.os.Bundle
import im.vector.riotx.features.popup.PopupAlertManager
import javax.inject.Inject
class VectorActivityLifecycleCallbacks @Inject constructor() : Application.ActivityLifecycleCallbacks {
class VectorActivityLifecycleCallbacks constructor(private val popupAlertManager: PopupAlertManager) : Application.ActivityLifecycleCallbacks {
override fun onActivityPaused(activity: Activity) {
}
override fun onActivityResumed(activity: Activity) {
PopupAlertManager.onNewActivityDisplayed(activity)
popupAlertManager.onNewActivityDisplayed(activity)
}
override fun onActivityStarted(activity: Activity) {

View file

@ -83,12 +83,12 @@ class DefaultNavigator @Inject constructor(
}
}
override fun requestSessionVerification(context: Context) {
override fun requestSessionVerification(context: Context, otherSessionId: String) {
val session = sessionHolder.getSafeActiveSession() ?: return
val pr = session.cryptoService().verificationService().requestKeyVerification(
supportedVerificationMethodsProvider.provide(),
session.myUserId,
session.cryptoService().getUserDevices(session.myUserId).map { it.deviceId }
listOf(otherSessionId)
)
if (context is VectorBaseActivity) {
VerificationBottomSheet.withArgs(

View file

@ -30,7 +30,7 @@ interface Navigator {
fun performDeviceVerification(context: Context, otherUserId: String, sasTransactionId: String)
fun requestSessionVerification(context: Context)
fun requestSessionVerification(context: Context, otherSessionId: String)
fun waitSessionVerification(context: Context)

View file

@ -20,20 +20,23 @@ import android.os.Build
import android.os.Handler
import android.os.Looper
import android.view.View
import androidx.annotation.ColorInt
import androidx.annotation.ColorRes
import androidx.annotation.DrawableRes
import android.widget.ImageView
import com.tapadoo.alerter.Alerter
import com.tapadoo.alerter.OnHideAlertListener
import dagger.Lazy
import im.vector.riotx.R
import im.vector.riotx.features.home.AvatarRenderer
import timber.log.Timber
import java.lang.ref.WeakReference
import javax.inject.Inject
import javax.inject.Singleton
/**
* Responsible of displaying important popup alerts on top of the screen.
* Alerts are stacked and will be displayed sequentially
*/
object PopupAlertManager {
@Singleton
class PopupAlertManager @Inject constructor(private val avatarRenderer: Lazy<AvatarRenderer>) {
private var weakCurrentActivity: WeakReference<Activity>? = null
private var currentAlerter: VectorAlert? = null
@ -160,9 +163,19 @@ object PopupAlertManager {
clearLightStatusBar()
alert.weakCurrentActivity = WeakReference(activity)
Alerter.create(activity)
.setTitle(alert.title)
val alerter = if (alert is VerificationVectorAlert) Alerter.create(activity, R.layout.alerter_verification_layout)
else Alerter.create(activity)
alerter.setTitle(alert.title)
.setText(alert.description)
.also { al ->
if (alert is VerificationVectorAlert) {
val tvCustomView = al.getLayoutContainer()
tvCustomView?.findViewById<ImageView>(R.id.ivUserAvatar)?.let { imageView ->
alert.matrixItem?.let { avatarRenderer.get().render(it, imageView) }
}
}
}
.apply {
if (!animate) {
setEnterAnimation(R.anim.anim_alerter_no_anim)
@ -226,37 +239,4 @@ object PopupAlertManager {
displayNextIfPossible()
}, 500)
}
/**
* Dataclass to describe an important alert with actions.
*/
data class VectorAlert(val uid: String,
val title: String,
val description: String,
@DrawableRes val iconId: Int?,
val shouldBeDisplayedIn: ((Activity) -> Boolean)? = null) {
data class Button(val title: String, val action: Runnable, val autoClose: Boolean)
// will be set by manager, and accessible by actions at runtime
var weakCurrentActivity: WeakReference<Activity>? = null
val actions = ArrayList<Button>()
var contentAction: Runnable? = null
var dismissedAction: Runnable? = null
/** If this timestamp is after current time, this alert will be skipped */
var expirationTimestamp: Long? = null
fun addButton(title: String, action: Runnable, autoClose: Boolean = true) {
actions.add(Button(title, action, autoClose))
}
@ColorRes
var colorRes: Int? = null
@ColorInt
var colorInt: Int? = null
}
}

View file

@ -0,0 +1,95 @@
/*
* Copyright (c) 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.riotx.features.popup
import android.app.Activity
import androidx.annotation.ColorInt
import androidx.annotation.ColorRes
import androidx.annotation.DrawableRes
import im.vector.matrix.android.api.util.MatrixItem
import java.lang.ref.WeakReference
interface VectorAlert {
val uid: String
val title: String
val description: String
val iconId: Int?
val shouldBeDisplayedIn: ((Activity) -> Boolean)?
data class Button(val title: String, val action: Runnable, val autoClose: Boolean)
// will be set by manager, and accessible by actions at runtime
var weakCurrentActivity: WeakReference<Activity>?
val actions: MutableList<Button>
var contentAction: Runnable?
var dismissedAction: Runnable?
/** If this timestamp is after current time, this alert will be skipped */
var expirationTimestamp: Long?
fun addButton(title: String, action: Runnable, autoClose: Boolean = true) {
actions.add(Button(title, action, autoClose))
}
var colorRes: Int?
var colorInt: Int?
}
/**
* Dataclass to describe an important alert with actions.
*/
open class DefaultVectorAlert(override val uid: String,
override val title: String,
override val description: String,
@DrawableRes override val iconId: Int?,
override val shouldBeDisplayedIn: ((Activity) -> Boolean)? = null) : VectorAlert {
// will be set by manager, and accessible by actions at runtime
override var weakCurrentActivity: WeakReference<Activity>? = null
override val actions = ArrayList<VectorAlert.Button>()
override var contentAction: Runnable? = null
override var dismissedAction: Runnable? = null
/** If this timestamp is after current time, this alert will be skipped */
override var expirationTimestamp: Long? = null
override fun addButton(title: String, action: Runnable, autoClose: Boolean) {
actions.add(VectorAlert.Button(title, action, autoClose))
}
@ColorRes
override var colorRes: Int? = null
@ColorInt
override var colorInt: Int? = null
}
class VerificationVectorAlert(uid: String,
title: String,
override val description: String,
@DrawableRes override val iconId: Int?,
override val shouldBeDisplayedIn: ((Activity) -> Boolean)? = null
) : DefaultVectorAlert(
uid, title, description, iconId, shouldBeDisplayedIn
) {
var matrixItem: MatrixItem? = null
}

View file

@ -57,6 +57,8 @@ class VectorSettingsActivity : VectorBaseActivity(),
when (intent.getIntExtra(EXTRA_DIRECT_ACCESS, EXTRA_DIRECT_ACCESS_ROOT)) {
EXTRA_DIRECT_ACCESS_ADVANCED_SETTINGS ->
replaceFragment(R.id.vector_settings_page, VectorSettingsAdvancedSettingsFragment::class.java, null, FRAGMENT_TAG)
EXTRA_DIRECT_ACCESS_SECURITY_PRIVACY ->
replaceFragment(R.id.vector_settings_page, VectorSettingsSecurityPrivacyFragment::class.java, null, FRAGMENT_TAG)
else ->
replaceFragment(R.id.vector_settings_page, VectorSettingsRootFragment::class.java, null, FRAGMENT_TAG)
}
@ -116,6 +118,7 @@ class VectorSettingsActivity : VectorBaseActivity(),
const val EXTRA_DIRECT_ACCESS_ROOT = 0
const val EXTRA_DIRECT_ACCESS_ADVANCED_SETTINGS = 1
const val EXTRA_DIRECT_ACCESS_SECURITY_PRIVACY = 2
private const val FRAGMENT_TAG = "VectorSettingsPreferencesFragment"
}

View file

@ -78,6 +78,17 @@ class CrossSigningEpoxyController @Inject constructor(
interactionListener?.onResetCrossSigningKeys()
}
}
bottomSheetVerificationActionItem {
id("verify")
title(stringProvider.getString(R.string.complete_security))
titleColor(colorProvider.getColor(R.color.riotx_positive_accent))
iconRes(R.drawable.ic_arrow_right)
iconColor(colorProvider.getColor(R.color.riotx_positive_accent))
listener {
interactionListener?.verifySession()
}
}
}
} else if (data.xSigningIsEnableInAccount) {
genericItem {

View file

@ -62,7 +62,7 @@ class DeviceVerificationInfoBottomSheet : VectorBaseBottomSheetDialogFragment(),
super.onActivityCreated(savedInstanceState)
recyclerView.configureWith(
epoxyController,
showDivider = true,
showDivider = false,
hasFixedSize = false)
epoxyController.callback = this
bottomSheetTitle.isVisible = false

View file

@ -62,10 +62,10 @@ object ThemeUtils {
*/
fun setApplicationTheme(context: Context, aTheme: String) {
when (aTheme) {
THEME_DARK_VALUE -> context.setTheme(R.style.AppTheme_Dark)
THEME_BLACK_VALUE -> context.setTheme(R.style.AppTheme_Black)
THEME_DARK_VALUE -> context.setTheme(R.style.AppTheme_Dark)
THEME_BLACK_VALUE -> context.setTheme(R.style.AppTheme_Black)
THEME_STATUS_VALUE -> context.setTheme(R.style.AppTheme_Status)
else -> context.setTheme(R.style.AppTheme_Light)
else -> context.setTheme(R.style.AppTheme_Light)
}
// Clear the cache
@ -170,6 +170,7 @@ object ThemeUtils {
R.drawable.bg_search_edit_text_light -> R.drawable.bg_search_edit_text_dark
R.drawable.bg_unread_notification_light -> R.drawable.bg_unread_notification_dark
R.drawable.vector_label_background_light -> R.drawable.vector_label_background_dark
R.drawable.divider_horizontal_light -> R.drawable.divider_horizontal_dark
else -> {
Timber.w("Warning, missing case for wanted drawable in dark theme")
resourceId
@ -181,6 +182,7 @@ object ThemeUtils {
R.drawable.bg_search_edit_text_light -> R.drawable.bg_search_edit_text_black
R.drawable.bg_unread_notification_light -> R.drawable.bg_unread_notification_black
R.drawable.vector_label_background_light -> R.drawable.vector_label_background_black
R.drawable.divider_horizontal_light -> R.drawable.divider_horizontal_black
else -> {
Timber.w("Warning, missing case for wanted drawable in black theme")
resourceId

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<size android:height="1dp" />
<solid android:color="@color/riotx_header_panel_border_mobile_black" />
</shape>

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<size android:height="1dp" />
<solid android:color="@color/riotx_header_panel_border_mobile_dark" />
</shape>

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<size android:height="1dp" />
<solid android:color="@color/riotx_header_panel_border_mobile_light" />
</shape>

View file

@ -0,0 +1,92 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
tools:background="@android:color/darker_gray"
tools:foreground="?android:attr/selectableItemBackground"
tools:style="@style/AlertStyle">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/ivUserAvatar"
android:layout_width="40dp"
android:layout_height="40dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/alerter_texts"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:src="@tools:sample/avatars" />
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/ivIcon"
android:layout_width="24dp"
android:layout_height="24dp"
app:layout_constraintCircle="@+id/ivUserAvatar"
app:layout_constraintCircleAngle="135"
app:layout_constraintCircleRadius="20dp"
tools:ignore="MissingConstraints"
android:src="@drawable/ic_shield_warning"
tools:visibility="visible" />
<LinearLayout
android:id="@+id/alerter_texts"
android:layout_width="0dp"
android:layout_height="0dp"
android:orientation="vertical"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/ivUserAvatar"
app:layout_constraintTop_toTopOf="parent">
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/tvTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/alerter_padding_half"
android:layout_marginEnd="@dimen/alerter_padding_half"
android:paddingStart="@dimen/alerter_padding_small"
android:paddingLeft="@dimen/alerter_padding_small"
android:paddingEnd="@dimen/alerter_padding_small"
android:textAppearance="@style/AlertTextAppearance.Title"
android:visibility="gone"
tools:text="Title"
tools:visibility="visible" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/tvText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/alerter_padding_half"
android:layout_marginEnd="@dimen/alerter_padding_half"
android:paddingStart="@dimen/alerter_padding_small"
android:paddingLeft="@dimen/alerter_padding_small"
android:paddingTop="@dimen/alerter_padding_small"
android:paddingEnd="@dimen/alerter_padding_small"
android:paddingBottom="@dimen/alerter_padding_small"
android:textAppearance="@style/AlertTextAppearance.Text"
android:visibility="gone"
tools:text="Text"
tools:visibility="visible" />
</LinearLayout>
<!-- <FrameLayout-->
<!-- android:id="@+id/flRightIconContainer"-->
<!-- android:layout_width="wrap_content"-->
<!-- android:layout_height="wrap_content"-->
<!-- android:layout_gravity="center_vertical">-->
<!-- <androidx.appcompat.widget.AppCompatImageView-->
<!-- android:id="@+id/ivRightIcon"-->
<!-- android:layout_width="@dimen/alerter_alert_icn_size"-->
<!-- android:layout_height="@dimen/alerter_alert_icn_size"-->
<!-- android:maxWidth="@dimen/alerter_alert_icn_size"-->
<!-- android:maxHeight="@dimen/alerter_alert_icn_size"-->
<!-- android:visibility="gone"-->
<!-- app:srcCompat="@drawable/alerter_ic_notifications"-->
<!-- app:tint="@color/alert_default_icon_color"-->
<!-- tools:visibility="visible" />-->
<!-- </FrameLayout>-->
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -3,6 +3,7 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="8dp"
android:orientation="vertical">
<TextView

View file

@ -287,7 +287,7 @@
<string name="login_error_no_homeserver_found">This is not a valid Matrix server address</string>
<string name="login_error_homeserver_not_found">Cannot reach a homeserver at this URL, please check it</string>
<string name="login_error_ssl_handshake">Your device is using an outdated TLS security protocol, vulnerable to attack, for your security you will not be able to connect</string>
<string name="login_mobile_device">Mobile</string>
<string name="login_mobile_device">RiotX Android</string>
<string name="login_error_forbidden">Invalid username/password</string>
<string name="login_error_unknown_token">The access token specified was not recognised</string>

View file

@ -14,6 +14,23 @@
<string name="refresh">Refresh</string>
<string name="new_session">New Session</string>
<string name="new_session_review">Tap to review &amp; verify</string>
<string name="verify_new_session_notice">Use this session to verify your new one, granting it access to encrypted messages.</string>
<string name="verify_new_session_was_not_me">This wasnt me</string>
<string name="verify_new_session_compromized">Your account may be compromised</string>
<string name="verify_cancel_self_verification_from_untrusted">If you cancel, you wont be able to read encrypted messages on this device, and other users wont trust it</string>
<string name="verify_cancel_self_verification_from_trusted">If you cancel, you wont be able to read encrypted messages on your new device, and other users wont trust it</string>
<string name="verify_not_me_self_verification">
One of the following may be compromised:\n\n- Your password\n- Your homeserver\n- This device, or the other device\n- The internet connection either device is using\n\nWe recommend you change your password &amp; recovery key in Settings immediately.
</string>
<string name="verify_cancelled_notice">Verify your devices from Settings.</string>
<string name="verification_cancelled">Verification Cancelled</string>
<!-- END Strings added by Valere -->

View file

@ -36,8 +36,6 @@
<!-- Drawables -->
<item name="riotx_highlighted_message_background">@drawable/highlighted_message_background_dark</item>
<item name="android:listDivider">@color/riotx_header_panel_border_mobile_dark</item>
<!-- Material color: Note: this block should be the same in all theme because it references only common colors and ?riotx attributes -->
<item name="colorPrimary">@color/riotx_accent</item>
<item name="colorPrimaryVariant">@color/primary_color_dark_light</item>

View file

@ -35,8 +35,6 @@
<!-- Drawables -->
<item name="riotx_highlighted_message_background">@drawable/highlighted_message_background_light</item>
<item name="android:listDivider">@color/riotx_header_panel_border_mobile_light</item>
<!-- Material color: Note: this block should be the same in all theme because it references only common colors and ?riotx attributes -->
<item name="colorPrimary">@color/riotx_accent</item>
<!--item name="colorPrimaryVariant">@color/primary_color_dark_light</item-->