Update the backup playground to be more friendly.

This commit is contained in:
Greyson Parrelli 2024-12-09 15:56:20 -05:00
parent d1bfa6ee9e
commit 3ea9dd5e1d
4 changed files with 298 additions and 455 deletions

View file

@ -300,20 +300,6 @@ class ArchiveImportExportTests {
) )
} }
private fun assertPassesValidator(testName: String, generatedBackupData: ByteArray): TestResult.Failure? {
try {
BackupRepository.validate(
length = generatedBackupData.size.toLong(),
inputStreamFactory = { ByteArrayInputStream(generatedBackupData) },
selfData = BackupRepository.SelfData(SELF_ACI, SELF_PNI, SELF_E164, ProfileKey(SELF_PROFILE_KEY))
)
} catch (e: Exception) {
return TestResult.Failure(testName, "Generated backup failed validation: ${e.message}")
}
return null
}
private fun checkEquivalent(testName: String, import: ByteArray, export: ByteArray): TestResult.Failure? { private fun checkEquivalent(testName: String, import: ByteArray, export: ByteArray): TestResult.Failure? {
val importComparable = try { val importComparable = try {
ComparableBackup.readUnencrypted(MessageBackup.Purpose.REMOTE_BACKUP, import.inputStream(), import.size.toLong()) ComparableBackup.readUnencrypted(MessageBackup.Purpose.REMOTE_BACKUP, import.inputStream(), import.size.toLong())

View file

@ -33,9 +33,6 @@ import org.signal.core.util.requireNonNullString
import org.signal.core.util.stream.NonClosingOutputStream import org.signal.core.util.stream.NonClosingOutputStream
import org.signal.core.util.urlEncode import org.signal.core.util.urlEncode
import org.signal.core.util.withinTransaction import org.signal.core.util.withinTransaction
import org.signal.libsignal.messagebackup.MessageBackup
import org.signal.libsignal.messagebackup.MessageBackup.ValidationResult
import org.signal.libsignal.protocol.ServiceId.Aci
import org.signal.libsignal.zkgroup.backups.BackupLevel import org.signal.libsignal.zkgroup.backups.BackupLevel
import org.signal.libsignal.zkgroup.profiles.ProfileKey import org.signal.libsignal.zkgroup.profiles.ProfileKey
import org.thoughtcrime.securesms.attachments.Attachment import org.thoughtcrime.securesms.attachments.Attachment
@ -109,7 +106,6 @@ import java.util.concurrent.atomic.AtomicLong
import kotlin.time.Duration.Companion.days import kotlin.time.Duration.Companion.days
import kotlin.time.Duration.Companion.hours import kotlin.time.Duration.Companion.hours
import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.milliseconds
import org.signal.libsignal.messagebackup.MessageBackupKey as LibSignalMessageBackupKey
object BackupRepository { object BackupRepository {
@ -894,13 +890,6 @@ object BackupRepository {
return ImportResult.Success(backupTime = header.backupTimeMs) return ImportResult.Success(backupTime = header.backupTimeMs)
} }
fun validate(length: Long, inputStreamFactory: () -> InputStream, selfData: SelfData): ValidationResult {
val accountEntropyPool = SignalStore.account.accountEntropyPool.value
val key = LibSignalMessageBackupKey(accountEntropyPool, Aci.parseFromBinary(selfData.aci.toByteArray()))
return MessageBackup.validate(key, MessageBackup.Purpose.REMOTE_BACKUP, inputStreamFactory, length)
}
fun listRemoteMediaObjects(limit: Int, cursor: String? = null): NetworkResult<ArchiveGetMediaItemsResponse> { fun listRemoteMediaObjects(limit: Int, cursor: String? = null): NetworkResult<ArchiveGetMediaItemsResponse> {
return initBackupAndFetchAuth() return initBackupAndFetchAuth()
.then { credential -> .then { credential ->

View file

@ -7,7 +7,6 @@ package org.thoughtcrime.securesms.components.settings.app.internal.backup
import android.app.Activity.RESULT_OK import android.app.Activity.RESULT_OK
import android.content.Intent import android.content.Intent
import android.content.res.Configuration
import android.os.Bundle import android.os.Bundle
import android.widget.Toast import android.widget.Toast
import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.ActivityResultLauncher
@ -23,13 +22,11 @@ import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Checkbox import androidx.compose.material3.Checkbox
import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenu
@ -42,7 +39,6 @@ import androidx.compose.material3.RadioButton
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.material3.Switch
import androidx.compose.material3.Tab import androidx.compose.material3.Tab
import androidx.compose.material3.TabRow import androidx.compose.material3.TabRow
import androidx.compose.material3.Text import androidx.compose.material3.Text
@ -67,18 +63,15 @@ import androidx.compose.ui.unit.dp
import androidx.fragment.app.viewModels import androidx.fragment.app.viewModels
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.signal.core.ui.Buttons
import org.signal.core.ui.Dividers import org.signal.core.ui.Dividers
import org.signal.core.ui.Previews
import org.signal.core.ui.Rows
import org.signal.core.ui.SignalPreview
import org.signal.core.ui.Snackbars import org.signal.core.ui.Snackbars
import org.signal.core.ui.theme.SignalTheme
import org.signal.core.util.bytes
import org.signal.core.util.getLength import org.signal.core.util.getLength
import org.signal.core.util.roundedString
import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.attachments.AttachmentId import org.thoughtcrime.securesms.attachments.AttachmentId
import org.thoughtcrime.securesms.backup.v2.MessageBackupTier import org.thoughtcrime.securesms.backup.v2.MessageBackupTier
import org.thoughtcrime.securesms.components.settings.app.internal.backup.InternalBackupPlaygroundViewModel.BackupState
import org.thoughtcrime.securesms.components.settings.app.internal.backup.InternalBackupPlaygroundViewModel.BackupUploadState
import org.thoughtcrime.securesms.components.settings.app.internal.backup.InternalBackupPlaygroundViewModel.ScreenState import org.thoughtcrime.securesms.components.settings.app.internal.backup.InternalBackupPlaygroundViewModel.ScreenState
import org.thoughtcrime.securesms.compose.ComposeFragment import org.thoughtcrime.securesms.compose.ComposeFragment
import org.thoughtcrime.securesms.jobs.LocalBackupJob import org.thoughtcrime.securesms.jobs.LocalBackupJob
@ -87,46 +80,47 @@ import org.thoughtcrime.securesms.keyvalue.SignalStore
class InternalBackupPlaygroundFragment : ComposeFragment() { class InternalBackupPlaygroundFragment : ComposeFragment() {
private val viewModel: InternalBackupPlaygroundViewModel by viewModels() private val viewModel: InternalBackupPlaygroundViewModel by viewModels()
private lateinit var exportFileLauncher: ActivityResultLauncher<Intent> private lateinit var saveEncryptedBackupToDiskLauncher: ActivityResultLauncher<Intent>
private lateinit var importFileLauncher: ActivityResultLauncher<Intent> private lateinit var savePlaintextBackupToDiskLauncher: ActivityResultLauncher<Intent>
private lateinit var validateFileLauncher: ActivityResultLauncher<Intent> private lateinit var importEncryptedBackupFromDiskLauncher: ActivityResultLauncher<Intent>
private lateinit var savePlaintextcopyLauncher: ActivityResultLauncher<Intent> private lateinit var savePlaintextCopyLauncher: ActivityResultLauncher<Intent>
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
exportFileLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> saveEncryptedBackupToDiskLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode == RESULT_OK) { if (result.resultCode == RESULT_OK) {
result.data?.data?.let { uri -> result.data?.data?.let { uri ->
requireContext().contentResolver.openOutputStream(uri)?.use { outputStream -> viewModel.exportEncrypted(
outputStream.write(viewModel.backupData!!) openStream = { requireContext().contentResolver.openOutputStream(uri)!! },
Toast.makeText(requireContext(), "Saved successfully", Toast.LENGTH_SHORT).show() appendStream = { requireContext().contentResolver.openOutputStream(uri, "wa")!! }
} ?: Toast.makeText(requireContext(), "Failed to open output stream", Toast.LENGTH_SHORT).show() )
} ?: Toast.makeText(requireContext(), "No URI selected", Toast.LENGTH_SHORT).show() } ?: Toast.makeText(requireContext(), "No URI selected", Toast.LENGTH_SHORT).show()
} }
} }
importFileLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> savePlaintextBackupToDiskLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode == RESULT_OK) {
result.data?.data?.let { uri ->
viewModel.exportPlaintext(
openStream = { requireContext().contentResolver.openOutputStream(uri)!! },
appendStream = { requireContext().contentResolver.openOutputStream(uri, "wa")!! }
)
} ?: Toast.makeText(requireContext(), "No URI selected", Toast.LENGTH_SHORT).show()
}
}
importEncryptedBackupFromDiskLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode == RESULT_OK) { if (result.resultCode == RESULT_OK) {
result.data?.data?.let { uri -> result.data?.data?.let { uri ->
requireContext().contentResolver.getLength(uri)?.let { length -> requireContext().contentResolver.getLength(uri)?.let { length ->
viewModel.import(length) { requireContext().contentResolver.openInputStream(uri)!! } viewModel.importEncryptedBackup(length) { requireContext().contentResolver.openInputStream(uri)!! }
} }
} ?: Toast.makeText(requireContext(), "No URI selected", Toast.LENGTH_SHORT).show() } ?: Toast.makeText(requireContext(), "No URI selected", Toast.LENGTH_SHORT).show()
} }
} }
validateFileLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> savePlaintextCopyLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode == RESULT_OK) {
result.data?.data?.let { uri ->
requireContext().contentResolver.getLength(uri)?.let { length ->
viewModel.validate(length) { requireContext().contentResolver.openInputStream(uri)!! }
}
} ?: Toast.makeText(requireContext(), "No URI selected", Toast.LENGTH_SHORT).show()
}
}
savePlaintextcopyLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode == RESULT_OK) { if (result.resultCode == RESULT_OK) {
result.data?.data?.let { uri -> result.data?.data?.let { uri ->
viewModel.fetchRemoteBackupAndWritePlaintext(requireContext().contentResolver.openOutputStream(uri)) viewModel.fetchRemoteBackupAndWritePlaintext(requireContext().contentResolver.openOutputStream(uri))
@ -152,54 +146,32 @@ class InternalBackupPlaygroundFragment : ComposeFragment() {
mainContent = { mainContent = {
Screen( Screen(
state = state, state = state,
onExportClicked = { viewModel.export() }, onBackupTierSelected = { tier -> viewModel.onBackupTierSelected(tier) },
onExportDirectoryClicked = { LocalBackupJob.enqueueArchive() }, onCheckRemoteBackupStateClicked = { viewModel.checkRemoteBackupState() },
onImportMemoryClicked = { viewModel.import() }, onEnqueueRemoteBackupClicked = { viewModel.triggerBackupJob() },
onImportFileClicked = { onHaltAllBackupJobsClicked = { viewModel.haltAllJobs() },
val intent = Intent().apply { onValidateBackupClicked = { viewModel.validateBackup() },
action = Intent.ACTION_GET_CONTENT onSaveEncryptedBackupToDiskClicked = {
type = "application/octet-stream"
addCategory(Intent.CATEGORY_OPENABLE)
}
importFileLauncher.launch(intent)
},
onImportDirectoryClicked = {
viewModel.import(SignalStore.settings.signalBackupDirectory!!)
},
onPlaintextClicked = { viewModel.onPlaintextToggled() },
onSaveToDiskClicked = {
val intent = Intent().apply { val intent = Intent().apply {
action = Intent.ACTION_CREATE_DOCUMENT action = Intent.ACTION_CREATE_DOCUMENT
type = "application/octet-stream" type = "application/octet-stream"
addCategory(Intent.CATEGORY_OPENABLE) addCategory(Intent.CATEGORY_OPENABLE)
putExtra(Intent.EXTRA_TITLE, "backup-${if (state.plaintext) "plaintext" else "encrypted"}-${System.currentTimeMillis()}.bin") putExtra(Intent.EXTRA_TITLE, "backup-encrypted-${System.currentTimeMillis()}.bin")
} }
exportFileLauncher.launch(intent) saveEncryptedBackupToDiskLauncher.launch(intent)
}, },
onUploadToRemoteClicked = { viewModel.uploadBackupToRemote() }, onSavePlaintextBackupToDiskClicked = {
onCheckRemoteBackupStateClicked = { viewModel.checkRemoteBackupState() },
onValidateFileClicked = {
val intent = Intent().apply { val intent = Intent().apply {
action = Intent.ACTION_GET_CONTENT action = Intent.ACTION_CREATE_DOCUMENT
type = "application/octet-stream" type = "application/octet-stream"
addCategory(Intent.CATEGORY_OPENABLE) addCategory(Intent.CATEGORY_OPENABLE)
putExtra(Intent.EXTRA_TITLE, "backup-plaintext-${System.currentTimeMillis()}.bin")
} }
validateFileLauncher.launch(intent) savePlaintextBackupToDiskLauncher.launch(intent)
}, },
onTriggerBackupJobClicked = { viewModel.triggerBackupJob() }, onSavePlaintextCopyOfRemoteBackupClicked = {
onWipeDataAndRestoreClicked = {
MaterialAlertDialogBuilder(context)
.setTitle("Are you sure?")
.setMessage("This will delete all of your chats! Make sure you've finished a backup first, we don't check for you. Only do this on a test device!")
.setPositiveButton("Wipe and restore") { _, _ -> viewModel.wipeAllDataAndRestoreFromRemote() }
.show()
},
onBackupTierSelected = { tier -> viewModel.onBackupTierSelected(tier) },
onHaltAllJobs = { viewModel.haltAllJobs() },
onSavePlaintextCopy = {
val intent = Intent().apply { val intent = Intent().apply {
action = Intent.ACTION_CREATE_DOCUMENT action = Intent.ACTION_CREATE_DOCUMENT
type = "application/octet-stream" type = "application/octet-stream"
@ -207,7 +179,31 @@ class InternalBackupPlaygroundFragment : ComposeFragment() {
putExtra(Intent.EXTRA_TITLE, "backup-plaintext-${System.currentTimeMillis()}.binproto") putExtra(Intent.EXTRA_TITLE, "backup-plaintext-${System.currentTimeMillis()}.binproto")
} }
savePlaintextcopyLauncher.launch(intent) savePlaintextCopyLauncher.launch(intent)
},
onExportNewStyleLocalBackupClicked = { LocalBackupJob.enqueueArchive() },
onWipeDataAndRestoreFromRemoteClicked = {
MaterialAlertDialogBuilder(context)
.setTitle("Are you sure?")
.setMessage("This will delete all of your chats! Make sure you've finished a backup first, we don't check for you. Only do this on a test device!")
.setPositiveButton("Wipe and restore") { _, _ -> viewModel.wipeAllDataAndRestoreFromRemote() }
.show()
},
onImportEncryptedBackupFromDiskClicked = {
val intent = Intent().apply {
action = Intent.ACTION_GET_CONTENT
type = "application/octet-stream"
addCategory(Intent.CATEGORY_OPENABLE)
}
importEncryptedBackupFromDiskLauncher.launch(intent)
},
onImportNewStyleLocalBackupClicked = {
MaterialAlertDialogBuilder(context)
.setTitle("Are you sure?")
.setMessage("After you choose a file to import, this will delete all of your chats, then restore them from the file! Only do this on a test device!")
.setPositiveButton("Wipe and restore") { _, _ -> viewModel.import(SignalStore.settings.signalBackupDirectory!!) }
.show()
} }
) )
}, },
@ -290,21 +286,18 @@ fun Tabs(
@Composable @Composable
fun Screen( fun Screen(
state: ScreenState, state: ScreenState,
onExportClicked: () -> Unit = {}, onExportNewStyleLocalBackupClicked: () -> Unit = {},
onExportDirectoryClicked: () -> Unit = {}, onImportNewStyleLocalBackupClicked: () -> Unit = {},
onImportMemoryClicked: () -> Unit = {},
onImportFileClicked: () -> Unit = {},
onImportDirectoryClicked: () -> Unit = {},
onPlaintextClicked: () -> Unit = {},
onSaveToDiskClicked: () -> Unit = {},
onValidateFileClicked: () -> Unit = {},
onUploadToRemoteClicked: () -> Unit = {},
onCheckRemoteBackupStateClicked: () -> Unit = {}, onCheckRemoteBackupStateClicked: () -> Unit = {},
onTriggerBackupJobClicked: () -> Unit = {}, onEnqueueRemoteBackupClicked: () -> Unit = {},
onWipeDataAndRestoreClicked: () -> Unit = {}, onWipeDataAndRestoreFromRemoteClicked: () -> Unit = {},
onBackupTierSelected: (MessageBackupTier?) -> Unit = {}, onBackupTierSelected: (MessageBackupTier?) -> Unit = {},
onHaltAllJobs: () -> Unit = {}, onHaltAllBackupJobsClicked: () -> Unit = {},
onSavePlaintextCopy: () -> Unit = {} onSavePlaintextCopyOfRemoteBackupClicked: () -> Unit = {},
onValidateBackupClicked: () -> Unit = {},
onSaveEncryptedBackupToDiskClicked: () -> Unit = {},
onSavePlaintextBackupToDiskClicked: () -> Unit = {},
onImportEncryptedBackupFromDiskClicked: () -> Unit = {}
) { ) {
val scrollState = rememberScrollState() val scrollState = rememberScrollState()
val options = remember { val options = remember {
@ -322,7 +315,6 @@ fun Screen(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.verticalScroll(scrollState) .verticalScroll(scrollState)
.padding(16.dp)
) { ) {
Row(verticalAlignment = Alignment.CenterVertically) { Row(verticalAlignment = Alignment.CenterVertically) {
Text("Tier", fontWeight = FontWeight.Bold) Text("Tier", fontWeight = FontWeight.Bold)
@ -339,191 +331,121 @@ fun Screen(
Dividers.Default() Dividers.Default()
Buttons.LargePrimary( Rows.TextRow(
onClick = onTriggerBackupJobClicked text = {
) { Text(
Text("Enqueue remote backup") text = state.statusMessage ?: "Status messages will appear here as you perform operations",
} style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
Button( )
onClick = onWipeDataAndRestoreClicked,
colors = ButtonDefaults.buttonColors(containerColor = Color(0xFFC33C00))
) {
Text("Wipe data and restore")
}
Buttons.LargeTonal(
onClick = onHaltAllJobs
) {
Text("Halt all backup jobs")
}
Buttons.LargeTonal(
onClick = onSavePlaintextCopy
) {
Text("Save plaintext copy of remote backup")
} }
)
Dividers.Default() Dividers.Default()
Buttons.LargeTonal( Rows.TextRow(
text = "Check remote backup state",
label = "Get a summary of what your remote backup looks like (presence, size, etc).",
onClick = onCheckRemoteBackupStateClicked
)
Rows.TextRow(
text = "Enqueue remote backup",
label = "Schedules a job that will perform a routine remote backup.",
onClick = onEnqueueRemoteBackupClicked
)
Rows.TextRow(
text = "Halt all backup jobs",
label = "Stops all backup-related jobs to the best of our ability.",
onClick = onHaltAllBackupJobsClicked
)
Rows.TextRow(
text = "Validate backup",
label = "Generates a new backup and reports whether it passes validation. Does not save or upload anything.",
onClick = onValidateBackupClicked
)
Rows.TextRow(
text = "Save encrypted backup to disk",
label = "Generates an encrypted backup (the same thing you would upload) and saves it to your local disk.",
onClick = onSaveEncryptedBackupToDiskClicked
)
Rows.TextRow(
text = "Save plaintext backup to disk",
label = "Generates a plaintext, uncompressed backup and saves it to your local disk.",
onClick = onSavePlaintextBackupToDiskClicked
)
Rows.TextRow(
text = "Save plaintext copy of remote backup",
label = "Downloads your most recently uploaded backup and saves it to disk, plaintext and uncompressed.",
onClick = onSavePlaintextCopyOfRemoteBackupClicked
)
Rows.TextRow(
text = "Perform a new-style local backup",
label = "Creates a local backup (in your already-chosen backup directory) using the new on-disk backup format. This is the successor to the local backups existing users can build.",
onClick = onExportNewStyleLocalBackupClicked
)
Dividers.Default()
Text(
text = "DANGER ZONE",
color = Color.Red,
fontWeight = FontWeight.Bold
)
Rows.TextRow(
text = {
Text(
text = "The following operations are potentially destructive! Only use them if you know what you're doing.",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
)
Rows.TextRow(
text = "Wipe data and restore from remote",
label = "Erases all content on your device, followed by a restore of your remote backup.",
onClick = onWipeDataAndRestoreFromRemoteClicked
)
Rows.TextRow(
text = "Clear backup init flag",
label = "Clears our local state around whether backups have been initialized or not. Will force us to make request to claim backupId and set public keys.",
onClick = { onClick = {
SignalStore.backup.backupsInitialized = false SignalStore.backup.backupsInitialized = false
} }
) { )
Text("Clear backup init flag")
}
Buttons.LargeTonal( Rows.TextRow(
text = "Clear backup credentials",
label = "Clears any cached backup credentials, for both our message and media backups.",
onClick = { onClick = {
SignalStore.backup.messageCredentials.clearAll() SignalStore.backup.messageCredentials.clearAll()
SignalStore.backup.mediaCredentials.clearAll() SignalStore.backup.mediaCredentials.clearAll()
} }
) {
Text("Clear backup credentials")
}
Dividers.Default()
Row(
verticalAlignment = Alignment.CenterVertically
) {
StateLabel(text = "Plaintext?")
Spacer(modifier = Modifier.width(8.dp))
Switch(
checked = state.plaintext,
onCheckedChange = { onPlaintextClicked() }
) )
}
Spacer(modifier = Modifier.height(8.dp)) Rows.TextRow(
text = "Wipe all data and restore from file",
label = "Erases all content on your device, followed by a restore of an encrypted backup selected from disk.",
onClick = onImportEncryptedBackupFromDiskClicked
)
Buttons.LargePrimary( Rows.TextRow(
onClick = onExportClicked, text = "Wipe all data and restore a new-style local backup",
enabled = !state.backupState.inProgress label = "Erases all content on your device, followed by a restore of a previously-generated new-style local backup.",
) { onClick = onImportNewStyleLocalBackupClicked
Text("Export") )
}
Buttons.LargePrimary(
onClick = onExportDirectoryClicked,
enabled = !state.backupState.inProgress && state.canReadWriteBackupDirectory
) {
Text("Export to backup directory")
}
Dividers.Default() Dividers.Default()
Buttons.LargeTonal(
onClick = onImportMemoryClicked,
enabled = state.backupState == BackupState.EXPORT_DONE
) {
Text("Import from memory")
}
Buttons.LargeTonal(
onClick = onImportFileClicked
) {
Text("Import from file")
}
Buttons.LargeTonal(
onClick = onImportDirectoryClicked,
enabled = state.canReadWriteBackupDirectory
) {
Text("Import from backup directory")
}
Buttons.LargeTonal(
onClick = onValidateFileClicked
) {
Text("Validate file")
}
Spacer(modifier = Modifier.height(16.dp))
when (state.backupState) {
BackupState.NONE -> {
StateLabel("")
}
BackupState.EXPORT_IN_PROGRESS -> {
StateLabel("Export in progress...")
}
BackupState.EXPORT_DONE -> {
StateLabel("Export complete. Sitting in memory. You can click 'Import' to import that data, save it to a file, or upload it to remote.")
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
Buttons.MediumTonal(onClick = onSaveToDiskClicked) {
Text("Save to file")
}
}
BackupState.BACKUP_JOB_DONE -> {
StateLabel("Backup complete and uploaded")
}
BackupState.IMPORT_IN_PROGRESS -> {
StateLabel("Import in progress...")
}
}
Dividers.Default()
Buttons.LargeTonal(
onClick = onCheckRemoteBackupStateClicked
) {
Text("Check remote backup state")
}
Spacer(modifier = Modifier.height(8.dp))
when (state.remoteBackupState) {
is InternalBackupPlaygroundViewModel.RemoteBackupState.Available -> {
StateLabel("Exists/allocated. ${state.remoteBackupState.response.mediaCount} media items, using ${state.remoteBackupState.response.usedSpace} bytes (${state.remoteBackupState.response.usedSpace.bytes.inMebiBytes.roundedString(3)} MiB)")
}
InternalBackupPlaygroundViewModel.RemoteBackupState.GeneralError -> {
StateLabel("Hit an unknown error. Check the logs.")
}
InternalBackupPlaygroundViewModel.RemoteBackupState.NotFound -> {
StateLabel("Not found.")
}
InternalBackupPlaygroundViewModel.RemoteBackupState.Unknown -> {
StateLabel("Hit the button above to check the state.")
}
}
Spacer(modifier = Modifier.height(8.dp))
Buttons.LargePrimary(
onClick = onUploadToRemoteClicked,
enabled = state.backupState == BackupState.EXPORT_DONE
) {
Text("Upload to remote")
}
Spacer(modifier = Modifier.height(8.dp))
when (state.uploadState) {
BackupUploadState.NONE -> {
StateLabel("")
}
BackupUploadState.UPLOAD_IN_PROGRESS -> {
StateLabel("Upload in progress...")
}
BackupUploadState.UPLOAD_DONE -> {
StateLabel("Upload complete.")
}
BackupUploadState.UPLOAD_FAILED -> {
StateLabel("Upload failed.")
}
}
} }
} }
} }
@ -713,57 +635,18 @@ private data class MediaMultiSelectState(
val expandedOption: AttachmentId? = null val expandedOption: AttachmentId? = null
) )
@Preview(name = "Light Theme", group = "screen", uiMode = Configuration.UI_MODE_NIGHT_NO) @SignalPreview
@Preview(name = "Dark Theme", group = "screen", uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable @Composable
fun PreviewScreen() { fun PreviewScreen() {
SignalTheme { Previews.Preview {
Surface { Screen(state = ScreenState())
Screen(state = ScreenState(backupState = BackupState.NONE, plaintext = false))
}
} }
} }
@Preview(name = "Light Theme", group = "screen", uiMode = Configuration.UI_MODE_NIGHT_NO) @SignalPreview
@Preview(name = "Dark Theme", group = "screen", uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable @Composable
fun PreviewScreenExportInProgress() { fun PreviewScreenExportInProgress() {
SignalTheme { Previews.Preview {
Surface { Screen(state = ScreenState(statusMessage = "Some random status message."))
Screen(state = ScreenState(backupState = BackupState.EXPORT_IN_PROGRESS, plaintext = false))
}
}
}
@Preview(name = "Light Theme", group = "screen", uiMode = Configuration.UI_MODE_NIGHT_NO)
@Preview(name = "Dark Theme", group = "screen", uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
fun PreviewScreenExportDone() {
SignalTheme {
Surface {
Screen(state = ScreenState(backupState = BackupState.EXPORT_DONE, plaintext = false))
}
}
}
@Preview(name = "Light Theme", group = "screen", uiMode = Configuration.UI_MODE_NIGHT_NO)
@Preview(name = "Dark Theme", group = "screen", uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
fun PreviewScreenImportInProgress() {
SignalTheme {
Surface {
Screen(state = ScreenState(backupState = BackupState.IMPORT_IN_PROGRESS, plaintext = false))
}
}
}
@Preview(name = "Light Theme", group = "screen", uiMode = Configuration.UI_MODE_NIGHT_NO)
@Preview(name = "Dark Theme", group = "screen", uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
fun PreviewScreenUploadInProgress() {
SignalTheme {
Surface {
Screen(state = ScreenState(uploadState = BackupUploadState.UPLOAD_IN_PROGRESS, plaintext = false))
}
} }
} }

View file

@ -18,14 +18,17 @@ import io.reactivex.rxjava3.disposables.CompositeDisposable
import io.reactivex.rxjava3.kotlin.plusAssign import io.reactivex.rxjava3.kotlin.plusAssign
import io.reactivex.rxjava3.kotlin.subscribeBy import io.reactivex.rxjava3.kotlin.subscribeBy
import io.reactivex.rxjava3.schedulers.Schedulers import io.reactivex.rxjava3.schedulers.Schedulers
import org.signal.core.util.bytes
import org.signal.core.util.concurrent.SignalExecutors import org.signal.core.util.concurrent.SignalExecutors
import org.signal.core.util.copyTo import org.signal.core.util.copyTo
import org.signal.core.util.logging.Log import org.signal.core.util.logging.Log
import org.signal.core.util.readNBytesOrThrow import org.signal.core.util.readNBytesOrThrow
import org.signal.core.util.roundedString
import org.signal.core.util.stream.LimitedInputStream import org.signal.core.util.stream.LimitedInputStream
import org.signal.libsignal.zkgroup.profiles.ProfileKey import org.signal.libsignal.zkgroup.profiles.ProfileKey
import org.thoughtcrime.securesms.attachments.AttachmentId import org.thoughtcrime.securesms.attachments.AttachmentId
import org.thoughtcrime.securesms.attachments.DatabaseAttachment import org.thoughtcrime.securesms.attachments.DatabaseAttachment
import org.thoughtcrime.securesms.backup.v2.ArchiveValidator
import org.thoughtcrime.securesms.backup.v2.BackupMetadata import org.thoughtcrime.securesms.backup.v2.BackupMetadata
import org.thoughtcrime.securesms.backup.v2.BackupRepository import org.thoughtcrime.securesms.backup.v2.BackupRepository
import org.thoughtcrime.securesms.backup.v2.MessageBackupTier import org.thoughtcrime.securesms.backup.v2.MessageBackupTier
@ -54,7 +57,7 @@ import org.thoughtcrime.securesms.providers.BlobProvider
import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.recipients.Recipient
import org.whispersystems.signalservice.api.NetworkResult import org.whispersystems.signalservice.api.NetworkResult
import org.whispersystems.signalservice.api.backup.MediaName import org.whispersystems.signalservice.api.backup.MediaName
import java.io.ByteArrayInputStream import java.io.FileOutputStream
import java.io.IOException import java.io.IOException
import java.io.InputStream import java.io.InputStream
import java.io.OutputStream import java.io.OutputStream
@ -72,15 +75,10 @@ class InternalBackupPlaygroundViewModel : ViewModel() {
private val TAG = Log.tag(InternalBackupPlaygroundViewModel::class) private val TAG = Log.tag(InternalBackupPlaygroundViewModel::class)
} }
var backupData: ByteArray? = null
val disposables = CompositeDisposable() val disposables = CompositeDisposable()
private val _state: MutableState<ScreenState> = mutableStateOf( private val _state: MutableState<ScreenState> = mutableStateOf(
ScreenState( ScreenState(
backupState = BackupState.NONE,
uploadState = BackupUploadState.NONE,
plaintext = false,
canReadWriteBackupDirectory = SignalStore.settings.signalBackupDirectory?.let { canReadWriteBackupDirectory = SignalStore.settings.signalBackupDirectory?.let {
val file = DocumentFile.fromTreeUri(AppDependencies.application, it) val file = DocumentFile.fromTreeUri(AppDependencies.application, it)
file != null && file.canWrite() && file.canRead() file != null && file.canWrite() && file.canRead()
@ -93,66 +91,94 @@ class InternalBackupPlaygroundViewModel : ViewModel() {
private val _mediaState: MutableState<MediaState> = mutableStateOf(MediaState()) private val _mediaState: MutableState<MediaState> = mutableStateOf(MediaState())
val mediaState: State<MediaState> = _mediaState val mediaState: State<MediaState> = _mediaState
fun export() { fun exportEncrypted(openStream: () -> OutputStream, appendStream: () -> OutputStream) {
_state.value = _state.value.copy(backupState = BackupState.EXPORT_IN_PROGRESS) _state.value = _state.value.copy(statusMessage = "Exporting encrypted backup to disk...")
val plaintext = _state.value.plaintext disposables += Single
.fromCallable {
disposables += Single.fromCallable { BackupRepository.debugExport(plaintext = plaintext) } BackupRepository.export(
outputStream = openStream(),
append = { bytes -> appendStream().use { it.write(bytes) } }
)
}
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe { data -> .subscribe { data ->
backupData = data _state.value = _state.value.copy(statusMessage = "Encrypted backup complete!")
_state.value = _state.value.copy(backupState = BackupState.EXPORT_DONE) }
}
fun exportPlaintext(openStream: () -> OutputStream, appendStream: () -> OutputStream) {
_state.value = _state.value.copy(statusMessage = "Exporting plaintext backup to disk...")
disposables += Single
.fromCallable {
BackupRepository.export(
outputStream = openStream(),
append = { bytes -> appendStream().use { it.write(bytes) } },
plaintext = true
)
}
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe { data ->
_state.value = _state.value.copy(statusMessage = "Plaintext backup complete!")
}
}
fun validateBackup() {
_state.value = _state.value.copy(statusMessage = "Exporting to a temporary file...")
val tempFile = BlobProvider.getInstance().forNonAutoEncryptingSingleSessionOnDisk(AppDependencies.application)
disposables += Single
.fromCallable {
BackupRepository.export(
outputStream = FileOutputStream(tempFile),
append = { bytes -> tempFile.appendBytes(bytes) }
)
_state.value = _state.value.copy(statusMessage = "Export complete! Validating...")
ArchiveValidator.validate(tempFile, SignalStore.backup.messageBackupKey)
}
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe { result ->
val message = when (result) {
is ArchiveValidator.ValidationResult.ReadError -> "Failed to read backup file!"
ArchiveValidator.ValidationResult.Success -> "Validation passed!"
is ArchiveValidator.ValidationResult.ValidationError -> {
Log.w(TAG, "Validation failed!", result.exception)
"Validation failed :( Check the logs for details."
}
}
_state.value = _state.value.copy(statusMessage = message)
} }
} }
fun triggerBackupJob() { fun triggerBackupJob() {
_state.value = _state.value.copy(backupState = BackupState.EXPORT_IN_PROGRESS) _state.value = _state.value.copy(statusMessage = "Upload job in progress...")
disposables += Single.fromCallable { AppDependencies.jobManager.runSynchronously(BackupMessagesJob(), 120_000) } disposables += Single.fromCallable { AppDependencies.jobManager.runSynchronously(BackupMessagesJob(), 120_000) }
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribeBy { .subscribeBy { result ->
_state.value = _state.value.copy(backupState = BackupState.BACKUP_JOB_DONE) _state.value = _state.value.copy(statusMessage = "Upload job complete! Result: ${result.takeIf { it.isPresent }?.get() ?: "N/A"}")
} }
} }
fun import() { fun importEncryptedBackup(length: Long, inputStreamFactory: () -> InputStream) {
backupData?.let { _state.value = _state.value.copy(statusMessage = "Importing encrypted backup...")
_state.value = _state.value.copy(backupState = BackupState.IMPORT_IN_PROGRESS)
val plaintext = _state.value.plaintext
val self = Recipient.self() val self = Recipient.self()
val selfData = BackupRepository.SelfData(self.aci.get(), self.pni.get(), self.e164.get(), ProfileKey(self.profileKey)) val selfData = BackupRepository.SelfData(self.aci.get(), self.pni.get(), self.e164.get(), ProfileKey(self.profileKey))
disposables += Single.fromCallable { BackupRepository.import(it.size.toLong(), { ByteArrayInputStream(it) }, selfData, plaintext = plaintext) } disposables += Single.fromCallable { BackupRepository.import(length, inputStreamFactory, selfData, plaintext = false) }
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribeBy { .subscribeBy {
backupData = null _state.value = _state.value.copy(statusMessage = "Encrypted backup import complete!")
_state.value = _state.value.copy(backupState = BackupState.NONE)
}
}
}
fun import(length: Long, inputStreamFactory: () -> InputStream) {
_state.value = _state.value.copy(backupState = BackupState.IMPORT_IN_PROGRESS)
val plaintext = _state.value.plaintext
val self = Recipient.self()
val selfData = BackupRepository.SelfData(self.aci.get(), self.pni.get(), self.e164.get(), ProfileKey(self.profileKey))
disposables += Single.fromCallable { BackupRepository.import(length, inputStreamFactory, selfData, plaintext = plaintext) }
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribeBy {
backupData = null
_state.value = _state.value.copy(backupState = BackupState.NONE)
} }
} }
fun import(uri: Uri) { fun import(uri: Uri) {
_state.value = _state.value.copy(backupState = BackupState.IMPORT_IN_PROGRESS) _state.value = _state.value.copy(statusMessage = "Importing new-style local backup...")
val self = Recipient.self() val self = Recipient.self()
val selfData = BackupRepository.SelfData(self.aci.get(), self.pni.get(), self.e164.get(), ProfileKey(self.profileKey)) val selfData = BackupRepository.SelfData(self.aci.get(), self.pni.get(), self.e164.get(), ProfileKey(self.profileKey))
@ -170,42 +196,59 @@ class InternalBackupPlaygroundViewModel : ViewModel() {
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribeBy { .subscribeBy {
backupData = null _state.value = _state.value.copy(statusMessage = "New-style local backup import complete!")
_state.value = _state.value.copy(backupState = BackupState.NONE)
} }
} }
fun validate(length: Long, inputStreamFactory: () -> InputStream) { fun haltAllJobs() {
val self = Recipient.self() AppDependencies.jobManager.cancelAllInQueue(BackfillDigestJob.QUEUE)
val selfData = BackupRepository.SelfData(self.aci.get(), self.pni.get(), self.e164.get(), ProfileKey(self.profileKey)) AppDependencies.jobManager.cancelAllInQueue("ArchiveAttachmentJobs_0")
AppDependencies.jobManager.cancelAllInQueue("ArchiveAttachmentJobs_1")
AppDependencies.jobManager.cancelAllInQueue("ArchiveThumbnailUploadJob")
AppDependencies.jobManager.cancelAllInQueue("BackupRestoreJob")
AppDependencies.jobManager.cancelAllInQueue("__LOCAL_BACKUP__")
}
disposables += Single.fromCallable { BackupRepository.validate(length, inputStreamFactory, selfData) } fun fetchRemoteBackupAndWritePlaintext(outputStream: OutputStream?) {
.subscribeOn(Schedulers.io()) check(outputStream != null)
.observeOn(AndroidSchedulers.mainThread())
.subscribeBy { SignalExecutors.BOUNDED_IO.execute {
backupData = null Log.d(TAG, "Downloading file...")
_state.value = _state.value.copy(backupState = BackupState.NONE) val tempBackupFile = BlobProvider.getInstance().forNonAutoEncryptingSingleSessionOnDisk(AppDependencies.application)
when (val result = BackupRepository.downloadBackupFile(tempBackupFile)) {
is NetworkResult.Success -> Log.i(TAG, "Download successful")
else -> {
Log.w(TAG, "Failed to download backup file", result.getCause())
throw IOException(result.getCause())
} }
} }
fun onPlaintextToggled() { val encryptedStream = tempBackupFile.inputStream()
_state.value = _state.value.copy(plaintext = !_state.value.plaintext) val iv = encryptedStream.readNBytesOrThrow(16)
val backupKey = SignalStore.backup.messageBackupKey
val keyMaterial = backupKey.deriveBackupSecrets(Recipient.self().aci.get())
val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding").apply {
init(Cipher.DECRYPT_MODE, SecretKeySpec(keyMaterial.aesKey, "AES"), IvParameterSpec(iv))
} }
fun uploadBackupToRemote() { val plaintextStream = GZIPInputStream(
_state.value = _state.value.copy(uploadState = BackupUploadState.UPLOAD_IN_PROGRESS) CipherInputStream(
LimitedInputStream(
wrapped = encryptedStream,
maxBytes = tempBackupFile.length() - MAC_SIZE
),
cipher
)
)
disposables += Single Log.d(TAG, "Copying...")
.fromCallable { BackupRepository.debugUploadBackupFile(backupData!!.inputStream(), backupData!!.size.toLong()) is NetworkResult.Success } plaintextStream.copyTo(outputStream)
.subscribeOn(Schedulers.io()) Log.d(TAG, "Done!")
.subscribe { success ->
_state.value = _state.value.copy(uploadState = if (success) BackupUploadState.UPLOAD_DONE else BackupUploadState.UPLOAD_FAILED)
} }
} }
fun checkRemoteBackupState() { fun checkRemoteBackupState() {
_state.value = _state.value.copy(remoteBackupState = RemoteBackupState.Unknown)
disposables += Single disposables += Single
.fromCallable { .fromCallable {
BackupRepository.restoreBackupTier(SignalStore.account.requireAci()) BackupRepository.restoreBackupTier(SignalStore.account.requireAci())
@ -215,16 +258,18 @@ class InternalBackupPlaygroundViewModel : ViewModel() {
.subscribe { result -> .subscribe { result ->
when { when {
result is NetworkResult.Success -> { result is NetworkResult.Success -> {
_state.value = _state.value.copy(remoteBackupState = RemoteBackupState.Available(result.result)) _state.value = _state.value.copy(
statusMessage = "Remote backup exists. ${result.result.mediaCount} media items, using ${result.result.usedSpace} bytes (${result.result.usedSpace.bytes.inMebiBytes.roundedString(3)} MiB)"
)
} }
result is NetworkResult.StatusCodeError && result.code == 404 -> { result is NetworkResult.StatusCodeError && result.code == 404 -> {
_state.value = _state.value.copy(remoteBackupState = RemoteBackupState.NotFound) _state.value = _state.value.copy(statusMessage = "Remote backup does not exists.")
} }
else -> { else -> {
Log.w(TAG, "Error checking remote backup state", result.getCause()) Log.w(TAG, "Error checking remote backup state", result.getCause())
_state.value = _state.value.copy(remoteBackupState = RemoteBackupState.GeneralError) _state.value = _state.value.copy(statusMessage = "Failed to fetch remote backup state.")
} }
} }
} }
@ -242,7 +287,7 @@ class InternalBackupPlaygroundViewModel : ViewModel() {
} }
private fun restoreFromRemote() { private fun restoreFromRemote() {
_state.value = _state.value.copy(backupState = BackupState.IMPORT_IN_PROGRESS) _state.value = _state.value.copy(statusMessage = "Importing from remote...")
disposables += Single.fromCallable { disposables += Single.fromCallable {
AppDependencies AppDependencies
@ -255,7 +300,7 @@ class InternalBackupPlaygroundViewModel : ViewModel() {
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribeBy { .subscribeBy {
_state.value = _state.value.copy(backupState = BackupState.NONE) _state.value = _state.value.copy(statusMessage = "Import complete!")
} }
} }
@ -437,33 +482,21 @@ class InternalBackupPlaygroundViewModel : ViewModel() {
} }
data class ScreenState( data class ScreenState(
val backupState: BackupState = BackupState.NONE,
val uploadState: BackupUploadState = BackupUploadState.NONE,
val remoteBackupState: RemoteBackupState = RemoteBackupState.Unknown,
val plaintext: Boolean,
val canReadWriteBackupDirectory: Boolean = false, val canReadWriteBackupDirectory: Boolean = false,
val backupTier: MessageBackupTier? = null val backupTier: MessageBackupTier? = null,
val statusMessage: String? = null
) )
enum class BackupState(val inProgress: Boolean = false) { enum class BackupState(val inProgress: Boolean = false) {
NONE, NONE,
EXPORT_IN_PROGRESS(true), EXPORT_IN_PROGRESS(true),
EXPORT_DONE, EXPORT_DONE,
BACKUP_JOB_DONE,
IMPORT_IN_PROGRESS(true) IMPORT_IN_PROGRESS(true)
} }
enum class BackupUploadState(val inProgress: Boolean = false) {
NONE,
UPLOAD_IN_PROGRESS(true),
UPLOAD_DONE,
UPLOAD_FAILED
}
sealed class RemoteBackupState { sealed class RemoteBackupState {
object Unknown : RemoteBackupState() object Unknown : RemoteBackupState()
object NotFound : RemoteBackupState() object NotFound : RemoteBackupState()
object GeneralError : RemoteBackupState()
data class Available(val response: BackupMetadata) : RemoteBackupState() data class Available(val response: BackupMetadata) : RemoteBackupState()
} }
@ -532,52 +565,4 @@ class InternalBackupPlaygroundViewModel : ViewModel() {
fun <T> MutableState<T>.set(update: T.() -> T) { fun <T> MutableState<T>.set(update: T.() -> T) {
this.value = this.value.update() this.value = this.value.update()
} }
fun haltAllJobs() {
AppDependencies.jobManager.cancelAllInQueue(BackfillDigestJob.QUEUE)
AppDependencies.jobManager.cancelAllInQueue("ArchiveAttachmentJobs_0")
AppDependencies.jobManager.cancelAllInQueue("ArchiveAttachmentJobs_1")
AppDependencies.jobManager.cancelAllInQueue("ArchiveThumbnailUploadJob")
AppDependencies.jobManager.cancelAllInQueue("BackupRestoreJob")
AppDependencies.jobManager.cancelAllInQueue("__LOCAL_BACKUP__")
}
fun fetchRemoteBackupAndWritePlaintext(outputStream: OutputStream?) {
check(outputStream != null)
SignalExecutors.BOUNDED_IO.execute {
Log.d(TAG, "Downloading file...")
val tempBackupFile = BlobProvider.getInstance().forNonAutoEncryptingSingleSessionOnDisk(AppDependencies.application)
when (val result = BackupRepository.downloadBackupFile(tempBackupFile)) {
is NetworkResult.Success -> Log.i(TAG, "Download successful")
else -> {
Log.w(TAG, "Failed to download backup file", result.getCause())
throw IOException(result.getCause())
}
}
val encryptedStream = tempBackupFile.inputStream()
val iv = encryptedStream.readNBytesOrThrow(16)
val backupKey = SignalStore.backup.messageBackupKey
val keyMaterial = backupKey.deriveBackupSecrets(Recipient.self().aci.get())
val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding").apply {
init(Cipher.DECRYPT_MODE, SecretKeySpec(keyMaterial.aesKey, "AES"), IvParameterSpec(iv))
}
val plaintextStream = GZIPInputStream(
CipherInputStream(
LimitedInputStream(
wrapped = encryptedStream,
maxBytes = tempBackupFile.length() - MAC_SIZE
),
cipher
)
)
Log.d(TAG, "Copying...")
plaintextStream.copyTo(outputStream)
Log.d(TAG, "Done!")
}
}
} }