/* * Copyright 2017 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 #import #import "FIRAuthKeychain.h" /** @var kAccountPrefix @brief The keychain account prefix assumed by the tests. */ static NSString *const kAccountPrefix = @"firebase_auth_1_"; /** @var kKey @brief The key used in tests. */ static NSString *const kKey = @"ACCOUNT"; /** @var kService @brief The keychain service used in tests. */ static NSString *const kService = @"SERVICE"; /** @var kOtherService @brief Another keychain service used in tests. */ static NSString *const kOtherService = @"OTHER_SERVICE"; /** @var kData @brief A piece of keychain data used in tests. */ static NSString *const kData = @"DATA"; /** @var kOtherData @brief Another piece of keychain data used in tests. */ static NSString *const kOtherData = @"OTHER_DATA"; /** @fn accountFromKey @brief Converts a key string to an account string. @param key The key string to be converted from. @return The account string being the conversion result. */ static NSString *accountFromKey(NSString *key) { return [kAccountPrefix stringByAppendingString:key]; } /** @fn dataFromString @brief Converts a NSString to NSData. @param string The NSString to be converted from. @return The NSData being the conversion result. */ static NSData *dataFromString(NSString *string) { return [string dataUsingEncoding:NSUTF8StringEncoding]; } /** @fn stringFromData @brief Converts a NSData to NSString. @param data The NSData to be converted from. @return The NSString being the conversion result. */ static NSString *stringFromData(NSData *data) { return [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; } /** @fn fakeError @brief Creates a fake error object. @return a non-nil NSError instance. */ static NSError *fakeError() { return [NSError errorWithDomain:@"ERROR" code:-1 userInfo:nil]; } /** @class FIRAuthKeychainTests @brief Tests for @c FIRAuthKeychainTests . */ @interface FIRAuthKeychainTests : XCTestCase @end @implementation FIRAuthKeychainTests /** @fn testReadNonexisting @brief Tests reading non-existing keychain item. */ - (void)testReadNonexisting { [self setPassword:nil account:accountFromKey(kKey) service:kService]; [self setPassword:nil account:kKey service:nil]; // legacy form FIRAuthKeychain *keychain = [[FIRAuthKeychain alloc] initWithService:kService]; NSError *error = fakeError(); XCTAssertNil([keychain dataForKey:kKey error:&error]); XCTAssertNil(error); } /** @fn testReadExisting @brief Tests reading existing keychain item. */ - (void)testReadExisting { [self setPassword:kData account:accountFromKey(kKey) service:kService]; FIRAuthKeychain *keychain = [[FIRAuthKeychain alloc] initWithService:kService]; NSError *error = fakeError(); XCTAssertEqualObjects([keychain dataForKey:kKey error:&error], dataFromString(kData)); XCTAssertNil(error); [self deletePasswordWithAccount:accountFromKey(kKey) service:kService]; } /** @fn testNotReadOtherService @brief Tests not reading keychain item belonging to other service. */ - (void)testNotReadOtherService { [self setPassword:nil account:accountFromKey(kKey) service:kService]; [self setPassword:kData account:accountFromKey(kKey) service:kOtherService]; FIRAuthKeychain *keychain = [[FIRAuthKeychain alloc] initWithService:kService]; NSError *error = fakeError(); XCTAssertNil([keychain dataForKey:kKey error:&error]); XCTAssertNil(error); [self deletePasswordWithAccount:accountFromKey(kKey) service:kOtherService]; } /** @fn testWriteNonexisting @brief Tests writing new keychain item. */ - (void)testWriteNonexisting { [self setPassword:nil account:accountFromKey(kKey) service:kService]; FIRAuthKeychain *keychain = [[FIRAuthKeychain alloc] initWithService:kService]; XCTAssertTrue([keychain setData:dataFromString(kData) forKey:kKey error:NULL]); XCTAssertEqualObjects([self passwordWithAccount:accountFromKey(kKey) service:kService], kData); [self deletePasswordWithAccount:accountFromKey(kKey) service:kService]; } /** @fn testWriteExisting @brief Tests overwriting existing keychain item. */ - (void)testWriteExisting { [self setPassword:kData account:accountFromKey(kKey) service:kService]; FIRAuthKeychain *keychain = [[FIRAuthKeychain alloc] initWithService:kService]; XCTAssertTrue([keychain setData:dataFromString(kOtherData) forKey:kKey error:NULL]); XCTAssertEqualObjects([self passwordWithAccount:accountFromKey(kKey) service:kService], kOtherData); [self deletePasswordWithAccount:accountFromKey(kKey) service:kService]; } /** @fn testDeleteNonexisting @brief Tests deleting non-existing keychain item. */ - (void)testDeleteNonexisting { [self setPassword:nil account:accountFromKey(kKey) service:kService]; FIRAuthKeychain *keychain = [[FIRAuthKeychain alloc] initWithService:kService]; XCTAssertTrue([keychain removeDataForKey:kKey error:NULL]); XCTAssertNil([self passwordWithAccount:accountFromKey(kKey) service:kService]); } /** @fn testDeleteExisting @brief Tests deleting existing keychain item. */ - (void)testDeleteExisting { [self setPassword:kData account:accountFromKey(kKey) service:kService]; FIRAuthKeychain *keychain = [[FIRAuthKeychain alloc] initWithService:kService]; XCTAssertTrue([keychain removeDataForKey:kKey error:NULL]); XCTAssertNil([self passwordWithAccount:accountFromKey(kKey) service:kService]); } /** @fn testReadLegacy @brief Tests reading legacy keychain item. */ - (void)testReadLegacy { [self setPassword:nil account:accountFromKey(kKey) service:kService]; [self setPassword:kData account:kKey service:nil]; // legacy form FIRAuthKeychain *keychain = [[FIRAuthKeychain alloc] initWithService:kService]; NSError *error = fakeError(); XCTAssertEqualObjects([keychain dataForKey:kKey error:&error], dataFromString(kData)); XCTAssertNil(error); // Legacy item should have been moved to current form. XCTAssertEqualObjects([self passwordWithAccount:accountFromKey(kKey) service:kService], kData); XCTAssertNil([self passwordWithAccount:kKey service:nil]); [self deletePasswordWithAccount:accountFromKey(kKey) service:kService]; } /** @fn testNotReadLegacy @brief Tests not reading legacy keychain item because current keychain item exists. */ - (void)testNotReadLegacy { [self setPassword:kData account:accountFromKey(kKey) service:kService]; [self setPassword:kOtherData account:kKey service:nil]; // legacy form FIRAuthKeychain *keychain = [[FIRAuthKeychain alloc] initWithService:kService]; NSError *error = fakeError(); XCTAssertEqualObjects([keychain dataForKey:kKey error:&error], dataFromString(kData)); XCTAssertNil(error); // Legacy item should have leave untouched. XCTAssertEqualObjects([self passwordWithAccount:accountFromKey(kKey) service:kService], kData); XCTAssertEqualObjects([self passwordWithAccount:kKey service:nil], kOtherData); [self deletePasswordWithAccount:accountFromKey(kKey) service:kService]; [self deletePasswordWithAccount:kKey service:nil]; } /** @fn testRemoveLegacy @brief Tests removing keychain item also removes legacy keychain item. */ - (void)testRemoveLegacy { [self setPassword:kData account:accountFromKey(kKey) service:kService]; [self setPassword:kOtherData account:kKey service:nil]; // legacy form FIRAuthKeychain *keychain = [[FIRAuthKeychain alloc] initWithService:kService]; XCTAssertTrue([keychain removeDataForKey:kKey error:NULL]); XCTAssertNil([self passwordWithAccount:accountFromKey(kKey) service:kService]); XCTAssertNil([self passwordWithAccount:kKey service:nil]); } /** @fn testNullErrorParameter @brief Tests that 'NULL' can be safely passed in. */ - (void)testNullErrorParameter { FIRAuthKeychain *keychain = [[FIRAuthKeychain alloc] initWithService:kService]; [keychain dataForKey:kKey error:NULL]; [keychain setData:dataFromString(kData) forKey:kKey error:NULL]; [keychain removeDataForKey:kKey error:NULL]; } #pragma mark - Helpers /** @fn passwordWithAccount:service: @brief Reads a generic password string from the keychain. @param account The account attribute of the keychain item. @param service The service attribute of the keychain item, if provided. @return The generic password string, if the keychain item exists. */ - (nullable NSString *)passwordWithAccount:(nonnull NSString *)account service:(nullable NSString *)service { NSMutableDictionary *query = [@{ (__bridge id)kSecReturnData : @YES, (__bridge id)kSecClass : (__bridge id)kSecClassGenericPassword, (__bridge id)kSecAttrAccount : account, } mutableCopy]; if (service) { query[(__bridge id)kSecAttrService] = service; } CFDataRef result; OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)query, (CFTypeRef *)&result); if (status == errSecItemNotFound) { return nil; } XCTAssertEqual(status, errSecSuccess); return stringFromData((__bridge NSData *)(result)); } /** @fn addPassword:account:service: @brief Adds a generic password string to the keychain. @param password The value attribute for the password to write to the keychain item. @param account The account attribute of the keychain item. @param service The service attribute of the keychain item, if provided. */ - (void)addPassword:(nonnull NSString *)password account:(nonnull NSString *)account service:(nullable NSString *)service { NSMutableDictionary *query = [@{ (__bridge id)kSecValueData : dataFromString(password), (__bridge id)kSecClass : (__bridge id)kSecClassGenericPassword, (__bridge id)kSecAttrAccount : account, } mutableCopy]; if (service) { query[(__bridge id)kSecAttrService] = service; } OSStatus status = SecItemAdd((__bridge CFDictionaryRef)query, NULL); XCTAssertEqual(status, errSecSuccess); } /** @fn deletePasswordWithAccount:service: @brief Deletes a generic password string from the keychain. @param account The account attribute of the keychain item. @param service The service attribute of the keychain item, if provided. */ - (void)deletePasswordWithAccount:(nonnull NSString *)account service:(nullable NSString *)service { NSMutableDictionary *query = [@{ (__bridge id)kSecClass : (__bridge id)kSecClassGenericPassword, (__bridge id)kSecAttrAccount : account, } mutableCopy]; if (service) { query[(__bridge id)kSecAttrService] = service; } OSStatus status = SecItemDelete((__bridge CFDictionaryRef)query); XCTAssertEqual(status, errSecSuccess); } /** @fn setPasswordWithString:account:service: @brief Sets a generic password string to the keychain. @param password The value attribute of the keychain item, if provided, or nil to delete the existing password if any. @param account The account attribute of the keychain item. @param service The service attribute of the keychain item, if provided. */ - (void)setPassword:(nullable NSString *)password account:(nonnull NSString *)account service:(nullable NSString *)service { if ([self passwordWithAccount:account service:service]) { [self deletePasswordWithAccount:account service:service]; } if (password) { [self addPassword:password account:account service:service]; } } @end