From f537fa64366a07818d9a893b5b9b4dae3a7972fd Mon Sep 17 00:00:00 2001 From: Greyson Parrelli Date: Thu, 19 Dec 2024 14:52:31 -0500 Subject: [PATCH] Improve internal backup import UI tool. --- .../backup/v2/ArchiveImportExportTests.kt | 2 +- .../InternalBackupPlaygroundFragment.kt | 45 ++++++++++++++----- .../InternalBackupPlaygroundViewModel.kt | 31 ++++++++----- 3 files changed, 55 insertions(+), 23 deletions(-) diff --git a/app/src/androidTest/java/org/thoughtcrime/securesms/backup/v2/ArchiveImportExportTests.kt b/app/src/androidTest/java/org/thoughtcrime/securesms/backup/v2/ArchiveImportExportTests.kt index 1427e86994..06c3c4fd74 100644 --- a/app/src/androidTest/java/org/thoughtcrime/securesms/backup/v2/ArchiveImportExportTests.kt +++ b/app/src/androidTest/java/org/thoughtcrime/securesms/backup/v2/ArchiveImportExportTests.kt @@ -296,7 +296,7 @@ class ArchiveImportExportTests { length = importData.size.toLong(), inputStreamFactory = { ByteArrayInputStream(importData) }, selfData = BackupRepository.SelfData(SELF_ACI, SELF_PNI, SELF_E164, ProfileKey(SELF_PROFILE_KEY)), - plaintext = true + backupKey = null ) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/backup/InternalBackupPlaygroundFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/backup/InternalBackupPlaygroundFragment.kt index 2219953a94..02db0580b0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/backup/InternalBackupPlaygroundFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/backup/InternalBackupPlaygroundFragment.kt @@ -72,6 +72,7 @@ import org.signal.core.ui.Rows import org.signal.core.ui.SignalPreview import org.signal.core.ui.Snackbars import org.signal.core.ui.TextFields.TextField +import org.signal.core.util.Hex import org.signal.core.util.getLength import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.attachments.AttachmentId @@ -81,6 +82,7 @@ import org.thoughtcrime.securesms.components.settings.app.internal.backup.Intern import org.thoughtcrime.securesms.compose.ComposeFragment import org.thoughtcrime.securesms.jobs.LocalBackupJob import org.thoughtcrime.securesms.keyvalue.SignalStore +import org.thoughtcrime.securesms.util.Util class InternalBackupPlaygroundFragment : ComposeFragment() { @@ -200,9 +202,9 @@ class InternalBackupPlaygroundFragment : ComposeFragment() { onImportEncryptedBackupFromDiskDismissed = { viewModel.onDialogDismissed() }, - onImportEncryptedBackupFromDiskConfirmed = { aci, aep -> + onImportEncryptedBackupFromDiskConfirmed = { aci, backupKey -> viewModel.onDialogDismissed() - val valid = viewModel.onImportConfirmed(aci, aep) + val valid = viewModel.onImportConfirmed(aci, backupKey) if (valid) { val intent = Intent().apply { action = Intent.ACTION_GET_CONTENT @@ -315,8 +317,9 @@ fun Screen( onSavePlaintextBackupToDiskClicked: () -> Unit = {}, onImportEncryptedBackupFromDiskClicked: () -> Unit = {}, onImportEncryptedBackupFromDiskDismissed: () -> Unit = {}, - onImportEncryptedBackupFromDiskConfirmed: (aesKey: String, macKey: String) -> Unit = { _, _ -> } + onImportEncryptedBackupFromDiskConfirmed: (aci: String, backupKey: String) -> Unit = { _, _ -> } ) { + val context = LocalContext.current val scrollState = rememberScrollState() val options = remember { mapOf( @@ -419,6 +422,24 @@ fun Screen( onClick = onExportNewStyleLocalBackupClicked ) + Rows.TextRow( + text = "Copy Account Entropy Pool (AEP)", + label = "Copies the Account Entropy Pool (AEP) to the clipboard, which is labeled as the \"Backup Key\" in the designs.", + onClick = { + Util.copyToClipboard(context, SignalStore.account.accountEntropyPool.value) + Toast.makeText(context, "Copied!", Toast.LENGTH_SHORT).show() + } + ) + + Rows.TextRow( + text = "Copy Cryptographic BackupKey", + label = "Copies the cryptographic BackupKey to the clipboard as a hex string. Important: this is the key that is derived from the AEP, and therefore *not* the same as the key labeled \"Backup Key\" in the designs. That's actually the AEP, listed above.", + onClick = { + Util.copyToClipboard(context, Hex.toStringCondensed(SignalStore.account.accountEntropyPool.deriveMessageBackupKey().value)) + Toast.makeText(context, "Copied!", Toast.LENGTH_SHORT).show() + } + ) + Dividers.Default() Text( @@ -479,10 +500,10 @@ fun Screen( } @Composable -private fun ImportCredentialsDialog(onSubmit: (String, String) -> Unit = { _, _ -> }, onDismissed: () -> Unit = {}) { +private fun ImportCredentialsDialog(onSubmit: (aci: String, backupKey: String) -> Unit = { _, _ -> }, onDismissed: () -> Unit = {}) { val dialogScrollState = rememberScrollState() - var aesKey by remember { mutableStateOf("") } - var macKey by remember { mutableStateOf("") } + var aci by remember { mutableStateOf("") } + var backupKey by remember { mutableStateOf("") } val inputOptions = KeyboardOptions( capitalization = KeyboardCapitalization.None, autoCorrectEnabled = false, @@ -499,27 +520,27 @@ private fun ImportCredentialsDialog(onSubmit: (String, String) -> Unit = { _, _ } Row(modifier = Modifier.padding(vertical = 10.dp)) { TextField( - value = aesKey, + value = aci, keyboardOptions = inputOptions, label = { Text("ACI") }, supportingText = { Text("(leave blank for the current user)") }, - onValueChange = { aesKey = it } + onValueChange = { aci = it } ) } Row(modifier = Modifier.padding(vertical = 10.dp)) { TextField( - value = macKey, + value = backupKey, keyboardOptions = inputOptions.copy(imeAction = ImeAction.Done), - label = { Text("\"Backup Key\" (AEP)") }, + label = { Text("Cryptographic BackupKey (*not* AEP!)") }, supportingText = { Text("(leave blank for the current user)") }, - onValueChange = { macKey = it } + onValueChange = { backupKey = it } ) } } }, confirmButton = { TextButton(onClick = { - onSubmit(aesKey, macKey) + onSubmit(aci, backupKey) }) { Text(text = "Wipe and restore") } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/backup/InternalBackupPlaygroundViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/backup/InternalBackupPlaygroundViewModel.kt index f379008b6d..0ed120c392 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/backup/InternalBackupPlaygroundViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/backup/InternalBackupPlaygroundViewModel.kt @@ -18,10 +18,10 @@ import io.reactivex.rxjava3.disposables.CompositeDisposable import io.reactivex.rxjava3.kotlin.plusAssign import io.reactivex.rxjava3.kotlin.subscribeBy import io.reactivex.rxjava3.schedulers.Schedulers +import org.signal.core.util.Hex import org.signal.core.util.bytes import org.signal.core.util.concurrent.SignalExecutors import org.signal.core.util.copyTo -import org.signal.core.util.isNotNullOrBlank import org.signal.core.util.logging.Log import org.signal.core.util.readNBytesOrThrow import org.signal.core.util.roundedString @@ -56,9 +56,9 @@ import org.thoughtcrime.securesms.keyvalue.SignalStore import org.thoughtcrime.securesms.mms.IncomingMessage import org.thoughtcrime.securesms.providers.BlobProvider import org.thoughtcrime.securesms.recipients.Recipient -import org.whispersystems.signalservice.api.AccountEntropyPool import org.whispersystems.signalservice.api.NetworkResult import org.whispersystems.signalservice.api.backup.MediaName +import org.whispersystems.signalservice.api.backup.MessageBackupKey import org.whispersystems.signalservice.api.push.ServiceId.ACI import java.io.FileOutputStream import java.io.IOException @@ -178,7 +178,7 @@ class InternalBackupPlaygroundViewModel : ViewModel() { val self = Recipient.self() val aci = customCredentials?.aci ?: self.aci.get() val selfData = BackupRepository.SelfData(aci, self.pni.get(), self.e164.get(), ProfileKey(self.profileKey)) - val backupKey = customCredentials?.aep?.deriveMessageBackupKey() ?: SignalStore.backup.messageBackupKey + val backupKey = customCredentials?.messageBackupKey ?: SignalStore.backup.messageBackupKey disposables += Single.fromCallable { BackupRepository.import(length, inputStreamFactory, selfData, backupKey) } .subscribeOn(Schedulers.io()) @@ -302,22 +302,33 @@ class InternalBackupPlaygroundViewModel : ViewModel() { } /** True if data is valid, else false */ - fun onImportConfirmed(aci: String, aep: String): Boolean { + fun onImportConfirmed(aci: String, backupKey: String): Boolean { val parsedAci: ACI? = ACI.parseOrNull(aci) + if (aci.isNotBlank() && parsedAci == null) { _state.value = _state.value.copy(statusMessage = "Invalid ACI! Cannot import.") return false } - val parsedAep = AccountEntropyPool.parseOrNull(aep) - if (aep.isNotBlank() && parsedAep == null) { + val parsedBackupKey: MessageBackupKey? = try { + val bytes = Hex.fromStringOrThrow(backupKey) + MessageBackupKey(bytes) + } catch (e: Exception) { + Log.w(TAG, "Failed to parse key!", e) + null + } + + if (backupKey.isNotBlank() && parsedBackupKey == null) { _state.value = _state.value.copy(statusMessage = "Invalid AEP! Cannot import.") return false } - if (parsedAci != null && parsedAep != null) { - _state.value = state.value.copy(customBackupCredentials = ImportCredentials(aep = parsedAep, aci = parsedAci)) - } + _state.value = state.value.copy( + customBackupCredentials = ImportCredentials( + messageBackupKey = parsedBackupKey ?: SignalStore.backup.messageBackupKey, + aci = parsedAci ?: SignalStore.account.aci!! + ) + ) return true } @@ -602,7 +613,7 @@ class InternalBackupPlaygroundViewModel : ViewModel() { } data class ImportCredentials( - val aep: AccountEntropyPool, + val messageBackupKey: MessageBackupKey, val aci: ACI ) }