Quellcodebibliothek Statistik Leitseite products/Sources/formale Sprachen/C/Threema/ThreemaFramework/Business/   (Social Medium ©)  Datei vom 26.9.2026 mit Größe 39 kB image not shown  

Quelle  ServerAPIConnector.m  Sprache: unbekannt

 
Spracherkennung für: .m vermutete Sprache: Masm {Masm[50] ABAP[48] JAVA[44]} [Methode: maximale Elemente, drei Dimensionen]

#include <CommonCrypto/CommonDigest.h>

#import "ThreemaFramework/ThreemaFramework-Swift.h"
#import "ServerAPIConnector.h"
#import "ServerAPIRequest.h"
#import "NSString+Hex.h"
#import "NaClCrypto.h"
#import "ThreemaUtilityObjC.h"
#import "ThreemaError.h"
#import "UserSettings.h"
#import "MyIdentityStore.h"
#import "LicenseStore.h"
#import "AppGroup.h"
#import "ProtocolDefines.h"

@import FileUtility;

#ifdef DEBUG
  static const DDLogLevel ddLogLevel = DDLogLevelVerbose;
#else
  static const DDLogLevel ddLogLevel = DDLogLevelInfo;
#endif

/// Redacts personal info for logging, same format as `-[MDMSetup checkIsPersonalInfoAndLog:value:]`
static NSString *redactedPersonalInfo(NSString *value) {
    if (value == nil) {
        return @"(null)";
    }
    else if ([value length] == 0) {
        return @"[empty]";
    }
    else if ([value length] <= 2) {
        return [NSString stringWithFormat:@"%@***", [value substringToIndex:value.length - 1]];
    }
    else {
        return [NSString stringWithFormat:@"%@***", [value substringToIndex:2]];
    }
}

@implementation ServerAPIConnector

- (void)createIdentityWithStore:(id<MyIdentityStoreProtocol>)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": ThreemaUtility.clientVersion,
                              @"deviceId": [[[UIDevice currentDevice] identifierForVendor] UUIDString]
                              }];
    
    // Add App Store receipt if available (but not for Work)
    NSURL *receiptUrl = [[NSBundle mainBundle] appStoreReceiptURL];
    if (receiptUrl && [[FileUtility new] fileExistsAt:receiptUrl] && !TargetManagerObjC.isBusinessApp) {
        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) {
        [self sendSignedRequestPhase2:request toApiPath:apiPath phase1Response:response forStore:identityStore onCompletion:^(NSDictionary *response) {
            identityStore.identity = response[@"identity"];
            identityStore.serverGroup = response[@"serverGroup"];
            
            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:NSURLRequestReloadIgnoringLocalCacheData 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 = @0;
        if (![jsonObject[@"featureMask"] isEqual:[NSNull null]]) {
            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]];
            if ([item[@"featureMask"] isEqual: [NSNull null]]) {
                [responseFeatureMasks addObject:@0];
            } else {
                [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:(id<MyIdentityStoreProtocol>)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,
        @"appVariant": TargetManagerObjC.isBusinessApp ? @"work" : @"consumer"
    };

    [self sendSignedRequest:request toApiPath:@"identity/fetch_priv" forStore:identityStore onCompletion:^(id jsonObject) {
        onCompletion(jsonObject[@"serverGroup"], jsonObject[@"email"], jsonObject[@"mobileNo"]);
    } onError:onError];
}

- (void)fetchWorkIdentitiesInfo:(NSArray *)identities onCompletion:(void(^)(NSArray *foundIdentities))onCompletion onError:(void(^)(NSError *error))onError {
    if (identities == nil) {
        onError([ThreemaError threemaError:@"Identity is nil"]);
        return;
    }

    NSDictionary *request = @{
        @"username": [[LicenseStore sharedLicenseStore] licenseUsername],
        @"password": [[LicenseStore sharedLicenseStore] licensePassword],
        @"contacts": identities
    };
    [ServerAPIRequest postJSONToWorkAPIPath:@"identities" data:request onCompletion:^(id jsonObject) {
        onCompletion(jsonObject[@"contacts"]);
    } onError:^(NSError *error) {
        onError(error);
    }];
}

- (void)updateMyIdentityStore:(id<MyIdentityStoreProtocol>)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;
        
        if (email != nil) {
            identityStore.linkedEmail = email;
            identityStore.linkEmailPending = NO;
        }
        
        if (mobileNo != nil) {
            identityStore.linkedMobileNo = mobileNo;
            identityStore.linkMobileNoPending = NO;
        }
        
        onCompletion();
    } onError:^(NSError *error) {
        onError(error);
    }];
}

