|
|
|
@ -11,19 +11,33 @@ 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.Promise
|
|
|
|
|
import nl.komponents.kovenant.all
|
|
|
|
|
import nl.komponents.kovenant.deferred
|
|
|
|
|
import nl.komponents.kovenant.functional.bind
|
|
|
|
|
import nl.komponents.kovenant.functional.map
|
|
|
|
|
import nl.komponents.kovenant.task
|
|
|
|
|
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
|
|
|
|
|
import org.session.libsignal.protos.SignalServiceProtos
|
|
|
|
|
import org.session.libsignal.utilities.*
|
|
|
|
|
import org.session.libsignal.utilities.Base64
|
|
|
|
|
import org.session.libsignal.utilities.Broadcaster
|
|
|
|
|
import org.session.libsignal.utilities.HTTP
|
|
|
|
|
import org.session.libsignal.utilities.Hex
|
|
|
|
|
import org.session.libsignal.utilities.Log
|
|
|
|
|
import org.session.libsignal.utilities.Snode
|
|
|
|
|
import org.session.libsignal.utilities.ThreadUtils
|
|
|
|
|
import org.session.libsignal.utilities.prettifiedDescription
|
|
|
|
|
import org.session.libsignal.utilities.retryIfNeeded
|
|
|
|
|
import java.security.SecureRandom
|
|
|
|
|
import java.util.*
|
|
|
|
|
import kotlin.Pair
|
|
|
|
|
import java.util.Date
|
|
|
|
|
import java.util.Locale
|
|
|
|
|
import kotlin.collections.component1
|
|
|
|
|
import kotlin.collections.component2
|
|
|
|
|
import kotlin.collections.set
|
|
|
|
|
import kotlin.properties.Delegates.observable
|
|
|
|
|
|
|
|
|
|
object SnodeAPI {
|
|
|
|
|
private val sodium by lazy { LazySodiumAndroid(SodiumAndroid()) }
|
|
|
|
@ -41,6 +55,12 @@ object SnodeAPI {
|
|
|
|
|
* user's clock is incorrect.
|
|
|
|
|
*/
|
|
|
|
|
internal var clockOffset = 0L
|
|
|
|
|
internal var forkInfo by observable(database.getForkInfo()) { _, oldValue, newValue ->
|
|
|
|
|
if (newValue > oldValue) {
|
|
|
|
|
Log.d("Loki", "Setting new fork info new: $newValue, old: $oldValue")
|
|
|
|
|
database.setForkInfo(newValue)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Settings
|
|
|
|
|
private val maxRetryCount = 6
|
|
|
|
@ -55,11 +75,10 @@ object SnodeAPI {
|
|
|
|
|
setOf( "https://storage.seed1.loki.network:$seedNodePort", "https://storage.seed3.loki.network:$seedNodePort", "https://public.loki.foundation:$seedNodePort" )
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
private val snodeFailureThreshold = 3
|
|
|
|
|
private val targetSwarmSnodeCount = 2
|
|
|
|
|
private val useOnionRequests = true
|
|
|
|
|
private const val snodeFailureThreshold = 3
|
|
|
|
|
private const val useOnionRequests = true
|
|
|
|
|
|
|
|
|
|
internal val useTestnet = false
|
|
|
|
|
const val useTestnet = false
|
|
|
|
|
|
|
|
|
|
// Error
|
|
|
|
|
internal sealed class Error(val description: String) : Exception(description) {
|
|
|
|
@ -254,11 +273,6 @@ object SnodeAPI {
|
|
|
|
|
return promise
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun getTargetSnodes(publicKey: String): Promise<List<Snode>, Exception> {
|
|
|
|
|
// SecureRandom() should be cryptographically secure
|
|
|
|
|
return getSwarm(publicKey).map { it.shuffled(SecureRandom()).take(targetSwarmSnodeCount) }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun getSwarm(publicKey: String): Promise<Set<Snode>, Exception> {
|
|
|
|
|
val cachedSwarm = database.getSwarm(publicKey)
|
|
|
|
|
if (cachedSwarm != null && cachedSwarm.size >= minimumSwarmSnodeCount) {
|
|
|
|
@ -266,7 +280,7 @@ object SnodeAPI {
|
|
|
|
|
cachedSwarmCopy.addAll(cachedSwarm)
|
|
|
|
|
return task { cachedSwarmCopy }
|
|
|
|
|
} else {
|
|
|
|
|
val parameters = mapOf( "pubKey" to if (useTestnet) publicKey.removing05PrefixIfNeeded() else publicKey )
|
|
|
|
|
val parameters = mapOf( "pubKey" to publicKey )
|
|
|
|
|
return getRandomSnode().bind {
|
|
|
|
|
invoke(Snode.Method.GetSwarm, it, publicKey, parameters)
|
|
|
|
|
}.map {
|
|
|
|
@ -277,28 +291,39 @@ object SnodeAPI {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun getRawMessages(snode: Snode, publicKey: String): RawResponsePromise {
|
|
|
|
|
// val userED25519KeyPair = MessagingModuleConfiguration.shared.getUserED25519KeyPair() ?: return Promise.ofFail(Error.NoKeyPair)
|
|
|
|
|
fun getRawMessages(snode: Snode, publicKey: String, requiresAuth: Boolean = true, namespace: Int = 0): RawResponsePromise {
|
|
|
|
|
val userED25519KeyPair = MessagingModuleConfiguration.shared.getUserED25519KeyPair() ?: return Promise.ofFail(Error.NoKeyPair)
|
|
|
|
|
// Get last message hash
|
|
|
|
|
val lastHashValue = database.getLastMessageHashValue(snode, publicKey) ?: ""
|
|
|
|
|
val lastHashValue = database.getLastMessageHashValue(snode, publicKey, namespace) ?: ""
|
|
|
|
|
val parameters = mutableMapOf<String,Any>(
|
|
|
|
|
"pubKey" to publicKey,
|
|
|
|
|
"last_hash" to lastHashValue,
|
|
|
|
|
)
|
|
|
|
|
// Construct signature
|
|
|
|
|
// val timestamp = Date().time + SnodeAPI.clockOffset
|
|
|
|
|
// val ed25519PublicKey = userED25519KeyPair.publicKey.asHexString
|
|
|
|
|
// val verificationData = "retrieve$timestamp".toByteArray()
|
|
|
|
|
// val signature = ByteArray(Sign.BYTES)
|
|
|
|
|
// try {
|
|
|
|
|
// sodium.cryptoSignDetached(signature, verificationData, verificationData.size.toLong(), userED25519KeyPair.secretKey.asBytes)
|
|
|
|
|
// } catch (exception: Exception) {
|
|
|
|
|
// return Promise.ofFail(Error.SigningFailed)
|
|
|
|
|
// }
|
|
|
|
|
if (requiresAuth) {
|
|
|
|
|
val timestamp = Date().time + SnodeAPI.clockOffset
|
|
|
|
|
val ed25519PublicKey = userED25519KeyPair.publicKey.asHexString
|
|
|
|
|
val signature = ByteArray(Sign.BYTES)
|
|
|
|
|
val verificationData =
|
|
|
|
|
if (namespace != 0) "retrieve$namespace$timestamp".toByteArray()
|
|
|
|
|
else "retrieve$timestamp".toByteArray()
|
|
|
|
|
try {
|
|
|
|
|
sodium.cryptoSignDetached(signature, verificationData, verificationData.size.toLong(), userED25519KeyPair.secretKey.asBytes)
|
|
|
|
|
} catch (exception: Exception) {
|
|
|
|
|
return Promise.ofFail(Error.SigningFailed)
|
|
|
|
|
}
|
|
|
|
|
parameters["timestamp"] = timestamp
|
|
|
|
|
parameters["pubkey_ed25519"] = ed25519PublicKey
|
|
|
|
|
parameters["signature"] = Base64.encodeBytes(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
|
|
|
|
|
val parameters = mapOf(
|
|
|
|
|
"pubKey" to if (useTestnet) publicKey.removing05PrefixIfNeeded() else publicKey,
|
|
|
|
|
"lastHash" to lastHashValue,
|
|
|
|
|
// "timestamp" to timestamp,
|
|
|
|
|
// "pubkey_ed25519" to ed25519PublicKey,
|
|
|
|
|
// "signature" to Base64.encodeBytes(signature)
|
|
|
|
|
)
|
|
|
|
|
return invoke(Snode.Method.GetMessages, snode, publicKey, parameters)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@ -317,14 +342,35 @@ object SnodeAPI {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun sendMessage(message: SnodeMessage): Promise<Set<RawResponsePromise>, Exception> {
|
|
|
|
|
val destination = if (useTestnet) message.recipient.removing05PrefixIfNeeded() else message.recipient
|
|
|
|
|
fun sendMessage(message: SnodeMessage, requiresAuth: Boolean = false, namespace: Int = 0): RawResponsePromise {
|
|
|
|
|
val destination = message.recipient
|
|
|
|
|
return retryIfNeeded(maxRetryCount) {
|
|
|
|
|
getTargetSnodes(destination).map { swarm ->
|
|
|
|
|
swarm.map { snode ->
|
|
|
|
|
val parameters = message.toJSON()
|
|
|
|
|
invoke(Snode.Method.SendMessage, snode, destination, parameters)
|
|
|
|
|
}.toSet()
|
|
|
|
|
val module = MessagingModuleConfiguration.shared
|
|
|
|
|
val userED25519KeyPair = module.getUserED25519KeyPair() ?: return@retryIfNeeded Promise.ofFail(Error.NoKeyPair)
|
|
|
|
|
val parameters = message.toJSON().toMutableMap<String,Any>()
|
|
|
|
|
// Construct signature
|
|
|
|
|
if (requiresAuth) {
|
|
|
|
|
val sigTimestamp = System.currentTimeMillis() + SnodeAPI.clockOffset
|
|
|
|
|
val ed25519PublicKey = userED25519KeyPair.publicKey.asHexString
|
|
|
|
|
val signature = ByteArray(Sign.BYTES)
|
|
|
|
|
// assume namespace here is non-zero, as zero namespace doesn't require auth
|
|
|
|
|
val verificationData = "store$namespace$sigTimestamp".toByteArray()
|
|
|
|
|
try {
|
|
|
|
|
sodium.cryptoSignDetached(signature, verificationData, verificationData.size.toLong(), userED25519KeyPair.secretKey.asBytes)
|
|
|
|
|
} catch (exception: Exception) {
|
|
|
|
|
return@retryIfNeeded Promise.ofFail(Error.SigningFailed)
|
|
|
|
|
}
|
|
|
|
|
parameters["sig_timestamp"] = sigTimestamp
|
|
|
|
|
parameters["pubkey_ed25519"] = ed25519PublicKey
|
|
|
|
|
parameters["signature"] = Base64.encodeBytes(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
|
|
|
|
|
}
|
|
|
|
|
getSingleTargetSnode(destination).bind { snode ->
|
|
|
|
|
invoke(Snode.Method.SendMessage, snode, destination, parameters)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
@ -426,29 +472,29 @@ object SnodeAPI {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun parseRawMessagesResponse(rawResponse: RawResponse, snode: Snode, publicKey: String): List<Pair<SignalServiceProtos.Envelope, String?>> {
|
|
|
|
|
fun parseRawMessagesResponse(rawResponse: RawResponse, snode: Snode, publicKey: String, namespace: Int = 0): List<Pair<SignalServiceProtos.Envelope, String?>> {
|
|
|
|
|
val messages = rawResponse["messages"] as? List<*>
|
|
|
|
|
return if (messages != null) {
|
|
|
|
|
updateLastMessageHashValueIfPossible(snode, publicKey, messages)
|
|
|
|
|
val newRawMessages = removeDuplicates(publicKey, messages)
|
|
|
|
|
updateLastMessageHashValueIfPossible(snode, publicKey, messages, namespace)
|
|
|
|
|
val newRawMessages = removeDuplicates(publicKey, messages, namespace)
|
|
|
|
|
return parseEnvelopes(newRawMessages);
|
|
|
|
|
} else {
|
|
|
|
|
listOf()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private fun updateLastMessageHashValueIfPossible(snode: Snode, publicKey: String, rawMessages: List<*>) {
|
|
|
|
|
private fun updateLastMessageHashValueIfPossible(snode: Snode, publicKey: String, rawMessages: List<*>, namespace: Int) {
|
|
|
|
|
val lastMessageAsJSON = rawMessages.lastOrNull() as? Map<*, *>
|
|
|
|
|
val hashValue = lastMessageAsJSON?.get("hash") as? String
|
|
|
|
|
if (hashValue != null) {
|
|
|
|
|
database.setLastMessageHashValue(snode, publicKey, hashValue)
|
|
|
|
|
database.setLastMessageHashValue(snode, publicKey, hashValue, namespace)
|
|
|
|
|
} else if (rawMessages.isNotEmpty()) {
|
|
|
|
|
Log.d("Loki", "Failed to update last message hash value from: ${rawMessages.prettifiedDescription()}.")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private fun removeDuplicates(publicKey: String, rawMessages: List<*>): List<*> {
|
|
|
|
|
val receivedMessageHashValues = database.getReceivedMessageHashValues(publicKey)?.toMutableSet() ?: mutableSetOf()
|
|
|
|
|
private fun removeDuplicates(publicKey: String, rawMessages: List<*>, namespace: Int): List<*> {
|
|
|
|
|
val receivedMessageHashValues = database.getReceivedMessageHashValues(publicKey, namespace)?.toMutableSet() ?: mutableSetOf()
|
|
|
|
|
val result = rawMessages.filter { rawMessage ->
|
|
|
|
|
val rawMessageAsJSON = rawMessage as? Map<*, *>
|
|
|
|
|
val hashValue = rawMessageAsJSON?.get("hash") as? String
|
|
|
|
@ -461,7 +507,7 @@ object SnodeAPI {
|
|
|
|
|
false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
database.setReceivedMessageHashValues(publicKey, receivedMessageHashValues)
|
|
|
|
|
database.setReceivedMessageHashValues(publicKey, receivedMessageHashValues, namespace)
|
|
|
|
|
return result
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|