Merge pull request #652 from oxen-io/security

Add Option to Delete All Network Data
This commit is contained in:
Niels Andriesse 2021-07-12 14:28:42 +10:00 committed by GitHub
commit 7eae15594b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 228 additions and 48 deletions

View File

@ -39,12 +39,6 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
return TextSecurePreferences.getLocalNumber(context) return TextSecurePreferences.getLocalNumber(context)
} }
override fun getUserKeyPair(): Pair<String, ByteArray>? {
val userPublicKey = TextSecurePreferences.getLocalNumber(context) ?: return null
val userPrivateKey = IdentityKeyUtil.getIdentityKeyPair(context).privateKey.serialize()
return Pair(userPublicKey, userPrivateKey)
}
override fun getUserX25519KeyPair(): ECKeyPair { override fun getUserX25519KeyPair(): ECKeyPair {
return DatabaseFactory.getLokiAPIDatabase(context).getUserX25519KeyPair() return DatabaseFactory.getLokiAPIDatabase(context).getUserX25519KeyPair()
} }

View File

@ -2,37 +2,126 @@ package org.thoughtcrime.securesms.preferences
import android.view.LayoutInflater import android.view.LayoutInflater
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.core.view.isVisible
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.android.synthetic.main.dialog_clear_all_data.view.*
import kotlinx.coroutines.*
import network.loki.messenger.R import network.loki.messenger.R
import org.session.libsession.snode.SnodeAPI
import org.session.libsignal.utilities.Log
import org.thoughtcrime.securesms.ApplicationContext import org.thoughtcrime.securesms.ApplicationContext
import org.thoughtcrime.securesms.crypto.KeyPairUtilities
import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities
import org.thoughtcrime.securesms.conversation.v2.utilities.BaseDialog import org.thoughtcrime.securesms.conversation.v2.utilities.BaseDialog
class ClearAllDataDialog : BaseDialog() { class ClearAllDataDialog : BaseDialog() {
override fun setContentView(builder: AlertDialog.Builder) { enum class Steps {
val contentView = LayoutInflater.from(requireContext()).inflate(R.layout.dialog_clear_all_data, null) INFO_PROMPT,
contentView.cancelButton.setOnClickListener { dismiss() } NETWORK_PROMPT,
contentView.clearAllDataButton.setOnClickListener { clearAllData() } DELETING
builder.setView(contentView)
} }
private fun clearAllData() { var clearJob: Job? = null
if (KeyPairUtilities.hasV2KeyPair(requireContext())) { set(value) {
ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(requireContext()) field = value
}
var step = Steps.INFO_PROMPT
set(value) {
field = value
updateUI()
}
override fun setContentView(builder: AlertDialog.Builder) {
val contentView = LayoutInflater.from(requireContext()).inflate(R.layout.dialog_clear_all_data, null)
contentView.cancelButton.setOnClickListener {
if (step == Steps.NETWORK_PROMPT) {
clearAllData(false)
} else if (step != Steps.DELETING) {
dismiss()
}
}
contentView.clearAllDataButton.setOnClickListener {
when(step) {
Steps.INFO_PROMPT -> step = Steps.NETWORK_PROMPT
Steps.NETWORK_PROMPT -> {
clearAllData(true)
}
Steps.DELETING -> { /* do nothing intentionally */ }
}
}
builder.setView(contentView)
builder.setCancelable(false)
}
private fun updateUI() {
dialog?.let { view ->
val isLoading = step == Steps.DELETING
when (step) {
Steps.INFO_PROMPT -> {
view.dialogDescriptionText.setText(R.string.dialog_clear_all_data_explanation)
view.cancelButton.setText(R.string.cancel)
view.clearAllDataButton.setText(R.string.delete)
}
else -> {
view.dialogDescriptionText.setText(R.string.dialog_clear_all_data_network_explanation)
view.cancelButton.setText(R.string.dialog_clear_all_data_local_only)
view.clearAllDataButton.setText(R.string.dialog_clear_all_data_clear_network)
}
}
view.cancelButton.isVisible = !isLoading
view.clearAllDataButton.isVisible = !isLoading
view.progressBar.isVisible = isLoading
view.setCanceledOnTouchOutside(!isLoading)
isCancelable = !isLoading
}
}
private fun clearAllData(deleteNetworkMessages: Boolean) {
clearJob = lifecycleScope.launch(Dispatchers.IO) {
val previousStep = step
withContext(Dispatchers.Main) {
step = Steps.DELETING
}
if (!deleteNetworkMessages) {
try {
ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(requireContext()).get()
} catch (e: Exception) {
Log.e("Loki", "Failed to force sync", e)
}
ApplicationContext.getInstance(context).clearAllData(false) ApplicationContext.getInstance(context).clearAllData(false)
withContext(Dispatchers.Main) {
dismiss()
}
} else { } else {
val dialog = AlertDialog.Builder(requireContext()) // finish
val message = "Weve upgraded the way Session IDs are generated, so you will be unable to restore your current Session ID." val result = try {
dialog.setMessage(message) SnodeAPI.deleteAllMessages(requireContext()).get()
dialog.setPositiveButton("Yes") { _, _ -> } catch (e: Exception) {
null
}
if (result == null || result.values.any { !it } || result.isEmpty()) {
// didn't succeed (at least one)
withContext(Dispatchers.Main) {
step = previousStep
}
} else if (result.values.all { it }) {
// don't force sync because all the messages are deleted?
ApplicationContext.getInstance(context).clearAllData(false) ApplicationContext.getInstance(context).clearAllData(false)
withContext(Dispatchers.Main) {
dismiss()
}
} }
dialog.setNegativeButton("Cancel") { _, _ ->
// Do nothing
} }
dialog.create().show()
} }
} }
} }

