@ -1,5 +1,6 @@
import PromiseKit
import SessionUtilitiesKit
import Sodium
@objc ( SNSnodeAPI )
public final class SnodeAPI : NSObject {
@ -31,12 +32,20 @@ public final class SnodeAPI : NSObject {
case generic
case clockOutOfSync
case snodePoolUpdatingFailed
// O N S
case decryptionFailed
case hashingFailed
case validationFailed
public var errorDescription : String ? {
switch self {
case . generic : return " An error occurred. "
case . clockOutOfSync : return " Your clock is out of sync with the Service Node network. Please check that your device's clock is set to automatic time. "
case . snodePoolUpdatingFailed : return " Failed to update the Service Node pool. "
// O N S
case . decryptionFailed : return " Couldn't decrypt ONS name. "
case . hashingFailed : return " Couldn't compute ONS name hash. "
case . validationFailed : return " ONS name validation failed. "
}
}
}
@ -109,7 +118,7 @@ public final class SnodeAPI : NSObject {
}
// MARK: I n t e r n a l A P I
internal static func invoke ( _ method : Snode . Method , on snode : Snode , associatedWith publicKey : String , parameters : JSON ) -> RawResponsePromise {
internal static func invoke ( _ method : Snode . Method , on snode : Snode , associatedWith publicKey : String ? = nil , parameters : JSON ) -> RawResponsePromise {
if useOnionRequests {
return OnionRequestAPI . sendOnionRequest ( to : snode , invoking : method , with : parameters , associatedWith : publicKey ) . map2 { $0 as Any }
} else {
@ -181,6 +190,71 @@ public final class SnodeAPI : NSObject {
}
// MARK: P u b l i c A P I
public static func getSessionID ( for onsName : String ) -> Promise < String > {
let sodium = Sodium ( )
let validationCount = 3
let sessionIDByteCount = 33
// T h e n a m e m u s t b e l o w e r c a s e d
let onsName = onsName . lowercased ( )
// H a s h t h e O N S n a m e u s i n g B L A K E 2 b
let nameAsData = [ UInt8 ] ( onsName . data ( using : String . Encoding . utf8 ) ! )
guard let nameHash = sodium . genericHash . hash ( message : nameAsData ) ,
let base64EncodedNameHash = nameHash . toBase64 ( ) else { return Promise ( error : Error . hashingFailed ) }
// A s k 3 d i f f e r e n t s n o d e s f o r t h e S e s s i o n I D a s s o c i a t e d w i t h t h e g i v e n n a m e h a s h
let parameters : [ String : Any ] = [ " name_hash " : base64EncodedNameHash ]
let promises = ( 0. . < validationCount ) . map { _ in
return getRandomSnode ( ) . then2 { snode in
attempt ( maxRetryCount : 4 , recoveringOn : Threading . workQueue ) {
invoke ( . getSessionIDForONSName , on : snode , parameters : parameters )
}
}
}
let ( promise , seal ) = Promise < String > . pending ( )
when ( resolved : promises ) . done2 { results in
var sessionIDs : [ String ] = [ ]
for result in results {
switch result {
case . rejected ( let error ) : return seal . reject ( error )
case . fulfilled ( let rawResponse ) :
guard let json = rawResponse as ? JSON , let x0 = json [ " result " ] as ? JSON ,
let x1 = x0 [ " entries " ] as ? [ JSON ] , let x2 = x1 . first ,
let hexEncodedEncryptedBlob = x2 [ " encrypted_value " ] as ? String else { return seal . reject ( HTTP . Error . invalidJSON ) }
let encryptedBlob = [ UInt8 ] ( Data ( hex : hexEncodedEncryptedBlob ) )
let isArgon2Based = ( encryptedBlob . count = = sessionIDByteCount + sodium . secretBox . MacBytes )
if isArgon2Based {
// H a n d l e o l d A r g o n 2 - b a s e d e n c r y p t i o n u s e d b e f o r e H F 1 6
let salt = [ UInt8 ] ( Data ( repeating : 0 , count : sodium . pwHash . SaltBytes ) )
guard let key = sodium . pwHash . hash ( outputLength : sodium . secretBox . KeyBytes , passwd : nameAsData , salt : salt ,
opsLimit : sodium . pwHash . OpsLimitModerate , memLimit : sodium . pwHash . MemLimitModerate , alg : . Argon2ID13 ) else { return seal . reject ( Error . hashingFailed ) }
let nonce = [ UInt8 ] ( Data ( repeating : 0 , count : sodium . secretBox . NonceBytes ) )
guard let sessionIDAsData = sodium . secretBox . open ( authenticatedCipherText : encryptedBlob , secretKey : key , nonce : nonce ) else {
return seal . reject ( Error . decryptionFailed )
}
sessionIDs . append ( sessionIDAsData . toHexString ( ) )
} else {
// B L A K E 2 b - b a s e d e n c r y p t i o n
guard let key = sodium . genericHash . hash ( message : nameAsData , key : nameHash ) else { // k e y = H ( n a m e , k e y = H ( n a m e ) )
return seal . reject ( Error . hashingFailed )
}
let nonceSize = sodium . aead . xchacha20poly1305ietf . NonceBytes
guard encryptedBlob . count >= ( sessionIDByteCount + sodium . aead . xchacha20poly1305ietf . ABytes + nonceSize ) else { // S h o u l d a l w a y s b e e q u a l i n p r a c t i c e
return seal . reject ( Error . decryptionFailed )
}
let nonce = [ UInt8 ] ( encryptedBlob [ ( encryptedBlob . endIndex - nonceSize ) . . < encryptedBlob . endIndex ] )
let ciphertext = [ UInt8 ] ( encryptedBlob [ 0 . . < ( encryptedBlob . endIndex - nonceSize ) ] )
guard let sessionIDAsData = sodium . aead . xchacha20poly1305ietf . decrypt ( authenticatedCipherText : ciphertext , secretKey : key , nonce : nonce ) else {
return seal . reject ( Error . decryptionFailed )
}
sessionIDs . append ( sessionIDAsData . toHexString ( ) )
}
}
}
guard sessionIDs . count = = validationCount && Set ( sessionIDs ) . count = = 1 else { return seal . reject ( Error . validationFailed ) }
seal . fulfill ( sessionIDs . first ! )
}
return promise
}
public static func getTargetSnodes ( for publicKey : String ) -> Promise < [ Snode ] > {
// s h u f f l e d ( ) u s e s t h e s y s t e m ' s d e f a u l t r a n d o m g e n e r a t o r , w h i c h i s c r y p t o g r a p h i c a l l y s e c u r e
return getSwarm ( for : publicKey ) . map2 { Array ( $0 . shuffled ( ) . prefix ( targetSwarmSnodeCount ) ) }