Optimise SnodesAPI

This commit is contained in:
bemusementpark 2024-08-03 20:19:29 +09:30
parent 482f169df1
commit c1d40cdbe7
2 changed files with 181 additions and 189 deletions

View File

@ -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")

View File

@ -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()