View File

@ -303,6 +303,7 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
private fun clearAllData() { private fun clearAllData() {
ClearAllDataDialog().show(supportFragmentManager, "Clear All Data Dialog") ClearAllDataDialog().show(supportFragmentManager, "Clear All Data Dialog")
} }
// endregion // endregion
private inner class DisplayNameEditActionModeCallback: ActionMode.Callback { private inner class DisplayNameEditActionModeCallback: ActionMode.Callback {

View File

@ -1,6 +1,7 @@
package org.thoughtcrime.securesms.util package org.thoughtcrime.securesms.util
import android.content.Context import android.content.Context
import nl.komponents.kovenant.Promise
import org.session.libsession.messaging.messages.Destination import org.session.libsession.messaging.messages.Destination
import org.session.libsession.messaging.messages.control.ConfigurationMessage import org.session.libsession.messaging.messages.control.ConfigurationMessage
import org.session.libsession.messaging.sending_receiving.MessageSender import org.session.libsession.messaging.sending_receiving.MessageSender
@ -25,16 +26,17 @@ object ConfigurationMessageUtilities {
TextSecurePreferences.setLastConfigurationSyncTime(context, now) TextSecurePreferences.setLastConfigurationSyncTime(context, now)
} }
fun forceSyncConfigurationNowIfNeeded(context: Context) { fun forceSyncConfigurationNowIfNeeded(context: Context): Promise<Unit, Exception> {
val userPublicKey = TextSecurePreferences.getLocalNumber(context) ?: return val userPublicKey = TextSecurePreferences.getLocalNumber(context) ?: return Promise.ofSuccess(Unit)
val contacts = ContactUtilities.getAllContacts(context).filter { recipient -> val contacts = ContactUtilities.getAllContacts(context).filter { recipient ->
!recipient.isGroupRecipient && !recipient.isBlocked && !recipient.name.isNullOrEmpty() && !recipient.isLocalNumber && recipient.address.serialize().isNotEmpty() !recipient.isGroupRecipient && !recipient.isBlocked && !recipient.name.isNullOrEmpty() && !recipient.isLocalNumber && recipient.address.serialize().isNotEmpty()
}.map { recipient -> }.map { recipient ->
ConfigurationMessage.Contact(recipient.address.serialize(), recipient.name!!, recipient.profileAvatar, recipient.profileKey) ConfigurationMessage.Contact(recipient.address.serialize(), recipient.name!!, recipient.profileAvatar, recipient.profileKey)
} }
val configurationMessage = ConfigurationMessage.getCurrent(contacts) ?: return val configurationMessage = ConfigurationMessage.getCurrent(contacts) ?: return Promise.ofSuccess(Unit)
MessageSender.send(configurationMessage, Destination.from(Address.fromSerialized(userPublicKey))) val promise = MessageSender.send(configurationMessage, Destination.from(Address.fromSerialized(userPublicKey)))
TextSecurePreferences.setLastConfigurationSyncTime(context, System.currentTimeMillis()) TextSecurePreferences.setLastConfigurationSyncTime(context, System.currentTimeMillis())
return promise
} }
} }

