Add support for remote backup restore to regV5.

This commit is contained in:
Greyson Parrelli
2026-04-16 15:52:12 -04:00
parent 76e30ab09f
commit 82046dd55f
50 changed files with 1922 additions and 262 deletions

View File

@@ -14,6 +14,7 @@ import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.serialization.json.Json
import org.signal.core.models.AccountEntropyPool
import org.signal.core.models.MasterKey
import org.signal.core.util.logging.Log
import org.signal.libsignal.net.RequestResult
@@ -48,6 +49,7 @@ import org.signal.registration.proto.RegistrationProvisionMessage
import org.thoughtcrime.securesms.BuildConfig
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.gcm.FcmUtil
import org.thoughtcrime.securesms.jobs.RefreshAttributesJob
import org.thoughtcrime.securesms.jobs.ResetSvrGuessCountJob
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.net.SignalNetwork
@@ -538,7 +540,7 @@ class AppRegistrationNetworkController(
}
}
override suspend fun getRemoteBackupInfo(): RequestResult<NetworkController.GetBackupInfoResponse, NetworkController.GetBackupInfoError> = withContext(Dispatchers.IO) {
override suspend fun getRemoteBackupInfo(aep: AccountEntropyPool): RequestResult<NetworkController.GetBackupInfoResponse, NetworkController.GetBackupInfoError> = withContext(Dispatchers.IO) {
val aci = SignalStore.account.aci ?: return@withContext RequestResult.ApplicationError(IllegalStateException("ACI not available"))
val currentTime = System.currentTimeMillis()
@@ -589,6 +591,42 @@ class AppRegistrationNetworkController(
}
}
override suspend fun enqueueAccountAttributesSyncJob() {
AppDependencies.jobManager.add(RefreshAttributesJob())
}
override suspend fun getBackupFileLastModified(
aep: AccountEntropyPool,
backupInfo: NetworkController.GetBackupInfoResponse
): RequestResult<Long, NetworkController.GetBackupInfoError> = withContext(Dispatchers.IO) {
val aci = SignalStore.account.aci ?: return@withContext RequestResult.ApplicationError(IllegalStateException("ACI not available"))
val cdn = backupInfo.cdn ?: return@withContext RequestResult.ApplicationError(IllegalStateException("CDN number not available"))
val backupDir = backupInfo.backupDir ?: return@withContext RequestResult.ApplicationError(IllegalStateException("Backup dir not available"))
val backupName = backupInfo.backupName ?: return@withContext RequestResult.ApplicationError(IllegalStateException("Backup name not available"))
val currentTime = System.currentTimeMillis()
val messageCredential = SignalStore.backup.messageCredentials.byDay.getForCurrentTime(currentTime.milliseconds)
?: return@withContext RequestResult.ApplicationError(IllegalStateException("No message credential available"))
val access = ArchiveServiceAccess(messageCredential, SignalStore.backup.messageBackupKey)
val cdnCredentials = when (val cdnResult = SignalNetwork.archive.getCdnReadCredentials(cdn, aci, access)) {
is NetworkResult.Success -> cdnResult.result.headers
is NetworkResult.StatusCodeError -> return@withContext RequestResult.ApplicationError(IllegalStateException("Failed to get CDN credentials: ${cdnResult.code}"))
is NetworkResult.NetworkError -> return@withContext RequestResult.RetryableNetworkError(cdnResult.exception)
is NetworkResult.ApplicationError -> return@withContext RequestResult.ApplicationError(cdnResult.throwable)
}
try {
val lastModified = AppDependencies.signalServiceMessageReceiver.getCdnLastModifiedTime(cdn, cdnCredentials, "backups/$backupDir/$backupName")
RequestResult.Success(lastModified.toInstant().toEpochMilli())
} catch (e: IOException) {
RequestResult.RetryableNetworkError(e)
} catch (e: Exception) {
RequestResult.ApplicationError(e)
}
}
override fun startProvisioning(): Flow<ProvisioningEvent> = callbackFlow {
val socketHandles = mutableListOf<java.io.Closeable>()
val configuration = AppDependencies.signalServiceNetworkAccess.getConfiguration()

View File

@@ -9,11 +9,17 @@ import android.content.Context
import android.net.Uri
import androidx.documentfile.provider.DocumentFile
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import okio.ByteString.Companion.toByteString
import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
import org.signal.archive.LocalBackupRestoreProgress
import org.signal.core.models.AccountEntropyPool
import org.signal.core.models.MasterKey
@@ -22,8 +28,11 @@ import org.signal.registration.PreExistingRegistrationData
import org.signal.registration.StorageController
import org.signal.registration.proto.RegistrationData
import org.signal.registration.screens.localbackuprestore.LocalBackupInfo
import org.signal.registration.screens.remotebackuprestore.RemoteBackupRestoreProgress
import org.thoughtcrime.securesms.backup.FullBackupImporter
import org.thoughtcrime.securesms.backup.v2.BackupRepository
import org.thoughtcrime.securesms.backup.v2.RemoteRestoreResult
import org.thoughtcrime.securesms.backup.v2.RestoreV2Event
import org.thoughtcrime.securesms.backup.v2.local.LocalArchiver
import org.thoughtcrime.securesms.backup.v2.local.SnapshotFileSystem
import org.thoughtcrime.securesms.crypto.AttachmentSecretProvider
@@ -318,6 +327,57 @@ class AppRegistrationStorageController(private val context: Context) : StorageCo
backups.sortedByDescending { it.date }
}
override fun restoreRemoteBackup(aep: AccountEntropyPool): Flow<RemoteBackupRestoreProgress> = callbackFlow {
val subscriber = object {
@Subscribe(threadMode = ThreadMode.POSTING)
fun onRestoreEvent(event: RestoreV2Event) {
val progress = when (event.type) {
RestoreV2Event.Type.PROGRESS_DOWNLOAD -> RemoteBackupRestoreProgress.Downloading(event.count.inWholeBytes, event.estimatedTotalCount.inWholeBytes)
RestoreV2Event.Type.PROGRESS_RESTORE -> RemoteBackupRestoreProgress.Restoring(event.count.inWholeBytes, event.estimatedTotalCount.inWholeBytes)
RestoreV2Event.Type.PROGRESS_FINALIZING -> RemoteBackupRestoreProgress.Finalizing
}
trySend(progress)
}
}
EventBus.getDefault().register(subscriber)
launch(Dispatchers.IO) {
try {
when (BackupRepository.restoreRemoteBackup()) {
RemoteRestoreResult.Success -> {
send(RemoteBackupRestoreProgress.Complete)
}
RemoteRestoreResult.NetworkError -> {
send(RemoteBackupRestoreProgress.NetworkError())
}
RemoteRestoreResult.Canceled -> {
send(RemoteBackupRestoreProgress.Canceled)
}
RemoteRestoreResult.Failure -> {
if (SignalStore.backup.hasInvalidBackupVersion) {
send(RemoteBackupRestoreProgress.InvalidBackupVersion)
} else {
send(RemoteBackupRestoreProgress.GenericError())
}
}
RemoteRestoreResult.PermanentSvrBFailure -> {
send(RemoteBackupRestoreProgress.PermanentSvrBFailure)
}
}
} catch (e: Exception) {
Log.w(TAG, "Remote restore failed", e)
send(RemoteBackupRestoreProgress.GenericError(e))
} finally {
channel.close()
}
}
awaitClose {
EventBus.getDefault().unregister(subscriber)
}
}
private suspend fun writeRegistrationData(data: RegistrationData) = withContext(Dispatchers.IO) {
val file = File(context.cacheDir, TEMP_PROTO_FILENAME)
file.writeBytes(RegistrationData.ADAPTER.encode(data))