diff --git a/Signal/src/Profiles/OWSProfileManager.m b/Signal/src/Profiles/OWSProfileManager.m index 6ff382e63..fff6dc1e1 100644 --- a/Signal/src/Profiles/OWSProfileManager.m +++ b/Signal/src/Profiles/OWSProfileManager.m @@ -336,12 +336,15 @@ static const NSInteger kProfileKeyLength = 16; UserProfile *userProfile = self.localUserProfile; OWSAssert(userProfile); - // If we have a new avatar image, we must first: - // - // * Encode it to JPEG. - // * Write it to disk. - // * Upload it to service. if (avatarImage) { + + // If we have a new avatar image, we must first: + // + // * Encode it to JPEG. + // * Write it to disk. + // * Encrypt it + // * Upload it to asset service + // * Send asset service info to Signal Service if (self.localCachedAvatarImage == avatarImage) { OWSAssert(userProfile.avatarUrl.length > 0); OWSAssert(userProfile.avatarDigest.length > 0); @@ -400,21 +403,33 @@ static const NSInteger kProfileKeyLength = 16; }); } -// TODO: The exact API & encryption scheme for avatars is not yet settled. -- (void)uploadAvatarToService:(NSData *)data - fileName:(NSString *)fileName +- (NSData *)encryptedAvatarData:(NSData *)plainTextData outDigest:(NSData **)outDigest +{ + DDLogError(@"TODO: Profile encryption scheme not yet settled."); + + // server accepts up to 14 base64 chars for digest + // 14 <= 4 * ceil(n/3) + NSUInteger kAvatarDigestByteLength = 9; + *outDigest = [Cryptography computeSHA256Digest:plainTextData truncatedToBytes:kAvatarDigestByteLength]; + + return plainTextData; +} + +- (void)uploadAvatarToService:(NSData *)avatarData + fileName:(NSString *)fileName // TODO do we need filename? success:(void (^)(NSString *avatarUrl, NSData *avatarDigest))successBlock failure:(void (^)())failureBlock { - OWSAssert(data.length > 0); + OWSAssert(avatarData.length > 0); OWSAssert(fileName.length > 0); OWSAssert(successBlock); OWSAssert(failureBlock); dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ - // TODO: - NSString *avatarUrl = @"avatarUrl"; - NSData *avatarDigest = [@"avatarDigest" dataUsingEncoding:NSUTF8StringEncoding]; + NSData *encryptedAvatarDigest; + NSData *encryptedAvatarData = [self encryptedAvatarData:avatarData outDigest:&encryptedAvatarDigest]; + OWSAssert(encryptedAvatarData.length > 0); + OWSAssert(encryptedAvatarDigest.length > 0); // See: https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-UsingHTTPPOST.html TSProfileAvatarUploadFormRequest *formRequest = [TSProfileAvatarUploadFormRequest new]; @@ -429,15 +444,6 @@ static const NSInteger kProfileKeyLength = 16; } NSDictionary *responseMap = formResponseObject; DDLogError(@"responseObject: %@", formResponseObject); - // acl = private; - // algorithm = "AWS4-HMAC-SHA256"; - // credential = - // "AKIAINTYCHN42UH3LGRA/20170804/us-east-1/s3/aws4_request"; date = - // 20170804T193927Z; key = PtRO3iSkY6twBA; policy = - // eyAiZXhwaXJhdGlvbiI6ICIyMDE3LTA4LTA0VDIwOjA5OjI3LjMwMFoiLAogICJjb25kaXRpb25zIjogWwogICAgeyJidWNrZXQiOiAic2lnbmFsLXByb2ZpbGVzLXN0YWdpbmcifSwKICAgIHsia2V5IjogIlB0Uk8zaVNrWTZ0d0JBIn0sCiAgICB7ImFjbCI6ICJwcml2YXRlIn0sCiAgICBbInN0YXJ0cy13aXRoIiwgIiRDb250ZW50LVR5cGUiLCAiIl0sCgogICAgeyJ4LWFtei1jcmVkZW50aWFsIjogIkFLSUFJTlRZQ0hONDJVSDNMR1JBLzIwMTcwODA0L3VzLWVhc3QtMS9zMy9hd3M0X3JlcXVlc3QifSwKICAgIHsieC1hbXotYWxnb3JpdGhtIjogIkFXUzQtSE1BQy1TSEEyNTYifSwKICAgIHsieC1hbXotZGF0ZSI6ICIyMDE3MDgwNFQxOTM5MjdaIiB9CiAgXQp9; - // signature = - // 3608fdc9af8ca0d13c754c34eb37014c9995b058c2e0166550468de47b00f316; - // url = "profiles-staging.signal.org"; NSString *formUrl = responseMap[@"url"]; if (![formUrl isKindOfClass:[NSString class]] || formUrl.length < 1) { @@ -487,87 +493,66 @@ static const NSInteger kProfileKeyLength = 16; failureBlock(); return; } - NSDictionary *parameters = @{ - @"acl" : formAcl, - @"x-amz-algorithm" : formAlgorithm, - @"x-amz-credential" : formCredential, - @"x-amz-date" : formDate, - @"key" : formKey, - @"policy" : formPolicy, - @"x-amz-signature" : formSignature, - - }; - - NSString *filePath = [self.profileAvatarsDirPath stringByAppendingPathComponent:fileName]; - NSInputStream *fileInputStream = [[NSInputStream alloc] initWithFileAtPath:filePath]; - if (!fileInputStream) { - OWSProdFail(@"profile_manager_error_avatar_upload_invalid_file_stream"); - failureBlock(); - return; - } - - NSError *error; - long long fileSize = - [[[NSFileManager defaultManager] attributesOfItemAtPath:filePath error:&error][NSFileSize] - longLongValue]; - if (error || fileSize <= 0) { - OWSProdFail(@"profile_manager_error_avatar_upload_invalid_file_size"); - failureBlock(); - return; - } - - NSMutableURLRequest *uploadRequest = [[AFHTTPRequestSerializer serializer] - multipartFormRequestWithMethod:@"post" - URLString:[@"https://" stringByAppendingString:formUrl] - parameters:parameters - constructingBodyWithBlock:^(id formData) { - // [formData appendPartWithFormData:<#(nonnull NSData *)#> name:@"acl"]; - - [formData appendPartWithInputStream:fileInputStream - name:@"file" - fileName:fileName - length:fileSize - mimeType:@"image/jpeg"]; - } - error:&error]; - - if (error || !uploadRequest) { - OWSProdFail(@"profile_manager_error_avatar_upload_invalid_upload_request"); - failureBlock(); - return; - } - - [uploadRequest setValue:@"Content-Type: text/html; charset=UTF-8" forHTTPHeaderField:@"Content-Type"]; - - // [uploadRequest setAllHTTPHeaderFields:headerDictionary]; - - // TODO: Should we use a special configuration as we do in TSNetworkManager? - // TODO: How does censorship circumvention fit in? - AFURLSessionManager *manager = [[AFURLSessionManager alloc] - initWithSessionConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration]]; - manager.responseSerializer = [AFXMLParserResponseSerializer new]; - NSURLSessionUploadTask *uploadTask; - uploadTask = [manager uploadTaskWithStreamedRequest:uploadRequest + AFHTTPSessionManager *profileHttpManager = + [[OWSSignalService sharedInstance] profileUploadingSessionManagerWithHostname:formUrl]; + + // Default acceptable content headers are rejected by AWS + profileHttpManager.responseSerializer.acceptableContentTypes = nil; + + [profileHttpManager POST:@"" + parameters:nil + constructingBodyWithBlock:^(id _Nonnull formData) { + NSData * (^formDataForString)(NSString *formString) = ^(NSString *formString) { + return [formString dataUsingEncoding:NSUTF8StringEncoding]; + }; + + // We have to build up the form manually vs. simply passing in a paramaters dict + // because AWS is sensitive to the order of the order of the form params (at least + // the "key" field must occur early on). + // For consistency, all fields are ordered here in a known working order. + [formData appendPartWithFormData:formDataForString(formKey) name:@"key"]; + [formData appendPartWithFormData:formDataForString(formAcl) name:@"acl"]; + [formData appendPartWithFormData:formDataForString(formAlgorithm) name:@"x-amz-algorithm"]; + [formData appendPartWithFormData:formDataForString(formCredential) name:@"x-amz-credential"]; + [formData appendPartWithFormData:formDataForString(formDate) name:@"x-amz-date"]; + [formData appendPartWithFormData:formDataForString(formPolicy) name:@"policy"]; + [formData appendPartWithFormData:formDataForString(formSignature) name:@"x-amz-signature"]; + [formData appendPartWithFormData:formDataForString(OWSMimeTypeApplicationOctetStream) + name:@"Content-Type"]; + [formData appendPartWithFormData:encryptedAvatarData name:@"file"]; + + DDLogVerbose(@"%@ constructed body", self.tag); + } progress:^(NSProgress *_Nonnull uploadProgress) { - // This is not called back on the main queue. - // You are responsible for dispatching to the main queue for UI updates - - DDLogVerbose(@"%@ Avatar upload progress: %f", self.tag, uploadProgress.fractionCompleted); + DDLogVerbose( + @"%@ avatar upload progress: %.2f%%", self.tag, uploadProgress.fractionCompleted * 100); } - completionHandler:^(NSURLResponse *_Nonnull response, - id _Nullable uploadResponseObject, - NSError *_Nullable uploadError) { - - if (uploadError) { - DDLogError(@"%@ Avatar upload failed: %@", self.tag, uploadError); + success:^(NSURLSessionDataTask *_Nonnull uploadTask, id _Nullable responseObject) { + OWSAssert([uploadTask.response isKindOfClass:[NSHTTPURLResponse class]]); + NSHTTPURLResponse *response = (NSHTTPURLResponse *)uploadTask.response; + + // We could also construct this URL locally from manager.baseUrl + formKey + // but the approach of getting it from the remote provider seems a more + // robust way to ensure we've actually created the resource where we + // think we have. + NSString *avatarURL = response.allHeaderFields[@"Location"]; + if (avatarURL.length == 0) { + OWSProdFail(@"profile_manager_error_avatar_upload_no_location_in_response"); failureBlock(); - } else { - DDLogVerbose(@"%@ Avatar upload succeeded", self.tag); - successBlock(avatarUrl, avatarDigest); + return; } + + DDLogVerbose(@"%@ successfully uploaded avatar url: %@ digest: %@", + self.tag, + avatarURL, + encryptedAvatarDigest); + successBlock(avatarURL, encryptedAvatarDigest); + } + failure:^(NSURLSessionDataTask *_Nullable uploadTask, NSError *_Nonnull error) { + DDLogVerbose(@"%@ uploading avatar failed with error: %@", self.tag, error); + failureBlock(); }]; - [uploadTask resume]; } failure:^(NSURLSessionDataTask *task, NSError *error) { DDLogError(@"%@ Failed to get profile avatar upload form: %@", self.tag, error); diff --git a/SignalServiceKit/src/Network/API/Requests/TSSetProfileRequest.m b/SignalServiceKit/src/Network/API/Requests/TSSetProfileRequest.m index 0d10c88eb..822d0232e 100644 --- a/SignalServiceKit/src/Network/API/Requests/TSSetProfileRequest.m +++ b/SignalServiceKit/src/Network/API/Requests/TSSetProfileRequest.m @@ -22,11 +22,14 @@ NS_ASSUME_NONNULL_BEGIN if (profileNameEncrypted.length > 0) { self.parameters[@"name"] = [profileNameEncrypted base64EncodedString]; } - if (avatarUrl.length > 0) { + if (avatarUrl.length > 0 && avatarDigest.length > 0) { + // TODO why is this base64 encoded? self.parameters[@"avatar"] = [[avatarUrl dataUsingEncoding:NSUTF8StringEncoding] base64EncodedString]; - } - if (avatarDigest.length > 0) { + self.parameters[@"avatarDigest"] = [avatarDigest base64EncodedString]; + } else { + OWSAssert(avatarUrl.length == 0); + OWSAssert(avatarDigest.length == 0); } return self; diff --git a/SignalServiceKit/src/Network/API/TSNetworkManager.m b/SignalServiceKit/src/Network/API/TSNetworkManager.m index 3b18a7b7b..d8853ca6e 100644 --- a/SignalServiceKit/src/Network/API/TSNetworkManager.m +++ b/SignalServiceKit/src/Network/API/TSNetworkManager.m @@ -61,7 +61,7 @@ typedef void (^failureBlock)(NSURLSessionDataTask *task, NSError *error); void (^failure)(NSURLSessionDataTask *task, NSError *error) = [TSNetworkManager errorPrettifyingForFailureBlock:failureBlock]; - AFHTTPSessionManager *sessionManager = [OWSSignalService sharedInstance].HTTPSessionManager; + AFHTTPSessionManager *sessionManager = [OWSSignalService sharedInstance].signalServiceSessionManager; if ([request isKindOfClass:[TSVerifyCodeRequest class]]) { // We plant the Authorization parameter ourselves, no need to double add. diff --git a/SignalServiceKit/src/Network/OWSSignalService.h b/SignalServiceKit/src/Network/OWSSignalService.h index ecb570681..13376c53a 100644 --- a/SignalServiceKit/src/Network/OWSSignalService.h +++ b/SignalServiceKit/src/Network/OWSSignalService.h @@ -12,7 +12,9 @@ extern NSString *const kNSNotificationName_IsCensorshipCircumventionActiveDidCha @interface OWSSignalService : NSObject -@property (nonatomic, readonly) AFHTTPSessionManager *HTTPSessionManager; +@property (nonatomic, readonly) AFHTTPSessionManager *signalServiceSessionManager; + +- (AFHTTPSessionManager *)profileUploadingSessionManagerWithHostname:(NSString *)hostname; @property (atomic, readonly) BOOL isCensorshipCircumventionActive; diff --git a/SignalServiceKit/src/Network/OWSSignalService.m b/SignalServiceKit/src/Network/OWSSignalService.m index 1cf1bd035..53251c2d8 100644 --- a/SignalServiceKit/src/Network/OWSSignalService.m +++ b/SignalServiceKit/src/Network/OWSSignalService.m @@ -154,19 +154,20 @@ NSString *const kNSNotificationName_IsCensorshipCircumventionActiveDidChange = } } -- (AFHTTPSessionManager *)HTTPSessionManager +- (AFHTTPSessionManager *)signalServiceSessionManager { if (self.isCensorshipCircumventionActive) { DDLogInfo(@"%@ using reflector HTTPSessionManager", self.tag); - return self.reflectorHTTPSessionManager; + return self.reflectorSignalServiceSessionManager; } else { - return self.defaultHTTPSessionManager; + return self.defaultSignalServiceSessionManager; } } -- (AFHTTPSessionManager *)defaultHTTPSessionManager +- (AFHTTPSessionManager *)defaultSignalServiceSessionManager { NSURL *baseURL = [[NSURL alloc] initWithString:textSecureServerURL]; + OWSAssert(baseURL); NSURLSessionConfiguration *sessionConf = NSURLSessionConfiguration.ephemeralSessionConfiguration; AFHTTPSessionManager *sessionManager = [[AFHTTPSessionManager alloc] initWithBaseURL:baseURL sessionConfiguration:sessionConf]; @@ -178,7 +179,7 @@ NSString *const kNSNotificationName_IsCensorshipCircumventionActiveDidChange = return sessionManager; } -- (AFHTTPSessionManager *)reflectorHTTPSessionManager +- (AFHTTPSessionManager *)reflectorSignalServiceSessionManager { NSString *localNumber = [TSAccountManager localNumber]; OWSAssert(localNumber.length > 0); @@ -190,6 +191,7 @@ NSString *const kNSNotificationName_IsCensorshipCircumventionActiveDidChange = frontingHost = self.manualCensorshipCircumventionDomain; }; NSURL *baseURL = [[NSURL alloc] initWithString:[self.censorshipConfiguration frontingHost:localNumber]]; + OWSAssert(baseURL); NSURLSessionConfiguration *sessionConf = NSURLSessionConfiguration.ephemeralSessionConfiguration; AFHTTPSessionManager *sessionManager = [[AFHTTPSessionManager alloc] initWithBaseURL:baseURL sessionConfiguration:sessionConf]; @@ -204,6 +206,25 @@ NSString *const kNSNotificationName_IsCensorshipCircumventionActiveDidChange = return sessionManager; } +#pragma mark - Profile Uploading + +- (AFHTTPSessionManager *)profileUploadingSessionManagerWithHostname:(NSString *)hostname +{ + OWSAssert(hostname.length > 0); + if (self.isCensorshipCircumventionActive) { + DDLogInfo(@"%@ Profile uploading may not work when under censorship.", self.tag); + } + + NSURL *baseURL = [[NSURL alloc] initWithString:[@"https://" stringByAppendingString:hostname]]; + NSURLSessionConfiguration *sessionConf = NSURLSessionConfiguration.ephemeralSessionConfiguration; + AFHTTPSessionManager *sessionManager = + [[AFHTTPSessionManager alloc] initWithBaseURL:baseURL sessionConfiguration:sessionConf]; + + sessionManager.securityPolicy = [OWSHTTPSecurityPolicy sharedPolicy]; + + return sessionManager; +} + #pragma mark - Google Pinning Policy /** diff --git a/SignalServiceKit/src/TSConstants.h b/SignalServiceKit/src/TSConstants.h index 41a38eda1..19b5f485f 100644 --- a/SignalServiceKit/src/TSConstants.h +++ b/SignalServiceKit/src/TSConstants.h @@ -24,9 +24,11 @@ typedef enum { kSMSVerification, kPhoneNumberVerification } VerificationTranspor #define textSecureWebSocketAPI @"wss://textsecure-service.whispersystems.org/v1/websocket/" #define textSecureServerURL @"https://textsecure-service.whispersystems.org/" +#define textSecureProfileServerURL @"https://profiles.signal.org" -//#define textSecureWebSocketAPI @"wss://textsecure-service-staging.whispersystems.org/v1/websocket/" -//#define textSecureServerURL @"https://textsecure-service-staging.whispersystems.org/" +//#define textSecureWebSocketAPI @"wss://textsecure-service-staging.whispersystems.org/v1/websocket/" +//#define textSecureServerURL @"https://textsecure-service-staging.whispersystems.org/" +//#define textSecureProfileServerURL @"https://profiles-staging.signal.org" #define textSecureGeneralAPI @"v1" #define textSecureAccountsAPI @"v1/accounts"