BKTouchIDManager.m 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236
  1. //
  2. // BKTouchIDManager.m
  3. // BKPasscodeViewDemo
  4. //
  5. // Created by Byungkook Jang on 2014. 10. 12..
  6. // Copyright (c) 2014년 Byungkook Jang. All rights reserved.
  7. //
  8. #import "BKTouchIDManager.h"
  9. #import <LocalAuthentication/LocalAuthentication.h>
  10. static NSString *const BKTouchIDManagerPasscodeAccountName = @"passcode";
  11. static NSString *const BKTouchIDManagerTouchIDEnabledAccountName = @"enabled";
  12. @interface BKTouchIDManager () {
  13. dispatch_queue_t _queue;
  14. }
  15. @property (nonatomic, strong) NSString *keychainServiceName;
  16. @end
  17. @implementation BKTouchIDManager
  18. - (instancetype)initWithKeychainServiceName:(NSString *)serviceName
  19. {
  20. self = [super init];
  21. if (self) {
  22. _queue = dispatch_queue_create("BKTouchIDManagerQueue", DISPATCH_QUEUE_SERIAL);
  23. NSParameterAssert(serviceName);
  24. self.keychainServiceName = serviceName;
  25. }
  26. return self;
  27. }
  28. + (BOOL)canUseTouchID
  29. {
  30. if (![LAContext class]) {
  31. return NO;
  32. }
  33. LAContext *context = [[LAContext alloc] init];
  34. NSError *error = nil;
  35. BOOL result = [context canEvaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics error:&error];
  36. return result;
  37. }
  38. - (void)savePasscode:(NSString *)passcode completionBlock:(void(^)(BOOL success))completionBlock
  39. {
  40. NSParameterAssert(passcode);
  41. if (NO == [[self class] canUseTouchID]) {
  42. if (completionBlock) {
  43. completionBlock(NO);
  44. }
  45. return;
  46. }
  47. NSString *serviceName = self.keychainServiceName;
  48. NSData *passcodeData = [passcode dataUsingEncoding:NSUTF8StringEncoding];
  49. dispatch_async(_queue, ^{
  50. BOOL success = [[self class] saveKeychainItemWithServiceName:serviceName
  51. accountName:BKTouchIDManagerPasscodeAccountName
  52. data:passcodeData
  53. sacFlags:kSecAccessControlUserPresence];
  54. if (success) {
  55. BOOL enabled = YES;
  56. success = [[self class] saveKeychainItemWithServiceName:serviceName
  57. accountName:BKTouchIDManagerTouchIDEnabledAccountName
  58. data:[NSData dataWithBytes:&enabled length:sizeof(BOOL)]
  59. sacFlags:0];
  60. }
  61. if (completionBlock) {
  62. dispatch_async(dispatch_get_main_queue(), ^{
  63. completionBlock(success);
  64. });
  65. }
  66. });
  67. }
  68. - (void)loadPasscodeWithCompletionBlock:(void (^)(NSString *))completionBlock
  69. {
  70. if (NO == [[self class] canUseTouchID]) {
  71. if (completionBlock) {
  72. completionBlock(nil);
  73. }
  74. return;
  75. }
  76. NSMutableDictionary *query = [NSMutableDictionary dictionaryWithDictionary:@{ (__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword,
  77. (__bridge id)kSecAttrService: self.keychainServiceName,
  78. (__bridge id)kSecAttrAccount: BKTouchIDManagerPasscodeAccountName,
  79. (__bridge id)kSecReturnData: @YES }];
  80. if (self.promptText) {
  81. query[(__bridge id)kSecUseOperationPrompt] = self.promptText;
  82. }
  83. dispatch_async(_queue, ^{
  84. CFTypeRef dataTypeRef = NULL;
  85. OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)(query), &dataTypeRef);
  86. NSString *result = nil;
  87. if (status == errSecSuccess) {
  88. NSData *resultData = ( __bridge_transfer NSData *)dataTypeRef;
  89. result = [[NSString alloc] initWithData:resultData encoding:NSUTF8StringEncoding];
  90. }
  91. if (completionBlock) {
  92. dispatch_async(dispatch_get_main_queue(), ^{
  93. completionBlock(result);
  94. });
  95. }
  96. });
  97. }
  98. - (void)deletePasscodeWithCompletionBlock:(void (^)(BOOL))completionBlock
  99. {
  100. dispatch_async(_queue, ^{
  101. BOOL success = ([[self class] deleteKeychainItemWithServiceName:self.keychainServiceName accountName:BKTouchIDManagerPasscodeAccountName] &&
  102. [[self class] deleteKeychainItemWithServiceName:self.keychainServiceName accountName:BKTouchIDManagerTouchIDEnabledAccountName]);
  103. if (completionBlock) {
  104. dispatch_async(dispatch_get_main_queue(), ^{
  105. completionBlock(success);
  106. });
  107. }
  108. });
  109. }
  110. - (BOOL)isTouchIDEnabled
  111. {
  112. NSDictionary *query = @{ (__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword,
  113. (__bridge id)kSecAttrService: self.keychainServiceName,
  114. (__bridge id)kSecAttrAccount: BKTouchIDManagerTouchIDEnabledAccountName,
  115. (__bridge id)kSecReturnData: @YES };
  116. CFTypeRef dataTypeRef = NULL;
  117. OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)(query), &dataTypeRef);
  118. if (status == errSecSuccess) {
  119. NSData *resultData = ( __bridge_transfer NSData *)dataTypeRef;
  120. BOOL result;
  121. [resultData getBytes:&result length:sizeof(BOOL)];
  122. return result;
  123. } else {
  124. return NO;
  125. }
  126. }
  127. #pragma mark - Static Methods
  128. + (BOOL)saveKeychainItemWithServiceName:(NSString *)serviceName accountName:(NSString *)accountName data:(NSData *)data sacFlags:(SecAccessControlCreateFlags)sacFlags
  129. {
  130. // try to update first
  131. BOOL success = [self updateKeychainItemWithServiceName:serviceName accountName:accountName data:data];
  132. if (success) {
  133. return YES;
  134. }
  135. // try deleting when update failed (workaround for iOS 8 bug)
  136. [self deleteKeychainItemWithServiceName:serviceName accountName:accountName];
  137. // try add
  138. return [self addKeychainItemWithServiceName:serviceName accountName:accountName data:data sacFlags:sacFlags];
  139. }
  140. + (BOOL)addKeychainItemWithServiceName:(NSString *)serviceName accountName:(NSString *)accountName data:(NSData *)data sacFlags:(SecAccessControlCreateFlags)sacFlags
  141. {
  142. CFErrorRef error = NULL;
  143. SecAccessControlRef sacObject = SecAccessControlCreateWithFlags(kCFAllocatorDefault,
  144. kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly,
  145. sacFlags, &error);
  146. if (sacObject == NULL || error != NULL) {
  147. return NO;
  148. }
  149. NSDictionary *attributes = @{ (__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword,
  150. (__bridge id)kSecAttrService: serviceName,
  151. (__bridge id)kSecAttrAccount: accountName,
  152. (__bridge id)kSecValueData: data,
  153. (__bridge id)kSecUseNoAuthenticationUI: @YES,
  154. (__bridge id)kSecAttrAccessControl: (__bridge_transfer id)sacObject };
  155. OSStatus status = SecItemAdd((__bridge CFDictionaryRef)attributes, nil);
  156. return (status == errSecSuccess);
  157. }
  158. + (BOOL)updateKeychainItemWithServiceName:(NSString *)serviceName accountName:(NSString *)accountName data:(NSData *)data
  159. {
  160. NSDictionary *query = @{ (__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword,
  161. (__bridge id)kSecAttrService: serviceName,
  162. (__bridge id)kSecAttrAccount: accountName };
  163. NSDictionary *changes = @{ (__bridge id)kSecValueData: data };
  164. OSStatus status = SecItemUpdate((__bridge CFDictionaryRef)query, (__bridge CFDictionaryRef)changes);
  165. return (status == errSecSuccess);
  166. }
  167. + (BOOL)deleteKeychainItemWithServiceName:(NSString *)serviceName accountName:(NSString *)accountName
  168. {
  169. NSDictionary *query = @{ (__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword,
  170. (__bridge id)kSecAttrService: serviceName,
  171. (__bridge id)kSecAttrAccount: accountName };
  172. OSStatus status = SecItemDelete((__bridge CFDictionaryRef)(query));
  173. return (status == errSecSuccess || status == errSecItemNotFound);
  174. }
  175. @end