Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support client side encryption via SEGCrypto protocol #592

Merged
merged 8 commits into from
Aug 30, 2016
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Analytics.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,7 @@ Pod::Spec.new do |s|
s.ios.deployment_target = '7.0'
s.tvos.deployment_target = '9.0'

s.framework = 'Security'

s.source_files = 'Analytics/Classes/**/*'
end
23 changes: 23 additions & 0 deletions Analytics/Classes/Internal/SEGAES256Crypto.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
//
// SEGAES256Crypto.h
// Analytics
//
// Copyright © 2016 Segment. All rights reserved.
//

#import <Foundation/Foundation.h>
#import "SEGCrypto.h"

@interface SEGAES256Crypto : NSObject <SEGCrypto>

@property (nonatomic, readonly, nonnull) NSString *password;
@property (nonatomic, readonly, nonnull) NSData *salt;
@property (nonatomic, readonly, nonnull) NSData *iv;

- (instancetype _Nonnull)initWithPassword:(NSString * _Nonnull)password salt:(NSData * _Nonnull)salt iv:(NSData * _Nonnull)iv;
// Convenient shorthand. Will randomly generate salt and iv.
- (instancetype _Nonnull)initWithPassword:(NSString * _Nonnull)password;

+ (NSData * _Nonnull)randomDataOfLength:(size_t)length;

@end
140 changes: 140 additions & 0 deletions Analytics/Classes/Internal/SEGAES256Crypto.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
//
// SEGAES256Crypto.m
// Analytics
//
// Copyright © 2016 Segment. All rights reserved.
//


#import <CommonCrypto/CommonCryptor.h>
#import <CommonCrypto/CommonKeyDerivation.h>
#import "SEGAES256Crypto.h"
#import "SEGUtils.h"

// Implementation courtesy of http://robnapier.net/aes-commoncrypto

static NSString * const kRNCryptManagerErrorDomain = @"com.segment.crypto";

static const CCAlgorithm kAlgorithm = kCCAlgorithmAES;
static const NSUInteger kAlgorithmKeySize = kCCKeySizeAES256;
static const NSUInteger kAlgorithmBlockSize = kCCBlockSizeAES128;
static const NSUInteger kAlgorithmIVSize = kCCBlockSizeAES128;
static const NSUInteger kPBKDFSaltSize = 8;
static const NSUInteger kPBKDFRounds = 10000; // ~80ms on an iPhone 4

@implementation SEGAES256Crypto

- (instancetype)initWithPassword:(NSString *)password salt:(NSData *)salt iv:(NSData * _Nonnull)iv {
if (self = [super init]) {
_password = password;
_salt = salt;
_iv = iv;
}
return self;
}

- (instancetype)initWithPassword:(NSString *)password {
NSData *iv = [SEGAES256Crypto randomDataOfLength:kAlgorithmIVSize];
NSData *salt = [SEGAES256Crypto randomDataOfLength:kPBKDFSaltSize];
return [self initWithPassword:password salt:salt iv:iv];
}

- (NSData *)aesKey {
return [[self class] AESKeyForPassword:self.password salt:self.salt];
}

- (NSData *)encrypt:(NSData *)data {
size_t outLength;
NSMutableData *cipherData = [NSMutableData dataWithLength:data.length + kAlgorithmBlockSize];

CCCryptorStatus
result = CCCrypt(kCCEncrypt, // operation
kAlgorithm, // Algorithm
kCCOptionPKCS7Padding, // options
self.aesKey.bytes, // key
self.aesKey.length, // keylength
self.iv.bytes,// iv
data.bytes, // dataIn
data.length, // dataInLength,
cipherData.mutableBytes, // dataOut
cipherData.length, // dataOutAvailable
&outLength); // dataOutMoved

if (result == kCCSuccess) {
cipherData.length = outLength;
} else {
NSError *error = [NSError errorWithDomain:kRNCryptManagerErrorDomain
code:result
userInfo:nil];
SEGLog(@"Unable to encrypt data", error);
return nil;
}
return cipherData;
}