- (void)linkEmailWithStore:(id<MyIdentityStoreProtocol>)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 */
            ProfileStore *profileStore = [ProfileStore new];
            [profileStore syncAndSaveWithEmail:email completionHandler:^(NSError * _Nullable error) {
                if (error == nil) {
                    identityStore.linkEmailPending = NO;
                    onCompletion(YES);
                }
                else {
                    onError(error);
                }
            }];
            return;
        }
        
        [self sendSignedRequestPhase2:request toApiPath:apiPath phase1Response:response forStore:identityStore onCompletion:^(NSDictionary *response) {
            ProfileStore *profileStore = [ProfileStore new];
            [profileStore syncAndSaveWithEmail:email.length != 0 ? email : nil completionHandler:^(NSError * _Nullable error) {
                if (error == nil) {
                    if (email.length == 0) {
                        /* unlink */
                        identityStore.linkEmailPending = NO;
                    } else {
                        identityStore.linkEmailPending = YES;
                    }
                    onCompletion(NO);
                }
                else {
                    onError(error);
                }
            }];
        } onError:onError];
    } onError:onError];
}

- (NSString *)preferredLanguage {
    NSString *lang = [[[NSBundle mainBundle] preferredLocalizations] objectAtIndex:0];
    if (lang) {
        return lang;
    } else {
        return @"en_US";
    }
}

- (void)checkLinkEmailStatus:(id<MyIdentityStoreProtocol>)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:(id<MyIdentityStoreProtocol>)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 (TargetManagerObjC.isBusinessApp) {
        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 */
            ProfileStore *profileStore = [ProfileStore new];
            [profileStore syncAndSaveWithMobileNo:mobileNo completionHandler:^(NSError * _Nullable error) {
                if (error == nil) {
                    identityStore.linkMobileNoPending = NO;
                    identityStore.linkMobileNoVerificationId = nil;
                    onCompletion(YES);
                }
                else {
                    onError(error);
                }
            }];
            return;
        }
        
        [self sendSignedRequestPhase2:request toApiPath:apiPath phase1Response:response forStore:identityStore onCompletion:^(NSDictionary *response) {
            ProfileStore *profileStore = [ProfileStore new];
            [profileStore syncAndSaveWithMobileNo:mobileNo.length != 0 ? mobileNo : nil completionHandler:^(NSError * _Nullable error) {
                if (error == nil) {
                    if (mobileNo.length == 0) {
                        /* unlink */
                        identityStore.linkMobileNoPending = NO;
                        identityStore.linkMobileNoVerificationId = nil;
                    } else {
                        identityStore.linkMobileNoPending = YES;
                        identityStore.linkMobileNoVerificationId = response[@"verificationId"];
                        identityStore.linkMobileNoStartDate = [NSDate date];
                    }
                    onCompletion(NO);
                }
                else {
                    onError(error);
                }
            }];
        } onError:onError];
    } onError:onError];
}

- (void)linkMobileNoWithStore:(id<MyIdentityStoreProtocol>)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:(id<MyIdentityStoreProtocol>)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:(id<MyIdentityStoreProtocol>)identityStore forceRefresh:(BOOL)forceRefresh onCompletion:(void(^)(NSString *matchToken))onCompletion onError:(void(^)(NSError *error))onError {

    if (!identityStore.isValidIdentity || !AppSetup.isIdentityProvisioned) {
        DDLogNotice(@"Skip obtaining token, because my identity is invalid or not provisioned.");
        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:(id<MyIdentityStoreProtocol>)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:(id<MyIdentityStoreProtocol>)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 = [ThreemaUtilityObjC parseISO8601DateString:response[@"lastChanged"]];
        onCompletion(revocationPasswordSet, lastChanged);
    } onError:onError];
}

