// _____ _ // |_ _| |_ _ _ ___ ___ _ __ __ _ // | | | ' \| '_/ -_) -_) ' \/ _` |_ // |_| |_||_|_| \___\___|_|_|_\__,_(_) // // Threema iOS Client // Copyright (c) 2012-2020 Threema GmbH // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License, version 3, // as published by the Free Software Foundation. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . #include #import "ServerAPIConnector.h" #import "ServerAPIRequest.h" #import "NSString+Hex.h" #import "NaClCrypto.h" #import "Utils.h" #import "ThreemaError.h" #import "FeatureMask.h" #import "UserSettings.h" #import "MyIdentityStore.h" #import "LicenseStore.h" #import "AppGroup.h" #ifdef DEBUG static const DDLogLevel ddLogLevel = DDLogLevelVerbose; #else static const DDLogLevel ddLogLevel = DDLogLevelInfo; #endif @implementation ServerAPIConnector - (void)createIdentityWithStore:(MyIdentityStore*)identityStore onCompletion:(void(^)(MyIdentityStore *store))onCompletion onError:(void(^)(NSError *error))onError { static NSString *apiPath = @"identity/create"; NSMutableDictionary *request = [NSMutableDictionary dictionaryWithDictionary:@{ @"publicKey": [identityStore.publicKey base64EncodedStringWithOptions:0], @"version": [Utils getClientVersion], @"deviceId": [[[UIDevice currentDevice] identifierForVendor] UUIDString] }]; // add App Store receipt if available (but not for Work) NSURL *receiptUrl = [[NSBundle mainBundle] appStoreReceiptURL]; if (receiptUrl && [[NSFileManager defaultManager] fileExistsAtPath:receiptUrl.path] && ![LicenseStore requiresLicenseKey]) { NSData *receiptData = [NSData dataWithContentsOfURL:receiptUrl]; if (receiptData) { request[@"appStoreReceipt"] = [receiptData base64EncodedStringWithOptions:0]; } } // add Work license if available NSString *licenseUsername = [[LicenseStore sharedLicenseStore] licenseUsername]; NSString *licensePassword = [[LicenseStore sharedLicenseStore] licensePassword]; if (licenseUsername && licensePassword) { request[@"licenseUsername"] = licenseUsername; request[@"licensePassword"] = licensePassword; } [self sendSignedRequestPhase1:request toApiPath:apiPath onCompletion:^(NSDictionary *response) { NSData *nonce = [@"createIdentity response." dataUsingEncoding:NSASCIIStringEncoding]; [self sendSignedRequestPhase2:request toApiPath:apiPath phase1Response:response withNonce:nonce forStore:identityStore onCompletion:^(NSDictionary *response) { identityStore.identity = response[@"identity"]; identityStore.serverGroup = response[@"serverGroup"]; [identityStore storeInKeychain]; [FeatureMask updateFeatureMask]; onCompletion(identityStore); } onError:onError]; } onError:onError]; } - (void)fetchIdentityInfo:(NSString*)identity onCompletion:(void(^)(NSData *publicKey, NSNumber *state, NSNumber *type, NSNumber *featureMask))onCompletion onError:(void(^)(NSError *error))onError { NSString *apiPath = [NSString stringWithFormat:@"identity/%@", identity]; [ServerAPIRequest loadJSONFromAPIPath:apiPath withCachePolicy:NSURLRequestUseProtocolCachePolicy onCompletion:^(id jsonObject) { NSData *publicKey = [[NSData alloc] initWithBase64EncodedString:jsonObject[@"publicKey"] options:0]; NSNumber *state = [NSNumber numberWithInt:[jsonObject[@"state"] intValue]]; NSNumber *type = [NSNumber numberWithInt:[jsonObject[@"type"] intValue]]; NSNumber *featureMask = [NSNumber numberWithInt:[jsonObject[@"featureMask"] intValue]]; onCompletion(publicKey, state, type, featureMask); } onError:^(NSError *error) { onError(error); }]; } - (void)fetchBulkIdentityInfo:(NSArray*)identities onCompletion:(void(^)(NSArray *identities, NSArray *publicKeys, NSArray *featureMasks, NSArray *states, NSArray *types))onCompletion onError:(void(^)(NSError *error))onError { NSDictionary *req = [NSDictionary dictionaryWithObjectsAndKeys:identities, @"identities", nil]; [ServerAPIRequest postJSONToAPIPath:@"identity/fetch_bulk" data:req onCompletion:^(id jsonObject) { DDLogVerbose(@"Bulk fetch of ID status success: %@", jsonObject); NSMutableArray *responseIdentities = [[NSMutableArray alloc] init]; NSMutableArray *responsePublicKeys = [[NSMutableArray alloc] init]; NSMutableArray *responseFeatureMasks = [[NSMutableArray alloc] init]; NSMutableArray *responseStates = [[NSMutableArray alloc] init]; NSMutableArray *responseTypes = [[NSMutableArray alloc] init]; int index = 0; NSUInteger count = [jsonObject[@"identities"] count]; while(index < count) { id item = jsonObject[@"identities"][index]; [responseIdentities addObject:item[@"identity"]]; [responsePublicKeys addObject:[[NSData alloc] initWithBase64EncodedString:item[@"publicKey"] options:0]]; [responseFeatureMasks addObject:item[@"featureMask"]]; [responseStates addObject:item[@"state"]]; [responseTypes addObject:item[@"type"]]; index++; } onCompletion(responseIdentities, responsePublicKeys, responseFeatureMasks, responseStates, responseTypes); } onError:^(NSError *error) { DDLogError(@"Bulk fetch of ID status failed: %@", error); onError(error); }]; } - (void)fetchPrivateIdentityInfo:(MyIdentityStore*)identityStore onCompletion:(void(^)(NSString *serverGroup, NSString *email, NSString *mobileNo))onCompletion onError:(void(^)(NSError *error))onError { if (identityStore.identity == nil) { onError([ThreemaError threemaError:@"store has no valid identity"]); return; } NSDictionary *request = @{ @"identity": identityStore.identity }; [self sendSignedRequest:request toApiPath:@"identity/fetch_priv" forStore:identityStore onCompletion:^(id jsonObject) { onCompletion(jsonObject[@"serverGroup"], jsonObject[@"email"], jsonObject[@"mobileNo"]); } onError:onError]; } - (void)updateMyIdentityStore:(MyIdentityStore*)identityStore onCompletion:(void(^)(void))onCompletion onError:(void(^)(NSError* error))onError { [self fetchPrivateIdentityInfo:identityStore onCompletion:^(NSString *serverGroup, NSString *email, NSString *mobileNo) { DDLogVerbose(@"Got identity info: serverGroup %@, email %@, mobileNo %@", serverGroup, email, mobileNo); identityStore.serverGroup = serverGroup; identityStore.linkedEmail = email; if (identityStore.linkedEmail != nil) { identityStore.linkEmailPending = NO; } identityStore.linkedMobileNo = mobileNo; if (identityStore.linkedMobileNo != nil) { identityStore.linkMobileNoPending = NO; } onCompletion(); } onError:^(NSError *error) { onError(error); }]; } - (void)linkEmailWithStore:(MyIdentityStore*)identityStore email:(NSString*)email onCompletion:(void(^)(BOOL linked))onCompletion onError:(void(^)(NSError *error))onError { static NSString *apiPath = @"identity/link_email"; if (identityStore.identity == nil) { onError([ThreemaError threemaError:@"store has no valid identity"]); return; } if (email == nil) { onError([ThreemaError threemaError:@"link email with store: email missing"]); return; } NSDictionary *request = @{ @"identity": identityStore.identity, @"email": email, @"lang": [self preferredLanguage] }; [self sendSignedRequestPhase1:request toApiPath:apiPath onCompletion:^(NSDictionary *response) { if ([response[@"linked"] boolValue]) { /* Already linked - update address with user provided value, as it is possible that we currently only have ***@*** from the server after an ID restore */ identityStore.linkEmailPending = NO; identityStore.linkedEmail = email; onCompletion(YES); return; } [self sendSignedRequestPhase2:request toApiPath:apiPath phase1Response:response forStore:identityStore onCompletion:^(NSDictionary *response) { if (email.length == 0) { /* unlink */ identityStore.linkEmailPending = NO; identityStore.linkedEmail = nil; } else { identityStore.linkEmailPending = YES; identityStore.linkedEmail = email; } onCompletion(NO); } onError:onError]; } onError:onError]; } - (NSString *)preferredLanguage { NSString *lang = [[[NSBundle mainBundle] preferredLocalizations] objectAtIndex:0]; if (lang) { return lang; } else { return @"en_US"; } } - (void)checkLinkEmailStatus:(MyIdentityStore*)identityStore email:(NSString*)email onCompletion:(void(^)(BOOL linked))onCompletion onError:(void(^)(NSError *error))onError { if (identityStore.identity == nil) { onError([ThreemaError threemaError:@"store has no valid identity"]); return; } if (email == nil) { onError([ThreemaError threemaError:@"check link email status: email missing"]); return; } NSDictionary *request = @{ @"identity": identityStore.identity, @"email": email }; [self sendSignedRequestPhase1:request toApiPath:@"identity/link_email" onCompletion:^(NSDictionary *response) { BOOL linked = [response[@"linked"] boolValue]; if (linked) [MyIdentityStore sharedMyIdentityStore].linkEmailPending = NO; onCompletion(linked); } onError:onError]; } - (void)linkMobileNoWithStore:(MyIdentityStore*)identityStore mobileNo:(NSString*)mobileNo onCompletion:(void(^)(BOOL linked))onCompletion onError:(void(^)(NSError *error))onError { if (identityStore.identity == nil) { onError([ThreemaError threemaError:@"store has no valid identity"]); return; } if (mobileNo == nil) { onError([ThreemaError threemaError:@"link mobile: mobileNo missing"]); return; } static NSString *apiPath = @"identity/link_mobileno"; NSString *urlScheme = @"threema"; if ([LicenseStore requiresLicenseKey]) { urlScheme = @"threemawork"; } NSDictionary *request = @{ @"identity": identityStore.identity, @"mobileNo": mobileNo, @"lang": [self preferredLanguage], @"urlScheme": urlScheme }; [self sendSignedRequestPhase1:request toApiPath:apiPath onCompletion:^(NSDictionary *response) { if ([response[@"linked"] boolValue]) { /* already linked */ identityStore.linkMobileNoPending = NO; identityStore.linkedMobileNo = mobileNo; identityStore.linkMobileNoVerificationId = nil; onCompletion(YES); return; } [self sendSignedRequestPhase2:request toApiPath:apiPath phase1Response:response forStore:identityStore onCompletion:^(NSDictionary *response) { if (mobileNo.length == 0) { /* unlink */ identityStore.linkMobileNoPending = NO; identityStore.linkedMobileNo = nil; identityStore.linkMobileNoVerificationId = nil; } else { identityStore.linkMobileNoPending = YES; identityStore.linkedMobileNo = mobileNo; identityStore.linkMobileNoVerificationId = response[@"verificationId"]; identityStore.linkMobileNoStartDate = [NSDate date]; } onCompletion(NO); } onError:onError]; } onError:onError]; } - (void)linkMobileNoWithStore:(MyIdentityStore*)identityStore code:(NSString*)code onCompletion:(void(^)(BOOL linked))onCompletion onError:(void(^)(NSError *error))onError { if (!identityStore.linkMobileNoPending || identityStore.linkMobileNoVerificationId == nil) { DDLogWarn(@"No mobileNo verification pending"); onError([ThreemaError threemaError:@"No verification pending"]); return; } NSDictionary *req = @{ @"verificationId": identityStore.linkMobileNoVerificationId, @"code": code }; [ServerAPIRequest postJSONToAPIPath:@"identity/link_mobileno" data:req onCompletion:^(id jsonObject) { DDLogVerbose(@"link mobileNo phase 3 request success: %@", jsonObject); identityStore.linkMobileNoPending = NO; identityStore.linkMobileNoVerificationId = nil; [[NSNotificationCenter defaultCenter] postNotificationName:@"ThreemaIdentityLinkedWithMobileNo" object:nil]; onCompletion(YES); } onError:^(NSError *error) { DDLogError(@"link mobileNo phase 3 request failed: %@", error); onError(error); }]; } - (void)linkMobileNoRequestCallWithStore:(MyIdentityStore*)identityStore onCompletion:(void(^)(void))onCompletion onError:(void(^)(NSError *error))onError { if (!identityStore.linkMobileNoPending || identityStore.linkMobileNoVerificationId == nil) { DDLogWarn(@"No mobileNo verification pending"); onError([ThreemaError threemaError:@"No verification pending"]); return; } NSDictionary *req = @{ @"verificationId": identityStore.linkMobileNoVerificationId }; [ServerAPIRequest postJSONToAPIPath:@"identity/link_mobileno_call" data:req onCompletion:^(id jsonObject) { DDLogVerbose(@"link mobileNo call request success: %@", jsonObject); onCompletion(); } onError:^(NSError *error) { DDLogError(@"link mobileNo call request failed: %@", error); onError(error); }]; } - (void)obtainMatchTokenForIdentity:(MyIdentityStore*)identityStore forceRefresh:(BOOL)forceRefresh onCompletion:(void(^)(NSString *matchToken))onCompletion onError:(void(^)(NSError *error))onError { if (identityStore.identity == nil) { onCompletion(nil); return; } // Cached match token? NSUserDefaults *defaults = [AppGroup userDefaults]; NSString *matchToken = [defaults objectForKey:@"MatchToken"]; if (!forceRefresh && matchToken) { onCompletion(matchToken); return; } NSDictionary *request = @{ @"identity": identityStore.identity }; [self sendSignedRequest:request toApiPath:@"identity/match_token" forStore:identityStore onCompletion:^(id jsonObject) { if (jsonObject[@"matchToken"]) { [defaults setObject:jsonObject[@"matchToken"] forKey:@"MatchToken"]; [defaults synchronize]; onCompletion(jsonObject[@"matchToken"]); } else { onError([ThreemaError threemaError:jsonObject[@"error"]]); } } onError:onError]; } - (void)matchIdentitiesWithEmailHashes:(NSArray*)emailHashes mobileNoHashes:(NSArray*)mobileNoHashes includeInactive:(BOOL)includeInactive onCompletion:(void(^)(NSArray *identities, int checkInterval))onCompletion onError:(void(^)(NSError *error))onError { [self obtainMatchTokenForIdentity:[MyIdentityStore sharedMyIdentityStore] forceRefresh:NO onCompletion:^(NSString *matchToken) { DDLogInfo(@"Match token: %@", matchToken); [self matchIdentitiesWithEmailHashes:emailHashes mobileNoHashes:mobileNoHashes includeInactive:includeInactive matchToken:matchToken onCompletion:onCompletion onError:^(NSError *error) { // Match token may be invalid/expired, refresh and try again [self obtainMatchTokenForIdentity:[MyIdentityStore sharedMyIdentityStore] forceRefresh:YES onCompletion:^(NSString *matchToken) { [self matchIdentitiesWithEmailHashes:emailHashes mobileNoHashes:mobileNoHashes includeInactive:includeInactive matchToken:matchToken onCompletion:onCompletion onError:onError]; } onError:onError]; }]; } onError:^(NSError *error) { DDLogError(@"Cannot obtain match token: %@", error); onError(error); }]; } - (void)matchIdentitiesWithEmailHashes:(NSArray*)emailHashes mobileNoHashes:(NSArray*)mobileNoHashes includeInactive:(BOOL)includeInactive matchToken:(NSString*)matchToken onCompletion:(void(^)(NSArray *identities, int checkInterval))onCompletion onError:(void(^)(NSError *error))onError { NSMutableDictionary *req = [NSMutableDictionary dictionary]; req[@"emailHashes"] = emailHashes; req[@"mobileNoHashes"] = mobileNoHashes; if (matchToken != nil) { req[@"matchToken"] = matchToken; } if (includeInactive) { req[@"includeInactive"] = @YES; } [ServerAPIRequest postJSONToAPIPath:@"identity/match" data:req onCompletion:^(id jsonObject) { DDLogVerbose(@"Match identities request success: %@", jsonObject); onCompletion([jsonObject objectForKey:@"identities"], ((NSNumber*)jsonObject[@"checkInterval"]).intValue); } onError:^(NSError *error) { DDLogError(@"Match identities request failed: %@", error); onError(error); }]; } - (void)setFeatureMask:(NSNumber *)featureMask forStore:(MyIdentityStore*)identityStore onCompletion:(void(^)(void))onCompletion onError:(void(^)(NSError *error))onError { if (identityStore.identity == nil) { onError([ThreemaError threemaError:@"store has no valid identity"]); return; } NSString *apiPath = @"identity/set_featuremask"; NSDictionary *req = @{ @"identity": identityStore.identity, @"featureMask": featureMask }; [self sendSignedRequest:req toApiPath:apiPath forStore:identityStore onCompletion:^(NSDictionary *response) { if ([response[@"success"] boolValue]) onCompletion(); else onError([ThreemaError threemaError:response[@"error"]]); } onError:onError]; } - (void)getFeatureMasksForIdentities:(NSArray*)identities onCompletion:(void(^)(NSArray* featureMasks))onCompletion onError:(void(^)(NSError *error))onError { NSDictionary *req = [NSDictionary dictionaryWithObjectsAndKeys:identities, @"identities", nil]; [ServerAPIRequest postJSONToAPIPath:@"identity/check_featuremask" data:req onCompletion:^(id jsonObject) { DDLogVerbose(@"Check featureMask success: %@", jsonObject); NSArray *featureMaskStrings = jsonObject[@"featureMasks"]; NSMutableArray *featureMaskNumbers = [NSMutableArray arrayWithCapacity: [featureMaskStrings count]]; for (NSString *maskString in featureMaskStrings) { if ([maskString isEqual: [NSNull null]]) { NSNumber *number = [NSNumber numberWithInt: 0]; [featureMaskNumbers addObject: number]; } else { NSNumber *number = [NSNumber numberWithInt: maskString.intValue]; [featureMaskNumbers addObject: number]; } } onCompletion(featureMaskNumbers); } onError:^(NSError *error) { DDLogError(@"Check featureMask failed: %@", error); onError(error); }]; } - (void)checkRevocationPasswordForStore:(MyIdentityStore*)identityStore onCompletion:(void(^)(BOOL revocationPasswordSet, NSDate *lastChanged))onCompletion onError:(void(^)(NSError *error))onError { if (identityStore.identity == nil) { onError([ThreemaError threemaError:@"store has no valid identity"]); return; } NSDictionary *request = @{ @"identity": identityStore.identity }; [self sendSignedRequest:request toApiPath:@"identity/check_revocation_key" forStore:identityStore onCompletion:^(NSDictionary *response) { BOOL revocationPasswordSet = [response[@"revocationKeySet"] boolValue]; NSDate *lastChanged = nil; if (revocationPasswordSet) lastChanged = [Utils parseISO8601DateString:response[@"lastChanged"]]; onCompletion(revocationPasswordSet, lastChanged); } onError:onError]; } - (void)setRevocationPassword:(NSString*)revocationPassword forStore:(MyIdentityStore*)identityStore onCompletion:(void(^)(void))onCompletion onError:(void(^)(NSError *error))onError { if (identityStore.identity == nil) { onError([ThreemaError threemaError:@"store has no valid identity"]); return; } /* key = first 32 bits of SHA-256 hash of password string */ NSData *revocationPasswordData = [revocationPassword dataUsingEncoding:NSUTF8StringEncoding]; unsigned char digest[CC_SHA256_DIGEST_LENGTH]; CC_SHA256(revocationPasswordData.bytes, (CC_LONG)revocationPasswordData.length, digest); NSData *revocationKey = [NSData dataWithBytes:digest length:4]; NSDictionary *request = @{ @"identity": identityStore.identity, @"revocationKey": [revocationKey base64EncodedStringWithOptions:0] }; [self sendSignedRequest:request toApiPath:@"identity/set_revocation_key" forStore:identityStore onCompletion:^(id jsonObject) { if ([jsonObject[@"success"] boolValue]) onCompletion(); else onError([ThreemaError threemaError:jsonObject[@"error"]]); } onError:onError]; } - (void)checkStatusOfIdentities:(NSArray*)identities onCompletion:(void(^)(NSArray* states, NSArray* types, NSArray* featureMasks, int checkInterval))onCompletion onError:(void(^)(NSError *error))onError { NSDictionary *req = [NSDictionary dictionaryWithObjectsAndKeys:identities, @"identities", nil]; [ServerAPIRequest postJSONToAPIPath:@"identity/check" data:req onCompletion:^(id jsonObject) { DDLogVerbose(@"Check ID status success: %@", jsonObject); onCompletion(jsonObject[@"states"], jsonObject[@"types"], jsonObject[@"featureMasks"], [jsonObject[@"checkInterval"] intValue]); } onError:^(NSError *error) { DDLogError(@"Check ID status failed: %@", error); onError(error); }]; } - (void)revokeIdForStore:(MyIdentityStore*)identityStore onCompletion:(void(^)(void))onCompletion onError:(void(^)(NSError *error))onError { if (identityStore.identity == nil) { onError([ThreemaError threemaError:@"store has no valid identity"]); return; } NSDictionary *request = @{ @"identity": identityStore.identity, @"lang": [self preferredLanguage] }; [self sendSignedRequest:request toApiPath:@"identity/revoke" forStore:identityStore onCompletion:^(id jsonObject) { if ([jsonObject[@"success"] boolValue]) onCompletion(); else onError([ThreemaError threemaError:jsonObject[@"error"]]); } onError:onError]; } - (void)validateLicenseUsername:(NSString*)licenseUsername password:(NSString*)licensePassword appId:(NSString*)appId version:(NSString*)version deviceId:(NSString*)deviceId onCompletion:(void(^)(BOOL success, NSDictionary *info))onCompletion onError:(void(^)(NSError *error))onError { static NSString *apiPath = @"check_license"; NSDictionary *request = @{ @"licenseUsername": licenseUsername, @"licensePassword": licensePassword, @"appId": appId, @"version": version, @"deviceId": deviceId }; [self sendSignedRequestPhase1:request toApiPath:apiPath onCompletion:^(NSDictionary *response) { BOOL success = [response[@"success"] boolValue]; onCompletion(success, response); } onError:onError]; } - (void)updateWorkInfoForStore:(MyIdentityStore*)identityStore licenseUsername:(NSString*)licenseUsername password:(NSString*)licensePassword onCompletion:(void(^)(void))onCompletion onError:(void(^)(NSError *error))onError { if (identityStore.identity == nil) { onError([ThreemaError threemaError:@"store has no valid identity"]); return; } NSMutableDictionary *request = [@{ @"identity": identityStore.identity, @"licenseUsername": licenseUsername, @"licensePassword": licensePassword, @"publicNickname": (identityStore.pushFromName != nil ? identityStore.pushFromName : identityStore.identity), @"version": [Utils getClientVersion] } mutableCopy]; if (identityStore.firstName != nil) request[@"firstName"] = identityStore.firstName; if (identityStore.lastName != nil) request[@"lastName"] = identityStore.lastName; if (identityStore.csi != nil) request[@"csi"] = identityStore.csi; if (identityStore.category != nil) request[@"category"] = identityStore.category; if ([request isEqualToDictionary:identityStore.lastWorkUpdateRequest] && ![identityStore sendUpdateWorkInfoStatus]) { // request hasn't changed since last update and it's the same date onCompletion(); return; } identityStore.lastWorkUpdateRequest = request; [self sendSignedRequest:request toApiPath:@"identity/update_work_info" forStore:identityStore onCompletion:^(id jsonObject) { if ([jsonObject[@"success"] boolValue]) { [identityStore setLastWorkUpdateDate:[NSDate date]]; onCompletion(); } else { onError([ThreemaError threemaError:jsonObject[@"error"]]); } } onError:onError]; } - (void)searchInDirectory:(NSString *)searchString categories:(NSArray *)categories page:(int)page forLicenseStore:(LicenseStore *)licenseStore forMyIdentityStore:(MyIdentityStore*)identityStore onCompletion:(void(^)(NSArray *contacts, NSDictionary *paging))onCompletion onError:(void(^)(NSError *error))onError { NSString *sortOrder = [[UserSettings sharedUserSettings] sortOrderFirstName] ? @"firstName" : @"lastName"; NSMutableDictionary *req = [[NSMutableDictionary alloc] initWithDictionary:@{@"username": licenseStore.licenseUsername, @"password": licenseStore.licensePassword, @"identity": identityStore.identity, @"query": searchString, @"sort": @{@"by": sortOrder, @"asc": @true}, @"page": [NSNumber numberWithInt:page] }]; if (categories.count > 0) { [req setValue:categories forKey:@"categories"]; } [ServerAPIRequest postJSONToWorkAPIPath:@"directory" data:req onCompletion:^(id jsonObject) { DDLogVerbose(@"Work directory search success: %@", jsonObject); NSArray *contactsArray = jsonObject[@"contacts"]; NSDictionary *paging = jsonObject[@"paging"]; onCompletion(contactsArray, paging); } onError:^(NSError *error) { DDLogError(@"Check featureMask failed: %@", error); onError(error); }]; } - (void)obtainTurnServersWithStore:(MyIdentityStore*)identityStore onCompletion:(void(^)(NSDictionary *response))onCompletion onError:(void(^)(NSError *error))onError { if (identityStore.identity == nil) { onError([ThreemaError threemaError:@"No identity"]); return; } NSDictionary *request = @{ @"identity": identityStore.identity, @"type": @"voip" }; [self sendSignedRequest:request toApiPath:@"identity/turn_cred" forStore:identityStore onCompletion:^(id jsonObject) { if (jsonObject[@"turnUrls"]) { onCompletion(jsonObject); } else { onError([ThreemaError threemaError:jsonObject[@"error"]]); } } onError:onError]; } - (void)sendSignedRequest:(NSDictionary*)request toApiPath:(NSString*)apiPath forStore:(MyIdentityStore*)identityStore onCompletion:(void(^)(NSDictionary *response))onCompletion onError:(void(^)(NSError *error))onError { [self sendSignedRequestPhase1:request toApiPath:apiPath onCompletion:^(NSDictionary *response) { [self sendSignedRequestPhase2:request toApiPath:apiPath phase1Response:response forStore:identityStore onCompletion:onCompletion onError:onError]; } onError:onError]; } - (void)sendSignedRequestPhase1:(NSDictionary*)request toApiPath:(NSString*)apiPath onCompletion:(void(^)(NSDictionary* response))onCompletion onError:(void(^)(NSError *error))onError { [ServerAPIRequest postJSONToAPIPath:apiPath data:request onCompletion:^(id jsonObject) { DDLogVerbose(@"Send API request %@ phase 1 success: %@", apiPath, jsonObject); onCompletion((NSDictionary*)jsonObject); } onError:^(NSError *error) { DDLogVerbose(@"Send API request %@ phase 1 failed: %@", apiPath, error); onError(error); }]; } - (void)sendSignedRequestPhase2:(NSDictionary*)request toApiPath:(NSString*)apiPath phase1Response:(id)phase1Response forStore:(MyIdentityStore*)identityStore onCompletion:(void(^)(NSDictionary *response))onCompletion onError:(void(^)(NSError *error))onError { NSData *nonce = [[NaClCrypto sharedCrypto] randomBytes:kNaClCryptoNonceSize]; [self sendSignedRequestPhase2:request toApiPath:apiPath phase1Response:phase1Response withNonce:nonce forStore:identityStore onCompletion:onCompletion onError:onError]; } - (void)sendSignedRequestPhase2:(NSDictionary*)request toApiPath:(NSString*)apiPath phase1Response:(id)phase1Response withNonce:(NSData*)nonce forStore:(MyIdentityStore*)identityStore onCompletion:(void(^)(NSDictionary *response))onCompletion onError:(void(^)(NSError *error))onError { NSDictionary *resp1 = (NSDictionary*)phase1Response; NSString *tokenStr = resp1[@"token"]; NSData *token = [[NSData alloc] initWithBase64EncodedString:tokenStr options:0]; NSData *tokenRespKeyPub = [[NSData alloc] initWithBase64EncodedString:resp1[@"tokenRespKeyPub"] options:0]; /* sign token with our secret key */ NSData *response = [identityStore encryptData:token withNonce:nonce publicKey:tokenRespKeyPub]; if (response == nil) { NSError *error = [ThreemaError threemaError:@"could not encrypt response"]; DDLogVerbose(@"Send API request %@ phase 2 failed: %@", apiPath, error); onError(error); return; } NSMutableDictionary *signedRequest = [NSMutableDictionary dictionaryWithDictionary:request]; signedRequest[@"token"] = tokenStr; signedRequest[@"response"] = [response base64EncodedStringWithOptions:0]; signedRequest[@"nonce"] = [nonce base64EncodedStringWithOptions:0]; [ServerAPIRequest postJSONToAPIPath:apiPath data:signedRequest onCompletion:^(id jsonObject) { DDLogVerbose(@"Send API request %@ phase 2 success: %@", apiPath, jsonObject); onCompletion((NSDictionary*)jsonObject); } onError:^(NSError *error) { DDLogVerbose(@"Send API request %@ phase 2 failed: %@", apiPath, error); onError(error); }]; } @end