瀏覽代碼

Move FIRSecureStorage to GoogleUtilities (#5329)

* FIRSecureStorage -> GULKeychainStorage: renamed and moved to GoogleUtilities

* FIS updated to GULKeychainStorage

* Run ./scripts/style.sh

* Headers order

* GULKeychainUtils API docs

* GULKeychainStorage: require Keychain service name
Maksym Malyhin 6 年之前
父節點
當前提交
b2f22d8b21

+ 2 - 1
FirebaseInstallations.podspec

@@ -32,7 +32,8 @@ Pod::Spec.new do |s|
   s.framework = 'Security'
   s.dependency 'FirebaseCore', '~> 6.6'
   s.dependency 'PromisesObjC', '~> 1.2'
-  s.dependency 'GoogleUtilities/UserDefaults', '~> 6.5'
+  s.dependency 'GoogleUtilities/Environment', '~> 6.6'
+  s.dependency 'GoogleUtilities/UserDefaults', '~> 6.6'
 
   preprocessor_definitions = 'FIRInstallations_LIB_VERSION=' + String(s.version)
   if ENV['FIS_ALLOWS_INCOMPATIBLE_IID_VERSION'] && ENV['FIS_ALLOWS_INCOMPATIBLE_IID_VERSION'] == '1' then

+ 3 - 2
FirebaseInstallations/Source/Library/IIDMigration/FIRInstallationsIIDTokenStore.m

@@ -22,8 +22,9 @@
 #import "FBLPromises.h"
 #endif
 
+#import <GoogleUtilities/GULKeychainUtils.h>
+
 #import "FIRInstallationsErrorUtil.h"
-#import "FIRInstallationsKeychainUtils.h"
 
 static NSString *const kFIRInstallationsIIDTokenKeychainId = @"com.google.iid-tokens";
 
@@ -118,7 +119,7 @@ static NSString *const kFIRInstallationsIIDTokenKeychainId = @"com.google.iid-to
 
   NSMutableDictionary *keychainQuery = [self IIDDefaultTokenDataKeychainQuery];
   NSError *error;
-  NSData *data = [FIRInstallationsKeychainUtils getItemWithQuery:keychainQuery error:&error];
+  NSData *data = [GULKeychainUtils getItemWithQuery:keychainQuery error:&error];
 
   if (data) {
     [resultPromise fulfill:data];

+ 3 - 2
FirebaseInstallations/Source/Library/InstallationsIDController/FIRInstallationsIDController.m

@@ -23,6 +23,7 @@
 #endif
 
 #import <FirebaseCore/FIRAppInternal.h>
+#import <GoogleUtilities/GULKeychainStorage.h>
 
 #import "FIRInstallationsAPIService.h"
 #import "FIRInstallationsErrorUtil.h"
@@ -32,7 +33,6 @@
 #import "FIRInstallationsLogger.h"
 #import "FIRInstallationsSingleOperationPromiseCache.h"
 #import "FIRInstallationsStore.h"
-#import "FIRSecureStorage.h"
 
 #import "FIRInstallationsHTTPError.h"
 #import "FIRInstallationsStoredAuthToken.h"
@@ -72,7 +72,8 @@ NSTimeInterval const kFIRInstallationsTokenExpirationThreshold = 60 * 60;  // 1
                           projectID:(NSString *)projectID
                         GCMSenderID:(NSString *)GCMSenderID
                         accessGroup:(NSString *)accessGroup {
-  FIRSecureStorage *secureStorage = [[FIRSecureStorage alloc] init];
+  GULKeychainStorage *secureStorage =
+      [[GULKeychainStorage alloc] initWithService:@"com.firebase.FIRInstallations.installations"];
   FIRInstallationsStore *installationsStore =
       [[FIRInstallationsStore alloc] initWithSecureStorage:secureStorage accessGroup:accessGroup];
 

+ 2 - 2
FirebaseInstallations/Source/Library/InstallationsStore/FIRInstallationsStore.h

@@ -18,7 +18,7 @@
 
 @class FBLPromise<ValueType>;
 @class FIRInstallationsItem;
-@class FIRSecureStorage;
+@class GULKeychainStorage;
 
 NS_ASSUME_NONNULL_BEGIN
 
@@ -33,7 +33,7 @@ extern NSString *const kFIRInstallationsStoreUserDefaultsID;
  * @param storage The secure storage to save installations data.
  * @param accessGroup The Keychain Access Group to store and request the installations data.
  */
-- (instancetype)initWithSecureStorage:(FIRSecureStorage *)storage
+- (instancetype)initWithSecureStorage:(GULKeychainStorage *)storage
                           accessGroup:(nullable NSString *)accessGroup;
 
 /**

+ 4 - 3
FirebaseInstallations/Source/Library/InstallationsStore/FIRInstallationsStore.m

@@ -24,15 +24,16 @@
 #import "FBLPromises.h"
 #endif
 
+#import <GoogleUtilities/GULKeychainStorage.h>
+
 #import "FIRInstallationsErrorUtil.h"
 #import "FIRInstallationsItem.h"
 #import "FIRInstallationsStoredItem.h"
-#import "FIRSecureStorage.h"
 
 NSString *const kFIRInstallationsStoreUserDefaultsID = @"com.firebase.FIRInstallations";
 
 @interface FIRInstallationsStore ()
-@property(nonatomic, readonly) FIRSecureStorage *secureStorage;
+@property(nonatomic, readonly) GULKeychainStorage *secureStorage;
 @property(nonatomic, readonly, nullable) NSString *accessGroup;
 @property(nonatomic, readonly) dispatch_queue_t queue;
 @property(nonatomic, readonly) GULUserDefaults *userDefaults;
@@ -40,7 +41,7 @@ NSString *const kFIRInstallationsStoreUserDefaultsID = @"com.firebase.FIRInstall
 
 @implementation FIRInstallationsStore
 
-- (instancetype)initWithSecureStorage:(FIRSecureStorage *)storage
+- (instancetype)initWithSecureStorage:(GULKeychainStorage *)storage
                           accessGroup:(NSString *)accessGroup {
   self = [super init];
   if (self) {

+ 3 - 2
FirebaseInstallations/Source/Tests/Unit/FIRInstallationsStoreTests.m

@@ -18,14 +18,15 @@
 
 #import <OCMock/OCMock.h>
 
+#import <GoogleUtilities/GULKeychainStorage.h>
 #import <GoogleUtilities/GULUserDefaults.h>
+
 #import "FBLPromise+Testing.h"
 #import "FIRInstallationsErrorUtil.h"
 #import "FIRInstallationsItem+Tests.h"
 #import "FIRInstallationsItem.h"
 #import "FIRInstallationsStore.h"
 #import "FIRInstallationsStoredItem.h"
-#import "FIRSecureStorage.h"
 
 @interface FIRInstallationsStoreTests : XCTestCase
 @property(nonatomic) NSString *accessGroup;
@@ -38,7 +39,7 @@
 
 - (void)setUp {
   self.accessGroup = @"accessGroup";
-  self.mockSecureStorage = OCMClassMock([FIRSecureStorage class]);
+  self.mockSecureStorage = OCMClassMock([GULKeychainStorage class]);
   self.store = [[FIRInstallationsStore alloc] initWithSecureStorage:self.mockSecureStorage
                                                         accessGroup:self.accessGroup];
 

+ 3 - 1
GoogleUtilities.podspec

@@ -1,6 +1,6 @@
 Pod::Spec.new do |s|
   s.name             = 'GoogleUtilities'
-  s.version          = '6.5.2'
+  s.version          = '6.6.0'
   s.summary          = 'Google Utilities for iOS (plus community support for macOS and tvOS)'
 
   s.description      = <<-DESC
@@ -34,6 +34,8 @@ other Google CocoaPods. They're not intended for direct public usage.
     es.source_files = 'GoogleUtilities/Environment/**/*.[mh]'
     es.public_header_files = 'GoogleUtilities/Environment/**/*.h'
     es.private_header_files = 'GoogleUtilities/Environment/**/*.h'
+
+    es.dependency 'PromisesObjC', '~> 1.2'
   end
 
   s.subspec 'Logger' do |ls|

+ 9 - 1
FirebaseInstallations/Source/Library/SecureStorage/FIRSecureStorage.h → GoogleUtilities/Environment/Public/GULKeychainStorage.h

@@ -21,7 +21,15 @@
 NS_ASSUME_NONNULL_BEGIN
 
 /// The class provides a convenient abstraction on top of the iOS Keychain API to save data.
-@interface FIRSecureStorage : NSObject
+@interface GULKeychainStorage : NSObject
+
+- (instancetype)init NS_UNAVAILABLE;
+
+/** Initializes the keychain storage with Keychain Service name.
+ *  @param service A Keychain Service name that will be used to store and retrieve objects. See also
+ * `kSecAttrService`.
+ */
+- (instancetype)initWithService:(NSString *)service;
 
 /**
  * Get an object by key.

+ 61 - 0
GoogleUtilities/Environment/Public/GULKeychainUtils.h

@@ -0,0 +1,61 @@
+/*
+ * Copyright 2019 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+FOUNDATION_EXPORT NSString *const kGULKeychainUtilsErrorDomain;
+
+/// Helper functions to access Keychain.
+@interface GULKeychainUtils : NSObject
+
+/** Fetches a keychain item data matching to the provided query.
+ *  @param query A dictionary with Keychain query parameters. See docs for `SecItemCopyMatching` for
+ * details.
+ *  @param outError A pointer to `NSError` instance or `NULL`. The instance at `outError` will be
+ * assigned with an error if there is.
+ *  @returns Data for the first Keychain Item matching the provided query or `nil` if there is not
+ * such an item (`outError` will be `nil` in this case) or an error occurred.
+ */
++ (nullable NSData *)getItemWithQuery:(NSDictionary *)query
+                                error:(NSError *_Nullable *_Nullable)outError;
+
+/** Stores data to a Keychain Item matching to the provided query. An existing Keychain Item
+ * matching the query parameters will be updated or a new will be created.
+ *  @param item A Keychain Item data to store.
+ *  @param query A dictionary with Keychain query parameters. See docs for `SecItemAdd` and
+ * `SecItemUpdate` for details.
+ *  @param outError A pointer to `NSError` instance or `NULL`. The instance at `outError` will be
+ * assigned with an error if there is.
+ *  @returns `YES` when data was successfully stored, `NO` otherwise.
+ */
++ (BOOL)setItem:(NSData *)item
+      withQuery:(NSDictionary *)query
+          error:(NSError *_Nullable *_Nullable)outError;
+
+/** Removes a Keychain Item matching to the provided query.
+ *  @param query A dictionary with Keychain query parameters. See docs for `SecItemDelete` for
+ * details.
+ *  @param outError A pointer to `NSError` instance or `NULL`. The instance at `outError` will be
+ * assigned with an error if there is.
+ *  @returns `YES` if the item was removed successfully or doesn't exist, `NO` otherwise.
+ */
++ (BOOL)removeItemWithQuery:(NSDictionary *)query error:(NSError *_Nullable *_Nullable)outError;
+
+@end
+
+NS_ASSUME_NONNULL_END

+ 37 - 99
FirebaseInstallations/Source/Library/SecureStorage/FIRSecureStorage.m → GoogleUtilities/Environment/SecureStorage/GULKeychainStorage.m

@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-#import "FIRSecureStorage.h"
+#import "GULKeychainStorage.h"
 #import <Security/Security.h>
 
 #if __has_include(<FBLPromises/FBLPromises.h>)
@@ -23,32 +23,33 @@
 #import "FBLPromises.h"
 #endif
 
-#import "FIRInstallationsErrorUtil.h"
-#import "FIRInstallationsKeychainUtils.h"
+#import <GoogleUtilities/GULSecureCoding.h>
 
-@interface FIRSecureStorage ()
+#import "GULKeychainUtils.h"
+
+@interface GULKeychainStorage ()
 @property(nonatomic, readonly) dispatch_queue_t keychainQueue;
 @property(nonatomic, readonly) dispatch_queue_t inMemoryCacheQueue;
 @property(nonatomic, readonly) NSString *service;
 @property(nonatomic, readonly) NSCache<NSString *, id<NSSecureCoding>> *inMemoryCache;
 @end
 
-@implementation FIRSecureStorage
+@implementation GULKeychainStorage
 
-- (instancetype)init {
+- (instancetype)initWithService:(NSString *)service {
   NSCache *cache = [[NSCache alloc] init];
   // Cache up to 5 installations.
   cache.countLimit = 5;
-  return [self initWithService:@"com.firebase.FIRInstallations.installations" cache:cache];
+  return [self initWithService:service cache:cache];
 }
 
 - (instancetype)initWithService:(NSString *)service cache:(NSCache *)cache {
   self = [super init];
   if (self) {
-    _keychainQueue = dispatch_queue_create(
-        "com.firebase.FIRInstallations.FIRSecureStorage.Keychain", DISPATCH_QUEUE_SERIAL);
-    _inMemoryCacheQueue = dispatch_queue_create(
-        "com.firebase.FIRInstallations.FIRSecureStorage.InMemoryCache", DISPATCH_QUEUE_SERIAL);
+    _keychainQueue =
+        dispatch_queue_create("com.gul.KeychainStorage.Keychain", DISPATCH_QUEUE_SERIAL);
+    _inMemoryCacheQueue =
+        dispatch_queue_create("com.gul.KeychainStorage.InMemoryCache", DISPATCH_QUEUE_SERIAL);
     _service = [service copy];
     _inMemoryCache = cache;
   }
@@ -91,12 +92,12 @@
         // Then store the object to the keychain.
         NSDictionary *query = [self keychainQueryWithKey:key accessGroup:accessGroup];
         NSError *error;
-        NSData *encodedObject = [self archiveDataForObject:object error:&error];
+        NSData *encodedObject = [GULSecureCoding archivedDataWithRootObject:object error:&error];
         if (!encodedObject) {
           return error;
         }
 
-        if (![FIRInstallationsKeychainUtils setItem:encodedObject withQuery:query error:&error]) {
+        if (![GULKeychainUtils setItem:encodedObject withQuery:query error:&error]) {
           return error;
         }
 
@@ -115,7 +116,7 @@
         NSDictionary *query = [self keychainQueryWithKey:key accessGroup:accessGroup];
 
         NSError *error;
-        if (![FIRInstallationsKeychainUtils removeItemWithQuery:query error:&error]) {
+        if (![GULKeychainUtils removeItemWithQuery:query error:&error]) {
           return error;
         }
 
@@ -129,29 +130,28 @@
                                                     objectClass:(Class)objectClass
                                                     accessGroup:(nullable NSString *)accessGroup {
   // Look for the object in the keychain.
-  return [FBLPromise onQueue:self.keychainQueue
-                          do:^id {
-                            NSDictionary *query = [self keychainQueryWithKey:key
-                                                                 accessGroup:accessGroup];
-                            NSError *error;
-                            NSData *encodedObject =
-                                [FIRInstallationsKeychainUtils getItemWithQuery:query error:&error];
-
-                            if (error) {
-                              return error;
-                            }
-                            if (!encodedObject) {
-                              return nil;
-                            }
-                            id object = [self unarchivedObjectOfClass:objectClass
-                                                             fromData:encodedObject
-                                                                error:&error];
-                            if (error) {
-                              return error;
-                            }
-
-                            return object;
-                          }]
+  return [FBLPromise
+             onQueue:self.keychainQueue
+                  do:^id {
+                    NSDictionary *query = [self keychainQueryWithKey:key accessGroup:accessGroup];
+                    NSError *error;
+                    NSData *encodedObject = [GULKeychainUtils getItemWithQuery:query error:&error];
+
+                    if (error) {
+                      return error;
+                    }
+                    if (!encodedObject) {
+                      return nil;
+                    }
+                    id object = [GULSecureCoding unarchivedObjectOfClass:objectClass
+                                                                fromData:encodedObject
+                                                                   error:&error];
+                    if (error) {
+                      return error;
+                    }
+
+                    return object;
+                  }]
       .thenOn(self.inMemoryCacheQueue,
               ^id<NSSecureCoding> _Nullable(id<NSSecureCoding> _Nullable object) {
                 // Save object to the in-memory cache if exists and return the object.
@@ -190,66 +190,4 @@
   return query;
 }
 
-- (nullable NSData *)archiveDataForObject:(id<NSSecureCoding>)object error:(NSError **)outError {
-  NSData *archiveData;
-  if (@available(macOS 10.13, iOS 11.0, tvOS 11.0, *)) {
-    NSError *error;
-    archiveData = [NSKeyedArchiver archivedDataWithRootObject:object
-                                        requiringSecureCoding:YES
-                                                        error:&error];
-    if (error && outError) {
-      *outError = [FIRInstallationsErrorUtil keyedArchiverErrorWithError:error];
-    }
-  } else {
-    @try {
-      NSMutableData *data = [NSMutableData data];
-#pragma clang diagnostic push
-#pragma clang diagnostic ignored "-Wdeprecated-declarations"
-      NSKeyedArchiver *archiver = [[NSKeyedArchiver alloc] initForWritingWithMutableData:data];
-#pragma clang diagnostic pop
-      archiver.requiresSecureCoding = YES;
-
-      [archiver encodeObject:object forKey:NSKeyedArchiveRootObjectKey];
-      [archiver finishEncoding];
-
-      archiveData = [data copy];
-    } @catch (NSException *exception) {
-      if (outError) {
-        *outError = [FIRInstallationsErrorUtil keyedArchiverErrorWithException:exception];
-      }
-    }
-  }
-
-  return archiveData;
-}
-
-- (nullable id)unarchivedObjectOfClass:(Class)class
-                              fromData:(NSData *)data
-                                 error:(NSError **)outError {
-  id object;
-  if (@available(macOS 10.13, iOS 11.0, tvOS 11.0, *)) {
-    NSError *error;
-    object = [NSKeyedUnarchiver unarchivedObjectOfClass:class fromData:data error:&error];
-    if (error && outError) {
-      *outError = [FIRInstallationsErrorUtil keyedArchiverErrorWithError:error];
-    }
-  } else {
-    @try {
-#pragma clang diagnostic push
-#pragma clang diagnostic ignored "-Wdeprecated-declarations"
-      NSKeyedUnarchiver *unarchiver = [[NSKeyedUnarchiver alloc] initForReadingWithData:data];
-#pragma clang diagnostic pop
-      unarchiver.requiresSecureCoding = YES;
-
-      object = [unarchiver decodeObjectOfClass:class forKey:NSKeyedArchiveRootObjectKey];
-    } @catch (NSException *exception) {
-      if (outError) {
-        *outError = [FIRInstallationsErrorUtil keyedArchiverErrorWithException:exception];
-      }
-    }
-  }
-
-  return object;
-}
-
 @end

+ 14 - 8
FirebaseInstallations/Source/Library/SecureStorage/FIRInstallationsKeychainUtils.m → GoogleUtilities/Environment/SecureStorage/GULKeychainUtils.m

@@ -14,11 +14,11 @@
  * limitations under the License.
  */
 
-#import "FIRInstallationsKeychainUtils.h"
+#import "GULKeychainUtils.h"
 
-#import "FIRInstallationsErrorUtil.h"
+NSString *const kGULKeychainUtilsErrorDomain = @"com.gul.keychain.ErrorDomain";
 
-@implementation FIRInstallationsKeychainUtils
+@implementation GULKeychainUtils
 
 + (nullable NSData *)getItemWithQuery:(NSDictionary *)query
                                 error:(NSError *_Nullable *_Nullable)outError {
@@ -45,8 +45,7 @@
     }
   } else {
     if (outError) {
-      *outError = [FIRInstallationsErrorUtil keychainErrorWithFunction:@"SecItemCopyMatching"
-                                                                status:status];
+      *outError = [self keychainErrorWithFunction:@"SecItemCopyMatching" status:status];
     }
   }
   return nil;
@@ -82,7 +81,7 @@
 
   NSString *function = existingItem ? @"SecItemUpdate" : @"SecItemAdd";
   if (outError) {
-    *outError = [FIRInstallationsErrorUtil keychainErrorWithFunction:function status:status];
+    *outError = [self keychainErrorWithFunction:function status:status];
   }
   return NO;
 }
@@ -98,10 +97,17 @@
   }
 
   if (outError) {
-    *outError = [FIRInstallationsErrorUtil keychainErrorWithFunction:@"SecItemDelete"
-                                                              status:status];
+    *outError = [self keychainErrorWithFunction:@"SecItemDelete" status:status];
   }
   return NO;
 }
 
+#pragma mark - Errors
+
++ (NSError *)keychainErrorWithFunction:(NSString *)keychainFunction status:(OSStatus)status {
+  NSString *failureReason = [NSString stringWithFormat:@"%@ (%li)", keychainFunction, (long)status];
+  NSDictionary *userInfo = @{NSLocalizedFailureReasonErrorKey : failureReason};
+  return [NSError errorWithDomain:kGULKeychainUtilsErrorDomain code:0 userInfo:userInfo];
+}
+
 @end

+ 10 - 10
FirebaseInstallations/Source/Tests/Unit/FIRSecureStorageTests.m → GoogleUtilities/Example/Tests/Environment/GULKeychainStorageTests.m

@@ -18,36 +18,36 @@
 
 #import <OCMock/OCMock.h>
 #import "FBLPromise+Testing.h"
-#import "FIRTestKeychain.h"
+#import "GULTestKeychain.h"
 
-#import "FIRSecureStorage.h"
+#import "GULKeychainStorage.h"
 
-@interface FIRSecureStorage (Tests)
+@interface GULKeychainStorage (Tests)
 - (instancetype)initWithService:(NSString *)service cache:(NSCache *)cache;
 - (void)resetInMemoryCache;
 @end
 
-@interface FIRSecureStorageTests : XCTestCase
-@property(nonatomic, strong) FIRSecureStorage *storage;
+@interface GULKeychainStorageTests : XCTestCase
+@property(nonatomic, strong) GULKeychainStorage *storage;
 @property(nonatomic, strong) NSCache *cache;
 @property(nonatomic, strong) id mockCache;
 
 #if TARGET_OS_OSX
-@property(nonatomic) FIRTestKeychain *privateKeychain;
+@property(nonatomic) GULTestKeychain *privateKeychain;
 #endif  // TARGET_OSX
 
 @end
 
-@implementation FIRSecureStorageTests
+@implementation GULKeychainStorageTests
 
 - (void)setUp {
   self.cache = [[NSCache alloc] init];
   self.mockCache = OCMPartialMock(self.cache);
-  self.storage = [[FIRSecureStorage alloc] initWithService:@"com.tests.FIRSecureStorageTests"
-                                                     cache:self.mockCache];
+  self.storage = [[GULKeychainStorage alloc] initWithService:@"com.tests.GULKeychainStorageTests"
+                                                       cache:self.mockCache];
 
 #if TARGET_OS_OSX
-  self.privateKeychain = [[FIRTestKeychain alloc] init];
+  self.privateKeychain = [[GULTestKeychain alloc] init];
   self.storage.keychainRef = self.privateKeychain.testKeychainRef;
 #endif  // TARGET_OSX
 }

+ 7 - 9
FirebaseInstallations/Source/Library/SecureStorage/FIRInstallationsKeychainUtils.h → GoogleUtilities/Example/Tests/Utils/GULTestKeychain.h

@@ -13,23 +13,21 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-
 #import <Foundation/Foundation.h>
+#import <Security/Security.h>
 
 NS_ASSUME_NONNULL_BEGIN
 
-/// Helper functions to access Keychain.
-@interface FIRInstallationsKeychainUtils : NSObject
+#if TARGET_OS_OSX
 
-+ (nullable NSData *)getItemWithQuery:(NSDictionary *)query
-                                error:(NSError *_Nullable *_Nullable)outError;
+@interface GULTestKeychain : NSObject
 
-+ (BOOL)setItem:(NSData *)item
-      withQuery:(NSDictionary *)query
-          error:(NSError *_Nullable *_Nullable)outError;
+- (nullable instancetype)init;
 
-+ (BOOL)removeItemWithQuery:(NSDictionary *)query error:(NSError *_Nullable *_Nullable)outError;
+@property(nonatomic, readonly, nullable) SecKeychainRef testKeychainRef;
 
 @end
 
+#endif  // TARGET_OSX
+
 NS_ASSUME_NONNULL_END

+ 64 - 0
GoogleUtilities/Example/Tests/Utils/GULTestKeychain.m

@@ -0,0 +1,64 @@
+/*
+ * Copyright 2019 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#if TARGET_OS_OSX
+
+#import "GULTestKeychain.h"
+
+#import <XCTest/XCTest.h>
+
+@implementation GULTestKeychain
+
+- (nullable instancetype)init {
+  self = [super init];
+  if (self) {
+    SecKeychainRef privateKeychain;
+    NSString *keychainPath =
+        [NSTemporaryDirectory() stringByAppendingPathComponent:@"GULTestKeychain"];
+    if ([[NSFileManager defaultManager] fileExistsAtPath:keychainPath]) {
+      NSError *error;
+      if (![[NSFileManager defaultManager] removeItemAtPath:keychainPath error:&error]) {
+        NSLog(@"Failed to delete existing test keychain: %@", error);
+        return nil;
+      }
+    }
+    OSStatus result = SecKeychainCreate([keychainPath cStringUsingEncoding:NSUTF8StringEncoding], 0,
+                                        "1", false, nil, &privateKeychain);
+    if (result != errSecSuccess) {
+      NSError *error = [NSError errorWithDomain:NSOSStatusErrorDomain code:result userInfo:nil];
+      NSLog(@"SecKeychainCreate error: %@", error);
+      return nil;
+    }
+    _testKeychainRef = privateKeychain;
+  }
+  return self;
+}
+
+- (void)dealloc {
+  if (self.testKeychainRef) {
+    OSStatus result = SecKeychainDelete(self.testKeychainRef);
+    if (result != errSecSuccess) {
+      NSError *error = [NSError errorWithDomain:NSOSStatusErrorDomain code:result userInfo:nil];
+      NSLog(@"SecKeychainCreate error: %@", error);
+    }
+
+    CFRelease(self.testKeychainRef);
+  }
+}
+
+@end
+
+#endif  // TARGET_OSX