- (void)setRevocationPassword:(NSString*)revocationPassword forStore:(id<MyIdentityStoreProtocol>)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)revokeID:(id<MyIdentityStoreProtocol>)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)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:(id<MyIdentityStoreProtocol>)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:(id<MyIdentityStoreProtocol>)identityStore licenseUsername:(NSString*)licenseUsername password:(NSString*)licensePassword force:(BOOL)force onCompletion:(void(^)(BOOL sent))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.length > 0 ? identityStore.pushFromName : identityStore.identity),
        @"version": ThreemaUtility.clientVersionWithMDM
    } 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;
    
    // For backwards compatibility we have to send this values always
    // Otherwise it will not remove the values if there are empty or nil
    if (identityStore.jobTitle != nil) {
        request[@"jobTitle"] = identityStore.jobTitle;
    } else {
        request[@"jobTitle"] = @"";
    }
    
    if (identityStore.department != nil) {
        request[@"department"] = identityStore.department;
    } else {
        request[@"department"] = @"";
    }
    
    NSData *requestData = [NSJSONSerialization dataWithJSONObject:request options:NSJSONWritingPrettyPrinted error:nil];
    unsigned char digest[CC_SHA256_DIGEST_LENGTH];
    CC_SHA256(requestData.bytes, (CC_LONG)requestData.length, digest);
    NSData *requestHash = [NSData dataWithBytes:digest length:sizeof(digest)];

    if ((force == NO && [requestHash isEqualToData:identityStore.lastWorkUpdateRequestHash] && ![identityStore sendUpdateWorkInfoStatus])
        || [AppGroup getCurrentType] != AppGroupTypeApp) {
        // request hasn't changed since last update and it's the same date
        // or it's a extension
        onCompletion(false);
        return;
    }
    
    DDLogWarn(@"Send update work info with\nfirstName: %@\nlastName: %@\ncsi: %@\njobTitle: %@\ndepartment: %@\ncategory: %@", redactedPersonalInfo(identityStore.firstName), redactedPersonalInfo(identityStore.lastName), redactedPersonalInfo(identityStore.csi), redactedPersonalInfo(identityStore.jobTitle), redactedPersonalInfo(identityStore.department), redactedPersonalInfo(identityStore.category));
    
    [self sendSignedRequest:request toApiPath:@"identity/update_work_info" forStore:identityStore onCompletion:^(id jsonObject) {
        if ([jsonObject[@"success"] boolValue]) {
            identityStore.lastWorkUpdateRequestHash = requestHash;
            [identityStore setLastWorkUpdateDate:[NSDate date]];
            onCompletion(true);
        } else {
            onError([ThreemaError threemaError:jsonObject[@"error"]]);
        }
    } onError:onError];
}

- (void)searchInDirectory:(NSString *)searchString categories:(NSArray *)categories page:(int)page forLicenseStore:(id<LicenseStoreProtocol> )licenseStore forMyIdentityStore:(id<MyIdentityStoreProtocol>)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:(id<MyIdentityStoreProtocol>)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)obtainSFUCredentials:(id<MyIdentityStoreProtocol>)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,
    };

    [self sendSignedRequest:request toApiPath:@"identity/sfu_cred" forStore:identityStore onCompletion:^(id jsonObject) {
        if (jsonObject[@"sfuToken"]) {
            onCompletion(jsonObject);
        } else {
            onError([ThreemaError threemaError:jsonObject[@"error"]]);
        }
    } onError:onError];
}

- (void)obtainAuthTokenOnCompletion:(void(^)(NSString *authToken))onCompletion onError:(void(^)(NSError *error))onError {
    [ServerAPIRequest loadJSONFromAPIPath:@"auth_token" withCachePolicy:NSURLRequestReloadIgnoringCacheData onCompletion:^(id jsonObject) {
        if (jsonObject[@"authToken"]) {
            onCompletion(jsonObject[@"authToken"]);
        } else {
            onError([ThreemaError threemaError:jsonObject[@"error"]]);
        }
    } onError:onError];
}

