| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416 |
- /*
- * 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 <TargetConditionals.h>
- #if !TARGET_OS_MACCATALYST
- // Skip keychain tests on Catalyst.
- #import <XCTest/XCTest.h>
- #import <OCMock/OCMock.h>
- #import "Firebase/InstanceID/FIRInstanceIDAuthKeyChain.h"
- #import "Firebase/InstanceID/FIRInstanceIDCheckinPreferences+Internal.h"
- #import "Firebase/InstanceID/FIRInstanceIDTokenInfo.h"
- static NSString *const kFIRInstanceIDTestKeychainId = @"com.google.iid-tests";
- static NSString *const kAuthorizedEntity = @"test-audience";
- static NSString *const kScope = @"test-scope";
- static NSString *const kToken1 =
- @"dOr37DpYQ9M:APA91bE5aQ2expDEmoSNDDrZqS6drAz2V-GHJHEsa-qVdlHXVSlWpUsK-Ta6Oe1QsVSLovL7_"
- @"rbm8GNnP7XPfwjtDQrjxYS1BdtxHdVVnQKuxlF3Z0QOwL380l1e1Fz91PX5b77XKj0FIyqzX1z0uJc0-pM6YcaPGg";
- #if TARGET_OS_IOS || TARGET_OS_TV
- static NSString *const kAuthID = @"test-auth-id";
- static NSString *const kSecret = @"test-secret";
- static NSString *const kToken2 = @"c8oEXUYIl3s:APA91bHtJMs_dZ2lXYXIcwsC47abYIuWhEJ_CshY2PJRjVuI_"
- @"H659iYUwfmNNghnZVkCmeUdKDSrK8xqVb0PVHxyAW391Ynp2NchMB87kJWb3BS0z"
- @"ud6Ej_xDES_oc353eFRvt0E6NXefDmrUCpBY8y89_1eVFFfiA";
- #endif
- static NSString *const kFirebaseAppID = @"abcdefg:ios:QrjxYS1BdtxHdVVnQKuxlF3Z0QO";
- static NSString *const kBundleID1 = @"com.google.fcm.dev";
- static NSString *const kBundleID2 = @"com.google.abtesting.dev";
- @interface FIRInstanceIDAuthKeychain (ExposedForTest)
- @property(nonatomic, copy)
- NSMutableDictionary<NSString *, NSMutableDictionary<NSString *, NSArray<NSData *> *> *>
- *cachedKeychainData;
- - (NSMutableDictionary *)keychainQueryForService:(NSString *)service account:(NSString *)account;
- @end
- @interface FIRInstanceIDAuthKeyChainTest : XCTestCase
- @end
- @implementation FIRInstanceIDAuthKeyChainTest
- - (void)setUp {
- [super setUp];
- }
- - (void)tearDown {
- [super tearDown];
- }
- - (void)testKeyChainNoCorruptionWithUniqueAccount {
- // macOS only support one service and one account.
- #if TARGET_OS_IOS || TARGET_OS_TV
- XCTestExpectation *noCurruptionExpectation =
- [self expectationWithDescription:@"No corruption between different accounts."];
- // Create a keychain with a service and a unique account
- NSString *service = [NSString stringWithFormat:@"%@:%@", kAuthorizedEntity, kScope];
- NSString *account1 = kBundleID1;
- NSData *tokenInfoData1 = [self tokenDataWithAuthorizedEntity:kAuthorizedEntity
- scope:kScope
- token:kToken1];
- FIRInstanceIDAuthKeychain *keychain =
- [[FIRInstanceIDAuthKeychain alloc] initWithIdentifier:kFIRInstanceIDTestKeychainId];
- __weak FIRInstanceIDAuthKeychain *weakKeychain = keychain;
- [keychain setData:tokenInfoData1
- forService:service
- account:account1
- handler:^(NSError *error) {
- XCTAssertNil(error);
- // Create another keychain with the same service but different account.
- NSString *account2 = kBundleID2;
- NSData *tokenInfoData2 = [self tokenDataWithAuthorizedEntity:kAuthorizedEntity
- scope:kScope
- token:kToken2];
- [weakKeychain
- setData:tokenInfoData2
- forService:service
- account:account2
- handler:^(NSError *error) {
- XCTAssertNil(error);
- // Now query the token and compare, they should not corrupt
- // each other.
- NSData *data1 = [weakKeychain dataForService:service account:account1];
- #pragma clang diagnostic push
- #pragma clang diagnostic ignored "-Wdeprecated-declarations"
- FIRInstanceIDTokenInfo *tokenInfo1 =
- [NSKeyedUnarchiver unarchiveObjectWithData:data1];
- XCTAssertEqualObjects(kToken1, tokenInfo1.token);
- NSData *data2 = [weakKeychain dataForService:service account:account2];
- FIRInstanceIDTokenInfo *tokenInfo2 =
- [NSKeyedUnarchiver unarchiveObjectWithData:data2];
- #pragma clang diagnostic pop
- XCTAssertEqualObjects(kToken2, tokenInfo2.token);
- // Also check the cache data.
- XCTAssertEqual(weakKeychain.cachedKeychainData.count, 1);
- XCTAssertEqual(weakKeychain.cachedKeychainData[service].count, 2);
- XCTAssertEqualObjects(
- weakKeychain.cachedKeychainData[service][account1].firstObject,
- tokenInfoData1);
- XCTAssertEqualObjects(
- weakKeychain.cachedKeychainData[service][account2].firstObject,
- tokenInfoData2);
- // Check wildcard query
- NSArray *results = [weakKeychain itemsMatchingService:service account:@"*"];
- XCTAssertEqual(results.count, 2);
- // Clean up keychain at the end
- [weakKeychain removeItemsMatchingService:service
- account:@"*"
- handler:^(NSError *_Nonnull error) {
- XCTAssertNil(error);
- [noCurruptionExpectation fulfill];
- }];
- }];
- }];
- [self waitForExpectationsWithTimeout:1.0 handler:NULL];
- #endif
- }
- - (void)testKeyChainNoCorruptionWithUniqueService {
- #if TARGET_OS_IOS || TARGET_OS_TV
- XCTestExpectation *noCurruptionExpectation =
- [self expectationWithDescription:@"No corruption between different services."];
- // Create a keychain with a service and a unique account
- NSString *service1 = [NSString stringWithFormat:@"%@:%@", kAuthorizedEntity, kScope];
- NSString *account = kBundleID1;
- NSData *tokenData = [self tokenDataWithAuthorizedEntity:kAuthorizedEntity
- scope:kScope
- token:kToken1];
- FIRInstanceIDAuthKeychain *keychain =
- [[FIRInstanceIDAuthKeychain alloc] initWithIdentifier:kFIRInstanceIDTestKeychainId];
- __weak FIRInstanceIDAuthKeychain *weakKeychain = keychain;
- [keychain
- setData:tokenData
- forService:service1
- account:account
- handler:^(NSError *error) {
- XCTAssertNil(error);
- // Store a checkin info using the same keychain account, but different service.
- NSString *service2 = @"com.google.iid.checkin";
- FIRInstanceIDCheckinPreferences *preferences =
- [[FIRInstanceIDCheckinPreferences alloc] initWithDeviceID:kAuthID
- secretToken:kSecret];
- NSString *checkinKeychainContent = [preferences checkinKeychainContent];
- NSData *checkinData = [checkinKeychainContent dataUsingEncoding:NSUTF8StringEncoding];
- [weakKeychain
- setData:checkinData
- forService:service2
- account:account
- handler:^(NSError *error) {
- XCTAssertNil(error);
- // Now query the token and compare, they should not corrupt
- // each other.
- NSData *data1 = [weakKeychain dataForService:service1 account:account];
- #pragma clang diagnostic push
- #pragma clang diagnostic ignored "-Wdeprecated-declarations"
- FIRInstanceIDTokenInfo *tokenInfo1 =
- [NSKeyedUnarchiver unarchiveObjectWithData:data1];
- #pragma clang diagnostic pop
- XCTAssertEqualObjects(kToken1, tokenInfo1.token);
- NSData *data2 = [weakKeychain dataForService:service2 account:account];
- NSString *checkinKeychainContent =
- [[NSString alloc] initWithData:data2 encoding:NSUTF8StringEncoding];
- FIRInstanceIDCheckinPreferences *checkinPreferences =
- [FIRInstanceIDCheckinPreferences
- preferencesFromKeychainContents:checkinKeychainContent];
- XCTAssertEqualObjects(checkinPreferences.secretToken, kSecret);
- XCTAssertEqualObjects(checkinPreferences.deviceID, kAuthID);
- NSArray *results = [weakKeychain itemsMatchingService:@"*" account:account];
- XCTAssertEqual(results.count, 2);
- // Also check the cache data.
- XCTAssertEqual(weakKeychain.cachedKeychainData.count, 2);
- XCTAssertEqualObjects(
- weakKeychain.cachedKeychainData[service1][account].firstObject, tokenData);
- XCTAssertEqualObjects(
- weakKeychain.cachedKeychainData[service2][account].firstObject,
- checkinData);
- // Clean up keychain at the end
- [weakKeychain removeItemsMatchingService:@"*"
- account:@"*"
- handler:^(NSError *_Nonnull error) {
- XCTAssertNil(error);
- [noCurruptionExpectation fulfill];
- }];
- }];
- }];
- [self waitForExpectationsWithTimeout:1.0 handler:NULL];
- #endif
- }
- - (void)testQueryCachedKeychainItems {
- XCTestExpectation *addItemToKeychainExpectation =
- [self expectationWithDescription:@"Test added item should be cached properly"];
- // A wildcard query should return empty data when there's nothing in keychain
- FIRInstanceIDAuthKeychain *keychain =
- [[FIRInstanceIDAuthKeychain alloc] initWithIdentifier:kFIRInstanceIDTestKeychainId];
- id keychainMock = OCMPartialMock(keychain);
- NSArray *result = [keychain itemsMatchingService:@"*" account:@"*"];
- XCTAssertEqual(result.count, 0);
- // Create a keychain item
- NSString *service = [NSString stringWithFormat:@"%@:%@", kAuthorizedEntity, kScope];
- NSString *account = kBundleID1;
- NSData *tokenData = [self tokenDataWithAuthorizedEntity:kAuthorizedEntity
- scope:kScope
- token:kToken1];
- __weak FIRInstanceIDAuthKeychain *weakKeychain = keychain;
- __weak id weakKeychainMock = keychainMock;
- [keychain setData:tokenData
- forService:service
- account:account
- handler:^(NSError *error) {
- XCTAssertNil(error);
- // Now if we clean the cache
- [weakKeychain.cachedKeychainData removeAllObjects];
- // Then query the item should fetch from keychain.
- NSData *data = [weakKeychain dataForService:service account:account];
- XCTAssertEqualObjects(data, tokenData);
- // Verify we fetch from keychain by calling to get the query
- OCMVerify([weakKeychainMock keychainQueryForService:service account:account]);
- // Cache should now have the query item
- XCTAssertEqualObjects(weakKeychain.cachedKeychainData[service][account].firstObject,
- tokenData);
- // Wildcard query should simply return the results without cache it
- data = [weakKeychain dataForService:@"*" account:account];
- XCTAssertEqualObjects(data, tokenData);
- // Cache should not have wildcard query entry
- XCTAssertNil(weakKeychain.cachedKeychainData[@"*"]);
- // Assume keychain has empty service entry
- [weakKeychain.cachedKeychainData setObject:[@{} mutableCopy] forKey:service];
- // Query the item
- data = [weakKeychain dataForService:service account:account];
- XCTAssertEqualObjects(data, tokenData);
- // Cache should have the query item.
- XCTAssertEqualObjects(weakKeychain.cachedKeychainData[service][account].firstObject,
- tokenData);
- // Clean up keychain at the end
- [weakKeychain removeItemsMatchingService:@"*"
- account:@"*"
- handler:^(NSError *_Nonnull error) {
- XCTAssertNil(error);
- [addItemToKeychainExpectation fulfill];
- }];
- }];
- [self waitForExpectationsWithTimeout:1.0 handler:NULL];
- }
- - (void)testCachedKeychainOverwrite {
- XCTestExpectation *overwriteCachedKeychainExpectation =
- [self expectationWithDescription:@"Test the cached keychain item is overwrite properly"];
- FIRInstanceIDAuthKeychain *keychain =
- [[FIRInstanceIDAuthKeychain alloc] initWithIdentifier:kFIRInstanceIDTestKeychainId];
- // Set the cache a different data under the same service but different account
- NSData *data = [[NSData alloc] init];
- NSString *service = [NSString stringWithFormat:@"%@:%@", kAuthorizedEntity, kScope];
- [keychain.cachedKeychainData setObject:[@{kBundleID2 : data} mutableCopy] forKey:service];
- // Create a keychain item
- NSString *account = kBundleID1;
- NSData *tokenData = [self tokenDataWithAuthorizedEntity:kAuthorizedEntity
- scope:kScope
- token:kToken1];
- __weak FIRInstanceIDAuthKeychain *weakKeychain = keychain;
- [keychain setData:tokenData
- forService:service
- account:account
- handler:^(NSError *error) {
- XCTAssertNil(error);
- // Query the item should fetch from keychain because no entry under the same
- // service and account.
- NSData *data = [weakKeychain dataForService:service account:account];
- XCTAssertEqualObjects(data, tokenData);
- // Cache should now have the query item
- XCTAssertEqualObjects(weakKeychain.cachedKeychainData[service][account].firstObject,
- tokenData);
- // Clean up keychain at the end
- [weakKeychain removeItemsMatchingService:@"*"
- account:@"*"
- handler:^(NSError *_Nonnull error) {
- XCTAssertNil(error);
- [overwriteCachedKeychainExpectation fulfill];
- }];
- }];
- [self waitForExpectationsWithTimeout:1.0 handler:NULL];
- }
- - (void)testSetKeychainItemShouldDeleteOldEntry {
- XCTestExpectation *overwriteCachedKeychainExpectation = [self
- expectationWithDescription:@"Test keychain entry should be deleted before adding a new one"];
- FIRInstanceIDAuthKeychain *keychain =
- [[FIRInstanceIDAuthKeychain alloc] initWithIdentifier:kFIRInstanceIDTestKeychainId];
- // Assume keychain had a old entry under the same service and account.
- // Now if we set the cache a different data under the same service
- NSData *oldData = [[NSData alloc] init];
- NSString *service = [NSString stringWithFormat:@"%@:%@", kAuthorizedEntity, kScope];
- NSString *account = kBundleID1;
- [keychain.cachedKeychainData setObject:[@{account : oldData} mutableCopy] forKey:service];
- // add a new keychain item
- NSData *tokenData = [self tokenDataWithAuthorizedEntity:kAuthorizedEntity
- scope:kScope
- token:kToken1];
- __weak FIRInstanceIDAuthKeychain *weakKeychain = keychain;
- [keychain setData:tokenData
- forService:service
- account:account
- handler:^(NSError *error) {
- XCTAssertNil(error);
- // Cache should now have the updated item
- XCTAssertEqualObjects(weakKeychain.cachedKeychainData[service][account].firstObject,
- tokenData);
- // Clean up keychain at the end
- [weakKeychain removeItemsMatchingService:@"*"
- account:@"*"
- handler:^(NSError *_Nonnull error) {
- XCTAssertNil(error);
- [overwriteCachedKeychainExpectation fulfill];
- }];
- }];
- [self waitForExpectationsWithTimeout:1.0 handler:NULL];
- }
- - (void)testInvalidQuery {
- XCTestExpectation *invalidKeychainQueryExpectation =
- [self expectationWithDescription:@"Test invalid keychain query"];
- FIRInstanceIDAuthKeychain *keychain =
- [[FIRInstanceIDAuthKeychain alloc] initWithIdentifier:kFIRInstanceIDTestKeychainId];
- NSData *data = [[NSData alloc] init];
- [keychain setData:data
- forService:@"*"
- account:@"*"
- handler:^(NSError *error) {
- XCTAssertNotNil(error);
- [invalidKeychainQueryExpectation fulfill];
- }];
- [self waitForExpectationsWithTimeout:1.0 handler:NULL];
- }
- - (void)testQueryAndAddEntry {
- FIRInstanceIDAuthKeychain *keychain =
- [[FIRInstanceIDAuthKeychain alloc] initWithIdentifier:kFIRInstanceIDTestKeychainId];
- // Set the cache a different data under the same service but different account
- NSData *data = [[NSData alloc] init];
- NSString *service = [NSString stringWithFormat:@"%@:%@", kAuthorizedEntity, kScope];
- NSString *account1 = kBundleID1;
- [keychain.cachedKeychainData setObject:[@{account1 : data} mutableCopy] forKey:service];
- // Now account2 doesn't exist in cache
- NSString *account2 = kBundleID2;
- XCTAssertNil(keychain.cachedKeychainData[service][account2]);
- // Query account2
- XCTAssertNil([keychain dataForService:service account:account2]);
- // Service and account2 should exist in cache.
- XCTAssertNotNil(keychain.cachedKeychainData[service][account2]);
- }
- #pragma mark - helper function
- - (NSData *)tokenDataWithAuthorizedEntity:(NSString *)authorizedEntity
- scope:(NSString *)scope
- token:(NSString *)token {
- FIRInstanceIDTokenInfo *tokenInfo =
- [[FIRInstanceIDTokenInfo alloc] initWithAuthorizedEntity:authorizedEntity
- scope:scope
- token:token
- appVersion:@"1.0"
- firebaseAppID:kFirebaseAppID];
- #pragma clang diagnostic push
- #pragma clang diagnostic ignored "-Wdeprecated-declarations"
- return [NSKeyedArchiver archivedDataWithRootObject:tokenInfo];
- #pragma clang diagnostic pop
- }
- @end
- #endif // TARGET_OS_MACCATALYST
|