mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-02-25 20:23:19 +00:00
Update sad paths around manual backup key restore.
This commit is contained in:
committed by
Greyson Parrelli
parent
b5f323d4af
commit
139b62e469
@@ -37,7 +37,7 @@ object IBANVisualTransformation : VisualTransformation {
|
||||
}
|
||||
|
||||
override fun transformedToOriginal(offset: Int): Int {
|
||||
return offset - (offset / 4)
|
||||
return offset - (offset / 5)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,6 +85,7 @@ class AccountValues internal constructor(store: KeyValueStore, context: Context)
|
||||
private const val KEY_HAS_LINKED_DEVICES = "account.has_linked_devices"
|
||||
|
||||
private const val KEY_ACCOUNT_ENTROPY_POOL = "account.account_entropy_pool"
|
||||
private const val KEY_RESTORED_ACCOUNT_ENTROPY_KEY = "account.restored_account_entropy_pool"
|
||||
|
||||
private val AEP_LOCK = ReentrantLock()
|
||||
}
|
||||
@@ -140,16 +141,28 @@ class AccountValues internal constructor(store: KeyValueStore, context: Context)
|
||||
|
||||
fun restoreAccountEntropyPool(aep: AccountEntropyPool) {
|
||||
AEP_LOCK.withLock {
|
||||
store.beginWrite().putString(KEY_ACCOUNT_ENTROPY_POOL, aep.value).commit()
|
||||
store
|
||||
.beginWrite()
|
||||
.putString(KEY_ACCOUNT_ENTROPY_POOL, aep.value)
|
||||
.putBoolean(KEY_RESTORED_ACCOUNT_ENTROPY_KEY, true)
|
||||
.commit()
|
||||
}
|
||||
}
|
||||
|
||||
fun resetAccountEntropyPool() {
|
||||
AEP_LOCK.withLock {
|
||||
store.beginWrite().putString(KEY_ACCOUNT_ENTROPY_POOL, null).commit()
|
||||
Log.i(TAG, "Resetting Account Entropy Pool (AEP)", Throwable())
|
||||
store
|
||||
.beginWrite()
|
||||
.putString(KEY_ACCOUNT_ENTROPY_POOL, null)
|
||||
.putBoolean(KEY_RESTORED_ACCOUNT_ENTROPY_KEY, false)
|
||||
.commit()
|
||||
}
|
||||
}
|
||||
|
||||
@get:Synchronized
|
||||
val restoredAccountEntropyPool by booleanValue(KEY_RESTORED_ACCOUNT_ENTROPY_KEY, false)
|
||||
|
||||
/** The local user's [ACI]. */
|
||||
val aci: ACI?
|
||||
get() = ACI.parseOrNull(getString(KEY_ACI, null))
|
||||
|
||||
@@ -19,7 +19,6 @@ class SvrValues internal constructor(store: KeyValueStore) : SignalStoreValues(s
|
||||
private const val SVR2_AUTH_TOKENS = "kbs.kbs_auth_tokens"
|
||||
private const val SVR_LAST_AUTH_REFRESH_TIMESTAMP = "kbs.kbs_auth_tokens.last_refresh_timestamp"
|
||||
private const val SVR3_AUTH_TOKENS = "kbs.svr3_auth_tokens"
|
||||
private const val RESTORED_VIA_ACCOUNT_ENTROPY_KEY = "kbs.restore_via_account_entropy_pool"
|
||||
private const val INITIAL_RESTORE_MASTER_KEY = "kbs.initialRestoreMasterKey"
|
||||
}
|
||||
|
||||
@@ -145,7 +144,7 @@ class SvrValues internal constructor(store: KeyValueStore) : SignalStoreValues(s
|
||||
|
||||
@Synchronized
|
||||
fun hasOptedInWithAccess(): Boolean {
|
||||
return hasPin() || restoredViaAccountEntropyPool
|
||||
return hasPin() || SignalStore.account.restoredAccountEntropyPool
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
@@ -153,9 +152,6 @@ class SvrValues internal constructor(store: KeyValueStore) : SignalStoreValues(s
|
||||
return localPinHash != null
|
||||
}
|
||||
|
||||
@get:Synchronized
|
||||
val restoredViaAccountEntropyPool by booleanValue(RESTORED_VIA_ACCOUNT_ENTROPY_KEY, false)
|
||||
|
||||
@get:Synchronized
|
||||
@set:Synchronized
|
||||
var isPinForgottenOrSkipped: Boolean by booleanValue(PIN_FORGOTTEN_OR_SKIPPED, false)
|
||||
@@ -242,7 +238,6 @@ class SvrValues internal constructor(store: KeyValueStore) : SignalStoreValues(s
|
||||
.putBoolean(OPTED_OUT, true)
|
||||
.remove(LOCK_LOCAL_PIN_HASH)
|
||||
.remove(PIN)
|
||||
.remove(RESTORED_VIA_ACCOUNT_ENTROPY_KEY)
|
||||
.putLong(LAST_CREATE_FAILED_TIMESTAMP, -1)
|
||||
.commit()
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ public class LogSectionPin implements LogSection {
|
||||
.append("Next Reminder Interval: ").append(SignalStore.pin().getCurrentInterval()).append("\n")
|
||||
.append("Reglock: ").append(SignalStore.svr().isRegistrationLockEnabled()).append("\n")
|
||||
.append("Signal PIN: ").append(SignalStore.svr().hasPin()).append("\n")
|
||||
.append("Restored via AEP: ").append(SignalStore.svr().getRestoredViaAccountEntropyPool()).append("\n")
|
||||
.append("Restored via AEP: ").append(SignalStore.account().getRestoredAccountEntropyPool()).append("\n")
|
||||
.append("Opted Out: ").append(SignalStore.svr().hasOptedOut()).append("\n")
|
||||
.append("Last Creation Failed: ").append(SignalStore.svr().lastPinCreateFailed()).append("\n")
|
||||
.append("Needs Account Restore: ").append(SignalStore.storageService().getNeedsAccountRestore()).append("\n")
|
||||
|
||||
@@ -13,14 +13,13 @@ import androidx.compose.ui.text.input.VisualTransformation
|
||||
/**
|
||||
* Visual formatter for backup keys.
|
||||
*
|
||||
* @param length max length of key
|
||||
* @param chunkSize character count per group
|
||||
*/
|
||||
class BackupKeyVisualTransformation(private val length: Int, private val chunkSize: Int) : VisualTransformation {
|
||||
class BackupKeyVisualTransformation(private val chunkSize: Int) : VisualTransformation {
|
||||
override fun filter(text: AnnotatedString): TransformedText {
|
||||
var output = ""
|
||||
for (i in text.take(length).indices) {
|
||||
output += text[i]
|
||||
for ((i, c) in text.withIndex()) {
|
||||
output += c
|
||||
if (i % chunkSize == chunkSize - 1) {
|
||||
output += " "
|
||||
}
|
||||
@@ -38,7 +37,7 @@ class BackupKeyVisualTransformation(private val length: Int, private val chunkSi
|
||||
}
|
||||
|
||||
override fun transformedToOriginal(offset: Int): Int {
|
||||
return offset - (offset / chunkSize)
|
||||
return offset - (offset / (chunkSize + 1))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,9 @@
|
||||
package org.thoughtcrime.securesms.registrationv3.ui.restore
|
||||
|
||||
import android.graphics.Typeface
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
@@ -18,6 +21,7 @@ import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.LocalTextStyle
|
||||
@@ -36,6 +40,7 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
@@ -47,17 +52,24 @@ import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.launch
|
||||
import org.signal.core.ui.BottomSheets
|
||||
import org.signal.core.ui.Buttons
|
||||
import org.signal.core.ui.Dialogs
|
||||
import org.signal.core.ui.Previews
|
||||
import org.signal.core.ui.SignalPreview
|
||||
import org.signal.core.ui.horizontalGutters
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.backup.v2.ui.BackupsIconColors
|
||||
import org.thoughtcrime.securesms.compose.ComposeFragment
|
||||
import org.thoughtcrime.securesms.registration.data.network.RegisterAccountResult
|
||||
import org.thoughtcrime.securesms.registrationv3.ui.RegistrationState
|
||||
import org.thoughtcrime.securesms.registrationv3.ui.RegistrationViewModel
|
||||
import org.thoughtcrime.securesms.registrationv3.ui.phonenumber.EnterPhoneNumberMode
|
||||
@@ -72,29 +84,50 @@ import org.thoughtcrime.securesms.util.navigation.safeNavigate
|
||||
class EnterBackupKeyFragment : ComposeFragment() {
|
||||
|
||||
companion object {
|
||||
private const val LEARN_MORE_URL = "https://support.signal.org/hc/articles/360007059752-Backup-and-Restore-Messages"
|
||||
private const val LEARN_MORE_URL = "https://support.signal.org/hc/articles/360007059752"
|
||||
}
|
||||
|
||||
private val sharedViewModel by activityViewModels<RegistrationViewModel>()
|
||||
private val viewModel by viewModels<EnterBackupKeyViewModel>()
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
viewLifecycleOwner.lifecycleScope.launch {
|
||||
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.CREATED) {
|
||||
sharedViewModel
|
||||
.state
|
||||
.map { it.registerAccountError }
|
||||
.filterNotNull()
|
||||
.collect {
|
||||
sharedViewModel.registerAccountErrorShown()
|
||||
viewModel.handleRegistrationFailure(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun FragmentContent() {
|
||||
val state by viewModel.state
|
||||
val state by viewModel.state.collectAsStateWithLifecycle()
|
||||
val sharedState by sharedViewModel.state.collectAsStateWithLifecycle()
|
||||
|
||||
EnterBackupKeyScreen(
|
||||
backupKey = viewModel.backupKey,
|
||||
state = state,
|
||||
sharedState = sharedState,
|
||||
onBackupKeyChanged = viewModel::updateBackupKey,
|
||||
onNextClicked = {
|
||||
viewModel.registering()
|
||||
sharedViewModel.registerWithBackupKey(
|
||||
context = requireContext(),
|
||||
backupKey = state.backupKey,
|
||||
backupKey = viewModel.backupKey,
|
||||
e164 = null,
|
||||
pin = null
|
||||
)
|
||||
},
|
||||
onRegistrationErrorDismiss = viewModel::clearRegistrationError,
|
||||
onBackupKeyHelp = { CommunicationActions.openBrowserLink(requireContext(), LEARN_MORE_URL) },
|
||||
onLearnMore = { CommunicationActions.openBrowserLink(requireContext(), LEARN_MORE_URL) },
|
||||
onSkip = { findNavController().safeNavigate(EnterBackupKeyFragmentDirections.goToEnterPhoneNumber(EnterPhoneNumberMode.RESTART_AFTER_COLLECTION)) }
|
||||
)
|
||||
@@ -104,9 +137,12 @@ class EnterBackupKeyFragment : ComposeFragment() {
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun EnterBackupKeyScreen(
|
||||
backupKey: String,
|
||||
state: EnterBackupKeyState,
|
||||
sharedState: RegistrationState,
|
||||
onBackupKeyChanged: (String) -> Unit = {},
|
||||
onRegistrationErrorDismiss: () -> Unit = {},
|
||||
onBackupKeyHelp: () -> Unit = {},
|
||||
onNextClicked: () -> Unit = {},
|
||||
onLearnMore: () -> Unit = {},
|
||||
onSkip: () -> Unit = {}
|
||||
@@ -137,26 +173,38 @@ private fun EnterBackupKeyScreen(
|
||||
)
|
||||
}
|
||||
|
||||
Buttons.LargeTonal(
|
||||
enabled = state.backupKeyValid && !sharedState.inProgress,
|
||||
onClick = onNextClicked
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(id = R.string.RegistrationActivity_next)
|
||||
)
|
||||
AnimatedContent(
|
||||
targetState = state.isRegistering,
|
||||
label = "next-progress"
|
||||
) { isRegistering ->
|
||||
if (isRegistering) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.size(40.dp)
|
||||
)
|
||||
} else {
|
||||
Buttons.LargeTonal(
|
||||
enabled = state.backupKeyValid && state.aepValidationError == null,
|
||||
onClick = onNextClicked
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(id = R.string.RegistrationActivity_next)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
) {
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
val visualTransform = remember(state.length, state.chunkLength) { BackupKeyVisualTransformation(length = state.length, chunkSize = state.chunkLength) }
|
||||
val visualTransform = remember(state.chunkLength) { BackupKeyVisualTransformation(chunkSize = state.chunkLength) }
|
||||
val keyboardController = LocalSoftwareKeyboardController.current
|
||||
|
||||
TextField(
|
||||
value = state.backupKey,
|
||||
value = backupKey,
|
||||
onValueChange = onBackupKeyChanged,
|
||||
label = {
|
||||
Text(text = stringResource(id = R.string.EnterBackupKey_backup_key))
|
||||
},
|
||||
onValueChange = onBackupKeyChanged,
|
||||
textStyle = LocalTextStyle.current.copy(
|
||||
fontFamily = FontFamily(typeface = Typeface.MONOSPACE),
|
||||
lineHeight = 36.sp
|
||||
@@ -168,8 +216,15 @@ private fun EnterBackupKeyScreen(
|
||||
autoCorrectEnabled = false
|
||||
),
|
||||
keyboardActions = KeyboardActions(
|
||||
onNext = { if (state.backupKeyValid) onNextClicked() }
|
||||
onNext = {
|
||||
if (state.backupKeyValid) {
|
||||
keyboardController?.hide()
|
||||
onNextClicked()
|
||||
}
|
||||
}
|
||||
),
|
||||
supportingText = { state.aepValidationError?.ValidationErrorMessage() },
|
||||
isError = state.aepValidationError != null,
|
||||
minLines = 4,
|
||||
visualTransformation = visualTransform,
|
||||
modifier = Modifier
|
||||
@@ -201,6 +256,40 @@ private fun EnterBackupKeyScreen(
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (state.showRegistrationError) {
|
||||
if (state.registerAccountResult is RegisterAccountResult.IncorrectRecoveryPassword) {
|
||||
Dialogs.SimpleAlertDialog(
|
||||
title = stringResource(R.string.EnterBackupKey_incorrect_backup_key_title),
|
||||
body = stringResource(R.string.EnterBackupKey_incorrect_backup_key_message),
|
||||
confirm = stringResource(R.string.EnterBackupKey_try_again),
|
||||
dismiss = stringResource(R.string.EnterBackupKey_backup_key_help),
|
||||
onConfirm = {},
|
||||
onDeny = onBackupKeyHelp,
|
||||
onDismiss = onRegistrationErrorDismiss
|
||||
)
|
||||
} else {
|
||||
val message = when (state.registerAccountResult) {
|
||||
is RegisterAccountResult.RateLimited -> stringResource(R.string.RegistrationActivity_you_have_made_too_many_attempts_please_try_again_later)
|
||||
else -> stringResource(R.string.RegistrationActivity_error_connecting_to_service)
|
||||
}
|
||||
|
||||
Dialogs.SimpleMessageDialog(
|
||||
message = message,
|
||||
onDismiss = onRegistrationErrorDismiss,
|
||||
dismiss = stringResource(android.R.string.ok)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun EnterBackupKeyViewModel.AEPValidationError.ValidationErrorMessage() {
|
||||
when (this) {
|
||||
is EnterBackupKeyViewModel.AEPValidationError.TooLong -> Text(text = stringResource(R.string.EnterBackupKey_too_long_error, this.count, this.max))
|
||||
EnterBackupKeyViewModel.AEPValidationError.Invalid -> Text(text = stringResource(R.string.EnterBackupKey_invalid_backup_key_error))
|
||||
EnterBackupKeyViewModel.AEPValidationError.Incorrect -> Text(text = stringResource(R.string.EnterBackupKey_incorrect_backup_key_error))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -209,7 +298,20 @@ private fun EnterBackupKeyScreen(
|
||||
private fun EnterBackupKeyScreenPreview() {
|
||||
Previews.Preview {
|
||||
EnterBackupKeyScreen(
|
||||
state = EnterBackupKeyState(backupKey = "UY38jh2778hjjhj8lk19ga61s672jsj089r023s6a57809bap92j2yh5t326vv7t", length = 64, chunkLength = 4),
|
||||
backupKey = "UY38jh2778hjjhj8lk19ga61s672jsj089r023s6a57809bap92j2yh5t326vv7t",
|
||||
state = EnterBackupKeyState(requiredLength = 64, chunkLength = 4),
|
||||
sharedState = RegistrationState(phoneNumber = null, recoveryPassword = null)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@SignalPreview
|
||||
@Composable
|
||||
private fun EnterBackupKeyScreenErrorPreview() {
|
||||
Previews.Preview {
|
||||
EnterBackupKeyScreen(
|
||||
backupKey = "UY38jh2778hjjhj8lk19ga61s672jsj089r023s6a57809bap92j2yh5t326vv7t",
|
||||
state = EnterBackupKeyState(requiredLength = 64, chunkLength = 4, aepValidationError = EnterBackupKeyViewModel.AEPValidationError.Invalid),
|
||||
sharedState = RegistrationState(phoneNumber = null, recoveryPassword = null)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -5,50 +5,125 @@
|
||||
|
||||
package org.thoughtcrime.securesms.registrationv3.ui.restore
|
||||
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.ViewModel
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.registration.data.network.RegisterAccountResult
|
||||
import org.whispersystems.signalservice.api.AccountEntropyPool
|
||||
|
||||
class EnterBackupKeyViewModel : ViewModel() {
|
||||
|
||||
companion object {
|
||||
private val INVALID_CHARACTERS = Regex("[^0-9a-zA-Z]")
|
||||
private val TAG = Log.tag(EnterBackupKeyViewModel::class)
|
||||
}
|
||||
|
||||
private val _state = mutableStateOf(
|
||||
private val store = MutableStateFlow(
|
||||
EnterBackupKeyState(
|
||||
backupKey = "",
|
||||
length = 64,
|
||||
requiredLength = 64,
|
||||
chunkLength = 4
|
||||
)
|
||||
)
|
||||
|
||||
val state: State<EnterBackupKeyState> = _state
|
||||
var backupKey by mutableStateOf("")
|
||||
private set
|
||||
|
||||
val state: StateFlow<EnterBackupKeyState> = store
|
||||
|
||||
fun updateBackupKey(key: String) {
|
||||
_state.update {
|
||||
val newKey = key.removeIllegalCharacters().take(length).lowercase()
|
||||
copy(backupKey = newKey, backupKeyValid = validate(length, newKey))
|
||||
val newKey = AccountEntropyPool.removeIllegalCharacters(key).lowercase()
|
||||
val changed = newKey != backupKey
|
||||
backupKey = newKey
|
||||
store.update {
|
||||
val isValid = validateContents(backupKey)
|
||||
val isShort = backupKey.length < it.requiredLength
|
||||
val isExact = backupKey.length == it.requiredLength
|
||||
|
||||
var updatedError: AEPValidationError? = checkErrorStillApplies(it.aepValidationError, isShort || isExact, isValid, changed)
|
||||
if (updatedError == null) {
|
||||
updatedError = checkForNewError(isShort, isExact, isValid, it.requiredLength)
|
||||
}
|
||||
|
||||
it.copy(backupKeyValid = isValid, aepValidationError = updatedError)
|
||||
}
|
||||
}
|
||||
|
||||
private fun validate(length: Int, backupKey: String): Boolean {
|
||||
return backupKey.length == length
|
||||
private fun validateContents(backupKey: String): Boolean {
|
||||
return AccountEntropyPool.isFullyValid(backupKey)
|
||||
}
|
||||
|
||||
private fun String.removeIllegalCharacters(): String {
|
||||
return this.replace(INVALID_CHARACTERS, "")
|
||||
private fun checkErrorStillApplies(error: AEPValidationError?, isShortOrExact: Boolean, isValid: Boolean, isChanged: Boolean): AEPValidationError? {
|
||||
return when (error) {
|
||||
is AEPValidationError.TooLong -> if (isShortOrExact) null else error.copy(count = backupKey.length)
|
||||
AEPValidationError.Invalid -> if (isValid) null else error
|
||||
AEPValidationError.Incorrect -> if (isChanged) null else error
|
||||
null -> null
|
||||
}
|
||||
}
|
||||
|
||||
private inline fun <T> MutableState<T>.update(update: T.() -> T) {
|
||||
this.value = this.value.update()
|
||||
private fun checkForNewError(isShort: Boolean, isExact: Boolean, isValid: Boolean, requiredLength: Int): AEPValidationError? {
|
||||
return if (!isShort && !isExact) {
|
||||
AEPValidationError.TooLong(backupKey.length, requiredLength)
|
||||
} else if (!isValid && isExact) {
|
||||
AEPValidationError.Invalid
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun registering() {
|
||||
store.update { it.copy(isRegistering = true) }
|
||||
}
|
||||
|
||||
fun handleRegistrationFailure(registerAccountResult: RegisterAccountResult) {
|
||||
store.update {
|
||||
if (it.isRegistering) {
|
||||
Log.w(TAG, "Unable to register [${registerAccountResult::class.simpleName}]", registerAccountResult.getCause())
|
||||
val incorrectKeyError = registerAccountResult is RegisterAccountResult.IncorrectRecoveryPassword
|
||||
|
||||
if (incorrectKeyError && SignalStore.account.restoredAccountEntropyPool) {
|
||||
SignalStore.account.resetAccountEntropyPool()
|
||||
}
|
||||
|
||||
it.copy(
|
||||
isRegistering = false,
|
||||
showRegistrationError = true,
|
||||
registerAccountResult = registerAccountResult,
|
||||
aepValidationError = if (incorrectKeyError) AEPValidationError.Incorrect else it.aepValidationError
|
||||
)
|
||||
} else {
|
||||
it
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun clearRegistrationError() {
|
||||
store.update {
|
||||
it.copy(
|
||||
showRegistrationError = false,
|
||||
registerAccountResult = null
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
data class EnterBackupKeyState(
|
||||
val backupKey: String = "",
|
||||
val backupKeyValid: Boolean = false,
|
||||
val length: Int,
|
||||
val chunkLength: Int
|
||||
val requiredLength: Int,
|
||||
val chunkLength: Int,
|
||||
val isRegistering: Boolean = false,
|
||||
val showRegistrationError: Boolean = false,
|
||||
val registerAccountResult: RegisterAccountResult? = null,
|
||||
val aepValidationError: AEPValidationError? = null
|
||||
)
|
||||
|
||||
sealed interface AEPValidationError {
|
||||
data class TooLong(val count: Int, val max: Int) : AEPValidationError
|
||||
data object Invalid : AEPValidationError
|
||||
data object Incorrect : AEPValidationError
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,6 +104,7 @@ class WelcomeFragment : LoggingFragment(R.layout.fragment_registration_welcome_v
|
||||
}
|
||||
|
||||
private fun navigateToNextScreenViaRestore(userSelection: WelcomeUserSelection) {
|
||||
sharedViewModel.maybePrefillE164(requireContext())
|
||||
sharedViewModel.setRegistrationCheckpoint(RegistrationCheckpoint.PERMISSIONS_GRANTED)
|
||||
|
||||
when (userSelection) {
|
||||
|
||||
@@ -8063,6 +8063,20 @@
|
||||
<string name="EnterBackupKey_learn_more">Learn more</string>
|
||||
<!-- Backup key not known bottom sheet button to skip backup key entry and resume normal reregistration -->
|
||||
<string name="EnterBackupKey_skip_and_dont_restore">Skip and don\'t restore</string>
|
||||
<!-- Dialog title shown when entered AEP doesn't work to register with provided e164 -->
|
||||
<string name="EnterBackupKey_incorrect_backup_key_title">Incorrect backup key</string>
|
||||
<!-- Dialog message shown when entered AEP doesn't work to register with provided e164 -->
|
||||
<string name="EnterBackupKey_incorrect_backup_key_message">Make sure you\'re registering with the same phone number and 64-character backup key you saved when enabling Signal backups. Backups can not be recovered without this key.</string>
|
||||
<!-- Dialog positive button text to try entering their backup key again -->
|
||||
<string name="EnterBackupKey_try_again">Try again</string>
|
||||
<!-- Dialog negative button text to get help with backup key -->
|
||||
<string name="EnterBackupKey_backup_key_help">Backup key help</string>
|
||||
<!-- Text field error text when the backup key provided has been confirmed invalid -->
|
||||
<string name="EnterBackupKey_incorrect_backup_key_error">Incorrect backup key</string>
|
||||
<!-- Text field error text when the backup key is too long -->
|
||||
<string name="EnterBackupKey_too_long_error">Too long (%1$d/%2$d)</string>
|
||||
<!-- Text field error when the backup key is invalid -->
|
||||
<string name="EnterBackupKey_invalid_backup_key_error">Invalid backup key</string>
|
||||
|
||||
<!-- Title for restore via qr screen -->
|
||||
<string name="RestoreViaQr_title">Scan this code with your old phone</string>
|
||||
|
||||
@@ -30,6 +30,14 @@ class AccountEntropyPool(val value: String) {
|
||||
|
||||
return AccountEntropyPool(stripped)
|
||||
}
|
||||
|
||||
fun isFullyValid(input: String): Boolean {
|
||||
return LibSignalAccountEntropyPool.isValid(input)
|
||||
}
|
||||
|
||||
fun removeIllegalCharacters(input: String): String {
|
||||
return input.replace(INVALID_CHARACTERS, "")
|
||||
}
|
||||
}
|
||||
|
||||
fun deriveMasterKey(): MasterKey {
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
package org.whispersystems.signalservice.api.kbs;
|
||||
|
||||
import org.signal.libsignal.protocol.kdf.HKDF;
|
||||
import org.whispersystems.signalservice.api.backup.MessageBackupKey;
|
||||
import org.signal.core.util.Base64;
|
||||
import org.whispersystems.signalservice.api.storage.StorageKey;
|
||||
import org.whispersystems.signalservice.internal.util.Hex;
|
||||
import org.signal.core.util.Base64;
|
||||
import org.whispersystems.util.StringUtil;
|
||||
|
||||
import java.security.SecureRandom;
|
||||
@@ -46,11 +44,6 @@ public final class MasterKey {
|
||||
return derive("Logging Key");
|
||||
}
|
||||
|
||||
public MessageBackupKey deriveMessageBackupKey() {
|
||||
// TODO [backup] Derive from AEP
|
||||
return new MessageBackupKey(HKDF.deriveSecrets(masterKey, "20231003_Signal_Backups_GenerateBackupKey".getBytes(), 32));
|
||||
}
|
||||
|
||||
private byte[] derive(String keyName) {
|
||||
return hmacSha256(masterKey, StringUtil.utf8(keyName));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user