View File

@ -1,8 +1,9 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<LinearLayout <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:background="@drawable/default_dialog_background_inset" android:background="@drawable/default_dialog_background_inset"
android:gravity="center_horizontal" android:gravity="center_horizontal"
android:orientation="vertical" android:orientation="vertical"
@ -18,7 +19,7 @@
android:textSize="@dimen/medium_font_size" /> android:textSize="@dimen/medium_font_size" />
<TextView <TextView
android:id="@+id/seedTextView" android:id="@+id/dialogDescriptionText"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="@dimen/large_spacing" android:layout_marginTop="@dimen/large_spacing"
@ -50,6 +51,17 @@
android:layout_marginStart="@dimen/medium_spacing" android:layout_marginStart="@dimen/medium_spacing"
android:text="@string/delete" /> android:text="@string/delete" />
<com.github.ybq.android.spinkit.SpinKitView
style="@style/SpinKitView.Small.ThreeBounce"
android:layout_marginVertical="@dimen/small_spacing"
android:id="@+id/progressBar"
android:layout_width="0dp"
android:layout_height="@dimen/small_button_height"
android:layout_weight="1"
app:SpinKit_Color="@color/accent"
android:visibility="gone"
/>
</LinearLayout> </LinearLayout>
</LinearLayout> </LinearLayout>

View File

@ -755,6 +755,7 @@
<string name="activity_settings_invite_button_title">Invite</string> <string name="activity_settings_invite_button_title">Invite</string>
<string name="activity_settings_recovery_phrase_button_title">Recovery Phrase</string> <string name="activity_settings_recovery_phrase_button_title">Recovery Phrase</string>
<string name="activity_settings_clear_all_data_button_title">Clear Data</string> <string name="activity_settings_clear_all_data_button_title">Clear Data</string>
<string name="activity_settings_clear_all_data_and_network_button_title">Clear Data Including Network</string>
<string name="activity_settings_help_translate_session">Help us Translate Session</string> <string name="activity_settings_help_translate_session">Help us Translate Session</string>
<string name="activity_notification_settings_title">Notifications</string> <string name="activity_notification_settings_title">Notifications</string>
@ -777,6 +778,9 @@
<string name="dialog_clear_all_data_title">Clear All Data</string> <string name="dialog_clear_all_data_title">Clear All Data</string>
<string name="dialog_clear_all_data_explanation">This will permanently delete your messages, sessions, and contacts.</string> <string name="dialog_clear_all_data_explanation">This will permanently delete your messages, sessions, and contacts.</string>
<string name="dialog_clear_all_data_network_explanation">Would you like to clear only this device, or delete your entire account?</string>
<string name="dialog_clear_all_data_local_only">Delete Only</string>
<string name="dialog_clear_all_data_clear_network">Entire Account</string>
<string name="activity_qr_code_title">QR Code</string> <string name="activity_qr_code_title">QR Code</string>
<string name="activity_qr_code_view_my_qr_code_tab_title">View My QR Code</string> <string name="activity_qr_code_view_my_qr_code_tab_title">View My QR Code</string>

View File

@ -3,7 +3,7 @@
<domain-config cleartextTrafficPermitted="true"> <domain-config cleartextTrafficPermitted="true">
<domain includeSubdomains="true">127.0.0.1</domain> <domain includeSubdomains="true">127.0.0.1</domain>
</domain-config> </domain-config>
<domain-config cleartextTrafficPermitted="false"> <domain-config cleartextTrafficPermitted="true">
<domain includeSubdomains="false">public.loki.foundation</domain> <domain includeSubdomains="false">public.loki.foundation</domain>
<trust-anchors> <trust-anchors>
<certificates src="@raw/lf_session_cert"/> <certificates src="@raw/lf_session_cert"/>

