diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/transfer/details/IBANVisualTransformation.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/transfer/details/IBANVisualTransformation.kt index 12bd098d49..b28c9a3213 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/transfer/details/IBANVisualTransformation.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/transfer/details/IBANVisualTransformation.kt @@ -37,7 +37,7 @@ object IBANVisualTransformation : VisualTransformation { } override fun transformedToOriginal(offset: Int): Int { - return offset - (offset / 4) + return offset - (offset / 5) } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/AccountValues.kt b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/AccountValues.kt index eab99a490e..7e5911be53 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/AccountValues.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/AccountValues.kt @@ -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)) diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/SvrValues.kt b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/SvrValues.kt index 2f9691211d..d00f6e51c8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/SvrValues.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/SvrValues.kt @@ -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() } diff --git a/app/src/main/java/org/thoughtcrime/securesms/logsubmit/LogSectionPin.java b/app/src/main/java/org/thoughtcrime/securesms/logsubmit/LogSectionPin.java index 10e500ce4a..c2393acbee 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/logsubmit/LogSectionPin.java +++ b/app/src/main/java/org/thoughtcrime/securesms/logsubmit/LogSectionPin.java @@ -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") diff --git a/app/src/main/java/org/thoughtcrime/securesms/registrationv3/ui/restore/BackupKeyVisualTransformation.kt b/app/src/main/java/org/thoughtcrime/securesms/registrationv3/ui/restore/BackupKeyVisualTransformation.kt index 4f1f225310..1c1c498626 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registrationv3/ui/restore/BackupKeyVisualTransformation.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registrationv3/ui/restore/BackupKeyVisualTransformation.kt @@ -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)) } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/registrationv3/ui/restore/EnterBackupKeyFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/registrationv3/ui/restore/EnterBackupKeyFragment.kt index b1650ec064..f108c62707 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registrationv3/ui/restore/EnterBackupKeyFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registrationv3/ui/restore/EnterBackupKeyFragment.kt @@ -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() private val viewModel by viewModels() + 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) ) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/registrationv3/ui/restore/EnterBackupKeyViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/registrationv3/ui/restore/EnterBackupKeyViewModel.kt index 58ecdc331a..235f0d026a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registrationv3/ui/restore/EnterBackupKeyViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registrationv3/ui/restore/EnterBackupKeyViewModel.kt @@ -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 = _state + var backupKey by mutableStateOf("") + private set + + val state: StateFlow = 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 MutableState.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 + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/registrationv3/ui/welcome/WelcomeFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/registrationv3/ui/welcome/WelcomeFragment.kt index d3b8ee79a9..ca657fac80 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registrationv3/ui/welcome/WelcomeFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registrationv3/ui/welcome/WelcomeFragment.kt @@ -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) { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 9af9501e46..382183863e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -8063,6 +8063,20 @@ Learn more Skip and don\'t restore + + Incorrect backup key + + 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. + + Try again + + Backup key help + + Incorrect backup key + + Too long (%1$d/%2$d) + + Invalid backup key Scan this code with your old phone diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/AccountEntropyPool.kt b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/AccountEntropyPool.kt index 36b4959098..9c2aea78ae 100644 --- a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/AccountEntropyPool.kt +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/AccountEntropyPool.kt @@ -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 { diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/kbs/MasterKey.java b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/kbs/MasterKey.java index e3ab7ddaf7..09c1d97f1f 100644 --- a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/kbs/MasterKey.java +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/kbs/MasterKey.java @@ -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)); }