diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/BackupRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/BackupRepository.kt index bea1bbbfca..e5a5a24694 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/BackupRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/BackupRepository.kt @@ -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 { + 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() diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/stream/EncryptedBackupReader.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/stream/EncryptedBackupReader.kt index c72087bb0e..2caa01b60b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/stream/EncryptedBackupReader.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/stream/EncryptedBackupReader.kt @@ -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. diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/BackupMessagesJob.kt b/app/src/main/java/org/thoughtcrime/securesms/jobs/BackupMessagesJob.kt index 4d86214db3..639ef35960 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/BackupMessagesJob.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/BackupMessagesJob.kt @@ -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) diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/BackupValues.kt b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/BackupValues.kt index 45897d63b2..63a2768bdf 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/BackupValues.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/BackupValues.kt @@ -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) diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/ui/RegistrationViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/ui/RegistrationViewModel.kt index 06618aa626..c5d9230541 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/ui/RegistrationViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/ui/RegistrationViewModel.kt @@ -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() diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/ui/restore/RemoteRestoreViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/ui/restore/RemoteRestoreViewModel.kt index d035755024..7867b64b49 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/ui/restore/RemoteRestoreViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/ui/restore/RemoteRestoreViewModel.kt @@ -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) } diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageReceiver.java b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageReceiver.java index 121b9879f9..5b568c3063 100644 --- a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageReceiver.java +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageReceiver.java @@ -202,6 +202,10 @@ public class SignalServiceMessageReceiver { socket.retrieveBackup(cdnNumber, headers, cdnPath, destination, 1_000_000_000L, listener); } + public NetworkResult retrieveBackupForwardSecretMetadataBytes(int cdnNumber, Map headers, String cdnPath, int maxSizeBytes) { + return NetworkResult.fromFetch(() -> socket.retrieveBackupForwardSecrecyMetadataBytes(cdnNumber, headers, cdnPath, maxSizeBytes)); + } + /** * Retrieves a link+sync backup file. The data is written to @{code destination}. */ diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/archive/ArchiveApi.kt b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/archive/ArchiveApi.kt index a4a5fd8e2f..94c7acb98a 100644 --- a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/archive/ArchiveApi.kt +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/archive/ArchiveApi.kt @@ -163,6 +163,15 @@ class ArchiveApi( * return 0 for used space since that is stored under the media key/credential. * * Will return a [NetworkResult.StatusCodeError] with status code 404 if you haven't uploaded a backup yet. + * + * GET /v1/archives + * - 200: Success + * - 400: Bad arguments. The request may have been made on an authenticated channel. + * - 401: The provided backup auth credential presentation could not be verified or the public key signature was invalid or there is no backup associated with + * the backup-id in the presentation or the credential was of the wrong type (messages/media) + * - 403: Forbidden + * - 404: No backup + * - 429: Rate limited */ fun getBackupInfo(aci: ACI, archiveServiceAccess: ArchiveServiceAccess<*>): NetworkResult { return getCredentialPresentation(aci, archiveServiceAccess) diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java b/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java index c27afd3202..6384c7ff74 100644 --- a/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java @@ -430,6 +430,14 @@ public class PushServiceSocket { downloadFromCdn(destination, cdnNumber, headers, cdnPath, maxSizeBytes, listener); } + public byte[] retrieveBackupForwardSecrecyMetadataBytes(int cdnNumber, Map headers, String cdnPath, int maxSizeBytes) + throws MissingConfigurationException, IOException + { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + downloadFromCdn(outputStream, 0, cdnNumber, headers, cdnPath, maxSizeBytes, null); + return outputStream.toByteArray(); + } + public void retrieveAttachment(int cdnNumber, Map headers, SignalServiceAttachmentRemoteId remoteId, File destination, long maxSizeBytes, ProgressListener listener) throws IOException, MissingConfigurationException {