ably-cocoa/Source/ARTLocalDeviceStorage.m

201 lines
6.9 KiB
Objective-C

#import "ARTLocalDeviceStorage.h"
#import "ARTLog.h"
#import "ARTLocalDevice+Private.h"
@implementation ARTLocalDeviceStorage {
ARTLog *_logger;
}
- (instancetype)initWithLogger:(ARTLog *)logger {
if (self = [super init]) {
_logger = logger;
}
return self;
}
+ (instancetype)newWithLogger:(ARTLog *)logger {
return [[self alloc] initWithLogger:logger];
}
- (nullable id)objectForKey:(NSString *)key {
return [NSUserDefaults.standardUserDefaults objectForKey:key];
}
- (void)setObject:(nullable id)value forKey:(NSString *)key {
[NSUserDefaults.standardUserDefaults setObject:value forKey:key];
}
- (NSString *)secretForDevice:(ARTDeviceId *)deviceId {
NSError *error = nil;
NSString *value = [self keychainGetPasswordForService:ARTDeviceSecretKey account:(NSString *)deviceId error:&error];
if ([error code] == errSecItemNotFound) {
[_logger debug:__FILE__ line:__LINE__ message:@"Device Secret not found"];
}
else if (error) {
[_logger error:@"Device Secret couldn't be read (%@)", [error localizedDescription]];
}
return value;
}
- (void)setSecret:(NSString *)value forDevice:(ARTDeviceId *)deviceId {
NSError *error = nil;
if (value == nil) {
[self keychainDeletePasswordForService:ARTDeviceSecretKey account:(NSString *)deviceId error:&error];
if ([error code] == errSecItemNotFound) {
[_logger warn:@"Device Secret can't be deleted because it doesn't exist"];
}
else if (error) {
[_logger error:@"Device Secret couldn't be updated (%@)", [error localizedDescription]];
}
}
else {
[self keychainSetPassword:value forService:ARTDeviceSecretKey account:(NSString *)deviceId error:&error];
if (error) {
[_logger error:@"Device Secret couldn't be updated (%@)", [error localizedDescription]];
}
}
}
#pragma mark - Keychain
- (nonnull NSMutableDictionary *)newKeychainQueryForService:(nonnull NSString *)serviceName account:(nonnull NSString *)account {
NSMutableDictionary *dictionary = [NSMutableDictionary dictionaryWithCapacity:3];
[dictionary setObject:(__bridge id)kSecClassGenericPassword forKey:(__bridge id)kSecClass];
[dictionary setObject:serviceName forKey:(__bridge id)kSecAttrService];
[dictionary setObject:account forKey:(__bridge id)kSecAttrAccount];
#if TARGET_OS_IPHONE
[dictionary setObject:(__bridge id)kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly forKey:(__bridge id)kSecAttrAccessible];
#endif
return dictionary;
}
- (nonnull NSError *)keychainErrorWithCode:(OSStatus)status {
NSString *message = nil;
#if TARGET_OS_IPHONE
switch (status) {
case errSecUnimplemented: {
message = @"errSecUnimplemented";
break;
}
case errSecParam: {
message = @"errSecParam";
break;
}
case errSecAllocate: {
message = @"errSecAllocate";
break;
}
case errSecNotAvailable: {
message = @"errSecNotAvailable";
break;
}
case errSecDuplicateItem: {
message = @"errSecDuplicateItem";
break;
}
case errSecItemNotFound: {
message = @"errSecItemNotFound";
break;
}
case errSecInteractionNotAllowed: {
message = @"errSecInteractionNotAllowed";
break;
}
case errSecDecode: {
message = @"errSecDecode";
break;
}
case errSecAuthFailed: {
message = @"errSecAuthFailed";
break;
}
default: {
message = @"errSecDefault";
}
}
#else
message = (__bridge_transfer NSString *)SecCopyErrorMessageString(status, NULL);
#endif
NSDictionary *userInfo = nil;
if (message) {
userInfo = @{ NSLocalizedDescriptionKey : message };
}
return [NSError errorWithDomain:[NSString stringWithFormat:@"%@.%@", ARTAblyErrorDomain, @"Keychain"] code:status userInfo:userInfo];
}
- (nullable NSString *)keychainGetPasswordForService:(nonnull NSString *)serviceName account:(nonnull NSString *)account error:(NSError *__autoreleasing *)error {
NSMutableDictionary *query = [self newKeychainQueryForService:serviceName account:account];
[query setObject:@YES forKey:(__bridge id)kSecReturnData];
[query setObject:(__bridge id)kSecMatchLimitOne forKey:(__bridge id)kSecMatchLimit];
CFTypeRef result = NULL;
OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)query, &result);
if (status != errSecSuccess) {
if (error) {
*error = [self keychainErrorWithCode:status];
}
}
else {
NSData *passwordData = (__bridge_transfer NSData *)result;
if ([passwordData length]) {
return [[NSString alloc] initWithData:passwordData encoding:NSUTF8StringEncoding];
}
}
return nil;
}
- (BOOL)keychainDeletePasswordForService:(NSString *)serviceName account:(NSString *)account error:(NSError *__autoreleasing *)error {
NSMutableDictionary *query = [self newKeychainQueryForService:serviceName account:account];
OSStatus status;
#if TARGET_OS_IPHONE
status = SecItemDelete((__bridge CFDictionaryRef)query);
#else
CFTypeRef result = NULL;
[query setObject:@YES forKey:(__bridge id)kSecReturnRef];
status = SecItemCopyMatching((__bridge CFDictionaryRef)query, &result);
if (status == errSecSuccess) {
status = SecKeychainItemDelete((SecKeychainItemRef)result);
CFRelease(result);
}
#endif
if (status != errSecSuccess && error != NULL) {
*error = [self keychainErrorWithCode:status];
}
return (status == errSecSuccess);
}
- (BOOL)keychainSetPassword:(NSString *)password forService:(NSString *)serviceName account:(NSString *)account error:(NSError *__autoreleasing *)error {
NSMutableDictionary *query = nil;
NSMutableDictionary *searchQuery = [self newKeychainQueryForService:serviceName account:account];
NSData *passwordData = [password dataUsingEncoding:NSUTF8StringEncoding];
OSStatus status;
status = SecItemCopyMatching((__bridge CFDictionaryRef)searchQuery, nil);
if (status == errSecSuccess) { //item already exists, update it.
query = [[NSMutableDictionary alloc] init];
[query setObject:passwordData forKey:(__bridge id)kSecValueData];
status = SecItemUpdate((__bridge CFDictionaryRef)(searchQuery), (__bridge CFDictionaryRef)(query));
}
else if (status == errSecItemNotFound) { //item not found, create it.
query = [self newKeychainQueryForService:serviceName account:account];
[query setObject:passwordData forKey:(__bridge id)kSecValueData];
status = SecItemAdd((__bridge CFDictionaryRef)query, NULL);
}
if (status != errSecSuccess && error != NULL) {
*error = [self keychainErrorWithCode:status];
}
return (status == errSecSuccess);
}
@end