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
pull/1/head
Michael Kirk 8 years ago
parent 283d36c55b
commit 7499b3aaf0

@ -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 <ProfileManagerProtocol>
@ -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

@ -5,13 +5,14 @@
#import "OWSProfileManager.h"
#import "Environment.h"
#import "Signal-Swift.h"
#import <SignalServiceKit/Cryptography.h>
#import <SignalServiceKit/NSData+hexString.h>
#import <SignalServiceKit/NSDate+OWS.h>
#import <SignalServiceKit/OWSMessageSender.h>
#import <SignalServiceKit/OWSRequestBuilder.h>
#import <SignalServiceKit/SecurityUtils.h>
#import <SignalServiceKit/TSGroupThread.h>
#import <SignalServiceKit/TSProfileAvatarUploadFormRequest.h>
#import <SignalServiceKit/TSSetProfileRequest.h>
#import <SignalServiceKit/TSStorageManager.h>
#import <SignalServiceKit/TSThread.h>
#import <SignalServiceKit/TSYapDatabaseObject.h>
@ -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<AFMultipartFormData> _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<NSString *> *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

@ -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
}
}

@ -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];

@ -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) && \

@ -516,7 +516,7 @@ NS_ASSUME_NONNULL_BEGIN
NSData *profileKey = [dataMessage profileKey];
NSString *recipientId = incomingEnvelope.source;
id<ProfileManagerProtocol> 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<ProfileManagerProtocol> profileManager = [TextSecureKitEnv sharedEnv].profileManager;
[profileManager setProfileKey:profileKey forRecipientId:recipientId];
[profileManager setProfileKeyData:profileKey forRecipientId:recipientId];
}
if (callMessage.hasOffer) {

@ -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

@ -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

@ -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 <Foundation/Foundation.h>
@ -14,4 +10,9 @@
- (void)makeAuthenticatedRequest;
#pragma mark - Factory methods
// move to builder class/header
+ (instancetype)setProfileNameRequestWithProfileName:(NSString *)encryptedName;
@end

@ -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

@ -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

@ -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

@ -3,12 +3,13 @@
//
@class TSThread;
@class OWSAES128Key;
@protocol ProfileManagerProtocol <NSObject>
- (NSData *)localProfileKey;
- (OWSAES128Key *)localProfileKey;
- (void)setProfileKey:(NSData *)profileKey forRecipientId:(NSString *)recipientId;
- (void)setProfileKeyData:(NSData *)profileKeyData forRecipientId:(NSString *)recipientId;
- (BOOL)isUserInProfileWhitelist:(NSString *)recipientId;

@ -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<ProfileManagerProtocol> 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

@ -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 <sys/types.h>
#include <sys/param.h>
#include <stdint.h>
#include <string.h>
#ifdef KERNEL
#include <machine/limits.h>
#else
#include <limits.h>
#include <stdlib.h>
#endif /* KERNEL */
#include <Availability.h>
#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 sidechannel 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 shouldnt use this function as it invokes the precomputation
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_ */

@ -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

@ -4,6 +4,28 @@
NS_ASSUME_NONNULL_BEGIN
extern const NSUInteger kAES128_KeyByteLength;
/// Key appropriate for use in AES128 crypto
@interface OWSAES128Key: NSObject <NSSecureCoding>
/// 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

@ -5,6 +5,11 @@
#import <CommonCrypto/CommonCryptor.h>
#import <CommonCrypto/CommonHMAC.h>
// 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 <rdar://problem/30660074>
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 <rdar://problem/30660074>
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

@ -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

Loading…
Cancel
Save