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.
 
 
 
 

256 lines
8.4 KiB

/*
* 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 "FIRAuthKeychain.h"
#import <Security/Security.h>
#import "FIRAuthErrorUtils.h"
#import "FIRAuthUserDefaultsStorage.h"
#if FIRAUTH_USER_DEFAULTS_STORAGE_AVAILABLE
#import <UIKit/UIKit.h>
/** @var kOSVersionMatcherForUsingUserDefaults
@brief The regular expression to match all OS versions that @c FIRAuthUserDefaultsStorage is
used instead if available.
*/
static NSString *const kOSVersionMatcherForUsingUserDefaults = @"^10\\.[01](\\..*)?$";
#endif // FIRAUTH_USER_DEFAULTS_STORAGE_AVAILABLE
/** @var kAccountPrefix
@brief The prefix string for keychain item account attribute before the key.
@remarks A number "1" is encoded in the prefix in case we need to upgrade the scheme in future.
*/
static NSString *const kAccountPrefix = @"firebase_auth_1_";
@implementation FIRAuthKeychain {
/** @var _service
@brief The name of the keychain service.
*/
NSString *_service;
/** @var _legacyItemDeletedForKey
@brief Indicates whether or not this class knows that the legacy item for a particular key has
been deleted.
@remarks This dictionary is to avoid unecessary keychain operations against legacy items.
*/
NSMutableDictionary *_legacyEntryDeletedForKey;
}
- (id<FIRAuthStorage>)initWithService:(NSString *)service {
#if FIRAUTH_USER_DEFAULTS_STORAGE_AVAILABLE
NSString *OSVersion = [UIDevice currentDevice].systemVersion;
NSRegularExpression *regex =
[NSRegularExpression regularExpressionWithPattern:kOSVersionMatcherForUsingUserDefaults
options:0
error:NULL];
if ([regex numberOfMatchesInString:OSVersion options:0 range:NSMakeRange(0, OSVersion.length)]) {
return (id<FIRAuthStorage>)[[FIRAuthUserDefaultsStorage alloc] initWithService:service];
}
#endif // FIRAUTH_USER_DEFAULTS_STORAGE_AVAILABLE
self = [super init];
if (self) {
_service = [service copy];
_legacyEntryDeletedForKey = [[NSMutableDictionary alloc] init];
}
return self;
}
- (NSData *)dataForKey:(NSString *)key error:(NSError **_Nullable)error {
if (!key.length) {
[NSException raise:NSInvalidArgumentException
format:@"%@", @"The key cannot be nil or empty."];
return nil;
}
NSData *data = [self itemWithQuery:[self genericPasswordQueryWithKey:key] error:error];
if (error && *error) {
return nil;
}
if (data) {
return data;
}
// Check for legacy form.
if (_legacyEntryDeletedForKey[key]) {
return nil;
}
data = [self itemWithQuery:[self legacyGenericPasswordQueryWithKey:key] error:error];
if (error && *error) {
return nil;
}
if (!data) {
// Mark legacy data as non-existing so we don't have to query it again.
_legacyEntryDeletedForKey[key] = @YES;
return nil;
}
// Move the data to current form.
if (![self setData:data forKey:key error:error]) {
return nil;
}
[self deleteLegacyItemWithKey:key];
return data;
}
- (BOOL)setData:(NSData *)data forKey:(NSString *)key error:(NSError **_Nullable)error {
if (!key.length) {
[NSException raise:NSInvalidArgumentException
format:@"%@", @"The key cannot be nil or empty."];
return NO;
}
NSDictionary *attributes = @{
(__bridge id)kSecValueData : data,
(__bridge id)kSecAttrAccessible : (__bridge id)kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly,
};
return [self setItemWithQuery:[self genericPasswordQueryWithKey:key]
attributes:attributes
error:error];
}
- (BOOL)removeDataForKey:(NSString *)key error:(NSError **_Nullable)error {
if (!key.length) {
[NSException raise:NSInvalidArgumentException
format:@"%@", @"The key cannot be nil or empty."];
return NO;
}
if (![self deleteItemWithQuery:[self genericPasswordQueryWithKey:key] error:error]) {
return NO;
}
// Legacy form item, if exists, also needs to be removed, otherwise it will be exposed when
// current form item is removed, leading to incorrect semantics.
[self deleteLegacyItemWithKey:key];
return YES;
}
#pragma mark - Private
- (NSData *)itemWithQuery:(NSDictionary *)query error:(NSError **_Nullable)error {
NSMutableDictionary *returningQuery = [query mutableCopy];
returningQuery[(__bridge id)kSecReturnData] = @YES;
returningQuery[(__bridge id)kSecReturnAttributes] = @YES;
// Using a match limit of 2 means that we can check whether there is more than one item.
// If we used a match limit of 1 we would never find out.
returningQuery[(__bridge id)kSecMatchLimit] = @2;
CFArrayRef result = NULL;
OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)returningQuery,
(CFTypeRef *)&result);
if (status == noErr && result != NULL) {
NSArray *items = (__bridge_transfer NSArray *)result;
if (items.count != 1) {
if (error) {
*error = [FIRAuthErrorUtils keychainErrorWithFunction:@"SecItemCopyMatching"
status:status];
}
return nil;
}
if (error) {
*error = nil;
}
NSDictionary *item = items[0];
return item[(__bridge id)kSecValueData];
}
if (status == errSecItemNotFound) {
if (error) {
*error = nil;
}
} else {
if (error) {
*error = [FIRAuthErrorUtils keychainErrorWithFunction:@"SecItemCopyMatching" status:status];
}
}
return nil;
}
- (BOOL)setItemWithQuery:(NSDictionary *)query
attributes:(NSDictionary *)attributes
error:(NSError **_Nullable)error {
NSMutableDictionary *combined = [attributes mutableCopy];
[combined addEntriesFromDictionary:query];
BOOL hasItem = NO;
OSStatus status = SecItemAdd((__bridge CFDictionaryRef)combined, NULL);
if (status == errSecDuplicateItem) {
hasItem = YES;
status = SecItemUpdate((__bridge CFDictionaryRef)query, (__bridge CFDictionaryRef)attributes);
}
if (status == noErr) {
return YES;
}
if (error) {
NSString *function = hasItem ? @"SecItemUpdate" : @"SecItemAdd";
*error = [FIRAuthErrorUtils keychainErrorWithFunction:function status:status];
}
return NO;
}
- (BOOL)deleteItemWithQuery:(NSDictionary *)query error:(NSError **_Nullable)error {
OSStatus status = SecItemDelete((__bridge CFDictionaryRef)query);
if (status == noErr || status == errSecItemNotFound) {
return YES;
}
if (error) {
*error = [FIRAuthErrorUtils keychainErrorWithFunction:@"SecItemDelete" status:status];
}
return NO;
}
/** @fn deleteLegacyItemsWithKey:
@brief Deletes legacy item from the keychain if it is not already known to be deleted.
@param key The key for the item.
*/
- (void)deleteLegacyItemWithKey:(NSString *)key {
if (_legacyEntryDeletedForKey[key]) {
return;
}
NSDictionary *query = [self legacyGenericPasswordQueryWithKey:key];
SecItemDelete((__bridge CFDictionaryRef)query);
_legacyEntryDeletedForKey[key] = @YES;
}
/** @fn genericPasswordQueryWithKey:
@brief Returns a keychain query of generic password to be used to manipulate key'ed value.
@param key The key for the value being manipulated, used as the account field in the query.
*/
- (NSDictionary *)genericPasswordQueryWithKey:(NSString *)key {
return @{
(__bridge id)kSecClass : (__bridge id)kSecClassGenericPassword,
(__bridge id)kSecAttrAccount : [kAccountPrefix stringByAppendingString:key],
(__bridge id)kSecAttrService : _service,
};
}
/** @fn legacyGenericPasswordQueryWithKey:
@brief Returns a keychain query of generic password without service field, which is used by
previous version of this class.
@param key The key for the value being manipulated, used as the account field in the query.
*/
- (NSDictionary *)legacyGenericPasswordQueryWithKey:(NSString *)key {
return @{
(__bridge id)kSecClass : (__bridge id)kSecClassGenericPassword,
(__bridge id)kSecAttrAccount : key,
};
}
@end