2019-09-26 03:32:47 +02:00
import PromiseKit
2020-06-05 02:38:44 +02:00
import SessionMetadataKit
2019-09-26 03:32:47 +02:00
2020-06-25 09:36:32 +02:00
// / B a s e c l a s s f o r ` F i l e S e r v e r A P I ` a n d ` P u b l i c C h a t A P I ` .
public class DotNetAPI : NSObject {
2020-04-21 02:36:23 +02:00
internal static var storage : OWSPrimaryStorage { OWSPrimaryStorage . shared ( ) }
internal static var userKeyPair : ECKeyPair { OWSIdentityManager . shared ( ) . identityKeyPair ( ) ! }
2019-09-26 03:32:47 +02:00
2019-10-18 00:46:44 +02:00
// MARK: S e t t i n g s
private static let attachmentType = " network.loki "
2019-09-26 03:32:47 +02:00
// MARK: E r r o r
2020-06-25 09:36:32 +02:00
@objc ( LKDotNetAPIError )
public class DotNetAPIError : NSError { // N o t c a l l e d ` E r r o r ` f o r O b j - C i n t e r o p e r a b l i t y
2020-02-10 05:47:15 +01:00
2020-06-25 09:36:32 +02:00
@objc public static let generic = DotNetAPIError ( domain : " DotNetAPIErrorDomain " , code : 1 , userInfo : [ NSLocalizedDescriptionKey : " An error occurred. " ] )
@objc public static let parsingFailed = DotNetAPIError ( domain : " DotNetAPIErrorDomain " , code : 2 , userInfo : [ NSLocalizedDescriptionKey : " Invalid file server response. " ] )
@objc public static let signingFailed = DotNetAPIError ( domain : " DotNetAPIErrorDomain " , code : 3 , userInfo : [ NSLocalizedDescriptionKey : " Couldn't sign message. " ] )
@objc public static let encryptionFailed = DotNetAPIError ( domain : " DotNetAPIErrorDomain " , code : 4 , userInfo : [ NSLocalizedDescriptionKey : " Couldn't encrypt file. " ] )
@objc public static let decryptionFailed = DotNetAPIError ( domain : " DotNetAPIErrorDomain " , code : 5 , userInfo : [ NSLocalizedDescriptionKey : " Couldn't decrypt file. " ] )
@objc public static let maxFileSizeExceeded = DotNetAPIError ( domain : " DotNetAPIErrorDomain " , code : 6 , userInfo : [ NSLocalizedDescriptionKey : " Maximum file size exceeded. " ] )
2019-09-26 03:32:47 +02:00
}
2020-06-25 09:36:32 +02:00
// MARK: S t o r a g e
2019-09-26 03:32:47 +02:00
// / T o b e o v e r r i d d e n b y s u b c l a s s e s .
internal class var authTokenCollection : String { preconditionFailure ( " authTokenCollection is abstract and must be overridden. " ) }
2020-04-29 04:11:12 +02:00
internal static func getAuthToken ( for server : String ) -> Promise < String > {
if let token = getAuthTokenFromDatabase ( for : server ) {
2020-02-17 05:14:00 +01:00
return Promise . value ( token )
} else {
2020-06-11 08:33:11 +02:00
return requestNewAuthToken ( for : server ) . then2 { submitAuthToken ( $0 , for : server ) } . map2 { token in
2020-06-11 04:23:06 +02:00
try ! Storage . writeSync { transaction in
2020-06-11 04:12:42 +02:00
setAuthToken ( for : server , to : token , in : transaction )
2020-04-29 04:11:12 +02:00
}
2020-05-06 08:31:03 +02:00
return token
2020-02-17 05:14:00 +01:00
}
}
}
2019-09-26 03:32:47 +02:00
2020-07-21 01:11:26 +02:00
private static func getAuthTokenFromDatabase ( for server : String ) -> String ? {
var result : String ? = nil
storage . dbReadConnection . read { transaction in
if transaction . hasObject ( forKey : server , inCollection : authTokenCollection ) {
result = transaction . object ( forKey : server , inCollection : authTokenCollection ) as ? String
}
}
return result
}
2020-04-29 04:11:12 +02:00
private static func setAuthToken ( for server : String , to newValue : String , in transaction : YapDatabaseReadWriteTransaction ) {
transaction . setObject ( newValue , forKey : server , inCollection : authTokenCollection )
2019-09-26 03:32:47 +02:00
}
2020-06-10 03:06:56 +02:00
public static func clearAuthToken ( for server : String ) {
2020-06-11 04:23:06 +02:00
try ! Storage . writeSync { transaction in
2020-06-11 04:12:42 +02:00
transaction . removeObject ( forKey : server , inCollection : authTokenCollection )
2020-06-10 03:06:56 +02:00
}
}
2019-09-26 03:32:47 +02:00
// MARK: L i f e c y c l e
override private init ( ) { }
2020-04-21 02:36:23 +02:00
// MARK: P r i v a t e A P I
private static func requestNewAuthToken ( for server : String ) -> Promise < String > {
print ( " [Loki] Requesting auth token for server: \( server ) . " )
2020-05-07 03:57:55 +02:00
let queryParameters = " pubKey= \( getUserHexEncodedPublicKey ( ) ) "
2020-04-21 02:36:23 +02:00
let url = URL ( string : " \( server ) /loki/v1/get_challenge? \( queryParameters ) " ) !
let request = TSRequest ( url : url )
2020-07-24 08:47:51 +02:00
let serverPublicKeyPromise = ( server = = FileServerAPI . server ) ? Promise { $0 . fulfill ( FileServerAPI . fileServerPublicKey ) }
: PublicChatAPI . getOpenGroupServerPublicKey ( for : server )
2020-07-23 04:03:39 +02:00
return serverPublicKeyPromise . then2 { serverPublicKey in
OnionRequestAPI . sendOnionRequest ( request , to : server , using : serverPublicKey )
} . map2 { rawResponse in
2020-04-21 02:36:23 +02:00
guard let json = rawResponse as ? JSON , let base64EncodedChallenge = json [ " cipherText64 " ] as ? String , let base64EncodedServerPublicKey = json [ " serverPubKey64 " ] as ? String ,
let challenge = Data ( base64Encoded : base64EncodedChallenge ) , var serverPublicKey = Data ( base64Encoded : base64EncodedServerPublicKey ) else {
2020-06-25 09:36:32 +02:00
throw DotNetAPIError . parsingFailed
2020-04-21 02:36:23 +02:00
}
// D i s c a r d t h e " 0 5 " p r e f i x i f n e e d e d
if serverPublicKey . count = = 33 {
let hexEncodedServerPublicKey = serverPublicKey . toHexString ( )
serverPublicKey = Data . data ( fromHex : hexEncodedServerPublicKey . substring ( from : 2 ) ) !
}
// T h e c h a l l e n g e i s p r e f i x e d b y t h e 1 6 b i t I V
guard let tokenAsData = try ? DiffieHellman . decrypt ( challenge , publicKey : serverPublicKey , privateKey : userKeyPair . privateKey ) ,
let token = String ( bytes : tokenAsData , encoding : . utf8 ) else {
2020-06-25 09:36:32 +02:00
throw DotNetAPIError . decryptionFailed
2020-04-21 02:36:23 +02:00
}
return token
}
}
private static func submitAuthToken ( _ token : String , for server : String ) -> Promise < String > {
print ( " [Loki] Submitting auth token for server: \( server ) . " )
let url = URL ( string : " \( server ) /loki/v1/submit_challenge " ) !
2020-05-07 03:57:55 +02:00
let parameters = [ " pubKey " : getUserHexEncodedPublicKey ( ) , " token " : token ]
2020-04-21 02:36:23 +02:00
let request = TSRequest ( url : url , method : " POST " , parameters : parameters )
2020-07-24 08:47:51 +02:00
let serverPublicKeyPromise = ( server = = FileServerAPI . server ) ? Promise { $0 . fulfill ( FileServerAPI . fileServerPublicKey ) }
: PublicChatAPI . getOpenGroupServerPublicKey ( for : server )
2020-07-23 04:03:39 +02:00
return serverPublicKeyPromise . then2 { serverPublicKey in
OnionRequestAPI . sendOnionRequest ( request , to : server , using : serverPublicKey )
} . map2 { _ in token }
2020-04-21 02:36:23 +02:00
}
// MARK: P u b l i c A P I
@objc ( uploadAttachment : withID : toServer : )
public static func objc_uploadAttachment ( _ attachment : TSAttachmentStream , with attachmentID : String , to server : String ) -> AnyPromise {
return AnyPromise . from ( uploadAttachment ( attachment , with : attachmentID , to : server ) )
}
2019-10-18 00:46:44 +02:00
public static func uploadAttachment ( _ attachment : TSAttachmentStream , with attachmentID : String , to server : String ) -> Promise < Void > {
2020-06-25 09:36:32 +02:00
let isEncryptionRequired = ( server = = FileServerAPI . server )
2019-10-18 00:46:44 +02:00
return Promise < Void > ( ) { seal in
2020-02-10 04:38:33 +01:00
func proceed ( with token : String ) {
2020-02-10 02:01:23 +01:00
// G e t t h e a t t a c h m e n t
2019-10-18 02:33:14 +02:00
let data : Data
2019-10-18 00:46:44 +02:00
guard let unencryptedAttachmentData = try ? attachment . readDataFromFile ( ) else {
2020-02-10 02:01:23 +01:00
print ( " [Loki] Couldn't read attachment from disk. " )
2020-06-25 09:36:32 +02:00
return seal . reject ( DotNetAPIError . generic )
2019-10-18 00:46:44 +02:00
}
2019-10-18 02:33:14 +02:00
// E n c r y p t t h e a t t a c h m e n t i f n e e d e d
if isEncryptionRequired {
var encryptionKey = NSData ( )
var digest = NSData ( )
guard let encryptedAttachmentData = Cryptography . encryptAttachmentData ( unencryptedAttachmentData , outKey : & encryptionKey , outDigest : & digest ) else {
print ( " [Loki] Couldn't encrypt attachment. " )
2020-06-25 09:36:32 +02:00
return seal . reject ( DotNetAPIError . encryptionFailed )
2019-10-18 02:33:14 +02:00
}
attachment . encryptionKey = encryptionKey as Data
attachment . digest = digest as Data
data = encryptedAttachmentData
} else {
data = unencryptedAttachmentData
2019-10-18 00:46:44 +02:00
}
2020-02-10 05:47:15 +01:00
// C h e c k t h e f i l e s i z e i f n e e d e d
2020-07-31 07:24:26 +02:00
print ( " [Loki] File size: \( data . count ) " )
2020-07-31 07:50:14 +02:00
if Double ( data . count ) > Double ( FileServerAPI . maxFileSize ) / FileServerAPI . fileSizeORMultiplier {
2020-06-25 09:36:32 +02:00
return seal . reject ( DotNetAPIError . maxFileSizeExceeded )
2020-02-10 05:47:15 +01:00
}
2019-10-18 00:46:44 +02:00
// C r e a t e t h e r e q u e s t
let url = " \( server ) /files "
let parameters : JSON = [ " type " : attachmentType , " Content-Type " : " application/binary " ]
var error : NSError ?
var request = AFHTTPRequestSerializer ( ) . multipartFormRequest ( withMethod : " POST " , urlString : url , parameters : parameters , constructingBodyWith : { formData in
2020-07-31 06:27:08 +02:00
let uuid = UUID ( ) . uuidString
print ( " [Loki] File UUID: \( uuid ) " )
formData . appendPart ( withFileData : data , name : " content " , fileName : uuid , mimeType : " application/binary " )
2019-10-18 00:46:44 +02:00
} , error : & error )
2020-02-10 04:38:33 +01:00
request . addValue ( " Bearer \( token ) " , forHTTPHeaderField : " Authorization " )
2019-10-18 00:46:44 +02:00
if let error = error {
print ( " [Loki] Couldn't upload attachment due to error: \( error ) . " )
2020-02-10 02:01:23 +01:00
return seal . reject ( error )
2019-10-18 00:46:44 +02:00
}
// S e n d t h e r e q u e s t
2020-07-23 04:03:39 +02:00
let serverPublicKeyPromise = ( server = = FileServerAPI . server ) ? Promise { $0 . fulfill ( FileServerAPI . fileServerPublicKey ) }
: PublicChatAPI . getOpenGroupServerPublicKey ( for : server )
attachment . isUploaded = false
attachment . save ( )
let _ = serverPublicKeyPromise . then2 { serverPublicKey in
OnionRequestAPI . sendOnionRequest ( request , to : server , using : serverPublicKey )
} . done2 { json in
2020-02-03 10:55:42 +01:00
// P a r s e t h e s e r v e r I D & d o w n l o a d U R L
2020-07-23 04:03:39 +02:00
guard let data = json [ " data " ] as ? JSON , let serverID = data [ " id " ] as ? UInt64 , let downloadURL = data [ " url " ] as ? String else {
print ( " [Loki] Couldn't parse attachment from: \( json ) . " )
2020-06-25 09:36:32 +02:00
return seal . reject ( DotNetAPIError . parsingFailed )
2020-02-03 10:55:42 +01:00
}
// U p d a t e t h e a t t a c h m e n t
attachment . serverId = serverID
attachment . isUploaded = true
attachment . downloadURL = downloadURL
attachment . save ( )
seal . fulfill ( ( ) )
2020-07-23 04:03:39 +02:00
} . catch2 { error in
seal . reject ( error )
2020-02-03 06:50:14 +01:00
}
2020-02-10 02:01:23 +01:00
}
2020-06-25 09:36:32 +02:00
if server = = FileServerAPI . server {
2020-06-11 08:33:11 +02:00
DispatchQueue . global ( qos : . userInitiated ) . async {
2020-02-19 03:55:58 +01:00
proceed ( with : " loki " ) // U p l o a d s t o t h e L o k i F i l e S e r v e r s h o u l d n ' t i n c l u d e a n y p e r s o n a l l y i d e n t i f i a b l e i n f o r m a t i o n s o u s e a d u m m y a u t h t o k e n
}
2020-02-10 02:01:23 +01:00
} else {
2020-06-12 02:08:07 +02:00
getAuthToken ( for : server ) . done ( on : DispatchQueue . global ( qos : . userInitiated ) ) { token in
2020-02-10 02:01:23 +01:00
proceed ( with : token )
2020-06-11 08:33:11 +02:00
} . catch2 { error in
2020-02-10 02:01:23 +01:00
print ( " [Loki] Couldn't upload attachment due to error: \( error ) . " )
seal . reject ( error )
}
2019-10-18 00:46:44 +02:00
}
}
}
2019-09-26 03:32:47 +02:00
}
2020-06-10 03:06:56 +02:00
// MARK: E r r o r H a n d l i n g
internal extension Promise {
internal func handlingInvalidAuthTokenIfNeeded ( for server : String ) -> Promise < T > {
2020-06-11 08:33:11 +02:00
return recover2 { error -> Promise < T > in
2020-07-23 04:03:39 +02:00
if case HTTP . Error . httpRequestFailed ( let statusCode , _ ) = error , statusCode = = 401 || statusCode = = 403 {
2020-07-20 03:02:58 +02:00
print ( " [Loki] Auth token for: \( server ) expired; dropping it. " )
2020-06-25 09:36:32 +02:00
DotNetAPI . clearAuthToken ( for : server )
2020-06-10 03:06:56 +02:00
}
throw error
}
}
}