- (void)reportSpamForIdentity:(nonnull NSString *)peerIdentity andNickname:(nullable NSString *)peerNickname withStore:(nonnull id<MyIdentityStoreProtocol>)identityStore onCompletion:(void(^_Nonnull)(void))onCompletion onError:(void(^_Nonnull)( NSError * _Nullable error))onError {
     if (identityStore.identity == nil) {
         onError([ThreemaError threemaError:@"No identity"]);
         return;
     }
     
     NSMutableDictionary *request = [[NSMutableDictionary alloc] initWithDictionary: @{
        @"reporter_identity": identityStore.identity,
        @"peer_identity": peerIdentity,
     }];
     
     // Send an explicit null (not an empty string and not an omitted key) when no nickname was set
     if (peerNickname != nil && peerNickname.length > 0) {
         [request setObject:peerNickname forKey:@"peer_nickname"];
     } else {
         [request setObject:[NSNull null] forKey:@"peer_nickname"];
     }

     [self sendSignedRequest:request toApiPath:@"identity/report_identity" forStore:identityStore onCompletion:^(NSDictionary *jsonObject) {
         if ([self isReportSuccessResponse:jsonObject]) {
             onCompletion();
         }
         else {
             onError([self reportErrorFromResponse:jsonObject]);
         }
     } onError:onError];
}

- (void)reportSpamForGroupWithGroupID:(uint64_t)groupID adminIdentity:(nonnull NSString *)groupAdminIdentity adminNickname:(nullable NSString *)groupAdminNickname memberCount:(NSInteger)memberCount withStore:(nonnull id<MyIdentityStoreProtocol>)identityStore onCompletion:(void(^_Nonnull)(void))onCompletion onError:(void(^_Nonnull)( NSError * _Nullable error))onError {
     if (identityStore.identity == nil) {
         onError([ThreemaError threemaError:@"No identity"]);
         return;
     }

     NSMutableDictionary *request = [[NSMutableDictionary alloc] initWithDictionary: @{
        @"reporter_identity": identityStore.identity,
        @"group_creator_identity": groupAdminIdentity,
        @"group_name": [NSNull null],
        @"group_id": [NSString stringWithFormat:@"%llu", groupID],
        @"group_member_count": @(memberCount),
     }];

     // Send an explicit null (not an empty string and not an omitted key) when no nickname was set
     if (groupAdminNickname != nil && groupAdminNickname.length > 0) {
         [request setObject:groupAdminNickname forKey:@"group_creator_nickname"];
     } else {
         [request setObject:[NSNull null] forKey:@"group_creator_nickname"];
     }

     [self sendSignedRequest:request toApiPath:@"identity/report_group" forStore:identityStore onCompletion:^(NSDictionary *jsonObject) {
         if ([self isReportSuccessResponse:jsonObject]) {
             onCompletion();
         }
         else {
             onError([self reportErrorFromResponse:jsonObject]);
         }
     } onError:onError];
}

#pragma mark - Private helpers

- (BOOL)isReportSuccessResponse:(NSDictionary *)jsonObject {
    id status = jsonObject[@"status"];
    return [status isKindOfClass:[NSString class]] && [status isEqualToString:@"success"];
}

- (NSError *)reportErrorFromResponse:(NSDictionary *)jsonObject {
    NSData *jsonData = [NSJSONSerialization isValidJSONObject:jsonObject] ? [NSJSONSerialization dataWithJSONObject:jsonObject options:0 error:nil] : nil;
    NSString *message = jsonData != nil ? [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding] : [jsonObject description];
    return [ThreemaError threemaError:message];
}

- (void)sendSignedRequest:(NSDictionary*)request toApiPath:(NSString*)apiPath forStore:(id<MyIdentityStoreProtocol>)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:(id<MyIdentityStoreProtocol>)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];
    
    /* Create authenticator for token */
    NSData *sharedSecret = [identityStore sharedSecretWithPublicKey:tokenRespKeyPub];
    NSData *responseKey = [[[ThreemaKDF alloc] initWithPersonal:@"3ma-csp"] deriveKeyWithSalt:@"dir" key:sharedSecret];
    NSData *response = [ThreemaKDF calculateMacWithKey:responseKey input:token];
    
    NSMutableDictionary *signedRequest = [NSMutableDictionary dictionaryWithDictionary:request];
    signedRequest[@"token"] = tokenStr;
    signedRequest[@"response"] = [response 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

[Dauer der Verarbeitung: 0.32 Sekunden]