Prevent safety number changes during quick restore flow.

This commit is contained in:
Cody Henthorne
2025-05-20 15:14:29 -04:00
committed by GitHub
parent 58e462de06
commit 14dbaa7d05
8 changed files with 82 additions and 4 deletions

View File

@@ -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 */ /** Only to be used when restoring an identity public key from an old backup */
fun restoreLegacyIdentityPublicKeyFromBackup(base64: String) { fun restoreLegacyIdentityPublicKeyFromBackup(base64: String) {
Log.w(TAG, "Restoring legacy identity public key from backup.") Log.w(TAG, "Restoring legacy identity public key from backup.")

View File

@@ -10,6 +10,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive import kotlinx.coroutines.isActive
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import okio.ByteString.Companion.toByteString
import org.signal.core.util.Base64.decode import org.signal.core.util.Base64.decode
import org.signal.core.util.isNotNullOrBlank import org.signal.core.util.isNotNullOrBlank
import org.signal.core.util.logging.Log import org.signal.core.util.logging.Log
@@ -85,7 +86,11 @@ object QuickRegistrationRepository {
null -> null null -> null
}, },
backupSizeBytes = SignalStore.backup.totalBackupSize.takeIf { it > 0 }, 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() .successOrThrow()

View File

@@ -25,6 +25,7 @@ import kotlinx.coroutines.withContext
import org.signal.core.util.Base64 import org.signal.core.util.Base64
import org.signal.core.util.isNotNullOrBlank import org.signal.core.util.isNotNullOrBlank
import org.signal.core.util.logging.Log import org.signal.core.util.logging.Log
import org.signal.libsignal.protocol.IdentityKeyPair
import org.thoughtcrime.securesms.backup.v2.BackupRepository import org.thoughtcrime.securesms.backup.v2.BackupRepository
import org.thoughtcrime.securesms.database.model.databaseprotos.RestoreDecisionState import org.thoughtcrime.securesms.database.model.databaseprotos.RestoreDecisionState
import org.thoughtcrime.securesms.dependencies.AppDependencies import org.thoughtcrime.securesms.dependencies.AppDependencies
@@ -1029,7 +1030,7 @@ class RegistrationViewModel : ViewModel() {
setInProgress(false) 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) setInProgress(true)
viewModelScope.launch(context = coroutineExceptionHandler) { viewModelScope.launch(context = coroutineExceptionHandler) {
@@ -1040,6 +1041,13 @@ class RegistrationViewModel : ViewModel() {
val accountEntropyPool = AccountEntropyPool(backupKey) val accountEntropyPool = AccountEntropyPool(backupKey)
SignalStore.account.restoreAccountEntropyPool(accountEntropyPool) 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() val masterKey = accountEntropyPool.deriveMasterKey()
setRecoveryPassword(masterKey.deriveRegistrationRecoveryPassword()) setRecoveryPassword(masterKey.deriveRegistrationRecoveryPassword())
verifyReRegisterInternal(context = context, pin = pin, masterKey = masterKey) verifyReRegisterInternal(context = context, pin = pin, masterKey = masterKey)

View File

@@ -102,7 +102,9 @@ class EnterBackupKeyFragment : ComposeFragment() {
context = requireContext(), context = requireContext(),
backupKey = viewModel.backupKey, backupKey = viewModel.backupKey,
e164 = null, e164 = null,
pin = null pin = null,
aciIdentityKeyPair = null,
pniIdentityKeyPair = null
) )
}, },
onLearnMore = { CommunicationActions.openBrowserLink(requireContext(), LEARN_MORE_URL) }, onLearnMore = { CommunicationActions.openBrowserLink(requireContext(), LEARN_MORE_URL) },

View File

@@ -62,6 +62,7 @@ class EnterBackupKeyViewModel : ViewModel() {
if (incorrectKeyError && SignalStore.account.restoredAccountEntropyPool) { if (incorrectKeyError && SignalStore.account.restoredAccountEntropyPool) {
SignalStore.account.resetAccountEntropyPool() SignalStore.account.resetAccountEntropyPool()
SignalStore.account.resetAciAndPniIdentityKeysAfterFailedRestore()
} }
it.copy( it.copy(

View File

@@ -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
}
}

View File

@@ -89,7 +89,7 @@ class RestoreViaQrFragment : ComposeFragment() {
.distinctUntilChanged() .distinctUntilChanged()
.collect { message -> .collect { message ->
if (message.platform == RegistrationProvisionMessage.Platform.ANDROID || message.tier != null) { 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 { } else {
findNavController().safeNavigate(RestoreViaQrFragmentDirections.goToNoBackupToRestore()) findNavController().safeNavigate(RestoreViaQrFragmentDirections.goToNoBackupToRestore())
} }

View File

@@ -28,4 +28,8 @@ message RegistrationProvisionMessage {
optional Tier tier = 7; optional Tier tier = 7;
optional uint64 backupSizeBytes = 8; optional uint64 backupSizeBytes = 8;
string restoreMethodToken = 9; string restoreMethodToken = 9;
bytes aciIdentityKeyPublic = 10;
bytes aciIdentityKeyPrivate = 11;
bytes pniIdentityKeyPublic = 12;
bytes pniIdentityKeyPrivate = 13;
} }