/* * 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 "FIRInstanceIDStore.h" #import "FIRInstanceIDCheckinPreferences.h" #import "FIRInstanceIDCheckinStore.h" #import "FIRInstanceIDConstants.h" #import "FIRInstanceIDLogger.h" #import "FIRInstanceIDTokenStore.h" #import "FIRInstanceIDVersionUtilities.h" // NOTE: These values should be in sync with what InstanceID saves in as. static NSString *const kCheckinFileName = @"g-checkin"; // APNS token (use the old key value i.e. with prefix GMS) static NSString *const kFIRInstanceIDLibraryVersion = @"GMSInstanceID-version"; @interface FIRInstanceIDStore () @property(nonatomic, readwrite, strong) FIRInstanceIDCheckinStore *checkinStore; @property(nonatomic, readwrite, strong) FIRInstanceIDTokenStore *tokenStore; @end @implementation FIRInstanceIDStore - (instancetype)initWithDelegate:(NSObject *)delegate { FIRInstanceIDCheckinStore *checkinStore = [[FIRInstanceIDCheckinStore alloc] initWithCheckinPlistFileName:kCheckinFileName subDirectoryName:kFIRInstanceIDSubDirectoryName]; FIRInstanceIDTokenStore *tokenStore = [FIRInstanceIDTokenStore defaultStore]; return [self initWithCheckinStore:checkinStore tokenStore:tokenStore delegate:delegate]; } - (instancetype)initWithCheckinStore:(FIRInstanceIDCheckinStore *)checkinStore tokenStore:(FIRInstanceIDTokenStore *)tokenStore delegate:(NSObject *)delegate { self = [super init]; if (self) { _checkinStore = checkinStore; _tokenStore = tokenStore; _delegate = delegate; [self resetCredentialsIfNeeded]; } return self; } #pragma mark - Upgrades + (BOOL)hasSubDirectory:(NSString *)subDirectoryName { NSString *subDirectoryPath = [self pathForSupportSubDirectory:subDirectoryName]; BOOL isDirectory; if (![[NSFileManager defaultManager] fileExistsAtPath:subDirectoryPath isDirectory:&isDirectory]) { return NO; } else if (!isDirectory) { return NO; } return YES; } + (NSSearchPathDirectory)supportedDirectory { #if TARGET_OS_TV return NSCachesDirectory; #else return NSApplicationSupportDirectory; #endif } + (NSString *)pathForSupportSubDirectory:(NSString *)subDirectoryName { NSArray *directoryPaths = NSSearchPathForDirectoriesInDomains([self supportedDirectory], NSUserDomainMask, YES); NSString *dirPath = directoryPaths.lastObject; NSArray *components = @[ dirPath, subDirectoryName ]; return [NSString pathWithComponents:components]; } + (BOOL)createSubDirectory:(NSString *)subDirectoryName { NSString *subDirectoryPath = [self pathForSupportSubDirectory:subDirectoryName]; BOOL hasSubDirectory; if (![[NSFileManager defaultManager] fileExistsAtPath:subDirectoryPath isDirectory:&hasSubDirectory]) { NSError *error; [[NSFileManager defaultManager] createDirectoryAtPath:subDirectoryPath withIntermediateDirectories:YES attributes:nil error:&error]; if (error) { FIRInstanceIDLoggerError(kFIRInstanceIDMessageCodeStore000, @"Cannot create directory %@, error: %@", subDirectoryPath, error); return NO; } } else { if (!hasSubDirectory) { FIRInstanceIDLoggerError(kFIRInstanceIDMessageCodeStore001, @"Found file instead of directory at %@", subDirectoryPath); return NO; } } return YES; } + (BOOL)removeSubDirectory:(NSString *)subDirectoryName error:(NSError **)error { if ([self hasSubDirectory:subDirectoryName]) { NSString *subDirectoryPath = [self pathForSupportSubDirectory:subDirectoryName]; BOOL isDirectory; if ([[NSFileManager defaultManager] fileExistsAtPath:subDirectoryPath isDirectory:&isDirectory]) { return [[NSFileManager defaultManager] removeItemAtPath:subDirectoryPath error:error]; } } return YES; } /** * Reset the keychain preferences if the app had been deleted earlier and then reinstalled. * Keychain preferences are not cleared in the above scenario so explicitly clear them. * * In case of an iCloud backup and restore the Keychain preferences should already be empty * since the Keychain items are marked with `*BackupThisDeviceOnly`. */ - (void)resetCredentialsIfNeeded { BOOL checkinPlistExists = [self.checkinStore hasCheckinPlist]; // Checkin info existed in backup excluded plist. Should not be a fresh install. if (checkinPlistExists) { // FCM user can still have the old version of checkin, migration should only happen once. [self.checkinStore migrateCheckinItemIfNeeded]; return; } // reset checkin in keychain if a fresh install. // set the old checkin preferences to unregister pre-registered tokens FIRInstanceIDCheckinPreferences *oldCheckinPreferences = [self.checkinStore cachedCheckinPreferences]; if (oldCheckinPreferences) { [self.checkinStore removeCheckinPreferencesWithHandler:^(NSError *error) { if (!error) { FIRInstanceIDLoggerDebug( kFIRInstanceIDMessageCodeStore002, @"Removed cached checkin preferences from Keychain because this is a fresh install."); } else { FIRInstanceIDLoggerError( kFIRInstanceIDMessageCodeStore003, @"Couldn't remove cached checkin preferences for a fresh install. Error: %@", error); } if (oldCheckinPreferences.deviceID.length && oldCheckinPreferences.secretToken.length) { FIRInstanceIDLoggerDebug(kFIRInstanceIDMessageCodeStore006, @"App reset detected. Will delete server registrations."); // We don't really need to delete old FCM tokens created via IID auth tokens since // those tokens are already hashed by APNS token as the has so creating a new // token should automatically delete the old-token. [self.delegate store:self didDeleteFCMScopedTokensForCheckin:oldCheckinPreferences]; } else { FIRInstanceIDLoggerDebug(kFIRInstanceIDMessageCodeStore009, @"App reset detected but no valid checkin auth preferences found." @" Will not delete server registrations."); } }]; } } #pragma mark - Get - (FIRInstanceIDTokenInfo *)tokenInfoWithAuthorizedEntity:(NSString *)authorizedEntity scope:(NSString *)scope { // TODO(chliangGoogle): If we don't have the token plist we should delete all the tokens from // the keychain. This is because not having the plist signifies a backup and restore operation. // In case the keychain has any tokens these would now be stale and therefore should be // deleted. if (![authorizedEntity length] || ![scope length]) { return nil; } FIRInstanceIDTokenInfo *info = [self.tokenStore tokenInfoWithAuthorizedEntity:authorizedEntity scope:scope]; return info; } - (NSArray *)cachedTokenInfos { return [self.tokenStore cachedTokenInfos]; } #pragma mark - Save - (void)saveTokenInfo:(FIRInstanceIDTokenInfo *)tokenInfo handler:(void (^)(NSError *error))handler { [self.tokenStore saveTokenInfo:tokenInfo handler:handler]; } #pragma mark - Delete - (void)removeCachedTokenWithAuthorizedEntity:(NSString *)authorizedEntity scope:(NSString *)scope { if (![authorizedEntity length] || ![scope length]) { FIRInstanceIDLoggerError(kFIRInstanceIDMessageCodeStore012, @"Will not delete token with invalid entity: %@, scope: %@", authorizedEntity, scope); return; } [self.tokenStore removeTokenWithAuthorizedEntity:authorizedEntity scope:scope]; } - (void)removeAllCachedTokensWithHandler:(void (^)(NSError *error))handler { [self.tokenStore removeAllTokensWithHandler:handler]; } #pragma mark - FIRInstanceIDCheckinCache protocol - (void)saveCheckinPreferences:(FIRInstanceIDCheckinPreferences *)preferences handler:(void (^)(NSError *error))handler { [self.checkinStore saveCheckinPreferences:preferences handler:handler]; } - (FIRInstanceIDCheckinPreferences *)cachedCheckinPreferences { return [self.checkinStore cachedCheckinPreferences]; } - (void)removeCheckinPreferencesWithHandler:(void (^)(NSError *))handler { [self.checkinStore removeCheckinPreferencesWithHandler:^(NSError *error) { if (handler) { handler(error); } }]; } @end