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.

327 lines
10 KiB

6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
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. /** @var kAccountPrefix
  21. @brief The prefix string for keychain item account attribute before the key.
  22. @remarks A number "1" is encoded in the prefix in case we need to upgrade the scheme in future.
  23. */
  24. static NSString *const kAccountPrefix = @"firebase_auth_1_";
  25. NS_ASSUME_NONNULL_BEGIN
  26. @implementation FIRAuthKeychain {
  27. /** @var _service
  28. @brief The name of the keychain service.
  29. */
  30. NSString *_service;
  31. /** @var _legacyItemDeletedForKey
  32. @brief Indicates whether or not this class knows that the legacy item for a particular key has
  33. been deleted.
  34. @remarks This dictionary is to avoid unecessary keychain operations against legacy items.
  35. */
  36. NSMutableDictionary *_legacyEntryDeletedForKey;
  37. }
  38. - (id<FIRAuthStorage>)initWithService:(NSString *)service {
  39. self = [super init];
  40. if (self) {
  41. _service = [service copy];
  42. _legacyEntryDeletedForKey = [[NSMutableDictionary alloc] init];
  43. }
  44. return self;
  45. }
  46. - (nullable NSData *)dataForKey:(NSString *)key error:(NSError **_Nullable)error {
  47. if (!key.length) {
  48. [NSException raise:NSInvalidArgumentException
  49. 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
  82. 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
  96. format:@"%@", @"The key cannot be nil or empty."];
  97. return NO;
  98. }
  99. if (![self deleteItemWithQuery:[self genericPasswordQueryWithKey:key] error:error]) {
  100. return NO;
  101. }
  102. // Legacy form item, if exists, also needs to be removed, otherwise it will be exposed when
  103. // current form item is removed, leading to incorrect semantics.
  104. [self deleteLegacyItemWithKey:key];
  105. return YES;
  106. }
  107. #pragma mark - Private methods for non-sharing keychain operations
  108. - (nullable NSData *)itemWithQuery:(NSDictionary *)query error:(NSError **_Nullable)error {
  109. NSMutableDictionary *returningQuery = [query mutableCopy];
  110. returningQuery[(__bridge id)kSecReturnData] = @YES;
  111. returningQuery[(__bridge id)kSecReturnAttributes] = @YES;
  112. // Using a match limit of 2 means that we can check whether there is more than one item.
  113. // If we used a match limit of 1 we would never find out.
  114. returningQuery[(__bridge id)kSecMatchLimit] = @2;
  115. CFArrayRef result = NULL;
  116. OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)returningQuery,
  117. (CFTypeRef *)&result);
  118. if (status == noErr && result != NULL) {
  119. NSArray *items = (__bridge_transfer NSArray *)result;
  120. if (items.count != 1) {
  121. if (error) {
  122. *error = [FIRAuthErrorUtils keychainErrorWithFunction:@"SecItemCopyMatching"
  123. status:status];
  124. }
  125. return nil;
  126. }
  127. if (error) {
  128. *error = nil;
  129. }
  130. NSDictionary *item = items[0];
  131. return item[(__bridge id)kSecValueData];
  132. }
  133. if (status == errSecItemNotFound) {
  134. if (error) {
  135. *error = nil;
  136. }
  137. } else {
  138. if (error) {
  139. *error = [FIRAuthErrorUtils keychainErrorWithFunction:@"SecItemCopyMatching" status:status];
  140. }
  141. }
  142. return nil;
  143. }
  144. - (BOOL)setItemWithQuery:(NSDictionary *)query
  145. attributes:(NSDictionary *)attributes
  146. error:(NSError **_Nullable)error {
  147. NSMutableDictionary *combined = [attributes mutableCopy];
  148. [combined addEntriesFromDictionary:query];
  149. BOOL hasItem = NO;
  150. OSStatus status = SecItemAdd((__bridge CFDictionaryRef)combined, NULL);
  151. if (status == errSecDuplicateItem) {
  152. hasItem = YES;
  153. status = SecItemUpdate((__bridge CFDictionaryRef)query, (__bridge CFDictionaryRef)attributes);
  154. }
  155. if (status == noErr) {
  156. return YES;
  157. }
  158. if (error) {
  159. NSString *function = hasItem ? @"SecItemUpdate" : @"SecItemAdd";
  160. *error = [FIRAuthErrorUtils keychainErrorWithFunction:function status:status];
  161. }
  162. return NO;
  163. }
  164. - (BOOL)deleteItemWithQuery:(NSDictionary *)query error:(NSError **_Nullable)error {
  165. OSStatus status = SecItemDelete((__bridge CFDictionaryRef)query);
  166. if (status == noErr || status == errSecItemNotFound) {
  167. return YES;
  168. }
  169. if (error) {
  170. *error = [FIRAuthErrorUtils keychainErrorWithFunction:@"SecItemDelete" status:status];
  171. }
  172. return NO;
  173. }
  174. /** @fn deleteLegacyItemsWithKey:
  175. @brief Deletes legacy item from the keychain if it is not already known to be deleted.
  176. @param key The key for the item.
  177. */
  178. - (void)deleteLegacyItemWithKey:(NSString *)key {
  179. if (_legacyEntryDeletedForKey[key]) {
  180. return;
  181. }
  182. NSDictionary *query = [self legacyGenericPasswordQueryWithKey:key];
  183. SecItemDelete((__bridge CFDictionaryRef)query);
  184. _legacyEntryDeletedForKey[key] = @YES;
  185. }
  186. /** @fn genericPasswordQueryWithKey:
  187. @brief Returns a keychain query of generic password to be used to manipulate key'ed value.
  188. @param key The key for the value being manipulated, used as the account field in the query.
  189. */
  190. - (NSDictionary *)genericPasswordQueryWithKey:(NSString *)key {
  191. return @{
  192. (__bridge id)kSecClass : (__bridge id)kSecClassGenericPassword,
  193. (__bridge id)kSecAttrAccount : [kAccountPrefix stringByAppendingString:key],
  194. (__bridge id)kSecAttrService : _service,
  195. };
  196. }
  197. /** @fn legacyGenericPasswordQueryWithKey:
  198. @brief Returns a keychain query of generic password without service field, which is used by
  199. previous version of this class.
  200. @param key The key for the value being manipulated, used as the account field in the query.
  201. */
  202. - (NSDictionary *)legacyGenericPasswordQueryWithKey:(NSString *)key {
  203. return @{
  204. (__bridge id)kSecClass : (__bridge id)kSecClassGenericPassword,
  205. (__bridge id)kSecAttrAccount : key,
  206. };
  207. }
  208. #pragma mark - Private methods for shared keychain operations
  209. - (nullable NSData *)getItemWithQuery:(NSDictionary *)query
  210. error:(NSError *_Nullable *_Nullable)outError {
  211. NSMutableDictionary *mutableQuery = [query mutableCopy];
  212. mutableQuery[(__bridge id)kSecReturnData] = @YES;
  213. mutableQuery[(__bridge id)kSecReturnAttributes] = @YES;
  214. mutableQuery[(__bridge id)kSecMatchLimit] = @2;
  215. CFArrayRef result = NULL;
  216. OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)mutableQuery,
  217. (CFTypeRef *)&result);
  218. if (status == noErr && result != NULL) {
  219. NSArray *items = (__bridge_transfer NSArray *)result;
  220. if (items.count != 1) {
  221. if (outError) {
  222. *outError = [FIRAuthErrorUtils keychainErrorWithFunction:@"SecItemCopyMatching"
  223. status:status];
  224. }
  225. return nil;
  226. }
  227. if (outError) {
  228. *outError = nil;
  229. }
  230. NSDictionary *item = items[0];
  231. return item[(__bridge id)kSecValueData];
  232. }
  233. if (status == errSecItemNotFound) {
  234. if (outError) {
  235. *outError = nil;
  236. }
  237. } else {
  238. if (outError) {
  239. *outError = [FIRAuthErrorUtils keychainErrorWithFunction:@"SecItemCopyMatching" status:status];
  240. }
  241. }
  242. return nil;
  243. }
  244. - (BOOL)setItem:(NSData *)item
  245. withQuery:(NSDictionary *)query
  246. error:(NSError *_Nullable *_Nullable)outError {
  247. NSData *existingItem = [self getItemWithQuery:query error:outError];
  248. if (outError && *outError) {
  249. return NO;
  250. }
  251. OSStatus status;
  252. if (!existingItem) {
  253. NSMutableDictionary *queryWithItem = [query mutableCopy];
  254. [queryWithItem setObject:item forKey:(__bridge id)kSecValueData];
  255. status = SecItemAdd((__bridge CFDictionaryRef)queryWithItem, NULL);
  256. } else {
  257. NSDictionary *attributes = @{(__bridge id)kSecValueData: item};
  258. status = SecItemUpdate((__bridge CFDictionaryRef)query, (__bridge CFDictionaryRef)attributes);
  259. }
  260. if (status == noErr) {
  261. if (outError) {
  262. *outError = nil;
  263. }
  264. return YES;
  265. }
  266. NSString *function = existingItem ? @"SecItemUpdate" : @"SecItemAdd";
  267. if (outError) {
  268. *outError = [FIRAuthErrorUtils keychainErrorWithFunction:function status:status];
  269. }
  270. return NO;
  271. }
  272. - (BOOL)removeItemWithQuery:(NSDictionary *)query error:(NSError *_Nullable *_Nullable)outError {
  273. OSStatus status = SecItemDelete((__bridge CFDictionaryRef)query);
  274. if (status == noErr || status == errSecItemNotFound) {
  275. if (outError) {
  276. *outError = nil;
  277. }
  278. return YES;
  279. }
  280. if (outError) {
  281. *outError = [FIRAuthErrorUtils keychainErrorWithFunction:@"SecItemDelete" status:status];
  282. }
  283. return NO;
  284. }
  285. @end
  286. NS_ASSUME_NONNULL_END