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(),
inputStreamFactory = { ByteArrayInputStream(importData) },
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.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")
}

View file

@ -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
)
}