2020-12-02 06:38:12 +01:00
@file : Suppress ( " NAME_SHADOWING " )
package org.session.libsession.snode
2021-06-18 08:01:34 +02:00
import android.content.Context
2021-04-07 06:49:52 +02:00
import android.os.Build
2021-05-26 08:34:08 +02:00
import com.goterl.lazysodium.LazySodiumAndroid
import com.goterl.lazysodium.SodiumAndroid
2021-05-27 08:23:15 +02:00
import com.goterl.lazysodium.exceptions.SodiumException
import com.goterl.lazysodium.interfaces.GenericHash
2021-05-26 08:34:08 +02:00
import com.goterl.lazysodium.interfaces.PwHash
import com.goterl.lazysodium.interfaces.SecretBox
2021-06-18 08:01:34 +02:00
import com.goterl.lazysodium.interfaces.Sign
2021-05-27 08:23:15 +02:00
import com.goterl.lazysodium.utils.Key
2020-12-10 05:32:38 +01:00
import nl.komponents.kovenant.*
2020-12-02 06:38:12 +01:00
import nl.komponents.kovenant.functional.bind
import nl.komponents.kovenant.functional.map
2021-06-18 08:01:34 +02:00
import org.session.libsession.messaging.MessagingModuleConfiguration
2021-04-27 06:36:03 +02:00
import org.session.libsession.messaging.utilities.MessageWrapper
2021-05-18 07:55:24 +02:00
import org.session.libsignal.crypto.getRandomElement
2021-05-18 01:50:16 +02:00
import org.session.libsignal.database.LokiAPIDatabaseProtocol
2021-05-27 08:23:15 +02:00
import org.session.libsignal.protos.SignalServiceProtos
2021-02-01 02:10:48 +01:00
import org.session.libsignal.utilities.*
2021-05-26 08:34:08 +02:00
import org.session.libsignal.utilities.Base64
2020-12-02 06:38:12 +01:00
import java.security.SecureRandom
2021-05-26 08:34:08 +02:00
import java.util.*
2021-06-18 08:01:34 +02:00
import kotlin.Pair
2020-12-02 06:38:12 +01:00
object SnodeAPI {
2021-05-26 08:34:08 +02:00
private val sodium by lazy { LazySodiumAndroid ( SodiumAndroid ( ) ) }
2021-04-26 02:26:31 +02:00
private val database : LokiAPIDatabaseProtocol
get ( ) = SnodeModule . shared . storage
private val broadcaster : Broadcaster
get ( ) = SnodeModule . shared . broadcaster
2020-12-02 06:38:12 +01:00
internal var snodeFailureCount : MutableMap < Snode , Int > = mutableMapOf ( )
internal var snodePool : Set < Snode >
get ( ) = database . getSnodePool ( )
set ( newValue ) { database . setSnodePool ( newValue ) }
// Settings
private val maxRetryCount = 6
2021-05-06 07:46:22 +02:00
private val minimumSnodePoolCount = 12
2021-05-13 02:38:13 +02:00
private val minimumSwarmSnodeCount = 3
2021-04-26 02:26:31 +02:00
// 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 4433
2021-04-20 09:22:36 +02:00
private val seedNodePool by lazy {
if ( useTestnet ) {
setOf ( " http://public.loki.foundation:38157 " )
} else {
2021-04-28 09:41:30 +02:00
setOf ( " https://storage.seed1.loki.network: $seedNodePort " , " https://storage.seed3.loki.network: $seedNodePort " , " https://public.loki.foundation: $seedNodePort " )
2021-04-20 09:22:36 +02:00
}
}
2021-05-13 02:38:13 +02:00
private val snodeFailureThreshold = 3
2020-12-02 06:38:12 +01:00
private val targetSwarmSnodeCount = 2
private val useOnionRequests = true
2021-07-12 06:35:22 +02:00
internal val useTestnet = false
2020-12-02 06:38:12 +01:00
// Error
2021-03-16 06:31:52 +01:00
internal sealed class Error ( val description : String ) : Exception ( description ) {
2020-12-02 06:38:12 +01:00
object Generic : Error ( " An error occurred. " )
2021-04-26 02:26:31 +02:00
object ClockOutOfSync : Error ( " Your clock is out of sync with the Service Node network. " )
2021-05-26 08:34:08 +02:00
// ONS
object DecryptionFailed : Error ( " Couldn't decrypt ONS name. " )
object HashingFailed : Error ( " Couldn't compute ONS name hash. " )
object ValidationFailed : Error ( " ONS name validation failed. " )
2020-12-02 06:38:12 +01:00
}
// Internal API
2021-05-26 08:34:08 +02:00
internal fun invoke ( method : Snode . Method , snode : Snode , publicKey : String ? = null , parameters : Map < String , Any > ) : RawResponsePromise {
2020-12-02 06:38:12 +01:00
val url = " ${snode.address} : ${snode.port} /storage_rpc/v1 "
if ( useOnionRequests ) {
return OnionRequestAPI . sendOnionRequest ( method , parameters , snode , publicKey )
} else {
val deferred = deferred < Map < * , * > , Exception > ( )
2021-01-27 00:54:25 +01:00
ThreadUtils . queue {
2020-12-02 06:38:12 +01:00
val payload = mapOf ( " method " to method . rawValue , " params " to parameters )
try {
val json = HTTP . execute ( HTTP . Verb . POST , url , payload )
deferred . resolve ( json )
} catch ( exception : Exception ) {
val httpRequestFailedException = exception as ? HTTP . HTTPRequestFailedException
if ( httpRequestFailedException != null ) {
val error = handleSnodeError ( httpRequestFailedException . statusCode , httpRequestFailedException . json , snode , publicKey )
2021-01-27 00:54:25 +01:00
if ( error != null ) { return @queue deferred . reject ( exception ) }
2020-12-02 06:38:12 +01:00
}
Log . d ( " Loki " , " Unhandled exception: $exception . " )
deferred . reject ( exception )
}
2021-01-27 00:54:25 +01:00
}
2020-12-02 06:38:12 +01:00
return deferred . promise
}
}
internal fun getRandomSnode ( ) : Promise < Snode , Exception > {
val snodePool = this . snodePool
if ( snodePool . count ( ) < minimumSnodePoolCount ) {
val target = seedNodePool . random ( )
val url = " $target /json_rpc "
Log . d ( " Loki " , " Populating snode pool using: $target . " )
val parameters = mapOf (
" method " to " get_n_service_nodes " ,
" params " to mapOf (
2021-06-22 09:01:27 +02:00
" active_only " to true ,
" limit " to 256 ,
" fields " to mapOf ( " public_ip " to true , " storage_port " to true , " pubkey_x25519 " to true , " pubkey_ed25519 " to true )
2020-12-02 06:38:12 +01:00
)
)
val deferred = deferred < Snode , Exception > ( )
2021-04-26 02:26:31 +02:00
deferred < Snode , Exception > ( )
2021-01-27 00:54:25 +01:00
ThreadUtils . queue {
2020-12-02 06:38:12 +01:00
try {
val json = HTTP . execute ( HTTP . Verb . POST , url , parameters , useSeedNodeConnection = true )
val intermediate = json [ " result " ] as ? Map < * , * >
val rawSnodes = intermediate ?. get ( " service_node_states " ) as ? List < * >
if ( rawSnodes != null ) {
val snodePool = rawSnodes . mapNotNull { rawSnode ->
val rawSnodeAsJSON = rawSnode as ? Map < * , * >
val address = rawSnodeAsJSON ?. get ( " public_ip " ) as ? String
val port = rawSnodeAsJSON ?. get ( " storage_port " ) as ? Int
val ed25519Key = rawSnodeAsJSON ?. get ( " pubkey_ed25519 " ) as ? String
val x25519Key = rawSnodeAsJSON ?. get ( " pubkey_x25519 " ) as ? String
if ( address != null && port != null && ed25519Key != null && x25519Key != null && address != " 0.0.0.0 " ) {
Snode ( " https:// $address " , port , Snode . KeySet ( ed25519Key , x25519Key ) )
} else {
Log . d ( " Loki " , " Failed to parse: ${rawSnode?.prettifiedDescription()} . " )
null
}
} . toMutableSet ( )
Log . d ( " Loki " , " Persisting snode pool to database. " )
this . snodePool = snodePool
try {
deferred . resolve ( snodePool . getRandomElement ( ) )
} catch ( exception : Exception ) {
Log . d ( " Loki " , " Got an empty snode pool from: $target . " )
deferred . reject ( SnodeAPI . Error . Generic )
}
} else {
Log . d ( " Loki " , " Failed to update snode pool from: ${(rawSnodes as List<*>?)?.prettifiedDescription()} . " )
deferred . reject ( SnodeAPI . Error . Generic )
}
} catch ( exception : Exception ) {
deferred . reject ( exception )
}
2021-01-27 00:54:25 +01:00
}
2020-12-02 06:38:12 +01:00
return deferred . promise
} else {
return Promise . of ( snodePool . getRandomElement ( ) )
}
}
internal fun dropSnodeFromSwarmIfNeeded ( snode : Snode , publicKey : String ) {
val swarm = database . getSwarm ( publicKey ) ?. toMutableSet ( )
if ( swarm != null && swarm . contains ( snode ) ) {
swarm . remove ( snode )
database . setSwarm ( publicKey , swarm )
}
}
internal fun getSingleTargetSnode ( publicKey : String ) : Promise < Snode , Exception > {
// SecureRandom() should be cryptographically secure
return getSwarm ( publicKey ) . map { it . shuffled ( SecureRandom ( ) ) . random ( ) }
}
// Public API
2021-05-26 08:34:08 +02:00
fun getSessionIDFor ( onsName : String ) : Promise < String , Exception > {
2021-05-27 08:23:15 +02:00
val deferred = deferred < String , Exception > ( )
val promise = deferred . promise
2021-05-26 08:34:08 +02:00
val validationCount = 3
val sessionIDByteCount = 33
// Hash the ONS name using BLAKE2b
2021-06-02 02:51:36 +02:00
val onsName = onsName . toLowerCase ( Locale . US )
2021-05-27 08:23:15 +02:00
val nameAsData = onsName . toByteArray ( )
val nameHash = ByteArray ( GenericHash . BYTES )
if ( ! sodium . cryptoGenericHash ( nameHash , nameHash . size , nameAsData , nameAsData . size . toLong ( ) ) ) {
deferred . reject ( Error . HashingFailed )
return promise
}
val base64EncodedNameHash = Base64 . encodeBytes ( nameHash )
2021-05-26 08:34:08 +02:00
// Ask 3 different snodes for the Session ID associated with the given name hash
val parameters = mapOf (
2021-06-22 09:01:27 +02:00
" endpoint " to " ons_resolve " ,
" params " to mapOf ( " type " to 0 , " name_hash " to base64EncodedNameHash )
2021-05-26 08:34:08 +02:00
)
2021-05-28 03:22:06 +02:00
val promises = ( 1. . validationCount ) . map {
2021-05-26 08:34:08 +02:00
getRandomSnode ( ) . bind { snode ->
2021-05-28 07:14:05 +02:00
retryIfNeeded ( maxRetryCount ) {
invoke ( Snode . Method . OxenDaemonRPCCall , snode , null , parameters )
}
2021-05-26 08:34:08 +02:00
}
}
all ( promises ) . success { results ->
val sessionIDs = mutableListOf < String > ( )
for ( json in results ) {
val intermediate = json [ " result " ] as ? Map < * , * >
val hexEncodedCiphertext = intermediate ?. get ( " encrypted_value " ) as ? String
if ( hexEncodedCiphertext != null ) {
2021-05-28 03:22:06 +02:00
val ciphertext = Hex . fromStringCondensed ( hexEncodedCiphertext )
2021-05-26 08:34:08 +02:00
val isArgon2Based = ( intermediate [ " nonce " ] == null )
if ( isArgon2Based ) {
// Handle old Argon2-based encryption used before HF16
val salt = ByteArray ( PwHash . SALTBYTES )
2021-05-28 03:22:06 +02:00
val key : ByteArray
2021-05-26 08:34:08 +02:00
val nonce = ByteArray ( SecretBox . NONCEBYTES )
2021-05-28 03:22:06 +02:00
val sessionIDAsData = ByteArray ( sessionIDByteCount )
2021-05-27 08:23:15 +02:00
try {
2021-05-28 03:22:06 +02:00
key = Key . fromHexString ( sodium . cryptoPwHash ( onsName , SecretBox . KEYBYTES , salt , PwHash . OPSLIMIT _MODERATE , PwHash . MEMLIMIT _MODERATE , PwHash . Alg . PWHASH _ALG _ARGON2ID13 ) ) . asBytes
2021-05-27 08:23:15 +02:00
} catch ( e : SodiumException ) {
deferred . reject ( Error . HashingFailed )
return @success
}
2021-05-28 03:22:06 +02:00
if ( ! sodium . cryptoSecretBoxOpenEasy ( sessionIDAsData , ciphertext , ciphertext . size . toLong ( ) , nonce , key ) ) {
2021-05-27 08:23:15 +02:00
deferred . reject ( Error . DecryptionFailed )
return @success
}
2021-05-28 03:22:06 +02:00
sessionIDs . add ( Hex . toStringCondensed ( sessionIDAsData ) )
2021-05-26 08:34:08 +02:00
} else {
2021-05-27 08:23:15 +02:00
val hexEncodedNonce = intermediate [ " nonce " ] as ? String
if ( hexEncodedNonce == null ) {
deferred . reject ( Error . Generic )
return @success
}
val nonce = Hex . fromStringCondensed ( hexEncodedNonce )
val key = ByteArray ( GenericHash . BYTES )
if ( ! sodium . cryptoGenericHash ( key , key . size , nameAsData , nameAsData . size . toLong ( ) , nameHash , nameHash . size ) ) {
deferred . reject ( Error . HashingFailed )
return @success
}
2021-05-28 04:04:37 +02:00
val sessionIDAsData = ByteArray ( sessionIDByteCount )
if ( ! sodium . cryptoAeadXChaCha20Poly1305IetfDecrypt ( sessionIDAsData , null , null , ciphertext , ciphertext . size . toLong ( ) , null , 0 , nonce , key ) ) {
2021-05-27 08:23:15 +02:00
deferred . reject ( Error . DecryptionFailed )
return @success
}
2021-05-28 04:04:37 +02:00
sessionIDs . add ( Hex . toStringCondensed ( sessionIDAsData ) )
2021-05-26 08:34:08 +02:00
}
} else {
deferred . reject ( Error . Generic )
2021-05-27 08:23:15 +02:00
return @success
2021-05-26 08:34:08 +02:00
}
}
2021-05-27 08:23:15 +02:00
if ( sessionIDs . size == validationCount && sessionIDs . toSet ( ) . size == 1 ) {
deferred . resolve ( sessionIDs . first ( ) )
} else {
deferred . reject ( Error . ValidationFailed )
}
2021-05-26 08:34:08 +02:00
}
return promise
}
2020-12-02 06:38:12 +01:00
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 ) {
val cachedSwarmCopy = mutableSetOf < Snode > ( ) // Workaround for a Kotlin compiler issue
cachedSwarmCopy . addAll ( cachedSwarm )
return task { cachedSwarmCopy }
} else {
2021-04-20 09:22:36 +02:00
val parameters = mapOf ( " pubKey " to if ( useTestnet ) publicKey . removing05PrefixIfNeeded ( ) else publicKey )
2020-12-02 06:38:12 +01:00
return getRandomSnode ( ) . bind {
invoke ( Snode . Method . GetSwarm , it , publicKey , parameters )
2021-04-26 02:26:31 +02:00
} . map {
2020-12-02 06:38:12 +01:00
parseSnodes ( it ) . toSet ( )
} . success {
database . setSwarm ( publicKey , it )
}
}
}
fun getRawMessages ( snode : Snode , publicKey : String ) : RawResponsePromise {
val lastHashValue = database . getLastMessageHashValue ( snode , publicKey ) ?: " "
2021-04-20 09:22:36 +02:00
val parameters = mapOf ( " pubKey " to if ( useTestnet ) publicKey . removing05PrefixIfNeeded ( ) else publicKey , " lastHash " to lastHashValue )
2020-12-02 06:38:12 +01:00
return invoke ( Snode . Method . GetMessages , snode , publicKey , parameters )
}
fun getMessages ( publicKey : String ) : MessageListPromise {
return retryIfNeeded ( maxRetryCount ) {
2021-04-26 02:26:31 +02:00
getSingleTargetSnode ( publicKey ) . bind { snode ->
getRawMessages ( snode , publicKey ) . map { parseRawMessagesResponse ( it , snode , publicKey ) }
2020-12-02 06:38:12 +01:00
}
}
}
2021-06-18 08:01:34 +02:00
fun getNetworkTime ( snode : Snode ) : Promise < Pair < Snode , Long > , Exception > {
return invoke ( Snode . Method . Info , snode , null , emptyMap ( ) ) . map { rawResponse ->
2021-06-22 09:01:27 +02:00
val timestamp = rawResponse [ " timestamp " ] as ? Long ?: - 1
2021-06-18 08:01:34 +02:00
snode to timestamp
}
}
2020-12-02 06:38:12 +01:00
fun sendMessage ( message : SnodeMessage ) : Promise < Set < RawResponsePromise > , Exception > {
2021-04-20 09:22:36 +02:00
val destination = if ( useTestnet ) message . recipient . removing05PrefixIfNeeded ( ) else message . recipient
2020-12-02 06:38:12 +01:00
return retryIfNeeded ( maxRetryCount ) {
2021-03-02 02:24:09 +01:00
getTargetSnodes ( destination ) . map { swarm ->
2020-12-02 06:38:12 +01:00
swarm . map { snode ->
val parameters = message . toJSON ( )
2021-06-29 06:20:52 +02:00
invoke ( Snode . Method . SendMessage , snode , destination , parameters )
2020-12-02 06:38:12 +01:00
} . toSet ( )
}
}
}
// Parsing
private fun parseSnodes ( rawResponse : Any ) : List < Snode > {
val json = rawResponse as ? Map < * , * >
val rawSnodes = json ?. get ( " snodes " ) as ? List < * >
if ( rawSnodes != null ) {
return rawSnodes . mapNotNull { rawSnode ->
val rawSnodeAsJSON = rawSnode as ? Map < * , * >
val address = rawSnodeAsJSON ?. get ( " ip " ) as ? String
val portAsString = rawSnodeAsJSON ?. get ( " port " ) as ? String
val port = portAsString ?. toInt ( )
val ed25519Key = rawSnodeAsJSON ?. get ( " pubkey_ed25519 " ) as ? String
val x25519Key = rawSnodeAsJSON ?. get ( " pubkey_x25519 " ) as ? String
if ( address != null && port != null && ed25519Key != null && x25519Key != null && address != " 0.0.0.0 " ) {
Snode ( " https:// $address " , port , Snode . KeySet ( ed25519Key , x25519Key ) )
} else {
Log . d ( " Loki " , " Failed to parse snode from: ${rawSnode?.prettifiedDescription()} . " )
null
}
}
} else {
Log . d ( " Loki " , " Failed to parse snodes from: ${rawResponse.prettifiedDescription()} . " )
return listOf ( )
}
}
2021-06-22 09:01:27 +02:00
fun deleteAllMessages ( context : Context ) : Promise < Map < String , Boolean > , Exception > {
return retryIfNeeded ( maxRetryCount ) {
// considerations: timestamp off in retrying logic, not being able to re-sign with latest timestamp? do we just not retry this as it will be synchronous
2021-07-12 06:27:14 +02:00
val module = MessagingModuleConfiguration . shared
val userED25519KeyPair = module . keyPairProvider ( ) ?: return @retryIfNeeded Promise . ofFail ( Error . Generic )
val userPublicKey = module . storage . getUserPublicKey ( ) ?: return @retryIfNeeded Promise . ofFail ( Error . Generic )
2021-06-22 09:01:27 +02:00
getSingleTargetSnode ( userPublicKey ) . bind { snode ->
retryIfNeeded ( maxRetryCount ) {
getNetworkTime ( snode ) . bind { ( _ , timestamp ) ->
val signature = ByteArray ( Sign . BYTES )
val data = ( Snode . Method . DeleteAll . rawValue + timestamp . toString ( ) ) . toByteArray ( )
sodium . cryptoSignDetached ( signature , data , data . size . toLong ( ) , userED25519KeyPair . secretKey . asBytes )
val deleteMessageParams = mapOf (
" pubkey " to userPublicKey ,
" pubkey_ed25519 " to userED25519KeyPair . publicKey . asHexString ,
" timestamp " to timestamp ,
" signature " to Base64 . encodeBytes ( signature )
)
invoke ( Snode . Method . DeleteAll , snode , userPublicKey , deleteMessageParams ) . map { rawResponse -> parseDeletions ( userPublicKey , timestamp , rawResponse ) } . fail { e ->
Log . e ( " Loki " , " Failed to clear data " , e )
}
}
}
}
}
}
2021-04-27 06:36:03 +02:00
fun parseRawMessagesResponse ( rawResponse : RawResponse , snode : Snode , publicKey : String ) : List < SignalServiceProtos . Envelope > {
2020-12-02 06:38:12 +01:00
val messages = rawResponse [ " messages " ] as ? List < * >
return if ( messages != null ) {
updateLastMessageHashValueIfPossible ( snode , publicKey , messages )
2021-04-27 06:36:03 +02:00
val newRawMessages = removeDuplicates ( publicKey , messages )
return parseEnvelopes ( newRawMessages ) ;
2020-12-02 06:38:12 +01:00
} else {
2021-04-27 06:36:03 +02:00
listOf ( )
2020-12-02 06:38:12 +01:00
}
}
private fun updateLastMessageHashValueIfPossible ( snode : Snode , publicKey : String , rawMessages : List < * > ) {
val lastMessageAsJSON = rawMessages . lastOrNull ( ) as ? Map < * , * >
val hashValue = lastMessageAsJSON ?. get ( " hash " ) as ? String
if ( hashValue != null ) {
database . setLastMessageHashValue ( snode , publicKey , hashValue )
} 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 ( )
2021-05-13 02:38:13 +02:00
val result = rawMessages . filter { rawMessage ->
2020-12-02 06:38:12 +01:00
val rawMessageAsJSON = rawMessage as ? Map < * , * >
val hashValue = rawMessageAsJSON ?. get ( " hash " ) as ? String
if ( hashValue != null ) {
val isDuplicate = receivedMessageHashValues . contains ( hashValue )
receivedMessageHashValues . add ( hashValue )
!is Duplicate
} else {
Log . d ( " Loki " , " Missing hash value for message: ${rawMessage?.prettifiedDescription()} . " )
false
}
}
2021-05-13 02:38:13 +02:00
database . setReceivedMessageHashValues ( publicKey , receivedMessageHashValues )
return result
2020-12-02 06:38:12 +01:00
}
2021-04-27 06:36:03 +02:00
private fun parseEnvelopes ( rawMessages : List < * > ) : List < SignalServiceProtos . Envelope > {
return rawMessages . mapNotNull { rawMessage ->
val rawMessageAsJSON = rawMessage as ? Map < * , * >
val base64EncodedData = rawMessageAsJSON ?. get ( " data " ) as ? String
val data = base64EncodedData ?. let { Base64 . decode ( it ) }
if ( data != null ) {
try {
MessageWrapper . unwrap ( data )
} 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
}
}
}
2021-06-21 07:48:42 +02:00
@Suppress ( " UNCHECKED_CAST " )
2021-06-22 09:01:27 +02:00
private fun parseDeletions ( userPublicKey : String , timestamp : Long , rawResponse : RawResponse ) : Map < String , Boolean > {
val swarms = rawResponse [ " swarm " ] as ? Map < String , Any > ?: return mapOf ( )
2021-06-21 07:48:42 +02:00
val swarmResponsesValid = swarms . mapNotNull { ( nodePubKeyHex , rawMap ) ->
2021-06-22 09:01:27 +02:00
val map = rawMap as ? Map < String , Any > ?: return @mapNotNull null
2021-06-21 07:48:42 +02:00
/ * * Deletes all messages owned by the given pubkey on this SN and broadcasts the delete request to
* all other swarm members .
* Returns dict of :
* - " swarms " dict mapping ed25519 pubkeys ( in hex ) of swarm members to dict values of :
* - " failed " and other failure keys -- see `recursive` .
* - " deleted " : hashes of deleted messages .
* - " signature " : signature of ( PUBKEY _HEX || TIMESTAMP || DELETEDHASH [ 0 ] || .. . || DELETEDHASH [ N ] ) , signed
* by the node ' s ed25519 pubkey .
* /
// failure
val failed = map [ " failed " ] as ? Boolean ?: false
val code = map [ " code " ] as ? String
val reason = map [ " reason " ] as ? String
nodePubKeyHex to if ( failed ) {
2021-06-22 09:01:27 +02:00
Log . e ( " Loki " , " Failed to delete all from $nodePubKeyHex with error code $code and reason $reason " )
2021-06-21 07:48:42 +02:00
false
} else {
// success
val deleted = map [ " deleted " ] as List < String > // list of deleted hashes
val signature = map [ " signature " ] as String
2021-06-22 09:01:27 +02:00
val nodePubKey = Key . fromHexString ( nodePubKeyHex )
2021-06-21 07:48:42 +02:00
// signature of ( PUBKEY_HEX || TIMESTAMP || DELETEDHASH[0] || ... || DELETEDHASH[N] )
2021-06-22 09:01:27 +02:00
val message = ( userPublicKey + timestamp . toString ( ) + deleted . fold ( " " ) { a , v -> a + v } ) . toByteArray ( )
sodium . cryptoSignVerifyDetached ( Base64 . decode ( signature ) , message , message . size , nodePubKey . asBytes )
2021-06-21 07:48:42 +02:00
}
}
return swarmResponsesValid . toMap ( )
}
2021-04-27 06:36:03 +02:00
// endregion
2020-12-02 06:38:12 +01:00
// Error Handling
internal fun handleSnodeError ( statusCode : Int , json : Map < * , * > ? , snode : Snode , publicKey : String ? = null ) : Exception ? {
fun handleBadSnode ( ) {
val oldFailureCount = snodeFailureCount [ snode ] ?: 0
val newFailureCount = oldFailureCount + 1
snodeFailureCount [ snode ] = newFailureCount
Log . d ( " Loki " , " Couldn't reach snode at $snode ; setting failure count to $newFailureCount . " )
if ( newFailureCount >= snodeFailureThreshold ) {
Log . d ( " Loki " , " Failure threshold reached for: $snode ; dropping it. " )
if ( publicKey != null ) {
dropSnodeFromSwarmIfNeeded ( snode , publicKey )
}
snodePool = snodePool . toMutableSet ( ) . minus ( snode ) . toSet ( )
Log . d ( " Loki " , " Snode pool count: ${snodePool.count()} . " )
snodeFailureCount [ snode ] = 0
}
}
when ( statusCode ) {
2021-05-13 02:38:13 +02:00
400 , 500 , 502 , 503 -> { // Usually indicates that the snode isn't up to date
2020-12-02 06:38:12 +01:00
handleBadSnode ( )
}
406 -> {
Log . d ( " Loki " , " The user's clock is out of sync with the service node network. " )
broadcaster . broadcast ( " clockOutOfSync " )
return Error . ClockOutOfSync
}
421 -> {
// The snode isn't associated with the given public key anymore
if ( publicKey != null ) {
2021-05-13 02:42:53 +02:00
fun invalidateSwarm ( ) {
Log . d ( " Loki " , " Invalidating swarm for: $publicKey . " )
dropSnodeFromSwarmIfNeeded ( snode , publicKey )
}
if ( json != null ) {
val snodes = parseSnodes ( json )
if ( snodes . isNotEmpty ( ) ) {
database . setSwarm ( publicKey , snodes . toSet ( ) )
} else {
invalidateSwarm ( )
}
} else {
invalidateSwarm ( )
}
2020-12-02 06:38:12 +01:00
} else {
Log . d ( " Loki " , " Got a 421 without an associated public key. " )
}
}
else -> {
handleBadSnode ( )
Log . d ( " Loki " , " Unhandled response code: ${statusCode} . " )
return Error . Generic
}
}
return null
}
}
// Type Aliases
typealias RawResponse = Map < * , * >
2021-04-27 06:36:03 +02:00
typealias MessageListPromise = Promise < List < SignalServiceProtos . Envelope > , Exception >
2020-12-02 06:38:12 +01:00
typealias RawResponsePromise = Promise < RawResponse , Exception >