Prevent SVRB falling out of sync after re-registrations.

This commit is contained in:
Cody Henthorne
2025-11-03 10:44:14 -05:00
committed by Michelle Tang
parent 10d6e5293b
commit d6156ab3f2
9 changed files with 115 additions and 0 deletions

View File

@@ -164,6 +164,7 @@ import org.whispersystems.signalservice.internal.crypto.PaddingInputStream
import org.whispersystems.signalservice.internal.push.AttachmentUploadForm
import org.whispersystems.signalservice.internal.push.AuthCredentials
import org.whispersystems.signalservice.internal.push.SubscriptionsConfiguration
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import java.io.File
import java.io.IOException
@@ -2394,6 +2395,26 @@ object BackupRepository {
).encodeByteString()
}
fun getRemoteBackupForwardSecrecyMetadata(): NetworkResult<ByteArray?> {
return initBackupAndFetchAuth()
.then { credential -> SignalNetwork.archive.getBackupInfo(SignalStore.account.requireAci(), credential.messageBackupAccess) }
.then { info -> getCdnReadCredentials(CredentialType.MESSAGE, info.cdn ?: Cdn.CDN_3.cdnNumber).map { it.headers to info } }
.then { pair ->
val (cdnCredentials, info) = pair
val headers = cdnCredentials.toMutableMap().apply {
this["range"] = "bytes=0-${EncryptedBackupReader.BACKUP_SECRET_METADATA_UPPERBOUND - 1}"
}
AppDependencies.signalServiceMessageReceiver.retrieveBackupForwardSecretMetadataBytes(
info.cdn!!,
headers,
"backups/${info.backupDir}/${info.backupName}",
EncryptedBackupReader.BACKUP_SECRET_METADATA_UPPERBOUND
)
}
.map { bytes -> EncryptedBackupReader.readForwardSecrecyMetadata(ByteArrayInputStream(bytes)) }
}
interface ExportProgressListener {
fun onAccount()
fun onRecipient()

View File

@@ -49,6 +49,13 @@ class EncryptedBackupReader private constructor(
companion object {
const val MAC_SIZE = 32
/**
* Estimated upperbound need to read backup secrecy metadata from the start of a file.
*
* Magic Number size + ~varint size (5) + forward secrecy metadata size estimate (200)
*/
val BACKUP_SECRET_METADATA_UPPERBOUND = EncryptedBackupWriter.MAGIC_NUMBER.size + 5 + 200
/**
* Create a reader for a backup from the archive CDN.
* The key difference is that we require forward secrecy data.

View File

@@ -20,6 +20,7 @@ import org.signal.core.util.logging.Log
import org.signal.core.util.logging.logW
import org.signal.libsignal.messagebackup.BackupForwardSecrecyToken
import org.signal.libsignal.net.SvrBStoreResponse
import org.signal.libsignal.zkgroup.VerificationFailedException
import org.signal.protos.resumableuploads.ResumableUpload
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.backup.ArchiveUploadProgress
@@ -165,6 +166,62 @@ class BackupMessagesJob private constructor(
is NetworkResult.ApplicationError -> throw result.throwable
}
if (SignalStore.backup.backupSecretRestoreRequired) {
Log.i(TAG, "[svrb-restore] First backup of re-registered account without remote restore, read remote data if available to re-init")
val forwardSecrecyMetadata: ByteArray? = when (val result = BackupRepository.getRemoteBackupForwardSecrecyMetadata()) {
is NetworkResult.Success -> result.result
is NetworkResult.NetworkError -> return Result.retry(defaultBackoff()).logW(TAG, "[svrb-restore] Network error getting remote forward secrecy metadata.", result.getCause(), true)
is NetworkResult.StatusCodeError -> {
if (result.code == 401 || result.code == 403 || result.code == 404) {
Log.i(TAG, "[svrb-restore] No backup data found, continuing.", true)
null
} else {
return Result.retry(defaultBackoff()).logW(TAG, "[svrb-restore] Status code error when getting remote forward secrecy metadata.", result.getCause(), true)
}
}
is NetworkResult.ApplicationError -> {
if (result.getCause() is VerificationFailedException) {
Log.w(TAG, "[svrb-restore] zkverification failed getting backup info, continuing.", true)
null
} else {
throw result.throwable
}
}
}
if (forwardSecrecyMetadata != null) {
when (val result = SignalNetwork.svrB.restore(auth, SignalStore.backup.messageBackupKey, forwardSecrecyMetadata)) {
is SvrBApi.RestoreResult.Success -> {
Log.i(TAG, "[svrb-restore] Remote secrecy data restored successfully.")
SignalStore.backup.nextBackupSecretData = result.data.nextBackupSecretData
}
is SvrBApi.RestoreResult.NetworkError -> {
Log.w(TAG, "[svrb-restore] Network error during SVRB.", result.exception)
return Result.retry(defaultBackoff())
}
is SvrBApi.RestoreResult.RestoreFailedError,
SvrBApi.RestoreResult.InvalidDataError -> {
Log.i(TAG, "[svrb-restore] Permanent SVRB error! Continuing $result")
}
SvrBApi.RestoreResult.DataMissingError,
is SvrBApi.RestoreResult.SvrError -> {
Log.i(TAG, "[svrb-restore] Failed to fetch SVRB data, continuing: $result")
}
is SvrBApi.RestoreResult.UnknownError -> {
Log.e(TAG, "[svrb-restore] Unknown SVRB result! Crashing.", result.throwable)
return Result.fatalFailure(RuntimeException(result.throwable))
}
}
}
SignalStore.backup.backupSecretRestoreRequired = false
}
val backupSecretData = SignalStore.backup.nextBackupSecretData ?: run {
Log.i(TAG, "First SVRB backup! Creating new backup chain.", true)
val secretData = SignalNetwork.svrB.createNewBackupChain(auth, SignalStore.backup.messageBackupKey)

View File

@@ -92,6 +92,7 @@ class BackupValues(store: KeyValueStore) : SignalStoreValues(store) {
private const val KEY_HAS_VERIFIED_BEFORE = "backup.has_verified_before"
private const val KEY_NEXT_BACKUP_SECRET_DATA = "backup.next_backup_secret_data"
private const val KEY_BACKUP_SECRET_RESTORE_REQUIRED = "backup.backup_secret_restore_required"
private const val KEY_RESTORING_VIA_QR = "backup.restore_via_qr"
@@ -393,6 +394,9 @@ class BackupValues(store: KeyValueStore) : SignalStoreValues(store) {
/** The value from the last successful SVRB operation that must be passed to the next SVRB operation. */
var nextBackupSecretData by nullableBlobValue(KEY_NEXT_BACKUP_SECRET_DATA, null)
/** True if user re-registered but did not restore SVRB secrets during registration, and should on backup. */
var backupSecretRestoreRequired by booleanValue(KEY_BACKUP_SECRET_RESTORE_REQUIRED, false)
/** True if attempting to restore backup from quick restore/QR code */
var restoringViaQr by booleanValue(KEY_RESTORING_VIA_QR, false)

View File

@@ -914,6 +914,10 @@ class RegistrationViewModel : ViewModel() {
SignalStore.registration.restoreDecisionState = RestoreDecisionState.NewAccount
}
if (remoteResult.reRegistration) {
SignalStore.backup.backupSecretRestoreRequired = true
}
if (reglockEnabled || SignalStore.account.restoredAccountEntropyPool) {
SignalStore.onboarding.clearAll()

View File

@@ -122,6 +122,7 @@ class RemoteRestoreViewModel(isOnlyRestoreOption: Boolean) : ViewModel() {
Log.i(TAG, "Restore successful", true)
SignalStore.registration.restoreDecisionState = RestoreDecisionState.Completed
SignalStore.backup.backupSecretRestoreRequired = false
StorageServiceRestore.restore()
store.update { it.copy(importState = ImportState.Restored) }