Brak opisu

FIRAuthKeychain.m 8.5KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260
  1. /*
  2. * Copyright 2017 Google
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License");
  5. * you may not use this file except in compliance with the License.
  6. * You may obtain a copy of the License at
  7. *
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS,
  12. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. * See the License for the specific language governing permissions and
  14. * limitations under the License.
  15. */
  16. #import "FIRAuthKeychain.h"
  17. #import <Security/Security.h>
  18. #import "FIRAuthErrorUtils.h"
  19. #import "FIRAuthUserDefaultsStorage.h"
  20. #if FIRAUTH_USER_DEFAULTS_STORAGE_AVAILABLE
  21. #import <UIKit/UIKit.h>
  22. /** @var kOSVersionMatcherForUsingUserDefaults
  23. @brief The regular expression to match all OS versions that @c FIRAuthUserDefaultsStorage is
  24. used instead if available.
  25. */
  26. static NSString *const kOSVersionMatcherForUsingUserDefaults = @"^10\\.[01](\\..*)?$";
  27. #endif // FIRAUTH_USER_DEFAULTS_STORAGE_AVAILABLE
  28. /** @var kAccountPrefix
  29. @brief The prefix string for keychain item account attribute before the key.
  30. @remarks A number "1" is encoded in the prefix in case we need to upgrade the scheme in future.
  31. */
  32. static NSString *const kAccountPrefix = @"firebase_auth_1_";
  33. NS_ASSUME_NONNULL_BEGIN
  34. @implementation FIRAuthKeychain {
  35. /** @var _service
  36. @brief The name of the keychain service.
  37. */
  38. NSString *_service;
  39. /** @var _legacyItemDeletedForKey
  40. @brief Indicates whether or not this class knows that the legacy item for a particular key has
  41. been deleted.
  42. @remarks This dictionary is to avoid unecessary keychain operations against legacy items.
  43. */
  44. NSMutableDictionary *_legacyEntryDeletedForKey;
  45. }
  46. - (id<FIRAuthStorage>)initWithService:(NSString *)service {
  47. #if FIRAUTH_USER_DEFAULTS_STORAGE_AVAILABLE
  48. NSString *OSVersion = [UIDevice currentDevice].systemVersion;
  49. NSRegularExpression *regex =
  50. [NSRegularExpression regularExpressionWithPattern:kOSVersionMatcherForUsingUserDefaults
  51. options:0
  52. error:NULL];
  53. if ([regex numberOfMatchesInString:OSVersion options:0 range:NSMakeRange(0, OSVersion.length)]) {
  54. return (id<FIRAuthStorage>)[[FIRAuthUserDefaultsStorage alloc] initWithService:service];
  55. }
  56. #endif // FIRAUTH_USER_DEFAULTS_STORAGE_AVAILABLE
  57. self = [super init];
  58. if (self) {
  59. _service = [service copy];
  60. _legacyEntryDeletedForKey = [[NSMutableDictionary alloc] init];
  61. }
  62. return self;
  63. }
  64. - (nullable NSData *)dataForKey:(NSString *)key error:(NSError **_Nullable)error {
  65. if (!key.length) {
  66. [NSException raise:NSInvalidArgumentException
  67. format:@"%@", @"The key cannot be nil or empty."];
  68. return nil;
  69. }
  70. NSData *data = [self itemWithQuery:[self genericPasswordQueryWithKey:key] error:error];
  71. if (error && *error) {
  72. return nil;
  73. }
  74. if (data) {
  75. return data;
  76. }
  77. // Check for legacy form.
  78. if (_legacyEntryDeletedForKey[key]) {
  79. return nil;
  80. }
  81. data = [self itemWithQuery:[self legacyGenericPasswordQueryWithKey:key] error:error];
  82. if (error && *error) {
  83. return nil;
  84. }
  85. if (!data) {
  86. // Mark legacy data as non-existing so we don't have to query it again.
  87. _legacyEntryDeletedForKey[key] = @YES;
  88. return nil;
  89. }
  90. // Move the data to current form.
  91. if (![self setData:data forKey:key error:error]) {
  92. return nil;
  93. }
  94. [self deleteLegacyItemWithKey:key];
  95. return data;
  96. }
  97. - (BOOL)setData:(NSData *)data forKey:(NSString *)key error:(NSError **_Nullable)error {
  98. if (!key.length) {
  99. [NSException raise:NSInvalidArgumentException
  100. format:@"%@", @"The key cannot be nil or empty."];
  101. return NO;
  102. }
  103. NSDictionary *attributes = @{
  104. (__bridge id)kSecValueData : data,
  105. (__bridge id)kSecAttrAccessible : (__bridge id)kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly,
  106. };
  107. return [self setItemWithQuery:[self genericPasswordQueryWithKey:key]
  108. attributes:attributes
  109. error:error];
  110. }
  111. - (BOOL)removeDataForKey:(NSString *)key error:(NSError **_Nullable)error {
  112. if (!key.length) {
  113. [NSException raise:NSInvalidArgumentException
  114. format:@"%@", @"The key cannot be nil or empty."];
  115. return NO;
  116. }
  117. if (![self deleteItemWithQuery:[self genericPasswordQueryWithKey:key] error:error]) {
  118. return NO;
  119. }
  120. // Legacy form item, if exists, also needs to be removed, otherwise it will be exposed when
  121. // current form item is removed, leading to incorrect semantics.
  122. [self deleteLegacyItemWithKey:key];
  123. return YES;
  124. }
  125. #pragma mark - Private
  126. - (NSData *)itemWithQuery:(NSDictionary *)query error:(NSError **_Nullable)error {
  127. NSMutableDictionary *returningQuery = [query mutableCopy];
  128. returningQuery[(__bridge id)kSecReturnData] = @YES;
  129. returningQuery[(__bridge id)kSecReturnAttributes] = @YES;
  130. // Using a match limit of 2 means that we can check whether there is more than one item.
  131. // If we used a match limit of 1 we would never find out.
  132. returningQuery[(__bridge id)kSecMatchLimit] = @2;
  133. CFArrayRef result = NULL;
  134. OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)returningQuery,
  135. (CFTypeRef *)&result);
  136. if (status == noErr && result != NULL) {
  137. NSArray *items = (__bridge_transfer NSArray *)result;
  138. if (items.count != 1) {
  139. if (error) {
  140. *error = [FIRAuthErrorUtils keychainErrorWithFunction:@"SecItemCopyMatching"
  141. status:status];
  142. }
  143. return nil;
  144. }
  145. if (error) {
  146. *error = nil;
  147. }
  148. NSDictionary *item = items[0];
  149. return item[(__bridge id)kSecValueData];
  150. }
  151. if (status == errSecItemNotFound) {
  152. if (error) {
  153. *error = nil;
  154. }
  155. } else {
  156. if (error) {
  157. *error = [FIRAuthErrorUtils keychainErrorWithFunction:@"SecItemCopyMatching" status:status];
  158. }
  159. }
  160. return nil;
  161. }
  162. - (BOOL)setItemWithQuery:(NSDictionary *)query
  163. attributes:(NSDictionary *)attributes
  164. error:(NSError **_Nullable)error {
  165. NSMutableDictionary *combined = [attributes mutableCopy];
  166. [combined addEntriesFromDictionary:query];
  167. BOOL hasItem = NO;
  168. OSStatus status = SecItemAdd((__bridge CFDictionaryRef)combined, NULL);
  169. if (status == errSecDuplicateItem) {
  170. hasItem = YES;
  171. status = SecItemUpdate((__bridge CFDictionaryRef)query, (__bridge CFDictionaryRef)attributes);
  172. }
  173. if (status == noErr) {
  174. return YES;
  175. }
  176. if (error) {
  177. NSString *function = hasItem ? @"SecItemUpdate" : @"SecItemAdd";
  178. *error = [FIRAuthErrorUtils keychainErrorWithFunction:function status:status];
  179. }
  180. return NO;
  181. }
  182. - (BOOL)deleteItemWithQuery:(NSDictionary *)query error:(NSError **_Nullable)error {
  183. OSStatus status = SecItemDelete((__bridge CFDictionaryRef)query);
  184. if (status == noErr || status == errSecItemNotFound) {
  185. return YES;
  186. }
  187. if (error) {
  188. *error = [FIRAuthErrorUtils keychainErrorWithFunction:@"SecItemDelete" status:status];
  189. }
  190. return NO;
  191. }
  192. /** @fn deleteLegacyItemsWithKey:
  193. @brief Deletes legacy item from the keychain if it is not already known to be deleted.
  194. @param key The key for the item.
  195. */
  196. - (void)deleteLegacyItemWithKey:(NSString *)key {
  197. if (_legacyEntryDeletedForKey[key]) {
  198. return;
  199. }
  200. NSDictionary *query = [self legacyGenericPasswordQueryWithKey:key];
  201. SecItemDelete((__bridge CFDictionaryRef)query);
  202. _legacyEntryDeletedForKey[key] = @YES;
  203. }
  204. /** @fn genericPasswordQueryWithKey:
  205. @brief Returns a keychain query of generic password to be used to manipulate key'ed value.
  206. @param key The key for the value being manipulated, used as the account field in the query.
  207. */
  208. - (NSDictionary *)genericPasswordQueryWithKey:(NSString *)key {
  209. return @{
  210. (__bridge id)kSecClass : (__bridge id)kSecClassGenericPassword,
  211. (__bridge id)kSecAttrAccount : [kAccountPrefix stringByAppendingString:key],
  212. (__bridge id)kSecAttrService : _service,
  213. };
  214. }
  215. /** @fn legacyGenericPasswordQueryWithKey:
  216. @brief Returns a keychain query of generic password without service field, which is used by
  217. previous version of this class.
  218. @param key The key for the value being manipulated, used as the account field in the query.
  219. */
  220. - (NSDictionary *)legacyGenericPasswordQueryWithKey:(NSString *)key {
  221. return @{
  222. (__bridge id)kSecClass : (__bridge id)kSecClassGenericPassword,
  223. (__bridge id)kSecAttrAccount : key,
  224. };
  225. }
  226. @end
  227. NS_ASSUME_NONNULL_END