Do not do a new SVRB store operation when reusing backup file.

This commit is contained in:
Greyson Parrelli
2026-08-14 16:53:48 -04:00
parent b3a099b85f
commit a82ed18d3d
2 changed files with 187 additions and 125 deletions
@@ -14,6 +14,8 @@ import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.content.ContextCompat
import arrow.core.Either
import okio.ByteString
import okio.ByteString.Companion.toByteString
import okio.IOException
import org.signal.core.models.backup.MediaRootBackupKey
import org.signal.core.util.PendingIntentFlags
@@ -72,6 +74,8 @@ import kotlin.time.Duration.Companion.milliseconds
class BackupMessagesJob private constructor(
private var syncTime: Long,
private var dataFile: String,
private var pendingNextBackupSecretData: ByteArray?,
private var pendingMessageCutoffTime: Long,
private var resumableMessagesBackupUploadSpec: ResumableMessagesBackupUploadSpec?,
parameters: Parameters
) : CoroutineJob(parameters) {
@@ -124,6 +128,8 @@ class BackupMessagesJob private constructor(
constructor() : this(
syncTime = 0L,
dataFile = "",
pendingNextBackupSecretData = null,
pendingMessageCutoffTime = 0L,
resumableMessagesBackupUploadSpec = null,
parameters = Parameters.Builder()
.addConstraint(BackupMessagesConstraint.KEY)
@@ -135,6 +141,8 @@ class BackupMessagesJob private constructor(
override fun serialize(): ByteArray = BackupMessagesJobData(
syncTime = syncTime,
dataFile = dataFile,
pendingNextBackupSecretData = pendingNextBackupSecretData?.toByteString() ?: ByteString.EMPTY,
pendingMessageCutoffTime = pendingMessageCutoffTime,
resumableUri = resumableMessagesBackupUploadSpec?.resumableUri ?: "",
uploadSpec = resumableMessagesBackupUploadSpec?.attachmentUploadForm?.toUploadSpec()
).encode()
@@ -168,115 +176,6 @@ class BackupMessagesJob private constructor(
val stopwatch = Stopwatch("BackupMessagesJob")
val auth = when (val result = AppDependencies.archiveService.getSvrBAuth()) {
is Either.Right -> result.value
is Either.Left -> when (val error = result.value) {
is ArchiveError.CredentialError.RateLimited -> {
return Result.retry(error.retryAfter?.inWholeMilliseconds ?: defaultBackoff()).logW(TAG, "Rate limited when getting SVRB auth.", error.cause, true)
}
is ArchiveError.NetworkError,
is ArchiveError.CredentialError.Unauthorized,
is ArchiveError.CredentialError.NotFound,
is ArchiveError.CredentialError.InvalidRequest,
is ArchiveError.CredentialError.ZkVerificationFailed -> {
return Result.retry(defaultBackoff()).logW(TAG, "Failed to get SVRB auth: ${error::class.simpleName}", error.cause, true)
}
is ArchiveError.ApplicationError -> {
throw error.exception
}
}
}
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 Either.Right -> result.value
is Either.Left -> when (val error = result.value) {
is ArchiveError.CredentialError.Unauthorized,
is ArchiveError.EntitlementError.NotEntitled,
is ArchiveError.CredentialError.NotFound -> {
Log.i(TAG, "[svrb-restore] No backup data found, continuing.", true)
null
}
is ArchiveError.CredentialError.ZkVerificationFailed -> {
Log.w(TAG, "[svrb-restore] zkverification failed getting backup info, continuing.", true)
null
}
is ArchiveError.CredentialError.RateLimited -> {
return Result.retry(error.retryAfter?.inWholeMilliseconds ?: defaultBackoff()).logW(TAG, "[svrb-restore] Rate limited when getting remote forward secrecy metadata.", error.cause, true)
}
is ArchiveError.NetworkError,
is ArchiveError.CredentialError.InvalidRequest,
is ArchiveError.BackupFileError.UnexpectedResponse -> {
return Result.retry(defaultBackoff()).logW(TAG, "[svrb-restore] Failed to get remote forward secrecy metadata: ${error::class.simpleName}", error.cause, true)
}
is ArchiveError.ApplicationError -> {
throw error.exception
}
}
}
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")
SignalStore.backup.nextBackupSecretData = null
}
SvrBApi.RestoreResult.DataMissingError -> {
Log.i(TAG, "[svrb-restore] No SVRB data found, resetting local secret data: $result")
SignalStore.backup.nextBackupSecretData = null
}
is SvrBApi.RestoreResult.SvrError -> {
Log.w(TAG, "[svrb-restore] SVR enclave error, blocking backup until restore succeeds.", result.throwable, true)
return Result.retry(defaultBackoff())
}
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)
SignalStore.backup.nextBackupSecretData = secretData
secretData
}
val svrBMetadata: SvrBStoreResponse = when (val result = SignalNetwork.svrB.store(auth, SignalStore.backup.messageBackupKey, backupSecretData)) {
is SvrBApi.StoreResult.Success -> result.data
is SvrBApi.StoreResult.NetworkError -> return Result.retry(result.retryAfter?.inWholeMilliseconds ?: defaultBackoff()).logW(TAG, "SVRB transient network error.", result.exception, true)
is SvrBApi.StoreResult.SvrError -> return Result.retry(defaultBackoff()).logW(TAG, "SVRB error.", result.throwable, true)
SvrBApi.StoreResult.InvalidDataError -> {
Log.w(TAG, "Invalid SVRB data on the server! Clearing backup secret data and retrying.", true)
SignalStore.backup.nextBackupSecretData = null
return Result.retry(defaultBackoff())
}
is SvrBApi.StoreResult.UnknownError -> return Result.fatalFailure(RuntimeException(result.throwable))
}
Log.i(TAG, "Successfully stored data on SVRB.", true)
stopwatch.split("svrb")
val createKeyResult = SignalDatabase.attachments.createRemoteKeyForAttachmentsThatNeedArchiveUpload()
if (createKeyResult.totalCount > 0) {
if (createKeyResult.unexpectedKeyCreation) {
@@ -295,17 +194,159 @@ class BackupMessagesJob private constructor(
return Result.failure()
}
val (tempBackupFile, currentTime, messageCutoffTime) = when (val generateBackupFileResult = getOrCreateBackupFile(stopwatch, svrBMetadata.forwardSecrecyToken, svrBMetadata.metadata)) {
is BackupFileResult.Success -> generateBackupFileResult
BackupFileResult.Failure -> return Result.failure()
BackupFileResult.Retry -> return Result.retry(defaultBackoff())
val tempBackupFile: File
val currentTime: Long
val messageCutoffTime: Long
val nextBackupSecretData: ByteArray
val reusableSecretData = pendingNextBackupSecretData
val reusableBackupFile = if (reusableSecretData != null) findReusableBackupFile() else null
if (reusableBackupFile != null && reusableSecretData != null) {
Log.i(TAG, "Reusing the backup file from a previous attempt, along with the SVRB state it was built with.", true)
tempBackupFile = reusableBackupFile
currentTime = syncTime
messageCutoffTime = pendingMessageCutoffTime
nextBackupSecretData = reusableSecretData
} else {
if (resumableMessagesBackupUploadSpec != null) {
Log.w(TAG, "Not reusing the previous backup file, but an upload spec is still present. Clearing it so we don't resume onto a stale upload.", true)
resumableMessagesBackupUploadSpec = null
}
// We're building a new file, so whatever the previous attempt left behind is dead, including any SVRB state we may advance past below.
clearPendingBackupFile()
val auth = when (val result = AppDependencies.archiveService.getSvrBAuth()) {
is Either.Right -> result.value
is Either.Left -> when (val error = result.value) {
is ArchiveError.CredentialError.RateLimited -> {
return Result.retry(error.retryAfter?.inWholeMilliseconds ?: defaultBackoff()).logW(TAG, "Rate limited when getting SVRB auth.", error.cause, true)
}
is ArchiveError.NetworkError,
is ArchiveError.CredentialError.Unauthorized,
is ArchiveError.CredentialError.NotFound,
is ArchiveError.CredentialError.InvalidRequest,
is ArchiveError.CredentialError.ZkVerificationFailed -> {
return Result.retry(defaultBackoff()).logW(TAG, "Failed to get SVRB auth: ${error::class.simpleName}", error.cause, true)
}
is ArchiveError.ApplicationError -> {
throw error.exception
}
}
}
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 Either.Right -> result.value
is Either.Left -> when (val error = result.value) {
is ArchiveError.CredentialError.Unauthorized,
is ArchiveError.EntitlementError.NotEntitled,
is ArchiveError.CredentialError.NotFound -> {
Log.i(TAG, "[svrb-restore] No backup data found, continuing.", true)
null
}
is ArchiveError.CredentialError.ZkVerificationFailed -> {
Log.w(TAG, "[svrb-restore] zkverification failed getting backup info, continuing.", true)
null
}
is ArchiveError.CredentialError.RateLimited -> {
return Result.retry(error.retryAfter?.inWholeMilliseconds ?: defaultBackoff()).logW(TAG, "[svrb-restore] Rate limited when getting remote forward secrecy metadata.", error.cause, true)
}
is ArchiveError.NetworkError,
is ArchiveError.CredentialError.InvalidRequest,
is ArchiveError.BackupFileError.UnexpectedResponse -> {
return Result.retry(defaultBackoff()).logW(TAG, "[svrb-restore] Failed to get remote forward secrecy metadata: ${error::class.simpleName}", error.cause, true)
}
is ArchiveError.ApplicationError -> {
throw error.exception
}
}
}
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")
SignalStore.backup.nextBackupSecretData = null
}
SvrBApi.RestoreResult.DataMissingError -> {
Log.i(TAG, "[svrb-restore] No SVRB data found, resetting local secret data: $result")
SignalStore.backup.nextBackupSecretData = null
}
is SvrBApi.RestoreResult.SvrError -> {
Log.w(TAG, "[svrb-restore] SVR enclave error, blocking backup until restore succeeds.", result.throwable, true)
return Result.retry(defaultBackoff())
}
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)
SignalStore.backup.nextBackupSecretData = secretData
secretData
}
val svrBMetadata: SvrBStoreResponse = when (val result = SignalNetwork.svrB.store(auth, SignalStore.backup.messageBackupKey, backupSecretData)) {
is SvrBApi.StoreResult.Success -> result.data
is SvrBApi.StoreResult.NetworkError -> return Result.retry(result.retryAfter?.inWholeMilliseconds ?: defaultBackoff()).logW(TAG, "SVRB transient network error.", result.exception, true)
is SvrBApi.StoreResult.SvrError -> return Result.retry(defaultBackoff()).logW(TAG, "SVRB error.", result.throwable, true)
SvrBApi.StoreResult.InvalidDataError -> {
Log.w(TAG, "Invalid SVRB data on the server! Clearing backup secret data and retrying.", true)
SignalStore.backup.nextBackupSecretData = null
return Result.retry(defaultBackoff())
}
is SvrBApi.StoreResult.UnknownError -> return Result.fatalFailure(RuntimeException(result.throwable))
}
Log.i(TAG, "Successfully stored data on SVRB.", true)
stopwatch.split("svrb")
when (val generateBackupFileResult = createBackupFile(stopwatch, svrBMetadata.forwardSecrecyToken, svrBMetadata.metadata)) {
is BackupFileResult.Success -> {
tempBackupFile = generateBackupFileResult.tempBackupFile
currentTime = generateBackupFileResult.currentTime
messageCutoffTime = generateBackupFileResult.messageInclusionCutoffTime
}
BackupFileResult.Failure -> return Result.failure()
BackupFileResult.Retry -> return Result.retry(defaultBackoff())
}
nextBackupSecretData = svrBMetadata.nextBackupSecretData
}
ArchiveUploadProgress.onMessageBackupCreated(tempBackupFile.length())
SignalStore.backup.lastBackupProtoVersion = BackupRepository.VERSION
// These must be written together: the secret data is only valid for the exact file it was exported alongside.
this.syncTime = currentTime
this.dataFile = tempBackupFile.path
this.pendingNextBackupSecretData = nextBackupSecretData
this.pendingMessageCutoffTime = messageCutoffTime
val existingSpec = resumableMessagesBackupUploadSpec
val form: AttachmentUploadForm = if (existingSpec == null) {
@@ -319,7 +360,7 @@ class BackupMessagesJob private constructor(
is ArchiveError.UploadFormError.TooLarge -> {
Log.i(TAG, "Backup file is too large! Size: ${tempBackupFile.length()} bytes. Current threshold: ${SignalStore.backup.messageCuttoffDuration}", error.cause, true)
tempBackupFile.delete()
this.dataFile = ""
clearPendingBackupFile()
BackupRepository.markBackupCreationFailed(BackupValues.BackupCreationError.BACKUP_FILE_TOO_LARGE)
backupErrorHandled = true
@@ -415,7 +456,8 @@ class BackupMessagesJob private constructor(
SignalStore.backup.hasBackupBeenUploaded = true
stopwatch.split("upload")
SignalStore.backup.nextBackupSecretData = svrBMetadata.nextBackupSecretData
SignalStore.backup.nextBackupSecretData = nextBackupSecretData
clearPendingBackupFile()
SignalStore.backup.lastBackupProtoSize = tempBackupFile.length()
if (!tempBackupFile.delete()) {
@@ -463,21 +505,37 @@ class BackupMessagesJob private constructor(
return Result.success()
}
private fun getOrCreateBackupFile(
stopwatch: Stopwatch,
forwardSecrecyToken: BackupForwardSecrecyToken,
forwardSecrecyMetadata: ByteArray
): BackupFileResult {
/**
* The backup file left behind by a previous attempt, if it's still present and recent enough to upload as-is.
*/
private fun findReusableBackupFile(): File? {
if (System.currentTimeMillis() > syncTime && syncTime > 0L && dataFile.isNotNullOrBlank()) {
val file = File(dataFile)
val elapsed = (System.currentTimeMillis() - syncTime).milliseconds
if (file.exists() && file.canRead() && elapsed < FILE_REUSE_TIMEOUT) {
Log.d(TAG, "File exists and is new enough to utilize.", true)
return BackupFileResult.Success(file, syncTime, messageInclusionCutoffTime = SignalStore.backup.lastUsedMessageCutoffTime)
return file
}
}
return null
}
/**
* Clear out backup data associated with previous backup attempts.
*/
private fun clearPendingBackupFile() {
this.dataFile = ""
this.pendingNextBackupSecretData = null
this.pendingMessageCutoffTime = 0L
}
private fun createBackupFile(
stopwatch: Stopwatch,
forwardSecrecyToken: BackupForwardSecrecyToken,
forwardSecrecyMetadata: ByteArray
): BackupFileResult {
AppDependencies.blobs.clearTemporaryBackupsDirectory(AppDependencies.application)
val tempBackupFile = AppDependencies.blobs.forTemporaryBackup(AppDependencies.application)
@@ -513,13 +571,13 @@ class BackupMessagesJob private constructor(
if (e.message?.contains("ENOSPC") == true) {
Log.w(TAG, "Not enough space to make a backup!", e, true)
tempBackupFile.delete()
this.dataFile = ""
clearPendingBackupFile()
BackupRepository.markBackupCreationFailed(BackupValues.BackupCreationError.NOT_ENOUGH_DISK_SPACE)
return BackupFileResult.Failure
} else {
Log.w(TAG, "Exception during backup export", e)
tempBackupFile.delete()
this.dataFile = ""
clearPendingBackupFile()
return BackupFileResult.Retry
}
}
@@ -549,7 +607,7 @@ class BackupMessagesJob private constructor(
is ArchiveValidator.ValidationResult.MessageValidationError -> {
Log.w(TAG, "The backup file fails validation! Message: ${result.exception.message}, Details: ${result.messageDetails}", true)
tempBackupFile.delete()
this.dataFile = ""
clearPendingBackupFile()
BackupRepository.markBackupCreationFailed(BackupValues.BackupCreationError.VALIDATION)
backupErrorHandled = true
return BackupFileResult.Failure
@@ -558,7 +616,7 @@ class BackupMessagesJob private constructor(
is ArchiveValidator.ValidationResult.RecipientDuplicateE164Error -> {
Log.w(TAG, "The backup file fails validation with a duplicate recipient! Message: ${result.exception.message}, Details: ${result.details}", true)
tempBackupFile.delete()
this.dataFile = ""
clearPendingBackupFile()
AppDependencies.jobManager.add(E164FormattingJob())
BackupRepository.markBackupCreationFailed(BackupValues.BackupCreationError.VALIDATION)
backupErrorHandled = true
@@ -651,6 +709,8 @@ class BackupMessagesJob private constructor(
return BackupMessagesJob(
syncTime = jobData.syncTime,
dataFile = jobData.dataFile,
pendingNextBackupSecretData = jobData.pendingNextBackupSecretData.takeIf { it.size > 0 }?.toByteArray(),
pendingMessageCutoffTime = jobData.pendingMessageCutoffTime,
resumableMessagesBackupUploadSpec = uploadSpecFromJobData(jobData),
parameters = parameters
)
+2
View File
@@ -165,6 +165,8 @@ message BackupMessagesJobData {
string dataFile = 2;
ResumableUpload uploadSpec = 3;
string resumableUri = 4;
bytes pendingNextBackupSecretData = 5;
uint64 pendingMessageCutoffTime = 6;
}
message NewLinkedDeviceNotificationJobData {