- (NSData *)decrypt:(NSData *)data {
size_t outLength;
NSMutableData *decryptedData = [NSMutableData dataWithLength:data.length + kAlgorithmBlockSize];

CCCryptorStatus
result = CCCrypt(kCCDecrypt, // operation
kAlgorithm, // Algorithm
kCCOptionPKCS7Padding, // options
self.aesKey.bytes, // key
self.aesKey.length, // keylength
self.iv.bytes,// iv
data.bytes, // dataIn
data.length, // dataInLength,
decryptedData.mutableBytes, // dataOut
decryptedData.length, // dataOutAvailable
&outLength); // dataOutMoved

if (result == kCCSuccess) {
decryptedData.length = outLength;
} else {
NSError *error = [NSError errorWithDomain:kRNCryptManagerErrorDomain
code:result
userInfo:nil];
SEGLog(@"Unable to encrypt data", error);
return nil;
}
return decryptedData;
}

+ (NSData *)randomDataOfLength:(size_t)length {
NSMutableData *data = [NSMutableData dataWithLength:length];

int result = SecRandomCopyBytes(kSecRandomDefault,
length,
data.mutableBytes);
if (result != kCCSuccess) {
SEGLog(@"Unable to generate random bytes: %d", result);
}

return data;
}

// Replace this with a 10,000 hash calls if you don't have CCKeyDerivationPBKDF
+ (NSData *)AESKeyForPassword:(NSString *)password
salt:(NSData *)salt {
NSMutableData *derivedKey = [NSMutableData dataWithLength:kAlgorithmKeySize];

int result = CCKeyDerivationPBKDF(kCCPBKDF2, // algorithm
password.UTF8String, // password
[password lengthOfBytesUsingEncoding:NSUTF8StringEncoding], // passwordLength
salt.bytes, // salt
salt.length, // saltLen
kCCPRFHmacAlgSHA1, // PRF
kPBKDFRounds, // rounds
derivedKey.mutableBytes, // derivedKey
derivedKey.length); // derivedKeyLen

// Do not log password here
if (result != kCCSuccess) {
SEGLog(@"Unable to create AES key for password: %d", result);
}

return derivedKey;
}

@end
15 changes: 15 additions & 0 deletions Analytics/Classes/Internal/SEGCrypto.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
//
// SEGCrypto.h
// Analytics
//
// Copyright © 2016 Segment. All rights reserved.
//

#import <Foundation/Foundation.h>

@protocol SEGCrypto <NSObject>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would be public so users can supply their own implementation, yeah?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool so let's move it out of the Internal folder?


- (NSData * _Nullable)encrypt:(NSData * _Nonnull)data;
- (NSData * _Nullable)decrypt:(NSData * _Nonnull)data;

@end
22 changes: 22 additions & 0 deletions Analytics/Classes/Internal/SEGFileStorage.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
//
// SEGFileStorage.h
// Analytics
//
// Copyright © 2016 Segment. All rights reserved.
//

#import <Foundation/Foundation.h>
#import "SEGStorage.h"

@interface SEGFileStorage : NSObject <SEGStorage>

@property (nonatomic, strong, nullable) id<SEGCrypto> crypto;

- (instancetype _Nonnull)init;
- (instancetype _Nonnull)initWithFolder:(NSURL * _Nonnull)folderURL crypto:(id<SEGCrypto> _Nullable)crypto;

- (NSURL * _Nonnull)urlForKey:(NSString * _Nonnull)key;

+ (NSURL * _Nullable)applicationSupportDirectoryURL;

@end
164 changes: 164 additions & 0 deletions Analytics/Classes/Internal/SEGFileStorage.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
//
// SEGFileStorage.m
// Analytics
//
// Copyright © 2016 Segment. All rights reserved.
//

#import "SEGUtils.h"
#import "SEGFileStorage.h"

@interface SEGFileStorage ()

@property (nonatomic, strong, nonnull) NSURL *folderURL;

@end

@implementation SEGFileStorage

- (instancetype)init {
return [self initWithFolder:[SEGFileStorage applicationSupportDirectoryURL] crypto:nil];
}

- (instancetype)initWithFolder:(NSURL *)folderURL crypto:(id<SEGCrypto>)crypto {
if (self = [super init]) {
_folderURL = folderURL;
_crypto = crypto;
[self createDirectoryAtURLIfNeeded:folderURL];
return self;
}
return nil;
}

