@ -3,24 +3,29 @@
package org.session.libsession.snode
import android.os.Build
import com.goterl.lazysodium.LazySodiumAndroid
import com.goterl.lazysodium.SodiumAndroid
import com.goterl.lazysodium.exceptions.SodiumException
import com.goterl.lazysodium.interfaces.AEAD
import com.goterl.lazysodium.interfaces.GenericHash
import com.goterl.lazysodium.interfaces.PwHash
import com.goterl.lazysodium.interfaces.SecretBox
import com.goterl.lazysodium.utils.Key
import nl.komponents.kovenant.*
import nl.komponents.kovenant.functional.bind
import nl.komponents.kovenant.functional.map
import org.session.libsession.messaging.utilities.MessageWrapper
import org.session.libsignal.crypto.getRandomElement
import org.session.libsignal.protos.SignalServiceProtos
import org.session.libsignal.utilities.Snode
import org.session.libsignal.utilities.HTTP
import org.session.libsignal.database.LokiAPIDatabaseProtocol
import org.session.libsignal.utilities.Broadcaster
import org.session.libsignal.utilities.prettifiedDescription
import org.session.libsignal.utilities.removing05PrefixIfNeeded
import org.session.libsignal.utilities.retryIfNeeded
import org.session.libsignal.protos.SignalServiceProtos
import org.session.libsignal.utilities.*
import org.session.libsignal.utilities. Log
import org.session.libsignal.utilities.Base64
import java.security.SecureRandom
import java.util.*
object SnodeAPI {
private val sodium by lazy { LazySodiumAndroid ( SodiumAndroid ( ) ) }
private val database : LokiAPIDatabaseProtocol
get ( ) = SnodeModule . shared . storage
private val broadcaster : Broadcaster
@ -54,10 +59,14 @@ object SnodeAPI {
internal sealed class Error ( val description : String ) : Exception ( description ) {
object Generic : Error ( " An error occurred. " )
object ClockOutOfSync : Error ( " Your clock is out of sync with the Service Node network. " )
// 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. " )
}
// Internal API
internal fun invoke ( method : Snode . Method , snode : Snode , publicKey : String , parameters : Map < String , String > ) : RawResponsePromise {
internal fun invoke ( method : Snode . Method , snode : Snode , publicKey : String ? = null , parameters : Map < String , Any > ) : RawResponsePromise {
val url = " ${snode.address} : ${snode.port} /storage_rpc/v1 "
if ( useOnionRequests ) {
return OnionRequestAPI . sendOnionRequest ( method , parameters , snode , publicKey )
@ -153,6 +162,91 @@ object SnodeAPI {
}
// Public API
fun getSessionIDFor ( onsName : String ) : Promise < String , Exception > {
val deferred = deferred < String , Exception > ( )
val promise = deferred . promise
val validationCount = 3
val sessionIDByteCount = 33
// Hash the ONS name using BLAKE2b
val onsName = onsName . toLowerCase ( Locale . ENGLISH )
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 )
// Ask 3 different snodes for the Session ID associated with the given name hash
val parameters = mapOf (
" endpoint " to " ons_resolve " ,
" params " to mapOf ( " type " to 0 , " name_hash " to base64EncodedNameHash )
)
val promises = ( 1. . validationCount ) . map {
getRandomSnode ( ) . bind { snode ->
retryIfNeeded ( maxRetryCount ) {
invoke ( Snode . Method . OxenDaemonRPCCall , snode , null , parameters )
}
}
}
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 ) {
val ciphertext = Hex . fromStringCondensed ( hexEncodedCiphertext )
val isArgon2Based = ( intermediate [ " nonce " ] == null )
if ( isArgon2Based ) {
// Handle old Argon2-based encryption used before HF16
val salt = ByteArray ( PwHash . SALTBYTES )
val key : ByteArray
val nonce = ByteArray ( SecretBox . NONCEBYTES )
val sessionIDAsData = ByteArray ( sessionIDByteCount )
try {
key = Key . fromHexString ( sodium . cryptoPwHash ( onsName , SecretBox . KEYBYTES , salt , PwHash . OPSLIMIT _MODERATE , PwHash . MEMLIMIT _MODERATE , PwHash . Alg . PWHASH _ALG _ARGON2ID13 ) ) . asBytes
} catch ( e : SodiumException ) {
deferred . reject ( Error . HashingFailed )
return @success
}
if ( ! sodium . cryptoSecretBoxOpenEasy ( sessionIDAsData , ciphertext , ciphertext . size . toLong ( ) , nonce , key ) ) {
deferred . reject ( Error . DecryptionFailed )
return @success
}
sessionIDs . add ( Hex . toStringCondensed ( sessionIDAsData ) )
} else {
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
}
val sessionIDAsData = ByteArray ( sessionIDByteCount )
if ( ! sodium . cryptoAeadXChaCha20Poly1305IetfDecrypt ( sessionIDAsData , null , null , ciphertext , ciphertext . size . toLong ( ) , null , 0 , nonce , key ) ) {
deferred . reject ( Error . DecryptionFailed )
return @success
}
sessionIDs . add ( Hex . toStringCondensed ( sessionIDAsData ) )
}
} else {
deferred . reject ( Error . Generic )
return @success
}
}
if ( sessionIDs . size == validationCount && sessionIDs . toSet ( ) . size == 1 ) {
deferred . resolve ( sessionIDs . first ( ) )
} else {
deferred . reject ( Error . ValidationFailed )
}
}
return promise
}
fun getTargetSnodes ( publicKey : String ) : Promise < List < Snode > , Exception > {
// SecureRandom() should be cryptographically secure
return getSwarm ( publicKey ) . map { it . shuffled ( SecureRandom ( ) ) . take ( targetSwarmSnodeCount ) }