Improve internal backup import UI tool.

This commit is contained in:
Greyson Parrelli 2024-12-19 14:52:31 -05:00
parent fa32f399b2
commit f537fa6436
3 changed files with 55 additions and 23 deletions

View file

@ -296,7 +296,7 @@ class ArchiveImportExportTests {
length = importData.size.toLong(), length = importData.size.toLong(),
inputStreamFactory = { ByteArrayInputStream(importData) }, inputStreamFactory = { ByteArrayInputStream(importData) },
selfData = BackupRepository.SelfData(SELF_ACI, SELF_PNI, SELF_E164, ProfileKey(SELF_PROFILE_KEY)), selfData = BackupRepository.SelfData(SELF_ACI, SELF_PNI, SELF_E164, ProfileKey(SELF_PROFILE_KEY)),
plaintext = true backupKey = null
) )
} }

View file

@ -72,6 +72,7 @@ import org.signal.core.ui.Rows
import org.signal.core.ui.SignalPreview import org.signal.core.ui.SignalPreview
import org.signal.core.ui.Snackbars import org.signal.core.ui.Snackbars
import org.signal.core.ui.TextFields.TextField import org.signal.core.ui.TextFields.TextField
import org.signal.core.util.Hex
import org.signal.core.util.getLength import org.signal.core.util.getLength
import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.attachments.AttachmentId 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.compose.ComposeFragment
import org.thoughtcrime.securesms.jobs.LocalBackupJob import org.thoughtcrime.securesms.jobs.LocalBackupJob
import org.thoughtcrime.securesms.keyvalue.SignalStore import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.util.Util
class InternalBackupPlaygroundFragment : ComposeFragment() { class InternalBackupPlaygroundFragment : ComposeFragment() {
@ -200,9 +202,9 @@ class InternalBackupPlaygroundFragment : ComposeFragment() {
onImportEncryptedBackupFromDiskDismissed = { onImportEncryptedBackupFromDiskDismissed = {
viewModel.onDialogDismissed() viewModel.onDialogDismissed()
}, },
onImportEncryptedBackupFromDiskConfirmed = { aci, aep -> onImportEncryptedBackupFromDiskConfirmed = { aci, backupKey ->
viewModel.onDialogDismissed() viewModel.onDialogDismissed()
val valid = viewModel.onImportConfirmed(aci, aep) val valid = viewModel.onImportConfirmed(aci, backupKey)
if (valid) { if (valid) {
val intent = Intent().apply { val intent = Intent().apply {
action = Intent.ACTION_GET_CONTENT action = Intent.ACTION_GET_CONTENT
@ -315,8 +317,9 @@ fun Screen(
onSavePlaintextBackupToDiskClicked: () -> Unit = {}, onSavePlaintextBackupToDiskClicked: () -> Unit = {},
onImportEncryptedBackupFromDiskClicked: () -> Unit = {}, onImportEncryptedBackupFromDiskClicked: () -> Unit = {},
onImportEncryptedBackupFromDiskDismissed: () -> Unit = {}, onImportEncryptedBackupFromDiskDismissed: () -> Unit = {},
onImportEncryptedBackupFromDiskConfirmed: (aesKey: String, macKey: String) -> Unit = { _, _ -> } onImportEncryptedBackupFromDiskConfirmed: (aci: String, backupKey: String) -> Unit = { _, _ -> }
) { ) {
val context = LocalContext.current
val scrollState = rememberScrollState() val scrollState = rememberScrollState()
val options = remember { val options = remember {
mapOf( mapOf(
@ -419,6 +422,24 @@ fun Screen(
onClick = onExportNewStyleLocalBackupClicked 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() Dividers.Default()
Text( Text(
@ -479,10 +500,10 @@ fun Screen(
} }
@Composable @Composable
private fun ImportCredentialsDialog(onSubmit: (String, String) -> Unit = { _, _ -> }, onDismissed: () -> Unit = {}) { private fun ImportCredentialsDialog(onSubmit: (aci: String, backupKey: String) -> Unit = { _, _ -> }, onDismissed: () -> Unit = {}) {
val dialogScrollState = rememberScrollState() val dialogScrollState = rememberScrollState()
var aesKey by remember { mutableStateOf("") } var aci by remember { mutableStateOf("") }
var macKey by remember { mutableStateOf("") } var backupKey by remember { mutableStateOf("") }
val inputOptions = KeyboardOptions( val inputOptions = KeyboardOptions(
capitalization = KeyboardCapitalization.None, capitalization = KeyboardCapitalization.None,
autoCorrectEnabled = false, autoCorrectEnabled = false,
@ -499,27 +520,27 @@ private fun ImportCredentialsDialog(onSubmit: (String, String) -> Unit = { _, _
} }
Row(modifier = Modifier.padding(vertical = 10.dp)) { Row(modifier = Modifier.padding(vertical = 10.dp)) {
TextField( TextField(
value = aesKey, value = aci,
keyboardOptions = inputOptions, keyboardOptions = inputOptions,
label = { Text("ACI") }, label = { Text("ACI") },
supportingText = { Text("(leave blank for the current user)") }, supportingText = { Text("(leave blank for the current user)") },
onValueChange = { aesKey = it } onValueChange = { aci = it }
) )
} }
Row(modifier = Modifier.padding(vertical = 10.dp)) { Row(modifier = Modifier.padding(vertical = 10.dp)) {
TextField( TextField(
value = macKey, value = backupKey,
keyboardOptions = inputOptions.copy(imeAction = ImeAction.Done), 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)") }, supportingText = { Text("(leave blank for the current user)") },
onValueChange = { macKey = it } onValueChange = { backupKey = it }
) )
} }
} }
}, },
confirmButton = { confirmButton = {
TextButton(onClick = { TextButton(onClick = {
onSubmit(aesKey, macKey) onSubmit(aci, backupKey)
}) { }) {
Text(text = "Wipe and restore") Text(text = "Wipe and restore")
} }

View file

@ -18,10 +18,10 @@ 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.Hex
import org.signal.core.util.bytes 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.isNotNullOrBlank
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.roundedString
@ -56,9 +56,9 @@ import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.mms.IncomingMessage import org.thoughtcrime.securesms.mms.IncomingMessage
import org.thoughtcrime.securesms.providers.BlobProvider import org.thoughtcrime.securesms.providers.BlobProvider
import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.recipients.Recipient
import org.whispersystems.signalservice.api.AccountEntropyPool
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 org.whispersystems.signalservice.api.backup.MessageBackupKey
import org.whispersystems.signalservice.api.push.ServiceId.ACI import org.whispersystems.signalservice.api.push.ServiceId.ACI
import java.io.FileOutputStream import java.io.FileOutputStream
import java.io.IOException import java.io.IOException
@ -178,7 +178,7 @@ class InternalBackupPlaygroundViewModel : ViewModel() {
val self = Recipient.self() val self = Recipient.self()
val aci = customCredentials?.aci ?: self.aci.get() val aci = customCredentials?.aci ?: self.aci.get()
val selfData = BackupRepository.SelfData(aci, self.pni.get(), self.e164.get(), ProfileKey(self.profileKey)) 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) } disposables += Single.fromCallable { BackupRepository.import(length, inputStreamFactory, selfData, backupKey) }
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
@ -302,22 +302,33 @@ class InternalBackupPlaygroundViewModel : ViewModel() {
} }
/** True if data is valid, else false */ /** 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) val parsedAci: ACI? = ACI.parseOrNull(aci)
if (aci.isNotBlank() && parsedAci == null) { if (aci.isNotBlank() && parsedAci == null) {
_state.value = _state.value.copy(statusMessage = "Invalid ACI! Cannot import.") _state.value = _state.value.copy(statusMessage = "Invalid ACI! Cannot import.")
return false return false
} }
val parsedAep = AccountEntropyPool.parseOrNull(aep) val parsedBackupKey: MessageBackupKey? = try {
if (aep.isNotBlank() && parsedAep == null) { 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.") _state.value = _state.value.copy(statusMessage = "Invalid AEP! Cannot import.")
return false return false
} }
if (parsedAci != null && parsedAep != null) { _state.value = state.value.copy(
_state.value = state.value.copy(customBackupCredentials = ImportCredentials(aep = parsedAep, aci = parsedAci)) customBackupCredentials = ImportCredentials(
} messageBackupKey = parsedBackupKey ?: SignalStore.backup.messageBackupKey,
aci = parsedAci ?: SignalStore.account.aci!!
)
)
return true return true
} }
@ -602,7 +613,7 @@ class InternalBackupPlaygroundViewModel : ViewModel() {
} }
data class ImportCredentials( data class ImportCredentials(
val aep: AccountEntropyPool, val messageBackupKey: MessageBackupKey,
val aci: ACI val aci: ACI
) )
} }