- (void)removeKey:(NSString *)key {
NSURL *url = [self urlForKey:key];
NSError *error = nil;
if (![[NSFileManager defaultManager] removeItemAtURL:url error:&error]) {
SEGLog(@"Unable to remove key %@ - error removing file at path %@", key, url);
}
}

- (void)resetAll {
NSError *error = nil;
if (![[NSFileManager defaultManager] removeItemAtURL:self.folderURL error:&error]) {
SEGLog(@"ERROR: Unable to reset file storage. Path cannot be removed - %@", self.folderURL.path);
}
[self createDirectoryAtURLIfNeeded:self.folderURL];
}

- (void)setData:(NSData *)data forKey:(NSString *)key {
NSURL *url = [self urlForKey:key];
if (self.crypto) {
NSData *encryptedData = [self.crypto encrypt:data];
[encryptedData writeToURL:url atomically:YES];
} else {
[data writeToURL:url atomically:YES];
}

NSError *error = nil;
if (![url setResourceValue:@YES
forKey:NSURLIsExcludedFromBackupKey
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if this is the best place for this - what if we do want some of the keys to be backed up and shared across devices? I've considered doing so for anonymousId and userId pending some testing on multiple devices.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we do that responsibility should still live inside the storage implementation, but we can just provide an API on top like
setBackup:(BOOL)backup forKey:(NSString *)key

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pretty simple to add if / when we do need it.

error:&error]) {
SEGLog(@"Error excluding %@ from backup %@", [url lastPathComponent], error);
}
}

- (NSData *)dataForKey:(NSString *)key {
NSURL *url = [self urlForKey:key];
NSData *data = [NSData dataWithContentsOfURL:url];
if (!data) {
SEGLog(@"WARNING: No data file for key %@", key);
return nil;
}
if (self.crypto) {
return [self.crypto decrypt:data];
}
return data;
}

- (NSDictionary *)dictionaryForKey:(NSString *)key {
return [self plistForKey:key];
}

- (void)setDictionary:(NSDictionary *)dictionary forKey:(NSString *)key {
[self setPlist:dictionary forKey:key];
}

- (NSArray *)arrayForKey:(NSString *)key {
return [self plistForKey:key];
}

- (void)setArray:(NSArray *)array forKey:(NSString *)key {
[self setPlist:array forKey:key];
}

- (NSString *)stringForKey:(NSString *)key {
return [self plistForKey:key];
}

- (void)setString:(NSString *)string forKey:(NSString *)key {
[self setPlist:string forKey:key];
}

+ (NSURL *)applicationSupportDirectoryURL {
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, YES);
NSString *supportPath = [paths firstObject];
return [NSURL fileURLWithPath:supportPath];
}

- (NSURL *)urlForKey:(NSString *)key {
return [self.folderURL URLByAppendingPathComponent:key];
}

#pragma mark - Helpers

- (id _Nullable)plistForKey:(NSString *)key {
NSData *data = [self dataForKey:key];
return data ? [self plistFromData:data] : nil;
}

- (void)setPlist:(id _Nonnull)plist forKey:(NSString *)key {
NSData *data = [self dataFromPlist:plist];
if (data) {
[self setData:data forKey:key];
}
}

- (NSData * _Nullable)dataFromPlist:(nonnull id)plist {
NSError *error = nil;
NSData *data = [NSPropertyListSerialization dataWithPropertyList:plist
format:NSPropertyListXMLFormat_v1_0
options:0
error:&error];
if (error) {
SEGLog(@"Unable to serialize data from plist object", error, plist);
}
return data;
}

- (id _Nullable)plistFromData:(NSData * _Nonnull)data {
NSError *error = nil;
id plist = [NSPropertyListSerialization propertyListWithData:data
options:0
format:nil
error:&error];
if (error) {
SEGLog(@"Unable to parse plist from data %@", error);
}
return plist;
}

- (void)createDirectoryAtURLIfNeeded:(NSURL *)url {
if (![[NSFileManager defaultManager] fileExistsAtPath:url.path
isDirectory:NULL]) {
NSError *error = nil;
if (![[NSFileManager defaultManager] createDirectoryAtPath:url.path
withIntermediateDirectories:YES
attributes:nil
error:&error]) {
SEGLog(@"error: %@", error.localizedDescription);
}
}
}

@end
Loading