You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

256 lines
8.4 KiB

6 years ago
  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. @implementation FIRAuthKeychain {
  34. /** @var _service
  35. @brief The name of the keychain service.
  36. */
  37. NSString *_service;
  38. /** @var _legacyItemDeletedForKey
  39. @brief Indicates whether or not this class knows that the legacy item for a particular key has
  40. been deleted.
  41. @remarks This dictionary is to avoid unecessary keychain operations against legacy items.
  42. */
  43. NSMutableDictionary *_legacyEntryDeletedForKey;
  44. }
  45. - (id<FIRAuthStorage>)initWithService:(NSString *)service {
  46. #if FIRAUTH_USER_DEFAULTS_STORAGE_AVAILABLE
  47. NSString *OSVersion = [UIDevice currentDevice].systemVersion;
  48. NSRegularExpression *regex =
  49. [NSRegularExpression regularExpressionWithPattern:kOSVersionMatcherForUsingUserDefaults
  50. options:0
  51. error:NULL];
  52. if ([regex numberOfMatchesInString:OSVersion options:0 range:NSMakeRange(0, OSVersion.length)]) {
  53. return (id<FIRAuthStorage>)[[FIRAuthUserDefaultsStorage alloc] initWithService:service];
  54. }
  55. #endif // FIRAUTH_USER_DEFAULTS_STORAGE_AVAILABLE
  56. self = [super init];
  57. if (self) {
  58. _service = [service copy];
  59. _legacyEntryDeletedForKey = [[NSMutableDictionary alloc] init];
  60. }
  61. return self;
  62. }
  63. - (NSData *)dataForKey:(NSString *)key error:(NSError **_Nullable)error {
  64. if (!key.length) {
  65. [NSException raise:NSInvalidArgumentException
  66. format:@"%@", @"The key cannot be nil or empty."];
  67. return nil;
  68. }
  69. NSData *data = [self itemWithQuery:[self genericPasswordQueryWithKey:key] error:error];
  70. if (error && *error) {
  71. return nil;
  72. }
  73. if (data) {
  74. return data;
  75. }
  76. // Check for legacy form.
  77. if (_legacyEntryDeletedForKey[key]) {
  78. return nil;
  79. }
  80. data = [self itemWithQuery:[self legacyGenericPasswordQueryWithKey:key] error:error];
  81. if (error && *error) {
  82. return nil;
  83. }
  84. if (!data) {
  85. // Mark legacy data as non-existing so we don't have to query it again.
  86. _legacyEntryDeletedForKey[key] = @YES;
  87. return nil;
  88. }
  89. // Move the data to current form.
  90. if (![self setData:data forKey:key error:error]) {
  91. return nil;
  92. }
  93. [self deleteLegacyItemWithKey:key];
  94. return data;
  95. }
  96. - (BOOL)setData:(NSData *)data forKey:(NSString *)key error:(NSError **_Nullable)error {
  97. if (!key.length) {
  98. [NSException raise:NSInvalidArgumentException
  99. format:@"%@", @"The key cannot be nil or empty."];
  100. return NO;
  101. }
  102. NSDictionary *attributes = @{
  103. (__bridge id)kSecValueData : data,
  104. (__bridge id)kSecAttrAccessible : (__bridge id)kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly,
  105. };
  106. return [self setItemWithQuery:[self genericPasswordQueryWithKey:key]
  107. attributes:attributes
  108. error:error];
  109. }
  110. - (BOOL)removeDataForKey:(NSString *)key error:(NSError **_Nullable)error {
  111. if (!key.length) {
  112. [NSException raise:NSInvalidArgumentException
  113. format:@"%@", @"The key cannot be nil or empty."];
  114. return NO;
  115. }
  116. if (![self deleteItemWithQuery:[self genericPasswordQueryWithKey:key] error:error]) {
  117. return NO;
  118. }
  119. // Legacy form item, if exists, also needs to be removed, otherwise it will be exposed when
  120. // current form item is removed, leading to incorrect semantics.
  121. [self deleteLegacyItemWithKey:key];
  122. return YES;
  123. }
  124. #pragma mark - Private
  125. - (NSData *)itemWithQuery:(NSDictionary *)query error:(NSError **_Nullable)error {
  126. NSMutableDictionary *returningQuery = [query mutableCopy];
  127. returningQuery[(__bridge id)kSecReturnData] = @YES;
  128. returningQuery[(__bridge id)kSecReturnAttributes] = @YES;
  129. // Using a match limit of 2 means that we can check whether there is more than one item.
  130. // If we used a match limit of 1 we would never find out.
  131. returningQuery[(__bridge id)kSecMatchLimit] = @2;
  132. CFArrayRef result = NULL;
  133. OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)returningQuery,
  134. (CFTypeRef *)&result);
  135. if (status == noErr && result != NULL) {
  136. NSArray *items = (__bridge_transfer NSArray *)result;
  137. if (items.count != 1) {
  138. if (error) {
  139. *error = [FIRAuthErrorUtils keychainErrorWithFunction:@"SecItemCopyMatching"
  140. status:status];
  141. }
  142. return nil;
  143. }
  144. if (error) {
  145. *error = nil;
  146. }
  147. NSDictionary *item = items[0];
  148. return item[(__bridge id)kSecValueData];
  149. }
  150. if (status == errSecItemNotFound) {
  151. if (error) {
  152. *error = nil;
  153. }
  154. } else {
  155. if (error) {
  156. *error = [FIRAuthErrorUtils keychainErrorWithFunction:@"SecItemCopyMatching" status:status];
  157. }
  158. }
  159. return nil;
  160. }
  161. - (BOOL)setItemWithQuery:(NSDictionary *)query
  162. attributes:(NSDictionary *)attributes
  163. error:(NSError **_Nullable)error {
  164. NSMutableDictionary *combined = [attributes mutableCopy];
  165. [combined addEntriesFromDictionary:query];
  166. BOOL hasItem = NO;
  167. OSStatus status = SecItemAdd((__bridge CFDictionaryRef)combined, NULL);
  168. if (status == errSecDuplicateItem) {
  169. hasItem = YES;
  170. status = SecItemUpdate((__bridge CFDictionaryRef)query, (__bridge CFDictionaryRef)attributes);
  171. }
  172. if (status == noErr) {
  173. return YES;
  174. }
  175. if (error) {
  176. NSString *function = hasItem ? @"SecItemUpdate" : @"SecItemAdd";
  177. *error = [FIRAuthErrorUtils keychainErrorWithFunction:function status:status];
  178. }
  179. return NO;
  180. }
  181. - (BOOL)deleteItemWithQuery:(NSDictionary *)query error:(NSError **_Nullable)error {
  182. OSStatus status = SecItemDelete((__bridge CFDictionaryRef)query);
  183. if (status == noErr || status == errSecItemNotFound) {
  184. return YES;
  185. }
  186. if (error) {
  187. *error = [FIRAuthErrorUtils keychainErrorWithFunction:@"SecItemDelete" status:status];
  188. }
  189. return NO;
  190. }
  191. /** @fn deleteLegacyItemsWithKey:
  192. @brief Deletes legacy item from the keychain if it is not already known to be deleted.
  193. @param key The key for the item.
  194. */
  195. - (void)deleteLegacyItemWithKey:(NSString *)key {
  196. if (_legacyEntryDeletedForKey[key]) {
  197. return;
  198. }
  199. NSDictionary *query = [self legacyGenericPasswordQueryWithKey:key];
  200. SecItemDelete((__bridge CFDictionaryRef)query);
  201. _legacyEntryDeletedForKey[key] = @YES;
  202. }
  203. /** @fn genericPasswordQueryWithKey:
  204. @brief Returns a keychain query of generic password to be used to manipulate key'ed value.
  205. @param key The key for the value being manipulated, used as the account field in the query.
  206. */
  207. - (NSDictionary *)genericPasswordQueryWithKey:(NSString *)key {
  208. return @{
  209. (__bridge id)kSecClass : (__bridge id)kSecClassGenericPassword,
  210. (__bridge id)kSecAttrAccount : [kAccountPrefix stringByAppendingString:key],
  211. (__bridge id)kSecAttrService : _service,
  212. };
  213. }
  214. /** @fn legacyGenericPasswordQueryWithKey:
  215. @brief Returns a keychain query of generic password without service field, which is used by
  216. previous version of this class.
  217. @param key The key for the value being manipulated, used as the account field in the query.
  218. */
  219. - (NSDictionary *)legacyGenericPasswordQueryWithKey:(NSString *)key {
  220. return @{
  221. (__bridge id)kSecClass : (__bridge id)kSecClassGenericPassword,
  222. (__bridge id)kSecAttrAccount : key,
  223. };
  224. }
  225. @end