-
Notifications
You must be signed in to change notification settings - Fork 332
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
Changes from 5 commits
9101eb3
b013ae2
63f1fec
7869fed
91ac918
ca65b60
a3baacb
90da8a7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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 |
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 |
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> | ||
|
||
- (NSData * _Nullable)encrypt:(NSData * _Nonnull)data; | ||
- (NSData * _Nullable)decrypt:(NSData * _Nonnull)data; | ||
|
||
@end |
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 |
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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep.
There was a problem hiding this comment.
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?