mirror of
https://github.com/oxen-io/session-android.git
synced 2025-04-07 06:24:30 +00:00
Optimise SnodesAPI
This commit is contained in:
parent
482f169df1
commit
c1d40cdbe7
@ -18,6 +18,8 @@ import nl.komponents.kovenant.task
|
|||||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||||
import org.session.libsession.messaging.utilities.MessageWrapper
|
import org.session.libsession.messaging.utilities.MessageWrapper
|
||||||
import org.session.libsession.messaging.utilities.SodiumUtilities.sodium
|
import org.session.libsession.messaging.utilities.SodiumUtilities.sodium
|
||||||
|
import org.session.libsession.utilities.buildMutableMap
|
||||||
|
import org.session.libsession.utilities.mapValuesNotNull
|
||||||
import org.session.libsession.utilities.toByteArray
|
import org.session.libsession.utilities.toByteArray
|
||||||
import org.session.libsignal.crypto.getRandomElement
|
import org.session.libsignal.crypto.getRandomElement
|
||||||
import org.session.libsignal.database.LokiAPIDatabaseProtocol
|
import org.session.libsignal.database.LokiAPIDatabaseProtocol
|
||||||
@ -73,20 +75,18 @@ object SnodeAPI {
|
|||||||
// Use port 4433 if the API level can handle the network security configuration and enforce pinned certificates
|
// Use port 4433 if the API level can handle the network security configuration and enforce pinned certificates
|
||||||
private val seedNodePort = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) 443 else 4443
|
private val seedNodePort = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) 443 else 4443
|
||||||
|
|
||||||
private const val snodeFailureThreshold = 3
|
|
||||||
private const val useOnionRequests = true
|
|
||||||
|
|
||||||
private const val useTestnet = false
|
private const val useTestnet = false
|
||||||
|
|
||||||
private val seedNodePool = if (useTestnet) {
|
private val seedNodePool = if (useTestnet) setOf(
|
||||||
setOf( "http://public.loki.foundation:38157" )
|
"http://public.loki.foundation:38157"
|
||||||
} else {
|
) else setOf(
|
||||||
setOf(
|
"https://seed1.getsession.org:$seedNodePort",
|
||||||
"https://seed1.getsession.org:$seedNodePort",
|
"https://seed2.getsession.org:$seedNodePort",
|
||||||
"https://seed2.getsession.org:$seedNodePort",
|
"https://seed3.getsession.org:$seedNodePort",
|
||||||
"https://seed3.getsession.org:$seedNodePort",
|
)
|
||||||
)
|
|
||||||
}
|
private const val snodeFailureThreshold = 3
|
||||||
|
private const val useOnionRequests = true
|
||||||
|
|
||||||
private const val KEY_IP = "public_ip"
|
private const val KEY_IP = "public_ip"
|
||||||
private const val KEY_PORT = "storage_port"
|
private const val KEY_PORT = "storage_port"
|
||||||
@ -121,48 +121,45 @@ object SnodeAPI {
|
|||||||
parameters: Map<String, Any>,
|
parameters: Map<String, Any>,
|
||||||
publicKey: String? = null,
|
publicKey: String? = null,
|
||||||
version: Version = Version.V3
|
version: Version = Version.V3
|
||||||
): RawResponsePromise = if (useOnionRequests) OnionRequestAPI.sendOnionRequest(method, parameters, snode, version, publicKey).map {
|
): RawResponsePromise = when {
|
||||||
val body = it.body ?: throw Error.Generic
|
useOnionRequests -> OnionRequestAPI.sendOnionRequest(method, parameters, snode, version, publicKey).map {
|
||||||
JsonUtil.fromJson(body, Map::class.java)
|
JsonUtil.fromJson(it.body ?: throw Error.Generic, Map::class.java)
|
||||||
} else task {
|
}
|
||||||
val payload = mapOf( "method" to method.rawValue, "params" to parameters )
|
else -> task {
|
||||||
try {
|
HTTP.execute(
|
||||||
val url = "${snode.address}:${snode.port}/storage_rpc/v1"
|
HTTP.Verb.POST,
|
||||||
val response = HTTP.execute(HTTP.Verb.POST, url, payload).toString()
|
url = "${snode.address}:${snode.port}/storage_rpc/v1",
|
||||||
JsonUtil.fromJson(response, Map::class.java)
|
parameters = buildMap {
|
||||||
} catch (exception: Exception) {
|
this["method"] = method.rawValue
|
||||||
(exception as? HTTP.HTTPRequestFailedException)?.run {
|
this["params"] = parameters
|
||||||
handleSnodeError(statusCode, json, snode, publicKey)
|
|
||||||
// TODO Check if we meant to throw the error returned by handleSnodeError
|
|
||||||
throw exception
|
|
||||||
}
|
}
|
||||||
Log.d("Loki", "Unhandled exception: $exception.")
|
).toString().let {
|
||||||
throw exception
|
JsonUtil.fromJson(it, Map::class.java)
|
||||||
|
}
|
||||||
|
}.fail { e ->
|
||||||
|
when (e) {
|
||||||
|
is HTTP.HTTPRequestFailedException -> handleSnodeError(e.statusCode, e.json, snode, publicKey)
|
||||||
|
else -> Log.d("Loki", "Unhandled exception: $e.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val GET_RANDOM_SNODE_PARAMS = buildMap<String, Any> {
|
||||||
|
this["method"] = "get_n_service_nodes"
|
||||||
|
this["params"] = buildMap {
|
||||||
|
this["active_only"] = true
|
||||||
|
this["fields"] = sequenceOf(KEY_IP, KEY_PORT, KEY_X25519, KEY_ED25519, KEY_VERSION).associateWith { true }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
internal fun getRandomSnode(): Promise<Snode, Exception> =
|
internal fun getRandomSnode(): Promise<Snode, Exception> =
|
||||||
snodePool.takeIf { it.size >= minimumSnodePoolCount }?.let { Promise.of(it.getRandomElement()) } ?: task {
|
snodePool.takeIf { it.size >= minimumSnodePoolCount }?.getRandomElement()?.let { Promise.of(it) } ?: task {
|
||||||
val target = seedNodePool.random()
|
val target = seedNodePool.random()
|
||||||
val url = "$target/json_rpc"
|
|
||||||
Log.d("Loki", "Populating snode pool using: $target.")
|
Log.d("Loki", "Populating snode pool using: $target.")
|
||||||
val parameters = mapOf(
|
val url = "$target/json_rpc"
|
||||||
"method" to "get_n_service_nodes",
|
val response = HTTP.execute(HTTP.Verb.POST, url, GET_RANDOM_SNODE_PARAMS, useSeedNodeConnection = true)
|
||||||
"params" to mapOf(
|
val json = runCatching { JsonUtil.fromJson(response, Map::class.java) }.getOrNull()
|
||||||
"active_only" to true,
|
?: buildMap { this["result"] = response.toString() }
|
||||||
"fields" to mapOf(
|
|
||||||
KEY_IP to true, KEY_PORT to true,
|
|
||||||
KEY_X25519 to true, KEY_ED25519 to true,
|
|
||||||
KEY_VERSION to true
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
val response = HTTP.execute(HTTP.Verb.POST, url, parameters, useSeedNodeConnection = true)
|
|
||||||
val json = try {
|
|
||||||
JsonUtil.fromJson(response, Map::class.java)
|
|
||||||
} catch (exception: Exception) {
|
|
||||||
mapOf( "result" to response.toString())
|
|
||||||
}
|
|
||||||
val intermediate = json["result"] as? Map<*, *> ?: throw Error.Generic
|
val intermediate = json["result"] as? Map<*, *> ?: throw Error.Generic
|
||||||
.also { Log.d("Loki", "Failed to update snode pool, intermediate was null.") }
|
.also { Log.d("Loki", "Failed to update snode pool, intermediate was null.") }
|
||||||
val rawSnodes = intermediate["service_node_states"] as? List<*> ?: throw Error.Generic
|
val rawSnodes = intermediate["service_node_states"] as? List<*> ?: throw Error.Generic
|
||||||
@ -180,7 +177,7 @@ object SnodeAPI {
|
|||||||
).also { if (it == null) Log.d("Loki", "Failed to parse: ${rawSnode.prettifiedDescription()}.") }
|
).also { if (it == null) Log.d("Loki", "Failed to parse: ${rawSnode.prettifiedDescription()}.") }
|
||||||
}.toSet().also {
|
}.toSet().also {
|
||||||
Log.d("Loki", "Persisting snode pool to database.")
|
Log.d("Loki", "Persisting snode pool to database.")
|
||||||
this.snodePool = it
|
snodePool = it
|
||||||
}.runCatching { getRandomElement() }.onFailure {
|
}.runCatching { getRandomElement() }.onFailure {
|
||||||
Log.d("Loki", "Got an empty snode pool from: $target.")
|
Log.d("Loki", "Got an empty snode pool from: $target.")
|
||||||
throw SnodeAPI.Error.Generic
|
throw SnodeAPI.Error.Generic
|
||||||
@ -220,10 +217,13 @@ object SnodeAPI {
|
|||||||
}
|
}
|
||||||
val base64EncodedNameHash = Base64.encodeBytes(nameHash)
|
val base64EncodedNameHash = Base64.encodeBytes(nameHash)
|
||||||
// Ask 3 different snodes for the Account ID associated with the given name hash
|
// Ask 3 different snodes for the Account ID associated with the given name hash
|
||||||
val parameters = mapOf(
|
val parameters = buildMap<String, Any> {
|
||||||
"endpoint" to "ons_resolve",
|
this["endpoint"] = "ons_resolve"
|
||||||
"params" to mapOf( "type" to 0, "name_hash" to base64EncodedNameHash )
|
this["params"] = buildMap {
|
||||||
)
|
this["type"] = 0
|
||||||
|
this["name_hash"] = base64EncodedNameHash
|
||||||
|
}
|
||||||
|
}
|
||||||
val promises = List(validationCount) {
|
val promises = List(validationCount) {
|
||||||
getRandomSnode().bind { snode ->
|
getRandomSnode().bind { snode ->
|
||||||
retryIfNeeded(maxRetryCount) {
|
retryIfNeeded(maxRetryCount) {
|
||||||
@ -232,10 +232,9 @@ object SnodeAPI {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return all(promises).map { results ->
|
return all(promises).map { results ->
|
||||||
val accountIDs = mutableListOf<String>()
|
results.map { json ->
|
||||||
for (json in results) {
|
val intermediate = json["result"] as? Map<*, *> ?: throw Error.Generic
|
||||||
val intermediate = json["result"] as? Map<*, *>
|
val hexEncodedCiphertext = intermediate["encrypted_value"] as? String ?: throw Error.Generic
|
||||||
val hexEncodedCiphertext = intermediate?.get("encrypted_value") as? String ?: throw Error.Generic
|
|
||||||
val ciphertext = Hex.fromStringCondensed(hexEncodedCiphertext)
|
val ciphertext = Hex.fromStringCondensed(hexEncodedCiphertext)
|
||||||
val isArgon2Based = (intermediate["nonce"] == null)
|
val isArgon2Based = (intermediate["nonce"] == null)
|
||||||
if (isArgon2Based) {
|
if (isArgon2Based) {
|
||||||
@ -251,7 +250,7 @@ object SnodeAPI {
|
|||||||
if (!sodium.cryptoSecretBoxOpenEasy(accountIDAsData, ciphertext, ciphertext.size.toLong(), nonce, key)) {
|
if (!sodium.cryptoSecretBoxOpenEasy(accountIDAsData, ciphertext, ciphertext.size.toLong(), nonce, key)) {
|
||||||
throw Error.DecryptionFailed
|
throw Error.DecryptionFailed
|
||||||
}
|
}
|
||||||
accountIDs.add(Hex.toStringCondensed(accountIDAsData))
|
Hex.toStringCondensed(accountIDAsData)
|
||||||
} else {
|
} else {
|
||||||
val hexEncodedNonce = intermediate["nonce"] as? String ?: throw Error.Generic
|
val hexEncodedNonce = intermediate["nonce"] as? String ?: throw Error.Generic
|
||||||
val nonce = Hex.fromStringCondensed(hexEncodedNonce)
|
val nonce = Hex.fromStringCondensed(hexEncodedNonce)
|
||||||
@ -263,10 +262,9 @@ object SnodeAPI {
|
|||||||
if (!sodium.cryptoAeadXChaCha20Poly1305IetfDecrypt(accountIDAsData, null, null, ciphertext, ciphertext.size.toLong(), null, 0, nonce, key)) {
|
if (!sodium.cryptoAeadXChaCha20Poly1305IetfDecrypt(accountIDAsData, null, null, ciphertext, ciphertext.size.toLong(), null, 0, nonce, key)) {
|
||||||
throw Error.DecryptionFailed
|
throw Error.DecryptionFailed
|
||||||
}
|
}
|
||||||
accountIDs.add(Hex.toStringCondensed(accountIDAsData))
|
Hex.toStringCondensed(accountIDAsData)
|
||||||
}
|
}
|
||||||
}
|
}.takeIf { it.size == validationCount && it.toSet().size == 1 }?.first()
|
||||||
accountIDs.takeIf { it.size == validationCount && it.toSet().size == 1 }?.first()
|
|
||||||
?: throw Error.ValidationFailed
|
?: throw Error.ValidationFailed
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -274,30 +272,31 @@ object SnodeAPI {
|
|||||||
fun getSwarm(publicKey: String): Promise<Set<Snode>, Exception> =
|
fun getSwarm(publicKey: String): Promise<Set<Snode>, Exception> =
|
||||||
database.getSwarm(publicKey)?.takeIf { it.size >= minimumSwarmSnodeCount }?.let(Promise.Companion::of)
|
database.getSwarm(publicKey)?.takeIf { it.size >= minimumSwarmSnodeCount }?.let(Promise.Companion::of)
|
||||||
?: getRandomSnode().bind {
|
?: getRandomSnode().bind {
|
||||||
invoke(Snode.Method.GetSwarm, it, parameters = mapOf( "pubKey" to publicKey ), publicKey)
|
invoke(Snode.Method.GetSwarm, it, parameters = buildMap { this["pubKey"] = publicKey }, publicKey)
|
||||||
}.map {
|
}.map {
|
||||||
parseSnodes(it).toSet()
|
parseSnodes(it).toSet()
|
||||||
}.success {
|
}.success {
|
||||||
database.setSwarm(publicKey, it)
|
database.setSwarm(publicKey, it)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun signAndEncode(data: ByteArray, userED25519KeyPair: KeyPair) = sign(data, userED25519KeyPair).let(Base64::encodeBytes)
|
private fun signAndEncodeCatching(data: ByteArray, userED25519KeyPair: KeyPair): Result<String> =
|
||||||
|
runCatching { signAndEncode(data, userED25519KeyPair) }
|
||||||
|
private fun signAndEncode(data: ByteArray, userED25519KeyPair: KeyPair): String =
|
||||||
|
sign(data, userED25519KeyPair).let(Base64::encodeBytes)
|
||||||
private fun sign(data: ByteArray, userED25519KeyPair: KeyPair): ByteArray = ByteArray(Sign.BYTES).also {
|
private fun sign(data: ByteArray, userED25519KeyPair: KeyPair): ByteArray = ByteArray(Sign.BYTES).also {
|
||||||
sodium.cryptoSignDetached(
|
sodium.cryptoSignDetached(it, data, data.size.toLong(), userED25519KeyPair.secretKey.asBytes)
|
||||||
it,
|
|
||||||
data,
|
|
||||||
data.size.toLong(),
|
|
||||||
userED25519KeyPair.secretKey.asBytes
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getRawMessages(snode: Snode, publicKey: String, requiresAuth: Boolean = true, namespace: Int = 0): RawResponsePromise {
|
fun getRawMessages(snode: Snode, publicKey: String, requiresAuth: Boolean = true, namespace: Int = 0): RawResponsePromise {
|
||||||
// Get last message hash
|
// Get last message hash
|
||||||
val lastHashValue = database.getLastMessageHashValue(snode, publicKey, namespace) ?: ""
|
val lastHashValue = database.getLastMessageHashValue(snode, publicKey, namespace) ?: ""
|
||||||
val parameters = mutableMapOf<String, Any>(
|
val parameters = buildMutableMap<String, Any> {
|
||||||
"pubKey" to publicKey,
|
this["pubKey"] = publicKey
|
||||||
"last_hash" to lastHashValue,
|
this["last_hash"] = lastHashValue
|
||||||
)
|
// If the namespace is default (0) here it will be implicitly read as 0 on the storage server
|
||||||
|
// we only need to specify it explicitly if we want to (in future) or if it is non-zero
|
||||||
|
namespace.takeIf { it != 0 }?.let { this["namespace"] = it }
|
||||||
|
}
|
||||||
// Construct signature
|
// Construct signature
|
||||||
if (requiresAuth) {
|
if (requiresAuth) {
|
||||||
val userED25519KeyPair = try {
|
val userED25519KeyPair = try {
|
||||||
@ -311,23 +310,13 @@ object SnodeAPI {
|
|||||||
val ed25519PublicKey = userED25519KeyPair.publicKey.asHexString
|
val ed25519PublicKey = userED25519KeyPair.publicKey.asHexString
|
||||||
val verificationData = buildString {
|
val verificationData = buildString {
|
||||||
append("retrieve")
|
append("retrieve")
|
||||||
if (namespace != 0) append(namespace)
|
namespace.takeIf { it != 0 }?.let(::append)
|
||||||
append(timestamp)
|
append(timestamp)
|
||||||
}.toByteArray()
|
}.toByteArray()
|
||||||
val signature = try {
|
parameters["signature"] = signAndEncodeCatching(verificationData, userED25519KeyPair).getOrNull()
|
||||||
signAndEncode(verificationData, userED25519KeyPair)
|
?: return Promise.ofFail(Error.SigningFailed)
|
||||||
} catch (exception: Exception) {
|
|
||||||
return Promise.ofFail(Error.SigningFailed)
|
|
||||||
}
|
|
||||||
parameters["timestamp"] = timestamp
|
parameters["timestamp"] = timestamp
|
||||||
parameters["pubkey_ed25519"] = ed25519PublicKey
|
parameters["pubkey_ed25519"] = ed25519PublicKey
|
||||||
parameters["signature"] = signature
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the namespace is default (0) here it will be implicitly read as 0 on the storage server
|
|
||||||
// we only need to specify it explicitly if we want to (in future) or if it is non-zero
|
|
||||||
if (namespace != 0) {
|
|
||||||
parameters["namespace"] = namespace
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make the request
|
// Make the request
|
||||||
@ -341,11 +330,8 @@ object SnodeAPI {
|
|||||||
val userED25519KeyPair = runCatching { MessagingModuleConfiguration.shared.getUserED25519KeyPair() }.getOrNull() ?: return null
|
val userED25519KeyPair = runCatching { MessagingModuleConfiguration.shared.getUserED25519KeyPair() }.getOrNull() ?: return null
|
||||||
|
|
||||||
val verificationData = "store$namespace$messageTimestamp".toByteArray()
|
val verificationData = "store$namespace$messageTimestamp".toByteArray()
|
||||||
val signature = try {
|
val signature = signAndEncodeCatching(verificationData, userED25519KeyPair).run {
|
||||||
signAndEncode(verificationData, userED25519KeyPair)
|
getOrNull() ?: return null.also { Log.e("Loki", "Signing data failed with user secret key", exceptionOrNull()) }
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e("Loki", "Signing data failed with user secret key", e)
|
|
||||||
return null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val params = buildMap {
|
val params = buildMap {
|
||||||
@ -478,13 +464,13 @@ object SnodeAPI {
|
|||||||
Log.e("Loki", "Signing data failed with user secret key", e)
|
Log.e("Loki", "Signing data failed with user secret key", e)
|
||||||
return@retryIfNeeded Promise.ofFail(e)
|
return@retryIfNeeded Promise.ofFail(e)
|
||||||
}
|
}
|
||||||
val params = mapOf(
|
val params = buildMap {
|
||||||
"pubkey" to publicKey,
|
this["pubkey"] = publicKey
|
||||||
"messages" to hashes,
|
this["messages"] = hashes
|
||||||
"timestamp" to timestamp,
|
this["timestamp"] = timestamp
|
||||||
"pubkey_ed25519" to ed25519PublicKey,
|
this["pubkey_ed25519"] = ed25519PublicKey
|
||||||
"signature" to signature
|
this["signature"] = signature
|
||||||
)
|
}
|
||||||
getSingleTargetSnode(publicKey) bind { snode ->
|
getSingleTargetSnode(publicKey) bind { snode ->
|
||||||
invoke(Snode.Method.GetExpiries, snode, params, publicKey)
|
invoke(Snode.Method.GetExpiries, snode, params, publicKey)
|
||||||
}
|
}
|
||||||
@ -578,7 +564,7 @@ object SnodeAPI {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun deleteMessage(publicKey: String, serverHashes: List<String>): Promise<Map<String,Boolean>, Exception> =
|
fun deleteMessage(publicKey: String, serverHashes: List<String>): Promise<Map<String, Boolean>, Exception> =
|
||||||
retryIfNeeded(maxRetryCount) {
|
retryIfNeeded(maxRetryCount) {
|
||||||
val module = MessagingModuleConfiguration.shared
|
val module = MessagingModuleConfiguration.shared
|
||||||
val userED25519KeyPair = module.getUserED25519KeyPair() ?: return@retryIfNeeded Promise.ofFail(Error.NoKeyPair)
|
val userED25519KeyPair = module.getUserED25519KeyPair() ?: return@retryIfNeeded Promise.ofFail(Error.NoKeyPair)
|
||||||
@ -586,35 +572,40 @@ object SnodeAPI {
|
|||||||
getSingleTargetSnode(publicKey).bind { snode ->
|
getSingleTargetSnode(publicKey).bind { snode ->
|
||||||
retryIfNeeded(maxRetryCount) {
|
retryIfNeeded(maxRetryCount) {
|
||||||
val verificationData = sequenceOf(Snode.Method.DeleteMessage.rawValue).plus(serverHashes).toByteArray()
|
val verificationData = sequenceOf(Snode.Method.DeleteMessage.rawValue).plus(serverHashes).toByteArray()
|
||||||
val deleteMessageParams = mapOf(
|
val deleteMessageParams = buildMap {
|
||||||
"pubkey" to userPublicKey,
|
this["pubkey"] = userPublicKey
|
||||||
"pubkey_ed25519" to userED25519KeyPair.publicKey.asHexString,
|
this["pubkey_ed25519"] = userED25519KeyPair.publicKey.asHexString
|
||||||
"messages" to serverHashes,
|
this["messages"] = serverHashes
|
||||||
"signature" to signAndEncode(verificationData, userED25519KeyPair)
|
this["signature"] = signAndEncode(verificationData, userED25519KeyPair)
|
||||||
)
|
}
|
||||||
invoke(Snode.Method.DeleteMessage, snode, deleteMessageParams, publicKey).map { rawResponse ->
|
invoke(Snode.Method.DeleteMessage, snode, deleteMessageParams, publicKey).map { rawResponse ->
|
||||||
val swarms = rawResponse["swarm"] as? Map<String, Any> ?: return@map mapOf()
|
val swarms = rawResponse["swarm"] as? Map<String, Any> ?: return@map mapOf()
|
||||||
val result = swarms.mapNotNull { (hexSnodePublicKey, rawJSON) ->
|
swarms.mapValuesNotNull { (hexSnodePublicKey, rawJSON) ->
|
||||||
val json = rawJSON as? Map<String, Any> ?: return@mapNotNull null
|
(rawJSON as? Map<String, Any>)?.let { json ->
|
||||||
val isFailed = json["failed"] as? Boolean ?: false
|
val isFailed = json["failed"] as? Boolean ?: false
|
||||||
val statusCode = json["code"] as? String
|
val statusCode = json["code"] as? String
|
||||||
val reason = json["reason"] as? String
|
val reason = json["reason"] as? String
|
||||||
hexSnodePublicKey to if (isFailed) {
|
|
||||||
Log.e("Loki", "Failed to delete messages from: $hexSnodePublicKey due to error: $reason ($statusCode).")
|
if (isFailed) {
|
||||||
false
|
Log.e("Loki", "Failed to delete messages from: $hexSnodePublicKey due to error: $reason ($statusCode).")
|
||||||
} else {
|
false
|
||||||
val hashes = json["deleted"] as List<String> // Hashes of deleted messages
|
} else {
|
||||||
val signature = json["signature"] as String
|
// Hashes of deleted messages
|
||||||
val snodePublicKey = Key.fromHexString(hexSnodePublicKey)
|
val hashes = json["deleted"] as List<String>
|
||||||
// The signature looks like ( PUBKEY_HEX || RMSG[0] || ... || RMSG[N] || DMSG[0] || ... || DMSG[M] )
|
val signature = json["signature"] as String
|
||||||
val message = sequenceOf(userPublicKey).plus(serverHashes).plus(hashes).toByteArray()
|
val snodePublicKey = Key.fromHexString(hexSnodePublicKey)
|
||||||
sodium.cryptoSignVerifyDetached(Base64.decode(signature), message, message.size, snodePublicKey.asBytes)
|
// The signature looks like ( PUBKEY_HEX || RMSG[0] || ... || RMSG[N] || DMSG[0] || ... || DMSG[M] )
|
||||||
|
val message = sequenceOf(userPublicKey).plus(serverHashes).plus(hashes).toByteArray()
|
||||||
|
sodium.cryptoSignVerifyDetached(
|
||||||
|
Base64.decode(signature),
|
||||||
|
message,
|
||||||
|
message.size,
|
||||||
|
snodePublicKey.asBytes
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return@map result.toMap()
|
}.fail { e -> Log.e("Loki", "Failed to delete messages", e) }
|
||||||
}.fail { e ->
|
|
||||||
Log.e("Loki", "Failed to delete messages", e)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -643,18 +634,16 @@ object SnodeAPI {
|
|||||||
retryIfNeeded(maxRetryCount) {
|
retryIfNeeded(maxRetryCount) {
|
||||||
getNetworkTime(snode).bind { (_, timestamp) ->
|
getNetworkTime(snode).bind { (_, timestamp) ->
|
||||||
val verificationData = (Snode.Method.DeleteAll.rawValue + Namespace.ALL + timestamp.toString()).toByteArray()
|
val verificationData = (Snode.Method.DeleteAll.rawValue + Namespace.ALL + timestamp.toString()).toByteArray()
|
||||||
val deleteMessageParams = mapOf(
|
val deleteMessageParams = buildMap {
|
||||||
"pubkey" to userPublicKey,
|
this["pubkey"] = userPublicKey
|
||||||
"pubkey_ed25519" to userED25519KeyPair.publicKey.asHexString,
|
this["pubkey_ed25519"] = userED25519KeyPair.publicKey.asHexString
|
||||||
"timestamp" to timestamp,
|
this["timestamp"] = timestamp
|
||||||
"signature" to signAndEncode(verificationData, userED25519KeyPair),
|
this["signature"] = signAndEncode(verificationData, userED25519KeyPair)
|
||||||
"namespace" to Namespace.ALL,
|
this["namespace"] = Namespace.ALL
|
||||||
)
|
|
||||||
invoke(Snode.Method.DeleteAll, snode, deleteMessageParams, userPublicKey).map {
|
|
||||||
rawResponse -> parseDeletions(userPublicKey, timestamp, rawResponse)
|
|
||||||
}.fail { e ->
|
|
||||||
Log.e("Loki", "Failed to clear data", e)
|
|
||||||
}
|
}
|
||||||
|
invoke(Snode.Method.DeleteAll, snode, deleteMessageParams, userPublicKey)
|
||||||
|
.map { rawResponse -> parseDeletions(userPublicKey, timestamp, rawResponse) }
|
||||||
|
.fail { e -> Log.e("Loki", "Failed to clear data", e) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -662,69 +651,53 @@ object SnodeAPI {
|
|||||||
|
|
||||||
fun parseRawMessagesResponse(rawResponse: RawResponse, snode: Snode, publicKey: String, namespace: Int = 0, updateLatestHash: Boolean = true, updateStoredHashes: Boolean = true): List<Pair<SignalServiceProtos.Envelope, String?>> =
|
fun parseRawMessagesResponse(rawResponse: RawResponse, snode: Snode, publicKey: String, namespace: Int = 0, updateLatestHash: Boolean = true, updateStoredHashes: Boolean = true): List<Pair<SignalServiceProtos.Envelope, String?>> =
|
||||||
(rawResponse["messages"] as? List<*>)?.let { messages ->
|
(rawResponse["messages"] as? List<*>)?.let { messages ->
|
||||||
if (updateLatestHash) {
|
if (updateLatestHash) updateLastMessageHashValueIfPossible(snode, publicKey, messages, namespace)
|
||||||
updateLastMessageHashValueIfPossible(snode, publicKey, messages, namespace)
|
|
||||||
}
|
|
||||||
removeDuplicates(publicKey, messages, namespace, updateStoredHashes).let(::parseEnvelopes)
|
removeDuplicates(publicKey, messages, namespace, updateStoredHashes).let(::parseEnvelopes)
|
||||||
} ?: listOf()
|
} ?: listOf()
|
||||||
|
|
||||||
fun updateLastMessageHashValueIfPossible(snode: Snode, publicKey: String, rawMessages: List<*>, namespace: Int) {
|
fun updateLastMessageHashValueIfPossible(snode: Snode, publicKey: String, rawMessages: List<*>, namespace: Int) {
|
||||||
val lastMessageAsJSON = rawMessages.lastOrNull() as? Map<*, *>
|
val lastMessageAsJSON = rawMessages.lastOrNull() as? Map<*, *>
|
||||||
val hashValue = lastMessageAsJSON?.get("hash") as? String
|
val hashValue = lastMessageAsJSON?.get("hash") as? String
|
||||||
if (hashValue != null) {
|
when {
|
||||||
database.setLastMessageHashValue(snode, publicKey, hashValue, namespace)
|
hashValue != null -> database.setLastMessageHashValue(snode, publicKey, hashValue, namespace)
|
||||||
} else if (rawMessages.isNotEmpty()) {
|
rawMessages.isNotEmpty() -> Log.d("Loki", "Failed to update last message hash value from: ${rawMessages.prettifiedDescription()}.")
|
||||||
Log.d("Loki", "Failed to update last message hash value from: ${rawMessages.prettifiedDescription()}.")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeDuplicates(publicKey: String, rawMessages: List<*>, namespace: Int, updateStoredHashes: Boolean): List<*> {
|
fun removeDuplicates(publicKey: String, rawMessages: List<*>, namespace: Int, updateStoredHashes: Boolean): List<*> {
|
||||||
val originalMessageHashValues = database.getReceivedMessageHashValues(publicKey, namespace) ?: emptySet()
|
val originalMessageHashValues = database.getReceivedMessageHashValues(publicKey, namespace) ?: emptySet()
|
||||||
val receivedMessageHashValues = originalMessageHashValues.toMutableSet()
|
val receivedMessageHashValues = originalMessageHashValues.toMutableSet()
|
||||||
val result = rawMessages.filter { rawMessage ->
|
return rawMessages.filter { rawMessage ->
|
||||||
(rawMessage as? Map<*, *>)
|
(rawMessage as? Map<*, *>)
|
||||||
?.let { it["hash"] as? String }
|
?.let { it["hash"] as? String }
|
||||||
?.let { receivedMessageHashValues.add(it) }
|
?.let { receivedMessageHashValues.add(it) }
|
||||||
?: false.also { Log.d("Loki", "Missing hash value for message: ${rawMessage?.prettifiedDescription()}.") }
|
?: false.also { Log.d("Loki", "Missing hash value for message: ${rawMessage?.prettifiedDescription()}.") }
|
||||||
}
|
}.also {
|
||||||
if (updateStoredHashes && originalMessageHashValues.containsAll(receivedMessageHashValues)) {
|
if (updateStoredHashes && originalMessageHashValues.containsAll(receivedMessageHashValues)) {
|
||||||
database.setReceivedMessageHashValues(publicKey, receivedMessageHashValues, namespace)
|
database.setReceivedMessageHashValues(publicKey, receivedMessageHashValues, namespace)
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun parseEnvelopes(rawMessages: List<*>): List<Pair<SignalServiceProtos.Envelope, String?>> =
|
|
||||||
rawMessages.mapNotNull { rawMessage ->
|
|
||||||
val rawMessageAsJSON = rawMessage as? Map<*, *>
|
|
||||||
val base64EncodedData = rawMessageAsJSON?.get("data") as? String
|
|
||||||
val data = base64EncodedData?.let { Base64.decode(it) }
|
|
||||||
|
|
||||||
data?.runCatching(MessageWrapper::unwrap)
|
|
||||||
?.map { it to rawMessageAsJSON["hash"] as? String }
|
|
||||||
?.onFailure { Log.d("Loki", "Failed to unwrap data for message: ${rawMessage.prettifiedDescription()}.") }
|
|
||||||
|
|
||||||
if (data != null) {
|
|
||||||
try {
|
|
||||||
MessageWrapper.unwrap(data) to rawMessageAsJSON["hash"] as? String
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.d("Loki", "Failed to unwrap data for message: ${rawMessage.prettifiedDescription()}.")
|
|
||||||
null
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Log.d("Loki", "Failed to decode data for message: ${rawMessage?.prettifiedDescription()}.")
|
|
||||||
null
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseEnvelopes(rawMessages: List<*>): List<Pair<SignalServiceProtos.Envelope, String?>> = rawMessages.mapNotNull { rawMessage ->
|
||||||
|
val rawMessageAsJSON = rawMessage as? Map<*, *>
|
||||||
|
val base64EncodedData = rawMessageAsJSON?.get("data") as? String
|
||||||
|
val data = base64EncodedData?.let { Base64.decode(it) }
|
||||||
|
|
||||||
|
data ?: Log.d("Loki", "Failed to decode data for message: ${rawMessage?.prettifiedDescription()}.")
|
||||||
|
|
||||||
|
data?.runCatching { MessageWrapper.unwrap(this) to rawMessageAsJSON["hash"] as? String }
|
||||||
|
?.onFailure { Log.d("Loki", "Failed to unwrap data for message: ${rawMessage.prettifiedDescription()}.") }
|
||||||
|
?.getOrNull()
|
||||||
|
}
|
||||||
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
@Suppress("UNCHECKED_CAST")
|
||||||
private fun parseDeletions(userPublicKey: String, timestamp: Long, rawResponse: RawResponse): Map<String, Boolean> {
|
private fun parseDeletions(userPublicKey: String, timestamp: Long, rawResponse: RawResponse): Map<String, Boolean> =
|
||||||
val swarms = rawResponse["swarm"] as? Map<String, Any> ?: return mapOf()
|
(rawResponse["swarm"] as? Map<String, Any>)?.mapValuesNotNull { (hexSnodePublicKey, rawJSON) ->
|
||||||
return swarms.mapNotNull { (hexSnodePublicKey, rawJSON) ->
|
val json = rawJSON as? Map<String, Any> ?: return@mapValuesNotNull null
|
||||||
val json = rawJSON as? Map<String, Any> ?: return@mapNotNull null
|
if (json["failed"] as? Boolean == true) {
|
||||||
val isFailed = json["failed"] as? Boolean ?: false
|
val reason = json["reason"] as? String
|
||||||
val statusCode = json["code"] as? String
|
val statusCode = json["code"] as? String
|
||||||
val reason = json["reason"] as? String
|
|
||||||
hexSnodePublicKey to if (isFailed) {
|
|
||||||
Log.e("Loki", "Failed to delete all messages from: $hexSnodePublicKey due to error: $reason ($statusCode).")
|
Log.e("Loki", "Failed to delete all messages from: $hexSnodePublicKey due to error: $reason ($statusCode).")
|
||||||
false
|
false
|
||||||
} else {
|
} else {
|
||||||
@ -735,8 +708,7 @@ object SnodeAPI {
|
|||||||
val message = sequenceOf(userPublicKey, "$timestamp").plus(hashes).toByteArray()
|
val message = sequenceOf(userPublicKey, "$timestamp").plus(hashes).toByteArray()
|
||||||
sodium.cryptoSignVerifyDetached(Base64.decode(signature), message, message.size, snodePublicKey.asBytes)
|
sodium.cryptoSignVerifyDetached(Base64.decode(signature), message, message.size, snodePublicKey.asBytes)
|
||||||
}
|
}
|
||||||
}.toMap()
|
} ?: mapOf()
|
||||||
}
|
|
||||||
|
|
||||||
// endregion
|
// endregion
|
||||||
|
|
||||||
@ -752,7 +724,7 @@ object SnodeAPI {
|
|||||||
publicKey?.let { dropSnodeFromSwarmIfNeeded(snode, it) }
|
publicKey?.let { dropSnodeFromSwarmIfNeeded(snode, it) }
|
||||||
snodePool -= snode
|
snodePool -= snode
|
||||||
Log.d("Loki", "Snode pool count: ${snodePool.count()}.")
|
Log.d("Loki", "Snode pool count: ${snodePool.count()}.")
|
||||||
snodeFailureCount[snode] = 0
|
snodeFailureCount.remove(snode)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
when (statusCode) {
|
when (statusCode) {
|
||||||
@ -765,15 +737,14 @@ object SnodeAPI {
|
|||||||
}
|
}
|
||||||
421 -> {
|
421 -> {
|
||||||
// The snode isn't associated with the given public key anymore
|
// The snode isn't associated with the given public key anymore
|
||||||
if (publicKey != null) {
|
if (publicKey == null) Log.d("Loki", "Got a 421 without an associated public key.")
|
||||||
json?.let(::parseSnodes)
|
else json?.let(::parseSnodes)
|
||||||
?.takeIf { it.isNotEmpty() }
|
?.takeIf { it.isNotEmpty() }
|
||||||
?.let { database.setSwarm(publicKey, it.toSet()) }
|
?.let { database.setSwarm(publicKey, it.toSet()) }
|
||||||
?: run {
|
?: run {
|
||||||
Log.d("Loki", "Invalidating swarm for: $publicKey.")
|
Log.d("Loki", "Invalidating swarm for: $publicKey.")
|
||||||
dropSnodeFromSwarmIfNeeded(snode, publicKey)
|
dropSnodeFromSwarmIfNeeded(snode, publicKey)
|
||||||
}
|
}
|
||||||
} else Log.d("Loki", "Got a 421 without an associated public key.")
|
|
||||||
}
|
}
|
||||||
404 -> {
|
404 -> {
|
||||||
Log.d("Loki", "404, probably no file found")
|
Log.d("Loki", "404, probably no file found")
|
||||||
|
@ -402,6 +402,12 @@ fun <E, K: Any, V: Any> Iterable<E>.associateByNotNull(
|
|||||||
for(e in this) { it[keySelector(e) ?: continue] = valueTransform(e) ?: continue }
|
for(e in this) { it[keySelector(e) ?: continue] = valueTransform(e) ?: continue }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun <K: Any, V: Any, W : Any> Map<K, V>.mapValuesNotNull(
|
||||||
|
valueTransform: (Map.Entry<K, V>) -> W?
|
||||||
|
): Map<K, W> = mutableMapOf<K, W>().also {
|
||||||
|
for(e in this) { it[e.key] = valueTransform(e) ?: continue }
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Groups elements of the original collection by the key returned by the given [keySelector] function
|
* Groups elements of the original collection by the key returned by the given [keySelector] function
|
||||||
* applied to each element and returns a map where each group key is associated with a list of
|
* applied to each element and returns a map where each group key is associated with a list of
|
||||||
@ -413,6 +419,21 @@ inline fun <E, K> Iterable<E>.groupByNotNull(keySelector: (E) -> K?): Map<K, Lis
|
|||||||
forEach { e -> keySelector(e)?.let { k -> it.getOrPut(k) { mutableListOf() } += e } }
|
forEach { e -> keySelector(e)?.let { k -> it.getOrPut(k) { mutableListOf() } += e } }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Analogous to [buildMap], this function creates a [MutableMap] and populates it using the given [action].
|
||||||
|
*/
|
||||||
|
inline fun <K, V> buildMutableMap(action: MutableMap<K, V>.() -> Unit): MutableMap<K, V> =
|
||||||
|
mutableMapOf<K, V>().apply(action)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a list of Pairs into a Map, filtering out any Pairs where the value is null.
|
||||||
|
*
|
||||||
|
* @param pairs The list of Pairs to convert.
|
||||||
|
* @return A Map with non-null values.
|
||||||
|
*/
|
||||||
|
fun <K : Any, V : Any> Iterable<Pair<K, V?>>.toMapNotNull(): Map<K, V> =
|
||||||
|
associateByNotNull(Pair<K, V?>::first, Pair<K, V?>::second)
|
||||||
|
|
||||||
fun Sequence<String>.toByteArray(): ByteArray = ByteArrayOutputStream().use { output ->
|
fun Sequence<String>.toByteArray(): ByteArray = ByteArrayOutputStream().use { output ->
|
||||||
forEach { it.byteInputStream().use { input -> input.copyTo(output) } }
|
forEach { it.byteInputStream().use { input -> input.copyTo(output) } }
|
||||||
output.toByteArray()
|
output.toByteArray()
|
||||||
|
Loading…
x
Reference in New Issue
Block a user