From 14dbaa7d0568bd772f85592dfd909829f6942f44 Mon Sep 17 00:00:00 2001 From: Cody Henthorne Date: Tue, 20 May 2025 15:14:29 -0400 Subject: [PATCH] Prevent safety number changes during quick restore flow. --- .../securesms/keyvalue/AccountValues.kt | 16 +++++++ .../data/QuickRegistrationRepository.kt | 7 +++- .../ui/RegistrationViewModel.kt | 10 ++++- .../ui/restore/EnterBackupKeyFragment.kt | 4 +- .../ui/restore/EnterBackupKeyViewModel.kt | 1 + .../RegistrationProvisionMessageExt.kt | 42 +++++++++++++++++++ .../ui/restore/RestoreViaQrFragment.kt | 2 +- .../protowire/RegistrationProvisioning.proto | 4 ++ 8 files changed, 82 insertions(+), 4 deletions(-) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/registrationv3/ui/restore/RegistrationProvisionMessageExt.kt 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 33517d816d..78425638f3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/AccountValues.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/AccountValues.kt @@ -337,6 +337,22 @@ class AccountValues internal constructor(store: KeyValueStore, context: Context) } } + /** + * Only to be used as part of Quick Restore, DO NOT USE OTHERWISE. + */ + fun resetAciAndPniIdentityKeysAfterFailedRestore() { + synchronized(this) { + Log.i(TAG, "Resetting ACI and PNI identity keys after failed quick registration and restore") + + store.beginWrite() + .remove(KEY_ACI_IDENTITY_PUBLIC_KEY) + .remove(KEY_ACI_IDENTITY_PRIVATE_KEY) + .remove(KEY_PNI_IDENTITY_PUBLIC_KEY) + .remove(KEY_PNI_IDENTITY_PRIVATE_KEY) + .commit() + } + } + /** Only to be used when restoring an identity public key from an old backup */ fun restoreLegacyIdentityPublicKeyFromBackup(base64: String) { Log.w(TAG, "Restoring legacy identity public key from backup.") diff --git a/app/src/main/java/org/thoughtcrime/securesms/registrationv3/data/QuickRegistrationRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/registrationv3/data/QuickRegistrationRepository.kt index 9f9a03eb2a..f2bfdcc3ca 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registrationv3/data/QuickRegistrationRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registrationv3/data/QuickRegistrationRepository.kt @@ -10,6 +10,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.isActive import kotlinx.coroutines.withContext +import okio.ByteString.Companion.toByteString import org.signal.core.util.Base64.decode import org.signal.core.util.isNotNullOrBlank import org.signal.core.util.logging.Log @@ -85,7 +86,11 @@ object QuickRegistrationRepository { null -> null }, backupSizeBytes = SignalStore.backup.totalBackupSize.takeIf { it > 0 }, - restoreMethodToken = restoreMethodToken + restoreMethodToken = restoreMethodToken, + aciIdentityKeyPublic = SignalStore.account.aciIdentityKey.publicKey.serialize().toByteString(), + aciIdentityKeyPrivate = SignalStore.account.aciIdentityKey.privateKey.serialize().toByteString(), + pniIdentityKeyPublic = SignalStore.account.pniIdentityKey.publicKey.serialize().toByteString(), + pniIdentityKeyPrivate = SignalStore.account.pniIdentityKey.privateKey.serialize().toByteString() ) ) .successOrThrow() diff --git a/app/src/main/java/org/thoughtcrime/securesms/registrationv3/ui/RegistrationViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/registrationv3/ui/RegistrationViewModel.kt index 591770bd03..fef3efb300 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registrationv3/ui/RegistrationViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registrationv3/ui/RegistrationViewModel.kt @@ -25,6 +25,7 @@ import kotlinx.coroutines.withContext import org.signal.core.util.Base64 import org.signal.core.util.isNotNullOrBlank import org.signal.core.util.logging.Log +import org.signal.libsignal.protocol.IdentityKeyPair import org.thoughtcrime.securesms.backup.v2.BackupRepository import org.thoughtcrime.securesms.database.model.databaseprotos.RestoreDecisionState import org.thoughtcrime.securesms.dependencies.AppDependencies @@ -1029,7 +1030,7 @@ class RegistrationViewModel : ViewModel() { setInProgress(false) } - fun registerWithBackupKey(context: Context, backupKey: String, e164: String?, pin: String?) { + fun registerWithBackupKey(context: Context, backupKey: String, e164: String?, pin: String?, aciIdentityKeyPair: IdentityKeyPair?, pniIdentityKeyPair: IdentityKeyPair?) { setInProgress(true) viewModelScope.launch(context = coroutineExceptionHandler) { @@ -1040,6 +1041,13 @@ class RegistrationViewModel : ViewModel() { val accountEntropyPool = AccountEntropyPool(backupKey) SignalStore.account.restoreAccountEntropyPool(accountEntropyPool) + if (aciIdentityKeyPair != null) { + SignalStore.account.restoreAciIdentityKeyFromBackup(aciIdentityKeyPair.publicKey.serialize(), aciIdentityKeyPair.privateKey.serialize()) + if (pniIdentityKeyPair != null) { + SignalStore.account.restorePniIdentityKeyFromBackup(pniIdentityKeyPair.publicKey.serialize(), pniIdentityKeyPair.privateKey.serialize()) + } + } + val masterKey = accountEntropyPool.deriveMasterKey() setRecoveryPassword(masterKey.deriveRegistrationRecoveryPassword()) verifyReRegisterInternal(context = context, pin = pin, masterKey = masterKey) 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 ccaadf9fbb..3d8ec1a344 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 @@ -102,7 +102,9 @@ class EnterBackupKeyFragment : ComposeFragment() { context = requireContext(), backupKey = viewModel.backupKey, e164 = null, - pin = null + pin = null, + aciIdentityKeyPair = null, + pniIdentityKeyPair = null ) }, onLearnMore = { CommunicationActions.openBrowserLink(requireContext(), LEARN_MORE_URL) }, 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 6d1a72d2a1..8cd2d5ed2c 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 @@ -62,6 +62,7 @@ class EnterBackupKeyViewModel : ViewModel() { if (incorrectKeyError && SignalStore.account.restoredAccountEntropyPool) { SignalStore.account.resetAccountEntropyPool() + SignalStore.account.resetAciAndPniIdentityKeysAfterFailedRestore() } it.copy( diff --git a/app/src/main/java/org/thoughtcrime/securesms/registrationv3/ui/restore/RegistrationProvisionMessageExt.kt b/app/src/main/java/org/thoughtcrime/securesms/registrationv3/ui/restore/RegistrationProvisionMessageExt.kt new file mode 100644 index 0000000000..e4902944cb --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/registrationv3/ui/restore/RegistrationProvisionMessageExt.kt @@ -0,0 +1,42 @@ +/* + * Copyright 2025 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.registrationv3.ui.restore + +import org.signal.libsignal.protocol.IdentityKey +import org.signal.libsignal.protocol.IdentityKeyPair +import org.signal.libsignal.protocol.ecc.ECPrivateKey +import org.signal.registration.proto.RegistrationProvisionMessage +import java.security.InvalidKeyException + +/** + * Attempt to parse the ACI identity key pair from the proto message parts. + */ +val RegistrationProvisionMessage.aciIdentityKeyPair: IdentityKeyPair? + get() { + return try { + IdentityKeyPair( + IdentityKey(aciIdentityKeyPublic.toByteArray()), + ECPrivateKey(aciIdentityKeyPrivate.toByteArray()) + ) + } catch (_: InvalidKeyException) { + null + } + } + +/** + * Attempt to parse the PNI identity key pair from the proto message parts. + */ +val RegistrationProvisionMessage.pniIdentityKeyPair: IdentityKeyPair? + get() { + return try { + IdentityKeyPair( + IdentityKey(pniIdentityKeyPublic.toByteArray()), + ECPrivateKey(pniIdentityKeyPrivate.toByteArray()) + ) + } catch (_: InvalidKeyException) { + null + } + } diff --git a/app/src/main/java/org/thoughtcrime/securesms/registrationv3/ui/restore/RestoreViaQrFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/registrationv3/ui/restore/RestoreViaQrFragment.kt index 23bc1bfbe4..9d9438a039 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registrationv3/ui/restore/RestoreViaQrFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registrationv3/ui/restore/RestoreViaQrFragment.kt @@ -89,7 +89,7 @@ class RestoreViaQrFragment : ComposeFragment() { .distinctUntilChanged() .collect { message -> if (message.platform == RegistrationProvisionMessage.Platform.ANDROID || message.tier != null) { - sharedViewModel.registerWithBackupKey(requireContext(), message.accountEntropyPool, message.e164, message.pin) + sharedViewModel.registerWithBackupKey(requireContext(), message.accountEntropyPool, message.e164, message.pin, message.aciIdentityKeyPair, message.pniIdentityKeyPair) } else { findNavController().safeNavigate(RestoreViaQrFragmentDirections.goToNoBackupToRestore()) } diff --git a/libsignal-service/src/main/protowire/RegistrationProvisioning.proto b/libsignal-service/src/main/protowire/RegistrationProvisioning.proto index 1a8fd76095..fac376e606 100644 --- a/libsignal-service/src/main/protowire/RegistrationProvisioning.proto +++ b/libsignal-service/src/main/protowire/RegistrationProvisioning.proto @@ -28,4 +28,8 @@ message RegistrationProvisionMessage { optional Tier tier = 7; optional uint64 backupSizeBytes = 8; string restoreMethodToken = 9; + bytes aciIdentityKeyPublic = 10; + bytes aciIdentityKeyPrivate = 11; + bytes pniIdentityKeyPublic = 12; + bytes pniIdentityKeyPrivate = 13; }