FIRSecureStorage.m 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331
  1. /*
  2. * Copyright 2019 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 "FIRSecureStorage.h"
  17. #import <Security/Security.h>
  18. #if __has_include(<FBLPromises/FBLPromises.h>)
  19. #import <FBLPromises/FBLPromises.h>
  20. #else
  21. #import "FBLPromises.h"
  22. #endif
  23. #import "FIRInstallationsErrorUtil.h"
  24. @interface FIRSecureStorage ()
  25. @property(nonatomic, readonly) dispatch_queue_t keychainQueue;
  26. @property(nonatomic, readonly) dispatch_queue_t inMemoryCacheQueue;
  27. @property(nonatomic, readonly) NSString *service;
  28. @property(nonatomic, readonly) NSCache<NSString *, id<NSSecureCoding>> *inMemoryCache;
  29. @end
  30. @implementation FIRSecureStorage
  31. - (instancetype)init {
  32. NSCache *cache = [[NSCache alloc] init];
  33. // Cache up to 5 installations.
  34. cache.countLimit = 5;
  35. return [self initWithService:@"com.firebase.FIRInstallations.installations" cache:cache];
  36. }
  37. - (instancetype)initWithService:(NSString *)service cache:(NSCache *)cache {
  38. self = [super init];
  39. if (self) {
  40. _keychainQueue = dispatch_queue_create(
  41. "com.firebase.FIRInstallations.FIRSecureStorage.Keychain", DISPATCH_QUEUE_SERIAL);
  42. _inMemoryCacheQueue = dispatch_queue_create(
  43. "com.firebase.FIRInstallations.FIRSecureStorage.InMemoryCache", DISPATCH_QUEUE_SERIAL);
  44. _service = [service copy];
  45. _inMemoryCache = cache;
  46. }
  47. return self;
  48. }
  49. #pragma mark - Public
  50. - (FBLPromise<id<NSSecureCoding>> *)getObjectForKey:(NSString *)key
  51. objectClass:(Class)objectClass
  52. accessGroup:(nullable NSString *)accessGroup {
  53. return [FBLPromise onQueue:self.inMemoryCacheQueue
  54. do:^id _Nullable {
  55. // Return cached object or fail otherwise.
  56. id object = [self.inMemoryCache objectForKey:key];
  57. return object
  58. ?: [[NSError alloc]
  59. initWithDomain:FBLPromiseErrorDomain
  60. code:FBLPromiseErrorCodeValidationFailure
  61. userInfo:nil];
  62. }]
  63. .recover(^id _Nullable(NSError *error) {
  64. // Look for the object in the keychain.
  65. return [self getObjectFromKeychainForKey:key
  66. objectClass:objectClass
  67. accessGroup:accessGroup];
  68. });
  69. }
  70. - (FBLPromise<NSNull *> *)setObject:(id<NSSecureCoding>)object
  71. forKey:(NSString *)key
  72. accessGroup:(nullable NSString *)accessGroup {
  73. return [FBLPromise onQueue:self.inMemoryCacheQueue
  74. do:^id _Nullable {
  75. // Save to the in-memory cache first.
  76. [self.inMemoryCache setObject:object forKey:[key copy]];
  77. return [NSNull null];
  78. }]
  79. .thenOn(self.keychainQueue, ^id(id result) {
  80. // Then store the object to the keychain.
  81. NSDictionary *query = [self keychainQueryWithKey:key accessGroup:accessGroup];
  82. NSError *error;
  83. NSData *encodedObject = [self archiveDataForObject:object error:&error];
  84. if (!encodedObject) {
  85. return error;
  86. }
  87. if (![self setItem:encodedObject withQuery:query error:&error]) {
  88. return error;
  89. }
  90. return [NSNull null];
  91. });
  92. }
  93. - (FBLPromise<NSNull *> *)removeObjectForKey:(NSString *)key
  94. accessGroup:(nullable NSString *)accessGroup {
  95. return [FBLPromise onQueue:self.inMemoryCacheQueue
  96. do:^id _Nullable {
  97. [self.inMemoryCache removeObjectForKey:key];
  98. return nil;
  99. }]
  100. .thenOn(self.keychainQueue, ^id(id result) {
  101. NSDictionary *query = [self keychainQueryWithKey:key accessGroup:accessGroup];
  102. NSError *error;
  103. if (![self removeItemWithQuery:query error:&error]) {
  104. return error;
  105. }
  106. return [NSNull null];
  107. });
  108. }
  109. #pragma mark - Private
  110. - (FBLPromise<id<NSSecureCoding>> *)getObjectFromKeychainForKey:(NSString *)key
  111. objectClass:(Class)objectClass
  112. accessGroup:(nullable NSString *)accessGroup {
  113. // Look for the object in the keychain.
  114. return [FBLPromise onQueue:self.keychainQueue
  115. do:^id {
  116. NSDictionary *query = [self keychainQueryWithKey:key
  117. accessGroup:accessGroup];
  118. NSError *error;
  119. NSData *encodedObject = [self getItemWithQuery:query error:&error];
  120. if (error) {
  121. return error;
  122. }
  123. if (!encodedObject) {
  124. return nil;
  125. }
  126. id object = [self unarchivedObjectOfClass:objectClass
  127. fromData:encodedObject
  128. error:&error];
  129. if (error) {
  130. return error;
  131. }
  132. return object;
  133. }]
  134. .thenOn(self.inMemoryCacheQueue,
  135. ^id<NSSecureCoding> _Nullable(id<NSSecureCoding> _Nullable object) {
  136. // Save object to the in-memory cache if exists and return the object.
  137. if (object) {
  138. [self.inMemoryCache setObject:object forKey:[key copy]];
  139. }
  140. return object;
  141. });
  142. }
  143. - (void)resetInMemoryCache {
  144. [self.inMemoryCache removeAllObjects];
  145. }
  146. #pragma mark - Keychain
  147. - (NSMutableDictionary<NSString *, id> *)keychainQueryWithKey:(NSString *)key
  148. accessGroup:(nullable NSString *)accessGroup {
  149. NSMutableDictionary<NSString *, id> *query = [NSMutableDictionary dictionary];
  150. query[(__bridge NSString *)kSecClass] = (__bridge NSString *)kSecClassGenericPassword;
  151. query[(__bridge NSString *)kSecAttrService] = self.service;
  152. query[(__bridge NSString *)kSecAttrAccount] = key;
  153. if (accessGroup) {
  154. query[(__bridge NSString *)kSecAttrAccessGroup] = accessGroup;
  155. }
  156. #if TARGET_OS_OSX
  157. if (self.keychainRef) {
  158. query[(__bridge NSString *)kSecUseKeychain] = (__bridge id)(self.keychainRef);
  159. query[(__bridge NSString *)kSecMatchSearchList] = @[ (__bridge id)(self.keychainRef) ];
  160. }
  161. #endif // TARGET_OSX
  162. return query;
  163. }
  164. - (nullable NSData *)archiveDataForObject:(id<NSSecureCoding>)object error:(NSError **)outError {
  165. NSData *archiveData;
  166. if (@available(macOS 10.13, iOS 11.0, tvOS 11.0, *)) {
  167. NSError *error;
  168. archiveData = [NSKeyedArchiver archivedDataWithRootObject:object
  169. requiringSecureCoding:YES
  170. error:&error];
  171. if (error && outError) {
  172. *outError = [FIRInstallationsErrorUtil keyedArchiverErrorWithError:error];
  173. }
  174. } else {
  175. @try {
  176. NSMutableData *data = [NSMutableData data];
  177. NSKeyedArchiver *archiver = [[NSKeyedArchiver alloc] initForWritingWithMutableData:data];
  178. archiver.requiresSecureCoding = YES;
  179. [archiver encodeObject:object forKey:NSKeyedArchiveRootObjectKey];
  180. [archiver finishEncoding];
  181. archiveData = [data copy];
  182. } @catch (NSException *exception) {
  183. if (outError) {
  184. *outError = [FIRInstallationsErrorUtil keyedArchiverErrorWithException:exception];
  185. }
  186. }
  187. }
  188. return archiveData;
  189. }
  190. - (nullable id)unarchivedObjectOfClass:(Class)class
  191. fromData:(NSData *)data
  192. error:(NSError **)outError {
  193. id object;
  194. if (@available(macOS 10.13, iOS 11.0, tvOS 11.0, *)) {
  195. NSError *error;
  196. object = [NSKeyedUnarchiver unarchivedObjectOfClass:class fromData:data error:&error];
  197. if (error && outError) {
  198. *outError = [FIRInstallationsErrorUtil keyedArchiverErrorWithError:error];
  199. }
  200. } else {
  201. @try {
  202. NSKeyedUnarchiver *unarchiver = [[NSKeyedUnarchiver alloc] initForReadingWithData:data];
  203. unarchiver.requiresSecureCoding = YES;
  204. object = [unarchiver decodeObjectOfClass:class forKey:NSKeyedArchiveRootObjectKey];
  205. } @catch (NSException *exception) {
  206. if (outError) {
  207. *outError = [FIRInstallationsErrorUtil keyedArchiverErrorWithException:exception];
  208. }
  209. }
  210. }
  211. return object;
  212. }
  213. - (nullable NSData *)getItemWithQuery:(NSDictionary *)query
  214. error:(NSError *_Nullable *_Nullable)outError {
  215. NSMutableDictionary *mutableQuery = [query mutableCopy];
  216. mutableQuery[(__bridge id)kSecReturnData] = @YES;
  217. mutableQuery[(__bridge id)kSecMatchLimit] = (__bridge id)kSecMatchLimitOne;
  218. CFArrayRef result = NULL;
  219. OSStatus status =
  220. SecItemCopyMatching((__bridge CFDictionaryRef)mutableQuery, (CFTypeRef *)&result);
  221. if (status == noErr && result != NULL) {
  222. if (outError) {
  223. *outError = nil;
  224. }
  225. return (__bridge_transfer NSData *)result;
  226. }
  227. if (status == errSecItemNotFound) {
  228. if (outError) {
  229. *outError = nil;
  230. }
  231. } else {
  232. if (outError) {
  233. *outError = [FIRInstallationsErrorUtil keychainErrorWithFunction:@"SecItemCopyMatching"
  234. status:status];
  235. }
  236. }
  237. return nil;
  238. }
  239. - (BOOL)setItem:(NSData *)item
  240. withQuery:(NSDictionary *)query
  241. error:(NSError *_Nullable *_Nullable)outError {
  242. NSData *existingItem = [self getItemWithQuery:query error:outError];
  243. if (outError && *outError) {
  244. return NO;
  245. }
  246. NSMutableDictionary *mutableQuery = [query mutableCopy];
  247. mutableQuery[(__bridge id)kSecAttrAccessible] =
  248. (__bridge id)kSecAttrAccessibleAlwaysThisDeviceOnly;
  249. OSStatus status;
  250. if (!existingItem) {
  251. mutableQuery[(__bridge id)kSecValueData] = item;
  252. status = SecItemAdd((__bridge CFDictionaryRef)mutableQuery, NULL);
  253. } else {
  254. NSDictionary *attributes = @{(__bridge id)kSecValueData : item};
  255. status = SecItemUpdate((__bridge CFDictionaryRef)query, (__bridge CFDictionaryRef)attributes);
  256. }
  257. if (status == noErr) {
  258. if (outError) {
  259. *outError = nil;
  260. }
  261. return YES;
  262. }
  263. NSString *function = existingItem ? @"SecItemUpdate" : @"SecItemAdd";
  264. if (outError) {
  265. *outError = [FIRInstallationsErrorUtil keychainErrorWithFunction:function status:status];
  266. }
  267. return NO;
  268. }
  269. - (BOOL)removeItemWithQuery:(NSDictionary *)query error:(NSError *_Nullable *_Nullable)outError {
  270. OSStatus status = SecItemDelete((__bridge CFDictionaryRef)query);
  271. if (status == noErr || status == errSecItemNotFound) {
  272. if (outError) {
  273. *outError = nil;
  274. }
  275. return YES;
  276. }
  277. if (outError) {
  278. *outError = [FIRInstallationsErrorUtil keychainErrorWithFunction:@"SecItemDelete"
  279. status:status];
  280. }
  281. return NO;
  282. }
  283. @end