FIRAuthKeychainServices.m 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341
  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 "FirebaseAuth/Sources/Storage/FIRAuthKeychainServices.h"
  17. #import <Security/Security.h>
  18. #import "FirebaseCore/Sources/Private/FirebaseCoreInternal.h"
  19. #import "FirebaseAuth/Sources/Storage/FIRAuthUserDefaults.h"
  20. #import "FirebaseAuth/Sources/Utilities/FIRAuthErrorUtils.h"
  21. /** @var kAccountPrefix
  22. @brief The prefix string for keychain item account attribute before the key.
  23. @remarks A number "1" is encoded in the prefix in case we need to upgrade the scheme in future.
  24. */
  25. static NSString *const kAccountPrefix = @"firebase_auth_1_";
  26. NS_ASSUME_NONNULL_BEGIN
  27. @implementation FIRAuthKeychainServices {
  28. /** @var _service
  29. @brief The name of the keychain service.
  30. */
  31. NSString *_service;
  32. /** @var _legacyItemDeletedForKey
  33. @brief Indicates whether or not this class knows that the legacy item for a particular key has
  34. been deleted.
  35. @remarks This dictionary is to avoid unecessary keychain operations against legacy items.
  36. */
  37. NSMutableDictionary *_legacyEntryDeletedForKey;
  38. }
  39. - (id<FIRAuthStorage>)initWithService:(NSString *)service {
  40. self = [super init];
  41. if (self) {
  42. _service = [service copy];
  43. _legacyEntryDeletedForKey = [[NSMutableDictionary alloc] init];
  44. }
  45. return self;
  46. }
  47. - (nullable NSData *)dataForKey:(NSString *)key error:(NSError **_Nullable)error {
  48. if (!key.length) {
  49. [NSException raise:NSInvalidArgumentException format:@"%@", @"The key cannot be nil or empty."];
  50. return nil;
  51. }
  52. NSData *data = [self itemWithQuery:[self genericPasswordQueryWithKey:key] error:error];
  53. if (error && *error) {
  54. return nil;
  55. }
  56. if (data) {
  57. return data;
  58. }
  59. // Check for legacy form.
  60. if (_legacyEntryDeletedForKey[key]) {
  61. return nil;
  62. }
  63. data = [self itemWithQuery:[self legacyGenericPasswordQueryWithKey:key] error:error];
  64. if (error && *error) {
  65. return nil;
  66. }
  67. if (!data) {
  68. // Mark legacy data as non-existing so we don't have to query it again.
  69. _legacyEntryDeletedForKey[key] = @YES;
  70. return nil;
  71. }
  72. // Move the data to current form.
  73. if (![self setData:data forKey:key error:error]) {
  74. return nil;
  75. }
  76. [self deleteLegacyItemWithKey:key];
  77. return data;
  78. }
  79. - (BOOL)setData:(NSData *)data forKey:(NSString *)key error:(NSError **_Nullable)error {
  80. if (!key.length) {
  81. [NSException raise:NSInvalidArgumentException format:@"%@", @"The key cannot be nil or empty."];
  82. return NO;
  83. }
  84. NSDictionary *attributes = @{
  85. (__bridge id)kSecValueData : data,
  86. (__bridge id)kSecAttrAccessible : (__bridge id)kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly,
  87. };
  88. return [self setItemWithQuery:[self genericPasswordQueryWithKey:key]
  89. attributes:attributes
  90. error:error];
  91. }
  92. - (BOOL)removeDataForKey:(NSString *)key error:(NSError **_Nullable)error {
  93. if (!key.length) {
  94. [NSException raise:NSInvalidArgumentException format:@"%@", @"The key cannot be nil or empty."];
  95. return NO;
  96. }
  97. if (![self deleteItemWithQuery:[self genericPasswordQueryWithKey:key] error:error]) {
  98. return NO;
  99. }
  100. // Legacy form item, if exists, also needs to be removed, otherwise it will be exposed when
  101. // current form item is removed, leading to incorrect semantics.
  102. [self deleteLegacyItemWithKey:key];
  103. return YES;
  104. }
  105. #pragma mark - Private methods for non-sharing keychain operations
  106. - (nullable NSData *)itemWithQuery:(NSDictionary *)query error:(NSError **_Nullable)error {
  107. NSMutableDictionary *returningQuery = [query mutableCopy];
  108. returningQuery[(__bridge id)kSecReturnData] = @YES;
  109. returningQuery[(__bridge id)kSecReturnAttributes] = @YES;
  110. // Using a match limit of 2 means that we can check whether there is more than one item.
  111. // If we used a match limit of 1 we would never find out.
  112. returningQuery[(__bridge id)kSecMatchLimit] = @2;
  113. CFArrayRef result = NULL;
  114. OSStatus status =
  115. SecItemCopyMatching((__bridge CFDictionaryRef)returningQuery, (CFTypeRef *)&result);
  116. if (status == noErr && result != NULL) {
  117. NSArray *items = (__bridge_transfer NSArray *)result;
  118. if (items.count == 0) {
  119. if (error) {
  120. // The keychain query returned no error, but there were no items found.
  121. *error = [FIRAuthErrorUtils keychainErrorWithFunction:@"SecItemCopyMatching" status:status];
  122. }
  123. return nil;
  124. } else if (items.count > 1) {
  125. // More than one keychain item was found, all but the first will be ignored.
  126. FIRLogWarning(
  127. kFIRLoggerAuth, @"I-AUT000005",
  128. @"Keychain query returned multiple results, all but the first will be ignored: %@",
  129. items);
  130. }
  131. if (error) {
  132. *error = nil;
  133. }
  134. // Return the non-legacy item.
  135. for (NSDictionary *item in items) {
  136. if (item[(__bridge NSString *)kSecAttrService] != nil) {
  137. return item[(__bridge id)kSecValueData];
  138. }
  139. }
  140. // If they were all legacy items, just return the first one.
  141. // This should not happen, since only one account should be
  142. // stored.
  143. return items[0][(__bridge id)kSecValueData];
  144. }
  145. if (status == errSecItemNotFound) {
  146. if (error) {
  147. *error = nil;
  148. }
  149. } else {
  150. if (error) {
  151. *error = [FIRAuthErrorUtils keychainErrorWithFunction:@"SecItemCopyMatching" status:status];
  152. }
  153. }
  154. return nil;
  155. }
  156. - (BOOL)setItemWithQuery:(NSDictionary *)query
  157. attributes:(NSDictionary *)attributes
  158. error:(NSError **_Nullable)error {
  159. NSMutableDictionary *combined = [attributes mutableCopy];
  160. [combined addEntriesFromDictionary:query];
  161. BOOL hasItem = NO;
  162. OSStatus status = SecItemAdd((__bridge CFDictionaryRef)combined, NULL);
  163. if (status == errSecDuplicateItem) {
  164. hasItem = YES;
  165. status = SecItemUpdate((__bridge CFDictionaryRef)query, (__bridge CFDictionaryRef)attributes);
  166. }
  167. if (status == noErr) {
  168. return YES;
  169. }
  170. if (error) {
  171. NSString *function = hasItem ? @"SecItemUpdate" : @"SecItemAdd";
  172. *error = [FIRAuthErrorUtils keychainErrorWithFunction:function status:status];
  173. }
  174. return NO;
  175. }
  176. - (BOOL)deleteItemWithQuery:(NSDictionary *)query error:(NSError **_Nullable)error {
  177. OSStatus status = SecItemDelete((__bridge CFDictionaryRef)query);
  178. if (status == noErr || status == errSecItemNotFound) {
  179. return YES;
  180. }
  181. if (error) {
  182. *error = [FIRAuthErrorUtils keychainErrorWithFunction:@"SecItemDelete" status:status];
  183. }
  184. return NO;
  185. }
  186. /** @fn deleteLegacyItemsWithKey:
  187. @brief Deletes legacy item from the keychain if it is not already known to be deleted.
  188. @param key The key for the item.
  189. */
  190. - (void)deleteLegacyItemWithKey:(NSString *)key {
  191. if (_legacyEntryDeletedForKey[key]) {
  192. return;
  193. }
  194. NSDictionary *query = [self legacyGenericPasswordQueryWithKey:key];
  195. SecItemDelete((__bridge CFDictionaryRef)query);
  196. _legacyEntryDeletedForKey[key] = @YES;
  197. }
  198. /** @fn genericPasswordQueryWithKey:
  199. @brief Returns a keychain query of generic password to be used to manipulate key'ed value.
  200. @param key The key for the value being manipulated, used as the account field in the query.
  201. */
  202. - (NSDictionary *)genericPasswordQueryWithKey:(NSString *)key {
  203. return @{
  204. (__bridge id)kSecClass : (__bridge id)kSecClassGenericPassword,
  205. (__bridge id)kSecAttrAccount : [kAccountPrefix stringByAppendingString:key],
  206. (__bridge id)kSecAttrService : _service,
  207. };
  208. }
  209. /** @fn legacyGenericPasswordQueryWithKey:
  210. @brief Returns a keychain query of generic password without service field, which is used by
  211. previous version of this class.
  212. @param key The key for the value being manipulated, used as the account field in the query.
  213. */
  214. - (NSDictionary *)legacyGenericPasswordQueryWithKey:(NSString *)key {
  215. return @{
  216. (__bridge id)kSecClass : (__bridge id)kSecClassGenericPassword,
  217. (__bridge id)kSecAttrAccount : key,
  218. };
  219. }
  220. #pragma mark - Private methods for shared keychain operations
  221. - (nullable NSData *)getItemWithQuery:(NSDictionary *)query
  222. error:(NSError *_Nullable *_Nullable)outError {
  223. NSMutableDictionary *mutableQuery = [query mutableCopy];
  224. mutableQuery[(__bridge id)kSecReturnData] = @YES;
  225. mutableQuery[(__bridge id)kSecReturnAttributes] = @YES;
  226. mutableQuery[(__bridge id)kSecMatchLimit] = @2;
  227. CFArrayRef result = NULL;
  228. OSStatus status =
  229. SecItemCopyMatching((__bridge CFDictionaryRef)mutableQuery, (CFTypeRef *)&result);
  230. if (status == noErr && result != NULL) {
  231. NSArray *items = (__bridge_transfer NSArray *)result;
  232. if (items.count != 1) {
  233. if (outError) {
  234. *outError = [FIRAuthErrorUtils keychainErrorWithFunction:@"SecItemCopyMatching"
  235. status:status];
  236. }
  237. return nil;
  238. }
  239. if (outError) {
  240. *outError = nil;
  241. }
  242. NSDictionary *item = items[0];
  243. return item[(__bridge id)kSecValueData];
  244. }
  245. if (status == errSecItemNotFound) {
  246. if (outError) {
  247. *outError = nil;
  248. }
  249. } else {
  250. if (outError) {
  251. *outError = [FIRAuthErrorUtils keychainErrorWithFunction:@"SecItemCopyMatching"
  252. status:status];
  253. }
  254. }
  255. return nil;
  256. }
  257. - (BOOL)setItem:(NSData *)item
  258. withQuery:(NSDictionary *)query
  259. error:(NSError *_Nullable *_Nullable)outError {
  260. NSData *existingItem = [self getItemWithQuery:query error:outError];
  261. if (outError && *outError) {
  262. return NO;
  263. }
  264. OSStatus status;
  265. if (!existingItem) {
  266. NSMutableDictionary *queryWithItem = [query mutableCopy];
  267. [queryWithItem setObject:item forKey:(__bridge id)kSecValueData];
  268. status = SecItemAdd((__bridge CFDictionaryRef)queryWithItem, NULL);
  269. } else {
  270. NSDictionary *attributes = @{(__bridge id)kSecValueData : item};
  271. status = SecItemUpdate((__bridge CFDictionaryRef)query, (__bridge CFDictionaryRef)attributes);
  272. }
  273. if (status == noErr) {
  274. if (outError) {
  275. *outError = nil;
  276. }
  277. return YES;
  278. }
  279. NSString *function = existingItem ? @"SecItemUpdate" : @"SecItemAdd";
  280. if (outError) {
  281. *outError = [FIRAuthErrorUtils keychainErrorWithFunction:function status:status];
  282. }
  283. return NO;
  284. }
  285. - (BOOL)removeItemWithQuery:(NSDictionary *)query error:(NSError *_Nullable *_Nullable)outError {
  286. OSStatus status = SecItemDelete((__bridge CFDictionaryRef)query);
  287. if (status == noErr || status == errSecItemNotFound) {
  288. if (outError) {
  289. *outError = nil;
  290. }
  291. return YES;
  292. }
  293. if (outError) {
  294. *outError = [FIRAuthErrorUtils keychainErrorWithFunction:@"SecItemDelete" status:status];
  295. }
  296. return NO;
  297. }
  298. @end
  299. NS_ASSUME_NONNULL_END