// // YYKeychain.m // YYKit // // Created by ibireme on 14/10/15. // Copyright (c) 2015 ibireme. // // This source code is licensed under the MIT-style license found in the // LICENSE file in the root directory of this source tree. // #import "YYKeychain.h" #import "UIDevice+YYAdd.h" #import "YYKitMacro.h" #import static YYKeychainErrorCode YYKeychainErrorCodeFromOSStatus(OSStatus status) { switch (status) { case errSecUnimplemented: return YYKeychainErrorUnimplemented; case errSecIO: return YYKeychainErrorIO; case errSecOpWr: return YYKeychainErrorOpWr; case errSecParam: return YYKeychainErrorParam; case errSecAllocate: return YYKeychainErrorAllocate; case errSecUserCanceled: return YYKeychainErrorUserCancelled; case errSecBadReq: return YYKeychainErrorBadReq; case errSecInternalComponent: return YYKeychainErrorInternalComponent; case errSecNotAvailable: return YYKeychainErrorNotAvailable; case errSecDuplicateItem: return YYKeychainErrorDuplicateItem; case errSecItemNotFound: return YYKeychainErrorItemNotFound; case errSecInteractionNotAllowed: return YYKeychainErrorInteractionNotAllowed; case errSecDecode: return YYKeychainErrorDecode; case errSecAuthFailed: return YYKeychainErrorAuthFailed; default: return 0; } } static NSString *YYKeychainErrorDesc(YYKeychainErrorCode code) { switch (code) { case YYKeychainErrorUnimplemented: return @"Function or operation not implemented."; case YYKeychainErrorIO: return @"I/O error (bummers)"; case YYKeychainErrorOpWr: return @"ile already open with with write permission."; case YYKeychainErrorParam: return @"One or more parameters passed to a function where not valid."; case YYKeychainErrorAllocate: return @"Failed to allocate memory."; case YYKeychainErrorUserCancelled: return @"User canceled the operation."; case YYKeychainErrorBadReq: return @"Bad parameter or invalid state for operation."; case YYKeychainErrorInternalComponent: return @"Inrernal Component"; case YYKeychainErrorNotAvailable: return @"No keychain is available. You may need to restart your computer."; case YYKeychainErrorDuplicateItem: return @"The specified item already exists in the keychain."; case YYKeychainErrorItemNotFound: return @"The specified item could not be found in the keychain."; case YYKeychainErrorInteractionNotAllowed: return @"User interaction is not allowed."; case YYKeychainErrorDecode: return @"Unable to decode the provided data."; case YYKeychainErrorAuthFailed: return @"The user name or passphrase you entered is not"; default: break; } return nil; } static NSString *YYKeychainAccessibleStr(YYKeychainAccessible e) { switch (e) { case YYKeychainAccessibleWhenUnlocked: return (__bridge NSString *)(kSecAttrAccessibleWhenUnlocked); case YYKeychainAccessibleAfterFirstUnlock: return (__bridge NSString *)(kSecAttrAccessibleAfterFirstUnlock); case YYKeychainAccessibleAlways: return (__bridge NSString *)(kSecAttrAccessibleAlways); case YYKeychainAccessibleWhenPasscodeSetThisDeviceOnly: return (__bridge NSString *)(kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly); case YYKeychainAccessibleWhenUnlockedThisDeviceOnly: return (__bridge NSString *)(kSecAttrAccessibleWhenUnlockedThisDeviceOnly); case YYKeychainAccessibleAfterFirstUnlockThisDeviceOnly: return (__bridge NSString *)(kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly); case YYKeychainAccessibleAlwaysThisDeviceOnly: return (__bridge NSString *)(kSecAttrAccessibleAlwaysThisDeviceOnly); default: return nil; } } static YYKeychainAccessible YYKeychainAccessibleEnum(NSString *s) { if ([s isEqualToString:(__bridge NSString *)kSecAttrAccessibleWhenUnlocked]) return YYKeychainAccessibleWhenUnlocked; if ([s isEqualToString:(__bridge NSString *)kSecAttrAccessibleAfterFirstUnlock]) return YYKeychainAccessibleAfterFirstUnlock; if ([s isEqualToString:(__bridge NSString *)kSecAttrAccessibleAlways]) return YYKeychainAccessibleAlways; if ([s isEqualToString:(__bridge NSString *)kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly]) return YYKeychainAccessibleWhenPasscodeSetThisDeviceOnly; if ([s isEqualToString:(__bridge NSString *)kSecAttrAccessibleWhenUnlockedThisDeviceOnly]) return YYKeychainAccessibleWhenUnlockedThisDeviceOnly; if ([s isEqualToString:(__bridge NSString *)kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly]) return YYKeychainAccessibleAfterFirstUnlockThisDeviceOnly; if ([s isEqualToString:(__bridge NSString *)kSecAttrAccessibleAlwaysThisDeviceOnly]) return YYKeychainAccessibleAlwaysThisDeviceOnly; return YYKeychainAccessibleNone; } static id YYKeychainQuerySynchonizationID(YYKeychainQuerySynchronizationMode mode) { switch (mode) { case YYKeychainQuerySynchronizationModeAny: return (__bridge id)(kSecAttrSynchronizableAny); case YYKeychainQuerySynchronizationModeNo: return (__bridge id)kCFBooleanFalse; case YYKeychainQuerySynchronizationModeYes: return (__bridge id)kCFBooleanTrue; default: return (__bridge id)(kSecAttrSynchronizableAny); } } static YYKeychainQuerySynchronizationMode YYKeychainQuerySynchonizationEnum(NSNumber *num) { if ([num isEqualToNumber:@NO]) return YYKeychainQuerySynchronizationModeNo; if ([num isEqualToNumber:@YES]) return YYKeychainQuerySynchronizationModeYes; return YYKeychainQuerySynchronizationModeAny; } @interface YYKeychainItem () @property (nonatomic, readwrite, strong) NSDate *modificationDate; @property (nonatomic, readwrite, strong) NSDate *creationDate; @end @implementation YYKeychainItem - (void)setPasswordObject:(id )object { self.passwordData = [NSKeyedArchiver archivedDataWithRootObject:object]; } - (id )passwordObject { if ([self.passwordData length]) { return [NSKeyedUnarchiver unarchiveObjectWithData:self.passwordData]; } return nil; } - (void)setPassword:(NSString *)password { self.passwordData = [password dataUsingEncoding:NSUTF8StringEncoding]; } - (NSString *)password { if ([self.passwordData length]) { return [[NSString alloc] initWithData:self.passwordData encoding:NSUTF8StringEncoding]; } return nil; } - (NSMutableDictionary *)queryDic { NSMutableDictionary *dic = [NSMutableDictionary new]; dic[(__bridge id)kSecClass] = (__bridge id)kSecClassGenericPassword; if (self.account) dic[(__bridge id)kSecAttrAccount] = self.account; if (self.service) dic[(__bridge id)kSecAttrService] = self.service; if (![UIDevice currentDevice].isSimulator) { // Remove the access group if running on the iPhone simulator. // // Apps that are built for the simulator aren't signed, so there's no keychain access group // for the simulator to check. This means that all apps can see all keychain items when run // on the simulator. // // If a SecItem contains an access group attribute, SecItemAdd and SecItemUpdate on the // simulator will return -25243 (errSecNoAccessForItem). // // The access group attribute will be included in items returned by SecItemCopyMatching, // which is why we need to remove it before updating the item. if (self.accessGroup) dic[(__bridge id)kSecAttrAccessGroup] = self.accessGroup; } if (kiOS7Later) { dic[(__bridge id)kSecAttrSynchronizable] = YYKeychainQuerySynchonizationID(self.synchronizable); } return dic; } - (NSMutableDictionary *)dic { NSMutableDictionary *dic = [NSMutableDictionary new]; dic[(__bridge id)kSecClass] = (__bridge id)kSecClassGenericPassword; if (self.account) dic[(__bridge id)kSecAttrAccount] = self.account; if (self.service) dic[(__bridge id)kSecAttrService] = self.service; if (self.label) dic[(__bridge id)kSecAttrLabel] = self.label; if (![UIDevice currentDevice].isSimulator) { // Remove the access group if running on the iPhone simulator. // // Apps that are built for the simulator aren't signed, so there's no keychain access group // for the simulator to check. This means that all apps can see all keychain items when run // on the simulator. // // If a SecItem contains an access group attribute, SecItemAdd and SecItemUpdate on the // simulator will return -25243 (errSecNoAccessForItem). // // The access group attribute will be included in items returned by SecItemCopyMatching, // which is why we need to remove it before updating the item. if (self.accessGroup) dic[(__bridge id)kSecAttrAccessGroup] = self.accessGroup; } if (kiOS7Later) { dic[(__bridge id)kSecAttrSynchronizable] = YYKeychainQuerySynchonizationID(self.synchronizable); } if (self.accessible) dic[(__bridge id)kSecAttrAccessible] = YYKeychainAccessibleStr(self.accessible); if (self.passwordData) dic[(__bridge id)kSecValueData] = self.passwordData; if (self.type != nil) dic[(__bridge id)kSecAttrType] = self.type; if (self.creater != nil) dic[(__bridge id)kSecAttrCreator] = self.creater; if (self.comment) dic[(__bridge id)kSecAttrComment] = self.comment; if (self.descr) dic[(__bridge id)kSecAttrDescription] = self.descr; return dic; } - (instancetype)initWithDic:(NSDictionary *)dic { if (dic.count == 0) return nil; self = self.init; self.service = dic[(__bridge id)kSecAttrService]; self.account = dic[(__bridge id)kSecAttrAccount]; self.passwordData = dic[(__bridge id)kSecValueData]; self.label = dic[(__bridge id)kSecAttrLabel]; self.type = dic[(__bridge id)kSecAttrType]; self.creater = dic[(__bridge id)kSecAttrCreator]; self.comment = dic[(__bridge id)kSecAttrComment]; self.descr = dic[(__bridge id)kSecAttrDescription]; self.modificationDate = dic[(__bridge id)kSecAttrModificationDate]; self.creationDate = dic[(__bridge id)kSecAttrCreationDate]; self.accessGroup = dic[(__bridge id)kSecAttrAccessGroup]; self.accessible = YYKeychainAccessibleEnum(dic[(__bridge id)kSecAttrAccessible]); self.synchronizable = YYKeychainQuerySynchonizationEnum(dic[(__bridge id)kSecAttrSynchronizable]); return self; } - (id)copyWithZone:(NSZone *)zone { YYKeychainItem *item = [YYKeychainItem new]; item.service = self.service; item.account = self.account; item.passwordData = self.passwordData; item.label = self.label; item.type = self.type; item.creater = self.creater; item.comment = self.comment; item.descr = self.descr; item.modificationDate = self.modificationDate; item.creationDate = self.creationDate; item.accessGroup = self.accessGroup; item.accessible = self.accessible; item.synchronizable = self.synchronizable; return item; } - (NSString *)description { NSMutableString *str = @"".mutableCopy; [str appendString:@"YYKeychainItem:{\n"]; if (self.service) [str appendFormat:@" service:%@,\n", self.service]; if (self.account) [str appendFormat:@" service:%@,\n", self.account]; if (self.password) [str appendFormat:@" service:%@,\n", self.password]; if (self.label) [str appendFormat:@" service:%@,\n", self.label]; if (self.type != nil) [str appendFormat:@" service:%@,\n", self.type]; if (self.creater != nil) [str appendFormat:@" service:%@,\n", self.creater]; if (self.comment) [str appendFormat:@" service:%@,\n", self.comment]; if (self.descr) [str appendFormat:@" service:%@,\n", self.descr]; if (self.modificationDate) [str appendFormat:@" service:%@,\n", self.modificationDate]; if (self.creationDate) [str appendFormat:@" service:%@,\n", self.creationDate]; if (self.accessGroup) [str appendFormat:@" service:%@,\n", self.accessGroup]; [str appendString:@"}"]; return str; } @end @implementation YYKeychain + (NSString *)getPasswordForService:(NSString *)serviceName account:(NSString *)account error:(NSError **)error { if (!serviceName || !account) { if (error) *error = [YYKeychain errorWithCode:errSecParam]; return nil; } YYKeychainItem *item = [YYKeychainItem new]; item.service = serviceName; item.account = account; YYKeychainItem *result = [self selectOneItem:item error:error]; return result.password; } + (nullable NSString *)getPasswordForService:(NSString *)serviceName account:(NSString *)account { return [self getPasswordForService:serviceName account:account error:NULL]; } + (BOOL)deletePasswordForService:(NSString *)serviceName account:(NSString *)account error:(NSError **)error { if (!serviceName || !account) { if (error) *error = [YYKeychain errorWithCode:errSecParam]; return NO; } YYKeychainItem *item = [YYKeychainItem new]; item.service = serviceName; item.account = account; return [self deleteItem:item error:error]; } + (BOOL)deletePasswordForService:(NSString *)serviceName account:(NSString *)account { return [self deletePasswordForService:serviceName account:account error:NULL]; } + (BOOL)setPassword:(NSString *)password forService:(NSString *)serviceName account:(NSString *)account error:(NSError **)error { if (!password || !serviceName || !account) { if (error) *error = [YYKeychain errorWithCode:errSecParam]; return NO; } YYKeychainItem *item = [YYKeychainItem new]; item.service = serviceName; item.account = account; YYKeychainItem *result = [self selectOneItem:item error:NULL]; if (result) { result.password = password; return [self updateItem:result error:error]; } else { item.password = password; return [self insertItem:item error:error]; } } + (BOOL)setPassword:(NSString *)password forService:(NSString *)serviceName account:(NSString *)account { return [self setPassword:password forService:serviceName account:account error:NULL]; } + (BOOL)insertItem:(YYKeychainItem *)item error:(NSError **)error { if (!item.service || !item.account || !item.passwordData) { if (error) *error = [YYKeychain errorWithCode:errSecParam]; return NO; } NSMutableDictionary *query = [item dic]; OSStatus status = status = SecItemAdd((__bridge CFDictionaryRef)query, NULL); if (status != errSecSuccess) { if (error) *error = [YYKeychain errorWithCode:status]; return NO; } return YES; } + (BOOL)insertItem:(YYKeychainItem *)item { return [self insertItem:item error:NULL]; } + (BOOL)updateItem:(YYKeychainItem *)item error:(NSError **)error { if (!item.service || !item.account || !item.passwordData) { if (error) *error = [YYKeychain errorWithCode:errSecParam]; return NO; } NSMutableDictionary *query = [item queryDic]; NSMutableDictionary *update = [item dic]; [update removeObjectForKey:(__bridge id)kSecClass]; if (!query || !update) return NO; OSStatus status = status = SecItemUpdate((__bridge CFDictionaryRef)query, (__bridge CFDictionaryRef)update); if (status != errSecSuccess) { if (error) *error = [YYKeychain errorWithCode:status]; return NO; } return YES; } + (BOOL)updateItem:(YYKeychainItem *)item { return [self updateItem:item error:NULL]; } + (BOOL)deleteItem:(YYKeychainItem *)item error:(NSError **)error { if (!item.service || !item.account) { if (error) *error = [YYKeychain errorWithCode:errSecParam]; return NO; } NSMutableDictionary *query = [item dic]; OSStatus status = SecItemDelete((__bridge CFDictionaryRef)query); if (status != errSecSuccess) { if (error) *error = [YYKeychain errorWithCode:status]; return NO; } return YES; } + (BOOL)deleteItem:(YYKeychainItem *)item { return [self deleteItem:item error:NULL]; } + (YYKeychainItem *)selectOneItem:(YYKeychainItem *)item error:(NSError **)error { if (!item.service || !item.account) { if (error) *error = [YYKeychain errorWithCode:errSecParam]; return nil; } NSMutableDictionary *query = [item dic]; query[(__bridge id)kSecMatchLimit] = (__bridge id)kSecMatchLimitOne; query[(__bridge id)kSecReturnAttributes] = @YES; query[(__bridge id)kSecReturnData] = @YES; OSStatus status; CFTypeRef result = NULL; status = SecItemCopyMatching((__bridge CFDictionaryRef)query, &result); if (status != errSecSuccess) { if (error) *error = [[self class] errorWithCode:status]; return nil; } if (!result) return nil; NSDictionary *dic = nil; if (CFGetTypeID(result) == CFDictionaryGetTypeID()) { dic = (__bridge NSDictionary *)(result); } else if (CFGetTypeID(result) == CFArrayGetTypeID()){ dic = [(__bridge NSArray *)(result) firstObject]; if (![dic isKindOfClass:[NSDictionary class]]) dic = nil; } if (!dic.count) return nil; return [[YYKeychainItem alloc] initWithDic:dic]; } + (YYKeychainItem *)selectOneItem:(YYKeychainItem *)item { return [self selectOneItem:item error:NULL]; } + (NSArray *)selectItems:(YYKeychainItem *)item error:(NSError **)error { NSMutableDictionary *query = [item dic]; query[(__bridge id)kSecMatchLimit] = (__bridge id)kSecMatchLimitAll; query[(__bridge id)kSecReturnAttributes] = @YES; query[(__bridge id)kSecReturnData] = @YES; OSStatus status; CFTypeRef result = NULL; status = SecItemCopyMatching((__bridge CFDictionaryRef)query, &result); if (status != errSecSuccess && error != NULL) { *error = [[self class] errorWithCode:status]; return nil; } NSMutableArray *res = [NSMutableArray new]; NSDictionary *dic = nil; if (CFGetTypeID(result) == CFDictionaryGetTypeID()) { dic = (__bridge NSDictionary *)(result); YYKeychainItem *item = [[YYKeychainItem alloc] initWithDic:dic]; if (item) [res addObject:item]; } else if (CFGetTypeID(result) == CFArrayGetTypeID()){ for (NSDictionary *dic in (__bridge NSArray *)(result)) { YYKeychainItem *item = [[YYKeychainItem alloc] initWithDic:dic]; if (item) [res addObject:item]; } } return res; } + (NSArray *)selectItems:(YYKeychainItem *)item { return [self selectItems:item error:NULL]; } + (NSError *)errorWithCode:(OSStatus)osCode { YYKeychainErrorCode code = YYKeychainErrorCodeFromOSStatus(osCode); NSString *desc = YYKeychainErrorDesc(code); NSDictionary *userInfo = desc ? @{ NSLocalizedDescriptionKey : desc } : nil; return [NSError errorWithDomain:@"com.ibireme.yykit.keychain" code:code userInfo:userInfo]; } @end