From 7499b3aaf0211a0377ac0c66f5b3e46f643bee2e Mon Sep 17 00:00:00 2001 From: Michael Kirk Date: Mon, 14 Aug 2017 11:31:43 -0400 Subject: [PATCH] Avatar API integration / WIP crypto scheme Crypto Scheme: - Name (un)padding - WIP AES-GCM (funtioning, but need to verify against android implementation, and tag functionality) Changes to avatar API: - hard code avatar domain (cdn.signal.org) - avatar form hands out new avatar key, invalidating old avatar - preliminary aes-gcm integration Also: - New type to represent AES128 keys, rather than passing around opaque data blobs everywhere, we can use the compiler to help us make sure we're passing compliant keying material. - Started using factory pattern for API requests. This is intended to be a lighter weight way to implement new API requests, rather than the current 1-method class ceremony. // FREEBIE --- Signal/src/Profiles/OWSProfileManager.h | 12 +- Signal/src/Profiles/OWSProfileManager.m | 255 +++++------ Signal/src/Profiles/ProfileFetcherJob.swift | 35 +- .../ViewControllers/SignalsViewController.m | 13 - SignalServiceKit/Makefile | 4 +- .../src/Messages/TSMessagesManager.m | 4 +- .../src/Network/API/OWSRequestBuilder.h | 15 + .../src/Network/API/OWSRequestBuilder.m | 43 ++ .../src/Network/API/Requests/TSRequest.h | 11 +- .../src/Network/API/Requests/TSRequest.m | 10 +- .../API/Requests/TSSetProfileRequest.h | 19 - .../API/Requests/TSSetProfileRequest.m | 40 -- .../src/Protocols/ProfileManagerProtocol.h | 5 +- SignalServiceKit/src/Protocols/ProtoBuf+OWS.m | 9 +- .../src/Security/CommonCryptorSPI.h | 397 ++++++++++++++++++ SignalServiceKit/src/TSConstants.h | 2 +- SignalServiceKit/src/Util/Cryptography.h | 25 ++ SignalServiceKit/src/Util/Cryptography.m | 187 +++++++++ .../tests/Util/CryptographyTests.m | 19 + 19 files changed, 833 insertions(+), 272 deletions(-) create mode 100644 SignalServiceKit/src/Network/API/OWSRequestBuilder.h create mode 100644 SignalServiceKit/src/Network/API/OWSRequestBuilder.m delete mode 100644 SignalServiceKit/src/Network/API/Requests/TSSetProfileRequest.h delete mode 100644 SignalServiceKit/src/Network/API/Requests/TSSetProfileRequest.m create mode 100644 SignalServiceKit/src/Security/CommonCryptorSPI.h diff --git a/Signal/src/Profiles/OWSProfileManager.h b/Signal/src/Profiles/OWSProfileManager.h index 7a4b372bf..59fb9c8c3 100644 --- a/Signal/src/Profiles/OWSProfileManager.h +++ b/Signal/src/Profiles/OWSProfileManager.h @@ -10,6 +10,7 @@ extern NSString *const kNSNotificationName_LocalProfileDidChange; extern NSString *const kNSNotificationName_OtherUsersProfileDidChange; @class TSThread; +@class OWSAES128Key; // This class can be safely accessed and used from any thread. @interface OWSProfileManager : NSObject @@ -23,7 +24,7 @@ extern NSString *const kNSNotificationName_OtherUsersProfileDidChange; #pragma mark - Local Profile // These two methods should only be called from the main thread. -- (NSData *)localProfileKey; +- (OWSAES128Key *)localProfileKey; - (BOOL)hasLocalProfile; - (nullable NSString *)localProfileName; - (nullable UIImage *)localProfileAvatarImage; @@ -50,9 +51,7 @@ extern NSString *const kNSNotificationName_OtherUsersProfileDidChange; #pragma mark - Other User's Profiles -- (void)setProfileKey:(NSData *)profileKey forRecipientId:(NSString *)recipientId; - -- (nullable NSData *)profileKeyForRecipientId:(NSString *)recipientId; +- (nullable OWSAES128Key *)profileKeyForRecipientId:(NSString *)recipientId; - (nullable NSString *)profileNameForRecipientId:(NSString *)recipientId; @@ -61,9 +60,8 @@ extern NSString *const kNSNotificationName_OtherUsersProfileDidChange; - (void)refreshProfileForRecipientId:(NSString *)recipientId; - (void)updateProfileForRecipientId:(NSString *)recipientId - profileNameEncrypted:(NSData *_Nullable)profileNameEncrypted - avatarUrlData:(NSData *_Nullable)avatarUrlData - avatarDigest:(NSData *_Nullable)avatarDigest; + profileNameEncrypted:(nullable NSData *)profileNameEncrypted + avatarUrl:(nullable NSString *)avatarUrl; @end diff --git a/Signal/src/Profiles/OWSProfileManager.m b/Signal/src/Profiles/OWSProfileManager.m index fd16a03bc..c5272059b 100644 --- a/Signal/src/Profiles/OWSProfileManager.m +++ b/Signal/src/Profiles/OWSProfileManager.m @@ -5,13 +5,14 @@ #import "OWSProfileManager.h" #import "Environment.h" #import "Signal-Swift.h" +#import #import #import #import +#import #import #import #import -#import #import #import #import @@ -24,10 +25,13 @@ NS_ASSUME_NONNULL_BEGIN // These properties may be accessed from any thread. @property (atomic, readonly) NSString *recipientId; -@property (atomic, nullable) NSData *profileKey; +@property (atomic, nullable) OWSAES128Key *profileKey; // These properties may be accessed only from the main thread. @property (nonatomic, nullable) NSString *profileName; + +// TODO This isn't really a URL, since it doesn't contain the host. +// rename to "avatarPath" or "avatarKey" @property (nonatomic, nullable) NSString *avatarUrl; // This filename is relative to OWSProfileManager.profileAvatarsDirPath. @@ -89,8 +93,9 @@ NSString *const kNSNotificationName_OtherUsersProfileDidChange = @"kNSNotificati NSString *const kOWSProfileManager_UserWhitelistCollection = @"kOWSProfileManager_UserWhitelistCollection"; NSString *const kOWSProfileManager_GroupWhitelistCollection = @"kOWSProfileManager_GroupWhitelistCollection"; -// TODO: -static const NSInteger kProfileKeyLength = 16; +/// The max bytes for a user's profile name, encoded in UTF8. +/// Before encrypting and submitting we NULL pad the name data to this length. +static const NSUInteger kOWSProfileManager_NameDataLength = 26; @interface OWSProfileManager () @@ -168,7 +173,8 @@ static const NSInteger kProfileKeyLength = 16; self.localUserProfile = [self getOrBuildUserProfileForRecipientId:kLocalProfileUniqueId]; OWSAssert(self.localUserProfile); if (!self.localUserProfile.profileKey) { - self.localUserProfile.profileKey = [OWSProfileManager generateLocalProfileKey]; + DDLogInfo(@"%@ Generating local profile key", self.tag); + self.localUserProfile.profileKey = [OWSAES128Key generateRandomKey]; // Make sure to save on the local db connection for consistency. // // NOTE: we do an async read/write here to avoid blocking during app launch path. @@ -176,7 +182,7 @@ static const NSInteger kProfileKeyLength = 16; [self.localUserProfile saveWithTransaction:transaction]; }]; } - OWSAssert(self.localUserProfile.profileKey.length == kProfileKeyLength); + OWSAssert(self.localUserProfile.profileKey.keyData.length == kAES128_KeyByteLength); return self; } @@ -194,6 +200,11 @@ static const NSInteger kProfileKeyLength = 16; object:nil]; } +- (AFHTTPSessionManager *)avatarHTTPManager +{ + return [OWSSignalService sharedInstance].cdnSessionManager; +} + #pragma mark - User Profile Accessor // This method can be safely called from any thread. @@ -238,21 +249,11 @@ static const NSInteger kProfileKeyLength = 16; } } -#pragma mark - Local Profile Key - -+ (NSData *)generateLocalProfileKey -{ - DDLogInfo(@"%@ Generating profile key for local user.", self.tag); - // TODO: - DDLogVerbose(@"%@ Profile key generation is not yet implemented.", self.tag); - return [SecurityUtils generateRandomBytes:kProfileKeyLength]; -} - #pragma mark - Local Profile -- (NSData *)localProfileKey +- (OWSAES128Key *)localProfileKey { - OWSAssert(self.localUserProfile.profileKey.length == kProfileKeyLength); + OWSAssert(self.localUserProfile.profileKey.keyData.length == kAES128_KeyByteLength); return self.localUserProfile.profileKey; } @@ -306,14 +307,16 @@ static const NSInteger kProfileKeyLength = 16; // * Update client state on success. void (^tryToUpdateService)(NSString *_Nullable, NSString *_Nullable) = ^( NSString *_Nullable avatarUrl, NSString *_Nullable avatarFileName) { - [self updateProfileOnService:profileName - avatarUrl:avatarUrl + [self updateServiceWithProfileName:profileName success:^{ // All reads and writes to user profiles should happen on the main thread. dispatch_async(dispatch_get_main_queue(), ^{ UserProfile *userProfile = self.localUserProfile; OWSAssert(userProfile); userProfile.profileName = profileName; + + // TODO remote avatarUrl changes as result of fetching form - + // we should probably invalidate it at that point, and refresh again when uploading file completes. userProfile.avatarUrl = avatarUrl; userProfile.avatarFileName = avatarFileName; @@ -343,7 +346,6 @@ static const NSInteger kProfileKeyLength = 16; // * Send asset service info to Signal Service if (self.localCachedAvatarImage == avatarImage) { OWSAssert(userProfile.avatarUrl.length > 0); - // TODO do we need avatarFileName? OWSAssert(userProfile.avatarFileName.length > 0); DDLogVerbose(@"%@ Updating local profile on service with unchanged avatar.", self.tag); @@ -354,7 +356,6 @@ static const NSInteger kProfileKeyLength = 16; [self writeAvatarToDisk:avatarImage success:^(NSData *data, NSString *fileName) { [self uploadAvatarToService:data - fileName:fileName success:^(NSString *avatarUrl) { tryToUpdateService(avatarUrl, fileName); } @@ -399,30 +400,25 @@ static const NSInteger kProfileKeyLength = 16; }); } -- (NSData *)encryptedAvatarData:(NSData *)plainTextData -{ - DDLogError(@"TODO: Profile encryption scheme not yet settled."); - - return plainTextData; -} - - (void)uploadAvatarToService:(NSData *)avatarData - fileName:(NSString *)fileName // TODO do we need filename? success:(void (^)(NSString *avatarUrl))successBlock failure:(void (^)())failureBlock { OWSAssert(avatarData.length > 0); - OWSAssert(fileName.length > 0); OWSAssert(successBlock); OWSAssert(failureBlock); dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ - NSData *encryptedAvatarData = [self encryptedAvatarData:avatarData]; + NSData *encryptedAvatarData = [self encryptProfileData:avatarData]; OWSAssert(encryptedAvatarData.length > 0); // See: https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-UsingHTTPPOST.html TSProfileAvatarUploadFormRequest *formRequest = [TSProfileAvatarUploadFormRequest new]; + // TODO: Since this form request causes the server to reset my avatar URL, if the update fails + // at some point from here on out, we want the user to understand they probably no longer have + // a profile avatar on the server. + [self.networkManager makeRequest:formRequest success:^(NSURLSessionDataTask *task, id formResponseObject) { @@ -477,10 +473,7 @@ static const NSInteger kProfileKeyLength = 16; return; } - AFHTTPSessionManager *profileHttpManager = - [[OWSSignalService sharedInstance] cdnSessionManager]; - - [profileHttpManager POST:@"" + [self.avatarHTTPManager POST:@"" parameters:nil constructingBodyWithBlock:^(id _Nonnull formData) { NSData * (^formDataForString)(NSString *formString) = ^(NSString *formString) { @@ -539,30 +532,25 @@ static const NSInteger kProfileKeyLength = 16; } // TODO: The exact API & encryption scheme for profiles is not yet settled. -- (void)updateProfileOnService:(nullable NSString *)localProfileName - avatarUrl:(nullable NSString *)avatarUrl - success:(void (^)())successBlock - failure:(void (^)())failureBlock +- (void)updateServiceWithProfileName:(nullable NSString *)localProfileName + success:(void (^)())successBlock + failure:(void (^)())failureBlock { OWSAssert(successBlock); OWSAssert(failureBlock); dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ - NSData *_Nullable profileNameEncrypted = [self encryptProfileString:localProfileName]; - - DDLogError(@"%@ TODO replace with set Name request.", self.tag); - // TSSetProfileRequest *request = [[TSSetProfileRequest alloc] initWithProfileName:profileNameEncrypted - // avatarUrl:avatarUrl - // avatarDigest:avatarDigest]; - // - // [self.networkManager makeRequest:request - // success:^(NSURLSessionDataTask *task, id responseObject) { - // successBlock(); - // } - // failure:^(NSURLSessionDataTask *task, NSError *error) { - // DDLogError(@"%@ Failed to update profile with error: %@", self.tag, error); - // failureBlock(); - // }]; + NSData *_Nullable encryptedPaddedName = [self encryptProfileNameWithUnpaddedName:localProfileName]; + + TSRequest *request = [OWSRequestBuilder profileNameSetRequestWithEncryptedPaddedName:encryptedPaddedName]; + [self.networkManager makeRequest:request + success:^(NSURLSessionDataTask *task, id responseObject) { + successBlock(); + } + failure:^(NSURLSessionDataTask *task, NSError *error) { + DDLogError(@"%@ Failed to update profile with error: %@", self.tag, error); + failureBlock(); + }]; }); } @@ -683,18 +671,18 @@ static const NSInteger kProfileKeyLength = 16; #pragma mark - Other User's Profiles -- (void)setProfileKey:(NSData *)profileKey forRecipientId:(NSString *)recipientId +- (void)setProfileKeyData:(NSData *)profileKeyData forRecipientId:(NSString *)recipientId; { - OWSAssert(profileKey.length == kProfileKeyLength); - OWSAssert(recipientId.length > 0); - if (profileKey.length != kProfileKeyLength) { + OWSAES128Key *_Nullable profileKey = [OWSAES128Key keyWithData:profileKeyData]; + if (profileKey == nil) { + OWSFail(@"Failed to make profile key for key data"); return; } - + dispatch_async(dispatch_get_main_queue(), ^{ UserProfile *userProfile = [self getOrBuildUserProfileForRecipientId:recipientId]; OWSAssert(userProfile); - if (userProfile.profileKey && [userProfile.profileKey isEqual:profileKey]) { + if (userProfile.profileKey && [userProfile.profileKey.keyData isEqual:profileKey.keyData]) { // Ignore redundant update. return; } @@ -712,7 +700,7 @@ static const NSInteger kProfileKeyLength = 16; }); } -- (nullable NSData *)profileKeyForRecipientId:(NSString *)recipientId +- (nullable OWSAES128Key *)profileKeyForRecipientId:(NSString *)recipientId { OWSAssert(recipientId.length > 0); @@ -745,12 +733,12 @@ static const NSInteger kProfileKeyLength = 16; } UserProfile *userProfile = [self getOrBuildUserProfileForRecipientId:recipientId]; - if (userProfile.avatarFileName) { + if (userProfile.avatarFileName.length > 0) { image = [self loadProfileAvatarWithFilename:userProfile.avatarFileName]; if (image) { [self.otherUsersProfileAvatarImageCache setObject:image forKey:recipientId]; } - } else if (userProfile.avatarUrl) { + } else if (userProfile.avatarUrl.length > 0) { [self downloadAvatarForUserProfile:userProfile]; } @@ -762,29 +750,18 @@ static const NSInteger kProfileKeyLength = 16; OWSAssert([NSThread isMainThread]); OWSAssert(userProfile); - if (userProfile.profileKey.length < 1 || userProfile.avatarUrl.length < 1) { + if (userProfile.avatarUrl.length < 1) { + OWSFail(@"%@ Malformed avatar URL: %@", self.tag, userProfile.avatarUrl); return; } - NSData *profileKeyAtStart = userProfile.profileKey; - - NSURL *url = [NSURL URLWithString:userProfile.avatarUrl]; - if (!url) { - OWSFail(@"%@ Malformed avatar URL: %@", self.tag, userProfile.avatarUrl); + if (userProfile.profileKey.keyData.length < 1 || userProfile.avatarUrl.length < 1) { return; } - NSString *_Nullable fileExtension = [[[url lastPathComponent] pathExtension] lowercaseString]; - NSSet *validFileExtensions = [NSSet setWithArray:@[ - @"jpg", - @"jpeg", - @"png", - @"gif", - ]]; - if (![validFileExtensions containsObject:fileExtension]) { - DDLogWarn(@"Ignoring avatar with invalid file extension: %@", userProfile.avatarUrl); - } - NSString *fileName = [[NSUUID UUID].UUIDString stringByAppendingPathExtension:fileExtension]; + OWSAES128Key *profileKeyAtStart = userProfile.profileKey; + + NSString *fileName = [[NSUUID UUID].UUIDString stringByAppendingPathExtension:@"jpg"]; NSString *filePath = [self.profileAvatarsDirPath stringByAppendingPathComponent:fileName]; if ([self.currentAvatarDownloads containsObject:userProfile.recipientId]) { @@ -796,25 +773,21 @@ static const NSInteger kProfileKeyLength = 16; NSString *tempDirectory = NSTemporaryDirectory(); NSString *tempFilePath = [tempDirectory stringByAppendingPathComponent:fileName]; - // TODO: Should we use a special configuration as we do in TSNetworkManager? - // TODO: How does censorship circumvention fit in? - NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration]; - AFURLSessionManager *manager = [[AFURLSessionManager alloc] initWithSessionConfiguration:configuration]; - NSURLRequest *request = [NSURLRequest requestWithURL:url]; - NSURLSessionDownloadTask *downloadTask = [manager downloadTaskWithRequest:request - progress:nil - destination:^NSURL *(NSURL *targetPath, NSURLResponse *response) { + NSURL *avatarUrl = [NSURL URLWithString:userProfile.avatarUrl relativeToURL:self.avatarHTTPManager.baseURL]; + NSURLRequest *request = [NSURLRequest requestWithURL:avatarUrl]; + NSURLSessionDownloadTask *downloadTask = [self.avatarHTTPManager downloadTaskWithRequest:request + progress:^(NSProgress *_Nonnull downloadProgress) { + DDLogVerbose(@"%@ Downloading avatar for %@", self.tag, userProfile.recipientId); + } + destination:^NSURL *_Nonnull(NSURL *_Nonnull targetPath, NSURLResponse *_Nonnull response) { return [NSURL fileURLWithPath:tempFilePath]; } - completionHandler:^(NSURLResponse *response, NSURL *filePathParam, NSError *error) { - OWSAssert([[NSURL fileURLWithPath:tempFilePath] isEqual:filePathParam]); - + completionHandler:^( + NSURLResponse *_Nonnull response, NSURL *_Nullable filePathParam, NSError *_Nullable error) { // Ensure disk IO and decryption occurs off the main thread. dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ - NSData *_Nullable encryptedData = (error ? nil : [NSData dataWithContentsOfFile:tempFilePath]); - NSData *_Nullable decryptedData = - [OWSProfileManager decryptProfileData:encryptedData profileKey:profileKeyAtStart]; + NSData *_Nullable decryptedData = [self decryptProfileData:encryptedData profileKey:profileKeyAtStart]; UIImage *_Nullable image = nil; if (decryptedData) { BOOL success = [decryptedData writeToFile:filePath atomically:YES]; @@ -828,7 +801,7 @@ static const NSInteger kProfileKeyLength = 16; UserProfile *currentUserProfile = [self getOrBuildUserProfileForRecipientId:userProfile.recipientId]; - if (currentUserProfile.profileKey.length < 1 + if (currentUserProfile.profileKey.keyData.length < 1 || ![currentUserProfile.profileKey isEqual:userProfile.profileKey]) { DDLogWarn(@"%@ Ignoring avatar download for obsolete user profile.", self.tag); } else if (error) { @@ -888,8 +861,8 @@ static const NSInteger kProfileKeyLength = 16; } - (void)updateProfileForRecipientId:(NSString *)recipientId - profileNameEncrypted:(NSData *_Nullable)profileNameEncrypted - avatarUrlData:(NSData *_Nullable)avatarUrlData + profileNameEncrypted:(nullable NSData *)profileNameEncrypted + avatarUrl:(nullable NSString *)avatarUrl; { OWSAssert(recipientId.length > 0); @@ -902,11 +875,7 @@ static const NSInteger kProfileKeyLength = 16; } NSString *_Nullable profileName = - [self decryptProfileString:profileNameEncrypted profileKey:userProfile.profileKey]; - - // TODO this will be plain text, no need for it to be base64 encoded - NSString *_Nullable avatarUrl - = (avatarUrlData ? [[NSString alloc] initWithData:avatarUrlData encoding:NSUTF8StringEncoding] : nil); + [self decryptProfileNameData:profileNameEncrypted profileKey:userProfile.profileKey]; BOOL isAvatarSame = [self isNullableStringEqual:userProfile.avatarUrl toString:avatarUrl]; @@ -954,76 +923,80 @@ static const NSInteger kProfileKeyLength = 16; #pragma mark - Profile Encryption -+ (NSData *_Nullable)decryptProfileData:(NSData *_Nullable)encryptedData profileKey:(NSData *)profileKey +- (nullable NSData *)encryptProfileData:(nullable NSData *)encryptedData profileKey:(OWSAES128Key *)profileKey { - OWSAssert(profileKey.length == kProfileKeyLength); + OWSAssert(profileKey.keyData.length == kAES128_KeyByteLength); if (!encryptedData) { return nil; } - // TODO: Decrypt. For now, return the input. - return encryptedData; + return [Cryptography encryptAESGCMWithData:encryptedData key:profileKey]; } -+ (NSString *_Nullable)decryptProfileString:(NSData *_Nullable)encryptedData profileKey:(NSData *)profileKey +- (nullable NSData *)decryptProfileData:(nullable NSData *)encryptedData profileKey:(OWSAES128Key *)profileKey { - OWSAssert(profileKey.length == kProfileKeyLength); - - NSData *_Nullable decryptedData = [self decryptProfileData:encryptedData profileKey:profileKey]; + OWSAssert(profileKey.keyData.length == kAES128_KeyByteLength); - if (decryptedData) { - return [[NSString alloc] initWithData:decryptedData encoding:NSUTF8StringEncoding]; - } else { + if (!encryptedData) { return nil; } + + return [Cryptography decryptAESGCMWithData:encryptedData key:profileKey]; } -+ (NSData *_Nullable)encryptProfileData:(NSData *_Nullable)data profileKey:(NSData *)profileKey +- (nullable NSString *)decryptProfileNameData:(nullable NSData *)encryptedData profileKey:(OWSAES128Key *)profileKey { - OWSAssert(profileKey.length == kProfileKeyLength); + OWSAssert(profileKey.keyData.length == kAES128_KeyByteLength); - if (!data) { + NSData *_Nullable decryptedData = [self decryptProfileData:encryptedData profileKey:profileKey]; + if (decryptedData.length < 1) { return nil; } - // TODO: Encrypt. For now, return the input. - return data; -} -+ (NSData *_Nullable)encryptProfileString:(NSString *_Nullable)value profileKey:(NSData *)profileKey -{ - OWSAssert(profileKey.length == kProfileKeyLength); + // Unpad profile name. + NSUInteger unpaddedLength = 0; + const char *bytes = decryptedData.bytes; - if (value) { - NSData *_Nullable data = [value dataUsingEncoding:NSUTF8StringEncoding]; - if (data) { - NSData *_Nullable encryptedData = [self encryptProfileData:data profileKey:profileKey]; - return encryptedData; + // Work through the bytes until we encounter our first + // padding byte (our padding scheme is NULL bytes) + for (NSUInteger i = 0; i < decryptedData.length; i++) { + if (bytes[i] == 0x00) { + break; } + unpaddedLength = i + 1; } - return nil; -} + NSData *unpaddedData = [decryptedData subdataWithRange:NSMakeRange(0, unpaddedLength)]; -- (NSData *_Nullable)decryptProfileData:(NSData *_Nullable)encryptedData profileKey:(NSData *)profileKey -{ - return [OWSProfileManager decryptProfileData:encryptedData profileKey:profileKey]; + return [[NSString alloc] initWithData:unpaddedData encoding:NSUTF8StringEncoding]; } -- (NSString *_Nullable)decryptProfileString:(NSData *_Nullable)encryptedData profileKey:(NSData *)profileKey +- (nullable NSData *)encryptProfileData:(nullable NSData *)data { - return [OWSProfileManager decryptProfileString:encryptedData profileKey:profileKey]; + return [self encryptProfileData:data profileKey:self.localProfileKey]; } -- (NSData *_Nullable)encryptProfileData:(NSData *_Nullable)data +- (nullable NSData *)encryptProfileNameWithUnpaddedName:(NSString *)name { - return [OWSProfileManager encryptProfileData:data profileKey:self.localProfileKey]; -} + if (name.length == 0) { + return nil; + } + + NSData *nameData = [name dataUsingEncoding:NSUTF8StringEncoding]; + if (nameData.length > kOWSProfileManager_NameDataLength) { + OWSFail(@"%@ name data is too long with length:%lu", self.tag, (unsigned long)nameData.length); + return nil; + } -- (NSData *_Nullable)encryptProfileString:(NSString *_Nullable)value -{ - return [OWSProfileManager encryptProfileString:value profileKey:self.localProfileKey]; + NSUInteger paddingByteCount = kOWSProfileManager_NameDataLength - nameData.length; + + NSMutableData *paddedNameData = [nameData mutableCopy]; + [paddedNameData increaseLengthBy:paddingByteCount]; + OWSAssert(paddedNameData.length == kOWSProfileManager_NameDataLength); + + return [self encryptProfileData:[paddedNameData copy] profileKey:self.localProfileKey]; } #pragma mark - Avatar Disk Cache diff --git a/Signal/src/Profiles/ProfileFetcherJob.swift b/Signal/src/Profiles/ProfileFetcherJob.swift index 4098f6279..b105fc4d8 100644 --- a/Signal/src/Profiles/ProfileFetcherJob.swift +++ b/Signal/src/Profiles/ProfileFetcherJob.swift @@ -114,10 +114,9 @@ class ProfileFetcherJob: NSObject { private func updateProfile(signalServiceProfile: SignalServiceProfile) { verifyIdentityUpToDateAsync(recipientId: signalServiceProfile.recipientId, latestIdentityKey: signalServiceProfile.identityKey) - OWSProfileManager.shared().updateProfile(forRecipientId : signalServiceProfile.recipientId, - profileNameEncrypted : signalServiceProfile.profileNameEncrypted, - avatarUrlData : signalServiceProfile.avatarUrlData, - avatarDigest : signalServiceProfile.avatarDigest) + OWSProfileManager.shared().updateProfile(forRecipientId: signalServiceProfile.recipientId, + profileNameEncrypted: signalServiceProfile.profileNameEncrypted, + avatarUrl: signalServiceProfile.avatarUrl) } private func verifyIdentityUpToDateAsync(recipientId: String, latestIdentityKey: Data) { @@ -140,14 +139,12 @@ struct SignalServiceProfile { case invalidIdentityKey(description: String) case invalidProfileName(description: String) case invalidAvatarUrl(description: String) - case invalidAvatarDigest(description: String) } public let recipientId: String public let identityKey: Data public let profileNameEncrypted: Data? - public let avatarUrlData: Data? - public let avatarDigest: Data? + public let avatarUrl: String? init(recipientId: String, rawResponse: Any?) throws { self.recipientId = recipientId @@ -167,34 +164,18 @@ struct SignalServiceProfile { throw ValidationError.invalidIdentityKey(description: "\(TAG) malformed key \(identityKeyString) with decoded length: \(identityKeyWithType.count)") } - var profileNameEncrypted: Data? = nil if let profileNameString = responseDict["name"] as? String { guard let data = Data(base64Encoded: profileNameString) else { throw ValidationError.invalidProfileName(description: "\(TAG) unable to parse profile name: \(profileNameString)") } - profileNameEncrypted = data + self.profileNameEncrypted = data + } else { + self.profileNameEncrypted = nil } - var avatarUrlData: Data? = nil - if let avatarUrlString = responseDict["avatar"] as? String { - guard let data = Data(base64Encoded: avatarUrlString) else { - throw ValidationError.invalidAvatarUrl(description: "\(TAG) unable to parse avatar URL: \(avatarUrlString)") - } - avatarUrlData = data - } - - var avatarDigest: Data? = nil - if let avatarDigestString = responseDict["avatarDigest"] as? String { - guard let data = Data(base64Encoded: avatarDigestString) else { - throw ValidationError.invalidAvatarDigest(description: "\(TAG) unable to parse avatar digest: \(avatarDigestString)") - } - avatarDigest = data - } + self.avatarUrl = responseDict["avatar"] as? String // `removeKeyType` is an objc category method only on NSData, so temporarily cast. self.identityKey = (identityKeyWithType as NSData).removeKeyType() as Data - self.profileNameEncrypted = profileNameEncrypted - self.avatarUrlData = avatarUrlData - self.avatarDigest = avatarDigest } } diff --git a/Signal/src/ViewControllers/SignalsViewController.m b/Signal/src/ViewControllers/SignalsViewController.m index 9d6d35402..5b271e001 100644 --- a/Signal/src/ViewControllers/SignalsViewController.m +++ b/Signal/src/ViewControllers/SignalsViewController.m @@ -481,19 +481,6 @@ typedef NS_ENUM(NSInteger, CellState) { kArchiveState, kInboxState }; - (void)viewDidAppear:(BOOL)animated { [super viewDidAppear:animated]; - dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ - [OWSProfileManager.sharedManager updateLocalProfileName:@"My profile name." - avatarImage:[UIImage imageNamed:@"introductory_splash_callkit"] - success:^{ - DDLogInfo(@"%@ fake profile upload.", self.tag); - } - failure:^{ - [OWSAlerts showAlertWithTitle:NSLocalizedString(@"ALERT_ERROR_TITLE", @"") - message:NSLocalizedString(@"PROFILE_VIEW_ERROR_UPDATE_FAILED", - @"Error message shown when a profile update fails.")]; - }]; - }); - if (self.newlyRegisteredUser) { [self.editingDbConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction * _Nonnull transaction) { [self.experienceUpgradeFinder markAllAsSeenWithTransaction:transaction]; diff --git a/SignalServiceKit/Makefile b/SignalServiceKit/Makefile index 79bc654c2..18d8829bc 100644 --- a/SignalServiceKit/Makefile +++ b/SignalServiceKit/Makefile @@ -9,9 +9,7 @@ XCODE_BUILD = xcrun xcodebuild -workspace $(SCHEME).xcworkspace -scheme $(SCHEME .PHONY: build test retest clean default: test -ci: test - -test: dependencies test +ci: dependencies test dependencies: cd $(WORKING_DIR) && \ diff --git a/SignalServiceKit/src/Messages/TSMessagesManager.m b/SignalServiceKit/src/Messages/TSMessagesManager.m index d3f7aff7c..272940895 100644 --- a/SignalServiceKit/src/Messages/TSMessagesManager.m +++ b/SignalServiceKit/src/Messages/TSMessagesManager.m @@ -516,7 +516,7 @@ NS_ASSUME_NONNULL_BEGIN NSData *profileKey = [dataMessage profileKey]; NSString *recipientId = incomingEnvelope.source; id profileManager = [TextSecureKitEnv sharedEnv].profileManager; - [profileManager setProfileKey:profileKey forRecipientId:recipientId]; + [profileManager setProfileKeyData:profileKey forRecipientId:recipientId]; } if (dataMessage.hasGroup) { @@ -590,7 +590,7 @@ NS_ASSUME_NONNULL_BEGIN NSData *profileKey = [callMessage profileKey]; NSString *recipientId = incomingEnvelope.source; id profileManager = [TextSecureKitEnv sharedEnv].profileManager; - [profileManager setProfileKey:profileKey forRecipientId:recipientId]; + [profileManager setProfileKeyData:profileKey forRecipientId:recipientId]; } if (callMessage.hasOffer) { diff --git a/SignalServiceKit/src/Network/API/OWSRequestBuilder.h b/SignalServiceKit/src/Network/API/OWSRequestBuilder.h new file mode 100644 index 000000000..10f155343 --- /dev/null +++ b/SignalServiceKit/src/Network/API/OWSRequestBuilder.h @@ -0,0 +1,15 @@ +// +// Copyright (c) 2017 Open Whisper Systems. All rights reserved. +// + +NS_ASSUME_NONNULL_BEGIN + +@class TSRequest; + +@interface OWSRequestBuilder : NSObject + ++ (TSRequest *)profileNameSetRequestWithEncryptedPaddedName:(nullable NSData *)encryptedPaddedName; + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalServiceKit/src/Network/API/OWSRequestBuilder.m b/SignalServiceKit/src/Network/API/OWSRequestBuilder.m new file mode 100644 index 000000000..9b943ce70 --- /dev/null +++ b/SignalServiceKit/src/Network/API/OWSRequestBuilder.m @@ -0,0 +1,43 @@ +// +// Copyright (c) 2017 Open Whisper Systems. All rights reserved. +// + +#import "OWSRequestBuilder.h" +#import "TSRequest.h" +#import "TSConstants.h" +#import "NSData+Base64.h" + +NS_ASSUME_NONNULL_BEGIN + +const NSUInteger kEncodedNameLength = 72; + +@implementation OWSRequestBuilder + ++ (TSRequest *)profileNameSetRequestWithEncryptedPaddedName:(nullable NSData *)encryptedPaddedName +{ + NSString *urlString; + + NSString *base64EncodedName = [encryptedPaddedName base64EncodedString]; + // name length must match exactly + if (base64EncodedName.length == kEncodedNameLength) { + // Remove any "/" in the base64 (all other base64 chars are URL safe. + // Apples built-in `stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URL*]]` doesn't offer a + // flavor for encoding "/". + NSString *urlEncodedName = [base64EncodedName stringByReplacingOccurrencesOfString:@"/" withString:@"%2F"]; + urlString = [NSString stringWithFormat:textSecureSetProfileNameAPIFormat, urlEncodedName]; + } else { + // if name length doesn't match exactly, assume blank name + OWSAssert(encryptedPaddedName == nil); + urlString = [NSString stringWithFormat:textSecureSetProfileNameAPIFormat, @""]; + } + + NSURL *url = [NSURL URLWithString:urlString]; + TSRequest *request = [[TSRequest alloc] initWithURL:url]; + request.HTTPMethod = @"PUT"; + + return request; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalServiceKit/src/Network/API/Requests/TSRequest.h b/SignalServiceKit/src/Network/API/Requests/TSRequest.h index 7f03d5ee5..3c7fd1383 100644 --- a/SignalServiceKit/src/Network/API/Requests/TSRequest.h +++ b/SignalServiceKit/src/Network/API/Requests/TSRequest.h @@ -1,9 +1,5 @@ // -// TSRequest.h -// TextSecureiOS -// -// Created by Frederic Jacobs on 9/27/13. -// Copyright (c) 2013 Open Whisper Systems. All rights reserved. +// Copyright (c) 2017 Open Whisper Systems. All rights reserved. // #import @@ -14,4 +10,9 @@ - (void)makeAuthenticatedRequest; +#pragma mark - Factory methods + +// move to builder class/header ++ (instancetype)setProfileNameRequestWithProfileName:(NSString *)encryptedName; + @end diff --git a/SignalServiceKit/src/Network/API/Requests/TSRequest.m b/SignalServiceKit/src/Network/API/Requests/TSRequest.m index f74d71a23..8922ad353 100644 --- a/SignalServiceKit/src/Network/API/Requests/TSRequest.m +++ b/SignalServiceKit/src/Network/API/Requests/TSRequest.m @@ -1,9 +1,5 @@ // -// TSRequest.m -// TextSecureiOS -// -// Created by Frederic Jacobs on 9/27/13. -// Copyright (c) 2013 Open Whisper Systems. All rights reserved. +// Copyright (c) 2017 Open Whisper Systems. All rights reserved. // #import "TSRequest.h" @@ -14,6 +10,7 @@ @implementation TSRequest - (id)initWithURL:(NSURL *)URL { + OWSAssert(URL); self = [super initWithURL:URL cachePolicy:NSURLRequestReloadIgnoringLocalAndRemoteCacheData timeoutInterval:textSecureHTTPTimeOut]; @@ -43,7 +40,4 @@ [self.parameters addEntriesFromDictionary:@{ @"Authorization" : [TSStorageManager serverAuthToken] }]; } -- (BOOL)usingExternalServer { - return NO; -} @end diff --git a/SignalServiceKit/src/Network/API/Requests/TSSetProfileRequest.h b/SignalServiceKit/src/Network/API/Requests/TSSetProfileRequest.h deleted file mode 100644 index 663ef1126..000000000 --- a/SignalServiceKit/src/Network/API/Requests/TSSetProfileRequest.h +++ /dev/null @@ -1,19 +0,0 @@ -// -// Copyright (c) 2017 Open Whisper Systems. All rights reserved. -// - -#import "TSRequest.h" - -NS_ASSUME_NONNULL_BEGIN - -@interface TSSetProfileRequest : TSRequest - -- (nullable instancetype)initWithProfileName:(NSData *_Nullable)profileNameEncrypted - avatarUrl:(NSString *_Nullable)avatarUrl - avatarDigest:(NSData *_Nullable)avatarDigest; - -- (instancetype)init NS_UNAVAILABLE; - -@end - -NS_ASSUME_NONNULL_END diff --git a/SignalServiceKit/src/Network/API/Requests/TSSetProfileRequest.m b/SignalServiceKit/src/Network/API/Requests/TSSetProfileRequest.m deleted file mode 100644 index 822d0232e..000000000 --- a/SignalServiceKit/src/Network/API/Requests/TSSetProfileRequest.m +++ /dev/null @@ -1,40 +0,0 @@ -// -// Copyright (c) 2017 Open Whisper Systems. All rights reserved. -// - -#import "TSSetProfileRequest.h" -#import "NSData+Base64.h" -#import "TSConstants.h" - -NS_ASSUME_NONNULL_BEGIN - -@implementation TSSetProfileRequest - -- (nullable instancetype)initWithProfileName:(NSData *_Nullable)profileNameEncrypted - avatarUrl:(NSString *_Nullable)avatarUrl - avatarDigest:(NSData *_Nullable)avatarDigest -{ - - self = [super initWithURL:[NSURL URLWithString:textSecureSetProfileAPI]]; - - self.HTTPMethod = @"PUT"; - - if (profileNameEncrypted.length > 0) { - self.parameters[@"name"] = [profileNameEncrypted base64EncodedString]; - } - if (avatarUrl.length > 0 && avatarDigest.length > 0) { - // TODO why is this base64 encoded? - self.parameters[@"avatar"] = [[avatarUrl dataUsingEncoding:NSUTF8StringEncoding] base64EncodedString]; - - self.parameters[@"avatarDigest"] = [avatarDigest base64EncodedString]; - } else { - OWSAssert(avatarUrl.length == 0); - OWSAssert(avatarDigest.length == 0); - } - - return self; -} - -@end - -NS_ASSUME_NONNULL_END diff --git a/SignalServiceKit/src/Protocols/ProfileManagerProtocol.h b/SignalServiceKit/src/Protocols/ProfileManagerProtocol.h index 897460c6b..32dc66e94 100644 --- a/SignalServiceKit/src/Protocols/ProfileManagerProtocol.h +++ b/SignalServiceKit/src/Protocols/ProfileManagerProtocol.h @@ -3,12 +3,13 @@ // @class TSThread; +@class OWSAES128Key; @protocol ProfileManagerProtocol -- (NSData *)localProfileKey; +- (OWSAES128Key *)localProfileKey; -- (void)setProfileKey:(NSData *)profileKey forRecipientId:(NSString *)recipientId; +- (void)setProfileKeyData:(NSData *)profileKeyData forRecipientId:(NSString *)recipientId; - (BOOL)isUserInProfileWhitelist:(NSString *)recipientId; diff --git a/SignalServiceKit/src/Protocols/ProtoBuf+OWS.m b/SignalServiceKit/src/Protocols/ProtoBuf+OWS.m index 0222a1131..601d6153f 100644 --- a/SignalServiceKit/src/Protocols/ProtoBuf+OWS.m +++ b/SignalServiceKit/src/Protocols/ProtoBuf+OWS.m @@ -7,6 +7,7 @@ #import "SignalRecipient.h" #import "TSThread.h" #import "TextSecureKitEnv.h" +#import "Cryptography.h" NS_ASSUME_NONNULL_BEGIN @@ -32,7 +33,7 @@ NS_ASSUME_NONNULL_BEGIN return NO; } -- (NSData *)localProfileKey +- (OWSAES128Key *)localProfileKey { id profileManager = [TextSecureKitEnv sharedEnv].profileManager; return profileManager.localProfileKey; @@ -49,7 +50,7 @@ NS_ASSUME_NONNULL_BEGIN OWSAssert(thread); if ([self shouldMessageHaveLocalProfileKey:thread recipientId:recipientId]) { - [self setProfileKey:self.localProfileKey]; + [self setProfileKey:self.localProfileKey.keyData]; if (recipientId.length > 0) { // Once we've shared our profile key with a user (perhaps due to being @@ -75,7 +76,7 @@ NS_ASSUME_NONNULL_BEGIN OWSAssert(recipientId.length > 0); if ([self shouldMessageHaveLocalProfileKey:thread recipientId:recipientId]) { - [self setProfileKey:self.localProfileKey]; + [self setProfileKey:self.localProfileKey.keyData]; // Once we've shared our profile key with a user (perhaps due to being // a member of a whitelisted group), make sure they're whitelisted. @@ -95,7 +96,7 @@ NS_ASSUME_NONNULL_BEGIN - (void)addLocalProfileKey { - [self setProfileKey:self.localProfileKey]; + [self setProfileKey:self.localProfileKey.keyData]; } @end diff --git a/SignalServiceKit/src/Security/CommonCryptorSPI.h b/SignalServiceKit/src/Security/CommonCryptorSPI.h new file mode 100644 index 000000000..9efbc6afd --- /dev/null +++ b/SignalServiceKit/src/Security/CommonCryptorSPI.h @@ -0,0 +1,397 @@ +/* + * Copyright (c) 2010 Apple Inc. All Rights Reserved. + * + * @APPLE_LICENSE_HEADER_START@ + * + * This file contains Original Code and/or Modifications of Original Code + * as defined in and that are subject to the Apple Public Source License + * Version 2.0 (the 'License'). You may not use this file except in + * compliance with the License. Please obtain a copy of the License at + * http://www.opensource.apple.com/apsl/ and read it before using this + * file. + * + * The Original Code and all software distributed under the License are + * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER + * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES, + * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT. + * Please see the License for the specific language governing rights and + * limitations under the License. + * + * @APPLE_LICENSE_HEADER_END@ + */ + +#ifndef _CC_CryptorSPI_H_ +#define _CC_CryptorSPI_H_ + +#include +#include +#include + +#include +#ifdef KERNEL +#include +#else +#include +#include +#endif /* KERNEL */ +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* + This is an SPI header. It includes some work in progress implementation notes that + will be removed when this is promoted to an API set. +*/ + +/* + Private Ciphers + */ + +/* Lion SPI name for no padding. Defining for compatibility. Is now + ccNoPadding in CommonCryptor.h + */ + +enum { + ccDefaultPadding = 0, +}; + + +enum { + kCCAlgorithmAES128NoHardware = 20, + kCCAlgorithmAES128WithHardware = 21 +}; + +/* + Private Modes + */ +enum { + kCCModeGCM = 11, + kCCModeCCM = 12, +}; + +/* + Private Paddings + */ +enum { + ccCBCCTS1 = 10, + ccCBCCTS2 = 11, + ccCBCCTS3 = 12, +}; + +/* + Private Cryptor direction (op) + */ +enum { + kCCBoth = 3, +}; + + + + +/* + Supports a mode call of + int mode_setup(int cipher, const unsigned char *IV, const unsigned char *key, int keylen, + const unsigned char *tweak, int tweaklen, int num_rounds, int options, mode_context *ctx); +*/ + +/* User supplied space for the CryptorRef */ + +CCCryptorStatus CCCryptorCreateFromDataWithMode( + CCOperation op, /* kCCEncrypt, kCCEncrypt, kCCBoth (default for BlockMode) */ + CCMode mode, + CCAlgorithm alg, + CCPadding padding, + const void *iv, /* optional initialization vector */ + const void *key, /* raw key material */ + size_t keyLength, + const void *tweak, /* raw tweak material */ + size_t tweakLength, + int numRounds, + CCModeOptions options, + const void *data, /* caller-supplied memory */ + size_t dataLength, /* length of data in bytes */ + CCCryptorRef *cryptorRef, /* RETURNED */ + size_t *dataUsed) /* optional, RETURNED */ +__OSX_AVAILABLE_STARTING(__MAC_10_7, __IPHONE_5_0); + + +/* + Assuming we can use existing CCCryptorCreateFromData for all modes serviced by these: + int mode_encrypt(const unsigned char *pt, unsigned char *ct, unsigned long len, mode_context *ctx); + int mode_decrypt(const unsigned char *ct, unsigned char *pt, unsigned long len, mode_context *ctx); +*/ + +/* + Block mode encrypt and decrypt interfaces for IV tweaked blocks (XTS and CBC) + + int mode_encrypt_tweaked(const unsigned char *pt, unsigned long len, unsigned char *ct, const unsigned char *tweak, mode_context *ctx); + int mode_decrypt_tweaked(const unsigned char *ct, unsigned long len, unsigned char *pt, const unsigned char *tweak, mode_context *ctx); +*/ + +CCCryptorStatus CCCryptorEncryptDataBlock( + CCCryptorRef cryptorRef, + const void *iv, + const void *dataIn, + size_t dataInLength, + void *dataOut) +__OSX_AVAILABLE_STARTING(__MAC_10_7, __IPHONE_5_0); + + +CCCryptorStatus CCCryptorDecryptDataBlock( + CCCryptorRef cryptorRef, + const void *iv, + const void *dataIn, + size_t dataInLength, + void *dataOut) +__OSX_AVAILABLE_STARTING(__MAC_10_7, __IPHONE_5_0); + +/* + Assuming we can use the existing CCCryptorRelease() interface for + int mode_done(mode_context *ctx); +*/ + +/* + Not surfacing these other than with CCCryptorReset() + + int mode_setiv(const unsigned char *IV, unsigned long len, mode_context *ctx); + int mode_getiv(const unsigned char *IV, unsigned long *len, mode_context *ctx); +*/ + +/* + DES key utilities +*/ + +CCCryptorStatus CCDesIsWeakKey( + void *key, + size_t Length) +__OSX_AVAILABLE_STARTING(__MAC_10_7, __IPHONE_5_0); + +void CCDesSetOddParity( + void *key, + size_t Length) +__OSX_AVAILABLE_STARTING(__MAC_10_7, __IPHONE_5_0); + +uint32_t CCDesCBCCksum(void *input, void *output, + size_t length, void *key, size_t keylen, + void *ivec) +__OSX_AVAILABLE_STARTING(__MAC_10_7, __IPHONE_5_0); + + +/* + * returns a cipher blocksize length iv in the provided iv buffer. + */ + +CCCryptorStatus +CCCryptorGetIV(CCCryptorRef cryptorRef, void *iv) +__OSX_AVAILABLE_STARTING(__MAC_10_7, __IPHONE_5_0); + +/* + GCM Support Interfaces + + Use CCCryptorCreateWithMode() with the kCCModeGCM selector to initialize + a CryptoRef. Only kCCAlgorithmAES128 can be used with GCM and these + functions. IV Setting etc will be ignored from CCCryptorCreateWithMode(). + Use the CCCryptorGCMAddIV() routine below for IV setup. +*/ + +/* + This adds the initial vector octets from iv of length ivLen to the GCM + CCCryptorRef. You can call this function as many times as required to + process the entire IV. +*/ + +CCCryptorStatus +CCCryptorGCMAddIV(CCCryptorRef cryptorRef, + const void *iv, + size_t ivLen) +__OSX_AVAILABLE_STARTING(__MAC_10_8, __IPHONE_5_0); + +/* + Additional Authentication Data + After the entire IV has been processed, the additional authentication + data can be processed. Unlike the IV, a packet/session does not require + additional authentication data (AAD) for security. The AAD is meant to + be used as side–channel data you want to be authenticated with the packet. + Note: once you begin adding AAD to the GCM CCCryptorRef you cannot return + to adding IV data until the state has been reset. +*/ + +CCCryptorStatus +CCCryptorGCMAddAAD(CCCryptorRef cryptorRef, + const void *aData, + size_t aDataLen) +__OSX_AVAILABLE_STARTING(__MAC_10_8, __IPHONE_6_0); + +// Maintain the old symbol with incorrect camel-case for now. +CCCryptorStatus +CCCryptorGCMaddAAD(CCCryptorRef cryptorRef, + const void *aData, + size_t aDataLen) +__OSX_AVAILABLE_STARTING(__MAC_10_8, __IPHONE_6_0); + +// This is for old iOS5 clients +CCCryptorStatus +CCCryptorGCMAddADD(CCCryptorRef cryptorRef, + const void *aData, + size_t aDataLen) +__OSX_AVAILABLE_STARTING(__MAC_10_8, __IPHONE_5_0); + + +CCCryptorStatus CCCryptorGCMEncrypt( + CCCryptorRef cryptorRef, + const void *dataIn, + size_t dataInLength, + void *dataOut) +__OSX_AVAILABLE_STARTING(__MAC_10_8, __IPHONE_5_0); + + +CCCryptorStatus CCCryptorGCMDecrypt( + CCCryptorRef cryptorRef, + const void *dataIn, + size_t dataInLength, + void *dataOut) +__OSX_AVAILABLE_STARTING(__MAC_10_8, __IPHONE_5_0); + +/* + This terminates the GCM state gcm and stores the tag in tag of length + taglen octets. +*/ + +CCCryptorStatus CCCryptorGCMFinal( + CCCryptorRef cryptorRef, + const void *tag, + size_t *tagLength) +__OSX_AVAILABLE_STARTING(__MAC_10_8, __IPHONE_5_0); + +/* + This will reset the GCM CCCryptorRef to the state that CCCryptorCreateWithMode() + left it. The user would then call CCCryptorGCMAddIV(), CCCryptorGCMaddAAD(), etc. +*/ + +CCCryptorStatus CCCryptorGCMReset( + CCCryptorRef cryptorRef) +__OSX_AVAILABLE_STARTING(__MAC_10_8, __IPHONE_5_0); + +/* + This will initialize the GCM state with the given key, IV and AAD value + then proceed to encrypt or decrypt the message text and store the final + message tag. The definition of the variables is the same as it is for all + the manual functions. If you are processing many packets under the same + key you shouldn’t use this function as it invokes the pre–computation + with each call. +*/ + +CCCryptorStatus CCCryptorGCM( + CCOperation op, /* kCCEncrypt, kCCDecrypt */ + CCAlgorithm alg, + const void *key, /* raw key material */ + size_t keyLength, + const void *iv, + size_t ivLen, + const void *aData, + size_t aDataLen, + const void *dataIn, + size_t dataInLength, + void *dataOut, + const void *tag, + size_t *tagLength) +__OSX_AVAILABLE_STARTING(__MAC_10_8, __IPHONE_5_0); + + +void CC_RC4_set_key(void *ctx, int len, const unsigned char *data) +__OSX_AVAILABLE_STARTING(__MAC_10_4, __IPHONE_5_0); + +void CC_RC4(void *ctx, unsigned long len, const unsigned char *indata, + unsigned char *outdata) +__OSX_AVAILABLE_STARTING(__MAC_10_4, __IPHONE_5_0); + +/* +GCM interface can then be easily bolt on the rest of standard CCCryptor interface; typically following sequence can be used: + +CCCryptorCreateWithMode(mode = kCCModeGCM) +0..Nx: CCCryptorAddParameter(kCCParameterIV, iv) +0..Nx: CCCryptorAddParameter(kCCParameterAuthData, data) +0..Nx: CCCryptorUpdate(inData, outData) +0..1: CCCryptorFinal(outData) +0..1: CCCryptorGetParameter(kCCParameterAuthTag, tag) +CCCryptorRelease() + +*/ + +enum { + /* + Initialization vector - cryptor input parameter, typically + needs to have the same length as block size, but in some cases + (GCM) it can be arbitrarily long and even might be called + multiple times. + */ + kCCParameterIV, + + /* + Authentication data - cryptor input parameter, input for + authenticating encryption modes like GCM. If supported, can + be called multiple times before encryption starts. + */ + kCCParameterAuthData, + + /* + Mac Size - cryptor input parameter, input for + authenticating encryption modes like CCM. Specifies the size of + the AuthTag the algorithm is expected to produce. + */ + kCCMacSize, + + /* + Data Size - cryptor input parameter, input for + authenticating encryption modes like CCM. Specifies the amount of + data the algorithm is expected to process. + */ + kCCDataSize, + + /* + Authentication tag - cryptor output parameter, output from + authenticating encryption modes like GCM. If supported, + should be retrieved after the encryption finishes. + */ + kCCParameterAuthTag, +}; +typedef uint32_t CCParameter; + +/* + Sets or adds some other cryptor input parameter. According to the + cryptor type and state, parameter can be either accepted or + refused with kCCUnimplemented (when given parameter is not + supported for this type of cryptor at all) or kCCParamError (bad + data length or format). +*/ + +CCCryptorStatus CCCryptorAddParameter( + CCCryptorRef cryptorRef, + CCParameter parameter, + const void *data, + size_t dataSize); + + +/* + Gets value of output cryptor parameter. According to the cryptor + type state, the request can be either accepted or refused with + kCCUnimplemented (when given parameteris not supported for this + type of cryptor) or kCCBufferTooSmall (in this case, *dataSize + argument is set to the requested size of data). +*/ + +CCCryptorStatus CCCryptorGetParameter( + CCCryptorRef cryptorRef, + CCParameter parameter, + void *data, + size_t *dataSize); + + +#ifdef __cplusplus +} +#endif + +#endif /* _CC_CryptorSPI_H_ */ diff --git a/SignalServiceKit/src/TSConstants.h b/SignalServiceKit/src/TSConstants.h index 33c3935d6..45c20c8fc 100644 --- a/SignalServiceKit/src/TSConstants.h +++ b/SignalServiceKit/src/TSConstants.h @@ -43,7 +43,7 @@ typedef enum { kSMSVerification, kPhoneNumberVerification } VerificationTranspor #define textSecureDeviceProvisioningAPIFormat @"v1/provisioning/%@" #define textSecureDevicesAPIFormat @"v1/devices/%@" #define textSecureProfileAPIFormat @"v1/profile/%@" -#define textSecureSetProfileAPI @"v1/profile" +#define textSecureSetProfileNameAPIFormat @"v1/profile/name/%@" #define textSecureProfileAvatarFormAPI @"v1/profile/form/avatar" #pragma mark Push RegistrationSpecific Constants diff --git a/SignalServiceKit/src/Util/Cryptography.h b/SignalServiceKit/src/Util/Cryptography.h index 7f9abd021..efeb5b38a 100755 --- a/SignalServiceKit/src/Util/Cryptography.h +++ b/SignalServiceKit/src/Util/Cryptography.h @@ -4,6 +4,28 @@ NS_ASSUME_NONNULL_BEGIN +extern const NSUInteger kAES128_KeyByteLength; + +/// Key appropriate for use in AES128 crypto +@interface OWSAES128Key: NSObject + +/// Generates new secure random key +- (instancetype)init; ++ (instancetype)generateRandomKey; + +/** + * @param data representing the raw key bytes + * + * @returns a new instance if key is of appropriate length for AES128 crypto + * else returns nil. + */ ++ (nullable instancetype)keyWithData:(NSData *)data; + +/// The raw key material +@property (nonatomic, readonly) NSData *keyData; + +@end + @interface Cryptography : NSObject typedef NS_ENUM(NSInteger, TSMACType) { @@ -38,6 +60,9 @@ typedef NS_ENUM(NSInteger, TSMACType) { outKey:(NSData *_Nonnull *_Nullable)outKey outDigest:(NSData *_Nonnull *_Nullable)outDigest; ++ (nullable NSData *)encryptAESGCMWithData:(NSData *)plainTextData key:(OWSAES128Key *)key; ++ (nullable NSData *)decryptAESGCMWithData:(NSData *)encryptedData key:(OWSAES128Key *)key; + @end NS_ASSUME_NONNULL_END diff --git a/SignalServiceKit/src/Util/Cryptography.m b/SignalServiceKit/src/Util/Cryptography.m index 650c604c5..64f197067 100755 --- a/SignalServiceKit/src/Util/Cryptography.m +++ b/SignalServiceKit/src/Util/Cryptography.m @@ -5,6 +5,11 @@ #import #import +// CommonCryptorSPI.h is a local copy of a private APple header fetched: Fri Aug 11 18:33:25 EDT 2017 +// from https://opensource.apple.com/source/CommonCrypto/CommonCrypto-60074/include/CommonCryptorSPI.h +// We use it to provide the not-yet-public AES128-GCM cryptor +#import "CommonCryptorSPI.h" + #import "Cryptography.h" #import "NSData+Base64.h" #import "NSData+OWSConstantTimeCompare.h" @@ -16,6 +21,80 @@ NS_ASSUME_NONNULL_BEGIN +// length of initialization nonce +static const NSUInteger kAESGCM128_IVLength = 12; + +// length of authentication tag for AES128-GCM +static const NSUInteger kAESGCM128_TagLength = 16; + +const NSUInteger kAES128_KeyByteLength = 16; + +@implementation OWSAES128Key + ++ (nullable instancetype)keyWithData:(NSData *)data +{ + if (data.length != kAES128_KeyByteLength) { + OWSFail(@"Invalid key length for AES128: %lu", (unsigned long)data.length); + return nil; + } + + return [[self alloc] initWithData:data]; +} + ++ (instancetype)generateRandomKey +{ + return [self new]; +} + +- (instancetype)init +{ + return [self initWithData:[Cryptography generateRandomBytes:kAES128_KeyByteLength]]; +} + +- (instancetype)initWithData:(NSData *)data +{ + self = [super init]; + if (!self) { + return self; + } + + _keyData = data; + + return self; +} + +#pragma mark - SecureCoding + ++ (BOOL)supportsSecureCoding +{ + return YES; +} + +- (nullable instancetype)initWithCoder:(NSCoder *)aDecoder +{ + self = [super init]; + if (!self) { + return self; + } + + NSData *keyData = [aDecoder decodeObjectOfClass:[NSData class] forKey:@"keyData"]; + if (keyData.length != kAES128_KeyByteLength) { + OWSFail(@"Invalid key length for AES128: %lu", (unsigned long)keyData.length); + return nil; + } + + _keyData = keyData; + + return self; +} + +- (void)encodeWithCoder:(NSCoder *)aCoder +{ + [aCoder encodeObject:_keyData forKey:@"keyData"]; +} + +@end + @implementation Cryptography #pragma mark random bytes methods @@ -307,6 +386,114 @@ NS_ASSUME_NONNULL_BEGIN return [encryptedAttachmentData copy]; } ++ (nullable NSData *)encryptAESGCMWithData:(NSData *)plainTextData key:(OWSAES128Key *)key +{ + NSData *initializationVector = [Cryptography generateRandomBytes:kAESGCM128_IVLength]; + uint8_t *cipherTextBytes = malloc(plainTextData.length); + if (cipherTextBytes == NULL) { + OWSFail(@"%@ Failed to allocate encryptedBytes", self.tag); + return nil; + } + + uint8_t *authTagBytes = malloc(kAESGCM128_TagLength); + if (authTagBytes == NULL) { + free(cipherTextBytes); + OWSFail(@"%@ Failed to allocate authTagBytes", self.tag); + return nil; + } + + // NOTE: Since `tagLength` is an input parameter, it seems weird that the signature for tagLength is a `size_t*` rather than just a `size_t`. + // + // I found a vague reference in the Safari repository implying that this may be a bug: + // source: https://www.mail-archive.com/webkit-changes@lists.webkit.org/msg114561.html + // + // Comment was: + // tagLength is actual an input + size_t tagLength = kAESGCM128_TagLength; + + CCCryptorStatus status = CCCryptorGCM(kCCEncrypt, // CCOperation op, /* kCCEncrypt, kCCDecrypt */ + kCCAlgorithmAES128, // CCAlgorithm alg, + key.keyData.bytes, // const void *key, /* raw key material */ + key.keyData.length, // size_t keyLength, + initializationVector.bytes, // const void *iv, + initializationVector.length, // size_t ivLen, + NULL, // const void *aData, + 0, // size_t aDataLen, + plainTextData.bytes, // const void *dataIn, + plainTextData.length, // size_t dataInLength, + cipherTextBytes, // void *dataOut, + authTagBytes, // const void *tag, + &tagLength //size_t *tagLength) + ); + + if (status != kCCSuccess) { + OWSFail(@"CCCryptorGCM encrypt failed with status: %d", status); + free(cipherTextBytes); + free(authTagBytes); + return nil; + } + + // build up return value: initializationVector || cipherText || authTag + NSMutableData *encryptedData = [initializationVector mutableCopy]; + [encryptedData appendBytes:cipherTextBytes length:plainTextData.length]; + [encryptedData appendBytes:authTagBytes length:tagLength]; + + free(cipherTextBytes); + free(authTagBytes); + + return [encryptedData copy]; +} + ++ (nullable NSData *)decryptAESGCMWithData:(NSData *)encryptedData key:(OWSAES128Key *)key +{ + OWSAssert(encryptedData.length > kAESGCM128_IVLength + kAESGCM128_TagLength); + NSUInteger cipherTextLength = encryptedData.length - kAESGCM128_IVLength - kAESGCM128_TagLength; + + // encryptedData layout: initializationVector || cipherText || authTag + NSData *initializationVector = [encryptedData subdataWithRange:NSMakeRange(0, kAESGCM128_IVLength)]; + NSData *cipherText = [encryptedData subdataWithRange:NSMakeRange(kAESGCM128_IVLength, cipherTextLength)]; + NSData *authTag = [encryptedData subdataWithRange:NSMakeRange(kAESGCM128_IVLength + cipherTextLength, + kAESGCM128_TagLength)]; + + void * plainTextBytes = malloc(cipherTextLength); + if (plainTextBytes == NULL) { + OWSFail(@"Failed to malloc plainTextBytes"); + return nil; + } + + // NOTE: Since `tagLength` is an input parameter, it seems weird that the signature for tagLength is a `size_t*` rather than just a `size_t`. + // + // I found a vague reference in the Safari repository implying that this may be a bug: + // source: https://www.mail-archive.com/webkit-changes@lists.webkit.org/msg114561.html + // + // Comment was: + // tagLength is actual an input + size_t tagLength = kAESGCM128_TagLength; + + CCCryptorStatus status = CCCryptorGCM(kCCDecrypt, // CCOperation op, /* kCCEncrypt, kCCDecrypt */ + kCCAlgorithmAES128, // CCAlgorithm alg, + key.keyData.bytes, // const void *key, /* raw key material */ + key.keyData.length, // size_t keyLength, + initializationVector.bytes, // const void *iv, + initializationVector.length, // size_t ivLen, + NULL, // const void *aData, + 0, // size_t aDataLen, + cipherText.bytes, // const void *dataIn, + cipherText.length, // size_t dataInLength, + plainTextBytes, // void *dataOut, + authTag.bytes, // const void *tag, + &tagLength //size_t *tagLength) + ); + + if (status != kCCSuccess) { + OWSFail(@"CCCryptorGCM decrypt failed with status: %d", status); + free(plainTextBytes); + return nil; + } + + return [NSData dataWithBytesNoCopy:plainTextBytes length:cipherTextLength freeWhenDone:YES]; +} + #pragma mark - Logging + (NSString *)tag diff --git a/SignalServiceKit/tests/Util/CryptographyTests.m b/SignalServiceKit/tests/Util/CryptographyTests.m index 24b568cfa..612c6d27d 100644 --- a/SignalServiceKit/tests/Util/CryptographyTests.m +++ b/SignalServiceKit/tests/Util/CryptographyTests.m @@ -120,6 +120,25 @@ NS_ASSUME_NONNULL_BEGIN XCTAssertEqualObjects(expectedTruncatedDigest, truncatedDigest); } +- (void)testGCMRoundTrip +{ + NSData *plainTextData = [@"Super🔥secret🔥test🔥data🏁🏁" dataUsingEncoding:NSUTF8StringEncoding]; + // Sanity Check + XCTAssertEqual(39, plainTextData.length); + + OWSAES128Key *key = [OWSAES128Key new]; + NSData *encryptedData = [Cryptography encryptAESGCMWithData:plainTextData key:key]; + + const NSUInteger ivLength = 12; + const NSUInteger tagLength = 16; + + XCTAssertEqual(ivLength + plainTextData.length + tagLength, encryptedData.length); + + NSData *decryptedData = [Cryptography decryptAESGCMWithData:encryptedData key:key]; + XCTAssertEqual(39, decryptedData.length); + XCTAssertEqualObjects(plainTextData, decryptedData); + XCTAssertEqualObjects(@"Super🔥secret🔥test🔥data🏁🏁", [[NSString alloc] initWithData:decryptedData encoding:NSUTF8StringEncoding]); +} @end