feat: testnet clearing network data on delete and differentiating dialogs

This commit is contained in:
jubb 2021-06-18 16:01:34 +10:00
parent 11f64a1d1a
commit fdc042e6d4
15 changed files with 156 additions and 64 deletions

View File

@ -42,12 +42,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

@ -92,6 +92,7 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
helpTranslateButton.setOnClickListener { helpTranslate() } helpTranslateButton.setOnClickListener { helpTranslate() }
seedButton.setOnClickListener { showSeed() } seedButton.setOnClickListener { showSeed() }
clearAllDataButton.setOnClickListener { clearAllData() } clearAllDataButton.setOnClickListener { clearAllData() }
clearAllDataAndNetworkButton.setOnClickListener { clearAllDataIncludingNetwork() }
versionTextView.text = String.format(getString(R.string.version_s), "${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})") 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() { 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 // endregion
private inner class DisplayNameEditActionModeCallback: ActionMode.Callback { private inner class DisplayNameEditActionModeCallback: ActionMode.Callback {

View File

@ -1,6 +1,7 @@
package org.thoughtcrime.securesms.loki.dialogs package org.thoughtcrime.securesms.loki.dialogs
import android.app.Dialog import android.app.Dialog
import android.content.DialogInterface
import android.graphics.Color import android.graphics.Color
import android.graphics.drawable.ColorDrawable import android.graphics.drawable.ColorDrawable
import android.os.Bundle import android.os.Bundle
@ -11,21 +12,22 @@ import androidx.fragment.app.DialogFragment
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import kotlinx.android.synthetic.main.dialog_clear_all_data.* 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.Job import kotlinx.coroutines.*
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import network.loki.messenger.R import network.loki.messenger.R
import nl.komponents.kovenant.Promise
import org.session.libsession.messaging.MessagingModuleConfiguration import org.session.libsession.messaging.MessagingModuleConfiguration
import org.session.libsession.snode.SnodeAPI import org.session.libsession.snode.SnodeAPI
import org.session.libsession.snode.SnodeDeleteMessage
import org.session.libsession.utilities.KeyPairUtilities 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 var clearJob: Job? = null
set(value) { set(value) {
field = value field = value
updateUI()
} }
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
@ -40,41 +42,73 @@ class ClearAllDataDialog : DialogFragment() {
return result return result
} }
private fun updateUI() { override fun onDismiss(dialog: DialogInterface) {
if (clearJob?.isActive == true) { super.onDismiss(dialog)
// clear background job is running, prevent interaction
dialog?.let { view ->
view.cancelButton.isVisible = false
view.clearAllDataButton.isVisible = false
} }
} else {
dialog?.let { view -> override fun onStart() {
view.cancelButton.isVisible = false super.onStart()
view.clearAllDataButton.isVisible = false 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() { private fun clearAllData() {
if (KeyPairUtilities.hasV2KeyPair(requireContext())) { if (KeyPairUtilities.hasV2KeyPair(requireContext())) {
clearJob = lifecycleScope.launch { clearJob = lifecycleScope.launch(Dispatchers.IO) {
delay(5_000) 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 // finish
val userPublicKey = MessagingModuleConfiguration.shared.storage.getUserPublicKey() val promises = SnodeAPI.deleteAllMessages(requireContext()).get()
val rawResponses = promises.map {
val deleteMessage = SnodeDeleteMessage(userKey, System.currentTimeMillis(), ) try {
SnodeAPI.deleteAllMessages() it.get()
// TODO: re-add the clear data here } catch (e: Exception) {
//ApplicationContext.getInstance(context).clearAllData(false) 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 { } else {
val dialog = AlertDialog.Builder(requireContext()) val dialog = AlertDialog.Builder(requireContext())
val message = "Weve upgraded the way Session IDs are generated, so you will be unable to restore your current Session ID." val message = "Weve upgraded the way Session IDs are generated, so you will be unable to restore your current Session ID."
dialog.setMessage(message) dialog.setMessage(message)
dialog.setPositiveButton("Yes") { _, _ -> dialog.setPositiveButton("Yes") { _, _ ->
// TODO: re-add the clear data here ApplicationContext.getInstance(context).clearAllData(false)
// ApplicationContext.getInstance(context).clearAllData(false)
} }
dialog.setNegativeButton("Cancel") { _, _ -> dialog.setNegativeButton("Cancel") { _, _ ->
// Do nothing // Do nothing

View File

@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.loki.protocol
import android.content.Context import android.content.Context
import com.google.protobuf.ByteString import com.google.protobuf.ByteString
import nl.komponents.kovenant.Promise
import org.session.libsession.messaging.MessagingModuleConfiguration import org.session.libsession.messaging.MessagingModuleConfiguration
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
@ -28,16 +29,17 @@ object MultiDeviceProtocol {
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

@ -185,6 +185,16 @@
android:textStyle="bold" android:textStyle="bold"
android:gravity="center" android:gravity="center"
android:text="@string/activity_settings_clear_all_data_button_title" /> android:text="@string/activity_settings_clear_all_data_button_title" />
<TextView
android:id="@+id/clearAllDataAndNetworkButton"
android:layout_width="match_parent"
android:layout_height="@dimen/setting_button_height"
android:background="@drawable/setting_button_background"
android:textColor="@color/destructive"
android:textSize="@dimen/medium_font_size"
android:textStyle="bold"
android:gravity="center"
android:text="@string/activity_settings_clear_all_data_and_network_button_title" />
<View <View
android:layout_width="match_parent" android:layout_width="match_parent"

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"
@ -36,6 +37,7 @@
android:orientation="horizontal"> android:orientation="horizontal">
<Button <Button
tools:visibility="gone"
style="@style/Widget.Session.Button.Dialog.Unimportant" style="@style/Widget.Session.Button.Dialog.Unimportant"
android:id="@+id/cancelButton" android:id="@+id/cancelButton"
android:layout_width="0dp" android:layout_width="0dp"
@ -44,6 +46,7 @@
android:text="@string/cancel" /> android:text="@string/cancel" />
<Button <Button
tools:visibility="gone"
style="@style/Widget.Session.Button.Dialog.Destructive" style="@style/Widget.Session.Button.Dialog.Destructive"
android:id="@+id/clearAllDataButton" android:id="@+id/clearAllDataButton"
android:layout_width="0dp" android:layout_width="0dp"
@ -52,6 +55,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>

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

@ -2,6 +2,7 @@
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
@ -9,18 +10,25 @@ import com.goterl.lazysodium.exceptions.SodiumException
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.libsession.utilities.IdentityKeyUtil
import org.session.libsession.utilities.KeyPairUtilities
import org.session.libsignal.crypto.getRandomElement import org.session.libsignal.crypto.getRandomElement
import org.session.libsignal.database.LokiAPIDatabaseProtocol import org.session.libsignal.database.LokiAPIDatabaseProtocol
import org.session.libsignal.protos.SignalServiceProtos import org.session.libsignal.protos.SignalServiceProtos
import org.session.libsignal.utilities.* import org.session.libsignal.utilities.*
import org.session.libsignal.utilities.Base64 import org.session.libsignal.utilities.Base64
import org.whispersystems.curve25519.Curve25519
import java.nio.charset.Charset
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()) }
@ -52,7 +60,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) {
@ -283,6 +291,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
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) {
@ -306,15 +321,30 @@ object SnodeAPI {
* - "signature": signature of ( PUBKEY_HEX || TIMESTAMP || DELETEDHASH[0] || ... || DELETEDHASH[N] ), signed * - "signature": signature of ( PUBKEY_HEX || TIMESTAMP || DELETEDHASH[0] || ... || DELETEDHASH[N] ), signed
* by the node's ed25519 pubkey. * by the node's ed25519 pubkey.
*/ */
fun deleteAllMessages(deleteMessage: SnodeDeleteMessage): Promise<Set<RawResponsePromise>, Exception> { fun deleteAllMessages(context: Context): Promise<Set<RawResponsePromise>, Exception> {
return retryIfNeeded(1) {
// 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 // 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 val ed = KeyPairUtilities.getUserED25519KeyPair(context) ?: return@retryIfNeeded Promise.ofFail(Error.Generic)
return retryIfNeeded(maxRetryCount) { val xPrivateKey = IdentityKeyUtil.getIdentityKeyPair(context).privateKey
val userKeyPair = MessagingModuleConfiguration.shared.storage.getUserX25519KeyPair()
val userPublicKey = MessagingModuleConfiguration.shared.storage.getUserPublicKey() ?: return@retryIfNeeded Promise.ofFail(Error.Generic)
val destination = if (useTestnet) userPublicKey.removing05PrefixIfNeeded() else userPublicKey
getTargetSnodes(destination).map { swarm -> getTargetSnodes(destination).map { swarm ->
swarm.map { snode -> swarm.map { snode ->
retryIfNeeded(1) {
getNetworkTime(snode).bind { (_, timestamp) ->
val signature = ByteArray(Sign.BYTES)
val data = Snode.Method.DeleteAll.rawValue.toByteArray() + timestamp.toString().toByteArray()
val signed = sodium.cryptoSignDetached(signature, data, data.size.toLong(), xPrivateKey.serialize())
val deleteMessage = SnodeDeleteMessage(userPublicKey, timestamp, Base64.encodeBytes(signature))
val parameters = deleteMessage.toJSON() val parameters = deleteMessage.toJSON()
retryIfNeeded(maxRetryCount) { invoke(Snode.Method.DeleteAll, snode, destination, parameters).fail { e ->
invoke(Snode.Method.DeleteAll, snode, destination, parameters) Log.e("Loki", "Failed to clear data",e)
}
}
} }
}.toSet() }.toSet()
} }

View File

@ -14,15 +14,15 @@ data class SnodeDeleteMessage(
*/ */
val timestamp: Long, val timestamp: Long,
/** /**
* an Ed25519 signature of ( "delete_all" || timestamp ), signed by the ed25519 * a Base64-encoded signature of ( "delete_all" || timestamp ), signed by the pubKey
*/ */
val signature: String, val signature: String,
) { ) {
internal fun toJSON(): Map<String, String> { internal fun toJSON(): Map<String, Any> {
return mapOf( return mapOf(
"pubkey" to if (SnodeAPI.useTestnet) pubKey.removing05PrefixIfNeeded() else pubKey, "pubkey" to pubKey,
"timestamp" to timestamp.toString(), "timestamp" to timestamp,
"signature" to signature "signature" to signature
) )
} }

View File

@ -8,6 +8,7 @@ class Snode(val address: String, val port: Int, val publicKeySet: KeySet?) {
GetMessages("retrieve"), GetMessages("retrieve"),
SendMessage("store"), SendMessage("store"),
OxenDaemonRPCCall("oxend_request"), OxenDaemonRPCCall("oxend_request"),
Info("info"),
DeleteAll("delete_all") DeleteAll("delete_all")
} }