mirror of
https://github.com/SchildiChat/SchildiChat-android.git
synced 2025-03-18 04:08:44 +03:00
Merge pull request #1489 from vector-im/feature/cross_sigining_evol
Feature/cross sigining evol
This commit is contained in:
commit
c23819bfcf
68 changed files with 1445 additions and 788 deletions
|
@ -8,6 +8,7 @@ Features ✨:
|
|||
Improvements 🙌:
|
||||
- "Add Matrix app" menu is now always visible (#1495)
|
||||
- Handle `/op`, `/deop`, and `/nick` commands (#12)
|
||||
- Prioritising Recovery key over Recovery passphrase (#1463)
|
||||
|
||||
Bugfix 🐛:
|
||||
- Fix dark theme issue on login screen (#1097)
|
||||
|
|
|
@ -1,3 +1,7 @@
|
|||
Useful links:
|
||||
- https://codelabs.developers.google.com/codelabs/webrtc-web/#0
|
||||
|
||||
|
||||
╔════════════════════════════════════════════════╗
|
||||
║ ║
|
||||
║A] Placing a call offer ║
|
||||
|
|
|
@ -20,10 +20,13 @@ import androidx.lifecycle.LiveData
|
|||
|
||||
interface InitialSyncProgressService {
|
||||
|
||||
fun getInitialSyncProgressStatus() : LiveData<Status?>
|
||||
fun getInitialSyncProgressStatus(): LiveData<Status>
|
||||
|
||||
data class Status(
|
||||
@StringRes val statusText: Int,
|
||||
val percentProgress: Int = 0
|
||||
)
|
||||
sealed class Status {
|
||||
object Idle : Status()
|
||||
data class Progressing(
|
||||
@StringRes val statusText: Int,
|
||||
val percentProgress: Int = 0
|
||||
) : Status()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -99,7 +99,9 @@ interface CryptoService {
|
|||
fun removeRoomKeysRequestListener(listener: GossipingRequestListener)
|
||||
|
||||
fun fetchDevicesList(callback: MatrixCallback<DevicesListResponse>)
|
||||
|
||||
fun getMyDevicesInfo() : List<DeviceInfo>
|
||||
|
||||
fun getLiveMyDevicesInfo() : LiveData<List<DeviceInfo>>
|
||||
|
||||
fun getDeviceInfo(deviceId: String, callback: MatrixCallback<DeviceInfo>)
|
||||
|
|
|
@ -41,11 +41,13 @@ interface CrossSigningService {
|
|||
* Users needs to enter credentials
|
||||
*/
|
||||
fun initializeCrossSigning(authParams: UserPasswordAuth?,
|
||||
callback: MatrixCallback<Unit>? = null)
|
||||
callback: MatrixCallback<Unit>)
|
||||
|
||||
fun isCrossSigningInitialized(): Boolean = getMyCrossSigningKeys() != null
|
||||
|
||||
fun checkTrustFromPrivateKeys(masterKeyPrivateKey: String?,
|
||||
uskKeyPrivateKey: String?,
|
||||
sskPrivateKey: String?) : UserTrustResult
|
||||
sskPrivateKey: String?): UserTrustResult
|
||||
|
||||
fun getUserCrossSigningKeys(otherUserId: String): MXCrossSigningInfo?
|
||||
|
||||
|
@ -74,6 +76,8 @@ interface CrossSigningService {
|
|||
otherDeviceId: String,
|
||||
locallyTrusted: Boolean?): DeviceTrustResult
|
||||
|
||||
// FIXME Those method do not have to be in the service
|
||||
fun onSecretMSKGossip(mskPrivateKey: String)
|
||||
fun onSecretSSKGossip(sskPrivateKey: String)
|
||||
fun onSecretUSKGossip(uskPrivateKey: String)
|
||||
}
|
||||
|
|
|
@ -18,6 +18,9 @@ package im.vector.matrix.android.api.session.securestorage
|
|||
|
||||
import im.vector.matrix.android.api.MatrixCallback
|
||||
import im.vector.matrix.android.api.listeners.ProgressListener
|
||||
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
|
||||
|
||||
/**
|
||||
* Some features may require clients to store encrypted data on the server so that it can be shared securely between clients.
|
||||
|
@ -111,7 +114,17 @@ interface SharedSecretStorageService {
|
|||
*/
|
||||
fun getSecret(name: String, keyId: String?, secretKey: SsssKeySpec, callback: MatrixCallback<String>)
|
||||
|
||||
fun checkShouldBeAbleToAccessSecrets(secretNames: List<String>, keyId: String?) : IntegrityResult
|
||||
/**
|
||||
* Return true if SSSS is configured
|
||||
*/
|
||||
fun isRecoverySetup(): Boolean {
|
||||
return checkShouldBeAbleToAccessSecrets(
|
||||
secretNames = listOf(MASTER_KEY_SSSS_NAME, USER_SIGNING_KEY_SSSS_NAME, SELF_SIGNING_KEY_SSSS_NAME),
|
||||
keyId = null
|
||||
) is IntegrityResult.Success
|
||||
}
|
||||
|
||||
fun checkShouldBeAbleToAccessSecrets(secretNames: List<String>, keyId: String?): IntegrityResult
|
||||
|
||||
fun requestSecret(name: String, myOtherDeviceId: String)
|
||||
|
||||
|
|
|
@ -29,11 +29,13 @@ import dagger.Lazy
|
|||
import im.vector.matrix.android.api.MatrixCallback
|
||||
import im.vector.matrix.android.api.NoOpMatrixCallback
|
||||
import im.vector.matrix.android.api.crypto.MXCryptoConfig
|
||||
import im.vector.matrix.android.api.extensions.tryThis
|
||||
import im.vector.matrix.android.api.failure.Failure
|
||||
import im.vector.matrix.android.api.listeners.ProgressListener
|
||||
import im.vector.matrix.android.api.session.crypto.CryptoService
|
||||
import im.vector.matrix.android.api.session.crypto.MXCryptoError
|
||||
import im.vector.matrix.android.api.session.crypto.crosssigning.KEYBACKUP_SECRET_SSSS_NAME
|
||||
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.keyshare.GossipingRequestListener
|
||||
|
@ -326,35 +328,67 @@ internal class DefaultCryptoService @Inject constructor(
|
|||
* and, then, if this is the first time, this new device will be announced to all other users
|
||||
* devices.
|
||||
*
|
||||
* @param isInitialSync true if it starts from an initial sync
|
||||
*/
|
||||
fun start(isInitialSync: Boolean) {
|
||||
if (isStarted.get() || isStarting.get()) {
|
||||
return
|
||||
}
|
||||
isStarting.set(true)
|
||||
|
||||
fun start() {
|
||||
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
|
||||
internalStart(isInitialSync)
|
||||
internalStart()
|
||||
}
|
||||
// Just update
|
||||
fetchDevicesList(NoOpMatrixCallback())
|
||||
}
|
||||
|
||||
private suspend fun internalStart(isInitialSync: Boolean) {
|
||||
// Open the store
|
||||
cryptoStore.open()
|
||||
runCatching {
|
||||
fun ensureDevice() {
|
||||
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
|
||||
// Open the store
|
||||
cryptoStore.open()
|
||||
// TODO why do that everytime? we should mark that it was done
|
||||
uploadDeviceKeys()
|
||||
oneTimeKeysUploader.maybeUploadOneTimeKeys()
|
||||
keysBackupService.checkAndStartKeysBackup()
|
||||
if (isInitialSync) {
|
||||
// refresh the devices list for each known room members
|
||||
deviceListManager.invalidateAllDeviceLists()
|
||||
deviceListManager.refreshOutdatedDeviceLists()
|
||||
} else {
|
||||
incomingGossipingRequestManager.processReceivedGossipingRequests()
|
||||
// this can throw if no backup
|
||||
tryThis {
|
||||
keysBackupService.checkAndStartKeysBackup()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun onSyncWillProcess(isInitialSync: Boolean) {
|
||||
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
|
||||
if (isInitialSync) {
|
||||
try {
|
||||
// On initial sync, we start all our tracking from
|
||||
// scratch, so mark everything as untracked. onCryptoEvent will
|
||||
// be called for all e2e rooms during the processing of the sync,
|
||||
// at which point we'll start tracking all the users of that room.
|
||||
deviceListManager.invalidateAllDeviceLists()
|
||||
// always track my devices?
|
||||
deviceListManager.startTrackingDeviceList(listOf(userId))
|
||||
deviceListManager.refreshOutdatedDeviceLists()
|
||||
} catch (failure: Throwable) {
|
||||
Timber.e(failure, "## CRYPTO onSyncWillProcess ")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun internalStart() {
|
||||
if (isStarted.get() || isStarting.get()) {
|
||||
return
|
||||
}
|
||||
isStarting.set(true)
|
||||
|
||||
// Open the store
|
||||
cryptoStore.open()
|
||||
|
||||
runCatching {
|
||||
// if (isInitialSync) {
|
||||
// // refresh the devices list for each known room members
|
||||
// deviceListManager.invalidateAllDeviceLists()
|
||||
// deviceListManager.refreshOutdatedDeviceLists()
|
||||
// } else {
|
||||
|
||||
// Why would we do that? it will be called at end of syn
|
||||
incomingGossipingRequestManager.processReceivedGossipingRequests()
|
||||
// }
|
||||
}.fold(
|
||||
{
|
||||
isStarting.set(false)
|
||||
|
@ -623,10 +657,10 @@ internal class DefaultCryptoService @Inject constructor(
|
|||
roomId: String,
|
||||
callback: MatrixCallback<MXEncryptEventContentResult>) {
|
||||
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
|
||||
if (!isStarted()) {
|
||||
Timber.v("## CRYPTO | encryptEventContent() : wait after e2e init")
|
||||
internalStart(false)
|
||||
}
|
||||
// if (!isStarted()) {
|
||||
// Timber.v("## CRYPTO | encryptEventContent() : wait after e2e init")
|
||||
// internalStart(false)
|
||||
// }
|
||||
val userIds = getRoomUserIds(roomId)
|
||||
var alg = roomEncryptorsStore.get(roomId)
|
||||
if (alg == null) {
|
||||
|
@ -835,6 +869,10 @@ internal class DefaultCryptoService @Inject constructor(
|
|||
*/
|
||||
private fun handleSDKLevelGossip(secretName: String?, secretValue: String): Boolean {
|
||||
return when (secretName) {
|
||||
MASTER_KEY_SSSS_NAME -> {
|
||||
crossSigningService.onSecretMSKGossip(secretValue)
|
||||
true
|
||||
}
|
||||
SELF_SIGNING_KEY_SSSS_NAME -> {
|
||||
crossSigningService.onSecretSSKGossip(secretValue)
|
||||
true
|
||||
|
@ -1153,10 +1191,10 @@ internal class DefaultCryptoService @Inject constructor(
|
|||
}
|
||||
|
||||
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
|
||||
if (!isStarted()) {
|
||||
Timber.v("## CRYPTO | requestRoomKeyForEvent() : wait after e2e init")
|
||||
internalStart(false)
|
||||
}
|
||||
// if (!isStarted()) {
|
||||
// Timber.v("## CRYPTO | requestRoomKeyForEvent() : wait after e2e init")
|
||||
// internalStart(false)
|
||||
// }
|
||||
roomDecryptorProvider
|
||||
.getOrCreateRoomDecryptor(event.roomId, wireContent.algorithm)
|
||||
?.requestKeysForEvent(event) ?: run {
|
||||
|
|
|
@ -156,6 +156,7 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
|
|||
* @param left the user ids list which left a room
|
||||
*/
|
||||
fun handleDeviceListsChanges(changed: Collection<String>, left: Collection<String>) {
|
||||
Timber.v("## CRYPTO: handleDeviceListsChanges changed:$changed / left:$left")
|
||||
var isUpdated = false
|
||||
val deviceTrackingStatuses = cryptoStore.getDeviceTrackingStatuses().toMutableMap()
|
||||
|
||||
|
@ -483,6 +484,7 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
|
|||
* This method must be called on getEncryptingThreadHandler() thread.
|
||||
*/
|
||||
suspend fun refreshOutdatedDeviceLists() {
|
||||
Timber.v("## CRYPTO | refreshOutdatedDeviceLists()")
|
||||
val deviceTrackingStatuses = cryptoStore.getDeviceTrackingStatuses().toMutableMap()
|
||||
|
||||
val users = deviceTrackingStatuses.keys.filterTo(mutableListOf()) { userId ->
|
||||
|
|
|
@ -19,6 +19,7 @@ package im.vector.matrix.android.internal.crypto
|
|||
import im.vector.matrix.android.api.auth.data.Credentials
|
||||
import im.vector.matrix.android.api.crypto.MXCryptoConfig
|
||||
import im.vector.matrix.android.api.session.crypto.crosssigning.KEYBACKUP_SECRET_SSSS_NAME
|
||||
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.keyshare.GossipingRequestListener
|
||||
|
@ -310,8 +311,8 @@ internal class IncomingGossipingRequestManager @Inject constructor(
|
|||
|
||||
val isDeviceLocallyVerified = cryptoStore.getUserDevice(userId, deviceId)?.trustLevel?.isLocallyVerified()
|
||||
|
||||
// Should SDK always Silently reject any request for the master key?
|
||||
when (secretName) {
|
||||
MASTER_KEY_SSSS_NAME -> cryptoStore.getCrossSigningPrivateKeys()?.master
|
||||
SELF_SIGNING_KEY_SSSS_NAME -> cryptoStore.getCrossSigningPrivateKeys()?.selfSigned
|
||||
USER_SIGNING_KEY_SSSS_NAME -> cryptoStore.getCrossSigningPrivateKeys()?.user
|
||||
KEYBACKUP_SECRET_SSSS_NAME -> cryptoStore.getKeyBackupRecoveryKeyInfo()?.recoveryKey
|
||||
|
|
|
@ -140,7 +140,7 @@ internal class DefaultCrossSigningService @Inject constructor(
|
|||
* - Sign the keys and upload them
|
||||
* - Sign the current device with SSK and sign MSK with device key (migration) and upload signatures
|
||||
*/
|
||||
override fun initializeCrossSigning(authParams: UserPasswordAuth?, callback: MatrixCallback<Unit>?) {
|
||||
override fun initializeCrossSigning(authParams: UserPasswordAuth?, callback: MatrixCallback<Unit>) {
|
||||
Timber.d("## CrossSigning initializeCrossSigning")
|
||||
|
||||
val params = InitializeCrossSigningTask.Params(
|
||||
|
@ -150,7 +150,8 @@ internal class DefaultCrossSigningService @Inject constructor(
|
|||
this.callbackThread = TaskThread.CRYPTO
|
||||
this.callback = object : MatrixCallback<InitializeCrossSigningTask.Result> {
|
||||
override fun onFailure(failure: Throwable) {
|
||||
callback?.onFailure(failure)
|
||||
Timber.e(failure, "Error in initializeCrossSigning()")
|
||||
callback.onFailure(failure)
|
||||
}
|
||||
|
||||
override fun onSuccess(data: InitializeCrossSigningTask.Result) {
|
||||
|
@ -162,12 +163,39 @@ internal class DefaultCrossSigningService @Inject constructor(
|
|||
userPkSigning = OlmPkSigning().apply { initWithSeed(data.userKeyPK.fromBase64()) }
|
||||
selfSigningPkSigning = OlmPkSigning().apply { initWithSeed(data.selfSigningKeyPK.fromBase64()) }
|
||||
|
||||
callback?.onSuccess(Unit)
|
||||
callback.onSuccess(Unit)
|
||||
}
|
||||
}
|
||||
}.executeBy(taskExecutor)
|
||||
}
|
||||
|
||||
override fun onSecretMSKGossip(mskPrivateKey: String) {
|
||||
Timber.i("## CrossSigning - onSecretSSKGossip")
|
||||
val mxCrossSigningInfo = getMyCrossSigningKeys() ?: return Unit.also {
|
||||
Timber.e("## CrossSigning - onSecretMSKGossip() received secret but public key is not known")
|
||||
}
|
||||
|
||||
mskPrivateKey.fromBase64()
|
||||
.let { privateKeySeed ->
|
||||
val pkSigning = OlmPkSigning()
|
||||
try {
|
||||
if (pkSigning.initWithSeed(privateKeySeed) == mxCrossSigningInfo.masterKey()?.unpaddedBase64PublicKey) {
|
||||
masterPkSigning?.releaseSigning()
|
||||
masterPkSigning = pkSigning
|
||||
Timber.i("## CrossSigning - Loading MSK success")
|
||||
cryptoStore.storeMSKPrivateKey(mskPrivateKey)
|
||||
return
|
||||
} else {
|
||||
Timber.e("## CrossSigning - onSecretMSKGossip() private key do not match public key")
|
||||
pkSigning.releaseSigning()
|
||||
}
|
||||
} catch (failure: Throwable) {
|
||||
Timber.e("## CrossSigning - onSecretMSKGossip() ${failure.localizedMessage}")
|
||||
pkSigning.releaseSigning()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSecretSSKGossip(sskPrivateKey: String) {
|
||||
Timber.i("## CrossSigning - onSecretSSKGossip")
|
||||
val mxCrossSigningInfo = getMyCrossSigningKeys() ?: return Unit.also {
|
||||
|
|
|
@ -32,16 +32,15 @@ internal data class SignatureUploadResponse(
|
|||
* If a signature is not valid, the homeserver should set the corresponding entry in failures to a JSON object
|
||||
* with the errcode property set to M_INVALID_SIGNATURE.
|
||||
*/
|
||||
val failures: Map<String, Map<String, @JvmSuppressWildcards Any>>? = null
|
||||
|
||||
val failures: Map<String, Map<String, UploadResponseFailure>>? = null
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class UploadResponseFailure(
|
||||
internal data class UploadResponseFailure(
|
||||
@Json(name = "status")
|
||||
val status: Int,
|
||||
|
||||
@Json(name = "errCode")
|
||||
@Json(name = "errcode")
|
||||
val errCode: String,
|
||||
|
||||
@Json(name = "message")
|
||||
|
|
|
@ -212,7 +212,9 @@ internal interface IMXCryptoStore {
|
|||
fun getLiveDeviceList(): LiveData<List<CryptoDeviceInfo>>
|
||||
|
||||
fun getMyDevicesInfo() : List<DeviceInfo>
|
||||
|
||||
fun getLiveMyDevicesInfo() : LiveData<List<DeviceInfo>>
|
||||
|
||||
fun saveMyDevicesInfo(info: List<DeviceInfo>)
|
||||
/**
|
||||
* Store the crypto algorithm for a room.
|
||||
|
@ -397,6 +399,7 @@ internal interface IMXCryptoStore {
|
|||
fun markMyMasterKeyAsLocallyTrusted(trusted: Boolean)
|
||||
|
||||
fun storePrivateKeysInfo(msk: String?, usk: String?, ssk: String?)
|
||||
fun storeMSKPrivateKey(msk: String?)
|
||||
fun storeSSKPrivateKey(ssk: String?)
|
||||
fun storeUSKPrivateKey(usk: String?)
|
||||
|
||||
|
|
|
@ -174,7 +174,11 @@ internal class RealmCryptoStore @Inject constructor(
|
|||
}
|
||||
|
||||
override fun open() {
|
||||
realmLocker = Realm.getInstance(realmConfiguration)
|
||||
synchronized(this) {
|
||||
if (realmLocker == null) {
|
||||
realmLocker = Realm.getInstance(realmConfiguration)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
|
@ -395,6 +399,14 @@ internal class RealmCryptoStore @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
override fun storeMSKPrivateKey(msk: String?) {
|
||||
doRealmTransaction(realmConfiguration) { realm ->
|
||||
realm.where<CryptoMetadataEntity>().findFirst()?.apply {
|
||||
xSignMasterPrivateKey = msk
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun storeSSKPrivateKey(ssk: String?) {
|
||||
doRealmTransaction(realmConfiguration) { realm ->
|
||||
realm.where<CryptoMetadataEntity>().findFirst()?.apply {
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
|
||||
package im.vector.matrix.android.internal.crypto.tasks
|
||||
|
||||
import im.vector.matrix.android.api.auth.data.LoginFlowTypes
|
||||
import im.vector.matrix.android.api.failure.Failure
|
||||
import im.vector.matrix.android.api.failure.toRegistrationFlowResponse
|
||||
import im.vector.matrix.android.internal.crypto.api.CryptoApi
|
||||
|
@ -31,12 +32,21 @@ import javax.inject.Inject
|
|||
|
||||
internal interface UploadSigningKeysTask : Task<UploadSigningKeysTask.Params, Unit> {
|
||||
data class Params(
|
||||
// the device keys to send.
|
||||
// the MSK
|
||||
val masterKey: CryptoCrossSigningKey,
|
||||
// the one-time keys to send.
|
||||
// the USK
|
||||
val userKey: CryptoCrossSigningKey,
|
||||
// the explicit device_id to use for upload (default is to use the same as that used during auth).
|
||||
// the SSK
|
||||
val selfSignedKey: CryptoCrossSigningKey,
|
||||
/**
|
||||
* - If null:
|
||||
* - no retry will be performed
|
||||
* - If not null, it may or may not contain a sessionId:
|
||||
* - If sessionId is null:
|
||||
* - password should not be null: the task will perform a first request to get a sessionId, and then a second one
|
||||
* - If sessionId is not null:
|
||||
* - password should not be null as well, and no retry will be performed
|
||||
*/
|
||||
val userPasswordAuth: UserPasswordAuth?
|
||||
)
|
||||
}
|
||||
|
@ -47,42 +57,41 @@ internal class DefaultUploadSigningKeysTask @Inject constructor(
|
|||
private val cryptoApi: CryptoApi,
|
||||
private val eventBus: EventBus
|
||||
) : UploadSigningKeysTask {
|
||||
|
||||
override suspend fun execute(params: UploadSigningKeysTask.Params) {
|
||||
val paramsHaveSessionId = params.userPasswordAuth?.session != null
|
||||
|
||||
val uploadQuery = UploadSigningKeysBody(
|
||||
masterKey = params.masterKey.toRest(),
|
||||
userSigningKey = params.userKey.toRest(),
|
||||
selfSigningKey = params.selfSignedKey.toRest(),
|
||||
auth = params.userPasswordAuth.takeIf { params.userPasswordAuth?.session != null }
|
||||
// If sessionId is provided, use the userPasswordAuth
|
||||
auth = params.userPasswordAuth.takeIf { paramsHaveSessionId }
|
||||
)
|
||||
try {
|
||||
// Make a first request to start user-interactive authentication
|
||||
val request = executeRequest<KeysQueryResponse>(eventBus) {
|
||||
apiCall = cryptoApi.uploadSigningKeys(uploadQuery)
|
||||
}
|
||||
if (request.failures?.isNotEmpty() == true) {
|
||||
throw UploadSigningKeys(request.failures)
|
||||
}
|
||||
return
|
||||
doRequest(uploadQuery)
|
||||
} catch (throwable: Throwable) {
|
||||
val registrationFlowResponse = throwable.toRegistrationFlowResponse()
|
||||
if (registrationFlowResponse != null
|
||||
&& params.userPasswordAuth != null
|
||||
/* Avoid infinite loop */
|
||||
&& params.userPasswordAuth.session.isNullOrEmpty()
|
||||
&& registrationFlowResponse.flows.orEmpty().any { it.stages?.contains(LoginFlowTypes.PASSWORD) == true }
|
||||
&& params.userPasswordAuth?.password != null
|
||||
&& !paramsHaveSessionId
|
||||
) {
|
||||
// Retry with authentication
|
||||
val req = executeRequest<KeysQueryResponse>(eventBus) {
|
||||
apiCall = cryptoApi.uploadSigningKeys(
|
||||
uploadQuery.copy(auth = params.userPasswordAuth.copy(session = registrationFlowResponse.session))
|
||||
)
|
||||
}
|
||||
if (req.failures?.isNotEmpty() == true) {
|
||||
throw UploadSigningKeys(req.failures)
|
||||
}
|
||||
doRequest(uploadQuery.copy(auth = params.userPasswordAuth.copy(session = registrationFlowResponse.session)))
|
||||
} else {
|
||||
// Other error
|
||||
throw throwable
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun doRequest(uploadQuery: UploadSigningKeysBody) {
|
||||
val keysQueryResponse = executeRequest<KeysQueryResponse>(eventBus) {
|
||||
apiCall = cryptoApi.uploadSigningKeys(uploadQuery)
|
||||
}
|
||||
if (keysQueryResponse.failures?.isNotEmpty() == true) {
|
||||
throw UploadSigningKeys(keysQueryResponse.failures)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,6 +23,7 @@ import im.vector.matrix.android.api.MatrixCallback
|
|||
import im.vector.matrix.android.api.session.crypto.CryptoService
|
||||
import im.vector.matrix.android.api.session.crypto.crosssigning.CrossSigningService
|
||||
import im.vector.matrix.android.api.session.crypto.crosssigning.KEYBACKUP_SECRET_SSSS_NAME
|
||||
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
|
||||
|
@ -809,6 +810,8 @@ internal class DefaultVerificationService @Inject constructor(
|
|||
?.let { vt ->
|
||||
val otherDeviceId = vt.otherDeviceId
|
||||
if (!crossSigningService.canCrossSign()) {
|
||||
outgoingGossipingRequestManager.sendSecretShareRequest(MASTER_KEY_SSSS_NAME, mapOf(userId to listOf(otherDeviceId
|
||||
?: "*")))
|
||||
outgoingGossipingRequestManager.sendSecretShareRequest(SELF_SIGNING_KEY_SSSS_NAME, mapOf(userId to listOf(otherDeviceId
|
||||
?: "*")))
|
||||
outgoingGossipingRequestManager.sendSecretShareRequest(USER_SIGNING_KEY_SSSS_NAME, mapOf(userId to listOf(otherDeviceId
|
||||
|
@ -821,7 +824,7 @@ internal class DefaultVerificationService @Inject constructor(
|
|||
}
|
||||
|
||||
private fun handleDoneReceived(senderId: String, doneReq: ValidVerificationDone) {
|
||||
Timber.v("## SAS Done receieved $doneReq")
|
||||
Timber.v("## SAS Done received $doneReq")
|
||||
val existing = getExistingTransaction(senderId, doneReq.transactionId)
|
||||
if (existing == null) {
|
||||
Timber.e("## SAS Received invalid Done request")
|
||||
|
|
|
@ -25,11 +25,11 @@ import javax.inject.Inject
|
|||
@SessionScope
|
||||
class DefaultInitialSyncProgressService @Inject constructor() : InitialSyncProgressService {
|
||||
|
||||
private var status = MutableLiveData<InitialSyncProgressService.Status>()
|
||||
private val status = MutableLiveData<InitialSyncProgressService.Status>()
|
||||
|
||||
private var rootTask: TaskInfo? = null
|
||||
|
||||
override fun getInitialSyncProgressStatus(): LiveData<InitialSyncProgressService.Status?> {
|
||||
override fun getInitialSyncProgressStatus(): LiveData<InitialSyncProgressService.Status> {
|
||||
return status
|
||||
}
|
||||
|
||||
|
@ -63,13 +63,13 @@ class DefaultInitialSyncProgressService @Inject constructor() : InitialSyncProgr
|
|||
parent?.setProgress(endedTask.offset + (endedTask.totalProgress * endedTask.parentWeight).toInt())
|
||||
}
|
||||
if (endedTask?.parent == null) {
|
||||
status.postValue(null)
|
||||
status.postValue(InitialSyncProgressService.Status.Idle)
|
||||
}
|
||||
}
|
||||
|
||||
fun endAll() {
|
||||
rootTask = null
|
||||
status.postValue(null)
|
||||
status.postValue(InitialSyncProgressService.Status.Idle)
|
||||
}
|
||||
|
||||
private inner class TaskInfo(@StringRes var nameRes: Int,
|
||||
|
@ -102,9 +102,7 @@ class DefaultInitialSyncProgressService @Inject constructor() : InitialSyncProgr
|
|||
it.setProgress(offset + parentProgress)
|
||||
} ?: run {
|
||||
Timber.v("--- ${leaf().nameRes}: $currentProgress")
|
||||
status.postValue(
|
||||
InitialSyncProgressService.Status(leaf().nameRes, currentProgress)
|
||||
)
|
||||
status.postValue(InitialSyncProgressService.Status.Progressing(leaf().nameRes, currentProgress))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -145,6 +145,7 @@ internal class DefaultSession @Inject constructor(
|
|||
override fun open() {
|
||||
assert(!isOpen)
|
||||
isOpen = true
|
||||
cryptoService.get().ensureDevice()
|
||||
uiHandler.post {
|
||||
lifecycleObservers.forEach { it.onStart() }
|
||||
}
|
||||
|
|
|
@ -51,8 +51,9 @@ internal class SyncResponseHandler @Inject constructor(@SessionDatabase private
|
|||
measureTimeMillis {
|
||||
if (!cryptoService.isStarted()) {
|
||||
Timber.v("Should start cryptoService")
|
||||
cryptoService.start(isInitialSync)
|
||||
cryptoService.start()
|
||||
}
|
||||
cryptoService.onSyncWillProcess(isInitialSync)
|
||||
}.also {
|
||||
Timber.v("Finish handling start cryptoService in $it ms")
|
||||
}
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
* 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.core.utils
|
||||
|
||||
import org.amshove.kluent.shouldBe
|
||||
import org.junit.Test
|
||||
import java.lang.Thread.sleep
|
||||
|
||||
class TemporaryStoreTest {
|
||||
|
||||
@Test
|
||||
fun testTemporaryStore() {
|
||||
// Keep the data 30 millis
|
||||
val store = TemporaryStore<String>(30)
|
||||
|
||||
store.data = "test"
|
||||
store.data shouldBe "test"
|
||||
sleep(15)
|
||||
store.data shouldBe "test"
|
||||
sleep(20)
|
||||
store.data shouldBe null
|
||||
}
|
||||
}
|
|
@ -32,6 +32,7 @@ import im.vector.riotx.features.crypto.recover.BootstrapConfirmPassphraseFragmen
|
|||
import im.vector.riotx.features.crypto.recover.BootstrapEnterPassphraseFragment
|
||||
import im.vector.riotx.features.crypto.recover.BootstrapMigrateBackupFragment
|
||||
import im.vector.riotx.features.crypto.recover.BootstrapSaveRecoveryKeyFragment
|
||||
import im.vector.riotx.features.crypto.recover.BootstrapSetupRecoveryKeyFragment
|
||||
import im.vector.riotx.features.crypto.recover.BootstrapWaitingFragment
|
||||
import im.vector.riotx.features.crypto.verification.cancel.VerificationCancelFragment
|
||||
import im.vector.riotx.features.crypto.verification.cancel.VerificationNotMeFragment
|
||||
|
@ -468,6 +469,11 @@ interface FragmentModule {
|
|||
@FragmentKey(BootstrapWaitingFragment::class)
|
||||
fun bindBootstrapWaitingFragment(fragment: BootstrapWaitingFragment): Fragment
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@FragmentKey(BootstrapSetupRecoveryKeyFragment::class)
|
||||
fun bindBootstrapSetupRecoveryKeyFragment(fragment: BootstrapSetupRecoveryKeyFragment): Fragment
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@FragmentKey(BootstrapSaveRecoveryKeyFragment::class)
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
* 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.core.utils
|
||||
|
||||
import java.util.Timer
|
||||
import java.util.TimerTask
|
||||
|
||||
const val THREE_MINUTES = 3 * 60_000L
|
||||
|
||||
/**
|
||||
* Store an object T for a specific period of time
|
||||
*/
|
||||
open class TemporaryStore<T>(private val delay: Long = THREE_MINUTES) {
|
||||
|
||||
private var timer: Timer? = null
|
||||
|
||||
var data: T? = null
|
||||
set(value) {
|
||||
field = value
|
||||
timer?.cancel()
|
||||
timer = Timer().also {
|
||||
it.schedule(object : TimerTask() {
|
||||
override fun run() {
|
||||
field = null
|
||||
}
|
||||
}, delay)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -44,11 +44,11 @@ class BackupToQuadSMigrationTask @Inject constructor(
|
|||
|
||||
sealed class Result {
|
||||
object Success : Result()
|
||||
abstract class Failure(val error: String?) : Result()
|
||||
abstract class Failure(val throwable: Throwable?) : Result()
|
||||
object InvalidRecoverySecret : Failure(null)
|
||||
object NoKeyBackupVersion : Failure(null)
|
||||
object IllegalParams : Failure(null)
|
||||
class ErrorFailure(throwable: Throwable) : Failure(throwable.localizedMessage)
|
||||
class ErrorFailure(throwable: Throwable) : Failure(throwable)
|
||||
}
|
||||
|
||||
data class Params(
|
||||
|
@ -97,7 +97,7 @@ class BackupToQuadSMigrationTask @Inject constructor(
|
|||
when {
|
||||
params.passphrase?.isNotEmpty() == true -> {
|
||||
reportProgress(params, R.string.bootstrap_progress_generating_ssss)
|
||||
awaitCallback {
|
||||
awaitCallback<SsssKeyCreationInfo> {
|
||||
quadS.generateKeyWithPassphrase(
|
||||
UUID.randomUUID().toString(),
|
||||
"ssss_key",
|
||||
|
|
|
@ -16,7 +16,6 @@
|
|||
|
||||
package im.vector.riotx.features.crypto.recover
|
||||
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.UserPasswordAuth
|
||||
import im.vector.riotx.core.platform.VectorViewModelAction
|
||||
import java.io.OutputStream
|
||||
|
||||
|
@ -29,8 +28,12 @@ sealed class BootstrapActions : VectorViewModelAction {
|
|||
object GoToCompleted : BootstrapActions()
|
||||
object GoToEnterAccountPassword : BootstrapActions()
|
||||
|
||||
data class DoInitialize(val passphrase: String, val auth: UserPasswordAuth? = null) : BootstrapActions()
|
||||
data class DoInitializeGeneratedKey(val auth: UserPasswordAuth? = null) : BootstrapActions()
|
||||
data class Start(val userWantsToEnterPassphrase: Boolean) : BootstrapActions()
|
||||
|
||||
object StartKeyBackupMigration : BootstrapActions()
|
||||
|
||||
data class DoInitialize(val passphrase: String) : BootstrapActions()
|
||||
object DoInitializeGeneratedKey : BootstrapActions()
|
||||
object TogglePasswordVisibility : BootstrapActions()
|
||||
data class UpdateCandidatePassphrase(val pass: String) : BootstrapActions()
|
||||
data class UpdateConfirmCandidatePassphrase(val pass: String) : BootstrapActions()
|
||||
|
|
|
@ -26,6 +26,7 @@ import android.view.ViewGroup
|
|||
import android.view.WindowManager
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import com.airbnb.mvrx.fragmentViewModel
|
||||
|
@ -44,7 +45,7 @@ class BootstrapBottomSheet : VectorBaseBottomSheetDialogFragment() {
|
|||
|
||||
@Parcelize
|
||||
data class Args(
|
||||
val isNewAccount: Boolean
|
||||
val initCrossSigningOnly: Boolean
|
||||
) : Parcelable
|
||||
|
||||
override val showExpanded = true
|
||||
|
@ -76,24 +77,17 @@ class BootstrapBottomSheet : VectorBaseBottomSheetDialogFragment() {
|
|||
KeepItSafeDialog().show(requireActivity())
|
||||
}
|
||||
is BootstrapViewEvents.SkipBootstrap -> {
|
||||
promptSkip(event.genKeyOption)
|
||||
promptSkip()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun promptSkip(genKeyOption: Boolean) {
|
||||
private fun promptSkip() {
|
||||
AlertDialog.Builder(requireContext())
|
||||
.setTitle(R.string.are_you_sure)
|
||||
.setMessage(if (genKeyOption) R.string.bootstrap_skip_text else R.string.bootstrap_skip_text_no_gen_key)
|
||||
.setMessage(R.string.bootstrap_cancel_text)
|
||||
.setPositiveButton(R.string._continue, null)
|
||||
.apply {
|
||||
if (genKeyOption) {
|
||||
setNeutralButton(R.string.generate_message_key) { _, _ ->
|
||||
viewModel.handle(BootstrapActions.DoInitializeGeneratedKey())
|
||||
}
|
||||
}
|
||||
}
|
||||
.setNegativeButton(R.string.skip) { _, _ ->
|
||||
dismiss()
|
||||
}
|
||||
|
@ -120,49 +114,57 @@ class BootstrapBottomSheet : VectorBaseBottomSheetDialogFragment() {
|
|||
}
|
||||
|
||||
override fun invalidate() = withState(viewModel) { state ->
|
||||
|
||||
when (state.step) {
|
||||
is BootstrapStep.CheckingMigration -> {
|
||||
bootstrapIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.ic_message_password))
|
||||
bootstrapTitleText.text = getString(R.string.upgrade_security)
|
||||
bootstrapIcon.isVisible = false
|
||||
bootstrapTitleText.text = getString(R.string.bottom_sheet_setup_secure_backup_title)
|
||||
showFragment(BootstrapWaitingFragment::class, Bundle())
|
||||
}
|
||||
is BootstrapStep.FirstForm -> {
|
||||
bootstrapIcon.isVisible = false
|
||||
bootstrapTitleText.text = getString(R.string.bottom_sheet_setup_secure_backup_title)
|
||||
showFragment(BootstrapSetupRecoveryKeyFragment::class, Bundle())
|
||||
}
|
||||
is BootstrapStep.SetupPassphrase -> {
|
||||
bootstrapIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.ic_message_password))
|
||||
bootstrapTitleText.text = getString(R.string.set_recovery_passphrase, getString(R.string.recovery_passphrase))
|
||||
bootstrapIcon.isVisible = true
|
||||
bootstrapIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.ic_security_phrase_24dp))
|
||||
bootstrapTitleText.text = getString(R.string.set_a_security_phrase_title)
|
||||
showFragment(BootstrapEnterPassphraseFragment::class, Bundle())
|
||||
}
|
||||
is BootstrapStep.ConfirmPassphrase -> {
|
||||
bootstrapIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.ic_message_password))
|
||||
bootstrapTitleText.text = getString(R.string.confirm_recovery_passphrase, getString(R.string.recovery_passphrase))
|
||||
bootstrapIcon.isVisible = true
|
||||
bootstrapIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.ic_security_phrase_24dp))
|
||||
bootstrapTitleText.text = getString(R.string.set_a_security_phrase_title)
|
||||
showFragment(BootstrapConfirmPassphraseFragment::class, Bundle())
|
||||
}
|
||||
is BootstrapStep.AccountPassword -> {
|
||||
bootstrapIcon.isVisible = true
|
||||
bootstrapIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.ic_user))
|
||||
bootstrapTitleText.text = getString(R.string.account_password)
|
||||
showFragment(BootstrapAccountPasswordFragment::class, Bundle())
|
||||
}
|
||||
is BootstrapStep.Initializing -> {
|
||||
bootstrapIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.ic_message_key))
|
||||
bootstrapIcon.isVisible = true
|
||||
bootstrapIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.ic_security_key_24dp))
|
||||
bootstrapTitleText.text = getString(R.string.bootstrap_loading_title)
|
||||
showFragment(BootstrapWaitingFragment::class, Bundle())
|
||||
}
|
||||
is BootstrapStep.SaveRecoveryKey -> {
|
||||
bootstrapIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.ic_message_key))
|
||||
bootstrapTitleText.text = getString(R.string.keys_backup_setup_step3_please_make_copy)
|
||||
bootstrapIcon.isVisible = true
|
||||
bootstrapIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.ic_security_key_24dp))
|
||||
bootstrapTitleText.text = getString(R.string.bottom_sheet_save_your_recovery_key_title)
|
||||
showFragment(BootstrapSaveRecoveryKeyFragment::class, Bundle())
|
||||
}
|
||||
is BootstrapStep.DoneSuccess -> {
|
||||
bootstrapIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.ic_message_key))
|
||||
bootstrapIcon.isVisible = true
|
||||
bootstrapIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.ic_security_key_24dp))
|
||||
bootstrapTitleText.text = getString(R.string.bootstrap_finish_title)
|
||||
showFragment(BootstrapConclusionFragment::class, Bundle())
|
||||
}
|
||||
is BootstrapStep.GetBackupSecretForMigration -> {
|
||||
val isKey = when (state.step) {
|
||||
is BootstrapStep.GetBackupSecretPassForMigration -> state.step.useKey
|
||||
else -> true
|
||||
}
|
||||
val drawableRes = if (isKey) R.drawable.ic_message_key else R.drawable.ic_message_password
|
||||
val isKey = state.step.useKey()
|
||||
val drawableRes = if (isKey) R.drawable.ic_security_key_24dp else R.drawable.ic_security_phrase_24dp
|
||||
bootstrapIcon.isVisible = true
|
||||
bootstrapIcon.setImageDrawable(ContextCompat.getDrawable(
|
||||
requireContext(),
|
||||
drawableRes)
|
||||
|
@ -178,10 +180,10 @@ class BootstrapBottomSheet : VectorBaseBottomSheetDialogFragment() {
|
|||
|
||||
const val EXTRA_ARGS = "EXTRA_ARGS"
|
||||
|
||||
fun show(fragmentManager: FragmentManager, isAccountCreation: Boolean) {
|
||||
fun show(fragmentManager: FragmentManager, initCrossSigningOnly: Boolean) {
|
||||
BootstrapBottomSheet().apply {
|
||||
isCancelable = false
|
||||
arguments = Bundle().apply { this.putParcelable(EXTRA_ARGS, Args(isAccountCreation)) }
|
||||
arguments = Bundle().apply { this.putParcelable(EXTRA_ARGS, Args(initCrossSigningOnly)) }
|
||||
}.show(fragmentManager, "BootstrapBottomSheet")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,7 +19,6 @@ package im.vector.riotx.features.crypto.recover
|
|||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import androidx.core.text.toSpannable
|
||||
import androidx.core.view.isGone
|
||||
import com.airbnb.mvrx.parentFragmentViewModel
|
||||
import com.airbnb.mvrx.withState
|
||||
|
@ -29,16 +28,12 @@ import im.vector.riotx.R
|
|||
import im.vector.riotx.core.extensions.hideKeyboard
|
||||
import im.vector.riotx.core.extensions.showPassword
|
||||
import im.vector.riotx.core.platform.VectorBaseFragment
|
||||
import im.vector.riotx.core.resources.ColorProvider
|
||||
import im.vector.riotx.core.utils.colorizeMatchingText
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||
import kotlinx.android.synthetic.main.fragment_bootstrap_enter_passphrase.*
|
||||
import java.util.concurrent.TimeUnit
|
||||
import javax.inject.Inject
|
||||
|
||||
class BootstrapConfirmPassphraseFragment @Inject constructor(
|
||||
private val colorProvider: ColorProvider
|
||||
) : VectorBaseFragment() {
|
||||
class BootstrapConfirmPassphraseFragment @Inject constructor() : VectorBaseFragment() {
|
||||
|
||||
override fun getLayoutResId() = R.layout.fragment_bootstrap_enter_passphrase
|
||||
|
||||
|
@ -49,12 +44,8 @@ class BootstrapConfirmPassphraseFragment @Inject constructor(
|
|||
|
||||
ssss_passphrase_security_progress.isGone = true
|
||||
|
||||
val recPassPhrase = getString(R.string.recovery_passphrase)
|
||||
bootstrapDescriptionText.text = getString(R.string.bootstrap_info_confirm_text, recPassPhrase)
|
||||
.toSpannable()
|
||||
.colorizeMatchingText(recPassPhrase, colorProvider.getColorFromAttribute(android.R.attr.textColorLink))
|
||||
|
||||
ssss_passphrase_enter_edittext.hint = getString(R.string.passphrase_confirm_passphrase)
|
||||
bootstrapDescriptionText.text = getString(R.string.set_a_security_phrase_again_notice)
|
||||
ssss_passphrase_enter_edittext.hint = getString(R.string.set_a_security_phrase_hint)
|
||||
|
||||
withState(sharedViewModel) {
|
||||
// set initial value (useful when coming back)
|
||||
|
|
|
@ -40,12 +40,14 @@ import im.vector.riotx.core.platform.ViewModelTask
|
|||
import im.vector.riotx.core.platform.WaitingViewData
|
||||
import im.vector.riotx.core.resources.StringProvider
|
||||
import timber.log.Timber
|
||||
import java.lang.IllegalArgumentException
|
||||
import java.util.UUID
|
||||
import javax.inject.Inject
|
||||
|
||||
sealed class BootstrapResult {
|
||||
|
||||
data class Success(val keyInfo: SsssKeyCreationInfo) : BootstrapResult()
|
||||
object SuccessCrossSigningOnly : BootstrapResult()
|
||||
|
||||
abstract class Failure(val error: String?) : BootstrapResult()
|
||||
|
||||
|
@ -58,7 +60,7 @@ sealed class BootstrapResult {
|
|||
class FailedToStorePrivateKeyInSSSS(failure: Throwable) : Failure(failure.localizedMessage)
|
||||
object MissingPrivateKey : Failure(null)
|
||||
|
||||
data class PasswordAuthFlowMissing(val sessionId: String, val userId: String) : Failure(null)
|
||||
data class PasswordAuthFlowMissing(val sessionId: String) : Failure(null)
|
||||
}
|
||||
|
||||
interface BootstrapProgressListener {
|
||||
|
@ -67,31 +69,45 @@ interface BootstrapProgressListener {
|
|||
|
||||
data class Params(
|
||||
val userPasswordAuth: UserPasswordAuth? = null,
|
||||
val initOnlyCrossSigning: Boolean = false,
|
||||
val progressListener: BootstrapProgressListener? = null,
|
||||
val passphrase: String?,
|
||||
val keySpec: SsssKeySpec? = null
|
||||
)
|
||||
|
||||
// TODO Rename to CreateServerRecovery
|
||||
class BootstrapCrossSigningTask @Inject constructor(
|
||||
private val session: Session,
|
||||
private val stringProvider: StringProvider
|
||||
) : ViewModelTask<Params, BootstrapResult> {
|
||||
|
||||
override suspend fun execute(params: Params): BootstrapResult {
|
||||
params.progressListener?.onProgress(
|
||||
WaitingViewData(
|
||||
stringProvider.getString(R.string.bootstrap_crosssigning_progress_initializing),
|
||||
isIndeterminate = true
|
||||
)
|
||||
)
|
||||
val crossSigningService = session.cryptoService().crossSigningService()
|
||||
|
||||
try {
|
||||
awaitCallback<Unit> {
|
||||
crossSigningService.initializeCrossSigning(params.userPasswordAuth, it)
|
||||
// Ensure cross-signing is initialized. Due to migration it is maybe not always correctly initialized
|
||||
if (!crossSigningService.isCrossSigningInitialized()) {
|
||||
params.progressListener?.onProgress(
|
||||
WaitingViewData(
|
||||
stringProvider.getString(R.string.bootstrap_crosssigning_progress_initializing),
|
||||
isIndeterminate = true
|
||||
)
|
||||
)
|
||||
|
||||
try {
|
||||
awaitCallback<Unit> {
|
||||
crossSigningService.initializeCrossSigning(params.userPasswordAuth, it)
|
||||
}
|
||||
if (params.initOnlyCrossSigning) {
|
||||
return BootstrapResult.SuccessCrossSigningOnly
|
||||
}
|
||||
} catch (failure: Throwable) {
|
||||
return handleInitializeXSigningError(failure)
|
||||
}
|
||||
} else {
|
||||
// not sure how this can happen??
|
||||
if (params.initOnlyCrossSigning) {
|
||||
return handleInitializeXSigningError(IllegalArgumentException("Cross signing already setup"))
|
||||
}
|
||||
} catch (failure: Throwable) {
|
||||
return handleInitializeXSigningError(failure)
|
||||
}
|
||||
|
||||
val keyInfo: SsssKeyCreationInfo
|
||||
|
@ -232,9 +248,11 @@ class BootstrapCrossSigningTask @Inject constructor(
|
|||
} else {
|
||||
val registrationFlowResponse = failure.toRegistrationFlowResponse()
|
||||
if (registrationFlowResponse != null) {
|
||||
if (registrationFlowResponse.flows?.any { it.stages?.contains(LoginFlowTypes.PASSWORD) == true } != true) {
|
||||
return if (registrationFlowResponse.flows.orEmpty().any { it.stages?.contains(LoginFlowTypes.PASSWORD) == true }) {
|
||||
BootstrapResult.PasswordAuthFlowMissing(registrationFlowResponse.session ?: "")
|
||||
} else {
|
||||
// can't do this from here
|
||||
return BootstrapResult.UnsupportedAuthFlow()
|
||||
BootstrapResult.UnsupportedAuthFlow()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,7 +19,6 @@ package im.vector.riotx.features.crypto.recover
|
|||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import androidx.core.text.toSpannable
|
||||
import com.airbnb.mvrx.parentFragmentViewModel
|
||||
import com.airbnb.mvrx.withState
|
||||
import com.jakewharton.rxbinding3.widget.editorActionEvents
|
||||
|
@ -27,17 +26,13 @@ import com.jakewharton.rxbinding3.widget.textChanges
|
|||
import im.vector.riotx.R
|
||||
import im.vector.riotx.core.extensions.showPassword
|
||||
import im.vector.riotx.core.platform.VectorBaseFragment
|
||||
import im.vector.riotx.core.resources.ColorProvider
|
||||
import im.vector.riotx.core.utils.colorizeMatchingText
|
||||
import im.vector.riotx.features.settings.VectorLocale
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||
import kotlinx.android.synthetic.main.fragment_bootstrap_enter_passphrase.*
|
||||
import java.util.concurrent.TimeUnit
|
||||
import javax.inject.Inject
|
||||
|
||||
class BootstrapEnterPassphraseFragment @Inject constructor(
|
||||
private val colorProvider: ColorProvider
|
||||
) : VectorBaseFragment() {
|
||||
class BootstrapEnterPassphraseFragment @Inject constructor() : VectorBaseFragment() {
|
||||
|
||||
override fun getLayoutResId() = R.layout.fragment_bootstrap_enter_passphrase
|
||||
|
||||
|
@ -46,12 +41,9 @@ class BootstrapEnterPassphraseFragment @Inject constructor(
|
|||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
val recPassPhrase = getString(R.string.recovery_passphrase)
|
||||
bootstrapDescriptionText.text = getString(R.string.bootstrap_info_text, recPassPhrase)
|
||||
.toSpannable()
|
||||
.colorizeMatchingText(recPassPhrase, colorProvider.getColorFromAttribute(android.R.attr.textColorLink))
|
||||
bootstrapDescriptionText.text = getString(R.string.set_a_security_phrase_notice)
|
||||
ssss_passphrase_enter_edittext.hint = getString(R.string.set_a_security_phrase_hint)
|
||||
|
||||
ssss_passphrase_enter_edittext.hint = getString(R.string.passphrase_enter_passphrase)
|
||||
withState(sharedViewModel) {
|
||||
// set initial value (useful when coming back)
|
||||
ssss_passphrase_enter_edittext.setText(it.passphrase ?: "")
|
||||
|
|
|
@ -86,17 +86,12 @@ class BootstrapMigrateBackupFragment @Inject constructor(
|
|||
}
|
||||
|
||||
private fun submit() = withState(sharedViewModel) { state ->
|
||||
if (state.step !is BootstrapStep.GetBackupSecretForMigration) {
|
||||
return@withState
|
||||
}
|
||||
val isEnteringKey =
|
||||
when (state.step) {
|
||||
is BootstrapStep.GetBackupSecretPassForMigration -> state.step.useKey
|
||||
else -> true
|
||||
}
|
||||
val getBackupSecretForMigration = state.step as? BootstrapStep.GetBackupSecretForMigration ?: return@withState
|
||||
|
||||
val isEnteringKey = getBackupSecretForMigration.useKey()
|
||||
|
||||
val secret = bootstrapMigrateEditText.text?.toString()
|
||||
if (secret.isNullOrBlank()) {
|
||||
if (secret.isNullOrEmpty()) {
|
||||
val errRes = if (isEnteringKey) R.string.recovery_key_empty_error_message else R.string.passphrase_empty_error_message
|
||||
bootstrapRecoveryKeyEnterTil.error = getString(errRes)
|
||||
} else if (isEnteringKey && !isValidRecoveryKey(secret)) {
|
||||
|
@ -112,15 +107,9 @@ class BootstrapMigrateBackupFragment @Inject constructor(
|
|||
}
|
||||
|
||||
override fun invalidate() = withState(sharedViewModel) { state ->
|
||||
if (state.step !is BootstrapStep.GetBackupSecretForMigration) {
|
||||
return@withState
|
||||
}
|
||||
val getBackupSecretForMigration = state.step as? BootstrapStep.GetBackupSecretForMigration ?: return@withState
|
||||
|
||||
val isEnteringKey =
|
||||
when (state.step) {
|
||||
is BootstrapStep.GetBackupSecretPassForMigration -> state.step.useKey
|
||||
else -> true
|
||||
}
|
||||
val isEnteringKey = getBackupSecretForMigration.useKey()
|
||||
|
||||
if (isEnteringKey) {
|
||||
bootstrapMigrateShowPassword.isVisible = false
|
||||
|
@ -128,8 +117,6 @@ class BootstrapMigrateBackupFragment @Inject constructor(
|
|||
|
||||
val recKey = getString(R.string.bootstrap_migration_backup_recovery_key)
|
||||
bootstrapDescriptionText.text = getString(R.string.enter_account_password, recKey)
|
||||
.toSpannable()
|
||||
.colorizeMatchingText(recKey, colorProvider.getColorFromAttribute(android.R.attr.textColorLink))
|
||||
|
||||
bootstrapMigrateEditText.hint = recKey
|
||||
|
||||
|
|
|
@ -21,14 +21,12 @@ import android.content.ActivityNotFoundException
|
|||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.core.text.toSpannable
|
||||
import androidx.core.view.isVisible
|
||||
import com.airbnb.mvrx.parentFragmentViewModel
|
||||
import com.airbnb.mvrx.withState
|
||||
import im.vector.riotx.R
|
||||
import im.vector.riotx.core.platform.VectorBaseFragment
|
||||
import im.vector.riotx.core.resources.ColorProvider
|
||||
import im.vector.riotx.core.utils.colorizeMatchingText
|
||||
import im.vector.riotx.core.utils.startSharePlainTextIntent
|
||||
import im.vector.riotx.core.utils.toast
|
||||
import kotlinx.android.synthetic.main.fragment_bootstrap_save_key.*
|
||||
|
@ -48,17 +46,14 @@ class BootstrapSaveRecoveryKeyFragment @Inject constructor(
|
|||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
val messageKey = getString(R.string.message_key)
|
||||
val recoveryPassphrase = getString(R.string.recovery_passphrase)
|
||||
val color = colorProvider.getColorFromAttribute(R.attr.vctr_toolbar_link_text_color)
|
||||
bootstrapSaveText.text = getString(R.string.bootstrap_save_key_description, messageKey, recoveryPassphrase)
|
||||
.toSpannable()
|
||||
.colorizeMatchingText(messageKey, color)
|
||||
.colorizeMatchingText(recoveryPassphrase, color)
|
||||
|
||||
recoverySave.clickableView.debouncedClicks { downloadRecoveryKey() }
|
||||
recoveryCopy.clickableView.debouncedClicks { shareRecoveryKey() }
|
||||
recoveryContinue.clickableView.debouncedClicks { sharedViewModel.handle(BootstrapActions.GoToCompleted) }
|
||||
recoveryContinue.clickableView.debouncedClicks {
|
||||
// We do not display the final Fragment anymore
|
||||
// TODO Do some cleanup
|
||||
// sharedViewModel.handle(BootstrapActions.GoToCompleted)
|
||||
sharedViewModel.handle(BootstrapActions.Completed)
|
||||
}
|
||||
}
|
||||
|
||||
private fun downloadRecoveryKey() = withState(sharedViewModel) { _ ->
|
||||
|
|
|
@ -0,0 +1,69 @@
|
|||
/*
|
||||
* 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.recover
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.core.view.isVisible
|
||||
import com.airbnb.mvrx.parentFragmentViewModel
|
||||
import com.airbnb.mvrx.withState
|
||||
import im.vector.riotx.R
|
||||
import im.vector.riotx.core.platform.VectorBaseFragment
|
||||
import kotlinx.android.synthetic.main.fragment_bootstrap_setup_recovery.*
|
||||
import javax.inject.Inject
|
||||
|
||||
class BootstrapSetupRecoveryKeyFragment @Inject constructor() : VectorBaseFragment() {
|
||||
|
||||
override fun getLayoutResId() = R.layout.fragment_bootstrap_setup_recovery
|
||||
|
||||
val sharedViewModel: BootstrapSharedViewModel by parentFragmentViewModel()
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
// Actions when a key backup exist
|
||||
bootstrapSetupSecureSubmit.clickableView.debouncedClicks {
|
||||
sharedViewModel.handle(BootstrapActions.StartKeyBackupMigration)
|
||||
}
|
||||
|
||||
// Actions when there is no key backup
|
||||
bootstrapSetupSecureUseSecurityKey.clickableView.debouncedClicks {
|
||||
sharedViewModel.handle(BootstrapActions.Start(userWantsToEnterPassphrase = false))
|
||||
}
|
||||
bootstrapSetupSecureUseSecurityPassphrase.clickableView.debouncedClicks {
|
||||
sharedViewModel.handle(BootstrapActions.Start(userWantsToEnterPassphrase = true))
|
||||
}
|
||||
}
|
||||
|
||||
override fun invalidate() = withState(sharedViewModel) { state ->
|
||||
if (state.step is BootstrapStep.FirstForm) {
|
||||
if (state.step.keyBackUpExist) {
|
||||
// Display the set up action
|
||||
bootstrapSetupSecureSubmit.isVisible = true
|
||||
bootstrapSetupSecureUseSecurityKey.isVisible = false
|
||||
bootstrapSetupSecureUseSecurityPassphrase.isVisible = false
|
||||
bootstrapSetupSecureUseSecurityPassphraseSeparator.isVisible = false
|
||||
} else {
|
||||
// Choose between create a passphrase or use a recovery key
|
||||
bootstrapSetupSecureSubmit.isVisible = false
|
||||
bootstrapSetupSecureUseSecurityKey.isVisible = true
|
||||
bootstrapSetupSecureUseSecurityPassphrase.isVisible = true
|
||||
bootstrapSetupSecureUseSecurityPassphraseSeparator.isVisible = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -17,27 +17,25 @@
|
|||
package im.vector.riotx.features.crypto.recover
|
||||
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.airbnb.mvrx.Async
|
||||
import com.airbnb.mvrx.Fail
|
||||
import com.airbnb.mvrx.FragmentViewModelContext
|
||||
import com.airbnb.mvrx.Loading
|
||||
import com.airbnb.mvrx.MvRxState
|
||||
import com.airbnb.mvrx.MvRxViewModelFactory
|
||||
import com.airbnb.mvrx.Success
|
||||
import com.airbnb.mvrx.Uninitialized
|
||||
import com.airbnb.mvrx.ViewModelContext
|
||||
import com.nulabinc.zxcvbn.Strength
|
||||
import com.nulabinc.zxcvbn.Zxcvbn
|
||||
import com.squareup.inject.assisted.Assisted
|
||||
import com.squareup.inject.assisted.AssistedInject
|
||||
import im.vector.matrix.android.api.failure.Failure
|
||||
import im.vector.matrix.android.api.session.Session
|
||||
import im.vector.matrix.android.api.session.securestorage.RawBytesKeySpec
|
||||
import im.vector.matrix.android.api.session.securestorage.SsssKeyCreationInfo
|
||||
import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.KeysVersionResult
|
||||
import im.vector.matrix.android.internal.crypto.keysbackup.util.extractCurveKeyFromRecoveryKey
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.UserPasswordAuth
|
||||
import im.vector.matrix.android.internal.util.awaitCallback
|
||||
import im.vector.riotx.R
|
||||
import im.vector.riotx.core.error.ErrorFormatter
|
||||
import im.vector.riotx.core.extensions.exhaustive
|
||||
import im.vector.riotx.core.platform.VectorViewModel
|
||||
import im.vector.riotx.core.platform.WaitingViewData
|
||||
|
@ -47,30 +45,19 @@ import kotlinx.coroutines.Dispatchers
|
|||
import kotlinx.coroutines.launch
|
||||
import java.io.OutputStream
|
||||
|
||||
data class BootstrapViewState(
|
||||
val step: BootstrapStep = BootstrapStep.SetupPassphrase(false),
|
||||
val passphrase: String? = null,
|
||||
val migrationRecoveryKey: String? = null,
|
||||
val passphraseRepeat: String? = null,
|
||||
val crossSigningInitialization: Async<Unit> = Uninitialized,
|
||||
val passphraseStrength: Async<Strength> = Uninitialized,
|
||||
val passphraseConfirmMatch: Async<Unit> = Uninitialized,
|
||||
val recoveryKeyCreationInfo: SsssKeyCreationInfo? = null,
|
||||
val initializationWaitingViewData: WaitingViewData? = null,
|
||||
val currentReAuth: UserPasswordAuth? = null,
|
||||
val recoverySaveFileProcess: Async<Unit> = Uninitialized
|
||||
) : MvRxState
|
||||
|
||||
class BootstrapSharedViewModel @AssistedInject constructor(
|
||||
@Assisted initialState: BootstrapViewState,
|
||||
@Assisted val args: BootstrapBottomSheet.Args,
|
||||
private val stringProvider: StringProvider,
|
||||
private val errorFormatter: ErrorFormatter,
|
||||
private val session: Session,
|
||||
private val bootstrapTask: BootstrapCrossSigningTask,
|
||||
private val migrationTask: BackupToQuadSMigrationTask,
|
||||
private val reAuthHelper: ReAuthHelper
|
||||
) : VectorViewModel<BootstrapViewState, BootstrapActions, BootstrapViewEvents>(initialState) {
|
||||
|
||||
private var doesKeyBackupExist: Boolean = false
|
||||
private var isBackupCreatedFromPassphrase: Boolean = false
|
||||
private val zxcvbn = Zxcvbn()
|
||||
|
||||
@AssistedInject.Factory
|
||||
|
@ -78,13 +65,17 @@ class BootstrapSharedViewModel @AssistedInject constructor(
|
|||
fun create(initialState: BootstrapViewState, args: BootstrapBottomSheet.Args): BootstrapSharedViewModel
|
||||
}
|
||||
|
||||
private var _pendingSession: String? = null
|
||||
|
||||
init {
|
||||
// need to check if user have an existing keybackup
|
||||
if (args.isNewAccount) {
|
||||
|
||||
if (args.initCrossSigningOnly) {
|
||||
// Go straight to account password
|
||||
setState {
|
||||
copy(step = BootstrapStep.SetupPassphrase(false))
|
||||
copy(step = BootstrapStep.AccountPassword(false))
|
||||
}
|
||||
} else {
|
||||
// need to check if user have an existing keybackup
|
||||
setState {
|
||||
copy(step = BootstrapStep.CheckingMigration)
|
||||
}
|
||||
|
@ -96,8 +87,9 @@ class BootstrapSharedViewModel @AssistedInject constructor(
|
|||
}
|
||||
if (version == null) {
|
||||
// we just resume plain bootstrap
|
||||
doesKeyBackupExist = false
|
||||
setState {
|
||||
copy(step = BootstrapStep.SetupPassphrase(false))
|
||||
copy(step = BootstrapStep.FirstForm(keyBackUpExist = doesKeyBackupExist))
|
||||
}
|
||||
} else {
|
||||
// we need to get existing backup passphrase/key and convert to SSSS
|
||||
|
@ -108,15 +100,10 @@ class BootstrapSharedViewModel @AssistedInject constructor(
|
|||
// strange case... just finish?
|
||||
_viewEvents.post(BootstrapViewEvents.Dismiss)
|
||||
} else {
|
||||
val isBackupCreatedFromPassphrase = keyVersion.getAuthDataAsMegolmBackupAuthData()?.privateKeySalt != null
|
||||
if (isBackupCreatedFromPassphrase) {
|
||||
setState {
|
||||
copy(step = BootstrapStep.GetBackupSecretPassForMigration(isPasswordVisible = false, useKey = false))
|
||||
}
|
||||
} else {
|
||||
setState {
|
||||
copy(step = BootstrapStep.GetBackupSecretKeyForMigration)
|
||||
}
|
||||
doesKeyBackupExist = true
|
||||
isBackupCreatedFromPassphrase = keyVersion.getAuthDataAsMegolmBackupAuthData()?.privateKeySalt != null
|
||||
setState {
|
||||
copy(step = BootstrapStep.FirstForm(keyBackUpExist = doesKeyBackupExist))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -124,6 +111,18 @@ class BootstrapSharedViewModel @AssistedInject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun handleStartMigratingKeyBackup() {
|
||||
if (isBackupCreatedFromPassphrase) {
|
||||
setState {
|
||||
copy(step = BootstrapStep.GetBackupSecretPassForMigration(isPasswordVisible = false, useKey = false))
|
||||
}
|
||||
} else {
|
||||
setState {
|
||||
copy(step = BootstrapStep.GetBackupSecretKeyForMigration)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun handle(action: BootstrapActions) = withState { state ->
|
||||
when (action) {
|
||||
is BootstrapActions.GoBack -> queryBack()
|
||||
|
@ -149,11 +148,15 @@ class BootstrapSharedViewModel @AssistedInject constructor(
|
|||
copy(step = state.step.copy(isPasswordVisible = !state.step.isPasswordVisible))
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
}
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
BootstrapActions.StartKeyBackupMigration -> {
|
||||
handleStartMigratingKeyBackup()
|
||||
}
|
||||
is BootstrapActions.Start -> {
|
||||
handleStart(action)
|
||||
}
|
||||
is BootstrapActions.UpdateCandidatePassphrase -> {
|
||||
val strength = zxcvbn.measure(action.pass)
|
||||
setState {
|
||||
|
@ -182,15 +185,15 @@ class BootstrapSharedViewModel @AssistedInject constructor(
|
|||
}
|
||||
is BootstrapActions.DoInitialize -> {
|
||||
if (state.passphrase == state.passphraseRepeat) {
|
||||
val auth = action.auth ?: reAuthHelper.rememberedAuth()
|
||||
if (auth == null) {
|
||||
val userPassword = reAuthHelper.data
|
||||
if (userPassword == null) {
|
||||
setState {
|
||||
copy(
|
||||
step = BootstrapStep.AccountPassword(false)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
startInitializeFlow(action.auth)
|
||||
startInitializeFlow(userPassword)
|
||||
}
|
||||
} else {
|
||||
setState {
|
||||
|
@ -201,8 +204,8 @@ class BootstrapSharedViewModel @AssistedInject constructor(
|
|||
}
|
||||
}
|
||||
is BootstrapActions.DoInitializeGeneratedKey -> {
|
||||
val auth = action.auth ?: reAuthHelper.rememberedAuth()
|
||||
if (auth == null) {
|
||||
val userPassword = reAuthHelper.data
|
||||
if (userPassword == null) {
|
||||
setState {
|
||||
copy(
|
||||
passphrase = null,
|
||||
|
@ -217,7 +220,7 @@ class BootstrapSharedViewModel @AssistedInject constructor(
|
|||
passphraseRepeat = null
|
||||
)
|
||||
}
|
||||
startInitializeFlow(action.auth)
|
||||
startInitializeFlow(userPassword)
|
||||
}
|
||||
}
|
||||
BootstrapActions.RecoveryKeySaved -> {
|
||||
|
@ -260,10 +263,7 @@ class BootstrapSharedViewModel @AssistedInject constructor(
|
|||
} else return@withState
|
||||
}
|
||||
is BootstrapActions.ReAuth -> {
|
||||
startInitializeFlow(
|
||||
state.currentReAuth?.copy(password = action.pass)
|
||||
?: UserPasswordAuth(user = session.myUserId, password = action.pass)
|
||||
)
|
||||
startInitializeFlow(action.pass)
|
||||
}
|
||||
is BootstrapActions.DoMigrateWithPassphrase -> {
|
||||
startMigrationFlow(state.step, action.passphrase, null)
|
||||
|
@ -274,6 +274,18 @@ class BootstrapSharedViewModel @AssistedInject constructor(
|
|||
}.exhaustive
|
||||
}
|
||||
|
||||
private fun handleStart(action: BootstrapActions.Start) = withState {
|
||||
if (action.userWantsToEnterPassphrase) {
|
||||
setState {
|
||||
copy(
|
||||
step = BootstrapStep.SetupPassphrase(isPasswordVisible = false)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
startInitializeFlow(null)
|
||||
}
|
||||
}
|
||||
|
||||
// =======================================
|
||||
// Business Logic
|
||||
// =======================================
|
||||
|
@ -299,7 +311,7 @@ class BootstrapSharedViewModel @AssistedInject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun startMigrationFlow(prevState: BootstrapStep, passphrase: String?, recoveryKey: String?) {
|
||||
private fun startMigrationFlow(previousStep: BootstrapStep, passphrase: String?, recoveryKey: String?) { // TODO Rename param
|
||||
setState {
|
||||
copy(step = BootstrapStep.Initializing)
|
||||
}
|
||||
|
@ -314,43 +326,44 @@ class BootstrapSharedViewModel @AssistedInject constructor(
|
|||
}
|
||||
}
|
||||
migrationTask.invoke(this, BackupToQuadSMigrationTask.Params(passphrase, recoveryKey, progressListener)) {
|
||||
if (it is BackupToQuadSMigrationTask.Result.Success) {
|
||||
setState {
|
||||
copy(
|
||||
passphrase = passphrase,
|
||||
passphraseRepeat = passphrase,
|
||||
migrationRecoveryKey = recoveryKey
|
||||
)
|
||||
}
|
||||
val auth = reAuthHelper.rememberedAuth()
|
||||
if (auth == null) {
|
||||
when (it) {
|
||||
is BackupToQuadSMigrationTask.Result.Success -> {
|
||||
setState {
|
||||
copy(
|
||||
step = BootstrapStep.AccountPassword(false)
|
||||
passphrase = passphrase,
|
||||
passphraseRepeat = passphrase,
|
||||
migrationRecoveryKey = recoveryKey
|
||||
)
|
||||
}
|
||||
} else {
|
||||
startInitializeFlow(auth)
|
||||
val userPassword = reAuthHelper.data
|
||||
if (userPassword == null) {
|
||||
setState {
|
||||
copy(
|
||||
step = BootstrapStep.AccountPassword(false)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
startInitializeFlow(userPassword)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
_viewEvents.post(
|
||||
BootstrapViewEvents.ModalError(
|
||||
(it as? BackupToQuadSMigrationTask.Result.Failure)?.error
|
||||
?: stringProvider.getString(R.string.matrix_error
|
||||
)
|
||||
)
|
||||
)
|
||||
setState {
|
||||
copy(
|
||||
step = prevState
|
||||
is BackupToQuadSMigrationTask.Result.Failure -> {
|
||||
_viewEvents.post(
|
||||
BootstrapViewEvents.ModalError(it.toHumanReadable())
|
||||
)
|
||||
setState {
|
||||
copy(
|
||||
step = previousStep
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}.exhaustive
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun startInitializeFlow(auth: UserPasswordAuth?) {
|
||||
private fun startInitializeFlow(userPassword: String?) = withState { state ->
|
||||
val previousStep = state.step
|
||||
|
||||
setState {
|
||||
copy(step = BootstrapStep.Initializing)
|
||||
}
|
||||
|
@ -365,60 +378,77 @@ class BootstrapSharedViewModel @AssistedInject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
withState { state ->
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
bootstrapTask.invoke(this, Params(
|
||||
userPasswordAuth = auth ?: reAuthHelper.rememberedAuth(),
|
||||
progressListener = progressListener,
|
||||
passphrase = state.passphrase,
|
||||
keySpec = state.migrationRecoveryKey?.let { extractCurveKeyFromRecoveryKey(it)?.let { RawBytesKeySpec(it) } }
|
||||
)) {
|
||||
when (it) {
|
||||
is BootstrapResult.Success -> {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val userPasswordAuth = userPassword?.let {
|
||||
UserPasswordAuth(
|
||||
// Note that _pendingSession may or may not be null, this is OK, it will be managed by the task
|
||||
session = _pendingSession,
|
||||
user = session.myUserId,
|
||||
password = it
|
||||
)
|
||||
}
|
||||
|
||||
bootstrapTask.invoke(this,
|
||||
Params(
|
||||
userPasswordAuth = userPasswordAuth,
|
||||
initOnlyCrossSigning = args.initCrossSigningOnly,
|
||||
progressListener = progressListener,
|
||||
passphrase = state.passphrase,
|
||||
keySpec = state.migrationRecoveryKey?.let { extractCurveKeyFromRecoveryKey(it)?.let { RawBytesKeySpec(it) } }
|
||||
)
|
||||
) { bootstrapResult ->
|
||||
when (bootstrapResult) {
|
||||
is BootstrapResult.SuccessCrossSigningOnly -> {
|
||||
// TPD
|
||||
_viewEvents.post(BootstrapViewEvents.Dismiss)
|
||||
}
|
||||
is BootstrapResult.Success -> {
|
||||
setState {
|
||||
copy(
|
||||
recoveryKeyCreationInfo = bootstrapResult.keyInfo,
|
||||
step = BootstrapStep.SaveRecoveryKey(false)
|
||||
)
|
||||
}
|
||||
}
|
||||
is BootstrapResult.PasswordAuthFlowMissing -> {
|
||||
// Ask the password to the user
|
||||
_pendingSession = bootstrapResult.sessionId
|
||||
setState {
|
||||
copy(
|
||||
step = BootstrapStep.AccountPassword(false)
|
||||
)
|
||||
}
|
||||
}
|
||||
is BootstrapResult.UnsupportedAuthFlow -> {
|
||||
_viewEvents.post(BootstrapViewEvents.ModalError(stringProvider.getString(R.string.auth_flow_not_supported)))
|
||||
_viewEvents.post(BootstrapViewEvents.Dismiss)
|
||||
}
|
||||
is BootstrapResult.InvalidPasswordError -> {
|
||||
// it's a bad password
|
||||
// We clear the auth session, to avoid 'Requested operation has changed during the UI authentication session' error
|
||||
_pendingSession = null
|
||||
setState {
|
||||
copy(
|
||||
step = BootstrapStep.AccountPassword(false, stringProvider.getString(R.string.auth_invalid_login_param))
|
||||
)
|
||||
}
|
||||
}
|
||||
is BootstrapResult.Failure -> {
|
||||
if (bootstrapResult is BootstrapResult.GenericError
|
||||
&& bootstrapResult.failure is Failure.OtherServerError
|
||||
&& bootstrapResult.failure.httpCode == 401) {
|
||||
// Ignore this error
|
||||
} else {
|
||||
_viewEvents.post(BootstrapViewEvents.ModalError(bootstrapResult.error ?: stringProvider.getString(R.string.matrix_error)))
|
||||
// Not sure
|
||||
setState {
|
||||
copy(
|
||||
recoveryKeyCreationInfo = it.keyInfo,
|
||||
step = BootstrapStep.SaveRecoveryKey(false)
|
||||
step = previousStep
|
||||
)
|
||||
}
|
||||
}
|
||||
is BootstrapResult.PasswordAuthFlowMissing -> {
|
||||
setState {
|
||||
copy(
|
||||
currentReAuth = UserPasswordAuth(session = it.sessionId, user = it.userId),
|
||||
step = BootstrapStep.AccountPassword(false)
|
||||
)
|
||||
}
|
||||
}
|
||||
is BootstrapResult.UnsupportedAuthFlow -> {
|
||||
_viewEvents.post(BootstrapViewEvents.ModalError(stringProvider.getString(R.string.auth_flow_not_supported)))
|
||||
_viewEvents.post(BootstrapViewEvents.Dismiss)
|
||||
}
|
||||
is BootstrapResult.InvalidPasswordError -> {
|
||||
// it's a bad password
|
||||
setState {
|
||||
copy(
|
||||
// We clear the auth session, to avoid 'Requested operation has changed during the UI authentication session' error
|
||||
currentReAuth = UserPasswordAuth(session = null, user = session.myUserId),
|
||||
step = BootstrapStep.AccountPassword(false, stringProvider.getString(R.string.auth_invalid_login_param))
|
||||
)
|
||||
}
|
||||
}
|
||||
is BootstrapResult.Failure -> {
|
||||
if (it is BootstrapResult.GenericError
|
||||
&& it.failure is im.vector.matrix.android.api.failure.Failure.OtherServerError
|
||||
&& it.failure.httpCode == 401) {
|
||||
} else {
|
||||
_viewEvents.post(BootstrapViewEvents.ModalError(it.error ?: stringProvider.getString(R.string.matrix_error)))
|
||||
setState {
|
||||
copy(
|
||||
step = BootstrapStep.ConfirmPassphrase(false)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}.exhaustive
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -441,22 +471,33 @@ class BootstrapSharedViewModel @AssistedInject constructor(
|
|||
)
|
||||
}
|
||||
} else {
|
||||
_viewEvents.post(BootstrapViewEvents.SkipBootstrap())
|
||||
setState {
|
||||
copy(
|
||||
step = BootstrapStep.FirstForm(keyBackUpExist = doesKeyBackupExist),
|
||||
// Also reset the passphrase
|
||||
passphrase = null,
|
||||
passphraseRepeat = null,
|
||||
// Also reset the key
|
||||
migrationRecoveryKey = null
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
is BootstrapStep.GetBackupSecretKeyForMigration -> {
|
||||
// do we let you cancel from here?
|
||||
_viewEvents.post(BootstrapViewEvents.SkipBootstrap())
|
||||
}
|
||||
is BootstrapStep.SetupPassphrase -> {
|
||||
// do we let you cancel from here?
|
||||
_viewEvents.post(BootstrapViewEvents.SkipBootstrap())
|
||||
setState {
|
||||
copy(
|
||||
step = BootstrapStep.FirstForm(keyBackUpExist = doesKeyBackupExist),
|
||||
// Also reset the passphrase
|
||||
passphrase = null,
|
||||
passphraseRepeat = null
|
||||
)
|
||||
}
|
||||
}
|
||||
is BootstrapStep.ConfirmPassphrase -> {
|
||||
setState {
|
||||
copy(
|
||||
step = BootstrapStep.SetupPassphrase(
|
||||
isPasswordVisible = (state.step as? BootstrapStep.ConfirmPassphrase)?.isPasswordVisible ?: false
|
||||
isPasswordVisible = state.step.isPasswordVisible
|
||||
)
|
||||
)
|
||||
}
|
||||
|
@ -472,6 +513,32 @@ class BootstrapSharedViewModel @AssistedInject constructor(
|
|||
BootstrapStep.DoneSuccess -> {
|
||||
// nop
|
||||
}
|
||||
BootstrapStep.CheckingMigration -> Unit
|
||||
is BootstrapStep.FirstForm -> {
|
||||
_viewEvents.post(BootstrapViewEvents.SkipBootstrap())
|
||||
}
|
||||
is BootstrapStep.GetBackupSecretForMigration -> {
|
||||
setState {
|
||||
copy(
|
||||
step = BootstrapStep.FirstForm(keyBackUpExist = doesKeyBackupExist),
|
||||
// Also reset the passphrase
|
||||
passphrase = null,
|
||||
passphraseRepeat = null,
|
||||
// Also reset the key
|
||||
migrationRecoveryKey = null
|
||||
)
|
||||
}
|
||||
}
|
||||
}.exhaustive
|
||||
}
|
||||
|
||||
private fun BackupToQuadSMigrationTask.Result.Failure.toHumanReadable(): String {
|
||||
return when (this) {
|
||||
is BackupToQuadSMigrationTask.Result.InvalidRecoverySecret -> stringProvider.getString(R.string.keys_backup_passphrase_error_decrypt)
|
||||
is BackupToQuadSMigrationTask.Result.ErrorFailure -> errorFormatter.toHumanReadable(throwable)
|
||||
// is BackupToQuadSMigrationTask.Result.NoKeyBackupVersion,
|
||||
// is BackupToQuadSMigrationTask.Result.IllegalParams,
|
||||
else -> stringProvider.getString(R.string.unexpected_error)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -484,7 +551,7 @@ class BootstrapSharedViewModel @AssistedInject constructor(
|
|||
override fun create(viewModelContext: ViewModelContext, state: BootstrapViewState): BootstrapSharedViewModel? {
|
||||
val fragment: BootstrapBottomSheet = (viewModelContext as FragmentViewModelContext).fragment()
|
||||
val args: BootstrapBottomSheet.Args = fragment.arguments?.getParcelable(BootstrapBottomSheet.EXTRA_ARGS)
|
||||
?: BootstrapBottomSheet.Args(true)
|
||||
?: BootstrapBottomSheet.Args(initCrossSigningOnly = true)
|
||||
return fragment.bootstrapViewModelFactory.create(state, args)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,6 +17,14 @@
|
|||
package im.vector.riotx.features.crypto.recover
|
||||
|
||||
/**
|
||||
* TODO The schema is not up to date
|
||||
*
|
||||
* ┌───────────────────────────────────┐
|
||||
* │ BootstrapStep.SetupSecureBackup │
|
||||
* └───────────────────────────────────┘
|
||||
* │
|
||||
* │
|
||||
* ▼
|
||||
* ┌─────────────────────────┐
|
||||
* │ User has signing keys? │──────────── Account
|
||||
* └─────────────────────────┘ Creation ?
|
||||
|
@ -77,10 +85,16 @@ package im.vector.riotx.features.crypto.recover
|
|||
*/
|
||||
|
||||
sealed class BootstrapStep {
|
||||
// This is the first step
|
||||
object CheckingMigration : BootstrapStep()
|
||||
|
||||
// Use will be asked to choose between passphrase or recovery key, or to start process if a key backup exists
|
||||
data class FirstForm(val keyBackUpExist: Boolean) : BootstrapStep()
|
||||
|
||||
data class SetupPassphrase(val isPasswordVisible: Boolean) : BootstrapStep()
|
||||
data class ConfirmPassphrase(val isPasswordVisible: Boolean) : BootstrapStep()
|
||||
|
||||
data class AccountPassword(val isPasswordVisible: Boolean, val failure: String? = null) : BootstrapStep()
|
||||
object CheckingMigration : BootstrapStep()
|
||||
|
||||
abstract class GetBackupSecretForMigration : BootstrapStep()
|
||||
data class GetBackupSecretPassForMigration(val isPasswordVisible: Boolean, val useKey: Boolean) : GetBackupSecretForMigration()
|
||||
|
@ -90,3 +104,10 @@ sealed class BootstrapStep {
|
|||
data class SaveRecoveryKey(val isSaved: Boolean) : BootstrapStep()
|
||||
object DoneSuccess : BootstrapStep()
|
||||
}
|
||||
|
||||
fun BootstrapStep.GetBackupSecretForMigration.useKey(): Boolean {
|
||||
return when (this) {
|
||||
is BootstrapStep.GetBackupSecretPassForMigration -> useKey
|
||||
else -> true
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
* 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.recover
|
||||
|
||||
import com.airbnb.mvrx.Async
|
||||
import com.airbnb.mvrx.MvRxState
|
||||
import com.airbnb.mvrx.Uninitialized
|
||||
import com.nulabinc.zxcvbn.Strength
|
||||
import im.vector.matrix.android.api.session.securestorage.SsssKeyCreationInfo
|
||||
import im.vector.riotx.core.platform.WaitingViewData
|
||||
|
||||
data class BootstrapViewState(
|
||||
val step: BootstrapStep = BootstrapStep.CheckingMigration,
|
||||
val passphrase: String? = null,
|
||||
val migrationRecoveryKey: String? = null,
|
||||
val passphraseRepeat: String? = null,
|
||||
val crossSigningInitialization: Async<Unit> = Uninitialized,
|
||||
val passphraseStrength: Async<Strength> = Uninitialized,
|
||||
val passphraseConfirmMatch: Async<Unit> = Uninitialized,
|
||||
val recoveryKeyCreationInfo: SsssKeyCreationInfo? = null,
|
||||
val initializationWaitingViewData: WaitingViewData? = null,
|
||||
val recoverySaveFileProcess: Async<Unit> = Uninitialized
|
||||
) : MvRxState
|
|
@ -44,7 +44,6 @@ import im.vector.matrix.android.api.session.crypto.verification.VerificationTran
|
|||
import im.vector.matrix.android.api.session.crypto.verification.VerificationTxState
|
||||
import im.vector.matrix.android.api.session.events.model.LocalEcho
|
||||
import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams
|
||||
import im.vector.matrix.android.api.session.securestorage.IntegrityResult
|
||||
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
|
||||
|
@ -118,10 +117,6 @@ class VerificationBottomSheetViewModel @AssistedInject constructor(
|
|||
session.cryptoService().verificationService().getExistingTransaction(args.otherUserId, it) as? QrCodeVerificationTransaction
|
||||
}
|
||||
|
||||
val ssssOk = session.sharedSecretStorageService.checkShouldBeAbleToAccessSecrets(
|
||||
listOf(MASTER_KEY_SSSS_NAME, USER_SIGNING_KEY_SSSS_NAME, SELF_SIGNING_KEY_SSSS_NAME),
|
||||
null // default key
|
||||
) is IntegrityResult.Success
|
||||
setState {
|
||||
copy(
|
||||
otherUserMxItem = userItem?.toMatrixItem(),
|
||||
|
@ -133,7 +128,7 @@ class VerificationBottomSheetViewModel @AssistedInject constructor(
|
|||
roomId = args.roomId,
|
||||
isMe = args.otherUserId == session.myUserId,
|
||||
currentDeviceCanCrossSign = session.cryptoService().crossSigningService().canCrossSign(),
|
||||
quadSContainsSecrets = ssssOk
|
||||
quadSContainsSecrets = session.sharedSecretStorageService.isRecoverySetup()
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -19,6 +19,7 @@ package im.vector.riotx.features.home
|
|||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.os.Parcelable
|
||||
import android.view.MenuItem
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
|
@ -26,12 +27,10 @@ import androidx.core.content.ContextCompat
|
|||
import androidx.core.view.GravityCompat
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.drawerlayout.widget.DrawerLayout
|
||||
import androidx.lifecycle.Observer
|
||||
import im.vector.matrix.android.api.MatrixCallback
|
||||
import im.vector.matrix.android.api.session.Session
|
||||
import im.vector.matrix.android.api.util.toMatrixItem
|
||||
import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
|
||||
import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap
|
||||
import com.airbnb.mvrx.MvRx
|
||||
import com.airbnb.mvrx.viewModel
|
||||
import im.vector.matrix.android.api.session.InitialSyncProgressService
|
||||
import im.vector.matrix.android.api.util.MatrixItem
|
||||
import im.vector.riotx.R
|
||||
import im.vector.riotx.core.di.ActiveSessionHolder
|
||||
import im.vector.riotx.core.di.ScreenComponent
|
||||
|
@ -41,7 +40,6 @@ import im.vector.riotx.core.extensions.replaceFragment
|
|||
import im.vector.riotx.core.platform.ToolbarConfigurable
|
||||
import im.vector.riotx.core.platform.VectorBaseActivity
|
||||
import im.vector.riotx.core.pushers.PushersManager
|
||||
import im.vector.riotx.features.crypto.recover.BootstrapBottomSheet
|
||||
import im.vector.riotx.features.disclaimer.showDisclaimerDialog
|
||||
import im.vector.riotx.features.notifications.NotificationDrawerManager
|
||||
import im.vector.riotx.features.popup.PopupAlertManager
|
||||
|
@ -50,15 +48,25 @@ import im.vector.riotx.features.rageshake.VectorUncaughtExceptionHandler
|
|||
import im.vector.riotx.features.settings.VectorPreferences
|
||||
import im.vector.riotx.features.workers.signout.SignOutViewModel
|
||||
import im.vector.riotx.push.fcm.FcmHelper
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
import kotlinx.android.synthetic.main.activity_home.*
|
||||
import kotlinx.android.synthetic.main.merge_overlay_waiting_view.*
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
@Parcelize
|
||||
data class HomeActivityArgs(
|
||||
val clearNotification: Boolean,
|
||||
val accountCreation: Boolean
|
||||
) : Parcelable
|
||||
|
||||
class HomeActivity : VectorBaseActivity(), ToolbarConfigurable, UnknownDeviceDetectorSharedViewModel.Factory {
|
||||
|
||||
private lateinit var sharedActionViewModel: HomeSharedActionViewModel
|
||||
|
||||
private val homeActivityViewModel: HomeActivityViewModel by viewModel()
|
||||
@Inject lateinit var viewModelFactory: HomeActivityViewModel.Factory
|
||||
|
||||
@Inject lateinit var activeSessionHolder: ActiveSessionHolder
|
||||
@Inject lateinit var vectorUncaughtExceptionHandler: VectorUncaughtExceptionHandler
|
||||
@Inject lateinit var pushManager: PushersManager
|
||||
|
@ -98,35 +106,40 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable, UnknownDeviceDet
|
|||
.observe()
|
||||
.subscribe { sharedAction ->
|
||||
when (sharedAction) {
|
||||
is HomeActivitySharedAction.OpenDrawer -> drawerLayout.openDrawer(GravityCompat.START)
|
||||
is HomeActivitySharedAction.CloseDrawer -> drawerLayout.closeDrawer(GravityCompat.START)
|
||||
is HomeActivitySharedAction.OpenGroup -> {
|
||||
is HomeActivitySharedAction.OpenDrawer -> drawerLayout.openDrawer(GravityCompat.START)
|
||||
is HomeActivitySharedAction.CloseDrawer -> drawerLayout.closeDrawer(GravityCompat.START)
|
||||
is HomeActivitySharedAction.OpenGroup -> {
|
||||
drawerLayout.closeDrawer(GravityCompat.START)
|
||||
replaceFragment(R.id.homeDetailFragmentContainer, HomeDetailFragment::class.java)
|
||||
}
|
||||
is HomeActivitySharedAction.PromptForSecurityBootstrap -> {
|
||||
BootstrapBottomSheet.show(supportFragmentManager, true)
|
||||
}
|
||||
}.exhaustive
|
||||
}
|
||||
.disposeOnDestroy()
|
||||
|
||||
if (intent.getBooleanExtra(EXTRA_CLEAR_EXISTING_NOTIFICATION, false)) {
|
||||
val args = intent.getParcelableExtra<HomeActivityArgs>(MvRx.KEY_ARG)
|
||||
|
||||
if (args?.clearNotification == true) {
|
||||
notificationDrawerManager.clearAllEvents()
|
||||
intent.removeExtra(EXTRA_CLEAR_EXISTING_NOTIFICATION)
|
||||
}
|
||||
if (intent.getBooleanExtra(EXTRA_ACCOUNT_CREATION, false)) {
|
||||
sharedActionViewModel.post(HomeActivitySharedAction.PromptForSecurityBootstrap)
|
||||
sharedActionViewModel.isAccountCreation = true
|
||||
intent.removeExtra(EXTRA_ACCOUNT_CREATION)
|
||||
}
|
||||
|
||||
activeSessionHolder.getSafeActiveSession()?.getInitialSyncProgressStatus()?.observe(this, Observer { status ->
|
||||
if (status == null) {
|
||||
homeActivityViewModel.observeViewEvents {
|
||||
when (it) {
|
||||
is HomeActivityViewEvents.AskPasswordToInitCrossSigning -> handleAskPasswordToInitCrossSigning(it)
|
||||
is HomeActivityViewEvents.OnNewSession -> handleOnNewSession(it)
|
||||
}.exhaustive
|
||||
}
|
||||
homeActivityViewModel.subscribe(this) { renderState(it) }
|
||||
|
||||
shortcutsHandler.observeRoomsAndBuildShortcuts()
|
||||
.disposeOnDestroy()
|
||||
}
|
||||
|
||||
private fun renderState(state: HomeActivityViewState) {
|
||||
when (val status = state.initialSyncProgressServiceStatus) {
|
||||
is InitialSyncProgressService.Status.Idle -> {
|
||||
waiting_view.isVisible = false
|
||||
promptCompleteSecurityIfNeeded()
|
||||
} else {
|
||||
sharedActionViewModel.hasDisplayedCompleteSecurityPrompt = false
|
||||
}
|
||||
is InitialSyncProgressService.Status.Progressing -> {
|
||||
Timber.v("${getString(status.statusText)} ${status.percentProgress}")
|
||||
waiting_view.setOnClickListener {
|
||||
// block interactions
|
||||
|
@ -143,67 +156,32 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable, UnknownDeviceDet
|
|||
}
|
||||
waiting_view.isVisible = true
|
||||
}
|
||||
})
|
||||
|
||||
// Ask again if the app is relaunched
|
||||
if (!sharedActionViewModel.hasDisplayedCompleteSecurityPrompt
|
||||
&& activeSessionHolder.getSafeActiveSession()?.hasAlreadySynced() == true) {
|
||||
promptCompleteSecurityIfNeeded()
|
||||
}
|
||||
|
||||
shortcutsHandler.observeRoomsAndBuildShortcuts()
|
||||
.disposeOnDestroy()
|
||||
}.exhaustive
|
||||
}
|
||||
|
||||
private fun promptCompleteSecurityIfNeeded() {
|
||||
val session = activeSessionHolder.getSafeActiveSession() ?: return
|
||||
if (!session.hasAlreadySynced()) return
|
||||
if (sharedActionViewModel.hasDisplayedCompleteSecurityPrompt) return
|
||||
|
||||
// ensure keys are downloaded
|
||||
session.cryptoService().downloadKeys(listOf(session.myUserId), true, object : MatrixCallback<MXUsersDevicesMap<CryptoDeviceInfo>> {
|
||||
override fun onSuccess(data: MXUsersDevicesMap<CryptoDeviceInfo>) {
|
||||
runOnUiThread {
|
||||
alertCompleteSecurity(session)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun alertCompleteSecurity(session: Session) {
|
||||
val myCrossSigningKeys = session.cryptoService().crossSigningService()
|
||||
.getMyCrossSigningKeys()
|
||||
val crossSigningEnabledOnAccount = myCrossSigningKeys != null
|
||||
|
||||
if (!crossSigningEnabledOnAccount && !sharedActionViewModel.isAccountCreation) {
|
||||
// Do not propose for SSO accounts, because we do not support yet confirming account credentials using SSO
|
||||
if (session.getHomeServerCapabilities().canChangePassword) {
|
||||
// We need to ask
|
||||
promptSecurityEvent(
|
||||
session,
|
||||
R.string.upgrade_security,
|
||||
R.string.security_prompt_text
|
||||
) {
|
||||
it.navigator.upgradeSessionSecurity(it)
|
||||
}
|
||||
} else {
|
||||
// Do not do it again
|
||||
sharedActionViewModel.hasDisplayedCompleteSecurityPrompt = true
|
||||
}
|
||||
} else if (myCrossSigningKeys?.isTrusted() == false) {
|
||||
// We need to ask
|
||||
promptSecurityEvent(
|
||||
session,
|
||||
R.string.crosssigning_verify_this_session,
|
||||
R.string.confirm_your_identity
|
||||
) {
|
||||
it.navigator.waitSessionVerification(it)
|
||||
}
|
||||
private fun handleAskPasswordToInitCrossSigning(events: HomeActivityViewEvents.AskPasswordToInitCrossSigning) {
|
||||
// We need to ask
|
||||
promptSecurityEvent(
|
||||
events.userItem,
|
||||
R.string.upgrade_security,
|
||||
R.string.security_prompt_text
|
||||
) {
|
||||
it.navigator.upgradeSessionSecurity(it, true)
|
||||
}
|
||||
}
|
||||
|
||||
private fun promptSecurityEvent(session: Session, titleRes: Int, descRes: Int, action: ((VectorBaseActivity) -> Unit)) {
|
||||
sharedActionViewModel.hasDisplayedCompleteSecurityPrompt = true
|
||||
private fun handleOnNewSession(event: HomeActivityViewEvents.OnNewSession) {
|
||||
// We need to ask
|
||||
promptSecurityEvent(
|
||||
event.userItem,
|
||||
R.string.crosssigning_verify_this_session,
|
||||
R.string.confirm_your_identity
|
||||
) {
|
||||
it.navigator.waitSessionVerification(it)
|
||||
}
|
||||
}
|
||||
|
||||
private fun promptSecurityEvent(userItem: MatrixItem.UserItem?, titleRes: Int, descRes: Int, action: ((VectorBaseActivity) -> Unit)) {
|
||||
popupAlertManager.postVectorAlert(
|
||||
VerificationVectorAlert(
|
||||
uid = "upgradeSecurity",
|
||||
|
@ -211,7 +189,7 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable, UnknownDeviceDet
|
|||
description = getString(descRes),
|
||||
iconId = R.drawable.ic_shield_warning
|
||||
).apply {
|
||||
matrixItem = session.getUser(session.myUserId)?.toMatrixItem()
|
||||
matrixItem = userItem
|
||||
colorInt = ContextCompat.getColor(this@HomeActivity, R.color.riotx_positive_accent)
|
||||
contentAction = Runnable {
|
||||
(weakCurrentActivity?.get() as? VectorBaseActivity)?.let {
|
||||
|
@ -225,9 +203,8 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable, UnknownDeviceDet
|
|||
|
||||
override fun onNewIntent(intent: Intent?) {
|
||||
super.onNewIntent(intent)
|
||||
if (intent?.hasExtra(EXTRA_CLEAR_EXISTING_NOTIFICATION) == true) {
|
||||
if (intent?.getParcelableExtra<HomeActivityArgs>(MvRx.KEY_ARG)?.clearNotification == true) {
|
||||
notificationDrawerManager.clearAllEvents()
|
||||
intent.removeExtra(EXTRA_CLEAR_EXISTING_NOTIFICATION)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -290,14 +267,15 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable, UnknownDeviceDet
|
|||
}
|
||||
|
||||
companion object {
|
||||
private const val EXTRA_CLEAR_EXISTING_NOTIFICATION = "EXTRA_CLEAR_EXISTING_NOTIFICATION"
|
||||
private const val EXTRA_ACCOUNT_CREATION = "EXTRA_ACCOUNT_CREATION"
|
||||
|
||||
fun newIntent(context: Context, clearNotification: Boolean = false, accountCreation: Boolean = false): Intent {
|
||||
val args = HomeActivityArgs(
|
||||
clearNotification = clearNotification,
|
||||
accountCreation = accountCreation
|
||||
)
|
||||
|
||||
return Intent(context, HomeActivity::class.java)
|
||||
.apply {
|
||||
putExtra(EXTRA_CLEAR_EXISTING_NOTIFICATION, clearNotification)
|
||||
putExtra(EXTRA_ACCOUNT_CREATION, accountCreation)
|
||||
putExtra(MvRx.KEY_ARG, args)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,5 +25,4 @@ sealed class HomeActivitySharedAction : VectorSharedAction {
|
|||
object OpenDrawer : HomeActivitySharedAction()
|
||||
object CloseDrawer : HomeActivitySharedAction()
|
||||
object OpenGroup : HomeActivitySharedAction()
|
||||
object PromptForSecurityBootstrap : HomeActivitySharedAction()
|
||||
}
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* 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 im.vector.matrix.android.api.util.MatrixItem
|
||||
import im.vector.riotx.core.platform.VectorViewEvents
|
||||
|
||||
sealed class HomeActivityViewEvents : VectorViewEvents {
|
||||
data class AskPasswordToInitCrossSigning(val userItem: MatrixItem.UserItem?) : HomeActivityViewEvents()
|
||||
data class OnNewSession(val userItem: MatrixItem.UserItem?) : HomeActivityViewEvents()
|
||||
}
|
|
@ -0,0 +1,166 @@
|
|||
/*
|
||||
* 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.MvRx
|
||||
import com.airbnb.mvrx.MvRxViewModelFactory
|
||||
import com.airbnb.mvrx.ViewModelContext
|
||||
import com.squareup.inject.assisted.Assisted
|
||||
import com.squareup.inject.assisted.AssistedInject
|
||||
import im.vector.matrix.android.api.MatrixCallback
|
||||
import im.vector.matrix.android.api.NoOpMatrixCallback
|
||||
import im.vector.matrix.android.api.session.InitialSyncProgressService
|
||||
import im.vector.matrix.android.api.util.toMatrixItem
|
||||
import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
|
||||
import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.UserPasswordAuth
|
||||
import im.vector.matrix.rx.asObservable
|
||||
import im.vector.riotx.core.di.ActiveSessionHolder
|
||||
import im.vector.riotx.core.platform.EmptyAction
|
||||
import im.vector.riotx.core.platform.VectorViewModel
|
||||
import im.vector.riotx.features.login.ReAuthHelper
|
||||
import timber.log.Timber
|
||||
|
||||
class HomeActivityViewModel @AssistedInject constructor(
|
||||
@Assisted initialState: HomeActivityViewState,
|
||||
@Assisted private val args: HomeActivityArgs,
|
||||
private val activeSessionHolder: ActiveSessionHolder,
|
||||
private val reAuthHelper: ReAuthHelper
|
||||
) : VectorViewModel<HomeActivityViewState, EmptyAction, HomeActivityViewEvents>(initialState) {
|
||||
|
||||
@AssistedInject.Factory
|
||||
interface Factory {
|
||||
fun create(initialState: HomeActivityViewState, args: HomeActivityArgs): HomeActivityViewModel
|
||||
}
|
||||
|
||||
companion object : MvRxViewModelFactory<HomeActivityViewModel, HomeActivityViewState> {
|
||||
|
||||
@JvmStatic
|
||||
override fun create(viewModelContext: ViewModelContext, state: HomeActivityViewState): HomeActivityViewModel? {
|
||||
val activity: HomeActivity = viewModelContext.activity()
|
||||
val args: HomeActivityArgs? = activity.intent.getParcelableExtra(MvRx.KEY_ARG)
|
||||
return args?.let { activity.viewModelFactory.create(state, it) }
|
||||
}
|
||||
}
|
||||
|
||||
private var checkBootstrap = false
|
||||
|
||||
init {
|
||||
observeInitialSync()
|
||||
mayBeInitializeCrossSigning()
|
||||
}
|
||||
|
||||
private fun observeInitialSync() {
|
||||
val session = activeSessionHolder.getSafeActiveSession() ?: return
|
||||
|
||||
session.getInitialSyncProgressStatus()
|
||||
.asObservable()
|
||||
.subscribe { status ->
|
||||
when (status) {
|
||||
is InitialSyncProgressService.Status.Progressing -> {
|
||||
// Schedule a check of the bootstrap when the init sync will be finished
|
||||
checkBootstrap = true
|
||||
}
|
||||
is InitialSyncProgressService.Status.Idle -> {
|
||||
if (checkBootstrap) {
|
||||
checkBootstrap = false
|
||||
maybeBootstrapCrossSigning()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setState {
|
||||
copy(
|
||||
initialSyncProgressServiceStatus = status
|
||||
)
|
||||
}
|
||||
}
|
||||
.disposeOnClear()
|
||||
}
|
||||
|
||||
private fun mayBeInitializeCrossSigning() {
|
||||
if (args.accountCreation) {
|
||||
val password = reAuthHelper.data ?: return Unit.also {
|
||||
Timber.w("No password to init cross signing")
|
||||
}
|
||||
|
||||
val session = activeSessionHolder.getSafeActiveSession() ?: return Unit.also {
|
||||
Timber.w("No session to init cross signing")
|
||||
}
|
||||
|
||||
// We do not use the viewModel context because we do not want to cancel this action
|
||||
Timber.d("Initialize cross signing")
|
||||
session.cryptoService().crossSigningService().initializeCrossSigning(
|
||||
authParams = UserPasswordAuth(
|
||||
session = null,
|
||||
user = session.myUserId,
|
||||
password = password
|
||||
),
|
||||
callback = NoOpMatrixCallback()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun maybeBootstrapCrossSigning() {
|
||||
// In case of account creation, it is already done before
|
||||
if (args.accountCreation) return
|
||||
|
||||
val session = activeSessionHolder.getSafeActiveSession() ?: return
|
||||
|
||||
// Ensure keys of the user are downloaded
|
||||
session.cryptoService().downloadKeys(listOf(session.myUserId), true, object : MatrixCallback<MXUsersDevicesMap<CryptoDeviceInfo>> {
|
||||
override fun onSuccess(data: MXUsersDevicesMap<CryptoDeviceInfo>) {
|
||||
// Is there already cross signing keys here?
|
||||
val mxCrossSigningInfo = session.cryptoService().crossSigningService().getMyCrossSigningKeys()
|
||||
if (mxCrossSigningInfo != null) {
|
||||
// Cross-signing is already set up for this user, is it trusted?
|
||||
if (!mxCrossSigningInfo.isTrusted()) {
|
||||
// New session
|
||||
_viewEvents.post(HomeActivityViewEvents.OnNewSession(session.getUser(session.myUserId)?.toMatrixItem()))
|
||||
}
|
||||
} else {
|
||||
// Initialize cross-signing
|
||||
val password = reAuthHelper.data
|
||||
|
||||
if (password == null) {
|
||||
// Check this is not an SSO account
|
||||
if (session.getHomeServerCapabilities().canChangePassword) {
|
||||
// Ask password to the user: Upgrade security
|
||||
_viewEvents.post(HomeActivityViewEvents.AskPasswordToInitCrossSigning(session.getUser(session.myUserId)?.toMatrixItem()))
|
||||
}
|
||||
// Else (SSO) just ignore for the moment
|
||||
} else {
|
||||
// We do not use the viewModel context because we do not want to cancel this action
|
||||
Timber.d("Initialize cross signing")
|
||||
session.cryptoService().crossSigningService().initializeCrossSigning(
|
||||
authParams = UserPasswordAuth(
|
||||
session = null,
|
||||
user = session.myUserId,
|
||||
password = password
|
||||
),
|
||||
callback = NoOpMatrixCallback()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
override fun handle(action: EmptyAction) {
|
||||
// NA
|
||||
}
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
/*
|
||||
* 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.MvRxState
|
||||
import im.vector.matrix.android.api.session.InitialSyncProgressService
|
||||
|
||||
data class HomeActivityViewState(
|
||||
val initialSyncProgressServiceStatus: InitialSyncProgressService.Status = InitialSyncProgressService.Status.Idle
|
||||
) : MvRxState
|
|
@ -19,7 +19,4 @@ package im.vector.riotx.features.home
|
|||
import im.vector.riotx.core.platform.VectorSharedActionViewModel
|
||||
import javax.inject.Inject
|
||||
|
||||
class HomeSharedActionViewModel @Inject constructor() : VectorSharedActionViewModel<HomeActivitySharedAction>() {
|
||||
var hasDisplayedCompleteSecurityPrompt : Boolean = false
|
||||
var isAccountCreation : Boolean = false
|
||||
}
|
||||
class HomeSharedActionViewModel @Inject constructor() : VectorSharedActionViewModel<HomeActivitySharedAction>()
|
||||
|
|
|
@ -41,7 +41,6 @@ import im.vector.matrix.android.api.auth.registration.Stage
|
|||
import im.vector.matrix.android.api.auth.wellknown.WellknownResult
|
||||
import im.vector.matrix.android.api.session.Session
|
||||
import im.vector.matrix.android.api.util.Cancelable
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.UserPasswordAuth
|
||||
import im.vector.riotx.R
|
||||
import im.vector.riotx.core.di.ActiveSessionHolder
|
||||
import im.vector.riotx.core.extensions.configureAndStart
|
||||
|
@ -289,7 +288,7 @@ class LoginViewModel @AssistedInject constructor(
|
|||
|
||||
private fun handleRegisterWith(action: LoginAction.LoginOrRegister) {
|
||||
setState { copy(asyncRegistration = Loading()) }
|
||||
reAuthHelper.rememberAuth(UserPasswordAuth(user = action.username, password = action.password))
|
||||
reAuthHelper.data = action.password
|
||||
currentTask = registrationWizard?.createAccount(
|
||||
action.username,
|
||||
action.password,
|
||||
|
@ -569,6 +568,7 @@ class LoginViewModel @AssistedInject constructor(
|
|||
action.initialDeviceName,
|
||||
object : MatrixCallback<Session> {
|
||||
override fun onSuccess(data: Session) {
|
||||
reAuthHelper.data = action.password
|
||||
onSessionCreated(data)
|
||||
}
|
||||
|
||||
|
|
|
@ -16,33 +16,12 @@
|
|||
|
||||
package im.vector.riotx.features.login
|
||||
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.UserPasswordAuth
|
||||
import java.util.Timer
|
||||
import java.util.TimerTask
|
||||
import im.vector.riotx.core.utils.TemporaryStore
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
const val THREE_MINUTES = 3 * 60_000L
|
||||
|
||||
/**
|
||||
* Will store the account password for 3 minutes
|
||||
*/
|
||||
@Singleton
|
||||
class ReAuthHelper @Inject constructor() {
|
||||
|
||||
private var timer: Timer? = null
|
||||
|
||||
private var rememberedInfo: UserPasswordAuth? = null
|
||||
|
||||
fun rememberAuth(password: UserPasswordAuth?) {
|
||||
timer?.cancel()
|
||||
timer = null
|
||||
rememberedInfo = password
|
||||
timer = Timer().apply {
|
||||
schedule(object : TimerTask() {
|
||||
override fun run() {
|
||||
rememberedInfo = null
|
||||
}
|
||||
}, THREE_MINUTES)
|
||||
}
|
||||
}
|
||||
|
||||
fun rememberedAuth() = rememberedInfo
|
||||
}
|
||||
class ReAuthHelper @Inject constructor() : TemporaryStore<String>()
|
||||
|
|
|
@ -124,9 +124,9 @@ class DefaultNavigator @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
override fun upgradeSessionSecurity(context: Context) {
|
||||
override fun upgradeSessionSecurity(context: Context, initCrossSigningOnly: Boolean) {
|
||||
if (context is VectorBaseActivity) {
|
||||
BootstrapBottomSheet.show(context.supportFragmentManager, false)
|
||||
BootstrapBottomSheet.show(context.supportFragmentManager, initCrossSigningOnly)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -42,7 +42,7 @@ interface Navigator {
|
|||
|
||||
fun waitSessionVerification(context: Context)
|
||||
|
||||
fun upgradeSessionSecurity(context: Context)
|
||||
fun upgradeSessionSecurity(context: Context, initCrossSigningOnly: Boolean)
|
||||
|
||||
fun openRoomForSharingAndFinish(activity: Activity, roomId: String, sharedData: SharedData)
|
||||
|
||||
|
|
|
@ -119,8 +119,7 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor(
|
|||
}
|
||||
|
||||
private fun refreshXSigningStatus() {
|
||||
val crossSigningKeys = session.cryptoService().crossSigningService().getMyCrossSigningKeys()
|
||||
val xSigningIsEnableInAccount = crossSigningKeys != null
|
||||
val xSigningIsEnableInAccount = session.cryptoService().crossSigningService().isCrossSigningInitialized()
|
||||
val xSigningKeysAreTrusted = session.cryptoService().crossSigningService().checkUserTrust(session.myUserId).isVerified()
|
||||
val xSigningKeyCanSign = session.cryptoService().crossSigningService().canCrossSign()
|
||||
|
||||
|
|
|
@ -1,163 +0,0 @@
|
|||
/*
|
||||
* Copyright 2020 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package im.vector.riotx.features.settings.crosssigning
|
||||
|
||||
import com.airbnb.epoxy.TypedEpoxyController
|
||||
import im.vector.riotx.R
|
||||
import im.vector.riotx.core.epoxy.loadingItem
|
||||
import im.vector.riotx.core.resources.ColorProvider
|
||||
import im.vector.riotx.core.resources.StringProvider
|
||||
import im.vector.riotx.core.ui.list.genericItem
|
||||
import im.vector.riotx.core.ui.list.genericItemWithValue
|
||||
import im.vector.riotx.core.utils.DimensionConverter
|
||||
import im.vector.riotx.features.crypto.verification.epoxy.bottomSheetVerificationActionItem
|
||||
import im.vector.riotx.features.settings.VectorPreferences
|
||||
import me.gujun.android.span.span
|
||||
import javax.inject.Inject
|
||||
|
||||
class CrossSigningEpoxyController @Inject constructor(
|
||||
private val stringProvider: StringProvider,
|
||||
private val colorProvider: ColorProvider,
|
||||
private val dimensionConverter: DimensionConverter,
|
||||
private val vectorPreferences: VectorPreferences
|
||||
) : TypedEpoxyController<CrossSigningSettingsViewState>() {
|
||||
|
||||
interface InteractionListener {
|
||||
fun onInitializeCrossSigningKeys()
|
||||
fun verifySession()
|
||||
}
|
||||
|
||||
var interactionListener: InteractionListener? = null
|
||||
|
||||
override fun buildModels(data: CrossSigningSettingsViewState?) {
|
||||
if (data == null) return
|
||||
if (data.xSigningKeyCanSign) {
|
||||
genericItem {
|
||||
id("can")
|
||||
titleIconResourceId(R.drawable.ic_shield_trusted)
|
||||
title(stringProvider.getString(R.string.encryption_information_dg_xsigning_complete))
|
||||
}
|
||||
} else if (data.xSigningKeysAreTrusted) {
|
||||
genericItem {
|
||||
id("trusted")
|
||||
titleIconResourceId(R.drawable.ic_shield_custom)
|
||||
title(stringProvider.getString(R.string.encryption_information_dg_xsigning_trusted))
|
||||
}
|
||||
if (!data.isUploadingKeys) {
|
||||
bottomSheetVerificationActionItem {
|
||||
id("verify")
|
||||
title(stringProvider.getString(R.string.crosssigning_verify_this_session))
|
||||
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 {
|
||||
id("enable")
|
||||
titleIconResourceId(R.drawable.ic_shield_black)
|
||||
title(stringProvider.getString(R.string.encryption_information_dg_xsigning_not_trusted))
|
||||
}
|
||||
bottomSheetVerificationActionItem {
|
||||
id("verify")
|
||||
title(stringProvider.getString(R.string.crosssigning_verify_this_session))
|
||||
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 {
|
||||
genericItem {
|
||||
id("not")
|
||||
title(stringProvider.getString(R.string.encryption_information_dg_xsigning_disabled))
|
||||
}
|
||||
if (vectorPreferences.developerMode() && !data.isUploadingKeys) {
|
||||
bottomSheetVerificationActionItem {
|
||||
id("initKeys")
|
||||
title(stringProvider.getString(R.string.initialize_cross_signing))
|
||||
titleColor(colorProvider.getColor(R.color.riotx_positive_accent))
|
||||
iconRes(R.drawable.ic_arrow_right)
|
||||
iconColor(colorProvider.getColor(R.color.riotx_positive_accent))
|
||||
listener {
|
||||
interactionListener?.onInitializeCrossSigningKeys()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (data.isUploadingKeys) {
|
||||
loadingItem {
|
||||
id("loading")
|
||||
}
|
||||
} else {
|
||||
val crossSigningKeys = data.crossSigningInfo
|
||||
|
||||
crossSigningKeys?.masterKey()?.let {
|
||||
genericItemWithValue {
|
||||
id("msk")
|
||||
titleIconResourceId(R.drawable.key_small)
|
||||
title(
|
||||
span {
|
||||
+"Master Key:\n"
|
||||
span {
|
||||
text = it.unpaddedBase64PublicKey ?: ""
|
||||
textColor = colorProvider.getColorFromAttribute(R.attr.riotx_text_secondary)
|
||||
textSize = dimensionConverter.spToPx(12)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
crossSigningKeys?.userKey()?.let {
|
||||
genericItemWithValue {
|
||||
id("usk")
|
||||
titleIconResourceId(R.drawable.key_small)
|
||||
title(
|
||||
span {
|
||||
+"User Key:\n"
|
||||
span {
|
||||
text = it.unpaddedBase64PublicKey ?: ""
|
||||
textColor = colorProvider.getColorFromAttribute(R.attr.riotx_text_secondary)
|
||||
textSize = dimensionConverter.spToPx(12)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
crossSigningKeys?.selfSigningKey()?.let {
|
||||
genericItemWithValue {
|
||||
id("ssk")
|
||||
titleIconResourceId(R.drawable.key_small)
|
||||
title(
|
||||
span {
|
||||
+"Self Signed Key:\n"
|
||||
span {
|
||||
text = it.unpaddedBase64PublicKey ?: ""
|
||||
textColor = colorProvider.getColorFromAttribute(R.attr.riotx_text_secondary)
|
||||
textSize = dimensionConverter.spToPx(12)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* 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.settings.crosssigning
|
||||
|
||||
import im.vector.riotx.core.platform.VectorViewModelAction
|
||||
|
||||
sealed class CrossSigningSettingsAction : VectorViewModelAction {
|
||||
object SetUpRecovery : CrossSigningSettingsAction()
|
||||
object VerifySession : CrossSigningSettingsAction()
|
||||
object SetupCrossSigning : CrossSigningSettingsAction()
|
||||
}
|
|
@ -0,0 +1,165 @@
|
|||
/*
|
||||
* Copyright 2020 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package im.vector.riotx.features.settings.crosssigning
|
||||
|
||||
import com.airbnb.epoxy.TypedEpoxyController
|
||||
import im.vector.riotx.R
|
||||
import im.vector.riotx.core.resources.ColorProvider
|
||||
import im.vector.riotx.core.resources.StringProvider
|
||||
import im.vector.riotx.core.ui.list.genericItem
|
||||
import im.vector.riotx.core.ui.list.genericItemWithValue
|
||||
import im.vector.riotx.core.utils.DimensionConverter
|
||||
import im.vector.riotx.features.crypto.verification.epoxy.bottomSheetVerificationActionItem
|
||||
import me.gujun.android.span.span
|
||||
import javax.inject.Inject
|
||||
|
||||
class CrossSigningSettingsController @Inject constructor(
|
||||
private val stringProvider: StringProvider,
|
||||
private val colorProvider: ColorProvider,
|
||||
private val dimensionConverter: DimensionConverter
|
||||
) : TypedEpoxyController<CrossSigningSettingsViewState>() {
|
||||
|
||||
interface InteractionListener {
|
||||
fun setupRecovery()
|
||||
fun verifySession()
|
||||
fun initCrossSigning()
|
||||
}
|
||||
|
||||
var interactionListener: InteractionListener? = null
|
||||
|
||||
override fun buildModels(data: CrossSigningSettingsViewState?) {
|
||||
if (data == null) return
|
||||
when {
|
||||
data.xSigningKeyCanSign -> {
|
||||
genericItem {
|
||||
id("can")
|
||||
titleIconResourceId(R.drawable.ic_shield_trusted)
|
||||
title(stringProvider.getString(R.string.encryption_information_dg_xsigning_complete))
|
||||
}
|
||||
}
|
||||
data.xSigningKeysAreTrusted -> {
|
||||
genericItem {
|
||||
id("trusted")
|
||||
titleIconResourceId(R.drawable.ic_shield_custom)
|
||||
title(stringProvider.getString(R.string.encryption_information_dg_xsigning_trusted))
|
||||
}
|
||||
}
|
||||
data.xSigningIsEnableInAccount -> {
|
||||
genericItem {
|
||||
id("enable")
|
||||
titleIconResourceId(R.drawable.ic_shield_black)
|
||||
title(stringProvider.getString(R.string.encryption_information_dg_xsigning_not_trusted))
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
genericItem {
|
||||
id("not")
|
||||
title(stringProvider.getString(R.string.encryption_information_dg_xsigning_disabled))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (data.recoveryHasToBeSetUp) {
|
||||
if (data.xSigningIsEnableInAccount) {
|
||||
bottomSheetVerificationActionItem {
|
||||
id("setup_recovery")
|
||||
title(stringProvider.getString(R.string.settings_setup_secure_backup))
|
||||
titleColor(colorProvider.getColorFromAttribute(R.attr.riotx_text_primary))
|
||||
iconRes(R.drawable.ic_arrow_right)
|
||||
listener {
|
||||
interactionListener?.setupRecovery()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Propose to setup cross signing
|
||||
bottomSheetVerificationActionItem {
|
||||
id("setup_xSgning")
|
||||
title(stringProvider.getString(R.string.setup_cross_signing))
|
||||
titleColor(colorProvider.getColorFromAttribute(R.attr.riotx_text_primary))
|
||||
subTitle(stringProvider.getString(R.string.security_prompt_text))
|
||||
iconRes(R.drawable.ic_arrow_right)
|
||||
listener {
|
||||
interactionListener?.initCrossSigning()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (data.deviceHasToBeVerified) {
|
||||
bottomSheetVerificationActionItem {
|
||||
id("verify")
|
||||
title(stringProvider.getString(R.string.crosssigning_verify_this_session))
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val crossSigningKeys = data.crossSigningInfo
|
||||
|
||||
crossSigningKeys?.masterKey()?.let {
|
||||
genericItemWithValue {
|
||||
id("msk")
|
||||
titleIconResourceId(R.drawable.key_small)
|
||||
title(
|
||||
span {
|
||||
+"Master Key:\n"
|
||||
span {
|
||||
text = it.unpaddedBase64PublicKey ?: ""
|
||||
textColor = colorProvider.getColorFromAttribute(R.attr.riotx_text_secondary)
|
||||
textSize = dimensionConverter.spToPx(12)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
crossSigningKeys?.userKey()?.let {
|
||||
genericItemWithValue {
|
||||
id("usk")
|
||||
titleIconResourceId(R.drawable.key_small)
|
||||
title(
|
||||
span {
|
||||
+"User Key:\n"
|
||||
span {
|
||||
text = it.unpaddedBase64PublicKey ?: ""
|
||||
textColor = colorProvider.getColorFromAttribute(R.attr.riotx_text_secondary)
|
||||
textSize = dimensionConverter.spToPx(12)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
crossSigningKeys?.selfSigningKey()?.let {
|
||||
genericItemWithValue {
|
||||
id("ssk")
|
||||
titleIconResourceId(R.drawable.key_small)
|
||||
title(
|
||||
span {
|
||||
+"Self Signed Key:\n"
|
||||
span {
|
||||
text = it.unpaddedBase64PublicKey ?: ""
|
||||
textColor = colorProvider.getColorFromAttribute(R.attr.riotx_text_secondary)
|
||||
textSize = dimensionConverter.spToPx(12)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -21,7 +21,6 @@ import androidx.appcompat.app.AlertDialog
|
|||
import com.airbnb.mvrx.fragmentViewModel
|
||||
import com.airbnb.mvrx.withState
|
||||
import im.vector.riotx.R
|
||||
import im.vector.riotx.core.dialogs.PromptPasswordDialog
|
||||
import im.vector.riotx.core.extensions.cleanup
|
||||
import im.vector.riotx.core.extensions.configureWith
|
||||
import im.vector.riotx.core.extensions.exhaustive
|
||||
|
@ -31,9 +30,9 @@ import kotlinx.android.synthetic.main.fragment_generic_recycler.*
|
|||
import javax.inject.Inject
|
||||
|
||||
class CrossSigningSettingsFragment @Inject constructor(
|
||||
private val epoxyController: CrossSigningEpoxyController,
|
||||
private val controller: CrossSigningSettingsController,
|
||||
val viewModelFactory: CrossSigningSettingsViewModel.Factory
|
||||
) : VectorBaseFragment(), CrossSigningEpoxyController.InteractionListener {
|
||||
) : VectorBaseFragment(), CrossSigningSettingsController.InteractionListener {
|
||||
|
||||
override fun getLayoutResId() = R.layout.fragment_generic_recycler
|
||||
|
||||
|
@ -43,7 +42,7 @@ class CrossSigningSettingsFragment @Inject constructor(
|
|||
super.onActivityCreated(savedInstanceState)
|
||||
viewModel.observeViewEvents {
|
||||
when (it) {
|
||||
is CrossSigningSettingsViewEvents.Failure -> {
|
||||
is CrossSigningSettingsViewEvents.Failure -> {
|
||||
AlertDialog.Builder(requireContext())
|
||||
.setTitle(R.string.dialog_title_error)
|
||||
.setMessage(errorFormatter.toHumanReadable(it.throwable))
|
||||
|
@ -51,13 +50,14 @@ class CrossSigningSettingsFragment @Inject constructor(
|
|||
.show()
|
||||
Unit
|
||||
}
|
||||
is CrossSigningSettingsViewEvents.RequestPassword -> {
|
||||
requestPassword()
|
||||
CrossSigningSettingsViewEvents.VerifySession -> {
|
||||
navigator.waitSessionVerification(requireActivity())
|
||||
}
|
||||
CrossSigningSettingsViewEvents.VerifySession -> {
|
||||
(requireActivity() as? VectorBaseActivity)?.let { activity ->
|
||||
activity.navigator.waitSessionVerification(activity)
|
||||
}
|
||||
CrossSigningSettingsViewEvents.SetUpRecovery -> {
|
||||
navigator.upgradeSessionSecurity(requireActivity(), false)
|
||||
}
|
||||
CrossSigningSettingsViewEvents.SetupCrossSigning -> {
|
||||
navigator.upgradeSessionSecurity(requireActivity(), true)
|
||||
}
|
||||
}.exhaustive
|
||||
}
|
||||
|
@ -74,31 +74,29 @@ class CrossSigningSettingsFragment @Inject constructor(
|
|||
}
|
||||
|
||||
override fun invalidate() = withState(viewModel) { state ->
|
||||
epoxyController.setData(state)
|
||||
controller.setData(state)
|
||||
}
|
||||
|
||||
private fun setupRecyclerView() {
|
||||
recyclerView.configureWith(epoxyController, hasFixedSize = false, disableItemAnimation = true)
|
||||
epoxyController.interactionListener = this
|
||||
recyclerView.configureWith(controller, hasFixedSize = false, disableItemAnimation = true)
|
||||
controller.interactionListener = this
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
recyclerView.cleanup()
|
||||
epoxyController.interactionListener = null
|
||||
controller.interactionListener = null
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
private fun requestPassword() {
|
||||
PromptPasswordDialog().show(requireActivity()) { password ->
|
||||
viewModel.handle(CrossSigningAction.PasswordEntered(password))
|
||||
}
|
||||
}
|
||||
|
||||
override fun onInitializeCrossSigningKeys() {
|
||||
viewModel.handle(CrossSigningAction.InitializeCrossSigning)
|
||||
override fun setupRecovery() {
|
||||
viewModel.handle(CrossSigningSettingsAction.SetUpRecovery)
|
||||
}
|
||||
|
||||
override fun verifySession() {
|
||||
viewModel.handle(CrossSigningAction.VerifySession)
|
||||
viewModel.handle(CrossSigningSettingsAction.VerifySession)
|
||||
}
|
||||
|
||||
override fun initCrossSigning() {
|
||||
viewModel.handle(CrossSigningSettingsAction.SetupCrossSigning)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -24,6 +24,7 @@ import im.vector.riotx.core.platform.VectorViewEvents
|
|||
sealed class CrossSigningSettingsViewEvents : VectorViewEvents {
|
||||
data class Failure(val throwable: Throwable) : CrossSigningSettingsViewEvents()
|
||||
|
||||
object RequestPassword : CrossSigningSettingsViewEvents()
|
||||
object SetUpRecovery : CrossSigningSettingsViewEvents()
|
||||
object VerifySession : CrossSigningSettingsViewEvents()
|
||||
object SetupCrossSigning : CrossSigningSettingsViewEvents()
|
||||
}
|
||||
|
|
|
@ -16,126 +16,70 @@
|
|||
package im.vector.riotx.features.settings.crosssigning
|
||||
|
||||
import com.airbnb.mvrx.FragmentViewModelContext
|
||||
import com.airbnb.mvrx.MvRxState
|
||||
import com.airbnb.mvrx.MvRxViewModelFactory
|
||||
import com.airbnb.mvrx.ViewModelContext
|
||||
import com.squareup.inject.assisted.Assisted
|
||||
import com.squareup.inject.assisted.AssistedInject
|
||||
import im.vector.matrix.android.api.MatrixCallback
|
||||
import im.vector.matrix.android.api.auth.data.LoginFlowTypes
|
||||
import im.vector.matrix.android.api.failure.toRegistrationFlowResponse
|
||||
import im.vector.matrix.android.api.session.Session
|
||||
import im.vector.matrix.android.api.session.crypto.crosssigning.MXCrossSigningInfo
|
||||
import im.vector.matrix.android.api.util.Optional
|
||||
import im.vector.matrix.android.internal.crypto.crosssigning.isVerified
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.UserPasswordAuth
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
|
||||
import im.vector.matrix.rx.rx
|
||||
import im.vector.riotx.core.extensions.exhaustive
|
||||
import im.vector.riotx.core.platform.VectorViewModel
|
||||
import im.vector.riotx.core.platform.VectorViewModelAction
|
||||
|
||||
data class CrossSigningSettingsViewState(
|
||||
val crossSigningInfo: MXCrossSigningInfo? = null,
|
||||
val xSigningIsEnableInAccount: Boolean = false,
|
||||
val xSigningKeysAreTrusted: Boolean = false,
|
||||
val xSigningKeyCanSign: Boolean = true,
|
||||
val isUploadingKeys: Boolean = false
|
||||
) : MvRxState
|
||||
|
||||
sealed class CrossSigningAction : VectorViewModelAction {
|
||||
object InitializeCrossSigning : CrossSigningAction()
|
||||
object VerifySession : CrossSigningAction()
|
||||
data class PasswordEntered(val password: String) : CrossSigningAction()
|
||||
}
|
||||
import io.reactivex.Observable
|
||||
import io.reactivex.functions.BiFunction
|
||||
|
||||
class CrossSigningSettingsViewModel @AssistedInject constructor(@Assisted private val initialState: CrossSigningSettingsViewState,
|
||||
private val session: Session)
|
||||
: VectorViewModel<CrossSigningSettingsViewState, CrossSigningAction, CrossSigningSettingsViewEvents>(initialState) {
|
||||
: VectorViewModel<CrossSigningSettingsViewState, CrossSigningSettingsAction, CrossSigningSettingsViewEvents>(initialState) {
|
||||
|
||||
init {
|
||||
session.rx().liveCrossSigningInfo(session.myUserId)
|
||||
.execute {
|
||||
val crossSigningKeys = it.invoke()?.getOrNull()
|
||||
Observable.combineLatest<List<DeviceInfo>, Optional<MXCrossSigningInfo>, Pair<List<DeviceInfo>, Optional<MXCrossSigningInfo>>>(
|
||||
session.rx().liveMyDeviceInfo(),
|
||||
session.rx().liveCrossSigningInfo(session.myUserId),
|
||||
BiFunction { myDeviceInfo, mxCrossSigningInfo ->
|
||||
(myDeviceInfo to mxCrossSigningInfo)
|
||||
}
|
||||
)
|
||||
.execute { data ->
|
||||
val crossSigningKeys = data.invoke()?.second?.getOrNull()
|
||||
val xSigningIsEnableInAccount = crossSigningKeys != null
|
||||
val xSigningKeysAreTrusted = session.cryptoService().crossSigningService().checkUserTrust(session.myUserId).isVerified()
|
||||
val xSigningKeyCanSign = session.cryptoService().crossSigningService().canCrossSign()
|
||||
val hasSeveralDevices = data.invoke()?.first?.size ?: 0 > 1
|
||||
|
||||
copy(
|
||||
crossSigningInfo = crossSigningKeys,
|
||||
xSigningIsEnableInAccount = xSigningIsEnableInAccount,
|
||||
xSigningKeysAreTrusted = xSigningKeysAreTrusted,
|
||||
xSigningKeyCanSign = xSigningKeyCanSign
|
||||
xSigningKeyCanSign = xSigningKeyCanSign,
|
||||
deviceHasToBeVerified = hasSeveralDevices && (xSigningIsEnableInAccount && !xSigningKeyCanSign),
|
||||
recoveryHasToBeSetUp = !session.sharedSecretStorageService.isRecoverySetup()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Storage when password is required
|
||||
private var _pendingSession: String? = null
|
||||
|
||||
@AssistedInject.Factory
|
||||
interface Factory {
|
||||
fun create(initialState: CrossSigningSettingsViewState): CrossSigningSettingsViewModel
|
||||
}
|
||||
|
||||
override fun handle(action: CrossSigningAction) {
|
||||
override fun handle(action: CrossSigningSettingsAction) {
|
||||
when (action) {
|
||||
is CrossSigningAction.InitializeCrossSigning -> {
|
||||
initializeCrossSigning(null)
|
||||
CrossSigningSettingsAction.SetUpRecovery -> {
|
||||
_viewEvents.post(CrossSigningSettingsViewEvents.SetUpRecovery)
|
||||
}
|
||||
is CrossSigningAction.PasswordEntered -> {
|
||||
initializeCrossSigning(UserPasswordAuth(
|
||||
session = _pendingSession,
|
||||
user = session.myUserId,
|
||||
password = action.password
|
||||
))
|
||||
}
|
||||
CrossSigningAction.VerifySession -> {
|
||||
CrossSigningSettingsAction.VerifySession -> {
|
||||
_viewEvents.post(CrossSigningSettingsViewEvents.VerifySession)
|
||||
}
|
||||
CrossSigningSettingsAction.SetupCrossSigning -> {
|
||||
_viewEvents.post(CrossSigningSettingsViewEvents.SetupCrossSigning)
|
||||
}
|
||||
}.exhaustive
|
||||
}
|
||||
|
||||
private fun initializeCrossSigning(auth: UserPasswordAuth?) {
|
||||
_pendingSession = null
|
||||
|
||||
setState {
|
||||
copy(isUploadingKeys = true)
|
||||
}
|
||||
session.cryptoService().crossSigningService().initializeCrossSigning(auth, object : MatrixCallback<Unit> {
|
||||
override fun onSuccess(data: Unit) {
|
||||
_pendingSession = null
|
||||
|
||||
setState {
|
||||
copy(isUploadingKeys = false)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onFailure(failure: Throwable) {
|
||||
_pendingSession = null
|
||||
|
||||
val registrationFlowResponse = failure.toRegistrationFlowResponse()
|
||||
if (registrationFlowResponse != null) {
|
||||
// Retry with authentication
|
||||
if (registrationFlowResponse.flows?.any { it.stages?.contains(LoginFlowTypes.PASSWORD) == true } == true) {
|
||||
_pendingSession = registrationFlowResponse.session ?: ""
|
||||
_viewEvents.post(CrossSigningSettingsViewEvents.RequestPassword)
|
||||
} else {
|
||||
// can't do this from here
|
||||
_viewEvents.post(CrossSigningSettingsViewEvents.Failure(Throwable("You cannot do that from mobile")))
|
||||
|
||||
setState {
|
||||
copy(isUploadingKeys = false)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
_viewEvents.post(CrossSigningSettingsViewEvents.Failure(failure))
|
||||
|
||||
setState {
|
||||
copy(isUploadingKeys = false)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
companion object : MvRxViewModelFactory<CrossSigningSettingsViewModel, CrossSigningSettingsViewState> {
|
||||
|
||||
@JvmStatic
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* 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.settings.crosssigning
|
||||
|
||||
import com.airbnb.mvrx.MvRxState
|
||||
import im.vector.matrix.android.api.session.crypto.crosssigning.MXCrossSigningInfo
|
||||
|
||||
data class CrossSigningSettingsViewState(
|
||||
val crossSigningInfo: MXCrossSigningInfo? = null,
|
||||
val xSigningIsEnableInAccount: Boolean = false,
|
||||
val xSigningKeysAreTrusted: Boolean = false,
|
||||
val xSigningKeyCanSign: Boolean = true,
|
||||
|
||||
val deviceHasToBeVerified: Boolean = false,
|
||||
val recoveryHasToBeSetUp: Boolean = false
|
||||
) : MvRxState
|
|
@ -54,7 +54,7 @@ class DeviceVerificationInfoBottomSheetViewModel @AssistedInject constructor(@As
|
|||
|
||||
setState {
|
||||
copy(
|
||||
hasAccountCrossSigning = session.cryptoService().crossSigningService().getMyCrossSigningKeys() != null,
|
||||
hasAccountCrossSigning = session.cryptoService().crossSigningService().isCrossSigningInitialized(),
|
||||
accountCrossSigningIsTrusted = session.cryptoService().crossSigningService().isCrossSigningVerified()
|
||||
)
|
||||
}
|
||||
|
|
|
@ -89,7 +89,7 @@ class DeviceVerificationInfoEpoxyController @Inject constructor(private val stri
|
|||
description(stringProvider.getString(R.string.settings_active_sessions_verified_device_desc))
|
||||
}
|
||||
} else {
|
||||
// You need tomcomplete security
|
||||
// You need to complete security
|
||||
genericItem {
|
||||
id("trust${cryptoDeviceInfo.deviceId}")
|
||||
style(GenericItem.STYLE.BIG_TEXT)
|
||||
|
|
|
@ -95,7 +95,7 @@ class DevicesViewModel @AssistedInject constructor(
|
|||
|
||||
setState {
|
||||
copy(
|
||||
hasAccountCrossSigning = session.cryptoService().crossSigningService().getMyCrossSigningKeys() != null,
|
||||
hasAccountCrossSigning = session.cryptoService().crossSigningService().isCrossSigningInitialized(),
|
||||
accountCrossSigningIsTrusted = session.cryptoService().crossSigningService().isCrossSigningVerified(),
|
||||
myDeviceId = session.sessionParams.deviceId ?: ""
|
||||
)
|
||||
|
|
|
@ -1,19 +0,0 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="22dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="22"
|
||||
android:viewportHeight="24">
|
||||
<group>
|
||||
<clip-path
|
||||
android:pathData="M0,0h22v6h-22zM0,17h22v7h-22z"/>
|
||||
<path
|
||||
android:pathData="M19,14C19,16.0333 17.7458,17.9018 16.043,19.4808C14.3615,21.0401 12.4,22.1689 11.3349,22.7219C11.1216,22.8327 10.8784,22.8327 10.6651,22.7219C9.6,22.1689 7.6385,21.0401 5.957,19.4808C4.2542,17.9018 3,16.0333 3,14V3.6043C3,3.1356 3.3255,2.7298 3.7831,2.6282L10.7831,1.0726C10.9259,1.0409 11.0741,1.0409 11.2169,1.0726L18.2169,2.6282C18.6745,2.7298 19,3.1356 19,3.6043V14Z"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#2E2F32"/>
|
||||
</group>
|
||||
<path
|
||||
android:pathData="M2,8C0.8954,8 0,8.8954 0,10V13C0,14.1046 0.8954,15 2,15H20C21.1046,15 22,14.1046 22,13V10C22,8.8954 21.1046,8 20,8H2ZM4.25,9.5C3.8358,9.5 3.5,9.8358 3.5,10.25C3.5,10.6642 3.8358,11 4.25,11H6.75C7.1642,11 7.5,10.6642 7.5,10.25C7.5,9.8358 7.1642,9.5 6.75,9.5H4.25ZM8.5,10.25C8.5,9.8358 8.8358,9.5 9.25,9.5H9.75C10.1642,9.5 10.5,9.8358 10.5,10.25C10.5,10.6642 10.1642,11 9.75,11H9.25C8.8358,11 8.5,10.6642 8.5,10.25ZM12.25,9.5C11.8358,9.5 11.5,9.8358 11.5,10.25C11.5,10.6642 11.8358,11 12.25,11H14.75C15.1642,11 15.5,10.6642 15.5,10.25C15.5,9.8358 15.1642,9.5 14.75,9.5H12.25ZM16.5,10.25C16.5,9.8358 16.8358,9.5 17.25,9.5H17.75C18.1642,9.5 18.5,9.8358 18.5,10.25C18.5,10.6642 18.1642,11 17.75,11H17.25C16.8358,11 16.5,10.6642 16.5,10.25ZM4.25,12C3.8358,12 3.5,12.3358 3.5,12.75C3.5,13.1642 3.8358,13.5 4.25,13.5H4.75C5.1642,13.5 5.5,13.1642 5.5,12.75C5.5,12.3358 5.1642,12 4.75,12H4.25ZM6.5,12.75C6.5,12.3358 6.8358,12 7.25,12H9.75C10.1642,12 10.5,12.3358 10.5,12.75C10.5,13.1642 10.1642,13.5 9.75,13.5H7.25C6.8358,13.5 6.5,13.1642 6.5,12.75ZM12.25,12C11.8358,12 11.5,12.3358 11.5,12.75C11.5,13.1642 11.8358,13.5 12.25,13.5H12.75C13.1642,13.5 13.5,13.1642 13.5,12.75C13.5,12.3358 13.1642,12 12.75,12H12.25Z"
|
||||
android:fillColor="#2E2F32"
|
||||
android:fillType="evenOdd"/>
|
||||
</vector>
|
|
@ -1,19 +0,0 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="22dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="22"
|
||||
android:viewportHeight="24">
|
||||
<group>
|
||||
<clip-path
|
||||
android:pathData="M0,0h22v6h-22zM0,17h22v7h-22z"/>
|
||||
<path
|
||||
android:pathData="M11.3349,22.7219C11.1216,22.8327 10.8784,22.8327 10.6651,22.7219C9.6,22.1689 7.6385,21.0401 5.957,19.4808C4.2542,17.9018 3,16.0333 3,14V3.6043C3,3.1356 3.3255,2.7298 3.7831,2.6282L10.7831,1.0726C10.9259,1.0409 11.0741,1.0409 11.2169,1.0726L18.2169,2.6282C18.6745,2.7298 19,3.1356 19,3.6043V14C19,16.0333 17.7458,17.9018 16.043,19.4808C14.3615,21.0401 12.4,22.1689 11.3349,22.7219Z"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#2E2F32"/>
|
||||
</group>
|
||||
<path
|
||||
android:pathData="M0,10C0,8.8954 0.8954,8 2,8H20C21.1046,8 22,8.8954 22,10V13C22,14.1046 21.1046,15 20,15H2C0.8954,15 0,14.1046 0,13V10ZM5,11.5C5,12.3284 4.3284,13 3.5,13C2.6716,13 2,12.3284 2,11.5C2,10.6716 2.6716,10 3.5,10C4.3284,10 5,10.6716 5,11.5ZM8.5,13C9.3284,13 10,12.3284 10,11.5C10,10.6716 9.3284,10 8.5,10C7.6716,10 7,10.6716 7,11.5C7,12.3284 7.6716,13 8.5,13ZM15,11.5C15,12.3284 14.3284,13 13.5,13C12.6716,13 12,12.3284 12,11.5C12,10.6716 12.6716,10 13.5,10C14.3284,10 15,10.6716 15,11.5ZM18.5,13C19.3284,13 20,12.3284 20,11.5C20,10.6716 19.3284,10 18.5,10C17.6716,10 17,10.6716 17,11.5C17,12.3284 17.6716,13 18.5,13Z"
|
||||
android:fillColor="#2E2F32"
|
||||
android:fillType="evenOdd"/>
|
||||
</vector>
|
23
vector/src/main/res/drawable/ic_security_key_24dp.xml
Normal file
23
vector/src/main/res/drawable/ic_security_key_24dp.xml
Normal file
|
@ -0,0 +1,23 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<group>
|
||||
<clip-path android:pathData="M1,2h21.5v5h-21.5zM1,17.7h21.5v5h-21.5z" />
|
||||
<path
|
||||
android:fillColor="#2E2F32"
|
||||
android:pathData="M3.1663,12.4014C3.1663,7.6467 6.9953,3.8177 11.75,3.8177C13.0964,3.8177 14.4429,4.1544 15.6631,4.7435H14.9899C14.5691,4.7435 14.2325,5.0801 14.2325,5.5008C14.2325,5.9216 14.5691,6.2582 14.9899,6.2582H17.3041C17.809,6.2582 18.1877,5.8375 18.1877,5.3746V3.0604C18.1877,2.6396 17.8511,2.303 17.4303,2.303C17.0096,2.303 16.673,2.6396 16.673,3.0604V3.6074C16.6309,3.5653 16.5888,3.5653 16.5467,3.5232C15.074,2.7238 13.433,2.303 11.75,2.303C6.1958,2.303 1.6515,6.8473 1.6515,12.4014C1.6515,14.0845 2.0723,15.7676 2.8717,17.2403C2.998,17.4928 3.2504,17.619 3.545,17.619C3.6712,17.619 3.7974,17.5769 3.9236,17.5348C4.3023,17.3245 4.4286,16.8616 4.2182,16.525C3.5029,15.2627 3.1663,13.8321 3.1663,12.4014Z"
|
||||
tools:fillColor="#FF0000" />
|
||||
<path
|
||||
android:fillColor="#2E2F32"
|
||||
android:pathData="M20.6281,7.5626C20.4177,7.1839 19.9548,7.0577 19.6182,7.2681C19.2395,7.4785 19.1133,7.9413 19.3237,8.2779C19.9969,9.5402 20.3756,10.9288 20.3756,12.4015C20.3756,17.1562 16.5045,20.9852 11.7919,20.9852C10.4454,20.9852 9.099,20.6486 7.8787,20.0595H8.552C8.9727,20.0595 9.3094,19.7229 9.3094,19.3021C9.3094,18.8813 8.9727,18.5447 8.552,18.5447H6.2377C5.7328,18.5447 5.3541,18.9655 5.3541,19.4283V21.7426C5.3541,22.1633 5.6908,22.4999 6.1115,22.4999C6.5323,22.4999 6.8689,22.1633 6.8689,21.7426V21.1956C6.911,21.2376 6.9531,21.2376 6.9951,21.2797C8.4257,22.0792 10.0667,22.4999 11.7498,22.4999C17.304,22.4999 21.8483,17.9556 21.8483,12.4015C21.8483,10.7184 21.4275,9.0353 20.6281,7.5626Z"
|
||||
tools:fillColor="#FF0000" />
|
||||
</group>
|
||||
<path
|
||||
android:fillColor="#2E2F32"
|
||||
android:fillType="evenOdd"
|
||||
android:pathData="M3,9C1.8954,9 1,9.8954 1,11V14C1,15.1046 1.8954,16 3,16H21C22.1046,16 23,15.1046 23,14V11C23,9.8954 22.1046,9 21,9H3ZM5.25,10.5C4.8358,10.5 4.5,10.8358 4.5,11.25C4.5,11.6642 4.8358,12 5.25,12H7.75C8.1642,12 8.5,11.6642 8.5,11.25C8.5,10.8358 8.1642,10.5 7.75,10.5H5.25ZM9.5,11.25C9.5,10.8358 9.8358,10.5 10.25,10.5H10.75C11.1642,10.5 11.5,10.8358 11.5,11.25C11.5,11.6642 11.1642,12 10.75,12H10.25C9.8358,12 9.5,11.6642 9.5,11.25ZM13.25,10.5C12.8358,10.5 12.5,10.8358 12.5,11.25C12.5,11.6642 12.8358,12 13.25,12H15.75C16.1642,12 16.5,11.6642 16.5,11.25C16.5,10.8358 16.1642,10.5 15.75,10.5H13.25ZM17.5,11.25C17.5,10.8358 17.8358,10.5 18.25,10.5H18.75C19.1642,10.5 19.5,10.8358 19.5,11.25C19.5,11.6642 19.1642,12 18.75,12H18.25C17.8358,12 17.5,11.6642 17.5,11.25ZM5.25,13C4.8358,13 4.5,13.3358 4.5,13.75C4.5,14.1642 4.8358,14.5 5.25,14.5H5.75C6.1642,14.5 6.5,14.1642 6.5,13.75C6.5,13.3358 6.1642,13 5.75,13H5.25ZM7.5,13.75C7.5,13.3358 7.8358,13 8.25,13H10.75C11.1642,13 11.5,13.3358 11.5,13.75C11.5,14.1642 11.1642,14.5 10.75,14.5H8.25C7.8358,14.5 7.5,14.1642 7.5,13.75ZM13.25,13C12.8358,13 12.5,13.3358 12.5,13.75C12.5,14.1642 12.8358,14.5 13.25,14.5H13.75C14.1642,14.5 14.5,14.1642 14.5,13.75C14.5,13.3358 14.1642,13 13.75,13H13.25Z"
|
||||
tools:fillColor="#FF0000" />
|
||||
</vector>
|
23
vector/src/main/res/drawable/ic_security_phrase_24dp.xml
Normal file
23
vector/src/main/res/drawable/ic_security_phrase_24dp.xml
Normal file
|
@ -0,0 +1,23 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<group>
|
||||
<clip-path android:pathData="M1,2h21.5v5h-21.5zM1,17.7h21.5v5h-21.5z" />
|
||||
<path
|
||||
android:fillColor="#2E2F32"
|
||||
android:pathData="M3.1663,12.4014C3.1663,7.6467 6.9953,3.8177 11.75,3.8177C13.0964,3.8177 14.4429,4.1544 15.6631,4.7435H14.9899C14.5691,4.7435 14.2325,5.0801 14.2325,5.5008C14.2325,5.9216 14.5691,6.2582 14.9899,6.2582H17.3041C17.809,6.2582 18.1877,5.8375 18.1877,5.3746V3.0604C18.1877,2.6396 17.8511,2.303 17.4303,2.303C17.0096,2.303 16.673,2.6396 16.673,3.0604V3.6074C16.6309,3.5653 16.5888,3.5653 16.5467,3.5232C15.074,2.7238 13.433,2.303 11.75,2.303C6.1958,2.303 1.6515,6.8473 1.6515,12.4014C1.6515,14.0845 2.0723,15.7676 2.8717,17.2403C2.998,17.4928 3.2504,17.619 3.545,17.619C3.6712,17.619 3.7974,17.5769 3.9236,17.5348C4.3023,17.3245 4.4286,16.8616 4.2182,16.525C3.5029,15.2627 3.1663,13.8321 3.1663,12.4014Z"
|
||||
tools:fillColor="#FF0000" />
|
||||
<path
|
||||
android:fillColor="#2E2F32"
|
||||
android:pathData="M20.6281,7.5626C20.4177,7.1839 19.9548,7.0577 19.6182,7.2681C19.2395,7.4785 19.1133,7.9413 19.3237,8.2779C19.9969,9.5402 20.3756,10.9288 20.3756,12.4015C20.3756,17.1562 16.5045,20.9852 11.7919,20.9852C10.4454,20.9852 9.099,20.6486 7.8787,20.0595H8.552C8.9727,20.0595 9.3094,19.7229 9.3094,19.3021C9.3094,18.8813 8.9727,18.5447 8.552,18.5447H6.2377C5.7328,18.5447 5.3541,18.9655 5.3541,19.4283V21.7426C5.3541,22.1633 5.6908,22.4999 6.1115,22.4999C6.5323,22.4999 6.8689,22.1633 6.8689,21.7426V21.1956C6.911,21.2376 6.9531,21.2376 6.9951,21.2797C8.4257,22.0792 10.0667,22.4999 11.7498,22.4999C17.304,22.4999 21.8483,17.9556 21.8483,12.4015C21.8483,10.7184 21.4275,9.0353 20.6281,7.5626Z"
|
||||
tools:fillColor="#FF0000" />
|
||||
</group>
|
||||
<path
|
||||
android:fillColor="#2E2F32"
|
||||
android:fillType="evenOdd"
|
||||
android:pathData="M1,11C1,9.8954 1.8954,9 3,9H21C22.1046,9 23,9.8954 23,11V14C23,15.1046 22.1046,16 21,16H3C1.8954,16 1,15.1046 1,14V11ZM6,12.5C6,13.3284 5.3284,14 4.5,14C3.6716,14 3,13.3284 3,12.5C3,11.6716 3.6716,11 4.5,11C5.3284,11 6,11.6716 6,12.5ZM9.5,14C10.3284,14 11,13.3284 11,12.5C11,11.6716 10.3284,11 9.5,11C8.6716,11 8,11.6716 8,12.5C8,13.3284 8.6716,14 9.5,14ZM16,12.5C16,13.3284 15.3284,14 14.5,14C13.6716,14 13,13.3284 13,12.5C13,11.6716 13.6716,11 14.5,11C15.3284,11 16,11.6716 16,12.5ZM19.5,14C20.3284,14 21,13.3284 21,12.5C21,11.6716 20.3284,11 19.5,11C18.6716,11 18,11.6716 18,12.5C18,13.3284 18.6716,14 19.5,14Z"
|
||||
tools:fillColor="#FF0000" />
|
||||
</vector>
|
|
@ -1,21 +1,24 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M20,21V19C20,16.7909 18.2091,15 16,15H8C5.7909,15 4,16.7909 4,19V21"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#2E2F32"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:pathData="M12,11C14.2091,11 16,9.2091 16,7C16,4.7909 14.2091,3 12,3C9.7909,3 8,4.7909 8,7C8,9.2091 9.7909,11 12,11Z"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:fillType="evenOdd"
|
||||
android:strokeColor="#2E2F32"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M20,21V19C20,16.7909 18.2091,15 16,15H8C5.7909,15 4,16.7909 4,19V21"
|
||||
android:strokeWidth="2"
|
||||
android:strokeColor="#2E2F32"
|
||||
android:strokeLineCap="round"
|
||||
android:strokeLineJoin="round"
|
||||
tools:strokeColor="#FF0000" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:fillType="evenOdd"
|
||||
android:pathData="M12,11C14.2091,11 16,9.2091 16,7C16,4.7909 14.2091,3 12,3C9.7909,3 8,4.7909 8,7C8,9.2091 9.7909,11 12,11Z"
|
||||
android:strokeWidth="2"
|
||||
android:strokeColor="#2E2F32"
|
||||
android:strokeLineCap="round"
|
||||
android:strokeLineJoin="round"
|
||||
tools:strokeColor="#FF0000" />
|
||||
</vector>
|
||||
|
|
|
@ -20,9 +20,10 @@
|
|||
android:layout_width="32dp"
|
||||
android:layout_height="32dp"
|
||||
android:layout_marginStart="16dp"
|
||||
android:contentDescription="@string/avatar"
|
||||
android:importantForAccessibility="no"
|
||||
android:scaleType="fitCenter"
|
||||
android:src="@drawable/ic_message_password"
|
||||
android:src="@drawable/ic_security_key_24dp"
|
||||
android:tint="?riotx_text_primary"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
|
@ -30,7 +31,7 @@
|
|||
android:id="@+id/bootstrapTitleText"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:ellipsize="end"
|
||||
android:textColor="?riotx_text_primary"
|
||||
|
@ -39,7 +40,7 @@
|
|||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@+id/bootstrapIcon"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:text="@string/recovery_passphrase" />
|
||||
tools:text="@string/bottom_sheet_setup_secure_backup_title" />
|
||||
|
||||
<androidx.fragment.app.FragmentContainerView
|
||||
android:id="@+id/bottomSheetFragmentContainer"
|
||||
|
|
|
@ -27,9 +27,10 @@
|
|||
android:id="@+id/bootstrapIcon"
|
||||
android:layout_width="32dp"
|
||||
android:layout_height="32dp"
|
||||
android:contentDescription="@string/avatar"
|
||||
android:importantForAccessibility="no"
|
||||
android:scaleType="fitCenter"
|
||||
android:src="@drawable/ic_message_key" />
|
||||
android:src="@drawable/ic_security_key_24dp"
|
||||
android:tint="?riotx_text_primary" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/bootstrapTitleText"
|
||||
|
@ -49,6 +50,7 @@
|
|||
android:id="@+id/keepItSafeText"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="?riotx_text_secondary"
|
||||
android:textSize="16sp"
|
||||
tools:text="@string/bootstrap_crosssigning_save_usb" />
|
||||
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:text="@string/bootstrap_info_text"
|
||||
android:text="@string/bootstrap_info_text_2"
|
||||
android:textColor="?riotx_text_primary"
|
||||
android:textSize="14sp"
|
||||
app:layout_constraintBottom_toTopOf="@id/ssss_passphrase_enter_til"
|
||||
|
|
|
@ -15,10 +15,9 @@
|
|||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:text="@string/bootstrap_save_key_description"
|
||||
android:text="@string/bottom_sheet_save_your_recovery_key_content"
|
||||
android:textColor="?riotx_text_primary"
|
||||
android:textSize="14sp"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
android:textSize="14sp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/bootstrapRecoveryKeyText"
|
||||
|
@ -44,7 +43,7 @@
|
|||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:minHeight="50dp"
|
||||
app:actionTitle="@string/copy_value"
|
||||
app:actionTitle="@string/action_copy"
|
||||
app:leftIcon="@drawable/ic_clipboard"
|
||||
app:rightIcon="@drawable/ic_arrow_right"
|
||||
app:tint="?colorAccent" />
|
||||
|
|
|
@ -0,0 +1,74 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<LinearLayout 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="wrap_content"
|
||||
android:minHeight="200dp"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/bootstrapSetupSecureText"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:text="@string/bottom_sheet_setup_secure_backup_subtitle"
|
||||
android:textColor="?riotx_text_primary"
|
||||
android:textSize="14sp" />
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:layout_marginTop="@dimen/layout_vertical_margin"
|
||||
android:background="?attr/vctr_list_divider_color" />
|
||||
|
||||
<im.vector.riotx.core.ui.views.BottomSheetActionButton
|
||||
android:id="@+id/bootstrapSetupSecureSubmit"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:minHeight="50dp"
|
||||
app:actionTitle="@string/bottom_sheet_setup_secure_backup_submit"
|
||||
app:rightIcon="@drawable/ic_arrow_right"
|
||||
app:tint="?colorAccent" />
|
||||
|
||||
<im.vector.riotx.core.ui.views.BottomSheetActionButton
|
||||
android:id="@+id/bootstrapSetupSecureUseSecurityKey"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:minHeight="50dp"
|
||||
android:visibility="gone"
|
||||
app:actionDescription="@string/bottom_sheet_setup_secure_backup_security_key_subtitle"
|
||||
app:actionTitle="@string/bottom_sheet_setup_secure_backup_security_key_title"
|
||||
app:leftIcon="@drawable/ic_security_key_24dp"
|
||||
app:rightIcon="@drawable/ic_arrow_right"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<View
|
||||
android:id="@+id/bootstrapSetupSecureUseSecurityPassphraseSeparator"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:background="?attr/vctr_list_divider_color"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<im.vector.riotx.core.ui.views.BottomSheetActionButton
|
||||
android:id="@+id/bootstrapSetupSecureUseSecurityPassphrase"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:minHeight="50dp"
|
||||
android:visibility="gone"
|
||||
app:actionDescription="@string/bottom_sheet_setup_secure_backup_security_phrase_subtitle"
|
||||
app:actionTitle="@string/bottom_sheet_setup_secure_backup_security_phrase_title"
|
||||
app:leftIcon="@drawable/ic_security_phrase_24dp"
|
||||
app:rightIcon="@drawable/ic_arrow_right"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:background="?attr/vctr_list_divider_color" />
|
||||
|
||||
</LinearLayout>
|
|
@ -19,7 +19,7 @@
|
|||
app:layout_constraintBottom_toBottomOf="@+id/ssss_restore_with_key"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@+id/ssss_restore_with_key"
|
||||
android:src="@drawable/ic_message_key" />
|
||||
android:src="@drawable/ic_security_key_24dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/ssss_restore_with_key"
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
app:layout_constraintBottom_toBottomOf="@+id/ssss_restore_with_passphrase"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@+id/ssss_restore_with_passphrase"
|
||||
android:src="@drawable/ic_message_password" />
|
||||
android:src="@drawable/ic_security_phrase_24dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/ssss_restore_with_passphrase"
|
||||
|
@ -98,7 +98,7 @@
|
|||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/use_recovery_key"
|
||||
app:icon="@drawable/ic_message_key"
|
||||
app:icon="@drawable/ic_security_key_24dp"
|
||||
tools:ignore="MissingConstraints" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
|
|
|
@ -2,7 +2,6 @@
|
|||
<resources>
|
||||
<!-- use to retrieve the supported languages list -->
|
||||
<!-- should the same value as the file name -->
|
||||
|
||||
<string name="resources_language">en</string>
|
||||
<string name="resources_country_code">US</string>
|
||||
<!-- NOTE TO TRANSLATORS: Value MUST have 4 letters and MUST be in this list: https://www.unicode.org/iso15924/iso15924-codes.html. Example: "Arab", "Cyrl", "Latn", etc. -->
|
||||
|
@ -35,7 +34,6 @@
|
|||
<string name="title_activity_verify_device">Verify session</string>
|
||||
|
||||
<!-- Signing out screen -->
|
||||
|
||||
<string name="keys_backup_is_not_finished_please_wait">Keys backup is not finished, please wait…</string>
|
||||
<string name="sign_out_bottom_sheet_warning_no_backup">You’ll lose your encrypted messages if you sign out now</string>
|
||||
<string name="sign_out_bottom_sheet_warning_backing_up">Key backup in progress. If you sign out now you’ll lose access to your encrypted messages.</string>
|
||||
|
@ -46,7 +44,6 @@
|
|||
<string name="are_you_sure">Are you sure?</string>
|
||||
<string name="backup">Back up</string>
|
||||
<string name="sign_out_bottom_sheet_will_lose_secure_messages">You’ll lose access to your encrypted messages unless you back up your keys before signing out.</string>
|
||||
|
||||
<string name="dialog_title_third_party_licences">Third party licences</string>
|
||||
|
||||
<!-- splash screen accessibility -->
|
||||
|
@ -117,6 +114,7 @@
|
|||
<string name="action_mark_room_read">Mark as read</string>
|
||||
<string name="action_open">Open</string>
|
||||
<string name="action_close">Close</string>
|
||||
<string name="action_copy">Copy</string>
|
||||
<string name="copied_to_clipboard">Copied to clipboard</string>
|
||||
<string name="disable">Disable</string>
|
||||
|
||||
|
@ -2078,8 +2076,6 @@ Not all features in Riot are implemented in RiotX yet. Main missing (and coming
|
|||
<string name="verification_request_you_accepted">You accepted</string>
|
||||
<string name="verification_sent">Verification Sent</string>
|
||||
<string name="verification_request">Verification Request</string>
|
||||
|
||||
|
||||
<string name="verification_verify_device">Verify this session</string>
|
||||
<string name="verification_verify_device_manually">Manually verify</string>
|
||||
|
||||
|
@ -2166,7 +2162,6 @@ Not all features in Riot are implemented in RiotX yet. Main missing (and coming
|
|||
<string name="verification_conclusion_ok_notice">Messages with this user are end-to-end encrypted and can\'t be read by third parties.</string>
|
||||
<string name="verification_conclusion_ok_self_notice">Your new session is now verified. It has access to your encrypted messages, and other users will see it as trusted.</string>
|
||||
|
||||
|
||||
<string name="encryption_information_cross_signing_state">Cross-Signing</string>
|
||||
<string name="encryption_information_dg_xsigning_complete">Cross-Signing is enabled\nPrivate Keys on device.</string>
|
||||
<string name="encryption_information_dg_xsigning_trusted">Cross-Signing is enabled\nKeys are trusted.\nPrivate keys are not known</string>
|
||||
|
@ -2305,8 +2300,9 @@ Not all features in Riot are implemented in RiotX yet. Main missing (and coming
|
|||
<string name="bootstrap_info_text">Secure & unlock encrypted messages and trust with a %s.</string>
|
||||
<!-- %s will be replaced by recovery_passphrase -->
|
||||
<string name="bootstrap_info_confirm_text">Enter your %s again to confirm it.</string>
|
||||
<string name="bootstrap_dont_reuse_pwd">Don’t re-use your account password.</string>
|
||||
<string name="bootstrap_dont_reuse_pwd">Don’t use your account password.</string>
|
||||
|
||||
<string name="bootstrap_info_text_2">Enter a security phrase only you know, used to secure secrets on your server.</string>
|
||||
|
||||
<string name="bootstrap_loading_text">This might take several seconds, please be patient.</string>
|
||||
<string name="bootstrap_loading_title">Setting up recovery.</string>
|
||||
|
@ -2339,7 +2335,7 @@ Not all features in Riot are implemented in RiotX yet. Main missing (and coming
|
|||
|
||||
<string name="bootstrap_skip_text">Setting a Recovery Passphrase lets you secure & unlock encrypted messages and trust.\n\nIf you don’t want to set a Message Password, generate a Message Key instead.</string>
|
||||
<string name="bootstrap_skip_text_no_gen_key">Setting a Recovery Passphrase lets you secure & unlock encrypted messages and trust.</string>
|
||||
|
||||
<string name="bootstrap_cancel_text">If you cancel now, you may lose encrypted messages & data if you lose access to your logins.\n\nYou can also set up Secure Backup & manage your keys in Settings.</string>
|
||||
|
||||
<string name="encryption_enabled">Encryption enabled</string>
|
||||
<string name="encryption_enabled_tile_description">Messages in this room are end-to-end encrypted. Learn more & verify users in their profile.</string>
|
||||
|
@ -2370,6 +2366,7 @@ Not all features in Riot are implemented in RiotX yet. Main missing (and coming
|
|||
<string name="room_message_placeholder">Message…</string>
|
||||
|
||||
<string name="upgrade_security">Encryption upgrade available</string>
|
||||
<string name="setup_cross_signing">Enable Cross Signing</string>
|
||||
<string name="security_prompt_text">Verify yourself & others to keep your chats safe</string>
|
||||
|
||||
<!-- %s will be replaced by recovery_key -->
|
||||
|
@ -2478,4 +2475,25 @@ Not all features in Riot are implemented in RiotX yet. Main missing (and coming
|
|||
<string name="a11y_stop_camera">Stop the camera</string>
|
||||
<string name="a11y_start_camera">Start the camera</string>
|
||||
|
||||
</resources>
|
||||
<string name="settings_setup_secure_backup">Set up Secure Backup</string>
|
||||
|
||||
<string name="bottom_sheet_setup_secure_backup_title">Secure backup</string>
|
||||
<string name="bottom_sheet_setup_secure_backup_subtitle">Safeguard against losing access to encrypted messages & data by backing up encryption keys on your server.</string>
|
||||
<string name="bottom_sheet_setup_secure_backup_submit">Set up</string>
|
||||
<string name="bottom_sheet_setup_secure_backup_security_key_title">Use a Security Key</string>
|
||||
<string name="bottom_sheet_setup_secure_backup_security_key_subtitle">Generate a security key to store somewhere safe like a password manager or a safe.</string>
|
||||
<string name="bottom_sheet_setup_secure_backup_security_phrase_title">Use a Security Phrase</string>
|
||||
<string name="bottom_sheet_setup_secure_backup_security_phrase_subtitle">Enter a secret phrase only you know, and generate a key for backup.</string>
|
||||
|
||||
<string name="bottom_sheet_save_your_recovery_key_title">Save your Security Key</string>
|
||||
<string name="bottom_sheet_save_your_recovery_key_content">Store your Security Key somewhere safe, like a password manager or a safe.</string>
|
||||
|
||||
<string name="set_a_security_phrase_title">Set a Security Phrase</string>
|
||||
<string name="set_a_security_phrase_notice">Enter a security phrase only you know, used to secure secrets on your server.</string>
|
||||
<string name="set_a_security_phrase_hint">Security Phrase</string>
|
||||
<string name="set_a_security_phrase_again_notice">Enter your Security Phrase again to confirm it.</string>
|
||||
|
||||
<string name="save_your_security_key_title">Save your Security Key</string>
|
||||
<string name="save_your_security_key_notice">Store your Security Key somewhere safe, like a password manager or a safe.</string>
|
||||
|
||||
</resources>
|
Loading…
Add table
Reference in a new issue