mirror of
https://github.com/oxen-io/session-android.git
synced 2023-12-14 02:53:01 +01:00
Merge pull request #652 from oxen-io/security
Add Option to Delete All Network Data
This commit is contained in:
commit
7eae15594b
13 changed files with 228 additions and 48 deletions
|
@ -39,12 +39,6 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(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 {
|
||||
return DatabaseFactory.getLokiAPIDatabase(context).getUserX25519KeyPair()
|
||||
}
|
||||
|
|
|
@ -2,37 +2,126 @@ package org.thoughtcrime.securesms.preferences
|
|||
|
||||
import android.view.LayoutInflater
|
||||
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.coroutines.*
|
||||
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.crypto.KeyPairUtilities
|
||||
import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities
|
||||
import org.thoughtcrime.securesms.conversation.v2.utilities.BaseDialog
|
||||
|
||||
class ClearAllDataDialog : BaseDialog() {
|
||||
|
||||
override fun setContentView(builder: AlertDialog.Builder) {
|
||||
val contentView = LayoutInflater.from(requireContext()).inflate(R.layout.dialog_clear_all_data, null)
|
||||
contentView.cancelButton.setOnClickListener { dismiss() }
|
||||
contentView.clearAllDataButton.setOnClickListener { clearAllData() }
|
||||
builder.setView(contentView)
|
||||
enum class Steps {
|
||||
INFO_PROMPT,
|
||||
NETWORK_PROMPT,
|
||||
DELETING
|
||||
}
|
||||
|
||||
private fun clearAllData() {
|
||||
if (KeyPairUtilities.hasV2KeyPair(requireContext())) {
|
||||
ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(requireContext())
|
||||
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") { _, _ ->
|
||||
var clearJob: Job? = null
|
||||
set(value) {
|
||||
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)
|
||||
withContext(Dispatchers.Main) {
|
||||
dismiss()
|
||||
}
|
||||
} else {
|
||||
// finish
|
||||
val result = try {
|
||||
SnodeAPI.deleteAllMessages(requireContext()).get()
|
||||
} 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)
|
||||
withContext(Dispatchers.Main) {
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
dialog.setNegativeButton("Cancel") { _, _ ->
|
||||
// Do nothing
|
||||
}
|
||||
dialog.create().show()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -303,6 +303,7 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
|
|||
private fun clearAllData() {
|
||||
ClearAllDataDialog().show(supportFragmentManager, "Clear All Data Dialog")
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
private inner class DisplayNameEditActionModeCallback: ActionMode.Callback {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package org.thoughtcrime.securesms.util
|
||||
|
||||
import android.content.Context
|
||||
import nl.komponents.kovenant.Promise
|
||||
import org.session.libsession.messaging.messages.Destination
|
||||
import org.session.libsession.messaging.messages.control.ConfigurationMessage
|
||||
import org.session.libsession.messaging.sending_receiving.MessageSender
|
||||
|
@ -25,16 +26,17 @@ object ConfigurationMessageUtilities {
|
|||
TextSecurePreferences.setLastConfigurationSyncTime(context, now)
|
||||
}
|
||||
|
||||
fun forceSyncConfigurationNowIfNeeded(context: Context) {
|
||||
val userPublicKey = TextSecurePreferences.getLocalNumber(context) ?: return
|
||||
fun forceSyncConfigurationNowIfNeeded(context: Context): Promise<Unit, Exception> {
|
||||
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
|
||||
}
|
||||
|
||||
}
|
|
@ -1,8 +1,9 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="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:gravity="center_horizontal"
|
||||
android:orientation="vertical"
|
||||
|
@ -18,7 +19,7 @@
|
|||
android:textSize="@dimen/medium_font_size" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/seedTextView"
|
||||
android:id="@+id/dialogDescriptionText"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/large_spacing"
|
||||
|
@ -50,6 +51,17 @@
|
|||
android:layout_marginStart="@dimen/medium_spacing"
|
||||
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>
|
|
@ -755,6 +755,7 @@
|
|||
<string name="activity_settings_invite_button_title">Invite</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_and_network_button_title">Clear Data Including Network</string>
|
||||
<string name="activity_settings_help_translate_session">Help us Translate Session</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_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_view_my_qr_code_tab_title">View My QR Code</string>
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
<domain-config cleartextTrafficPermitted="true">
|
||||
<domain includeSubdomains="true">127.0.0.1</domain>
|
||||
</domain-config>
|
||||
<domain-config cleartextTrafficPermitted="false">
|
||||
<domain-config cleartextTrafficPermitted="true">
|
||||
<domain includeSubdomains="false">public.loki.foundation</domain>
|
||||
<trust-anchors>
|
||||
<certificates src="@raw/lf_session_cert"/>
|
||||
|
|
|
@ -28,7 +28,6 @@ interface StorageProtocol {
|
|||
|
||||
// General
|
||||
fun getUserPublicKey(): String?
|
||||
fun getUserKeyPair(): Pair<String, ByteArray>?
|
||||
fun getUserX25519KeyPair(): ECKeyPair
|
||||
fun getUserDisplayName(): String?
|
||||
fun getUserProfileKey(): ByteArray?
|
||||
|
|
|
@ -172,9 +172,9 @@ object OpenGroupAPIV2 {
|
|||
}
|
||||
|
||||
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)
|
||||
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)
|
||||
return send(request).map { json ->
|
||||
val challenge = json["challenge"] as? Map<*, *> ?: throw Error.ParsingFailed
|
||||
|
|
|
@ -6,6 +6,7 @@ import org.session.libsignal.protos.SignalServiceProtos
|
|||
import org.session.libsignal.utilities.Base64
|
||||
import org.session.libsignal.utilities.Base64.decode
|
||||
import org.session.libsignal.utilities.Log
|
||||
import org.session.libsignal.utilities.toHexString
|
||||
import org.whispersystems.curve25519.Curve25519
|
||||
|
||||
data class OpenGroupMessageV2(
|
||||
|
@ -45,10 +46,10 @@ data class OpenGroupMessageV2(
|
|||
|
||||
fun sign(): OpenGroupMessageV2? {
|
||||
if (base64EncodedData.isEmpty()) return null
|
||||
val (publicKey, privateKey) = MessagingModuleConfiguration.shared.storage.getUserKeyPair() ?: return null
|
||||
if (sender != publicKey) return null
|
||||
val (publicKey, privateKey) = MessagingModuleConfiguration.shared.storage.getUserX25519KeyPair().let { it.publicKey to it.privateKey }
|
||||
if (sender != publicKey.serialize().toHexString()) return null
|
||||
val signature = try {
|
||||
curve.calculateSignature(privateKey, decode(base64EncodedData))
|
||||
curve.calculateSignature(privateKey.serialize(), decode(base64EncodedData))
|
||||
} catch (e: Exception) {
|
||||
Log.w("Loki", "Couldn't sign open group message.", e)
|
||||
return null
|
||||
|
|
|
@ -22,7 +22,7 @@ object MessageEncrypter {
|
|||
*
|
||||
* @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 userED25519KeyPair = MessagingModuleConfiguration.shared.keyPairProvider() ?: throw Error.NoUserED25519KeyPair
|
||||
val recipientX25519PublicKey = Hex.fromStringCondensed(recipientHexEncodedX25519PublicKey.removing05PrefixIfNeeded())
|
||||
|
|
|
@ -2,18 +2,20 @@
|
|||
|
||||
package org.session.libsession.snode
|
||||
|
||||
import android.content.Context
|
||||
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
|
||||
import com.goterl.lazysodium.interfaces.Sign
|
||||
import com.goterl.lazysodium.utils.Key
|
||||
import nl.komponents.kovenant.*
|
||||
import nl.komponents.kovenant.functional.bind
|
||||
import nl.komponents.kovenant.functional.map
|
||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||
import org.session.libsession.messaging.utilities.MessageWrapper
|
||||
import org.session.libsignal.crypto.getRandomElement
|
||||
import org.session.libsignal.database.LokiAPIDatabaseProtocol
|
||||
|
@ -22,6 +24,7 @@ import org.session.libsignal.utilities.*
|
|||
import org.session.libsignal.utilities.Base64
|
||||
import java.security.SecureRandom
|
||||
import java.util.*
|
||||
import kotlin.Pair
|
||||
|
||||
object SnodeAPI {
|
||||
private val sodium by lazy { LazySodiumAndroid(SodiumAndroid()) }
|
||||
|
@ -53,7 +56,7 @@ object SnodeAPI {
|
|||
private val targetSwarmSnodeCount = 2
|
||||
private val useOnionRequests = true
|
||||
|
||||
internal val useTestnet = false
|
||||
internal val useTestnet = true
|
||||
|
||||
// Error
|
||||
internal sealed class Error(val description: String) : Exception(description) {
|
||||
|
@ -100,9 +103,9 @@ object SnodeAPI {
|
|||
val parameters = mapOf(
|
||||
"method" to "get_n_service_nodes",
|
||||
"params" to mapOf(
|
||||
"active_only" to true,
|
||||
"limit" to 256,
|
||||
"fields" to mapOf( "public_ip" to true, "storage_port" to true, "pubkey_x25519" to true, "pubkey_ed25519" to true )
|
||||
"active_only" to true,
|
||||
"limit" to 256,
|
||||
"fields" to mapOf("public_ip" to true, "storage_port" to true, "pubkey_x25519" to true, "pubkey_ed25519" to true)
|
||||
)
|
||||
)
|
||||
val deferred = deferred<Snode, Exception>()
|
||||
|
@ -178,8 +181,8 @@ object SnodeAPI {
|
|||
val base64EncodedNameHash = Base64.encodeBytes(nameHash)
|
||||
// Ask 3 different snodes for the Session ID associated with the given name hash
|
||||
val parameters = mapOf(
|
||||
"endpoint" to "ons_resolve",
|
||||
"params" to mapOf( "type" to 0, "name_hash" to base64EncodedNameHash )
|
||||
"endpoint" to "ons_resolve",
|
||||
"params" to mapOf( "type" to 0, "name_hash" to base64EncodedNameHash )
|
||||
)
|
||||
val promises = (1..validationCount).map {
|
||||
getRandomSnode().bind { snode ->
|
||||
|
@ -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> {
|
||||
val destination = if (useTestnet) message.recipient.removing05PrefixIfNeeded() else message.recipient
|
||||
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> {
|
||||
val messages = rawResponse["messages"] as? List<*>
|
||||
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
|
||||
|
||||
// Error Handling
|
||||
|
|
|
@ -7,7 +7,9 @@ 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"),
|
||||
Info("info"),
|
||||
DeleteAll("delete_all")
|
||||
}
|
||||
|
||||
data class KeySet(val ed25519Key: String, val x25519Key: String)
|
||||
|
|
Loading…
Reference in a new issue