FIRAuthKeychainServices.m 12 KB

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