Merge pull request #1759 from vector-im/feature/bma/recoveryKeyWithSpaces

Recovery key with spaces
This commit is contained in:
Benoit Marty 2023-11-08 14:01:30 +01:00 committed by GitHub
commit 4c9da30fff
15 changed files with 124 additions and 24 deletions

View file

@ -26,6 +26,7 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import io.element.android.features.securebackup.impl.setup.views.RecoveryKeyUserStory import io.element.android.features.securebackup.impl.setup.views.RecoveryKeyUserStory
import io.element.android.features.securebackup.impl.setup.views.RecoveryKeyViewState import io.element.android.features.securebackup.impl.setup.views.RecoveryKeyViewState
import io.element.android.features.securebackup.impl.tools.RecoveryKeyTools
import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.Async
import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.architecture.runCatchingUpdatingState import io.element.android.libraries.architecture.runCatchingUpdatingState
@ -36,6 +37,7 @@ import javax.inject.Inject
class SecureBackupEnterRecoveryKeyPresenter @Inject constructor( class SecureBackupEnterRecoveryKeyPresenter @Inject constructor(
private val encryptionService: EncryptionService, private val encryptionService: EncryptionService,
private val recoveryKeyTools: RecoveryKeyTools,
) : Presenter<SecureBackupEnterRecoveryKeyState> { ) : Presenter<SecureBackupEnterRecoveryKeyState> {
@Composable @Composable
@ -54,7 +56,14 @@ class SecureBackupEnterRecoveryKeyPresenter @Inject constructor(
submitAction.value = Async.Uninitialized submitAction.value = Async.Uninitialized
} }
is SecureBackupEnterRecoveryKeyEvents.OnRecoveryKeyChange -> { is SecureBackupEnterRecoveryKeyEvents.OnRecoveryKeyChange -> {
recoveryKey = event.recoveryKey.replace("\\s+".toRegex(), "") val previousRecoveryKey = recoveryKey
recoveryKey = if (previousRecoveryKey.isEmpty() && recoveryKeyTools.isRecoveryKeyFormatValid(event.recoveryKey)) {
// A Recovery key has been entered, remove the spaces for a better rendering
event.recoveryKey.replace("\\s+".toRegex(), "")
} else {
// Keep the recovery key as entered by the user. May contains spaces.
event.recoveryKey
}
} }
SecureBackupEnterRecoveryKeyEvents.Submit -> { SecureBackupEnterRecoveryKeyEvents.Submit -> {
// No need to remove the spaces, the SDK will do it. // No need to remove the spaces, the SDK will do it.

View file

@ -30,10 +30,14 @@ import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.autofill.AutofillType
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@ -46,6 +50,7 @@ import io.element.android.libraries.designsystem.theme.components.CircularProgre
import io.element.android.libraries.designsystem.theme.components.Icon import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.designsystem.theme.components.OutlinedTextField import io.element.android.libraries.designsystem.theme.components.OutlinedTextField
import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.theme.components.autofill
import io.element.android.libraries.designsystem.utils.CommonDrawables import io.element.android.libraries.designsystem.utils.CommonDrawables
import io.element.android.libraries.theme.ElementTheme import io.element.android.libraries.theme.ElementTheme
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
@ -147,6 +152,7 @@ private fun RecoveryKeyStaticContent(
} }
} }
@OptIn(ExperimentalComposeUiApi::class)
@Composable @Composable
private fun RecoveryKeyFormContent( private fun RecoveryKeyFormContent(
state: RecoveryKeyViewState, state: RecoveryKeyViewState,
@ -155,17 +161,25 @@ private fun RecoveryKeyFormContent(
) { ) {
onChange ?: error("onChange should not be null") onChange ?: error("onChange should not be null")
onSubmit ?: error("onSubmit should not be null") onSubmit ?: error("onSubmit should not be null")
val recoveryKeyVisualTransformation = remember { val keyHasSpace = state.formattedRecoveryKey.orEmpty().contains(" ")
RecoveryKeyVisualTransformation() val recoveryKeyVisualTransformation = remember(keyHasSpace) {
// Do not apply a visual transformation if the key has spaces, to let user enter passphrase
if (keyHasSpace) VisualTransformation.None else RecoveryKeyVisualTransformation()
} }
OutlinedTextField( OutlinedTextField(
modifier = Modifier.fillMaxWidth(), modifier = Modifier
.fillMaxWidth()
.autofill(
autofillTypes = listOf(AutofillType.Password),
onFill = { onChange(it) },
),
minLines = 2, minLines = 2,
value = state.formattedRecoveryKey.orEmpty(), value = state.formattedRecoveryKey.orEmpty(),
onValueChange = onChange, onValueChange = onChange,
enabled = state.inProgress.not(), enabled = state.inProgress.not(),
visualTransformation = recoveryKeyVisualTransformation, visualTransformation = recoveryKeyVisualTransformation,
keyboardOptions = KeyboardOptions( keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Password,
imeAction = ImeAction.Done, imeAction = ImeAction.Done,
), ),
keyboardActions = KeyboardActions( keyboardActions = KeyboardActions(

View file

@ -0,0 +1,29 @@
/*
* Copyright (c) 2023 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 io.element.android.features.securebackup.impl.tools
import javax.inject.Inject
private const val RECOVERY_KEY_LENGTH = 48
private const val BASE_58_ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
class RecoveryKeyTools @Inject constructor() {
fun isRecoveryKeyFormatValid(recoveryKey: String): Boolean {
val recoveryKeyWithoutSpace = recoveryKey.replace("\\s+".toRegex(), "")
return recoveryKeyWithoutSpace.length == RECOVERY_KEY_LENGTH && recoveryKeyWithoutSpace.all { BASE_58_ALPHABET.contains(it) }
}
}

View file

@ -22,6 +22,7 @@ import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import io.element.android.features.securebackup.impl.setup.views.RecoveryKeyUserStory import io.element.android.features.securebackup.impl.setup.views.RecoveryKeyUserStory
import io.element.android.features.securebackup.impl.setup.views.RecoveryKeyViewState import io.element.android.features.securebackup.impl.setup.views.RecoveryKeyViewState
import io.element.android.features.securebackup.impl.tools.RecoveryKeyTools
import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.Async
import io.element.android.libraries.matrix.api.encryption.EncryptionService import io.element.android.libraries.matrix.api.encryption.EncryptionService
import io.element.android.libraries.matrix.test.AN_EXCEPTION import io.element.android.libraries.matrix.test.AN_EXCEPTION
@ -99,5 +100,6 @@ class SecureBackupEnterRecoveryKeyPresenterTest {
encryptionService: EncryptionService = FakeEncryptionService(), encryptionService: EncryptionService = FakeEncryptionService(),
) = SecureBackupEnterRecoveryKeyPresenter( ) = SecureBackupEnterRecoveryKeyPresenter(
encryptionService = encryptionService, encryptionService = encryptionService,
recoveryKeyTools = RecoveryKeyTools(),
) )
} }

View file

@ -0,0 +1,46 @@
/*
* Copyright (c) 2023 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 io.element.android.features.securebackup.impl.tools
import com.google.common.truth.Truth.assertThat
import org.junit.Test
class RecoveryKeyToolsTest {
@Test
fun `isRecoveryKeyFormatValid return false for invalid key`() {
val sut = RecoveryKeyTools()
assertThat(sut.isRecoveryKeyFormatValid("")).isFalse()
// Wrong size
assertThat(sut.isRecoveryKeyFormatValid("abcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabc")).isFalse()
assertThat(sut.isRecoveryKeyFormatValid("abcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcda")).isFalse()
// Wrong alphabet 0
assertThat(sut.isRecoveryKeyFormatValid("0bcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcd")).isFalse()
// Wrong alphabet O
assertThat(sut.isRecoveryKeyFormatValid("Obcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcd")).isFalse()
// Wrong alphabet l
assertThat(sut.isRecoveryKeyFormatValid("lbcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcd")).isFalse()
}
@Test
fun `isRecoveryKeyFormatValid return true for valid key`() {
val sut = RecoveryKeyTools()
assertThat(sut.isRecoveryKeyFormatValid("abcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcd")).isTrue()
// Spaces does not count
assertThat(sut.isRecoveryKeyFormatValid("abcd abcd abcd abcd abcd abcd abcd abcd abcd abcd abcd abcd")).isTrue()
}
}

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:ad0763e5588212f988a56f1f2b3558c29b332ce832cb9ebaaffcbcebab4d457c oid sha256:829846411a53193e392dd82a024a835489064b495095a00967ec96e943ccc567
size 46477 size 46323

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:383a54396c54861554b53c95700ac49e84c0b9e0ded95aeb4246ab8aa7e9a703 oid sha256:b5b01a97aec276f26df21b6c6d83968f330d89fd8c7e7ae42c79d88a67a7f705
size 44834 size 44649

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:f9892182ed80b39ebc3151335979967a516793b0ca761df35a7649091372d390 oid sha256:3eec71b78aa369d280904d3c0e7698c57c3d7902b6bb2d96912bae25fe691210
size 32737 size 32591

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:9e421009e738803d834e4fe603900b828f5e4a2d85ffa5784e8d4f7a40810b80 oid sha256:d8725f375d5740d2b494b8ec2ce12e063a173f09de947c41989ae9d2311acfb5
size 43156 size 42987

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:e254d17ebeb9e991eaeac000b9148a0c0279d31eac2996e8bf78491747089b30 oid sha256:3bbaf4d274fa3a52b96d6ac9daaf81843e3b2b3abfa7ee64d54df9135a6d35c8
size 41767 size 41559

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:b8ae3debf28076d7d0d71a2f3784c7d5cfd9dfc50ea534a4b44a952da1a4d947 oid sha256:806c8ead5894e6c0394b53cebce3f4fb53acd4dc021de2c6b7df9e5b2824e681
size 29166 size 29020

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:a5ba80d0d623bc8ddb694d47cfd85e3421ce2baa21e4f19e185c30c86f2fd6a3 oid sha256:b30edf234beec3069a3b0ad051f3c166cd0b9d266491a0dcf5f62a1f5c4ce3fb
size 24259 size 24123

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:a472e8ecafb6d4d19d1b13d12c234a7886981356fbe8b00c9394d5a130585c07 oid sha256:cd8bf20f556a3fe2decdbb5e1f471ed9de9545d39ef5651c5c7b915d3562210f
size 22320 size 22167

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:f3c30ea61015a894b1e2f8c3a576fe83862233c5b3675489c786cff589bf3164 oid sha256:192a4633b725c9bc24c1782cecb4f4e7bf1266372af7ebacf64637aca943a260
size 22718 size 22553

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:ec5a621733bc6923dd3769afc2d3bab644cc51894039e9b0b233716a68a1edbc oid sha256:ce05cc46725f3f283067284b79c7187ec1f6491618d29636b37e55eef4ce39d2
size 20673 size 20567