From 11f64a1d1aa5ceb0abb644eb90f06f344c3a9bb4 Mon Sep 17 00:00:00 2001 From: Harris Date: Thu, 17 Jun 2021 18:29:05 +1000 Subject: [PATCH 1/6] feat: add snode method delete_all with data class for params, refactoring ClearAllDataDialog.kt to handle async requests better and prevent ANR --- .../loki/dialogs/ClearAllDataDialog.kt | 53 ++++++++++++++++--- .../org/session/libsession/snode/SnodeAPI.kt | 25 ++++++++- .../libsession/snode/SnodeDeleteMessage.kt | 30 +++++++++++ .../org/session/libsignal/utilities/Snode.kt | 3 +- 4 files changed, 102 insertions(+), 9 deletions(-) create mode 100644 libsession/src/main/java/org/session/libsession/snode/SnodeDeleteMessage.kt diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/dialogs/ClearAllDataDialog.kt b/app/src/main/java/org/thoughtcrime/securesms/loki/dialogs/ClearAllDataDialog.kt index cb1bf2256..ac18e224a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/dialogs/ClearAllDataDialog.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/loki/dialogs/ClearAllDataDialog.kt @@ -4,38 +4,77 @@ import android.app.Dialog import android.graphics.Color import android.graphics.drawable.ColorDrawable import android.os.Bundle -import androidx.fragment.app.DialogFragment -import androidx.appcompat.app.AlertDialog import android.view.LayoutInflater +import androidx.appcompat.app.AlertDialog +import androidx.core.view.isVisible +import androidx.fragment.app.DialogFragment +import androidx.lifecycle.lifecycleScope +import kotlinx.android.synthetic.main.dialog_clear_all_data.* import kotlinx.android.synthetic.main.dialog_clear_all_data.view.* +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch import network.loki.messenger.R -import org.thoughtcrime.securesms.ApplicationContext -import org.thoughtcrime.securesms.loki.protocol.MultiDeviceProtocol +import org.session.libsession.messaging.MessagingModuleConfiguration +import org.session.libsession.snode.SnodeAPI +import org.session.libsession.snode.SnodeDeleteMessage import org.session.libsession.utilities.KeyPairUtilities class ClearAllDataDialog : DialogFragment() { + var clearJob: Job? = null + set(value) { + field = value + updateUI() + } + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { val builder = AlertDialog.Builder(requireContext()) val contentView = LayoutInflater.from(requireContext()).inflate(R.layout.dialog_clear_all_data, null) contentView.cancelButton.setOnClickListener { dismiss() } contentView.clearAllDataButton.setOnClickListener { clearAllData() } builder.setView(contentView) + builder.setCancelable(false) val result = builder.create() result.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT)) return result } + private fun updateUI() { + if (clearJob?.isActive == true) { + // clear background job is running, prevent interaction + dialog?.let { view -> + view.cancelButton.isVisible = false + view.clearAllDataButton.isVisible = false + } + } else { + dialog?.let { view -> + view.cancelButton.isVisible = false + view.clearAllDataButton.isVisible = false + } + } + } + private fun clearAllData() { if (KeyPairUtilities.hasV2KeyPair(requireContext())) { - MultiDeviceProtocol.forceSyncConfigurationNowIfNeeded(requireContext()) - ApplicationContext.getInstance(context).clearAllData(false) + clearJob = lifecycleScope.launch { + delay(5_000) + // finish + val userPublicKey = MessagingModuleConfiguration.shared.storage.getUserPublicKey() + + + val deleteMessage = SnodeDeleteMessage(userKey, System.currentTimeMillis(), ) + SnodeAPI.deleteAllMessages() + // TODO: re-add the clear data here + //ApplicationContext.getInstance(context).clearAllData(false) + } } else { val dialog = AlertDialog.Builder(requireContext()) val message = "We’ve upgraded the way Session IDs are generated, so you will be unable to restore your current Session ID." dialog.setMessage(message) dialog.setPositiveButton("Yes") { _, _ -> - ApplicationContext.getInstance(context).clearAllData(false) + // TODO: re-add the clear data here + // ApplicationContext.getInstance(context).clearAllData(false) } dialog.setNegativeButton("Cancel") { _, _ -> // Do nothing diff --git a/libsession/src/main/java/org/session/libsession/snode/SnodeAPI.kt b/libsession/src/main/java/org/session/libsession/snode/SnodeAPI.kt index f126871a4..32c221b0e 100644 --- a/libsession/src/main/java/org/session/libsession/snode/SnodeAPI.kt +++ b/libsession/src/main/java/org/session/libsession/snode/SnodeAPI.kt @@ -6,7 +6,6 @@ import android.os.Build import com.goterl.lazysodium.LazySodiumAndroid import com.goterl.lazysodium.SodiumAndroid import com.goterl.lazysodium.exceptions.SodiumException -import com.goterl.lazysodium.interfaces.AEAD import com.goterl.lazysodium.interfaces.GenericHash import com.goterl.lazysodium.interfaces.PwHash import com.goterl.lazysodium.interfaces.SecretBox @@ -298,6 +297,30 @@ object SnodeAPI { } } + /** Deletes all messages owned by the given pubkey on this SN and broadcasts the delete request to + * all other swarm members. + * Returns dict of: + * - "swarms" dict mapping ed25519 pubkeys (in hex) of swarm members to dict values of: + * - "failed" and other failure keys -- see `recursive`. + * - "deleted": hashes of deleted messages. + * - "signature": signature of ( PUBKEY_HEX || TIMESTAMP || DELETEDHASH[0] || ... || DELETEDHASH[N] ), signed + * by the node's ed25519 pubkey. + */ + fun deleteAllMessages(deleteMessage: SnodeDeleteMessage): Promise, Exception> { + // considerations: timestamp off in retrying logic, not being able to re-sign with latest timestamp? do we just not retry this as it will be synchronous + val destination = if (useTestnet) deleteMessage.pubKey.removing05PrefixIfNeeded() else deleteMessage.pubKey + return retryIfNeeded(maxRetryCount) { + getTargetSnodes(destination).map { swarm -> + swarm.map { snode -> + val parameters = deleteMessage.toJSON() + retryIfNeeded(maxRetryCount) { + invoke(Snode.Method.DeleteAll, snode, destination, parameters) + } + }.toSet() + } + } + } + // Parsing private fun parseSnodes(rawResponse: Any): List { val json = rawResponse as? Map<*, *> diff --git a/libsession/src/main/java/org/session/libsession/snode/SnodeDeleteMessage.kt b/libsession/src/main/java/org/session/libsession/snode/SnodeDeleteMessage.kt new file mode 100644 index 000000000..a9f11fef0 --- /dev/null +++ b/libsession/src/main/java/org/session/libsession/snode/SnodeDeleteMessage.kt @@ -0,0 +1,30 @@ +package org.session.libsession.snode + +import org.session.libsignal.utilities.removing05PrefixIfNeeded + +data class SnodeDeleteMessage( + /** + * The hex encoded public key of the user. + */ + val pubKey: String, + /** + * The timestamp at which this request was initiated, in milliseconds since unix epoch. + * Must be within Must be within ±60s of the current time. + * (For clients it is recommended to retrieve a timestamp via `info` first, to avoid client time sync issues). + */ + val timestamp: Long, + /** + * an Ed25519 signature of ( "delete_all" || timestamp ), signed by the ed25519 + */ + val signature: String, +) { + + internal fun toJSON(): Map { + return mapOf( + "pubkey" to if (SnodeAPI.useTestnet) pubKey.removing05PrefixIfNeeded() else pubKey, + "timestamp" to timestamp.toString(), + "signature" to signature + ) + } + +} \ No newline at end of file diff --git a/libsignal/src/main/java/org/session/libsignal/utilities/Snode.kt b/libsignal/src/main/java/org/session/libsignal/utilities/Snode.kt index 92c30095e..251116f31 100644 --- a/libsignal/src/main/java/org/session/libsignal/utilities/Snode.kt +++ b/libsignal/src/main/java/org/session/libsignal/utilities/Snode.kt @@ -7,7 +7,8 @@ class Snode(val address: String, val port: Int, val publicKeySet: KeySet?) { GetSwarm("get_snodes_for_pubkey"), GetMessages("retrieve"), SendMessage("store"), - OxenDaemonRPCCall("oxend_request") + OxenDaemonRPCCall("oxend_request"), + DeleteAll("delete_all") } data class KeySet(val ed25519Key: String, val x25519Key: String) From fdc042e6d40af34f1ce01e0c1a5c967f23ea2b33 Mon Sep 17 00:00:00 2001 From: jubb Date: Fri, 18 Jun 2021 16:01:34 +1000 Subject: [PATCH 2/6] feat: testnet clearing network data on delete and differentiating dialogs --- .../securesms/database/Storage.kt | 6 -- .../loki/activities/SettingsActivity.kt | 8 +- .../loki/dialogs/ClearAllDataDialog.kt | 96 +++++++++++++------ .../loki/protocol/MultiDeviceProtocol.kt | 10 +- app/src/main/res/layout/activity_settings.xml | 10 ++ .../main/res/layout/dialog_clear_all_data.xml | 18 +++- app/src/main/res/values/strings.xml | 1 + .../xml/network_security_configuration.xml | 2 +- .../libsession/database/StorageProtocol.kt | 1 - .../messaging/open_groups/OpenGroupAPIV2.kt | 4 +- .../open_groups/OpenGroupMessageV2.kt | 7 +- .../sending_receiving/MessageEncrypter.kt | 2 +- .../org/session/libsession/snode/SnodeAPI.kt | 46 +++++++-- .../libsession/snode/SnodeDeleteMessage.kt | 8 +- .../org/session/libsignal/utilities/Snode.kt | 1 + 15 files changed, 156 insertions(+), 64 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt b/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt index 1c67eac4e..07ab9e20c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt @@ -42,12 +42,6 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context, return TextSecurePreferences.getLocalNumber(context) } - override fun getUserKeyPair(): Pair? { - val userPublicKey = TextSecurePreferences.getLocalNumber(context) ?: return null - val userPrivateKey = IdentityKeyUtil.getIdentityKeyPair(context).privateKey.serialize() - return Pair(userPublicKey, userPrivateKey) - } - override fun getUserX25519KeyPair(): ECKeyPair { return DatabaseFactory.getLokiAPIDatabase(context).getUserX25519KeyPair() } diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/activities/SettingsActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/loki/activities/SettingsActivity.kt index ac342a707..af834f18d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/activities/SettingsActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/loki/activities/SettingsActivity.kt @@ -92,6 +92,7 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() { helpTranslateButton.setOnClickListener { helpTranslate() } seedButton.setOnClickListener { showSeed() } clearAllDataButton.setOnClickListener { clearAllData() } + clearAllDataAndNetworkButton.setOnClickListener { clearAllDataIncludingNetwork() } versionTextView.text = String.format(getString(R.string.version_s), "${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})") } @@ -302,8 +303,13 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() { } private fun clearAllData() { - ClearAllDataDialog().show(supportFragmentManager, "Clear All Data Dialog") + ClearAllDataDialog(deleteNetworkMessages = false).show(supportFragmentManager, "Clear All Data Dialog") } + + private fun clearAllDataIncludingNetwork() { + ClearAllDataDialog(deleteNetworkMessages = true).show(supportFragmentManager, "Clear All Data Dialog") + } + // endregion private inner class DisplayNameEditActionModeCallback: ActionMode.Callback { diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/dialogs/ClearAllDataDialog.kt b/app/src/main/java/org/thoughtcrime/securesms/loki/dialogs/ClearAllDataDialog.kt index ac18e224a..2e34f83b7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/dialogs/ClearAllDataDialog.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/loki/dialogs/ClearAllDataDialog.kt @@ -1,6 +1,7 @@ package org.thoughtcrime.securesms.loki.dialogs import android.app.Dialog +import android.content.DialogInterface import android.graphics.Color import android.graphics.drawable.ColorDrawable import android.os.Bundle @@ -11,22 +12,23 @@ import androidx.fragment.app.DialogFragment import androidx.lifecycle.lifecycleScope import kotlinx.android.synthetic.main.dialog_clear_all_data.* import kotlinx.android.synthetic.main.dialog_clear_all_data.view.* -import kotlinx.coroutines.Job -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch +import kotlinx.coroutines.* import network.loki.messenger.R +import nl.komponents.kovenant.Promise import org.session.libsession.messaging.MessagingModuleConfiguration import org.session.libsession.snode.SnodeAPI -import org.session.libsession.snode.SnodeDeleteMessage import org.session.libsession.utilities.KeyPairUtilities +import org.session.libsignal.utilities.Log +import org.thoughtcrime.securesms.ApplicationContext +import org.thoughtcrime.securesms.loki.protocol.MultiDeviceProtocol +import java.util.concurrent.Executors -class ClearAllDataDialog : DialogFragment() { +class ClearAllDataDialog(val deleteNetworkMessages: Boolean) : DialogFragment() { var clearJob: Job? = null - set(value) { - field = value - updateUI() - } + set(value) { + field = value + } override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { val builder = AlertDialog.Builder(requireContext()) @@ -40,41 +42,73 @@ class ClearAllDataDialog : DialogFragment() { return result } - private fun updateUI() { - if (clearJob?.isActive == true) { - // clear background job is running, prevent interaction - dialog?.let { view -> - view.cancelButton.isVisible = false - view.clearAllDataButton.isVisible = false - } - } else { - dialog?.let { view -> - view.cancelButton.isVisible = false - view.clearAllDataButton.isVisible = false - } + override fun onDismiss(dialog: DialogInterface) { + super.onDismiss(dialog) + } + + override fun onStart() { + super.onStart() + isCancelable = false + dialog?.setCanceledOnTouchOutside(false) + } + + private fun updateUI(isLoading: Boolean) { + dialog?.let { view -> + view.cancelButton.isVisible = !isLoading + view.clearAllDataButton.isVisible = !isLoading + view.progressBar.isVisible = isLoading } } private fun clearAllData() { if (KeyPairUtilities.hasV2KeyPair(requireContext())) { - clearJob = lifecycleScope.launch { - delay(5_000) - // finish - val userPublicKey = MessagingModuleConfiguration.shared.storage.getUserPublicKey() + clearJob = lifecycleScope.launch(Dispatchers.IO) { + withContext(Dispatchers.Main) { + updateUI(true) + } + if (!deleteNetworkMessages) { + try { + MultiDeviceProtocol.forceSyncConfigurationNowIfNeeded(requireContext()).get() + ApplicationContext.getInstance(context).clearAllData(false) + withContext(Dispatchers.Main) { + dismiss() + } + } catch (e: Exception) { + Log.e("Loki", "Failed to force sync", e) + } + } else { + // finish + val promises = SnodeAPI.deleteAllMessages(requireContext()).get() - val deleteMessage = SnodeDeleteMessage(userKey, System.currentTimeMillis(), ) - SnodeAPI.deleteAllMessages() - // TODO: re-add the clear data here - //ApplicationContext.getInstance(context).clearAllData(false) + val rawResponses = promises.map { + try { + it.get() + } catch (e: Exception) { + null + } + } + // TODO: process the responses here + if (rawResponses.any { it == null || it["failed"] as? Boolean == true }) { + // didn't succeed (at least one) + withContext(Dispatchers.Main) { + updateUI(false) + } + } else { + // don't force sync because all the messages are deleted? + ApplicationContext.getInstance(context).clearAllData(false) + withContext(Dispatchers.Main) { + dismiss() + } + } + } } } else { val dialog = AlertDialog.Builder(requireContext()) val message = "We’ve upgraded the way Session IDs are generated, so you will be unable to restore your current Session ID." dialog.setMessage(message) dialog.setPositiveButton("Yes") { _, _ -> - // TODO: re-add the clear data here - // ApplicationContext.getInstance(context).clearAllData(false) + ApplicationContext.getInstance(context).clearAllData(false) } dialog.setNegativeButton("Cancel") { _, _ -> // Do nothing diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/protocol/MultiDeviceProtocol.kt b/app/src/main/java/org/thoughtcrime/securesms/loki/protocol/MultiDeviceProtocol.kt index e9e88215e..29fb98a89 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/protocol/MultiDeviceProtocol.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/loki/protocol/MultiDeviceProtocol.kt @@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.loki.protocol import android.content.Context import com.google.protobuf.ByteString +import nl.komponents.kovenant.Promise import org.session.libsession.messaging.MessagingModuleConfiguration import org.session.libsession.messaging.messages.Destination import org.session.libsession.messaging.messages.control.ConfigurationMessage @@ -28,16 +29,17 @@ object MultiDeviceProtocol { TextSecurePreferences.setLastConfigurationSyncTime(context, now) } - fun forceSyncConfigurationNowIfNeeded(context: Context) { - val userPublicKey = TextSecurePreferences.getLocalNumber(context) ?: return + fun forceSyncConfigurationNowIfNeeded(context: Context): Promise { + val userPublicKey = TextSecurePreferences.getLocalNumber(context) ?: return Promise.ofSuccess(Unit) val contacts = ContactUtilities.getAllContacts(context).filter { recipient -> !recipient.isGroupRecipient && !recipient.isBlocked && !recipient.name.isNullOrEmpty() && !recipient.isLocalNumber && recipient.address.serialize().isNotEmpty() }.map { recipient -> ConfigurationMessage.Contact(recipient.address.serialize(), recipient.name!!, recipient.profileAvatar, recipient.profileKey) } - val configurationMessage = ConfigurationMessage.getCurrent(contacts) ?: return - MessageSender.send(configurationMessage, Destination.from(Address.fromSerialized(userPublicKey))) + val configurationMessage = ConfigurationMessage.getCurrent(contacts) ?: return Promise.ofSuccess(Unit) + val promise = MessageSender.send(configurationMessage, Destination.from(Address.fromSerialized(userPublicKey))) TextSecurePreferences.setLastConfigurationSyncTime(context, System.currentTimeMillis()) + return promise } } \ No newline at end of file diff --git a/app/src/main/res/layout/activity_settings.xml b/app/src/main/res/layout/activity_settings.xml index bafea5146..5b655b9fe 100644 --- a/app/src/main/res/layout/activity_settings.xml +++ b/app/src/main/res/layout/activity_settings.xml @@ -185,6 +185,16 @@ android:textStyle="bold" android:gravity="center" android:text="@string/activity_settings_clear_all_data_button_title" /> + -