View File

@ -28,7 +28,6 @@ interface StorageProtocol {
// General // General
fun getUserPublicKey(): String? fun getUserPublicKey(): String?
fun getUserKeyPair(): Pair<String, ByteArray>?
fun getUserX25519KeyPair(): ECKeyPair fun getUserX25519KeyPair(): ECKeyPair
fun getUserDisplayName(): String? fun getUserDisplayName(): String?
fun getUserProfileKey(): ByteArray? fun getUserProfileKey(): ByteArray?

View File

@ -172,9 +172,9 @@ object OpenGroupAPIV2 {
} }
fun requestNewAuthToken(room: String, server: String): Promise<String, Exception> { fun requestNewAuthToken(room: String, server: String): Promise<String, Exception> {
val (publicKey, privateKey) = MessagingModuleConfiguration.shared.storage.getUserKeyPair() val (publicKey, privateKey) = MessagingModuleConfiguration.shared.storage.getUserX25519KeyPair().let { it.publicKey.serialize() to it.privateKey.serialize() }
?: return Promise.ofFail(Error.Generic) ?: return Promise.ofFail(Error.Generic)
val queryParameters = mutableMapOf( "public_key" to publicKey ) val queryParameters = mutableMapOf( "public_key" to publicKey.toHexString() )
val request = Request(GET, room, server, "auth_token_challenge", queryParameters, isAuthRequired = false, parameters = null) val request = Request(GET, room, server, "auth_token_challenge", queryParameters, isAuthRequired = false, parameters = null)
return send(request).map { json -> return send(request).map { json ->
val challenge = json["challenge"] as? Map<*, *> ?: throw Error.ParsingFailed val challenge = json["challenge"] as? Map<*, *> ?: throw Error.ParsingFailed

View File

@ -6,6 +6,7 @@ import org.session.libsignal.protos.SignalServiceProtos
import org.session.libsignal.utilities.Base64 import org.session.libsignal.utilities.Base64
import org.session.libsignal.utilities.Base64.decode import org.session.libsignal.utilities.Base64.decode
import org.session.libsignal.utilities.Log import org.session.libsignal.utilities.Log
import org.session.libsignal.utilities.toHexString
import org.whispersystems.curve25519.Curve25519 import org.whispersystems.curve25519.Curve25519
data class OpenGroupMessageV2( data class OpenGroupMessageV2(
@ -45,10 +46,10 @@ data class OpenGroupMessageV2(
fun sign(): OpenGroupMessageV2? { fun sign(): OpenGroupMessageV2? {
if (base64EncodedData.isEmpty()) return null if (base64EncodedData.isEmpty()) return null
val (publicKey, privateKey) = MessagingModuleConfiguration.shared.storage.getUserKeyPair() ?: return null val (publicKey, privateKey) = MessagingModuleConfiguration.shared.storage.getUserX25519KeyPair().let { it.publicKey to it.privateKey }
if (sender != publicKey) return null if (sender != publicKey.serialize().toHexString()) return null
val signature = try { val signature = try {
curve.calculateSignature(privateKey, decode(base64EncodedData)) curve.calculateSignature(privateKey.serialize(), decode(base64EncodedData))
} catch (e: Exception) { } catch (e: Exception) {
Log.w("Loki", "Couldn't sign open group message.", e) Log.w("Loki", "Couldn't sign open group message.", e)
return null return null

View File

@ -22,7 +22,7 @@ object MessageEncrypter {
* *
* @return the encrypted message. * @return the encrypted message.
*/ */
internal fun encrypt(plaintext: ByteArray, recipientHexEncodedX25519PublicKey: String): ByteArray{ internal fun encrypt(plaintext: ByteArray, recipientHexEncodedX25519PublicKey: String): ByteArray {
val context = MessagingModuleConfiguration.shared.context val context = MessagingModuleConfiguration.shared.context
val userED25519KeyPair = MessagingModuleConfiguration.shared.keyPairProvider() ?: throw Error.NoUserED25519KeyPair val userED25519KeyPair = MessagingModuleConfiguration.shared.keyPairProvider() ?: throw Error.NoUserED25519KeyPair
val recipientX25519PublicKey = Hex.fromStringCondensed(recipientHexEncodedX25519PublicKey.removing05PrefixIfNeeded()) val recipientX25519PublicKey = Hex.fromStringCondensed(recipientHexEncodedX25519PublicKey.removing05PrefixIfNeeded())

View File

@ -2,18 +2,20 @@
package org.session.libsession.snode package org.session.libsession.snode
import android.content.Context
import android.os.Build import android.os.Build
import com.goterl.lazysodium.LazySodiumAndroid import com.goterl.lazysodium.LazySodiumAndroid
import com.goterl.lazysodium.SodiumAndroid import com.goterl.lazysodium.SodiumAndroid
import com.goterl.lazysodium.exceptions.SodiumException import com.goterl.lazysodium.exceptions.SodiumException
import com.goterl.lazysodium.interfaces.AEAD
import com.goterl.lazysodium.interfaces.GenericHash import com.goterl.lazysodium.interfaces.GenericHash
import com.goterl.lazysodium.interfaces.PwHash import com.goterl.lazysodium.interfaces.PwHash
import com.goterl.lazysodium.interfaces.SecretBox import com.goterl.lazysodium.interfaces.SecretBox
import com.goterl.lazysodium.interfaces.Sign
import com.goterl.lazysodium.utils.Key import com.goterl.lazysodium.utils.Key
import nl.komponents.kovenant.* import nl.komponents.kovenant.*
import nl.komponents.kovenant.functional.bind import nl.komponents.kovenant.functional.bind
import nl.komponents.kovenant.functional.map import nl.komponents.kovenant.functional.map
import org.session.libsession.messaging.MessagingModuleConfiguration
import org.session.libsession.messaging.utilities.MessageWrapper import org.session.libsession.messaging.utilities.MessageWrapper
import org.session.libsignal.crypto.getRandomElement import org.session.libsignal.crypto.getRandomElement
import org.session.libsignal.database.LokiAPIDatabaseProtocol import org.session.libsignal.database.LokiAPIDatabaseProtocol
@ -22,6 +24,7 @@ import org.session.libsignal.utilities.*
import org.session.libsignal.utilities.Base64 import org.session.libsignal.utilities.Base64
import java.security.SecureRandom import java.security.SecureRandom
import java.util.* import java.util.*
import kotlin.Pair
object SnodeAPI { object SnodeAPI {
private val sodium by lazy { LazySodiumAndroid(SodiumAndroid()) } private val sodium by lazy { LazySodiumAndroid(SodiumAndroid()) }
@ -53,7 +56,7 @@ object SnodeAPI {
private val targetSwarmSnodeCount = 2 private val targetSwarmSnodeCount = 2
private val useOnionRequests = true private val useOnionRequests = true
internal val useTestnet = false internal val useTestnet = true
// Error // Error
internal sealed class Error(val description: String) : Exception(description) { internal sealed class Error(val description: String) : Exception(description) {
@ -102,7 +105,7 @@ object SnodeAPI {
"params" to mapOf( "params" to mapOf(
"active_only" to true, "active_only" to true,
"limit" to 256, "limit" to 256,
"fields" to mapOf( "public_ip" to true, "storage_port" to true, "pubkey_x25519" to true, "pubkey_ed25519" to true ) "fields" to mapOf("public_ip" to true, "storage_port" to true, "pubkey_x25519" to true, "pubkey_ed25519" to true)
) )
) )
val deferred = deferred<Snode, Exception>() val deferred = deferred<Snode, Exception>()
@ -284,6 +287,13 @@ object SnodeAPI {
} }
} }
fun getNetworkTime(snode: Snode): Promise<Pair<Snode,Long>, Exception> {
return invoke(Snode.Method.Info, snode, null, emptyMap()).map { rawResponse ->
val timestamp = rawResponse["timestamp"] as? Long ?: -1
snode to timestamp
}
}
fun sendMessage(message: SnodeMessage): Promise<Set<RawResponsePromise>, Exception> { fun sendMessage(message: SnodeMessage): Promise<Set<RawResponsePromise>, Exception> {
val destination = if (useTestnet) message.recipient.removing05PrefixIfNeeded() else message.recipient val destination = if (useTestnet) message.recipient.removing05PrefixIfNeeded() else message.recipient
return retryIfNeeded(maxRetryCount) { return retryIfNeeded(maxRetryCount) {
@ -321,6 +331,35 @@ object SnodeAPI {
} }
} }
fun deleteAllMessages(context: Context): Promise<Map<String,Boolean>, Exception> {
return retryIfNeeded(maxRetryCount) {
// 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 module = MessagingModuleConfiguration.shared
val userED25519KeyPair = module.keyPairProvider() ?: return@retryIfNeeded Promise.ofFail(Error.Generic)
val userPublicKey = module.storage.getUserPublicKey() ?: return@retryIfNeeded Promise.ofFail(Error.Generic)
getSingleTargetSnode(userPublicKey).bind { snode ->
retryIfNeeded(maxRetryCount) {
getNetworkTime(snode).bind { (_, timestamp) ->
val signature = ByteArray(Sign.BYTES)
val data = (Snode.Method.DeleteAll.rawValue + timestamp.toString()).toByteArray()
sodium.cryptoSignDetached(signature, data, data.size.toLong(), userED25519KeyPair.secretKey.asBytes)
val deleteMessageParams = mapOf(
"pubkey" to userPublicKey,
"pubkey_ed25519" to userED25519KeyPair.publicKey.asHexString,
"timestamp" to timestamp,
"signature" to Base64.encodeBytes(signature)
)
invoke(Snode.Method.DeleteAll, snode, userPublicKey, deleteMessageParams).map { rawResponse -> parseDeletions(userPublicKey, timestamp, rawResponse) }.fail { e ->
Log.e("Loki", "Failed to clear data", e)
}
}
}
}
}
}
fun parseRawMessagesResponse(rawResponse: RawResponse, snode: Snode, publicKey: String): List<SignalServiceProtos.Envelope> { fun parseRawMessagesResponse(rawResponse: RawResponse, snode: Snode, publicKey: String): List<SignalServiceProtos.Envelope> {
val messages = rawResponse["messages"] as? List<*> val messages = rawResponse["messages"] as? List<*>
return if (messages != null) { return if (messages != null) {
@ -378,6 +417,43 @@ object SnodeAPI {
} }
} }
} }
@Suppress("UNCHECKED_CAST")
private fun parseDeletions(userPublicKey: String, timestamp: Long, rawResponse: RawResponse): Map<String, Boolean> {
val swarms = rawResponse["swarm"] as? Map<String, Any> ?: return mapOf()
val swarmResponsesValid = swarms.mapNotNull { (nodePubKeyHex, rawMap) ->
val map = rawMap as? Map<String, Any> ?: return@mapNotNull null
/** 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.
*/
// failure
val failed = map["failed"] as? Boolean ?: false
val code = map["code"] as? String
val reason = map["reason"] as? String
nodePubKeyHex to if (failed) {
Log.e("Loki", "Failed to delete all from $nodePubKeyHex with error code $code and reason $reason")
false
} else {
// success
val deleted = map["deleted"] as List<String> // list of deleted hashes
val signature = map["signature"] as String
val nodePubKey = Key.fromHexString(nodePubKeyHex)
// signature of ( PUBKEY_HEX || TIMESTAMP || DELETEDHASH[0] || ... || DELETEDHASH[N] )
val message = (userPublicKey + timestamp.toString() + deleted.fold("") { a, v -> a + v }).toByteArray()
sodium.cryptoSignVerifyDetached(Base64.decode(signature), message, message.size, nodePubKey.asBytes)
}
}
return swarmResponsesValid.toMap()
}
// endregion // endregion
// Error Handling // Error Handling

View File

@ -7,7 +7,9 @@ class Snode(val address: String, val port: Int, val publicKeySet: KeySet?) {
GetSwarm("get_snodes_for_pubkey"), GetSwarm("get_snodes_for_pubkey"),
GetMessages("retrieve"), GetMessages("retrieve"),
SendMessage("store"), SendMessage("store"),
OxenDaemonRPCCall("oxend_request") OxenDaemonRPCCall("oxend_request"),
Info("info"),
DeleteAll("delete_all")
} }
data class KeySet(val ed25519Key: String, val x25519Key: String) data class KeySet(val ed25519Key: String, val x25519Key: String)