diff --git a/CHANGES.md b/CHANGES.md index 086b8f49e3..756b3b23fc 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -7,7 +7,6 @@ Features ✨: - Cross-Signing | Verify new session from existing session (#1134) - Cross-Signing | Bootstraping cross signing with 4S from mobile (#985) - Improvements πŸ™Œ: - Verification DM / Handle concurrent .start after .ready (#794) - Reimplementation of multiple attachment picker @@ -21,6 +20,7 @@ Improvements πŸ™Œ: - Cross-Sign | QR code scan confirmation screens design update (#1187) - Emoji Verification | It's not the same butterfly! (#1220) - Cross-Signing | Composer decoration: shields (#1077) + - Cross-Signing | Migrate existing keybackup to cross signing with 4S from mobile (#1197) Bugfix πŸ›: - Fix summary notification staying after "mark as read" @@ -34,6 +34,7 @@ Bugfix πŸ›: - Local echo are not updated in timeline (for failed & encrypted states) - Render image event even if thumbnail_info does not have mimetype defined (#1209) - RiotX now uses as many threads as it needs to do work and send messages (#1221) + - Fix issue with media path (#1227) Translations πŸ—£: - diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/ssss/QuadSTests.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/ssss/QuadSTests.kt index 1a0723c725..cbd175f53f 100644 --- a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/ssss/QuadSTests.kt +++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/ssss/QuadSTests.kt @@ -71,7 +71,7 @@ class QuadSTests : InstrumentedTest { val TEST_KEY_ID = "my.test.Key" mTestHelper.doSync { - quadS.generateKey(TEST_KEY_ID, "Test Key", emptyKeySigner, it) + quadS.generateKey(TEST_KEY_ID, null, "Test Key", emptyKeySigner, it) } // Assert Account data is updated @@ -177,7 +177,7 @@ class QuadSTests : InstrumentedTest { val TEST_KEY_ID = "my.test.Key" mTestHelper.doSync { - quadS.generateKey(TEST_KEY_ID, "Test Key", emptyKeySigner, it) + quadS.generateKey(TEST_KEY_ID, null, "Test Key", emptyKeySigner, it) } // Test that we don't need to wait for an account data sync to access directly the keyid from DB @@ -322,7 +322,7 @@ class QuadSTests : InstrumentedTest { val quadS = session.sharedSecretStorageService val creationInfo = mTestHelper.doSync { - quadS.generateKey(keyId, keyId, emptyKeySigner, it) + quadS.generateKey(keyId, null, keyId, emptyKeySigner, it) } assertAccountData(session, "${DefaultSharedSecretStorageService.KEY_ID_BASE}.$keyId") diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/content/ContentUrlResolver.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/content/ContentUrlResolver.kt index 9ba1631aec..6ed0b92e38 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/content/ContentUrlResolver.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/content/ContentUrlResolver.kt @@ -26,6 +26,11 @@ interface ContentUrlResolver { SCALE("scale") } + /** + * URL to use to upload content + */ + val uploadUrl: String + /** * Get the actual URL for accessing the full-size image of a Matrix media content URI. * diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/keysbackup/KeysBackupService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/keysbackup/KeysBackupService.kt index e9ed36ba23..9ad39d45fd 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/keysbackup/KeysBackupService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/keysbackup/KeysBackupService.kt @@ -217,4 +217,6 @@ interface KeysBackupService { // For gossiping fun saveBackupRecoveryKey(recoveryKey: String?, version: String?) fun getKeyBackupRecoveryKeyInfo() : SavedKeyBackupKeyInfo? + + fun isValidRecoveryKeyForCurrentVersion(recoveryKey: String, callback: MatrixCallback) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageLocationContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageLocationContent.kt index 02ba9eae6e..4bbd821819 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageLocationContent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageLocationContent.kt @@ -29,7 +29,8 @@ data class MessageLocationContent( @Json(name = "msgtype") override val msgType: String, /** - * Required. A description of the location e.g. 'Big Ben, London, UK', or some kind of content description for accessibility e.g. 'location attachment'. + * Required. A description of the location e.g. 'Big Ben, London, UK', or some kind + * of content description for accessibility e.g. 'location attachment'. */ @Json(name = "body") override val body: String, diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/securestorage/SharedSecretStorageService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/securestorage/SharedSecretStorageService.kt index d32e459dd6..5fd0975f1a 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/securestorage/SharedSecretStorageService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/securestorage/SharedSecretStorageService.kt @@ -35,12 +35,14 @@ interface SharedSecretStorageService { * Use the SsssKeyCreationInfo object returned by the callback to get more information about the created key (recovery key ...) * * @param keyId the ID of the key + * @param key keep null if you want to generate a random key * @param keyName a human readable name * @param keySigner Used to add a signature to the key (client should check key signature before storing secret) * * @param callback Get key creation info */ fun generateKey(keyId: String, + key: SsssKeySpec?, keyName: String, keySigner: KeySigner?, callback: MatrixCallback) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/DefaultKeysBackupService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/DefaultKeysBackupService.kt index 75e37d27f6..9245f77317 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/DefaultKeysBackupService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/DefaultKeysBackupService.kt @@ -1100,6 +1100,16 @@ internal class DefaultKeysBackupService @Inject constructor( return true } + override fun isValidRecoveryKeyForCurrentVersion(recoveryKey: String, callback: MatrixCallback) { + val safeKeysBackupVersion = keysBackupVersion ?: return Unit.also { callback.onSuccess(false) } + + cryptoCoroutineScope.launch(coroutineDispatchers.main) { + isValidRecoveryKeyForKeysBackupVersion(recoveryKey, safeKeysBackupVersion).let { + callback.onSuccess(it) + } + } + } + /** * Enable backing up of keys. * This method will update the state and will start sending keys in nominal case diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/model/rest/CreateKeysBackupVersionBody.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/model/rest/CreateKeysBackupVersionBody.kt index 3b267280e5..f0c0ada207 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/model/rest/CreateKeysBackupVersionBody.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/model/rest/CreateKeysBackupVersionBody.kt @@ -29,7 +29,8 @@ data class CreateKeysBackupVersionBody( override val algorithm: String? = null, /** - * algorithm-dependent data, for "m.megolm_backup.v1.curve25519-aes-sha2" see [im.vector.matrix.android.internal.crypto.keysbackup.MegolmBackupAuthData] + * algorithm-dependent data, for "m.megolm_backup.v1.curve25519-aes-sha2" + * see [im.vector.matrix.android.internal.crypto.keysbackup.MegolmBackupAuthData] */ @Json(name = "auth_data") override val authData: JsonDict? = null diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/model/rest/KeysVersionResult.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/model/rest/KeysVersionResult.kt index 0addd1491e..ba5cb2a379 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/model/rest/KeysVersionResult.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/model/rest/KeysVersionResult.kt @@ -29,7 +29,8 @@ data class KeysVersionResult( override val algorithm: String? = null, /** - * algorithm-dependent data, for "m.megolm_backup.v1.curve25519-aes-sha2" see [im.vector.matrix.android.internal.crypto.keysbackup.MegolmBackupAuthData] + * algorithm-dependent data, for "m.megolm_backup.v1.curve25519-aes-sha2" + * see [im.vector.matrix.android.internal.crypto.keysbackup.MegolmBackupAuthData] */ @Json(name = "auth_data") override val authData: JsonDict? = null, diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/model/rest/UpdateKeysBackupVersionBody.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/model/rest/UpdateKeysBackupVersionBody.kt index 9d88af20ef..bb12911e42 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/model/rest/UpdateKeysBackupVersionBody.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/model/rest/UpdateKeysBackupVersionBody.kt @@ -29,7 +29,8 @@ data class UpdateKeysBackupVersionBody( override val algorithm: String? = null, /** - * algorithm-dependent data, for "m.megolm_backup.v1.curve25519-aes-sha2" see [im.vector.matrix.android.internal.crypto.keysbackup.MegolmBackupAuthData] + * algorithm-dependent data, for "m.megolm_backup.v1.curve25519-aes-sha2" + * see [im.vector.matrix.android.internal.crypto.keysbackup.MegolmBackupAuthData] */ @Json(name = "auth_data") override val authData: JsonDict? = null, diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/secrets/DefaultSharedSecretStorageService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/secrets/DefaultSharedSecretStorageService.kt index 62bc4774c6..649a5a118f 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/secrets/DefaultSharedSecretStorageService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/secrets/DefaultSharedSecretStorageService.kt @@ -65,14 +65,16 @@ internal class DefaultSharedSecretStorageService @Inject constructor( ) : SharedSecretStorageService { override fun generateKey(keyId: String, + key: SsssKeySpec?, keyName: String, keySigner: KeySigner?, callback: MatrixCallback) { cryptoCoroutineScope.launch(coroutineDispatchers.main) { - val key = try { - ByteArray(32).also { - SecureRandom().nextBytes(it) - } + val bytes = try { + (key as? RawBytesKeySpec)?.privateKey + ?: ByteArray(32).also { + SecureRandom().nextBytes(it) + } } catch (failure: Throwable) { callback.onFailure(failure) return@launch @@ -102,8 +104,8 @@ internal class DefaultSharedSecretStorageService @Inject constructor( callback.onSuccess(SsssKeyCreationInfo( keyId = keyId, content = storageKeyContent, - recoveryKey = computeRecoveryKey(key), - keySpec = RawBytesKeySpec(key) + recoveryKey = computeRecoveryKey(bytes), + keySpec = RawBytesKeySpec(bytes) )) } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultVerificationService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultVerificationService.kt index 77dcc483bd..04a3560223 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultVerificationService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultVerificationService.kt @@ -82,6 +82,7 @@ import im.vector.matrix.android.internal.di.DeviceId import im.vector.matrix.android.internal.di.UserId import im.vector.matrix.android.internal.session.SessionScope import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import timber.log.Timber @@ -102,7 +103,8 @@ internal class DefaultVerificationService @Inject constructor( private val coroutineDispatchers: MatrixCoroutineDispatchers, private val verificationTransportRoomMessageFactory: VerificationTransportRoomMessageFactory, private val verificationTransportToDeviceFactory: VerificationTransportToDeviceFactory, - private val crossSigningService: CrossSigningService + private val crossSigningService: CrossSigningService, + private val cryptoCoroutineScope: CoroutineScope ) : DefaultVerificationTransaction.Listener, VerificationService { private val uiHandler = Handler(Looper.getMainLooper()) @@ -125,7 +127,7 @@ internal class DefaultVerificationService @Inject constructor( // Event received from the sync fun onToDeviceEvent(event: Event) { - GlobalScope.launch(coroutineDispatchers.crypto) { + cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { when (event.getClearType()) { EventType.KEY_VERIFICATION_START -> { onStartRequestReceived(event) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/SendVerificationMessageWorker.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/SendVerificationMessageWorker.kt index 2b049e0061..95376fb0cc 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/SendVerificationMessageWorker.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/SendVerificationMessageWorker.kt @@ -30,6 +30,10 @@ import im.vector.matrix.android.internal.worker.getSessionComponent import timber.log.Timber import javax.inject.Inject +/** + * Possible previous worker: None + * Possible next worker : None + */ internal class SendVerificationMessageWorker(context: Context, params: WorkerParameters) : CoroutineWorker(context, params) { @@ -48,7 +52,7 @@ internal class SendVerificationMessageWorker(context: Context, lateinit var cryptoService: CryptoService override suspend fun doWork(): Result { - val errorOutputData = Data.Builder().putBoolean("failed", true).build() + val errorOutputData = Data.Builder().putBoolean(OUTPUT_KEY_FAILED, true).build() val params = WorkerParamsFactory.fromData(inputData) ?: return Result.success(errorOutputData) @@ -76,4 +80,12 @@ internal class SendVerificationMessageWorker(context: Context, } } } + + companion object { + private const val OUTPUT_KEY_FAILED = "failed" + + fun hasFailed(outputData: Data): Boolean { + return outputData.getBoolean(SendVerificationMessageWorker.OUTPUT_KEY_FAILED, false) + } + } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationTransport.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationTransport.kt index 75ffa5e082..c5c4772c51 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationTransport.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationTransport.kt @@ -34,6 +34,9 @@ internal interface VerificationTransport { onErrorReason: CancelCode, onDone: (() -> Unit)?) + /** + * @param callback will be called with eventId and ValidVerificationInfoRequest in case of success + */ fun sendVerificationRequest(supportedMethods: List, localId: String, otherUserId: String, diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationTransportRoomMessage.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationTransportRoomMessage.kt index b7b7335011..77234e82f4 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationTransportRoomMessage.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationTransportRoomMessage.kt @@ -115,7 +115,7 @@ internal class VerificationTransportRoomMessage( ?.filter { it.state == WorkInfo.State.SUCCEEDED } ?.firstOrNull { it.id == enqueueInfo.second } ?.let { wInfo -> - if (wInfo.outputData.getBoolean("failed", false)) { + if (SendVerificationMessageWorker.hasFailed(wInfo.outputData)) { Timber.e("## SAS verification [${tx?.transactionId}] failed to send verification message in state : ${tx?.state}") tx?.cancel(onErrorReason) } else { @@ -196,12 +196,15 @@ internal class VerificationTransportRoomMessage( ?.filter { it.state == WorkInfo.State.SUCCEEDED } ?.firstOrNull { it.id == workRequest.id } ?.let { wInfo -> - if (wInfo.outputData.getBoolean("failed", false)) { + if (SendVerificationMessageWorker.hasFailed(wInfo.outputData)) { callback(null, null) - } else if (wInfo.outputData.getString(localId) != null) { - callback(wInfo.outputData.getString(localId), validInfo) } else { - callback(null, null) + val eventId = wInfo.outputData.getString(localId) + if (eventId != null) { + callback(eventId, validInfo) + } else { + callback(null, null) + } } workLiveData.removeObserver(this) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/account/AccountAPI.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/account/AccountAPI.kt index 8acfb3abb8..23d8210e89 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/account/AccountAPI.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/account/AccountAPI.kt @@ -20,7 +20,6 @@ import im.vector.matrix.android.api.session.account.model.ChangePasswordParams import im.vector.matrix.android.internal.network.NetworkConstants import retrofit2.Call import retrofit2.http.Body -import retrofit2.http.Headers import retrofit2.http.POST internal interface AccountAPI { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/DefaultContentUrlResolver.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/DefaultContentUrlResolver.kt index f288f949cd..df12ad6131 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/DefaultContentUrlResolver.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/DefaultContentUrlResolver.kt @@ -18,46 +18,46 @@ package im.vector.matrix.android.internal.session.content import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig import im.vector.matrix.android.api.session.content.ContentUrlResolver +import im.vector.matrix.android.internal.network.NetworkConstants import javax.inject.Inject private const val MATRIX_CONTENT_URI_SCHEME = "mxc://" -private const val URI_PREFIX_CONTENT_API = "_matrix/media/v1/" -internal class DefaultContentUrlResolver @Inject constructor(private val homeServerConnectionConfig: HomeServerConnectionConfig) : ContentUrlResolver { +internal class DefaultContentUrlResolver @Inject constructor(homeServerConnectionConfig: HomeServerConnectionConfig) : ContentUrlResolver { - companion object { - fun getUploadUrl(homeServerConnectionConfig: HomeServerConnectionConfig): String { - val baseUrl = homeServerConnectionConfig.homeServerUri.toString() - val sep = if (baseUrl.endsWith("/")) "" else "/" + private val baseUrl = homeServerConnectionConfig.homeServerUri.toString() + private val sep = if (baseUrl.endsWith("/")) "" else "/" - return baseUrl + sep + URI_PREFIX_CONTENT_API + "upload" - } - } + override val uploadUrl = baseUrl + sep + NetworkConstants.URI_API_MEDIA_PREFIX_PATH_R0 + "upload" override fun resolveFullSize(contentUrl: String?): String? { - if (contentUrl?.isValidMatrixContentUrl() == true) { - val baseUrl = homeServerConnectionConfig.homeServerUri.toString() - val prefix = URI_PREFIX_CONTENT_API + "download/" - return resolve(baseUrl, contentUrl, prefix) - } - return null + return contentUrl + // do not allow non-mxc content URLs + ?.takeIf { it.isValidMatrixContentUrl() } + ?.let { + resolve( + contentUrl = it, + prefix = NetworkConstants.URI_API_MEDIA_PREFIX_PATH_R0 + "download/" + ) + } } override fun resolveThumbnail(contentUrl: String?, width: Int, height: Int, method: ContentUrlResolver.ThumbnailMethod): String? { - if (contentUrl?.isValidMatrixContentUrl() == true) { - val baseUrl = homeServerConnectionConfig.homeServerUri.toString() - val prefix = URI_PREFIX_CONTENT_API + "thumbnail/" - val params = "?width=$width&height=$height&method=${method.value}" - return resolve(baseUrl, contentUrl, prefix, params) - } - // do not allow non-mxc content URLs - return null + return contentUrl + // do not allow non-mxc content URLs + ?.takeIf { it.isValidMatrixContentUrl() } + ?.let { + resolve( + contentUrl = it, + prefix = NetworkConstants.URI_API_MEDIA_PREFIX_PATH_R0 + "thumbnail/", + params = "?width=$width&height=$height&method=${method.value}" + ) + } } - private fun resolve(baseUrl: String, - contentUrl: String, + private fun resolve(contentUrl: String, prefix: String, - params: String? = null): String? { + params: String = ""): String? { var serverAndMediaId = contentUrl.removePrefix(MATRIX_CONTENT_URI_SCHEME) val fragmentOffset = serverAndMediaId.indexOf("#") var fragment = "" @@ -66,9 +66,7 @@ internal class DefaultContentUrlResolver @Inject constructor(private val homeSer serverAndMediaId = serverAndMediaId.substring(0, fragmentOffset) } - val sep = if (baseUrl.endsWith("/")) "" else "/" - - return baseUrl + sep + prefix + serverAndMediaId + (params ?: "") + fragment + return baseUrl + sep + prefix + serverAndMediaId + params + fragment } private fun String.isValidMatrixContentUrl(): Boolean { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/FileUploader.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/FileUploader.kt index 4fa0cb5013..1153b39b0a 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/FileUploader.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/FileUploader.kt @@ -17,7 +17,7 @@ package im.vector.matrix.android.internal.session.content import com.squareup.moshi.Moshi -import im.vector.matrix.android.api.auth.data.SessionParams +import im.vector.matrix.android.api.session.content.ContentUrlResolver import im.vector.matrix.android.internal.di.Authenticated import im.vector.matrix.android.internal.network.ProgressRequestBody import im.vector.matrix.android.internal.network.awaitResponse @@ -37,10 +37,10 @@ import javax.inject.Inject internal class FileUploader @Inject constructor(@Authenticated private val okHttpClient: OkHttpClient, private val eventBus: EventBus, - sessionParams: SessionParams, + contentUrlResolver: ContentUrlResolver, moshi: Moshi) { - private val uploadUrl = DefaultContentUrlResolver.getUploadUrl(sessionParams.homeServerConnectionConfig) + private val uploadUrl = contentUrlResolver.uploadUrl private val responseAdapter = moshi.adapter(ContentUploadResponse::class.java) suspend fun uploadFile(file: File, diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/UploadContentWorker.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/UploadContentWorker.kt index 1b736d349f..03ae366ed5 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/UploadContentWorker.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/UploadContentWorker.kt @@ -46,6 +46,10 @@ private data class NewImageAttributes( val newFileSize: Int ) +/** + * Possible previous worker: None + * Possible next worker : Always [MultipleEventSendingDispatcherWorker] + */ internal class UploadContentWorker(val context: Context, params: WorkerParameters) : CoroutineWorker(context, params) { @JsonClass(generateAdapter = true) @@ -64,12 +68,14 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter override suspend fun doWork(): Result { val params = WorkerParamsFactory.fromData(inputData) ?: return Result.success() + .also { Timber.e("Unable to parse work parameters") } + Timber.v("Starting upload media work with params $params") if (params.lastFailureMessage != null) { // Transmit the error - Timber.v("Stop upload media work due to input failure") return Result.success(inputData) + .also { Timber.e("Work cancelled due to input error from parent") } } // Just defensive code to ensure that we never have an uncaught exception that could break the queue diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/group/GetGroupDataWorker.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/group/GetGroupDataWorker.kt index 93705774e6..bb33212f9c 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/group/GetGroupDataWorker.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/group/GetGroupDataWorker.kt @@ -23,8 +23,13 @@ import com.squareup.moshi.JsonClass import im.vector.matrix.android.internal.worker.SessionWorkerParams import im.vector.matrix.android.internal.worker.WorkerParamsFactory import im.vector.matrix.android.internal.worker.getSessionComponent +import timber.log.Timber import javax.inject.Inject +/** + * Possible previous worker: None + * Possible next worker : None + */ internal class GetGroupDataWorker(context: Context, params: WorkerParameters) : CoroutineWorker(context, params) { @JsonClass(generateAdapter = true) @@ -39,6 +44,7 @@ internal class GetGroupDataWorker(context: Context, params: WorkerParameters) : override suspend fun doWork(): Result { val params = WorkerParamsFactory.fromData(inputData) ?: return Result.failure() + .also { Timber.e("Unable to parse work parameters") } val sessionComponent = getSessionComponent(params.sessionId) ?: return Result.success() sessionComponent.inject(this) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/group/GroupSummaryUpdater.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/group/GroupSummaryUpdater.kt index 3ca5a03822..9808b584aa 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/group/GroupSummaryUpdater.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/group/GroupSummaryUpdater.kt @@ -69,13 +69,13 @@ internal class GroupSummaryUpdater @Inject constructor( val workData = WorkerParamsFactory.toData(getGroupDataWorkerParams) - val sendWork = workManagerProvider.matrixOneTimeWorkRequestBuilder() + val getGroupWork = workManagerProvider.matrixOneTimeWorkRequestBuilder() .setInputData(workData) .setConstraints(WorkManagerProvider.workConstraints) .build() workManagerProvider.workManager - .beginUniqueWork(GET_GROUP_DATA_WORKER, ExistingWorkPolicy.APPEND, sendWork) + .beginUniqueWork(GET_GROUP_DATA_WORKER, ExistingWorkPolicy.APPEND, getGroupWork) .enqueue() } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/pushers/AddHttpPusherWorker.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/pushers/AddHttpPusherWorker.kt index adb4bf32c2..ecc39cfdc2 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/pushers/AddHttpPusherWorker.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/pushers/AddHttpPusherWorker.kt @@ -31,6 +31,7 @@ import im.vector.matrix.android.internal.worker.SessionWorkerParams import im.vector.matrix.android.internal.worker.WorkerParamsFactory import im.vector.matrix.android.internal.worker.getSessionComponent import org.greenrobot.eventbus.EventBus +import timber.log.Timber import javax.inject.Inject internal class AddHttpPusherWorker(context: Context, params: WorkerParameters) @@ -50,6 +51,7 @@ internal class AddHttpPusherWorker(context: Context, params: WorkerParameters) override suspend fun doWork(): Result { val params = WorkerParamsFactory.fromData(inputData) ?: return Result.failure() + .also { Timber.e("Unable to parse work parameters") } val sessionComponent = getSessionComponent(params.sessionId) ?: return Result.success() sessionComponent.inject(this) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/SendRelationWorker.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/SendRelationWorker.kt index 5857eaa89b..9a8f1a7dc6 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/SendRelationWorker.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/SendRelationWorker.kt @@ -31,6 +31,7 @@ import im.vector.matrix.android.internal.worker.SessionWorkerParams import im.vector.matrix.android.internal.worker.WorkerParamsFactory import im.vector.matrix.android.internal.worker.getSessionComponent import org.greenrobot.eventbus.EventBus +import timber.log.Timber import javax.inject.Inject // TODO This is not used. Delete? @@ -51,10 +52,12 @@ internal class SendRelationWorker(context: Context, params: WorkerParameters) : override suspend fun doWork(): Result { val params = WorkerParamsFactory.fromData(inputData) ?: return Result.failure() + .also { Timber.e("Unable to parse work parameters") } if (params.lastFailureMessage != null) { // Transmit the error return Result.success(inputData) + .also { Timber.e("Work cancelled due to input error from parent") } } val sessionComponent = getSessionComponent(params.sessionId) ?: return Result.success() diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/DefaultSendService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/DefaultSendService.kt index a99337695a..1037b7c79c 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/DefaultSendService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/DefaultSendService.kt @@ -228,7 +228,7 @@ internal class DefaultSendService @AssistedInject constructor( keys.forEach { isRoomEncrypted -> // Should never be empty val localEchoes = get(isRoomEncrypted).orEmpty() - val uploadWork = createUploadMediaWork(localEchoes, attachment, isRoomEncrypted, compressBeforeSending, startChain = true) + val uploadWork = createUploadMediaWork(localEchoes, attachment, isRoomEncrypted, compressBeforeSending) val dispatcherWork = createMultipleEventDispatcherWork(isRoomEncrypted) @@ -293,14 +293,13 @@ internal class DefaultSendService @AssistedInject constructor( private fun createUploadMediaWork(allLocalEchos: List, attachment: ContentAttachmentData, isRoomEncrypted: Boolean, - compressBeforeSending: Boolean, - startChain: Boolean): OneTimeWorkRequest { + compressBeforeSending: Boolean): OneTimeWorkRequest { val uploadMediaWorkerParams = UploadContentWorker.Params(sessionId, allLocalEchos, attachment, isRoomEncrypted, compressBeforeSending) val uploadWorkData = WorkerParamsFactory.toData(uploadMediaWorkerParams) return workManagerProvider.matrixOneTimeWorkRequestBuilder() .setConstraints(WorkManagerProvider.workConstraints) - .startChain(startChain) + .startChain(true) .setInputData(uploadWorkData) .setBackoffCriteria(BackoffPolicy.LINEAR, WorkManagerProvider.BACKOFF_DELAY, TimeUnit.MILLISECONDS) .build() diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/EncryptEventWorker.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/EncryptEventWorker.kt index e4424f1cb3..6af2f8dab6 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/EncryptEventWorker.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/EncryptEventWorker.kt @@ -35,6 +35,10 @@ import im.vector.matrix.android.internal.worker.getSessionComponent import timber.log.Timber import javax.inject.Inject +/** + * Possible previous worker: None + * Possible next worker : Always [SendEventWorker] + */ internal class EncryptEventWorker(context: Context, params: WorkerParameters) : CoroutineWorker(context, params) { @@ -53,14 +57,14 @@ internal class EncryptEventWorker(context: Context, params: WorkerParameters) override suspend fun doWork(): Result { Timber.v("Start Encrypt work") val params = WorkerParamsFactory.fromData(inputData) - ?: return Result.success().also { - Timber.e("Work cancelled due to input error from parent") - } + ?: return Result.success() + .also { Timber.e("Unable to parse work parameters") } Timber.v("Start Encrypt work for event ${params.event.eventId}") if (params.lastFailureMessage != null) { // Transmit the error return Result.success(inputData) + .also { Timber.e("Work cancelled due to input error from parent") } } val sessionComponent = getSessionComponent(params.sessionId) ?: return Result.success() diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/MultipleEventSendingDispatcherWorker.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/MultipleEventSendingDispatcherWorker.kt index 8c31dd1682..aec7cb3c5c 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/MultipleEventSendingDispatcherWorker.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/MultipleEventSendingDispatcherWorker.kt @@ -25,6 +25,7 @@ import com.squareup.moshi.JsonClass import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.room.send.SendState import im.vector.matrix.android.internal.di.WorkManagerProvider +import im.vector.matrix.android.internal.session.content.UploadContentWorker import im.vector.matrix.android.internal.session.room.timeline.TimelineSendEventWorkCommon import im.vector.matrix.android.internal.worker.SessionWorkerParams import im.vector.matrix.android.internal.worker.WorkerParamsFactory @@ -36,6 +37,9 @@ import javax.inject.Inject /** * This worker creates a new work for each events passed in parameter + * + * Possible previous worker: Always [UploadContentWorker] + * Possible next worker : None, but it will post new work to send events, encrypted or not */ internal class MultipleEventSendingDispatcherWorker(context: Context, params: WorkerParameters) : CoroutineWorker(context, params) { @@ -55,9 +59,8 @@ internal class MultipleEventSendingDispatcherWorker(context: Context, params: Wo override suspend fun doWork(): Result { Timber.v("Start dispatch sending multiple event work") val params = WorkerParamsFactory.fromData(inputData) - ?: return Result.success().also { - Timber.e("Work cancelled due to input error from parent") - } + ?: return Result.success() + .also { Timber.e("Unable to parse work parameters") } val sessionComponent = getSessionComponent(params.sessionId) ?: return Result.success() sessionComponent.inject(this) @@ -68,6 +71,7 @@ internal class MultipleEventSendingDispatcherWorker(context: Context, params: Wo } // Transmit the error if needed? return Result.success(inputData) + .also { Timber.e("Work cancelled due to input error from parent") } } // Create a work for every event diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/RedactEventWorker.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/RedactEventWorker.kt index 3ff318aa8a..7e0b665d63 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/RedactEventWorker.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/RedactEventWorker.kt @@ -26,8 +26,13 @@ import im.vector.matrix.android.internal.worker.SessionWorkerParams import im.vector.matrix.android.internal.worker.WorkerParamsFactory import im.vector.matrix.android.internal.worker.getSessionComponent import org.greenrobot.eventbus.EventBus +import timber.log.Timber import javax.inject.Inject +/** + * Possible previous worker: None + * Possible next worker : None + */ internal class RedactEventWorker(context: Context, params: WorkerParameters) : CoroutineWorker(context, params) { @JsonClass(generateAdapter = true) @@ -46,10 +51,12 @@ internal class RedactEventWorker(context: Context, params: WorkerParameters) : C override suspend fun doWork(): Result { val params = WorkerParamsFactory.fromData(inputData) ?: return Result.failure() + .also { Timber.e("Unable to parse work parameters") } if (params.lastFailureMessage != null) { // Transmit the error return Result.success(inputData) + .also { Timber.e("Work cancelled due to input error from parent") } } val sessionComponent = getSessionComponent(params.sessionId) ?: return Result.success() diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/SendEventWorker.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/SendEventWorker.kt index 69d288a932..d55b9665f6 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/SendEventWorker.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/SendEventWorker.kt @@ -32,6 +32,10 @@ import org.greenrobot.eventbus.EventBus import timber.log.Timber import javax.inject.Inject +/** + * Possible previous worker: [EncryptEventWorker] or first worker + * Possible next worker : None + */ internal class SendEventWorker(context: Context, params: WorkerParameters) : CoroutineWorker(context, params) { @@ -49,9 +53,8 @@ internal class SendEventWorker(context: Context, override suspend fun doWork(): Result { val params = WorkerParamsFactory.fromData(inputData) - ?: return Result.success().also { - Timber.e("Work cancelled due to input error from parent") - } + ?: return Result.success() + .also { Timber.e("Unable to parse work parameters") } val sessionComponent = getSessionComponent(params.sessionId) ?: return Result.success() sessionComponent.inject(this) @@ -65,6 +68,7 @@ internal class SendEventWorker(context: Context, localEchoUpdater.updateSendState(event.eventId, SendState.UNDELIVERED) // Transmit the error return Result.success(inputData) + .also { Timber.e("Work cancelled due to input error from parent") } } return try { sendEvent(event) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/job/SyncWorker.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/job/SyncWorker.kt index c844db8d33..ab4e1938ce 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/job/SyncWorker.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/job/SyncWorker.kt @@ -35,6 +35,10 @@ import javax.inject.Inject private const val DEFAULT_LONG_POOL_TIMEOUT = 0L +/** + * Possible previous worker: None + * Possible next worker : None + */ internal class SyncWorker(context: Context, workerParameters: WorkerParameters ) : CoroutineWorker(context, workerParameters) { @@ -53,7 +57,10 @@ internal class SyncWorker(context: Context, override suspend fun doWork(): Result { Timber.i("Sync work starting") - val params = WorkerParamsFactory.fromData(inputData) ?: return Result.success() + val params = WorkerParamsFactory.fromData(inputData) + ?: return Result.success() + .also { Timber.e("Unable to parse work parameters") } + val sessionComponent = getSessionComponent(params.sessionId) ?: return Result.success() sessionComponent.inject(this) return runCatching { @@ -76,7 +83,6 @@ internal class SyncWorker(context: Context, } companion object { - private const val BG_SYNC_WORK_NAME = "BG_SYNCP" fun requireBackgroundSync(workManagerProvider: WorkManagerProvider, sessionId: String, serverTimeout: Long = 0) { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/worker/SessionWorkerParams.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/worker/SessionWorkerParams.kt index c05367cf10..3cbe6c7866 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/worker/SessionWorkerParams.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/worker/SessionWorkerParams.kt @@ -16,9 +16,16 @@ package im.vector.matrix.android.internal.worker +/** + * Note about the Worker usage: + * The workers we chain, or when using the append strategy, should never return Result.Failure(), else the chain will be broken forever + */ interface SessionWorkerParams { val sessionId: String - // Null is no error occurs. When chaining Workers, first step is to check that there is no lastFailureMessage from the previous workers + /** + * Null when no error occurs. When chaining Workers, first step is to check that there is no lastFailureMessage from the previous workers + * If it is the case, the worker should just transmit the error and shouldn't do anything else + */ val lastFailureMessage: String? } diff --git a/tools/check/forbidden_strings_in_code.txt b/tools/check/forbidden_strings_in_code.txt index a0bd725118..3c0337ac99 100644 --- a/tools/check/forbidden_strings_in_code.txt +++ b/tools/check/forbidden_strings_in_code.txt @@ -60,7 +60,7 @@ private short final short ### Line length is limited to 160 chars. Please split long lines -.{161} +[^─]{161} ### "DO NOT COMMIT" has been committed DO NOT COMMIT diff --git a/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt b/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt index c68972cdd4..c2f2959bd7 100644 --- a/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt +++ b/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt @@ -30,6 +30,7 @@ import im.vector.riotx.features.crypto.recover.BootstrapAccountPasswordFragment import im.vector.riotx.features.crypto.recover.BootstrapConclusionFragment import im.vector.riotx.features.crypto.recover.BootstrapConfirmPassphraseFragment 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.BootstrapWaitingFragment import im.vector.riotx.features.crypto.verification.cancel.VerificationCancelFragment @@ -444,4 +445,8 @@ interface FragmentModule { @IntoMap @FragmentKey(BootstrapAccountPasswordFragment::class) fun bindBootstrapAccountPasswordFragment(fragment: BootstrapAccountPasswordFragment): Fragment + @Binds + @IntoMap + @FragmentKey(BootstrapMigrateBackupFragment::class) + fun bindBootstrapMigrateBackupFragment(fragment: BootstrapMigrateBackupFragment): Fragment } diff --git a/vector/src/main/java/im/vector/riotx/core/platform/ViewModelTask.kt b/vector/src/main/java/im/vector/riotx/core/platform/ViewModelTask.kt new file mode 100644 index 0000000000..abe5cc9095 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/core/platform/ViewModelTask.kt @@ -0,0 +1,34 @@ +/* + * 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.platform + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.async +import kotlinx.coroutines.launch + +interface ViewModelTask { + operator fun invoke( + scope: CoroutineScope, + params: Params, + onResult: (Result) -> Unit = {} + ) { + val backgroundJob = scope.async { execute(params) } + scope.launch { onResult(backgroundJob.await()) } + } + + suspend fun execute(params: Params): Result +} diff --git a/vector/src/main/java/im/vector/riotx/core/ui/views/KeysBackupBanner.kt b/vector/src/main/java/im/vector/riotx/core/ui/views/KeysBackupBanner.kt index 83a4e88ad5..817575d91a 100755 --- a/vector/src/main/java/im/vector/riotx/core/ui/views/KeysBackupBanner.kt +++ b/vector/src/main/java/im/vector/riotx/core/ui/views/KeysBackupBanner.kt @@ -159,7 +159,7 @@ class KeysBackupBanner @JvmOverloads constructor( render(state, true) } - // PRIVATE METHODS ***************************************************************************************************************************************** + // PRIVATE METHODS **************************************************************************************************************************************** private fun setupView() { inflate(context, R.layout.view_keys_backup_banner, this) diff --git a/vector/src/main/java/im/vector/riotx/core/ui/views/NotificationAreaView.kt b/vector/src/main/java/im/vector/riotx/core/ui/views/NotificationAreaView.kt index 145a26aed2..1c1f3fae1a 100644 --- a/vector/src/main/java/im/vector/riotx/core/ui/views/NotificationAreaView.kt +++ b/vector/src/main/java/im/vector/riotx/core/ui/views/NotificationAreaView.kt @@ -87,7 +87,7 @@ class NotificationAreaView @JvmOverloads constructor( } } - // PRIVATE METHODS ***************************************************************************************************************************************** + // PRIVATE METHODS **************************************************************************************************************************************** private fun setupView() { inflate(context, R.layout.view_notification_area, this) diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/setup/KeysBackupSetupActivity.kt b/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/setup/KeysBackupSetupActivity.kt index fbc69505fa..c7d3da30ea 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/setup/KeysBackupSetupActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/setup/KeysBackupSetupActivity.kt @@ -128,7 +128,10 @@ class KeysBackupSetupActivity : SimpleFragmentActivity() { } private fun exportKeysManually() { - if (checkPermissions(PERMISSIONS_FOR_WRITING_FILES, this, PERMISSION_REQUEST_CODE_EXPORT_KEYS, R.string.permissions_rationale_msg_keys_backup_export)) { + if (checkPermissions(PERMISSIONS_FOR_WRITING_FILES, + this, + PERMISSION_REQUEST_CODE_EXPORT_KEYS, + R.string.permissions_rationale_msg_keys_backup_export)) { ExportKeysDialog().show(this, object : ExportKeysDialog.ExportKeyDialogListener { override fun onPassphrase(passphrase: String) { showWaitingView() diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BackupToQuadSMigrationTask.kt b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BackupToQuadSMigrationTask.kt new file mode 100644 index 0000000000..e29d0d636d --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BackupToQuadSMigrationTask.kt @@ -0,0 +1,172 @@ +/* + * 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 im.vector.matrix.android.api.NoOpMatrixCallback +import im.vector.matrix.android.api.listeners.ProgressListener +import im.vector.matrix.android.api.session.Session +import im.vector.matrix.android.api.session.crypto.crosssigning.KEYBACKUP_SECRET_SSSS_NAME +import im.vector.matrix.android.api.session.securestorage.EmptyKeySigner +import im.vector.matrix.android.api.session.securestorage.RawBytesKeySpec +import im.vector.matrix.android.api.session.securestorage.SharedSecretStorageService +import im.vector.matrix.android.api.session.securestorage.SsssKeyCreationInfo +import im.vector.matrix.android.internal.crypto.crosssigning.toBase64NoPadding +import im.vector.matrix.android.internal.crypto.keysbackup.deriveKey +import im.vector.matrix.android.internal.crypto.keysbackup.util.computeRecoveryKey +import im.vector.matrix.android.internal.crypto.keysbackup.util.extractCurveKeyFromRecoveryKey +import im.vector.matrix.android.internal.util.awaitCallback +import im.vector.riotx.R +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.util.UUID +import javax.inject.Inject + +class BackupToQuadSMigrationTask @Inject constructor( + val session: Session, + val stringProvider: StringProvider +) : ViewModelTask { + + sealed class Result { + object Success : Result() + abstract class Failure(val error: String?) : Result() + object InvalidRecoverySecret : Failure(null) + object NoKeyBackupVersion : Failure(null) + object IllegalParams : Failure(null) + class ErrorFailure(throwable: Throwable) : Failure(throwable.localizedMessage) + } + + data class Params( + val passphrase: String?, + val recoveryKey: String?, + val progressListener: BootstrapProgressListener? = null + ) + + override suspend fun execute(params: Params): Result { + try { + // We need to use the current secret for keybackup and use it as the new master key for SSSS + // Then we need to put back the backup key in sss + val keysBackupService = session.cryptoService().keysBackupService() + val quadS = session.sharedSecretStorageService + + val version = keysBackupService.keysBackupVersion ?: return Result.NoKeyBackupVersion + + reportProgress(params, R.string.bootstrap_progress_checking_backup) + val curveKey = + (if (params.recoveryKey != null) { + extractCurveKeyFromRecoveryKey(params.recoveryKey) + } else if (!params.passphrase.isNullOrEmpty() && version.getAuthDataAsMegolmBackupAuthData()?.privateKeySalt != null) { + version.getAuthDataAsMegolmBackupAuthData()?.let { authData -> + deriveKey(params.passphrase, authData.privateKeySalt!!, authData.privateKeyIterations!!, object : ProgressListener { + override fun onProgress(progress: Int, total: Int) { + params.progressListener?.onProgress(WaitingViewData( + stringProvider.getString(R.string.bootstrap_progress_checking_backup_with_info, + "$progress/$total") + )) + } + }) + } + } else null) + ?: return Result.IllegalParams + + reportProgress(params, R.string.bootstrap_progress_compute_curve_key) + val recoveryKey = computeRecoveryKey(curveKey) + + val isValid = awaitCallback { + keysBackupService.isValidRecoveryKeyForCurrentVersion(recoveryKey, it) + } + + if (!isValid) return Result.InvalidRecoverySecret + + val info: SsssKeyCreationInfo = + when { + params.passphrase?.isNotEmpty() == true -> { + reportProgress(params, R.string.bootstrap_progress_generating_ssss) + awaitCallback { + quadS.generateKeyWithPassphrase( + UUID.randomUUID().toString(), + "ssss_key", + params.passphrase, + EmptyKeySigner(), + object : ProgressListener { + override fun onProgress(progress: Int, total: Int) { + params.progressListener?.onProgress( + WaitingViewData( + stringProvider.getString( + R.string.bootstrap_progress_generating_ssss_with_info, + "$progress/$total") + )) + } + }, + it + ) + } + } + params.recoveryKey != null -> { + reportProgress(params, R.string.bootstrap_progress_generating_ssss_recovery) + awaitCallback { + quadS.generateKey( + UUID.randomUUID().toString(), + extractCurveKeyFromRecoveryKey(params.recoveryKey)?.let { RawBytesKeySpec(it) }, + "ssss_key", + EmptyKeySigner(), + it + ) + } + } + else -> { + return Result.IllegalParams + } + } + + // Ok, so now we have migrated the old keybackup secret as the quadS key + // Now we need to store the keybackup key in SSSS in a compatible way + reportProgress(params, R.string.bootstrap_progress_storing_in_sss) + awaitCallback { + quadS.storeSecret( + KEYBACKUP_SECRET_SSSS_NAME, + curveKey.toBase64NoPadding(), + listOf(SharedSecretStorageService.KeyRef(info.keyId, info.keySpec)), + it + ) + } + + // save for gossiping + keysBackupService.saveBackupRecoveryKey(recoveryKey, version.version) + + // while we are there let's restore, but do not block + session.cryptoService().keysBackupService().restoreKeysWithRecoveryKey( + version, + recoveryKey, + null, + null, + null, + NoOpMatrixCallback() + ) + + return Result.Success + } catch (failure: Throwable) { + Timber.e(failure, "## BackupToQuadSMigrationTask - Failed to migrate backup") + return Result.ErrorFailure(failure) + } + } + + private fun reportProgress(params: Params, stringRes: Int) { + params.progressListener?.onProgress(WaitingViewData(stringProvider.getString(stringRes))) + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapActions.kt b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapActions.kt index 7c0f2c1c46..2d9440a77d 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapActions.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapActions.kt @@ -40,4 +40,8 @@ sealed class BootstrapActions : VectorViewModelAction { object SaveReqQueryStarted : BootstrapActions() data class SaveKeyToUri(val os: OutputStream) : BootstrapActions() object SaveReqFailed : BootstrapActions() + + object HandleForgotBackupPassphrase : BootstrapActions() + data class DoMigrateWithPassphrase(val passphrase: String) : BootstrapActions() + data class DoMigrateWithRecoveryKey(val recoveryKey: String) : BootstrapActions() } diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapBottomSheet.kt b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapBottomSheet.kt index 6305f161e3..e48c674159 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapBottomSheet.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapBottomSheet.kt @@ -18,6 +18,7 @@ package im.vector.riotx.features.crypto.recover import android.app.Dialog import android.os.Bundle +import android.os.Parcelable import android.view.KeyEvent import android.view.LayoutInflater import android.view.View @@ -26,18 +27,26 @@ import android.view.WindowManager import androidx.appcompat.app.AlertDialog import androidx.core.content.ContextCompat import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentManager import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.withState import im.vector.riotx.R import im.vector.riotx.core.di.ScreenComponent import im.vector.riotx.core.extensions.commitTransaction +import im.vector.riotx.core.extensions.exhaustive import im.vector.riotx.core.platform.VectorBaseBottomSheetDialogFragment +import kotlinx.android.parcel.Parcelize import kotlinx.android.synthetic.main.bottom_sheet_bootstrap.* import javax.inject.Inject import kotlin.reflect.KClass class BootstrapBottomSheet : VectorBaseBottomSheetDialogFragment() { + @Parcelize + data class Args( + val isNewAccount: Boolean + ) : Parcelable + override val showExpanded = true @Inject @@ -113,40 +122,70 @@ class BootstrapBottomSheet : VectorBaseBottomSheetDialogFragment() { override fun invalidate() = withState(viewModel) { state -> when (state.step) { - is BootstrapStep.SetupPassphrase -> { + is BootstrapStep.CheckingMigration -> { + bootstrapIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.ic_message_password)) + bootstrapTitleText.text = getString(R.string.upgrade_security) + showFragment(BootstrapWaitingFragment::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)) showFragment(BootstrapEnterPassphraseFragment::class, Bundle()) } - is BootstrapStep.ConfirmPassphrase -> { + 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)) showFragment(BootstrapConfirmPassphraseFragment::class, Bundle()) } - is BootstrapStep.AccountPassword -> { + is BootstrapStep.AccountPassword -> { bootstrapIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.ic_user)) bootstrapTitleText.text = getString(R.string.account_password) showFragment(BootstrapAccountPasswordFragment::class, Bundle()) } - is BootstrapStep.Initializing -> { + is BootstrapStep.Initializing -> { bootstrapIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.ic_message_key)) bootstrapTitleText.text = getString(R.string.bootstrap_loading_title) showFragment(BootstrapWaitingFragment::class, Bundle()) } - is BootstrapStep.SaveRecoveryKey -> { + 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) showFragment(BootstrapSaveRecoveryKeyFragment::class, Bundle()) } - is BootstrapStep.DoneSuccess -> { + is BootstrapStep.DoneSuccess -> { bootstrapIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.ic_message_key)) 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 + bootstrapIcon.setImageDrawable(ContextCompat.getDrawable( + requireContext(), + drawableRes) + ) + bootstrapTitleText.text = getString(R.string.upgrade_security) + showFragment(BootstrapMigrateBackupFragment::class, Bundle()) + } + }.exhaustive super.invalidate() } + companion object { + + const val EXTRA_ARGS = "EXTRA_ARGS" + + fun show(fragmentManager: FragmentManager, isAccountCreation: Boolean) { + BootstrapBottomSheet().apply { + isCancelable = false + arguments = Bundle().apply { this.putParcelable(EXTRA_ARGS, Args(isAccountCreation)) } + }.show(fragmentManager, "BootstrapBottomSheet") + } + } + private fun showFragment(fragmentClass: KClass, bundle: Bundle) { if (childFragmentManager.findFragmentByTag(fragmentClass.simpleName) == null) { childFragmentManager.commitTransaction { diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapCrossSigningTask.kt b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapCrossSigningTask.kt index a19604d78e..c2e0afbe3b 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapCrossSigningTask.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapCrossSigningTask.kt @@ -25,6 +25,7 @@ import im.vector.matrix.android.api.session.crypto.crosssigning.USER_SIGNING_KEY import im.vector.matrix.android.api.session.securestorage.EmptyKeySigner import im.vector.matrix.android.api.session.securestorage.SharedSecretStorageService import im.vector.matrix.android.api.session.securestorage.SsssKeyCreationInfo +import im.vector.matrix.android.api.session.securestorage.SsssKeySpec import im.vector.matrix.android.internal.auth.data.LoginFlowTypes import im.vector.matrix.android.internal.auth.registration.RegistrationFlowResponse import im.vector.matrix.android.internal.crypto.keysbackup.model.MegolmBackupCreationInfo @@ -33,11 +34,9 @@ import im.vector.matrix.android.internal.crypto.model.rest.UserPasswordAuth import im.vector.matrix.android.internal.di.MoshiProvider import im.vector.matrix.android.internal.util.awaitCallback import im.vector.riotx.R +import im.vector.riotx.core.platform.ViewModelTask import im.vector.riotx.core.platform.WaitingViewData import im.vector.riotx.core.resources.StringProvider -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.async -import kotlinx.coroutines.launch import timber.log.Timber import java.util.UUID import javax.inject.Inject @@ -67,24 +66,16 @@ interface BootstrapProgressListener { data class Params( val userPasswordAuth: UserPasswordAuth? = null, val progressListener: BootstrapProgressListener? = null, - val passphrase: String? + val passphrase: String?, + val keySpec: SsssKeySpec? = null ) class BootstrapCrossSigningTask @Inject constructor( private val session: Session, private val stringProvider: StringProvider -) { +) : ViewModelTask { - operator fun invoke( - scope: CoroutineScope, - params: Params, - onResult: (BootstrapResult) -> Unit = {} - ) { - val backgroundJob = scope.async { execute(params) } - scope.launch { onResult(backgroundJob.await()) } - } - - suspend fun execute(params: Params): BootstrapResult { + override suspend fun execute(params: Params): BootstrapResult { params.progressListener?.onProgress( WaitingViewData( stringProvider.getString(R.string.bootstrap_crosssigning_progress_initializing), @@ -124,6 +115,7 @@ class BootstrapCrossSigningTask @Inject constructor( } ?: kotlin.run { ssssService.generateKey( UUID.randomUUID().toString(), + params.keySpec, "ssss_key", EmptyKeySigner(), it @@ -205,14 +197,16 @@ class BootstrapCrossSigningTask @Inject constructor( ) ) try { - val creationInfo = awaitCallback { - session.cryptoService().keysBackupService().prepareKeysBackupVersion(null, null, it) + if (session.cryptoService().keysBackupService().keysBackupVersion == null) { + val creationInfo = awaitCallback { + session.cryptoService().keysBackupService().prepareKeysBackupVersion(null, null, it) + } + val version = awaitCallback { + session.cryptoService().keysBackupService().createKeysBackupVersion(creationInfo, it) + } + // Save it for gossiping + session.cryptoService().keysBackupService().saveBackupRecoveryKey(creationInfo.recoveryKey, version = version.version) } - val version = awaitCallback { - session.cryptoService().keysBackupService().createKeysBackupVersion(creationInfo, it) - } - // Save it for gossiping - session.cryptoService().keysBackupService().saveBackupRecoveryKey(creationInfo.recoveryKey, version = version.version) } catch (failure: Throwable) { Timber.e("## BootstrapCrossSigningTask: Failed to init keybackup") } diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapMigrateBackupFragment.kt b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapMigrateBackupFragment.kt new file mode 100644 index 0000000000..f1847e5ab5 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapMigrateBackupFragment.kt @@ -0,0 +1,211 @@ +/* + * 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.app.Activity +import android.content.Intent +import android.os.Bundle +import android.text.InputType.TYPE_CLASS_TEXT +import android.text.InputType.TYPE_TEXT_FLAG_MULTI_LINE +import android.text.InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD +import android.view.View +import android.view.inputmethod.EditorInfo +import androidx.core.text.toSpannable +import androidx.core.view.isVisible +import com.airbnb.mvrx.parentFragmentViewModel +import com.airbnb.mvrx.withState +import com.jakewharton.rxbinding3.view.clicks +import com.jakewharton.rxbinding3.widget.editorActionEvents +import com.jakewharton.rxbinding3.widget.textChanges +import im.vector.matrix.android.api.extensions.tryThis +import im.vector.matrix.android.internal.crypto.keysbackup.util.isValidRecoveryKey +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 im.vector.riotx.core.utils.startImportTextFromFileIntent +import io.reactivex.android.schedulers.AndroidSchedulers +import kotlinx.android.synthetic.main.fragment_bootstrap_enter_passphrase.bootstrapDescriptionText +import kotlinx.android.synthetic.main.fragment_bootstrap_migrate_backup.* +import java.util.concurrent.TimeUnit +import javax.inject.Inject + +class BootstrapMigrateBackupFragment @Inject constructor( + private val colorProvider: ColorProvider +) : VectorBaseFragment() { + + override fun getLayoutResId() = R.layout.fragment_bootstrap_migrate_backup + + val sharedViewModel: BootstrapSharedViewModel by parentFragmentViewModel() + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + withState(sharedViewModel) { + // set initial value (usefull when coming back) + bootstrapMigrateEditText.setText(it.passphrase ?: "") + } + bootstrapMigrateEditText.editorActionEvents() + .debounce(300, TimeUnit.MILLISECONDS) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe { + if (it.actionId == EditorInfo.IME_ACTION_DONE) { + submit() + } + } + .disposeOnDestroyView() + + bootstrapMigrateEditText.textChanges() + .skipInitialValue() + .subscribe { + bootstrapRecoveryKeyEnterTil.error = null + // sharedViewModel.handle(BootstrapActions.UpdateCandidatePassphrase(it?.toString() ?: "")) + } + .disposeOnDestroyView() + + // sharedViewModel.observeViewEvents {} + bootstrapMigrateContinueButton.clicks() + .debounce(300, TimeUnit.MILLISECONDS) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe { + submit() + } + .disposeOnDestroyView() + + bootstrapMigrateShowPassword.clicks() + .debounce(300, TimeUnit.MILLISECONDS) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe { + sharedViewModel.handle(BootstrapActions.TogglePasswordVisibility) + } + .disposeOnDestroyView() + + bootstrapMigrateForgotPassphrase.clicks() + .debounce(300, TimeUnit.MILLISECONDS) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe { + sharedViewModel.handle(BootstrapActions.HandleForgotBackupPassphrase) + } + .disposeOnDestroyView() + + bootstrapMigrateUseFile.clicks() + .debounce(300, TimeUnit.MILLISECONDS) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe { + startImportTextFromFileIntent(this, IMPORT_FILE_REQ) + } + .disposeOnDestroyView() + } + + 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 secret = bootstrapMigrateEditText.text?.toString() + if (secret.isNullOrBlank()) { + 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)) { + bootstrapRecoveryKeyEnterTil.error = getString(R.string.bootstrap_invalid_recovery_key) + } else { + view?.hideKeyboard() + if (isEnteringKey) { + sharedViewModel.handle(BootstrapActions.DoMigrateWithRecoveryKey(secret)) + } else { + sharedViewModel.handle(BootstrapActions.DoMigrateWithPassphrase(secret)) + } + } + } + + override fun invalidate() = withState(sharedViewModel) { state -> + if (state.step !is BootstrapStep.GetBackupSecretForMigration) { + return@withState + } + + val isEnteringKey = + when (state.step) { + is BootstrapStep.GetBackupSecretPassForMigration -> state.step.useKey + else -> true + } + + if (isEnteringKey) { + bootstrapMigrateShowPassword.isVisible = false + bootstrapMigrateEditText.inputType = TYPE_CLASS_TEXT or TYPE_TEXT_VARIATION_VISIBLE_PASSWORD or TYPE_TEXT_FLAG_MULTI_LINE + + 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 + + bootstrapMigrateEditText.hint = recKey + bootstrapMigrateForgotPassphrase.isVisible = false + bootstrapMigrateUseFile.isVisible = true + } else { + bootstrapMigrateShowPassword.isVisible = true + + if (state.step is BootstrapStep.GetBackupSecretPassForMigration) { + val isPasswordVisible = state.step.isPasswordVisible + bootstrapMigrateEditText.showPassword(isPasswordVisible, updateCursor = false) + bootstrapMigrateShowPassword.setImageResource(if (isPasswordVisible) R.drawable.ic_eye_closed_black else R.drawable.ic_eye_black) + } + + bootstrapDescriptionText.text = getString(R.string.bootstrap_migration_enter_backup_password) + + bootstrapMigrateEditText.hint = getString(R.string.passphrase_enter_passphrase) + + bootstrapMigrateForgotPassphrase.isVisible = true + + val recKey = getString(R.string.bootstrap_migration_use_recovery_key) + bootstrapMigrateForgotPassphrase.text = getString(R.string.bootstrap_migration_with_passphrase_helper_with_link, recKey) + .toSpannable() + .colorizeMatchingText(recKey, colorProvider.getColorFromAttribute(android.R.attr.textColorLink)) + + bootstrapMigrateUseFile.isVisible = false + } + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + if (requestCode == IMPORT_FILE_REQ && resultCode == Activity.RESULT_OK) { + data?.data?.let { dataURI -> + tryThis { + activity?.contentResolver?.openInputStream(dataURI) + ?.bufferedReader() + ?.use { it.readText() } + ?.let { + bootstrapMigrateEditText.setText(it) + } + } + } + return + } + super.onActivityResult(requestCode, resultCode, data) + } + + companion object { + private const val IMPORT_FILE_REQ = 0 + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapSharedViewModel.kt b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapSharedViewModel.kt index 998899374c..55481b1f5b 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapSharedViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapSharedViewModel.kt @@ -31,20 +31,26 @@ import com.nulabinc.zxcvbn.Zxcvbn import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.AssistedInject 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.extensions.exhaustive import im.vector.riotx.core.platform.VectorViewModel import im.vector.riotx.core.platform.WaitingViewData import im.vector.riotx.core.resources.StringProvider import im.vector.riotx.features.login.ReAuthHelper +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 = Uninitialized, val passphraseStrength: Async = Uninitialized, @@ -55,20 +61,13 @@ data class BootstrapViewState( val recoverySaveFileProcess: Async = Uninitialized ) : MvRxState -sealed class 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 Initializing : BootstrapStep() - data class SaveRecoveryKey(val isSaved: Boolean) : BootstrapStep() - object DoneSuccess : BootstrapStep() -} - class BootstrapSharedViewModel @AssistedInject constructor( @Assisted initialState: BootstrapViewState, + @Assisted val args: BootstrapBottomSheet.Args, private val stringProvider: StringProvider, private val session: Session, private val bootstrapTask: BootstrapCrossSigningTask, + private val migrationTask: BackupToQuadSMigrationTask, private val reAuthHelper: ReAuthHelper ) : VectorViewModel(initialState) { @@ -76,7 +75,53 @@ class BootstrapSharedViewModel @AssistedInject constructor( @AssistedInject.Factory interface Factory { - fun create(initialState: BootstrapViewState): BootstrapSharedViewModel + fun create(initialState: BootstrapViewState, args: BootstrapBottomSheet.Args): BootstrapSharedViewModel + } + + init { + // need to check if user have an existing keybackup + if (args.isNewAccount) { + setState { + copy(step = BootstrapStep.SetupPassphrase(false)) + } + } else { + setState { + copy(step = BootstrapStep.CheckingMigration) + } + + // We need to check if there is an existing backup + viewModelScope.launch(Dispatchers.IO) { + val version = awaitCallback { + session.cryptoService().keysBackupService().getCurrentVersion(it) + } + if (version == null) { + // we just resume plain bootstrap + setState { + copy(step = BootstrapStep.SetupPassphrase(false)) + } + } else { + // we need to get existing backup passphrase/key and convert to SSSS + val keyVersion = awaitCallback { + session.cryptoService().keysBackupService().getVersion(version.version ?: "", it) + } + if (keyVersion == null) { + // 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) + } + } + } + } + } + } } override fun handle(action: BootstrapActions) = withState { state -> @@ -84,23 +129,27 @@ class BootstrapSharedViewModel @AssistedInject constructor( is BootstrapActions.GoBack -> queryBack() BootstrapActions.TogglePasswordVisibility -> { when (state.step) { - is BootstrapStep.SetupPassphrase -> { + is BootstrapStep.SetupPassphrase -> { setState { copy(step = state.step.copy(isPasswordVisible = !state.step.isPasswordVisible)) } } - is BootstrapStep.ConfirmPassphrase -> { + is BootstrapStep.ConfirmPassphrase -> { setState { copy(step = state.step.copy(isPasswordVisible = !state.step.isPasswordVisible)) } } - - is BootstrapStep.AccountPassword -> { + is BootstrapStep.AccountPassword -> { setState { copy(step = state.step.copy(isPasswordVisible = !state.step.isPasswordVisible)) } } - else -> { + is BootstrapStep.GetBackupSecretPassForMigration -> { + setState { + copy(step = state.step.copy(isPasswordVisible = !state.step.isPasswordVisible)) + } + } + else -> { } } } @@ -197,12 +246,25 @@ class BootstrapSharedViewModel @AssistedInject constructor( copy(step = BootstrapStep.AccountPassword(false)) } } + BootstrapActions.HandleForgotBackupPassphrase -> { + if (state.step is BootstrapStep.GetBackupSecretPassForMigration) { + setState { + copy(step = BootstrapStep.GetBackupSecretPassForMigration(state.step.isPasswordVisible, true)) + } + } else return@withState + } is BootstrapActions.ReAuth -> { startInitializeFlow( state.currentReAuth?.copy(password = action.pass) ?: UserPasswordAuth(user = session.myUserId, password = action.pass) ) } + is BootstrapActions.DoMigrateWithPassphrase -> { + startMigrationFlow(state.step, action.passphrase, null) + } + is BootstrapActions.DoMigrateWithRecoveryKey -> { + startMigrationFlow(state.step, null, action.recoveryKey) + } }.exhaustive } @@ -210,7 +272,7 @@ class BootstrapSharedViewModel @AssistedInject constructor( // Business Logic // ======================================= private fun saveRecoveryKeyToUri(os: OutputStream) = withState { state -> - viewModelScope.launch { + viewModelScope.launch(Dispatchers.IO) { kotlin.runCatching { os.use { os.write((state.recoveryKeyCreationInfo?.recoveryKey?.formatRecoveryKey() ?: "").toByteArray()) @@ -231,6 +293,57 @@ class BootstrapSharedViewModel @AssistedInject constructor( } } + private fun startMigrationFlow(prevState: BootstrapStep, passphrase: String?, recoveryKey: String?) { + setState { + copy(step = BootstrapStep.Initializing) + } + viewModelScope.launch(Dispatchers.IO) { + val progressListener = object : BootstrapProgressListener { + override fun onProgress(data: WaitingViewData) { + setState { + copy( + initializationWaitingViewData = data + ) + } + } + } + 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) { + setState { + copy( + step = BootstrapStep.AccountPassword(false) + ) + } + } else { + startInitializeFlow(auth) + } + } else { + _viewEvents.post( + BootstrapViewEvents.ModalError( + (it as? BackupToQuadSMigrationTask.Result.Failure)?.error + ?: stringProvider.getString(R.string.matrix_error + ) + ) + ) + setState { + copy( + step = prevState + ) + } + } + } + } + } + private fun startInitializeFlow(auth: UserPasswordAuth?) { setState { copy(step = BootstrapStep.Initializing) @@ -247,11 +360,12 @@ class BootstrapSharedViewModel @AssistedInject constructor( } withState { state -> - viewModelScope.launch { + viewModelScope.launch(Dispatchers.IO) { bootstrapTask.invoke(this, Params( userPasswordAuth = auth ?: reAuthHelper.rememberedAuth(), progressListener = progressListener, - passphrase = state.passphrase + passphrase = state.passphrase, + keySpec = state.migrationRecoveryKey?.let { extractCurveKeyFromRecoveryKey(it)?.let { RawBytesKeySpec(it) } } )) { when (it) { is BootstrapResult.Success -> { @@ -309,11 +423,30 @@ class BootstrapSharedViewModel @AssistedInject constructor( private fun queryBack() = withState { state -> when (state.step) { - is BootstrapStep.SetupPassphrase -> { + is BootstrapStep.GetBackupSecretPassForMigration -> { + if (state.step.useKey) { + // go back to passphrase + setState { + copy( + step = BootstrapStep.GetBackupSecretPassForMigration( + isPasswordVisible = state.step.isPasswordVisible, + useKey = false + ) + ) + } + } else { + _viewEvents.post(BootstrapViewEvents.SkipBootstrap()) + } + } + is BootstrapStep.GetBackupSecretKeyForMigration -> { // do we let you cancel from here? _viewEvents.post(BootstrapViewEvents.SkipBootstrap()) } - is BootstrapStep.ConfirmPassphrase -> { + is BootstrapStep.SetupPassphrase -> { + // do we let you cancel from here? + _viewEvents.post(BootstrapViewEvents.SkipBootstrap()) + } + is BootstrapStep.ConfirmPassphrase -> { setState { copy( step = BootstrapStep.SetupPassphrase( @@ -322,15 +455,15 @@ class BootstrapSharedViewModel @AssistedInject constructor( ) } } - is BootstrapStep.AccountPassword -> { + is BootstrapStep.AccountPassword -> { _viewEvents.post(BootstrapViewEvents.SkipBootstrap(state.passphrase != null)) } - BootstrapStep.Initializing -> { + BootstrapStep.Initializing -> { // do we let you cancel from here? _viewEvents.post(BootstrapViewEvents.SkipBootstrap(state.passphrase != null)) } is BootstrapStep.SaveRecoveryKey, - BootstrapStep.DoneSuccess -> { + BootstrapStep.DoneSuccess -> { // nop } } @@ -344,7 +477,9 @@ class BootstrapSharedViewModel @AssistedInject constructor( override fun create(viewModelContext: ViewModelContext, state: BootstrapViewState): BootstrapSharedViewModel? { val fragment: BootstrapBottomSheet = (viewModelContext as FragmentViewModelContext).fragment() - return fragment.bootstrapViewModelFactory.create(state) + val args: BootstrapBottomSheet.Args = fragment.arguments?.getParcelable(BootstrapBottomSheet.EXTRA_ARGS) + ?: BootstrapBottomSheet.Args(true) + return fragment.bootstrapViewModelFactory.create(state, args) } } } diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapStep.kt b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapStep.kt new file mode 100644 index 0000000000..2c649ae99c --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapStep.kt @@ -0,0 +1,92 @@ +/* + * 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 + +/** + * β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + * β”‚ User has signing keys? │──────────── Account + * β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ Creation ? + * β”‚ β”‚ + * No β”‚ + * β”‚ β”‚ + * β”‚ β”‚ + * β–Ό β”‚ + * β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ + * β”‚ BootstrapStep.CheckingMigration β”‚ β”‚ + * β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ + * β”‚ β”‚ + * β”‚ β”‚ + * Existing β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€No ───────┐ β”‚ + * β”Œβ”€β”€β”€β”€Keybackupβ”€β”€β”€β”€β”€β”€β”€β”˜ KeyBackup β”‚ β”‚ + * β”‚ β”‚ β”‚ + * β”‚ β–Ό β–Ό + * β–Ό β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + * β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ BootstrapStep.SetupPassphrase │◀─┐ + * β”‚BootstrapStep.GetBackupSecretForMigrationβ”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ + * β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ + * β”‚ β”‚ β”ŒBack + * β”‚ β–Ό β”‚ + * β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ + * β”‚ β”‚ BootstrapStep.ConfirmPassphrase │──┐ + * β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ + * β”‚ β”‚ β”‚ + * β”‚ is password needed? β”‚ + * β”‚ β”‚ β”‚ + * β”‚ β–Ό β”‚ + * β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ + * β”‚ β”‚ BootstrapStep.AccountPassword β”‚ β”‚ + * β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ + * β”‚ β”‚ β”‚ + * β”‚ β”‚ β”‚ + * β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ password not needed (in + * β”‚ β”‚ memory) + * β”‚ β”‚ β”‚ + * β”‚ β–Ό β”‚ + * β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ + * └────────▢│ BootstrapStep.Initializing β”‚β—€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + * β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + * β”‚ + * β”‚ + * β”‚ + * β–Ό + * β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + * β”‚ BootstrapStep.SaveRecoveryKey β”‚ + * β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + * β”‚ + * β”‚ + * β”‚ + * β–Ό + * β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + * β”‚ BootstrapStep.DoneSuccess β”‚ + * β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + * + */ + +sealed class 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() + object GetBackupSecretKeyForMigration : GetBackupSecretForMigration() + + object Initializing : BootstrapStep() + data class SaveRecoveryKey(val isSaved: Boolean) : BootstrapStep() + object DoneSuccess : BootstrapStep() +} diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapWaitingFragment.kt b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapWaitingFragment.kt index ff79fa6a4b..1c6c9e6d9b 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapWaitingFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapWaitingFragment.kt @@ -16,8 +16,7 @@ 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 @@ -31,12 +30,22 @@ class BootstrapWaitingFragment @Inject constructor() : VectorBaseFragment() { val sharedViewModel: BootstrapSharedViewModel by parentFragmentViewModel() - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - } - override fun invalidate() = withState(sharedViewModel) { state -> - if (state.step !is BootstrapStep.Initializing) return@withState - bootstrapLoadingStatusText.text = state.initializationWaitingViewData?.message + when (state.step) { + is BootstrapStep.Initializing -> { + bootstrapLoadingStatusText.isVisible = true + bootstrapDescriptionText.isVisible = true + bootstrapLoadingStatusText.text = state.initializationWaitingViewData?.message + } +// is BootstrapStep.CheckingMigration -> { +// bootstrapLoadingStatusText.isVisible = false +// bootstrapDescriptionText.isVisible = false +// } + else -> { + // just show the spinner + bootstrapLoadingStatusText.isVisible = false + bootstrapDescriptionText.isVisible = false + } + } } } diff --git a/vector/src/main/java/im/vector/riotx/features/home/HomeActivity.kt b/vector/src/main/java/im/vector/riotx/features/home/HomeActivity.kt index e286c82532..2449c635e4 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/HomeActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/HomeActivity.kt @@ -91,13 +91,13 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable { .observe() .subscribe { sharedAction -> when (sharedAction) { - is HomeActivitySharedAction.OpenDrawer -> drawerLayout.openDrawer(GravityCompat.START) - is HomeActivitySharedAction.OpenGroup -> { + is HomeActivitySharedAction.OpenDrawer -> drawerLayout.openDrawer(GravityCompat.START) + is HomeActivitySharedAction.OpenGroup -> { drawerLayout.closeDrawer(GravityCompat.START) replaceFragment(R.id.homeDetailFragmentContainer, HomeDetailFragment::class.java) } is HomeActivitySharedAction.PromptForSecurityBootstrap -> { - BootstrapBottomSheet().apply { isCancelable = false }.show(supportFragmentManager, "BootstrapBottomSheet") + BootstrapBottomSheet.show(supportFragmentManager, true) } } } @@ -109,6 +109,7 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable { } if (intent.getBooleanExtra(EXTRA_ACCOUNT_CREATION, false)) { sharedActionViewModel.post(HomeActivitySharedAction.PromptForSecurityBootstrap) + sharedActionViewModel.isAccountCreation = true intent.removeExtra(EXTRA_ACCOUNT_CREATION) } @@ -163,29 +164,48 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable { .getMyCrossSigningKeys() val crossSigningEnabledOnAccount = myCrossSigningKeys != null - if (crossSigningEnabledOnAccount && myCrossSigningKeys?.isTrusted() == false) { + if (!crossSigningEnabledOnAccount && !sharedActionViewModel.isAccountCreation) { // We need to ask - sharedActionViewModel.hasDisplayedCompleteSecurityPrompt = true - popupAlertManager.postVectorAlert( - VerificationVectorAlert( - uid = "completeSecurity", - title = getString(R.string.complete_security), - description = getString(R.string.crosssigning_verify_this_session), - iconId = R.drawable.ic_shield_warning - ).apply { - matrixItem = session.getUser(session.myUserId)?.toMatrixItem() - colorInt = ContextCompat.getColor(this@HomeActivity, R.color.riotx_positive_accent) - contentAction = Runnable { - (weakCurrentActivity?.get() as? VectorBaseActivity)?.let { - it.navigator.waitSessionVerification(it) - } - } - dismissedAction = Runnable {} - } - ) + promptSecurityEvent( + session, + R.string.upgrade_security, + R.string.security_prompt_text + ) { + it.navigator.upgradeSessionSecurity(it) + } + } else if (myCrossSigningKeys?.isTrusted() == false) { + // We need to ask + promptSecurityEvent( + session, + R.string.complete_security, + R.string.crosssigning_verify_this_session + ) { + it.navigator.waitSessionVerification(it) + } } } + private fun promptSecurityEvent(session: Session, titleRes: Int, descRes: Int, action: ((VectorBaseActivity) -> Unit)) { + sharedActionViewModel.hasDisplayedCompleteSecurityPrompt = true + popupAlertManager.postVectorAlert( + VerificationVectorAlert( + uid = "upgradeSecurity", + title = getString(titleRes), + description = getString(descRes), + iconId = R.drawable.ic_shield_warning + ).apply { + matrixItem = session.getUser(session.myUserId)?.toMatrixItem() + colorInt = ContextCompat.getColor(this@HomeActivity, R.color.riotx_positive_accent) + contentAction = Runnable { + (weakCurrentActivity?.get() as? VectorBaseActivity)?.let { + action(it) + } + } + dismissedAction = Runnable {} + } + ) + } + override fun onNewIntent(intent: Intent?) { super.onNewIntent(intent) if (intent?.hasExtra(EXTRA_CLEAR_EXISTING_NOTIFICATION) == true) { diff --git a/vector/src/main/java/im/vector/riotx/features/home/HomeDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/HomeDetailFragment.kt index 47338f6335..564b030081 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/HomeDetailFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/HomeDetailFragment.kt @@ -102,7 +102,7 @@ class HomeDetailFragment @Inject constructor( VerificationVectorAlert( uid = uid, title = getString(R.string.new_session), - description = getString(R.string.new_session_review), + description = getString(R.string.new_session_review_with_info, newest.displayName ?: "", newest.deviceId ?: ""), iconId = R.drawable.ic_shield_warning ).apply { matrixItem = user diff --git a/vector/src/main/java/im/vector/riotx/features/home/HomeSharedActionViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/HomeSharedActionViewModel.kt index ecbe460b90..4af9262d05 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/HomeSharedActionViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/HomeSharedActionViewModel.kt @@ -21,4 +21,5 @@ import javax.inject.Inject class HomeSharedActionViewModel @Inject constructor() : VectorSharedActionViewModel() { var hasDisplayedCompleteSecurityPrompt : Boolean = false + var isAccountCreation : Boolean = false } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/VerificationRequestItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/VerificationRequestItem.kt index ef43605b04..12ef5c3577 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/VerificationRequestItem.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/VerificationRequestItem.kt @@ -81,7 +81,8 @@ abstract class VerificationRequestItem : AbsBaseMessageItem { holder.buttonBar.isVisible = false - holder.statusTextView.text = holder.view.context.getString(R.string.verification_request_other_cancelled, attributes.informationData.memberName) + holder.statusTextView.text = holder.view.context + .getString(R.string.verification_request_other_cancelled, attributes.informationData.memberName) holder.statusTextView.isVisible = true } VerificationState.CANCELED_BY_ME -> { diff --git a/vector/src/main/java/im/vector/riotx/features/media/VideoMediaViewerActivity.kt b/vector/src/main/java/im/vector/riotx/features/media/VideoMediaViewerActivity.kt index e92b58c681..4c98472f6e 100644 --- a/vector/src/main/java/im/vector/riotx/features/media/VideoMediaViewerActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/media/VideoMediaViewerActivity.kt @@ -43,7 +43,11 @@ class VideoMediaViewerActivity : VectorBaseActivity() { configureToolbar(videoMediaViewerToolbar, mediaData) imageContentRenderer.render(mediaData.thumbnailMediaData, ImageContentRenderer.Mode.FULL_SIZE, videoMediaViewerThumbnailView) - videoContentRenderer.render(mediaData, videoMediaViewerThumbnailView, videoMediaViewerLoading, videoMediaViewerVideoView, videoMediaViewerErrorView) + videoContentRenderer.render(mediaData, + videoMediaViewerThumbnailView, + videoMediaViewerLoading, + videoMediaViewerVideoView, + videoMediaViewerErrorView) } } diff --git a/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt b/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt index 2e91090ec4..0f19a1292a 100644 --- a/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt +++ b/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt @@ -34,6 +34,7 @@ import im.vector.riotx.core.utils.toast import im.vector.riotx.features.createdirect.CreateDirectRoomActivity import im.vector.riotx.features.crypto.keysbackup.settings.KeysBackupManageActivity import im.vector.riotx.features.crypto.keysbackup.setup.KeysBackupSetupActivity +import im.vector.riotx.features.crypto.recover.BootstrapBottomSheet import im.vector.riotx.features.crypto.verification.SupportedVerificationMethodsProvider import im.vector.riotx.features.crypto.verification.VerificationBottomSheet import im.vector.riotx.features.debug.DebugMenuActivity @@ -107,6 +108,12 @@ class DefaultNavigator @Inject constructor( } } + override fun upgradeSessionSecurity(context: Context) { + if (context is VectorBaseActivity) { + BootstrapBottomSheet.show(context.supportFragmentManager, false) + } + } + override fun openNotJoinedRoom(context: Context, roomIdOrAlias: String?, eventId: String?, buildTask: Boolean) { if (context is VectorBaseActivity) { context.notImplemented("Open not joined room") diff --git a/vector/src/main/java/im/vector/riotx/features/navigation/Navigator.kt b/vector/src/main/java/im/vector/riotx/features/navigation/Navigator.kt index 65ef08dd05..bf99643912 100644 --- a/vector/src/main/java/im/vector/riotx/features/navigation/Navigator.kt +++ b/vector/src/main/java/im/vector/riotx/features/navigation/Navigator.kt @@ -34,6 +34,8 @@ interface Navigator { fun waitSessionVerification(context: Context) + fun upgradeSessionSecurity(context: Context) + fun openRoomForSharingAndFinish(activity: Activity, roomId: String, sharedData: SharedData) fun openNotJoinedRoom(context: Context, roomIdOrAlias: String?, eventId: String? = null, buildTask: Boolean = false) diff --git a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsSecurityPrivacyFragment.kt b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsSecurityPrivacyFragment.kt index d357c6ae82..bb83658ae7 100644 --- a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsSecurityPrivacyFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsSecurityPrivacyFragment.kt @@ -58,23 +58,10 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor( override var titleRes = R.string.settings_security_and_privacy override val preferenceXmlRes = R.xml.vector_settings_security_privacy - // devices: device IDs and device names - private val mDevicesNameList: MutableList = mutableListOf() - - private var mMyDeviceInfo: DeviceInfo? = null - // cryptography private val mCryptographyCategory by lazy { findPreference(VectorPreferences.SETTINGS_CRYPTOGRAPHY_PREFERENCE_KEY)!! } - // cryptography manage - private val mCryptographyManageCategory by lazy { - findPreference(VectorPreferences.SETTINGS_CRYPTOGRAPHY_MANAGE_PREFERENCE_KEY)!! - } - // displayed pushers - private val mPushersSettingsCategory by lazy { - findPreference(VectorPreferences.SETTINGS_NOTIFICATIONS_TARGETS_PREFERENCE_KEY)!! - } private val mCrossSigningStatePreference by lazy { findPreference(VectorPreferences.SETTINGS_ENCRYPTION_CROSS_SIGNING_PREFERENCE_KEY)!! @@ -106,7 +93,6 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor( // My device name may have been updated refreshMyDevice() refreshXSigningStatus() - mCryptographyCategory.isVisible = vectorPreferences.developerMode() } override fun bindPref() { @@ -133,7 +119,6 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor( } private fun refreshXSigningStatus() { - if (vectorPreferences.developerMode()) { val crossSigningKeys = session.cryptoService().crossSigningService().getMyCrossSigningKeys() val xSigningIsEnableInAccount = crossSigningKeys != null val xSigningKeysAreTrusted = session.cryptoService().crossSigningService().checkUserTrust(session.myUserId).isVerified() @@ -154,9 +139,6 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor( } mCrossSigningStatePreference.isVisible = true - } else { - mCrossSigningStatePreference.isVisible = false - } } override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) { @@ -203,7 +185,10 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor( */ private fun exportKeys() { // We need WRITE_EXTERNAL permission - if (checkPermissions(PERMISSIONS_FOR_WRITING_FILES, this, PERMISSION_REQUEST_CODE_EXPORT_KEYS, R.string.permissions_rationale_msg_keys_backup_export)) { + if (checkPermissions(PERMISSIONS_FOR_WRITING_FILES, + this, + PERMISSION_REQUEST_CODE_EXPORT_KEYS, + R.string.permissions_rationale_msg_keys_backup_export)) { activity?.let { activity -> ExportKeysDialog().show(activity, object : ExportKeysDialog.ExportKeyDialogListener { override fun onPassphrase(passphrase: String) { @@ -346,15 +331,6 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor( // Cryptography // ============================================================================================================== - private fun removeCryptographyPreference() { - preferenceScreen.let { - it.removePreference(mCryptographyCategory) - - // Also remove keys management section - it.removePreference(mCryptographyManageCategory) - } - } - /** * Build the cryptography preference section. * diff --git a/vector/src/main/java/im/vector/riotx/features/settings/crosssigning/CrossSigningEpoxyController.kt b/vector/src/main/java/im/vector/riotx/features/settings/crosssigning/CrossSigningEpoxyController.kt index e33b12d19a..dd050c29ac 100644 --- a/vector/src/main/java/im/vector/riotx/features/settings/crosssigning/CrossSigningEpoxyController.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/crosssigning/CrossSigningEpoxyController.kt @@ -24,13 +24,15 @@ 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 dimensionConverter: DimensionConverter, + private val vectorPreferences: VectorPreferences ) : TypedEpoxyController() { interface InteractionListener { @@ -49,7 +51,7 @@ class CrossSigningEpoxyController @Inject constructor( titleIconResourceId(R.drawable.ic_shield_trusted) title(stringProvider.getString(R.string.encryption_information_dg_xsigning_complete)) } - if (!data.isUploadingKeys) { + if (vectorPreferences.developerMode() && !data.isUploadingKeys) { bottomSheetVerificationActionItem { id("resetkeys") title("Reset keys") @@ -68,14 +70,16 @@ class CrossSigningEpoxyController @Inject constructor( title(stringProvider.getString(R.string.encryption_information_dg_xsigning_trusted)) } if (!data.isUploadingKeys) { - bottomSheetVerificationActionItem { - id("resetkeys") - title("Reset keys") - titleColor(colorProvider.getColor(R.color.riotx_destructive_accent)) - iconRes(R.drawable.ic_arrow_right) - iconColor(colorProvider.getColor(R.color.riotx_destructive_accent)) - listener { - interactionListener?.onResetCrossSigningKeys() + if (vectorPreferences.developerMode()) { + bottomSheetVerificationActionItem { + id("resetkeys") + title("Reset keys") + titleColor(colorProvider.getColor(R.color.riotx_destructive_accent)) + iconRes(R.drawable.ic_arrow_right) + iconColor(colorProvider.getColor(R.color.riotx_destructive_accent)) + listener { + interactionListener?.onResetCrossSigningKeys() + } } } @@ -106,14 +110,16 @@ class CrossSigningEpoxyController @Inject constructor( interactionListener?.verifySession() } } - bottomSheetVerificationActionItem { - id("resetkeys") - title("Reset keys") - titleColor(colorProvider.getColor(R.color.riotx_destructive_accent)) - iconRes(R.drawable.ic_arrow_right) - iconColor(colorProvider.getColor(R.color.riotx_destructive_accent)) - listener { - interactionListener?.onResetCrossSigningKeys() + if (vectorPreferences.developerMode()) { + bottomSheetVerificationActionItem { + id("resetkeys") + title("Reset keys") + titleColor(colorProvider.getColor(R.color.riotx_destructive_accent)) + iconRes(R.drawable.ic_arrow_right) + iconColor(colorProvider.getColor(R.color.riotx_destructive_accent)) + listener { + interactionListener?.onResetCrossSigningKeys() + } } } } else { @@ -121,7 +127,7 @@ class CrossSigningEpoxyController @Inject constructor( id("not") title(stringProvider.getString(R.string.encryption_information_dg_xsigning_disabled)) } - if (!data.isUploadingKeys) { + if (vectorPreferences.developerMode() && !data.isUploadingKeys) { bottomSheetVerificationActionItem { id("initKeys") title("Initialize keys") diff --git a/vector/src/main/res/drawable/ic_file.xml b/vector/src/main/res/drawable/ic_file.xml new file mode 100644 index 0000000000..2b74a19a94 --- /dev/null +++ b/vector/src/main/res/drawable/ic_file.xml @@ -0,0 +1,21 @@ + + + + diff --git a/vector/src/main/res/drawable/ic_message_key.xml b/vector/src/main/res/drawable/ic_message_key.xml index c4a415477b..9c5e53571d 100644 --- a/vector/src/main/res/drawable/ic_message_key.xml +++ b/vector/src/main/res/drawable/ic_message_key.xml @@ -7,13 +7,10 @@ + android:strokeColor="#2E2F32"/> + android:strokeColor="#2E2F32"/> + + + + + + + + + + + + + + + + + + + + + diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index 6b0591595a..66638e5d62 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -2202,7 +2202,7 @@ Not all features in Riot are implemented in RiotX yet. Main missing (and coming Refresh - New Session + Unverified login. Was this you? Tap to review & verify Use this session to verify your new one, granting it access to encrypted messages. This wasn’t me @@ -2219,7 +2219,7 @@ Not all features in Riot are implemented in RiotX yet. Main missing (and coming Verify your devices from Settings. Verification Cancelled - Message Password + Recovery Passphrase Message Key Account Password @@ -2243,7 +2243,7 @@ Not all features in Riot are implemented in RiotX yet. Main missing (and coming This might take several seconds, please be patient. Setting up recovery. Your recovery key - Youβ€˜re done! + "You're done!" Keep it safe Finish @@ -2269,8 +2269,8 @@ Not all features in Riot are implemented in RiotX yet. Main missing (and coming You cannot do that from mobile - Setting a Message Password lets you secure & unlock encrypted messages and trust.\n\nIf you don’t want to set a Message Password, generate a Message Key instead. - Setting a Message Password lets you secure & unlock encrypted messages and trust. + 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. + Setting a Recovery Passphrase lets you secure & unlock encrypted messages and trust. Encryption enabled diff --git a/vector/src/main/res/values/strings_riotX.xml b/vector/src/main/res/values/strings_riotX.xml index b7b65ad311..2cd97098af 100644 --- a/vector/src/main/res/values/strings_riotX.xml +++ b/vector/src/main/res/values/strings_riotX.xml @@ -7,6 +7,37 @@ Message… + + Encryption upgrade available + Verify yourself & others to keep your chats safe + + + Enter your %s to continue + Use File + + + + Enter %s + Recovery Passphrase + "It's not a valid recovery key" + Please enter a recovery key + + Checking backup Key + Checking backup Key (%s) + Getting curve key + Generating SSSS key from passphrase + Generating SSSS key from passphrase (%s) + Generating SSSS key from recovery key + Storing keybackup secret in SSSS + + %1$s (%2$s) + + Enter your Key Backup Passphrase to continue. + use your Key Backup recovery key + + Don’t know your Key Backup Passphrase, you can %s. + Key Backup recovery key + diff --git a/vector/src/main/res/xml/vector_settings_security_privacy.xml b/vector/src/main/res/xml/vector_settings_security_privacy.xml index 1002c16782..f394e319c7 100644 --- a/vector/src/main/res/xml/vector_settings_security_privacy.xml +++ b/vector/src/main/res/xml/vector_settings_security_privacy.xml @@ -6,9 +6,7 @@ + android:title="@string/settings_cryptography">