Push from command line

This commit is contained in:
Deniz Duezgoeren
2019-08-12 11:20:21 +02:00
parent 3a919dcb23
commit f1345eac14
512 changed files with 103288 additions and 1930 deletions
@@ -0,0 +1,167 @@
/*
* 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>
// The format of the debug code will show in the log as: e.g.
// for code 1000, it will show as I-IID001000.
typedef NS_ENUM(NSInteger, FIRInstanceIDMessageCode) {
// DO NOT USE 2000, 2002.
kFIRInstanceIDMessageCodeFIRApp000 = 1000, // I-IID001000
kFIRInstanceIDMessageCodeFIRApp001 = 1001,
kFIRInstanceIDMessageCodeFIRApp002 = 1002,
kFIRInstanceIDMessageCodeInternal001 = 2001,
kFIRInstanceIDMessageCodeInternal002 = 2002,
// FIRInstanceID.m
// DO NOT USE 4000.
kFIRInstanceIDMessageCodeInstanceID000 = 3000,
kFIRInstanceIDMessageCodeInstanceID001 = 3001,
kFIRInstanceIDMessageCodeInstanceID002 = 3002,
kFIRInstanceIDMessageCodeInstanceID003 = 3003,
kFIRInstanceIDMessageCodeInstanceID004 = 3004,
kFIRInstanceIDMessageCodeInstanceID005 = 3005,
kFIRInstanceIDMessageCodeInstanceID006 = 3006,
kFIRInstanceIDMessageCodeInstanceID007 = 3007,
kFIRInstanceIDMessageCodeInstanceID008 = 3008,
kFIRInstanceIDMessageCodeInstanceID009 = 3009,
kFIRInstanceIDMessageCodeInstanceID010 = 3010,
kFIRInstanceIDMessageCodeInstanceID011 = 3011,
kFIRInstanceIDMessageCodeInstanceID012 = 3012,
kFIRInstanceIDMessageCodeInstanceID013 = 3013,
kFIRInstanceIDMessageCodeInstanceID014 = 3014,
kFIRInstanceIDMessageCodeInstanceID015 = 3015,
kFIRInstanceIDMessageCodeRefetchingTokenForAPNS = 3016,
kFIRInstanceIDMessageCodeInstanceID017 = 3017,
kFIRInstanceIDMessageCodeInstanceID018 = 3018,
// FIRInstanceIDAuthService.m
kFIRInstanceIDMessageCodeAuthService000 = 5000,
kFIRInstanceIDMessageCodeAuthService001 = 5001,
kFIRInstanceIDMessageCodeAuthService002 = 5002,
kFIRInstanceIDMessageCodeAuthService003 = 5003,
kFIRInstanceIDMessageCodeAuthService004 = 5004,
kFIRInstanceIDMessageCodeAuthServiceCheckinInProgress = 5004,
// FIRInstanceIDBackupExcludedPlist.m
kFIRInstanceIDMessageCodeBackupExcludedPlist000 = 6000,
kFIRInstanceIDMessageCodeBackupExcludedPlist001 = 6001,
kFIRInstanceIDMessageCodeBackupExcludedPlist002 = 6002,
kFIRInstanceIDMessageCodeBackupExcludedPlistInvalidPlistEnum = 6003,
// FIRInstanceIDCheckinService.m
kFIRInstanceIDMessageCodeService000 = 7000,
kFIRInstanceIDMessageCodeService001 = 7001,
kFIRInstanceIDMessageCodeService002 = 7002,
kFIRInstanceIDMessageCodeService003 = 7003,
kFIRInstanceIDMessageCodeService004 = 7004,
kFIRInstanceIDMessageCodeService005 = 7005,
kFIRInstanceIDMessageCodeService006 = 7006,
kFIRInstanceIDInvalidNetworkSession = 7007,
kFIRInstanceIDInvalidSettingResponse = 7008,
// FIRInstanceIDCheckinStore.m
// DO NOT USE 8002, 8004 - 8008
kFIRInstanceIDMessageCodeCheckinStore000 = 8000,
kFIRInstanceIDMessageCodeCheckinStore001 = 8001,
kFIRInstanceIDMessageCodeCheckinStore003 = 8003,
kFIRInstanceIDMessageCodeCheckinStoreCheckinPlistDeleted = 8009,
kFIRInstanceIDMessageCodeCheckinStoreCheckinPlistSaved = 8010,
// FIRInstanceIDKeyPair.m
// DO NOT USE 9001, 9003
kFIRInstanceIDMessageCodeKeyPair000 = 9000,
kFIRInstanceIDMessageCodeKeyPair002 = 9002,
kFIRInstanceIDMessageCodeKeyPairMigrationError = 9004,
kFIRInstanceIDMessageCodeKeyPairMigrationSuccess = 9005,
kFIRInstanceIDMessageCodeKeyPairNoLegacyKeyPair = 9006,
// FIRInstanceIDKeyPairStore.m
kFIRInstanceIDMessageCodeKeyPairStore000 = 10000,
kFIRInstanceIDMessageCodeKeyPairStore001 = 10001,
kFIRInstanceIDMessageCodeKeyPairStore002 = 10002,
kFIRInstanceIDMessageCodeKeyPairStore003 = 10003,
kFIRInstanceIDMessageCodeKeyPairStore004 = 10004,
kFIRInstanceIDMessageCodeKeyPairStore005 = 10005,
kFIRInstanceIDMessageCodeKeyPairStore006 = 10006,
kFIRInstanceIDMessageCodeKeyPairStore007 = 10007,
kFIRInstanceIDMessageCodeKeyPairStore008 = 10008,
kFIRInstanceIDMessageCodeKeyPairStoreCouldNotLoadKeyPair = 10009,
// FIRInstanceIDKeyPairUtilities.m
kFIRInstanceIDMessageCodeKeyPairUtilities000 = 11000,
kFIRInstanceIDMessageCodeKeyPairUtilities001 = 11001,
kFIRInstanceIDMessageCodeKeyPairUtilitiesFirstConcatenateParamNil = 11002,
// DO NOT USE 12000 - 12014
// FIRInstanceIDStore.m
// DO NOT USE 13004, 13005, 13007, 13008, 13010, 13011, 13013, 13014
kFIRInstanceIDMessageCodeStore000 = 13000,
kFIRInstanceIDMessageCodeStore001 = 13001,
kFIRInstanceIDMessageCodeStore002 = 13002,
kFIRInstanceIDMessageCodeStore003 = 13003,
kFIRInstanceIDMessageCodeStore006 = 13006,
kFIRInstanceIDMessageCodeStore009 = 13009,
kFIRInstanceIDMessageCodeStore012 = 13012,
// FIRInstanceIDTokenManager.m
// DO NOT USE 14002, 14005
kFIRInstanceIDMessageCodeTokenManager000 = 14000,
kFIRInstanceIDMessageCodeTokenManager001 = 14001,
kFIRInstanceIDMessageCodeTokenManager003 = 14003,
kFIRInstanceIDMessageCodeTokenManager004 = 14004,
kFIRInstanceIDMessageCodeTokenManagerErrorDeletingFCMTokensOnAppReset = 14006,
kFIRInstanceIDMessageCodeTokenManagerDeletedFCMTokensOnAppReset = 14007,
kFIRInstanceIDMessageCodeTokenManagerSavedAppVersion = 14008,
kFIRInstanceIDMessageCodeTokenManagerErrorInvalidatingAllTokens = 14009,
kFIRInstanceIDMessageCodeTokenManagerAPNSChanged = 14010,
kFIRInstanceIDMessageCodeTokenManagerAPNSChangedTokenInvalidated = 14011,
kFIRInstanceIDMessageCodeTokenManagerInvalidateStaleToken = 14012,
// FIRInstanceIDTokenStore.m
// DO NOT USE 15002 - 15013
kFIRInstanceIDMessageCodeTokenStore000 = 15000,
kFIRInstanceIDMessageCodeTokenStore001 = 15001,
kFIRInstanceIDMessageCodeTokenStoreExceptionUnarchivingTokenInfo = 15015,
// DO NOT USE 16000, 18004
// FIRInstanceIDUtilities.m
kFIRInstanceIDMessageCodeUtilitiesMissingBundleIdentifier = 18000,
kFIRInstanceIDMessageCodeUtilitiesAppEnvironmentUtilNotAvailable = 18001,
kFIRInstanceIDMessageCodeUtilitiesCannotGetHardwareModel = 18002,
kFIRInstanceIDMessageCodeUtilitiesCannotGetSystemVersion = 18003,
// FIRInstanceIDTokenOperation.m
kFIRInstanceIDMessageCodeTokenOperationFailedToSignParams = 19000,
// FIRInstanceIDTokenFetchOperation.m
// DO NOT USE 20004, 20005
kFIRInstanceIDMessageCodeTokenFetchOperationFetchRequest = 20000,
kFIRInstanceIDMessageCodeTokenFetchOperationRequestError = 20001,
kFIRInstanceIDMessageCodeTokenFetchOperationBadResponse = 20002,
kFIRInstanceIDMessageCodeTokenFetchOperationBadTokenStructure = 20003,
// FIRInstanceIDTokenDeleteOperation.m
kFIRInstanceIDMessageCodeTokenDeleteOperationFetchRequest = 21000,
kFIRInstanceIDMessageCodeTokenDeleteOperationRequestError = 21001,
kFIRInstanceIDMessageCodeTokenDeleteOperationBadResponse = 21002,
// FIRInstanceIDTokenInfo.m
kFIRInstanceIDMessageCodeTokenInfoBadAPNSInfo = 22000,
kFIRInstanceIDMessageCodeTokenInfoFirebaseAppIDChanged = 22001,
kFIRInstanceIDMessageCodeTokenInfoLocaleChanged = 22002,
// FIRInstanceIDKeychain.m
kFIRInstanceIDKeychainReadItemError = 23000,
kFIRInstanceIDKeychainAddItemError = 23001,
kFIRInstanceIDKeychainDeleteItemError = 23002,
kFIRInstanceIDKeychainCreateKeyPairError = 23003,
kFIRInstanceIDKeychainUpdateItemError = 23004,
// FIRInstanceIDStringEncoding.m
kFIRInstanceIDStringEncodingBufferUnderflow = 24000,
kFIRInstanceIDStringEncodingBufferOverflow = 24001,
};
@@ -0,0 +1,49 @@
/*
* 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 "FIRInstanceID.h"
#import "FIRInstanceIDCheckinService.h"
/**
* Internal API used by Firebase SDK teams by calling in reflection or internal teams.
*/
// TODO(chliangGoogle) Rename this to Internal.
@interface FIRInstanceID (Private)
/**
* Fetches checkin info for the app. If the app has valid cached checkin preferences
* they are returned instead of making a network request.
*
* @param handler The completion handler to invoke once the request has completed.
*/
- (void)fetchCheckinInfoWithHandler:(nullable FIRInstanceIDDeviceCheckinCompletion)handler;
/**
* Get the InstanceID for the app. If an ID was created before and cached
* successfully we will return that ID. If no cached ID exists we create
* a new ID, cache it and return that.
*
* This is a blocking call and should not really be called on the main thread.
*
* @param error The error object that represents the error while trying to
* retrieve the instance id.
*
* @return The InstanceID for the app.
*/
- (nullable NSString *)appInstanceID:(NSError *_Nullable *_Nullable)error;
@end
@@ -0,0 +1,42 @@
/*
* 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 "FIRInstanceID+Private.h"
#import "FIRInstanceIDAuthService.h"
#import "FIRInstanceIDKeyPairStore.h"
#import "FIRInstanceIDTokenManager.h"
@interface FIRInstanceID ()
@property(nonatomic, readonly, strong) FIRInstanceIDTokenManager *tokenManager;
@property(nonatomic, readonly, strong) FIRInstanceIDKeyPairStore *keyPairStore;
@end
@implementation FIRInstanceID (Private)
// This method just wraps our pre-configured auth service to make the request.
// This method is only needed by first-party users, like Remote Config.
- (void)fetchCheckinInfoWithHandler:(FIRInstanceIDDeviceCheckinCompletion)handler {
[self.tokenManager.authService fetchCheckinInfoWithHandler:handler];
}
- (NSString *)appInstanceID:(NSError **)error {
return [self.keyPairStore appIdentityWithError:error];
}
@end
File diff suppressed because it is too large Load Diff
@@ -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.
*/
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
/**
* Represents an APNS device token and whether its environment is for sandbox.
* It can read from and write to an NSDictionary for simple serialization.
*/
@interface FIRInstanceIDAPNSInfo : NSObject <NSCoding>
/// The APNs device token, provided by the OS to the application delegate
@property(nonatomic, readonly, strong) NSData *deviceToken;
/// Represents whether or not this is deviceToken is for the sandbox
/// environment, or production.
@property(nonatomic, readonly, getter=isSandbox) BOOL sandbox;
/**
* Initializes the receiver with an APNs device token, and boolean
* representing whether that token is for the sandbox environment.
*
* @param deviceToken The APNs device token typically provided by the
* operating system.
* @param isSandbox YES if the APNs device token is for the sandbox
* environment, or NO if it is for production.
* @return An instance of FIRInstanceIDAPNSInfo.
*/
- (instancetype)initWithDeviceToken:(NSData *)deviceToken isSandbox:(BOOL)isSandbox;
/**
* Initializes the receiver from a token options dictionary containing data
* within the `kFIRInstanceIDTokenOptionsAPNSKey` and
* `kFIRInstanceIDTokenOptionsAPNSIsSandboxKey` keys. The token should be an
* NSData blob, and the sandbox value should be an NSNumber
* representing a boolean value.
*
* @param dictionary A dictionary containing values under the keys
* `kFIRInstanceIDTokenOptionsAPNSKey` and
* `kFIRInstanceIDTokenOptionsAPNSIsSandboxKey`.
* @return An instance of FIRInstanceIDAPNSInfo, or nil if the
* dictionary data was invalid or missing.
*/
- (nullable instancetype)initWithTokenOptionsDictionary:(NSDictionary *)dictionary;
- (BOOL)isEqualToAPNSInfo:(FIRInstanceIDAPNSInfo *)otherInfo;
@end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,79 @@
/*
* 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 "FIRInstanceIDAPNSInfo.h"
#import "FIRInstanceIDConstants.h"
/// The key used to find the APNs device token in an archive.
NSString *const kFIRInstanceIDAPNSInfoTokenKey = @"device_token";
/// The key used to find the sandbox value in an archive.
NSString *const kFIRInstanceIDAPNSInfoSandboxKey = @"sandbox";
@implementation FIRInstanceIDAPNSInfo
- (instancetype)initWithDeviceToken:(NSData *)deviceToken isSandbox:(BOOL)isSandbox {
self = [super init];
if (self) {
_deviceToken = [deviceToken copy];
_sandbox = isSandbox;
}
return self;
}
- (instancetype)initWithTokenOptionsDictionary:(NSDictionary *)dictionary {
id deviceToken = dictionary[kFIRInstanceIDTokenOptionsAPNSKey];
if (![deviceToken isKindOfClass:[NSData class]]) {
return nil;
}
id isSandbox = dictionary[kFIRInstanceIDTokenOptionsAPNSIsSandboxKey];
if (![isSandbox isKindOfClass:[NSNumber class]]) {
return nil;
}
self = [super init];
if (self) {
_deviceToken = (NSData *)deviceToken;
_sandbox = ((NSNumber *)isSandbox).boolValue;
}
return self;
}
#pragma mark - NSCoding
- (nullable instancetype)initWithCoder:(NSCoder *)aDecoder {
id deviceToken = [aDecoder decodeObjectForKey:kFIRInstanceIDAPNSInfoTokenKey];
if (![deviceToken isKindOfClass:[NSData class]]) {
return nil;
}
BOOL isSandbox = [aDecoder decodeBoolForKey:kFIRInstanceIDAPNSInfoSandboxKey];
return [self initWithDeviceToken:(NSData *)deviceToken isSandbox:isSandbox];
}
- (void)encodeWithCoder:(NSCoder *)aCoder {
[aCoder encodeObject:self.deviceToken forKey:kFIRInstanceIDAPNSInfoTokenKey];
[aCoder encodeBool:self.sandbox forKey:kFIRInstanceIDAPNSInfoSandboxKey];
}
- (BOOL)isEqualToAPNSInfo:(FIRInstanceIDAPNSInfo *)otherInfo {
if ([super isEqual:otherInfo]) {
return YES;
}
return ([self.deviceToken isEqualToData:otherInfo.deviceToken] &&
self.isSandbox == otherInfo.isSandbox);
}
@end
@@ -0,0 +1,98 @@
/*
* 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>
extern NSString *__nonnull const kFIRInstanceIDKeychainWildcardIdentifier;
NS_ASSUME_NONNULL_BEGIN
/**
* Wrapper around storing FCM auth data in iOS keychain.
*/
@interface FIRInstanceIDAuthKeychain : NSObject
/**
* Designated Initializer. Init a generic `SecClassGenericPassword` keychain with `identifier`
* as the `kSecAttrGeneric`.
*
* @param identifier The generic attribute to be used by the keychain.
*
* @return A Keychain object with `kSecAttrGeneric` attribute set to identifier.
*/
- (instancetype)initWithIdentifier:(NSString *)identifier;
/**
* Get keychain items matching the given service and account. The service and/or account
* can be a wildcard (`kFIRInstanceIDKeychainWildcardIdentifier`), which case the query
* will include all items matching any services and/or accounts.
*
* @param service The kSecAttrService used to save the password. Can be wildcard.
* @param account The kSecAttrAccount used to save the password. Can be wildcard.
*
* @return An array of |NSData|s matching the provided inputs.
*/
- (NSArray<NSData *> *)itemsMatchingService:(NSString *)service account:(NSString *)account;
/**
* Get keychain item for a given service and account.
*
* @param service The kSecAttrService used to save the password.
* @param account The kSecAttrAccount used to save the password.
*
* @return A cached keychain item for a given account and service, or nil if it was not
* found or could not be retrieved.
*/
- (NSData *)dataForService:(NSString *)service account:(NSString *)account;
/**
* Remove the cached items from the keychain matching the service, account and access group.
* In case the items do not exist, YES is returned but with a valid error object with code
* `errSecItemNotFound`.
*
* @param service The kSecAttrService used to save the password.
* @param account The kSecAttrAccount used to save the password.
* @param handler The callback handler which is invoked when the remove operation is complete, with
* an error if there is any.
*/
- (void)removeItemsMatchingService:(NSString *)service
account:(NSString *)account
handler:(nullable void (^)(NSError *error))handler;
/**
* Set the data for a given service and account with a specific accessibility. If
* accessibility is NULL we use `kSecAttrAccessibleAlwaysThisDeviceOnly` which
* prevents backup and restore to iCloud, and works for app extension that can
* execute right after a device is restarted (and not unlocked).
*
* @param data The data to save.
* @param service The `kSecAttrService` used to save the password.
* @param accessibility The `kSecAttrAccessibility` used to save the password. If NULL
* set this to `kSecAttrAccessibleAlwaysThisDeviceOnly`.
* @param account The `kSecAttrAccount` used to save the password.
* @param handler The callback handler which is invoked when the add operation is complete,
* with an error if there is any.
*
*/
- (void)setData:(NSData *)data
forService:(NSString *)service
accessibility:(nullable CFTypeRef)accessibility
account:(NSString *)account
handler:(nullable void (^)(NSError *))handler;
@end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,223 @@
/*
* 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 "FIRInstanceIDAuthKeyChain.h"
#import "FIRInstanceIDKeychain.h"
#import "FIRInstanceIDLogger.h"
/**
* The error type representing why we couldn't read data from the keychain.
*/
typedef NS_ENUM(int, FIRInstanceIDKeychainErrorType) {
kFIRInstanceIDKeychainErrorBadArguments = -1301,
};
NSString *const kFIRInstanceIDKeychainWildcardIdentifier = @"*";
@interface FIRInstanceIDAuthKeychain ()
@property(nonatomic, copy) NSString *generic;
// cachedKeychainData is keyed by service and account, the value is an array of NSData.
// It is used to cache the tokens per service, per account, as well as checkin data per service,
// per account inside the keychain.
@property(nonatomic)
NSMutableDictionary<NSString *, NSMutableDictionary<NSString *, NSArray<NSData *> *> *>
*cachedKeychainData;
@end
@implementation FIRInstanceIDAuthKeychain
- (instancetype)initWithIdentifier:(NSString *)identifier {
self = [super init];
if (self) {
_generic = [identifier copy];
_cachedKeychainData = [[NSMutableDictionary alloc] init];
}
return self;
}
+ (NSMutableDictionary *)keychainQueryForService:(NSString *)service
account:(NSString *)account
generic:(NSString *)generic {
NSDictionary *query = @{(__bridge id)kSecClass : (__bridge id)kSecClassGenericPassword};
NSMutableDictionary *finalQuery = [NSMutableDictionary dictionaryWithDictionary:query];
if ([generic length] && ![kFIRInstanceIDKeychainWildcardIdentifier isEqualToString:generic]) {
finalQuery[(__bridge NSString *)kSecAttrGeneric] = generic;
}
if ([account length] && ![kFIRInstanceIDKeychainWildcardIdentifier isEqualToString:account]) {
finalQuery[(__bridge NSString *)kSecAttrAccount] = account;
}
if ([service length] && ![kFIRInstanceIDKeychainWildcardIdentifier isEqualToString:service]) {
finalQuery[(__bridge NSString *)kSecAttrService] = service;
}
return finalQuery;
}
- (NSMutableDictionary *)keychainQueryForService:(NSString *)service account:(NSString *)account {
return [[self class] keychainQueryForService:service account:account generic:self.generic];
}
- (NSArray<NSData *> *)itemsMatchingService:(NSString *)service account:(NSString *)account {
// If query wildcard service, it asks for all the results, which always query from keychain.
if (![service isEqualToString:kFIRInstanceIDKeychainWildcardIdentifier] &&
![account isEqualToString:kFIRInstanceIDKeychainWildcardIdentifier] &&
_cachedKeychainData[service][account]) {
// As long as service, account array exist, even it's empty, it means we've queried it before,
// returns the cache value.
return _cachedKeychainData[service][account];
}
NSMutableDictionary *keychainQuery = [self keychainQueryForService:service account:account];
NSMutableArray<NSData *> *results;
keychainQuery[(__bridge id)kSecReturnData] = (__bridge id)kCFBooleanTrue;
#if TARGET_OS_IOS || TARGET_OS_TV
keychainQuery[(__bridge id)kSecReturnAttributes] = (__bridge id)kCFBooleanTrue;
keychainQuery[(__bridge id)kSecMatchLimit] = (__bridge id)kSecMatchLimitAll;
// FIRInstanceIDKeychain should only take a query and return a result, will handle the query here.
NSArray *passwordInfos =
CFBridgingRelease([[FIRInstanceIDKeychain sharedInstance] itemWithQuery:keychainQuery]);
#elif TARGET_OS_OSX
keychainQuery[(__bridge id)kSecMatchLimit] = (__bridge id)kSecMatchLimitOne;
NSData *passwordInfos =
CFBridgingRelease([[FIRInstanceIDKeychain sharedInstance] itemWithQuery:keychainQuery]);
#endif
if (!passwordInfos) {
// Nothing was found, simply return from this sync block.
// Make sure to label the cache entry empty, signaling that we've queried this entry.
if ([service isEqualToString:kFIRInstanceIDKeychainWildcardIdentifier] ||
[account isEqualToString:kFIRInstanceIDKeychainWildcardIdentifier]) {
// Do not update cache if it's wildcard query.
return @[];
} else if (_cachedKeychainData[service]) {
[_cachedKeychainData[service] setObject:@[] forKey:account];
} else {
[_cachedKeychainData setObject:[@{account : @[]} mutableCopy] forKey:service];
}
return @[];
}
results = [[NSMutableArray alloc] init];
#if TARGET_OS_IOS || TARGET_OS_TV
NSInteger numPasswords = passwordInfos.count;
for (NSUInteger i = 0; i < numPasswords; i++) {
NSDictionary *passwordInfo = [passwordInfos objectAtIndex:i];
if (passwordInfo[(__bridge id)kSecValueData]) {
[results addObject:passwordInfo[(__bridge id)kSecValueData]];
}
}
#elif TARGET_OS_OSX
[results addObject:passwordInfos];
#endif
// We query the keychain because it didn't exist in cache, now query is done, update the result in
// the cache.
if ([service isEqualToString:kFIRInstanceIDKeychainWildcardIdentifier] ||
[account isEqualToString:kFIRInstanceIDKeychainWildcardIdentifier]) {
// Do not update cache if it's wildcard query.
return [results copy];
} else if (_cachedKeychainData[service]) {
[_cachedKeychainData[service] setObject:[results copy] forKey:account];
} else {
NSMutableDictionary *entry = [@{account : [results copy]} mutableCopy];
[_cachedKeychainData setObject:entry forKey:service];
}
return [results copy];
}
- (NSData *)dataForService:(NSString *)service account:(NSString *)account {
NSArray<NSData *> *items = [self itemsMatchingService:service account:account];
// If items is nil or empty, nil will be returned.
return items.firstObject;
}
- (void)removeItemsMatchingService:(NSString *)service
account:(NSString *)account
handler:(void (^)(NSError *error))handler {
if ([service isEqualToString:kFIRInstanceIDKeychainWildcardIdentifier]) {
// Delete all keychain items.
_cachedKeychainData = [[NSMutableDictionary alloc] init];
} else if ([account isEqualToString:kFIRInstanceIDKeychainWildcardIdentifier]) {
// Delete all entries under service,
if (_cachedKeychainData[service]) {
_cachedKeychainData[service] = [[NSMutableDictionary alloc] init];
}
} else if (_cachedKeychainData[service]) {
// We should keep the service/account entry instead of nil so we know
// it's "empty entry" instead of "not query from keychain yet".
[_cachedKeychainData[service] setObject:@[] forKey:account];
} else {
[_cachedKeychainData setObject:[@{account : @[]} mutableCopy] forKey:service];
}
NSMutableDictionary *keychainQuery = [self keychainQueryForService:service account:account];
[[FIRInstanceIDKeychain sharedInstance] removeItemWithQuery:keychainQuery handler:handler];
}
- (void)setData:(NSData *)data
forService:(NSString *)service
accessibility:(CFTypeRef)accessibility
account:(NSString *)account
handler:(void (^)(NSError *))handler {
if ([service isEqualToString:kFIRInstanceIDKeychainWildcardIdentifier] ||
[account isEqualToString:kFIRInstanceIDKeychainWildcardIdentifier]) {
if (handler) {
handler([NSError errorWithDomain:kFIRInstanceIDKeychainErrorDomain
code:kFIRInstanceIDKeychainErrorBadArguments
userInfo:nil]);
}
return;
}
[self removeItemsMatchingService:service
account:account
handler:^(NSError *error) {
if (error) {
if (handler) {
handler(error);
}
return;
}
if (data.length > 0) {
NSMutableDictionary *keychainQuery =
[self keychainQueryForService:service account:account];
keychainQuery[(__bridge id)kSecValueData] = data;
if (accessibility != NULL) {
keychainQuery[(__bridge id)kSecAttrAccessible] =
(__bridge id)accessibility;
} else {
// Defaults to No backup
keychainQuery[(__bridge id)kSecAttrAccessible] =
(__bridge id)kSecAttrAccessibleAlwaysThisDeviceOnly;
}
[[FIRInstanceIDKeychain sharedInstance]
addItemWithQuery:keychainQuery
handler:handler];
}
}];
// Set the cache value. This must happen after removeItemsMatchingService:account:handler was
// called, so the cache value was reset before setting a new value.
if (_cachedKeychainData[service]) {
if (_cachedKeychainData[service][account]) {
_cachedKeychainData[service][account] = @[ data ];
} else {
[_cachedKeychainData[service] setObject:@[ data ] forKey:account];
}
} else {
[_cachedKeychainData setObject:[@{account : @[ data ]} mutableCopy] forKey:service];
}
}
@end
@@ -0,0 +1,91 @@
/*
* 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>
#import "FIRInstanceIDCheckinService.h"
@class FIRInstanceIDCheckinPreferences;
@class FIRInstanceIDStore;
/**
* FIRInstanceIDAuthService is responsible for retrieving, caching, and supplying checkin info
* for the rest of Instance ID. A checkin can be scheduled, meaning that it will keep retrying the
* checkin request until it is successful. A checkin can also be requested directly, with a
* completion handler.
*/
@interface FIRInstanceIDAuthService : NSObject
/**
* Used only for testing. In addition to taking a store (for locally caching the checkin info), it
* also takes a checkinService.
*/
- (instancetype)initWithCheckinService:(FIRInstanceIDCheckinService *)checkinService
store:(FIRInstanceIDStore *)store;
/**
* Initializes the auth service given a store (which provides the local caching of checkin info).
* This initializer will create its own instance of FIRInstanceIDCheckinService.
*/
- (instancetype)initWithStore:(FIRInstanceIDStore *)store;
#pragma mark - Checkin Service
/**
* Checks if the current deviceID and secret are valid or not.
*
* @return YES if the checkin credentials are valid else NO.
*/
- (BOOL)hasValidCheckinInfo;
/**
* Fetch checkin info from the server. This would usually refresh the existing
* checkin credentials for the current app.
*
* @param handler The completion handler to invoke once the checkin info has been
* refreshed.
*/
- (void)fetchCheckinInfoWithHandler:(FIRInstanceIDDeviceCheckinCompletion)handler;
/**
* Schedule checkin. Will hit the network only if the currently loaded checkin
* preferences are stale.
*
* @param immediately YES if we want it to be scheduled immediately else NO.
*/
- (void)scheduleCheckin:(BOOL)immediately;
/**
* Returns the checkin preferences currently loaded in memory. The Checkin preferences
* can be either valid or invalid.
*
* @return The checkin preferences loaded in memory.
*/
- (FIRInstanceIDCheckinPreferences *)checkinPreferences;
/**
* Cancels any ongoing checkin fetch, if any.
*/
- (void)stopCheckinRequest;
/**
* Resets the checkin information.
*
* @param handler The callback handler which is invoked when checkin reset is complete,
* with an error if there is any.
*/
- (void)resetCheckinWithHandler:(void (^)(NSError *error))handler;
@end
@@ -0,0 +1,302 @@
/*
* 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 "FIRInstanceIDAuthService.h"
#import "FIRInstanceIDCheckinPreferences+Internal.h"
#import "FIRInstanceIDCheckinPreferences.h"
#import "FIRInstanceIDCheckinPreferences_Private.h"
#import "FIRInstanceIDConstants.h"
#import "FIRInstanceIDDefines.h"
#import "FIRInstanceIDLogger.h"
#import "FIRInstanceIDStore.h"
#import "NSError+FIRInstanceID.h"
// Max time interval between checkin retry in seconds.
static const int64_t kMaxCheckinRetryIntervalInSeconds = 1 << 5;
@interface FIRInstanceIDAuthService ()
// Used to retrieve and cache the checkin info to disk and Keychain.
@property(nonatomic, readwrite, strong) FIRInstanceIDStore *store;
// Used to perform single checkin fetches.
@property(nonatomic, readwrite, strong) FIRInstanceIDCheckinService *checkinService;
// The current checkin info. It will be compared to what is retrieved to determine whether it is
// different than what is in the cache.
@property(nonatomic, readwrite, strong) FIRInstanceIDCheckinPreferences *checkinPreferences;
// This array will track multiple handlers waiting for checkin to be performed. When a checkin
// request completes, all the handlers will be notified.
// Changes to the checkinHandlers array should happen in a thread-safe manner.
@property(nonatomic, readonly, strong)
NSMutableArray<FIRInstanceIDDeviceCheckinCompletion> *checkinHandlers;
// This is set to true if there is a checkin request in-flight.
@property(atomic, readwrite, assign) BOOL isCheckinInProgress;
// This timer is used a perform checkin retries. It is cancellable.
@property(atomic, readwrite, strong) NSTimer *scheduledCheckinTimer;
// The number of times checkin has been retried during a scheduled checkin.
@property(atomic, readwrite, assign) int checkinRetryCount;
@end
@implementation FIRInstanceIDAuthService
- (instancetype)initWithCheckinService:(FIRInstanceIDCheckinService *)checkinService
store:(FIRInstanceIDStore *)store {
self = [super init];
if (self) {
_store = store;
_checkinPreferences = [_store cachedCheckinPreferences];
_checkinService = checkinService;
_checkinHandlers = [NSMutableArray array];
}
return self;
}
- (void)dealloc {
[_scheduledCheckinTimer invalidate];
}
- (instancetype)initWithStore:(FIRInstanceIDStore *)store {
FIRInstanceIDCheckinService *checkinService = [[FIRInstanceIDCheckinService alloc] init];
return [self initWithCheckinService:checkinService store:store];
}
#pragma mark - Schedule Checkin
- (void)scheduleCheckin:(BOOL)immediately {
// Checkin is still valid, so a remote checkin is not required.
if ([self.checkinPreferences hasValidCheckinInfo]) {
return;
}
// Checkin is already scheduled, so this (non-immediate) request can be ignored.
if (!immediately && [self.scheduledCheckinTimer isValid]) {
FIRInstanceIDLoggerDebug(kFIRInstanceIDMessageCodeAuthService000,
@"Checkin sync already scheduled. Will not schedule.");
return;
}
if (immediately) {
[self performScheduledCheckin];
} else {
int64_t checkinRetryDuration = [self calculateNextCheckinRetryIntervalInSeconds];
[self startCheckinTimerWithDuration:(NSTimeInterval)checkinRetryDuration];
}
}
- (void)startCheckinTimerWithDuration:(NSTimeInterval)timerDuration {
self.scheduledCheckinTimer =
[NSTimer scheduledTimerWithTimeInterval:timerDuration
target:self
selector:@selector(onScheduledCheckinTimerFired:)
userInfo:nil
repeats:NO];
// Add some tolerance to the timer, to allow iOS to be more flexible with this timer
self.scheduledCheckinTimer.tolerance = 0.5;
}
- (void)clearScheduledCheckinTimer {
[self.scheduledCheckinTimer invalidate];
self.scheduledCheckinTimer = nil;
}
- (void)onScheduledCheckinTimerFired:(NSTimer *)timer {
[self performScheduledCheckin];
}
- (void)performScheduledCheckin {
// No checkin scheduled as of now.
[self clearScheduledCheckinTimer];
// Checkin is still valid, so a remote checkin is not required.
if ([self.checkinPreferences hasValidCheckinInfo]) {
return;
}
FIRInstanceID_WEAKIFY(self);
[self
fetchCheckinInfoWithHandler:^(FIRInstanceIDCheckinPreferences *preferences, NSError *error) {
FIRInstanceID_STRONGIFY(self);
self.checkinRetryCount++;
if (error) {
FIRInstanceIDLoggerDebug(kFIRInstanceIDMessageCodeAuthService001, @"Checkin error %@.",
error);
dispatch_async(dispatch_get_main_queue(), ^{
// Schedule another checkin
[self scheduleCheckin:NO];
});
} else {
FIRInstanceIDLoggerDebug(kFIRInstanceIDMessageCodeAuthService002, @"Checkin success.");
}
}];
}
- (int64_t)calculateNextCheckinRetryIntervalInSeconds {
// persistent failures can lead to overflow prevent that.
if (self.checkinRetryCount >= 10) {
return kMaxCheckinRetryIntervalInSeconds;
}
return MIN(1 << self.checkinRetryCount, kMaxCheckinRetryIntervalInSeconds);
}
#pragma mark - Checkin Service
- (BOOL)hasValidCheckinInfo {
return [self.checkinPreferences hasValidCheckinInfo];
}
- (void)fetchCheckinInfoWithHandler:(nonnull FIRInstanceIDDeviceCheckinCompletion)handler {
// Perform any changes to self.checkinHandlers and _isCheckinInProgress in a thread-safe way.
@synchronized(self) {
[self.checkinHandlers addObject:handler];
if (_isCheckinInProgress) {
// Nothing more to do until our checkin request is done
FIRInstanceIDLoggerDebug(kFIRInstanceIDMessageCodeAuthServiceCheckinInProgress,
@"Checkin is in progress\n");
return;
}
}
// Checkin is still valid, so a remote checkin is not required.
if ([self.checkinPreferences hasValidCheckinInfo]) {
[self notifyCheckinHandlersWithCheckin:self.checkinPreferences error:nil];
return;
}
@synchronized(self) {
_isCheckinInProgress = YES;
}
[self.checkinService
checkinWithExistingCheckin:self.checkinPreferences
completion:^(FIRInstanceIDCheckinPreferences *checkinPreferences,
NSError *error) {
@synchronized(self) {
self->_isCheckinInProgress = NO;
}
if (error) {
FIRInstanceIDLoggerDebug(kFIRInstanceIDMessageCodeAuthService003,
@"Failed to checkin device %@", error);
[self notifyCheckinHandlersWithCheckin:nil error:error];
return;
}
FIRInstanceIDLoggerDebug(kFIRInstanceIDMessageCodeAuthService004,
@"Successfully got checkin credentials");
BOOL hasSameCachedPreferences =
[self cachedCheckinMatchesCheckin:checkinPreferences];
checkinPreferences.hasPreCachedAuthCredentials = hasSameCachedPreferences;
// Update to the most recent checkin preferences
self.checkinPreferences = checkinPreferences;
// Save the checkin info to disk
// Keychain might not be accessible, so confirm that checkin preferences can
// be saved
[self.store
saveCheckinPreferences:checkinPreferences
handler:^(NSError *checkinSaveError) {
if (checkinSaveError && !hasSameCachedPreferences) {
// The checkin info was new, but it couldn't be
// written to the Keychain. Delete any stuff that was
// cached in memory. This doesn't delete any
// previously persisted preferences.
FIRInstanceIDLoggerError(
kFIRInstanceIDMessageCodeService004,
@"Unable to save checkin info, resetting "
@"checkin preferences "
"in memory.");
[checkinPreferences reset];
[self
notifyCheckinHandlersWithCheckin:nil
error:
checkinSaveError];
} else {
// The checkin is either new, or it was the same (and
// it couldn't be saved). Either way, report that the
// checkin preferences were received successfully.
[self notifyCheckinHandlersWithCheckin:
checkinPreferences
error:nil];
if (!hasSameCachedPreferences) {
// Checkin is new.
// Notify any listeners that might be waiting for
// checkin to be fetched, such as Firebase
// Messaging (for its MCS connection).
dispatch_async(dispatch_get_main_queue(), ^{
[[NSNotificationCenter defaultCenter]
postNotificationName:
kFIRInstanceIDCheckinFetchedNotification
object:nil];
});
}
}
}];
}];
}
- (FIRInstanceIDCheckinPreferences *)checkinPreferences {
return _checkinPreferences;
}
- (void)stopCheckinRequest {
[self.checkinService stopFetching];
}
- (void)resetCheckinWithHandler:(void (^)(NSError *error))handler {
[self.store removeCheckinPreferencesWithHandler:^(NSError *error) {
if (!error) {
self.checkinPreferences = nil;
}
if (handler) {
handler(error);
}
}];
}
#pragma mark - Private
/**
* Goes through the current list of checkin handlers and fires them with the same checkin and/or
* error info. The checkin handlers will get cleared after.
*/
- (void)notifyCheckinHandlersWithCheckin:(nullable FIRInstanceIDCheckinPreferences *)checkin
error:(nullable NSError *)error {
@synchronized(self) {
for (FIRInstanceIDDeviceCheckinCompletion handler in self.checkinHandlers) {
handler(checkin, error);
}
[self.checkinHandlers removeAllObjects];
}
}
/**
* Given a |checkin|, it will compare it to the current checkinPreferences to see if the
* deviceID and secretToken are the same.
*/
- (BOOL)cachedCheckinMatchesCheckin:(FIRInstanceIDCheckinPreferences *)checkin {
if (self.checkinPreferences && checkin) {
return ([self.checkinPreferences.deviceID isEqualToString:checkin.deviceID] &&
[self.checkinPreferences.secretToken isEqualToString:checkin.secretToken]);
}
return NO;
}
@end
@@ -0,0 +1,81 @@
/*
* 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>
@interface FIRInstanceIDBackupExcludedPlist : NSObject
/**
* Caches the plist contents in memory so we don't hit the disk each time we want
* to query something in the plist. This is loaded lazily i.e. if you write to the
* plist the contents you want to write will be stored here if the write was
* successful. The other case where it is loaded is if you read the plist contents
* by calling `contentAsDictionary`.
*
* In case you write to the plist and then try to read the file using
* `contentAsDictionary` we would just return the cachedPlistContents since it would
* represent the disk contents.
*/
@property(nonatomic, readonly, strong) NSDictionary *cachedPlistContents;
/**
* Init a backup excluded plist file.
*
* @param fileName The filename for the plist file.
* @param subDirectory The subdirectory in Application Support to save the plist.
*
* @return Helper which allows to read write data to a backup excluded plist.
*/
- (instancetype)initWithFileName:(NSString *)fileName subDirectory:(NSString *)subDirectory;
/**
* Write dictionary data to the backup excluded plist file. If the file does not exist
* it would be created before writing to it.
*
* @param dict The data to be written to the plist.
* @param error The error object if any while writing the data.
*
* @return YES if the write was successful else NO.
*/
- (BOOL)writeDictionary:(NSDictionary *)dict error:(NSError **)error;
/**
* Delete the backup excluded plist created with the above filename.
*
* @param error The error object if any while deleting the file.
*
* @return YES If the delete was successful else NO.
*/
- (BOOL)deleteFile:(NSError **)error;
/**
* The contents of the plist file. We also store the contents of the file in-memory.
* If the in-memory contents are valid we return the in-memory contents else we read
* the file from disk.
*
* @return A dictionary object that contains the contents of the plist file if the file
* exists else nil.
*/
- (NSDictionary *)contentAsDictionary;
/**
* Check if the plist exists on the disk or not.
*
* @return YES if the file exists on the disk else NO.
*/
- (BOOL)doesFileExist;
@end
@@ -0,0 +1,202 @@
/*
* 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 "FIRInstanceIDBackupExcludedPlist.h"
#import "FIRInstanceIDLogger.h"
typedef enum : NSUInteger {
FIRInstanceIDPlistDirectoryUnknown,
FIRInstanceIDPlistDirectoryDocuments,
FIRInstanceIDPlistDirectoryApplicationSupport,
} FIRInstanceIDPlistDirectory;
@interface FIRInstanceIDBackupExcludedPlist ()
@property(nonatomic, readwrite, copy) NSString *fileName;
@property(nonatomic, readwrite, copy) NSString *subDirectoryName;
@property(nonatomic, readwrite, assign) BOOL fileInStandardDirectory;
@property(nonatomic, readwrite, strong) NSDictionary *cachedPlistContents;
@end
@implementation FIRInstanceIDBackupExcludedPlist
- (instancetype)initWithFileName:(NSString *)fileName subDirectory:(NSString *)subDirectory {
self = [super init];
if (self) {
_fileName = [fileName copy];
_subDirectoryName = [subDirectory copy];
#if TARGET_OS_IOS
_fileInStandardDirectory = [self moveToApplicationSupportSubDirectory:subDirectory];
#else
// For tvOS and macOS, we never store the content in document folder, so
// the migration is unnecessary.
_fileInStandardDirectory = YES;
#endif
}
return self;
}
- (BOOL)writeDictionary:(NSDictionary *)dict error:(NSError **)error {
NSString *path = [self plistPathInDirectory:[self plistDirectory]];
if (![dict writeToFile:path atomically:YES]) {
FIRInstanceIDLoggerError(kFIRInstanceIDMessageCodeBackupExcludedPlist000,
@"Failed to write to %@.plist", self.fileName);
return NO;
}
// Successfully wrote contents -- change the in-memory contents
self.cachedPlistContents = [dict copy];
NSURL *URL = [NSURL fileURLWithPath:path];
if (error) {
*error = nil;
}
NSDictionary *preferences = [URL resourceValuesForKeys:@[ NSURLIsExcludedFromBackupKey ]
error:error];
if ([preferences[NSURLIsExcludedFromBackupKey] boolValue]) {
return YES;
}
BOOL success = [URL setResourceValue:@(YES) forKey:NSURLIsExcludedFromBackupKey error:error];
if (!success) {
FIRInstanceIDLoggerError(kFIRInstanceIDMessageCodeBackupExcludedPlist001,
@"Error excluding %@ from backup, %@", [URL lastPathComponent],
error ? *error : @"");
}
return success;
}
- (BOOL)deleteFile:(NSError **)error {
BOOL success = YES;
NSString *path = [self plistPathInDirectory:[self plistDirectory]];
if ([[NSFileManager defaultManager] fileExistsAtPath:path]) {
success = [[NSFileManager defaultManager] removeItemAtPath:path error:error];
}
// remove the in-memory contents
self.cachedPlistContents = nil;
return success;
}
- (NSDictionary *)contentAsDictionary {
if (!self.cachedPlistContents) {
NSString *path = [self plistPathInDirectory:[self plistDirectory]];
if ([[NSFileManager defaultManager] fileExistsAtPath:path]) {
self.cachedPlistContents = [[NSDictionary alloc] initWithContentsOfFile:path];
}
}
return self.cachedPlistContents;
}
- (BOOL)moveToApplicationSupportSubDirectory:(NSString *)subDirectoryName {
NSArray *directoryPaths =
NSSearchPathForDirectoriesInDomains([self supportedDirectory], NSUserDomainMask, YES);
// This only going to happen inside iOS so it is an applicationSupportDirectory.
NSString *applicationSupportDirPath = directoryPaths.lastObject;
NSArray *components = @[ applicationSupportDirPath, subDirectoryName ];
NSString *subDirectoryPath = [NSString pathWithComponents:components];
BOOL hasSubDirectory;
if (![[NSFileManager defaultManager] fileExistsAtPath:subDirectoryPath
isDirectory:&hasSubDirectory]) {
// Cannot move to non-existent directory
return NO;
}
if ([self doesFileExistInDirectory:FIRInstanceIDPlistDirectoryDocuments]) {
NSString *oldPlistPath = [self plistPathInDirectory:FIRInstanceIDPlistDirectoryDocuments];
NSString *newPlistPath =
[self plistPathInDirectory:FIRInstanceIDPlistDirectoryApplicationSupport];
if ([self doesFileExistInDirectory:FIRInstanceIDPlistDirectoryApplicationSupport]) {
// File exists in both Documents and ApplicationSupport
return NO;
}
NSError *moveError;
if (![[NSFileManager defaultManager] moveItemAtPath:oldPlistPath
toPath:newPlistPath
error:&moveError]) {
FIRInstanceIDLoggerError(kFIRInstanceIDMessageCodeBackupExcludedPlist002,
@"Failed to move file %@ from %@ to %@. Error: %@", self.fileName,
oldPlistPath, newPlistPath, moveError);
return NO;
}
}
// We moved the file if it existed, otherwise we didn't need to do anything
return YES;
}
- (BOOL)doesFileExist {
return [self doesFileExistInDirectory:[self plistDirectory]];
}
#pragma mark - Private
- (FIRInstanceIDPlistDirectory)plistDirectory {
if (_fileInStandardDirectory) {
return FIRInstanceIDPlistDirectoryApplicationSupport;
} else {
return FIRInstanceIDPlistDirectoryDocuments;
};
}
- (NSString *)plistPathInDirectory:(FIRInstanceIDPlistDirectory)directory {
return [self pathWithName:self.fileName inDirectory:directory];
}
- (NSString *)pathWithName:(NSString *)plistName
inDirectory:(FIRInstanceIDPlistDirectory)directory {
NSArray *directoryPaths;
NSArray *components = @[];
NSString *plistNameWithExtension = [NSString stringWithFormat:@"%@.plist", plistName];
switch (directory) {
case FIRInstanceIDPlistDirectoryDocuments:
directoryPaths =
NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
components = @[ directoryPaths.lastObject, plistNameWithExtension ];
break;
case FIRInstanceIDPlistDirectoryApplicationSupport:
directoryPaths =
NSSearchPathForDirectoriesInDomains([self supportedDirectory], NSUserDomainMask, YES);
components = @[ directoryPaths.lastObject, _subDirectoryName, plistNameWithExtension ];
break;
default:
FIRInstanceIDLoggerError(kFIRInstanceIDMessageCodeBackupExcludedPlistInvalidPlistEnum,
@"Invalid plist directory type: %lu", (unsigned long)directory);
NSAssert(NO, @"Invalid plist directory type: %lu", (unsigned long)directory);
break;
}
return [NSString pathWithComponents:components];
}
- (BOOL)doesFileExistInDirectory:(FIRInstanceIDPlistDirectory)directory {
NSString *path = [self plistPathInDirectory:directory];
return [[NSFileManager defaultManager] fileExistsAtPath:path];
}
- (NSSearchPathDirectory)supportedDirectory {
#if TARGET_OS_TV
return NSCachesDirectory;
#else
return NSApplicationSupportDirectory;
#endif
}
@end
@@ -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.
*/
#import "FIRInstanceIDCheckinPreferences.h"
@interface FIRInstanceIDCheckinPreferences (Internal)
/**
* Parse the checkin auth credentials saved in the Keychain to initialize checkin
* preferences.
*
* @param keychainContent The checkin auth credentials saved in the Keychain.
*
* @return A valid checkin preferences object if the checkin auth credentials in the
* keychain can be parsed successfully else nil.
*/
+ (FIRInstanceIDCheckinPreferences *)preferencesFromKeychainContents:(NSString *)keychainContent;
/**
* Default initializer for InstanceID checkin preferences.
*
* @param deviceID The deviceID for the app.
* @param secretToken The secret token the app uses to authenticate with the server.
*
* @return A checkin preferences object with given deviceID and secretToken.
*/
- (instancetype)initWithDeviceID:(NSString *)deviceID secretToken:(NSString *)secretToken;
/**
* Update checkin preferences from the preferences dict persisted as a plist. The dict contains
* all the checkin preferences retrieved from the server except the deviceID and secret which
* are stored in the Keychain.
*
* @param checkinPlistContent The checkin preferences saved in a plist on the disk.
*/
- (void)updateWithCheckinPlistContents:(NSDictionary *)checkinPlistContent;
/**
* Reset the current checkin preferences object.
*/
- (void)reset;
/**
* The string that contains the checkin auth credentials i.e. deviceID and secret. This
* needs to be stored in the Keychain.
*
* @return The checkin auth credential string containing the deviceID and secret.
*/
- (NSString *)checkinKeychainContent;
@end
@@ -0,0 +1,112 @@
/*
* 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 "FIRInstanceIDCheckinPreferences+Internal.h"
#import "FIRInstanceIDCheckinService.h"
#import "FIRInstanceIDUtilities.h"
static NSString *const kCheckinKeychainContentSeparatorString = @"|";
@interface FIRInstanceIDCheckinPreferences ()
@property(nonatomic, readwrite, copy) NSString *deviceID;
@property(nonatomic, readwrite, copy) NSString *secretToken;
@property(nonatomic, readwrite, copy) NSString *digest;
@property(nonatomic, readwrite, copy) NSString *versionInfo;
@property(nonatomic, readwrite, copy) NSString *deviceDataVersion;
@property(nonatomic, readwrite, strong) NSMutableDictionary *gServicesData;
@property(nonatomic, readwrite, assign) int64_t lastCheckinTimestampMillis;
@end
@implementation FIRInstanceIDCheckinPreferences (Internal)
+ (FIRInstanceIDCheckinPreferences *)preferencesFromKeychainContents:(NSString *)keychainContent {
NSString *deviceID = [self checkinDeviceIDFromKeychainContent:keychainContent];
NSString *secret = [self checkinSecretFromKeychainContent:keychainContent];
if ([deviceID length] && [secret length]) {
return [[FIRInstanceIDCheckinPreferences alloc] initWithDeviceID:deviceID secretToken:secret];
} else {
return nil;
}
}
- (instancetype)initWithDeviceID:(NSString *)deviceID secretToken:(NSString *)secretToken {
self = [super init];
if (self) {
self.deviceID = [deviceID copy];
self.secretToken = [secretToken copy];
}
return self;
}
- (void)reset {
self.deviceID = nil;
self.secretToken = nil;
self.digest = nil;
self.versionInfo = nil;
self.gServicesData = nil;
self.deviceDataVersion = nil;
self.lastCheckinTimestampMillis = 0;
}
- (void)updateWithCheckinPlistContents:(NSDictionary *)checkinPlistContent {
for (NSString *key in checkinPlistContent) {
if ([kFIRInstanceIDDigestStringKey isEqualToString:key]) {
self.digest = [checkinPlistContent[key] copy];
} else if ([kFIRInstanceIDVersionInfoStringKey isEqualToString:key]) {
self.versionInfo = [checkinPlistContent[key] copy];
} else if ([kFIRInstanceIDLastCheckinTimeKey isEqualToString:key]) {
self.lastCheckinTimestampMillis = [checkinPlistContent[key] longLongValue];
} else if ([kFIRInstanceIDGServicesDictionaryKey isEqualToString:key]) {
self.gServicesData = [checkinPlistContent[key] mutableCopy];
} else if ([kFIRInstanceIDDeviceDataVersionKey isEqualToString:key]) {
self.deviceDataVersion = [checkinPlistContent[key] copy];
}
// Otherwise we have some keys we don't care about
}
}
- (NSString *)checkinKeychainContent {
if ([self.deviceID length] && [self.secretToken length]) {
return [NSString stringWithFormat:@"%@%@%@", self.deviceID,
kCheckinKeychainContentSeparatorString, self.secretToken];
} else {
return nil;
}
}
+ (NSString *)checkinDeviceIDFromKeychainContent:(NSString *)keychainContent {
return [self checkinKeychainContent:keychainContent forIndex:0];
}
+ (NSString *)checkinSecretFromKeychainContent:(NSString *)keychainContent {
return [self checkinKeychainContent:keychainContent forIndex:1];
}
+ (NSString *)checkinKeychainContent:(NSString *)keychainContent forIndex:(int)index {
NSArray *keychainComponents =
[keychainContent componentsSeparatedByString:kCheckinKeychainContentSeparatorString];
if (index >= 0 && index < 2 && [keychainComponents count] == 2) {
return keychainComponents[index];
} else {
return nil;
}
}
@end
@@ -0,0 +1,62 @@
/*
* 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>
/**
* The preferences InstanceID loads from checkin server. The deviceID and secret that checkin
* provides is used to authenticate all future requests to the server. Besides the deviceID
* and secret the other information that checkin provides is stored in a plist on the device.
* The deviceID and secret are persisted in the device keychain.
*/
@interface FIRInstanceIDCheckinPreferences : NSObject
/**
* DeviceID and secretToken are the checkin auth credentials and are stored in the Keychain.
*/
@property(nonatomic, readonly, copy) NSString *deviceID;
@property(nonatomic, readonly, copy) NSString *secretToken;
/**
* All the other checkin preferences other than deviceID and secret are stored in a plist.
*/
@property(nonatomic, readonly, copy) NSString *deviceDataVersion;
@property(nonatomic, readonly, copy) NSString *digest;
@property(nonatomic, readonly, copy) NSString *versionInfo;
@property(nonatomic, readonly, assign) int64_t lastCheckinTimestampMillis;
/**
* The content retrieved from checkin server that should be persisted in a plist. This
* doesn't contain the deviceID and secret which are stored in the Keychain since they
* should be more private.
*
* @return The checkin preferences that should be persisted in a plist.
*/
- (NSDictionary *)checkinPlistContents;
/**
* Return whether checkin info exists, valid or not.
*/
- (BOOL)hasCheckinInfo;
/**
* Verify if checkin preferences are valid or not.
*
* @return YES if valid checkin preferences else NO.
*/
- (BOOL)hasValidCheckinInfo;
@end
@@ -0,0 +1,95 @@
/*
* 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 "FIRInstanceIDCheckinPreferences.h"
#import <GoogleUtilities/GULUserDefaults.h>
#import "FIRInstanceIDCheckinService.h"
#import "FIRInstanceIDUtilities.h"
const NSTimeInterval kFIRInstanceIDDefaultCheckinInterval = 7 * 24 * 60 * 60; // 7 days.
@interface FIRInstanceIDCheckinPreferences ()
@property(nonatomic, readwrite, copy) NSString *deviceID;
@property(nonatomic, readwrite, copy) NSString *secretToken;
@property(nonatomic, readwrite, copy) NSString *digest;
@property(nonatomic, readwrite, copy) NSString *versionInfo;
@property(nonatomic, readwrite, copy) NSString *deviceDataVersion;
@property(nonatomic, readwrite, strong) NSMutableDictionary *gServicesData;
@property(nonatomic, readwrite, assign) int64_t lastCheckinTimestampMillis;
// This flag indicates that we have already saved the above deviceID and secret
// to our keychain and hence we don't need to save again. This is helpful since
// on checkin refresh we can avoid writing to the Keychain which can sometimes
// be very buggy. For info check this https://forums.developer.apple.com/thread/4743
@property(nonatomic, readwrite, assign) BOOL hasPreCachedAuthCredentials;
@end
@implementation FIRInstanceIDCheckinPreferences
- (NSDictionary *)checkinPlistContents {
NSMutableDictionary *checkinPlistContents = [NSMutableDictionary dictionary];
checkinPlistContents[kFIRInstanceIDDigestStringKey] = self.digest ?: @"";
checkinPlistContents[kFIRInstanceIDVersionInfoStringKey] = self.versionInfo ?: @"";
checkinPlistContents[kFIRInstanceIDDeviceDataVersionKey] = self.deviceDataVersion ?: @"";
checkinPlistContents[kFIRInstanceIDLastCheckinTimeKey] = @(self.lastCheckinTimestampMillis);
checkinPlistContents[kFIRInstanceIDGServicesDictionaryKey] =
[self.gServicesData count] ? self.gServicesData : @{};
return checkinPlistContents;
}
- (BOOL)hasCheckinInfo {
return (self.deviceID.length && self.secretToken.length);
}
- (BOOL)hasValidCheckinInfo {
int64_t currentTimestampInMillis = FIRInstanceIDCurrentTimestampInMilliseconds();
int64_t timeSinceLastCheckinInMillis = currentTimestampInMillis - self.lastCheckinTimestampMillis;
BOOL hasCheckinInfo = [self hasCheckinInfo];
NSString *lastLocale =
[[GULUserDefaults standardUserDefaults] stringForKey:kFIRInstanceIDUserDefaultsKeyLocale];
// If it's app's first time open and checkin is already fetched and no locale information is
// stored, then checkin info is valid. We should not checkin again because locale is considered
// "changed".
if (hasCheckinInfo && !lastLocale) {
NSString *currentLocale = FIRInstanceIDCurrentLocale();
[[GULUserDefaults standardUserDefaults] setObject:currentLocale
forKey:kFIRInstanceIDUserDefaultsKeyLocale];
return YES;
}
// If locale has changed, checkin info is no longer valid.
// Also update locale information if changed. (Only do it here not in token refresh)
if (FIRInstanceIDHasLocaleChanged()) {
NSString *currentLocale = FIRInstanceIDCurrentLocale();
[[GULUserDefaults standardUserDefaults] setObject:currentLocale
forKey:kFIRInstanceIDUserDefaultsKeyLocale];
return NO;
}
return (hasCheckinInfo &&
(timeSinceLastCheckinInMillis / 1000.0 < kFIRInstanceIDDefaultCheckinInterval));
}
- (void)setHasPreCachedAuthCredentials:(BOOL)hasPreCachedAuthCredentials {
_hasPreCachedAuthCredentials = hasPreCachedAuthCredentials;
}
@end
@@ -0,0 +1,27 @@
/*
* 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 "FIRInstanceIDCheckinPreferences.h"
/** Checkin refresh interval. **/
FOUNDATION_EXPORT const NSTimeInterval kFIRInstanceIDDefaultCheckinInterval;
@interface FIRInstanceIDCheckinPreferences ()
- (BOOL)hasPreCachedAuthCredentials;
- (void)setHasPreCachedAuthCredentials:(BOOL)hasPreCachedAuthCredentials;
@end
@@ -0,0 +1,81 @@
/*
* 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>
#import "FIRInstanceIDUtilities.h"
NS_ASSUME_NONNULL_BEGIN
// keys in Checkin preferences
FOUNDATION_EXPORT NSString *const kFIRInstanceIDDeviceAuthIdKey;
FOUNDATION_EXPORT NSString *const kFIRInstanceIDSecretTokenKey;
FOUNDATION_EXPORT NSString *const kFIRInstanceIDDigestStringKey;
FOUNDATION_EXPORT NSString *const kFIRInstanceIDLastCheckinTimeKey;
FOUNDATION_EXPORT NSString *const kFIRInstanceIDVersionInfoStringKey;
FOUNDATION_EXPORT NSString *const kFIRInstanceIDGServicesDictionaryKey;
FOUNDATION_EXPORT NSString *const kFIRInstanceIDDeviceDataVersionKey;
@class FIRInstanceIDCheckinPreferences;
/**
* @related FIRInstanceIDCheckinService
*
* The completion handler invoked once the fetch from Checkin server finishes.
* For successful fetches we returned checkin information by the checkin service
* and `nil` error, else we return the appropriate error object as reported by the
* Checkin Service.
*
* @param checkinPreferences The checkin preferences as fetched from the server.
* @param error The error object which fetching GServices data.
*/
typedef void (^FIRInstanceIDDeviceCheckinCompletion)(
FIRInstanceIDCheckinPreferences *_Nullable checkinPreferences, NSError *_Nullable error);
/**
* Register the device with Checkin Service and get back the `authID`, `secret
* token` etc. for the client. Checkin results are cached in the
* `FIRInstanceIDCache` and periodically refreshed to prevent them from being stale.
* Each client needs to register with checkin before registering with InstanceID.
*/
@interface FIRInstanceIDCheckinService : NSObject
/**
* Execute a device checkin request to obtain an deviceID, secret token,
* gService data.
*
* @param existingCheckin An existing checkin preference object, if available.
* @param completion Completion hander called on success or failure of device checkin.
*/
- (void)checkinWithExistingCheckin:(nullable FIRInstanceIDCheckinPreferences *)existingCheckin
completion:(FIRInstanceIDDeviceCheckinCompletion)completion;
/**
* This would stop any request that the service made to the checkin backend and also
* release any callback handlers that it holds.
*/
- (void)stopFetching;
/**
* Set test block for mock testing network requests.
*
* @param block The block to invoke as a mock response from the network.
*/
+ (void)setCheckinTestBlock:(nullable FIRInstanceIDURLRequestTestBlock)block;
@end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,244 @@
/*
* 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 "FIRInstanceIDCheckinService.h"
#import "FIRInstanceIDCheckinPreferences+Internal.h"
#import "FIRInstanceIDCheckinPreferences_Private.h"
#import "FIRInstanceIDDefines.h"
#import "FIRInstanceIDLogger.h"
#import "FIRInstanceIDStore.h"
#import "FIRInstanceIDUtilities.h"
#import "NSError+FIRInstanceID.h"
static NSString *const kDeviceCheckinURL = @"https://device-provisioning.googleapis.com/checkin";
// keys in Checkin preferences
NSString *const kFIRInstanceIDDeviceAuthIdKey = @"GMSInstanceIDDeviceAuthIdKey";
NSString *const kFIRInstanceIDSecretTokenKey = @"GMSInstanceIDSecretTokenKey";
NSString *const kFIRInstanceIDDigestStringKey = @"GMSInstanceIDDigestKey";
NSString *const kFIRInstanceIDLastCheckinTimeKey = @"GMSInstanceIDLastCheckinTimestampKey";
NSString *const kFIRInstanceIDVersionInfoStringKey = @"GMSInstanceIDVersionInfo";
NSString *const kFIRInstanceIDGServicesDictionaryKey = @"GMSInstanceIDGServicesData";
NSString *const kFIRInstanceIDDeviceDataVersionKey = @"GMSInstanceIDDeviceDataVersion";
static NSUInteger const kCheckinType = 2; // DeviceType IOS in l/w/a/_checkin.proto
static NSUInteger const kCheckinVersion = 2;
static NSUInteger const kFragment = 0;
static FIRInstanceIDURLRequestTestBlock testBlock;
@interface FIRInstanceIDCheckinService ()
@property(nonatomic, readwrite, strong) NSURLSession *session;
@end
@implementation FIRInstanceIDCheckinService
;
- (instancetype)init {
self = [super init];
if (self) {
// Create an URLSession once, even though checkin should happen about once a day
NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];
config.timeoutIntervalForResource = 60.0f; // 1 minute
config.allowsCellularAccess = YES;
_session = [NSURLSession sessionWithConfiguration:config];
_session.sessionDescription = @"com.google.iid-checkin";
}
return self;
}
- (void)dealloc {
testBlock = nil;
[self.session invalidateAndCancel];
}
- (void)checkinWithExistingCheckin:(FIRInstanceIDCheckinPreferences *)existingCheckin
completion:(FIRInstanceIDDeviceCheckinCompletion)completion {
if (self.session == nil) {
FIRInstanceIDLoggerError(kFIRInstanceIDInvalidNetworkSession,
@"Inconsistent state: NSURLSession has been invalidated");
NSError *error =
[NSError errorWithFIRInstanceIDErrorCode:kFIRInstanceIDErrorCodeRegistrarFailedToCheckIn];
if (completion) {
completion(nil, error);
}
return;
}
NSURL *url = [NSURL URLWithString:kDeviceCheckinURL];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
[request setValue:@"application/json" forHTTPHeaderField:@"content-type"];
NSDictionary *checkinParameters = [self checkinParametersWithExistingCheckin:existingCheckin];
NSData *checkinData = [NSJSONSerialization dataWithJSONObject:checkinParameters
options:0
error:nil];
request.HTTPMethod = @"POST";
request.HTTPBody = checkinData;
void (^handler)(NSData *, NSURLResponse *, NSError *) =
^(NSData *data, NSURLResponse *response, NSError *error) {
if (error) {
FIRInstanceIDLoggerDebug(kFIRInstanceIDMessageCodeService000,
@"Device checkin HTTP fetch error. Error Code: %ld",
(long)error.code);
if (completion) {
completion(nil, error);
}
return;
}
NSError *serializationError;
NSDictionary *dataResponse = [NSJSONSerialization JSONObjectWithData:data
options:0
error:&serializationError];
if (serializationError) {
FIRInstanceIDLoggerDebug(kFIRInstanceIDMessageCodeService001,
@"Error serializing json object. Error Code: %ld",
_FIRInstanceID_L(serializationError.code));
if (completion) {
completion(nil, serializationError);
}
return;
}
NSString *deviceAuthID = [dataResponse[@"android_id"] stringValue];
NSString *secretToken = [dataResponse[@"security_token"] stringValue];
if ([deviceAuthID length] == 0) {
NSError *error =
[NSError errorWithFIRInstanceIDErrorCode:kFIRInstanceIDErrorCodeInvalidRequest];
if (completion) {
completion(nil, error);
}
return;
}
int64_t lastCheckinTimestampMillis = [dataResponse[@"time_msec"] longLongValue];
int64_t currentTimestampMillis = FIRInstanceIDCurrentTimestampInMilliseconds();
// Somehow the server clock gets out of sync with the device clock.
// Reset the last checkin timestamp in case this happens.
if (lastCheckinTimestampMillis > currentTimestampMillis) {
FIRInstanceIDLoggerDebug(
kFIRInstanceIDMessageCodeService002, @"Invalid last checkin timestamp %@ in future.",
[NSDate dateWithTimeIntervalSince1970:lastCheckinTimestampMillis / 1000.0]);
lastCheckinTimestampMillis = currentTimestampMillis;
}
NSString *deviceDataVersionInfo = dataResponse[@"device_data_version_info"] ?: @"";
NSString *digest = dataResponse[@"digest"] ?: @"";
FIRInstanceIDLoggerDebug(kFIRInstanceIDMessageCodeService003,
@"Checkin successful with authId: %@, "
@"digest: %@, "
@"lastCheckinTimestamp: %lld",
deviceAuthID, digest, lastCheckinTimestampMillis);
NSString *versionInfo = dataResponse[@"version_info"] ?: @"";
NSMutableDictionary *gservicesData = [NSMutableDictionary dictionary];
// Read gServices data.
NSArray *flatSettings = dataResponse[@"setting"];
for (NSDictionary *dict in flatSettings) {
if (dict[@"name"] && dict[@"value"]) {
gservicesData[dict[@"name"]] = dict[@"value"];
} else {
FIRInstanceIDLoggerDebug(kFIRInstanceIDInvalidSettingResponse,
@"Invalid setting in checkin response: (%@: %@)",
dict[@"name"], dict[@"value"]);
}
}
FIRInstanceIDCheckinPreferences *checkinPreferences =
[[FIRInstanceIDCheckinPreferences alloc] initWithDeviceID:deviceAuthID
secretToken:secretToken];
NSDictionary *preferences = @{
kFIRInstanceIDDigestStringKey : digest,
kFIRInstanceIDVersionInfoStringKey : versionInfo,
kFIRInstanceIDLastCheckinTimeKey : @(lastCheckinTimestampMillis),
kFIRInstanceIDGServicesDictionaryKey : gservicesData,
kFIRInstanceIDDeviceDataVersionKey : deviceDataVersionInfo,
};
[checkinPreferences updateWithCheckinPlistContents:preferences];
if (completion) {
completion(checkinPreferences, nil);
}
};
// Test block
if (testBlock) {
FIRInstanceIDLoggerDebug(kFIRInstanceIDMessageCodeService005,
@"Test block set, will not hit the server");
testBlock(request, handler);
return;
}
NSURLSessionDataTask *task = [self.session dataTaskWithRequest:request completionHandler:handler];
[task resume];
}
- (void)stopFetching {
[self.session invalidateAndCancel];
// The session cannot be reused after invalidation. Dispose it to prevent accident reusing.
self.session = nil;
}
#pragma mark - Private
- (NSDictionary *)checkinParametersWithExistingCheckin:
(nullable FIRInstanceIDCheckinPreferences *)checkinPreferences {
NSString *deviceModel = FIRInstanceIDDeviceModel();
NSString *systemVersion = FIRInstanceIDOperatingSystemVersion();
NSString *osVersion = [NSString stringWithFormat:@"IOS_%@", systemVersion];
// Get locale from GCM if GCM exists else use system API.
NSString *locale = FIRInstanceIDCurrentLocale();
NSInteger userNumber = 0; // Multi Profile may change this.
NSInteger userSerialNumber = 0; // Multi Profile may change this
uint32_t loggingID = arc4random();
NSString *timeZone = [NSTimeZone localTimeZone].name;
int64_t lastCheckingTimestampMillis = checkinPreferences.lastCheckinTimestampMillis;
NSDictionary *checkinParameters = @{
@"checkin" : @{
@"iosbuild" : @{@"model" : deviceModel, @"os_version" : osVersion},
@"type" : @(kCheckinType),
@"user_number" : @(userNumber),
@"last_checkin_msec" : @(lastCheckingTimestampMillis),
},
@"fragment" : @(kFragment),
@"logging_id" : @(loggingID),
@"locale" : locale,
@"version" : @(kCheckinVersion),
@"digest" : checkinPreferences.digest ?: @"",
@"time_zone" : timeZone,
@"user_serial_number" : @(userSerialNumber),
@"id" : @([checkinPreferences.deviceID longLongValue]),
@"security_token" : @([checkinPreferences.secretToken longLongValue]),
};
FIRInstanceIDLoggerDebug(kFIRInstanceIDMessageCodeService006, @"Checkin parameters: %@",
checkinParameters);
return checkinParameters;
}
+ (void)setCheckinTestBlock:(FIRInstanceIDURLRequestTestBlock)block {
testBlock = [block copy];
}
@end
@@ -0,0 +1,108 @@
/*
* 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>
@class FIRInstanceIDAuthKeychain;
@class FIRInstanceIDBackupExcludedPlist;
@class FIRInstanceIDCheckinPreferences;
// These values exposed for testing
extern NSString *const kFIRInstanceIDCheckinKeychainService;
extern NSString *const kFIRInstanceIDLegacyCheckinKeychainAccount;
extern NSString *const kFIRInstanceIDLegacyCheckinKeychainService;
/**
* Checkin preferences backing store.
*/
@interface FIRInstanceIDCheckinStore : NSObject
/**
* Designated Initializer. Initialize a checkin store with the given backup excluded
* plist filename.
*
* @param checkinFilename The backup excluded plist filename to persist checkin
* preferences.
*
* @param subDirectoryName Sub-directory in standard directory where we write
* InstanceID plist.
*
* @return Store to persist checkin preferences.
*/
- (instancetype)initWithCheckinPlistFileName:(NSString *)checkinFilename
subDirectoryName:(NSString *)subDirectoryName;
/**
* Initialize a checkin store with the given backup excluded plist and keychain.
*
* @param plist The backup excluded plist to persist checkin preferences.
* @param keychain The keychain used to persist checkin auth preferences.
*
* @return Store to persist checkin preferences.
*/
- (instancetype)initWithCheckinPlist:(FIRInstanceIDBackupExcludedPlist *)plist
keychain:(FIRInstanceIDAuthKeychain *)keychain;
/**
* Checks whether the backup excluded checkin preferences are present on the disk or not.
*
* @return YES if the backup excluded checkin plist exists on the disks else NO.
*/
- (BOOL)hasCheckinPlist;
#pragma mark - Save
/**
* Save the checkin preferences to backing store.
*
* @param preferences Checkin preferences to save.
* @param handler The callback handler which is invoked when the operation is complete,
* with an error if there is any.
*/
- (void)saveCheckinPreferences:(FIRInstanceIDCheckinPreferences *)preferences
handler:(void (^)(NSError *error))handler;
#pragma mark - Delete
/**
* Remove the cached checkin preferences.
*
* @param handler The callback handler which is invoked when the operation is complete,
* with an error if there is any.
*/
- (void)removeCheckinPreferencesWithHandler:(void (^)(NSError *error))handler;
#pragma mark - Get
/**
* Get the cached device secret. If we cannot access it for some reason we
* return the appropriate error object.
*
* @return The cached checkin preferences if present else nil.
*/
- (FIRInstanceIDCheckinPreferences *)cachedCheckinPreferences;
/**
* Migrate the checkin item from old service/account to the new one.
* The new account is dynamic as it uses bundle ID.
* This is to ensure checkin is not shared across apps, but still the same
* if app has used GCM before.
* This call should only happen once.
*
*/
- (void)migrateCheckinItemIfNeeded;
@end
@@ -0,0 +1,238 @@
/*
* 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 "FIRInstanceIDCheckinStore.h"
#import "FIRInstanceIDAuthKeyChain.h"
#import "FIRInstanceIDBackupExcludedPlist.h"
#import "FIRInstanceIDCheckinPreferences+Internal.h"
#import "FIRInstanceIDCheckinPreferences_Private.h"
#import "FIRInstanceIDCheckinService.h"
#import "FIRInstanceIDLogger.h"
#import "FIRInstanceIDUtilities.h"
#import "FIRInstanceIDVersionUtilities.h"
#import "NSError+FIRInstanceID.h"
static NSString *const kFIRInstanceIDCheckinKeychainGeneric = @"com.google.iid";
NSString *const kFIRInstanceIDCheckinKeychainService = @"com.google.iid.checkin";
NSString *const kFIRInstanceIDLegacyCheckinKeychainAccount = @"com.google.iid.checkin-account";
NSString *const kFIRInstanceIDLegacyCheckinKeychainService = @"com.google.iid.checkin-service";
// Checkin plist used to have the deviceID and secret stored in them and that's why they
// had 6 items in it. Since the deviceID and secret have been moved to the keychain
// there would only be 4 items.
static const NSInteger kOldCheckinPlistCount = 6;
@interface FIRInstanceIDCheckinStore ()
@property(nonatomic, readwrite, strong) FIRInstanceIDBackupExcludedPlist *plist;
@property(nonatomic, readwrite, strong) FIRInstanceIDAuthKeychain *keychain;
// Checkin will store items under
// Keychain account: <app bundle id>,
// Keychain service: |kFIRInstanceIDCheckinKeychainService|
@property(nonatomic, readonly) NSString *bundleIdentifierForKeychainAccount;
@end
@implementation FIRInstanceIDCheckinStore
- (instancetype)initWithCheckinPlistFileName:(NSString *)checkinFilename
subDirectoryName:(NSString *)subDirectoryName {
FIRInstanceIDBackupExcludedPlist *plist =
[[FIRInstanceIDBackupExcludedPlist alloc] initWithFileName:checkinFilename
subDirectory:subDirectoryName];
FIRInstanceIDAuthKeychain *keychain =
[[FIRInstanceIDAuthKeychain alloc] initWithIdentifier:kFIRInstanceIDCheckinKeychainGeneric];
return [self initWithCheckinPlist:plist keychain:keychain];
}
- (instancetype)initWithCheckinPlist:(FIRInstanceIDBackupExcludedPlist *)plist
keychain:(FIRInstanceIDAuthKeychain *)keychain {
self = [super init];
if (self) {
_plist = plist;
_keychain = keychain;
}
return self;
}
- (BOOL)hasCheckinPlist {
return [self.plist doesFileExist];
}
- (NSString *)bundleIdentifierForKeychainAccount {
static NSString *bundleIdentifier;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
bundleIdentifier = FIRInstanceIDAppIdentifier();
});
return bundleIdentifier;
}
- (void)saveCheckinPreferences:(FIRInstanceIDCheckinPreferences *)preferences
handler:(void (^)(NSError *error))handler {
NSDictionary *checkinPlistContents = [preferences checkinPlistContents];
NSString *checkinKeychainContent = [preferences checkinKeychainContent];
if (![checkinKeychainContent length]) {
FIRInstanceIDLoggerDebug(kFIRInstanceIDMessageCodeCheckinStore000,
@"Failed to get checkin keychain content from memory.");
if (handler) {
handler([NSError
errorWithFIRInstanceIDErrorCode:kFIRInstanceIDErrorCodeRegistrarFailedToCheckIn]);
}
return;
}
if (![checkinPlistContents count]) {
FIRInstanceIDLoggerDebug(kFIRInstanceIDMessageCodeCheckinStore001,
@"Failed to get checkin plist contents from memory.");
if (handler) {
handler([NSError
errorWithFIRInstanceIDErrorCode:kFIRInstanceIDErrorCodeRegistrarFailedToCheckIn]);
}
return;
}
// Save all other checkin preferences in a plist
NSError *error;
if (![self.plist writeDictionary:checkinPlistContents error:&error]) {
FIRInstanceIDLoggerDebug(kFIRInstanceIDMessageCodeCheckinStore003,
@"Failed to save checkin plist contents."
@"Will delete auth credentials");
[self.keychain removeItemsMatchingService:kFIRInstanceIDCheckinKeychainService
account:self.bundleIdentifierForKeychainAccount
handler:nil];
if (handler) {
handler(error);
}
return;
}
FIRInstanceIDLoggerDebug(kFIRInstanceIDMessageCodeCheckinStoreCheckinPlistSaved,
@"Checkin plist file is saved");
// Save the deviceID and secret in the Keychain
if (!preferences.hasPreCachedAuthCredentials) {
NSData *data = [checkinKeychainContent dataUsingEncoding:NSUTF8StringEncoding];
[self.keychain setData:data
forService:kFIRInstanceIDCheckinKeychainService
accessibility:nil
account:self.bundleIdentifierForKeychainAccount
handler:^(NSError *error) {
if (error) {
if (handler) {
handler(error);
}
return;
}
if (handler) {
handler(nil);
}
}];
} else {
handler(nil);
}
}
- (void)removeCheckinPreferencesWithHandler:(void (^)(NSError *error))handler {
// Delete the checkin preferences plist first to avoid delay.
NSError *deletePlistError;
if (![self.plist deleteFile:&deletePlistError]) {
handler(deletePlistError);
return;
}
FIRInstanceIDLoggerDebug(kFIRInstanceIDMessageCodeCheckinStoreCheckinPlistDeleted,
@"Deleted checkin plist file.");
// Remove deviceID and secret from Keychain
[self.keychain
removeItemsMatchingService:kFIRInstanceIDCheckinKeychainService
account:self.bundleIdentifierForKeychainAccount
handler:^(NSError *error) {
// Try to remove from old location as well because migration
// is no longer needed. Consider this is either a fresh install
// or an identity wipe.
[self.keychain
removeItemsMatchingService:kFIRInstanceIDLegacyCheckinKeychainService
account:kFIRInstanceIDLegacyCheckinKeychainAccount
handler:nil];
handler(error);
}];
}
- (FIRInstanceIDCheckinPreferences *)cachedCheckinPreferences {
// Query the keychain for deviceID and secret
NSData *item = [self.keychain dataForService:kFIRInstanceIDCheckinKeychainService
account:self.bundleIdentifierForKeychainAccount];
// Check info found in keychain
NSString *checkinKeychainContent = [[NSString alloc] initWithData:item
encoding:NSUTF8StringEncoding];
FIRInstanceIDCheckinPreferences *checkinPreferences =
[FIRInstanceIDCheckinPreferences preferencesFromKeychainContents:checkinKeychainContent];
NSDictionary *checkinPlistContents = [self.plist contentAsDictionary];
NSString *plistDeviceAuthID = checkinPlistContents[kFIRInstanceIDDeviceAuthIdKey];
NSString *plistSecretToken = checkinPlistContents[kFIRInstanceIDSecretTokenKey];
// If deviceID and secret not found in the keychain verify that we don't have them in the
// checkin preferences plist.
if (![checkinPreferences.deviceID length] && ![checkinPreferences.secretToken length]) {
if ([plistDeviceAuthID length] && [plistSecretToken length]) {
// Couldn't find checkin credentials in keychain but found them in the plist.
checkinPreferences =
[[FIRInstanceIDCheckinPreferences alloc] initWithDeviceID:plistDeviceAuthID
secretToken:plistSecretToken];
} else {
// Couldn't find checkin credentials in keychain nor plist
return nil;
}
} else if (kOldCheckinPlistCount == checkinPlistContents.count) {
// same check as above but just to be extra sure that we cover all upgrade cases properly.
// TODO(chliangGoogle): Remove this case, after verifying it's not needed
if ([plistDeviceAuthID length] && [plistSecretToken length]) {
checkinPreferences =
[[FIRInstanceIDCheckinPreferences alloc] initWithDeviceID:plistDeviceAuthID
secretToken:plistSecretToken];
}
}
[checkinPreferences updateWithCheckinPlistContents:checkinPlistContents];
return checkinPreferences;
}
- (void)migrateCheckinItemIfNeeded {
// Check for checkin in the old location, using the legacy keys
// Query the keychain for deviceID and secret
NSData *dataInOldLocation =
[self.keychain dataForService:kFIRInstanceIDLegacyCheckinKeychainService
account:kFIRInstanceIDLegacyCheckinKeychainAccount];
if (dataInOldLocation) {
// Save to new location
[self.keychain setData:dataInOldLocation
forService:kFIRInstanceIDCheckinKeychainService
accessibility:NULL
account:self.bundleIdentifierForKeychainAccount
handler:nil];
// Remove from old location
[self.keychain removeItemsMatchingService:kFIRInstanceIDLegacyCheckinKeychainService
account:kFIRInstanceIDLegacyCheckinKeychainAccount
handler:nil];
}
}
@end
@@ -0,0 +1,31 @@
/*
* 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
/**
* A generic class to combine several handler blocks into a single block in a thread-safe manner
*/
@interface FIRInstanceIDCombinedHandler<ResultType> : NSObject
- (void)addHandler:(void (^)(ResultType _Nullable result, NSError* _Nullable error))handler;
- (void (^)(ResultType _Nullable result, NSError* _Nullable error))combinedHandler;
@end
NS_ASSUME_NONNULL_END
@@ -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.
*/
#import "FIRInstanceIDCombinedHandler.h"
NS_ASSUME_NONNULL_BEGIN
typedef void (^FIRInstanseIDHandler)(id _Nullable result, NSError *_Nullable error);
@interface FIRInstanceIDCombinedHandler <ResultType>()
@property(atomic, readonly, strong) NSMutableArray<FIRInstanseIDHandler> *handlers;
@end
NS_ASSUME_NONNULL_END
@implementation FIRInstanceIDCombinedHandler
- (instancetype)init {
self = [super init];
if (self) {
_handlers = [NSMutableArray array];
}
return self;
}
- (void)addHandler:(FIRInstanseIDHandler)handler {
if (!handler) {
return;
}
@synchronized(self) {
[self.handlers addObject:handler];
}
}
- (FIRInstanseIDHandler)combinedHandler {
FIRInstanseIDHandler combinedHandler = nil;
@synchronized(self) {
NSArray<FIRInstanseIDHandler> *handlers = [self.handlers copy];
combinedHandler = ^(id result, NSError *error) {
for (FIRInstanseIDHandler handler in handlers) {
handler(result, error);
}
};
}
return combinedHandler;
}
@end
@@ -0,0 +1,63 @@
/*
* 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>
#pragma mark - Commands
/**
* Value included in a structured response or GCM message from IID, indicating
* an identity reset.
*/
FOUNDATION_EXPORT NSString *const kFIRInstanceID_CMD_RST;
#pragma mark - Notifications
/// Notification used to deliver GCM messages for InstanceID.
FOUNDATION_EXPORT NSString *const kFIRInstanceIDCheckinFetchedNotification;
FOUNDATION_EXPORT NSString *const kFIRInstanceIDAPNSTokenNotification;
FOUNDATION_EXPORT NSString *const kFIRInstanceIDDefaultGCMTokenNotification;
FOUNDATION_EXPORT NSString *const kFIRInstanceIDDefaultGCMTokenFailNotification;
FOUNDATION_EXPORT NSString *const kFIRInstanceIDIdentityInvalidatedNotification;
#pragma mark - Miscellaneous
/// The scope used to save the IID "*" scope token. This is used for saving the
/// IID auth token that we receive from the server. This feature was never
/// implemented on the server side.
FOUNDATION_EXPORT NSString *const kFIRInstanceIDAllScopeIdentifier;
/// The scope used to save the IID "*" scope token.
FOUNDATION_EXPORT NSString *const kFIRInstanceIDDefaultTokenScope;
/// Subdirectory in search path directory to store InstanceID preferences.
FOUNDATION_EXPORT NSString *const kFIRInstanceIDSubDirectoryName;
/// The key for APNS token in options dictionary.
FOUNDATION_EXPORT NSString *const kFIRInstanceIDTokenOptionsAPNSKey;
/// The key for APNS token environment type in options dictionary.
FOUNDATION_EXPORT NSString *const kFIRInstanceIDTokenOptionsAPNSIsSandboxKey;
/// The key for GMP AppID sent in registration requests.
FOUNDATION_EXPORT NSString *const kFIRInstanceIDTokenOptionsFirebaseAppIDKey;
/// The key to enable auto-register by swizzling AppDelegate's methods.
FOUNDATION_EXPORT NSString *const kFIRInstanceIDAppDelegateProxyEnabledInfoPlistKey;
/// Error code for missing entitlements in Keychain. iOS Keychain error
/// https://forums.developer.apple.com/thread/4743
FOUNDATION_EXPORT const int kFIRInstanceIDSecMissingEntitlementErrorCode;
@@ -0,0 +1,46 @@
/*
* 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 "FIRInstanceIDConstants.h"
// Commands
NSString *const kFIRInstanceID_CMD_RST = @"RST";
// NOTIFICATIONS
NSString *const kFIRInstanceIDCheckinFetchedNotification = @"com.google.gcm.notif-checkin-fetched";
NSString *const kFIRInstanceIDAPNSTokenNotification = @"com.firebase.iid.notif.apns-token";
NSString *const kFIRInstanceIDDefaultGCMTokenNotification = @"com.firebase.iid.notif.fcm-token";
NSString *const kFIRInstanceIDDefaultGCMTokenFailNotification =
@"com.firebase.iid.notif.fcm-token-fail";
NSString *const kFIRInstanceIDIdentityInvalidatedNotification = @"com.google.iid.identity-invalid";
// Miscellaneous
NSString *const kFIRInstanceIDAllScopeIdentifier = @"iid-all";
NSString *const kFIRInstanceIDDefaultTokenScope = @"*";
NSString *const kFIRInstanceIDSubDirectoryName = @"Google/FirebaseInstanceID";
// Registration Options
NSString *const kFIRInstanceIDTokenOptionsAPNSKey = @"apns_token";
NSString *const kFIRInstanceIDTokenOptionsAPNSIsSandboxKey = @"apns_sandbox";
NSString *const kFIRInstanceIDTokenOptionsFirebaseAppIDKey = @"gmp_app_id";
NSString *const kFIRInstanceIDAppDelegateProxyEnabledInfoPlistKey =
@"FirebaseAppDelegateProxyEnabled";
// iOS Keychain error https://forums.developer.apple.com/thread/4743
// An undocumented error code hence need to be redeclared.
const int kFIRInstanceIDSecMissingEntitlementErrorCode = -34018;
@@ -0,0 +1,47 @@
/*
* 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.
*/
#ifndef FIRInstanceIDLib_FIRInstanceIDDefines_h
#define FIRInstanceIDLib_FIRInstanceIDDefines_h
#define _FIRInstanceID_VERBOSE_LOGGING 1
// Verbose Logging
#if (_FIRInstanceID_VERBOSE_LOGGING)
#define FIRInstanceID_DEV_VERBOSE_LOG(...) NSLog(__VA_ARGS__)
#else
#define FIRInstanceID_DEV_VERBOSE_LOG(...) \
do { \
} while (0)
#endif // VERBOSE_LOGGING
// WEAKIFY & STRONGIFY
// Helper macro.
#define _FIRInstanceID_WEAKNAME(VAR) VAR##_weak_
#define FIRInstanceID_WEAKIFY(VAR) __weak __typeof__(VAR) _FIRInstanceID_WEAKNAME(VAR) = (VAR);
#define FIRInstanceID_STRONGIFY(VAR) \
_Pragma("clang diagnostic push") _Pragma("clang diagnostic ignored \"-Wshadow\"") \
__strong __typeof__(VAR) VAR = _FIRInstanceID_WEAKNAME(VAR); \
_Pragma("clang diagnostic pop")
// Type Conversions (used for NSInteger etc)
#ifndef _FIRInstanceID_L
#define _FIRInstanceID_L(v) (long)(v)
#endif
#endif
@@ -0,0 +1,78 @@
/*
* 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>
@interface FIRInstanceIDKeyPair : NSObject
- (instancetype)init __attribute__((
unavailable("Use -initWithPrivateKey:publicKey:publicKeyData:privateKeyData: instead.")));
;
/**
* Initialize a new 2048 bit RSA keypair. This also stores the keypair in the Keychain
* Preferences.
*
* @param publicKey The publicKey stored in Keychain.
* @param privateKey The privateKey stored in Keychain.
* @param publicKeyData The publicKey in NSData format.
* @param privateKeyData The privateKey in NSData format.
*
* @return A new KeyPair instance with the generated public and private key.
*/
- (instancetype)initWithPrivateKey:(SecKeyRef)privateKey
publicKey:(SecKeyRef)publicKey
publicKeyData:(NSData *)publicKeyData
privateKeyData:(NSData *)privateKeyData NS_DESIGNATED_INITIALIZER;
/**
* The public key in the RSA 20148 bit generated KeyPair.
*
* @return The 2048 bit RSA KeyPair's public key.
*/
@property(nonatomic, readonly, strong) NSData *publicKeyData;
/**
* The private key in the RSA 20148 bit generated KeyPair.
*
* @return The 2048 bit RSA KeyPair's private key.
*/
@property(nonatomic, readonly, strong) NSData *privateKeyData;
#pragma mark - Info
/**
* Checks if the private and public keyPair are valid or not.
*
* @return YES if keypair is valid else NO.
*/
- (BOOL)isValid;
/**
* The public key in the RSA 2048 bit generated KeyPair.
*
* @return The 2048 bit RSA KeyPair's public key.
*/
- (SecKeyRef)publicKey;
/**
* The private key in the RSA 2048 bit generated KeyPair.
*
* @return The 2048 bit RSA KeyPair's private key.
*/
- (SecKeyRef)privateKey;
@end
@@ -0,0 +1,73 @@
/*
* 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 "FIRInstanceIDKeyPair.h"
#import <Security/Security.h>
#import "FIRInstanceIDKeyPairUtilities.h"
#import "FIRInstanceIDKeychain.h"
#import "FIRInstanceIDLogger.h"
#import "NSError+FIRInstanceID.h"
@interface FIRInstanceIDKeyPair () {
SecKeyRef _privateKey;
SecKeyRef _publicKey;
}
@property(nonatomic, readwrite, strong) NSData *publicKeyData;
@property(nonatomic, readwrite, strong) NSData *privateKeyData;
@end
@implementation FIRInstanceIDKeyPair
- (instancetype)initWithPrivateKey:(SecKeyRef)privateKey
publicKey:(SecKeyRef)publicKey
publicKeyData:(NSData *)publicKeyData
privateKeyData:(NSData *)privateKeyData {
self = [super init];
if (self) {
_privateKey = privateKey;
_publicKey = publicKey;
_publicKeyData = publicKeyData;
_privateKeyData = privateKeyData;
}
return self;
}
- (void)dealloc {
if (_privateKey) {
CFRelease(_privateKey);
}
if (_publicKey) {
CFRelease(_publicKey);
}
}
#pragma mark - Info
- (BOOL)isValid {
return _privateKey != NULL && _publicKey != NULL;
}
- (SecKeyRef)publicKey {
return _publicKey;
}
- (SecKeyRef)privateKey {
return _privateKey;
}
@end
@@ -0,0 +1,85 @@
/*
* 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>
@class FIRInstanceIDKeyPair;
extern NSString *const kFIRInstanceIDKeyPairSubType;
@class FIRInstanceIDKeyPairStore;
@interface FIRInstanceIDKeyPairStore : NSObject
/**
* Invalidates the cached keypairs in the Keychain, if needed. The keypair metadata plist is
* checked for existence. If the plist file does not exist, it is a signal of a new installation,
* and therefore the key pairs are not valid.
*
* Returns YES if keypair has been invalidated.
*/
- (BOOL)invalidateKeyPairsIfNeeded;
/**
* Delete the cached RSA keypair from Keychain with the given subtype.
*
* @param subtype The subtype used to cache the RSA keypair in Keychain.
* @param handler The callback handler which is invoked when the keypair deletion is
* complete, with an error if there is any.
*/
- (void)deleteSavedKeyPairWithSubtype:(NSString *)subtype handler:(void (^)(NSError *))handler;
/**
* Delete the plist that caches KeyPair generation timestamps.
*
* @param error The error if any while deleting the plist else nil.
*
* @return YES if the delete was successful else NO.
*/
- (BOOL)removeKeyPairCreationTimePlistWithError:(NSError **)error;
/**
* Loads a cached KeyPair if it exists in the Keychain else generate a new
* one. If a keyPair already exists in memory this will just return that. This should
* not be called from the main thread since it could potentially lead to creating a new
* RSA-2048 bit keyPair which is an expensive operation.
*
* @param error The error, if any, while accessing the Keychain.
*
* @return A valid 2048 bit RSA key pair.
*/
- (FIRInstanceIDKeyPair *)loadKeyPairWithError:(NSError **)error;
/**
* Check if the Keychain has any cached keypairs or not.
*
* @return YES if the Keychain has cached RSA KeyPairs else NO.
*/
- (BOOL)hasCachedKeyPairs;
/**
* Return an identifier for the app instance. The result is a short identifier that can
* be used as a key when storing information about the app. This method will return the same
* ID as long as the application identity remains active. If the identity has been revoked or
* expired the method will generate and return a new identifier.
*
* @param error The error if any while loading the RSA KeyPair.
*
* @return The identifier, as url safe string.
*/
- (NSString *)appIdentityWithError:(NSError *__autoreleasing *)error;
@end
@@ -0,0 +1,525 @@
/*
* 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 "FIRInstanceIDKeyPairStore.h"
#import "FIRInstanceIDBackupExcludedPlist.h"
#import "FIRInstanceIDConstants.h"
#import "FIRInstanceIDKeyPair.h"
#import "FIRInstanceIDKeyPairUtilities.h"
#import "FIRInstanceIDKeychain.h"
#import "FIRInstanceIDLogger.h"
#import "FIRInstanceIDUtilities.h"
#import "NSError+FIRInstanceID.h"
// NOTE: These values should be in sync with what InstanceID saves in as.
static NSString *const kFIRInstanceIDKeyPairStoreFileName = @"com.google.iid-keypair";
static NSString *const kFIRInstanceIDStoreKeyGenerationTime = @"cre";
static NSString *const kFIRInstanceIDStoreKeyPrefix = @"com.google.iid-";
static NSString *const kFIRInstanceIDStoreKeyPublic = @"|P|";
static NSString *const kFIRInstanceIDStoreKeyPrivate = @"|K|";
static NSString *const kFIRInstanceIDStoreKeySubtype = @"|S|";
static NSString *const kFIRInstanceIDKeyPairPublicTagPrefix = @"com.google.iid.keypair.public-";
static NSString *const kFIRInstanceIDKeyPairPrivateTagPrefix = @"com.google.iid.keypair.private-";
static const int kMaxMissingEntitlementErrorCount = 3;
NSString *const kFIRInstanceIDKeyPairSubType = @"";
// Query the key with NSData format
NSData *FIRInstanceIDKeyDataWithTag(NSString *tag) {
if (![tag length]) {
return NULL;
}
NSDictionary *queryKey = FIRInstanceIDKeyPairQuery(tag, YES, YES);
CFTypeRef result = [[FIRInstanceIDKeychain sharedInstance] itemWithQuery:queryKey];
if (!result) {
return NULL;
}
return (__bridge NSData *)result;
}
// Query the key given a tag
SecKeyRef FIRInstanceIDCachedKeyRefWithTag(NSString *tag) {
if (!tag.length) {
return NULL;
}
NSDictionary *queryKey = FIRInstanceIDKeyPairQuery(tag, YES, NO);
CFTypeRef result = [[FIRInstanceIDKeychain sharedInstance] itemWithQuery:queryKey];
return (SecKeyRef)result;
}
// Check if keypair has been migrated from the legacy to the new version
BOOL FIRInstanceIDHasMigratedKeyPair(NSString *legacyPublicKeyTag, NSString *newPublicKeyTag) {
NSData *oldPublicKeyData = FIRInstanceIDKeyDataWithTag(legacyPublicKeyTag);
NSData *newPublicKeyData = FIRInstanceIDKeyDataWithTag(newPublicKeyTag);
return [oldPublicKeyData isEqualToData:newPublicKeyData];
}
// The legacy value is hardcoded to be the same key. This is a potential problem in shared keychain
// environments.
NSString *FIRInstanceIDLegacyPublicTagWithSubtype(NSString *subtype) {
NSString *prefix = kFIRInstanceIDStoreKeyPrefix;
return [NSString stringWithFormat:@"%@%@%@", prefix, subtype, kFIRInstanceIDStoreKeyPublic];
}
// The legacy value is hardcoded to be the same key. This is a potential problem in shared keychain
// environments.
NSString *FIRInstanceIDLegacyPrivateTagWithSubtype(NSString *subtype) {
NSString *prefix = kFIRInstanceIDStoreKeyPrefix;
return [NSString stringWithFormat:@"%@%@%@", prefix, subtype, kFIRInstanceIDStoreKeyPrivate];
}
NSString *FIRInstanceIDPublicTagWithSubtype(NSString *subtype) {
static NSString *publicTag;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSString *mainAppBundleID = FIRInstanceIDAppIdentifier();
publicTag =
[NSString stringWithFormat:@"%@%@", kFIRInstanceIDKeyPairPublicTagPrefix, mainAppBundleID];
});
return publicTag;
}
NSString *FIRInstanceIDPrivateTagWithSubtype(NSString *subtype) {
static NSString *privateTag;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSString *mainAppBundleID = FIRInstanceIDAppIdentifier();
privateTag =
[NSString stringWithFormat:@"%@%@", kFIRInstanceIDKeyPairPrivateTagPrefix, mainAppBundleID];
});
return privateTag;
}
NSString *FIRInstanceIDCreationTimeKeyWithSubtype(NSString *subtype) {
return [NSString stringWithFormat:@"%@%@%@", subtype, kFIRInstanceIDStoreKeySubtype,
kFIRInstanceIDStoreKeyGenerationTime];
}
@interface FIRInstanceIDKeyPairStore ()
@property(nonatomic, readwrite, strong) FIRInstanceIDBackupExcludedPlist *plist;
@property(atomic, readwrite, strong) FIRInstanceIDKeyPair *keyPair;
@property(nonatomic, readwrite, assign) NSInteger keychainEntitlementsErrorCount;
@end
@implementation FIRInstanceIDKeyPairStore
- (instancetype)init {
self = [super init];
if (self) {
NSString *fileName = [[self class] keyStoreFileName];
_plist =
[[FIRInstanceIDBackupExcludedPlist alloc] initWithFileName:fileName
subDirectory:kFIRInstanceIDSubDirectoryName];
}
return self;
}
- (BOOL)invalidateKeyPairsIfNeeded {
// Currently keypairs are always invalidated if self.plist is missing. This normally indicates
// a fresh install (or an uninstall/reinstall). In those situations the key pairs should be
// deleted.
// NOTE: Although this class refers to multiple key pairs, with different subtypes, in practice
// only a single subtype is currently supported. (b/64906549)
if (![self.plist doesFileExist]) {
// A fresh install, clear all the key pairs in the key chain. Do not perform migration as all
// key pairs are gone.
[self deleteSavedKeyPairWithSubtype:kFIRInstanceIDKeyPairSubType handler:nil];
return YES;
}
// Not a fresh install, perform migration at early state.
[self migrateKeyPairCacheIfNeededWithHandler:nil];
return NO;
}
- (BOOL)hasCachedKeyPairs {
NSError *error;
if ([self cachedKeyPairWithSubtype:kFIRInstanceIDKeyPairSubType error:&error] == nil) {
if (error) {
FIRInstanceIDLoggerDebug(kFIRInstanceIDMessageCodeKeyPairStore000,
@"Failed to get the cached keyPair %@", error);
}
error = nil;
[self removeKeyPairCreationTimePlistWithError:&error];
if (error) {
FIRInstanceIDLoggerDebug(kFIRInstanceIDMessageCodeKeyPairStore001,
@"Failed to remove keyPair creationTime plist %@", error);
}
return NO;
}
return YES;
}
- (NSString *)appIdentityWithError:(NSError *__autoreleasing *)error {
// Load the keyPair from Keychain (or generate a key pair, if this is the first run of the app).
FIRInstanceIDKeyPair *keyPair = [self loadKeyPairWithError:error];
if (!keyPair) {
FIRInstanceIDLoggerError(kFIRInstanceIDMessageCodeKeyPairStoreCouldNotLoadKeyPair,
@"Keypair could not be loaded from Keychain. Error: %@", (*error));
return nil;
}
if (error) {
*error = nil;
}
NSString *appIdentity = FIRInstanceIDAppIdentity(keyPair);
if (!appIdentity.length) {
if (error) {
*error = [NSError errorWithFIRInstanceIDErrorCode:kFIRInstanceIDErrorCodeUnknown];
}
}
return appIdentity;
}
- (FIRInstanceIDKeyPair *)loadKeyPairWithError:(NSError **)error {
// In case we call this from different threads we don't want to generate or fetch the
// keyPair multiple times. Once we have a keyPair in the cache it would mostly be used
// from there.
@synchronized(self) {
if ([self.keyPair isValid]) {
return self.keyPair;
}
if (self.keychainEntitlementsErrorCount >= kMaxMissingEntitlementErrorCount) {
FIRInstanceIDLoggerDebug(kFIRInstanceIDMessageCodeKeyPairStore002,
@"Keychain not accessible, Entitlements missing error (-34018). "
@"Will not check token in cache.");
return nil;
}
if (!self.keyPair) {
self.keyPair = [self validCachedKeyPairWithSubtype:kFIRInstanceIDKeyPairSubType error:error];
}
if ((*error).code == kFIRInstanceIDSecMissingEntitlementErrorCode) {
self.keychainEntitlementsErrorCount++;
}
if (!self.keyPair) {
self.keyPair = [self generateAndSaveKeyWithSubtype:kFIRInstanceIDKeyPairSubType
creationTime:FIRInstanceIDCurrentTimestampInSeconds()
error:error];
}
}
return self.keyPair;
}
// TODO(chliangGoogle: Remove subtype support, as it's not being used.
- (FIRInstanceIDKeyPair *)generateAndSaveKeyWithSubtype:(NSString *)subtype
creationTime:(int64_t)creationTime
error:(NSError **)error {
NSString *publicKeyTag = FIRInstanceIDPublicTagWithSubtype(subtype);
NSString *privateKeyTag = FIRInstanceIDPrivateTagWithSubtype(subtype);
FIRInstanceIDKeyPair *keyPair =
[[FIRInstanceIDKeychain sharedInstance] generateKeyPairWithPrivateTag:privateKeyTag
publicTag:publicKeyTag];
if (![keyPair isValid]) {
FIRInstanceIDLoggerError(kFIRInstanceIDMessageCodeKeyPairStore003,
@"Unable to generate keypair.");
return nil;
}
NSString *creationTimeKey = FIRInstanceIDCreationTimeKeyWithSubtype(subtype);
NSDictionary *keyPairData = @{creationTimeKey : @(creationTime)};
if (error) {
*error = nil;
}
NSMutableDictionary *allKeyPairs = [[self.plist contentAsDictionary] mutableCopy];
if (allKeyPairs.count) {
[allKeyPairs addEntriesFromDictionary:keyPairData];
} else {
allKeyPairs = [keyPairData mutableCopy];
}
if (![self.plist writeDictionary:allKeyPairs error:error]) {
[FIRInstanceIDKeyPairStore deleteKeyPairWithPrivateTag:privateKeyTag
publicTag:publicKeyTag
handler:nil];
FIRInstanceIDLoggerError(kFIRInstanceIDMessageCodeKeyPairStore004,
@"Failed to save keypair data to plist %@", error ? *error : @"");
return nil;
}
return keyPair;
}
- (FIRInstanceIDKeyPair *)validCachedKeyPairWithSubtype:(NSString *)subtype
error:(NSError **)error {
// On a new install (or if the ID was deleted), the plist will be missing, which should trigger
// a reset of the key pairs in Keychain (if they exist).
NSDictionary *allKeyPairs = [self.plist contentAsDictionary];
NSString *creationTimeKey = FIRInstanceIDCreationTimeKeyWithSubtype(subtype);
if (allKeyPairs[creationTimeKey] > 0) {
return [self cachedKeyPairWithSubtype:subtype error:error];
} else {
// There is no need to reset keypair again here as FIRInstanceID init call is always
// going to be ahead of this call, which already trigger keypair reset if it's new install
FIRInstanceIDErrorCode code = kFIRInstanceIDErrorCodeInvalidKeyPairCreationTime;
if (error) {
*error = [NSError errorWithFIRInstanceIDErrorCode:code];
}
return nil;
}
}
- (FIRInstanceIDKeyPair *)cachedKeyPairWithSubtype:(NSString *)subtype
error:(NSError *__autoreleasing *)error {
// base64 encoded keys
NSString *publicKeyTag = FIRInstanceIDPublicTagWithSubtype(subtype);
NSString *privateKeyTag = FIRInstanceIDPrivateTagWithSubtype(subtype);
return [FIRInstanceIDKeyPairStore keyPairForPrivateKeyTag:privateKeyTag
publicKeyTag:publicKeyTag
error:error];
}
+ (FIRInstanceIDKeyPair *)keyPairForPrivateKeyTag:(NSString *)privateKeyTag
publicKeyTag:(NSString *)publicKeyTag
error:(NSError *__autoreleasing *)error {
if (![privateKeyTag length] || ![publicKeyTag length]) {
if (error) {
*error = [NSError errorWithFIRInstanceIDErrorCode:kFIRInstanceIDErrorCodeInvalidKeyPairTags];
}
return nil;
}
SecKeyRef privateKeyRef = FIRInstanceIDCachedKeyRefWithTag(privateKeyTag);
SecKeyRef publicKeyRef = FIRInstanceIDCachedKeyRefWithTag(publicKeyTag);
if (!privateKeyRef || !publicKeyRef) {
if (error) {
*error = [NSError errorWithFIRInstanceIDErrorCode:kFIRInstanceIDErrorCodeMissingKeyPair];
}
FIRInstanceIDLoggerDebug(kFIRInstanceIDMessageCodeKeyPair000,
@"No keypair info is found with tag %@", privateKeyTag);
return nil;
}
NSData *publicKeyData = FIRInstanceIDKeyDataWithTag(publicKeyTag);
NSData *privateKeyData = FIRInstanceIDKeyDataWithTag(privateKeyTag);
FIRInstanceIDKeyPair *keyPair = [[FIRInstanceIDKeyPair alloc] initWithPrivateKey:privateKeyRef
publicKey:publicKeyRef
publicKeyData:publicKeyData
privateKeyData:privateKeyData];
return keyPair;
}
// Migrates from keypair saved under legacy keys (hardcoded value) to dynamic keys (stable, but
// unique for the app's bundle id
- (void)migrateKeyPairCacheIfNeededWithHandler:(void (^)(NSError *error))handler {
// Attempt to load keypair using legacy keys
NSString *legacyPublicKeyTag =
FIRInstanceIDLegacyPublicTagWithSubtype(kFIRInstanceIDKeyPairSubType);
NSString *legacyPrivateKeyTag =
FIRInstanceIDLegacyPrivateTagWithSubtype(kFIRInstanceIDKeyPairSubType);
NSError *error;
FIRInstanceIDKeyPair *keyPair =
[FIRInstanceIDKeyPairStore keyPairForPrivateKeyTag:legacyPrivateKeyTag
publicKeyTag:legacyPublicKeyTag
error:&error];
if (![keyPair isValid]) {
FIRInstanceIDLoggerDebug(kFIRInstanceIDMessageCodeKeyPairNoLegacyKeyPair,
@"There's no legacy keypair so no need to do migration.");
if (handler) {
handler(nil);
}
return;
}
// Check whether migration already done.
NSString *publicKeyTag = FIRInstanceIDPublicTagWithSubtype(kFIRInstanceIDKeyPairSubType);
if (FIRInstanceIDHasMigratedKeyPair(legacyPublicKeyTag, publicKeyTag)) {
if (handler) {
handler(nil);
}
return;
}
// Also cache locally since we are sure to use the migrated key pair.
self.keyPair = keyPair;
// Either new key pair doesn't exist or it's different than legacy key pair, start the migration.
__block NSError *updateKeyRefError;
NSString *privateKeyTag = FIRInstanceIDPrivateTagWithSubtype(kFIRInstanceIDKeyPairSubType);
[self updateKeyRef:keyPair.publicKey
withTag:publicKeyTag
handler:^(NSError *error) {
if (error) {
FIRInstanceIDLoggerError(kFIRInstanceIDMessageCodeKeyPairMigrationError,
@"Unable to migrate key pair from legacy ones.");
updateKeyRefError = error;
}
}];
[self updateKeyRef:keyPair.privateKey
withTag:privateKeyTag
handler:^(NSError *error) {
if (error) {
FIRInstanceIDLoggerError(kFIRInstanceIDMessageCodeKeyPairMigrationError,
@"Unable to migrate key pair from legacy ones.");
updateKeyRefError = error;
}
if (handler) {
handler(updateKeyRefError);
}
}];
}
// Used for migrating from legacy tags to updated tags. The legacy keychain is not deleted for
// backward compatibility.
// TODO(chliangGoogle) Delete the legacy keychain when GCM is fully deprecated.
- (void)updateKeyRef:(SecKeyRef)keyRef
withTag:(NSString *)tag
handler:(void (^)(NSError *error))handler {
NSData *updatedTagData = [tag dataUsingEncoding:NSUTF8StringEncoding];
__block NSError *keychainError;
// Always delete the old keychain before adding a new one to avoid conflicts.
NSDictionary *deleteQuery = @{
(__bridge id)kSecAttrApplicationTag : updatedTagData,
(__bridge id)kSecClass : (__bridge id)kSecClassKey,
(__bridge id)kSecAttrKeyType : (__bridge id)kSecAttrKeyTypeRSA,
(__bridge id)kSecReturnRef : @(YES),
};
[[FIRInstanceIDKeychain sharedInstance] removeItemWithQuery:deleteQuery
handler:^(NSError *error) {
if (error) {
keychainError = error;
}
}];
NSDictionary *addQuery = @{
(__bridge id)kSecAttrApplicationTag : updatedTagData,
(__bridge id)kSecClass : (__bridge id)kSecClassKey,
(__bridge id)kSecValueRef : (__bridge id)keyRef,
(__bridge id)kSecAttrAccessible : (__bridge id)kSecAttrAccessibleAlwaysThisDeviceOnly,
};
[[FIRInstanceIDKeychain sharedInstance] addItemWithQuery:addQuery
handler:^(NSError *addError) {
if (addError) {
keychainError = addError;
}
if (handler) {
handler(keychainError);
}
}];
}
- (void)deleteSavedKeyPairWithSubtype:(NSString *)subtype
handler:(void (^)(NSError *error))handler {
NSDictionary *allKeyPairs = [self.plist contentAsDictionary];
NSString *publicKeyTag = FIRInstanceIDPublicTagWithSubtype(subtype);
NSString *privateKeyTag = FIRInstanceIDPrivateTagWithSubtype(subtype);
NSString *creationTimeKey = FIRInstanceIDCreationTimeKeyWithSubtype(subtype);
// remove the creation time
if (allKeyPairs[creationTimeKey] > 0) {
NSMutableDictionary *newKeyPairs = [NSMutableDictionary dictionaryWithDictionary:allKeyPairs];
[newKeyPairs removeObjectForKey:creationTimeKey];
NSError *plistError;
if (![self.plist writeDictionary:newKeyPairs error:&plistError]) {
FIRInstanceIDLoggerError(kFIRInstanceIDMessageCodeKeyPairStore006,
@"Unable to remove keypair creation time from plist %@", plistError);
}
}
self.keyPair = nil;
[FIRInstanceIDKeyPairStore
deleteKeyPairWithPrivateTag:privateKeyTag
publicTag:publicKeyTag
handler:^(NSError *error) {
// Delete legacy key pairs from GCM/FCM If they exist. All key pairs
// should be deleted when app is newly installed.
NSString *legacyPublicKeyTag =
FIRInstanceIDLegacyPublicTagWithSubtype(subtype);
NSString *legacyPrivateKeyTag =
FIRInstanceIDLegacyPrivateTagWithSubtype(subtype);
[FIRInstanceIDKeyPairStore
deleteKeyPairWithPrivateTag:legacyPrivateKeyTag
publicTag:legacyPublicKeyTag
handler:nil];
if (error) {
FIRInstanceIDLoggerError(kFIRInstanceIDMessageCodeKeyPairStore007,
@"Unable to remove RSA keypair, error: %@",
error);
if (handler) {
handler(error);
}
} else {
if (handler) {
handler(nil);
}
}
}];
}
+ (void)deleteKeyPairWithPrivateTag:(NSString *)privateTag
publicTag:(NSString *)publicTag
handler:(void (^)(NSError *))handler {
NSDictionary *queryPublicKey = FIRInstanceIDKeyPairQuery(publicTag, NO, NO);
NSDictionary *queryPrivateKey = FIRInstanceIDKeyPairQuery(privateTag, NO, NO);
__block NSError *keychainError;
// Always remove public key first because it is the key we generate IID.
[[FIRInstanceIDKeychain sharedInstance] removeItemWithQuery:queryPublicKey
handler:^(NSError *error) {
if (error) {
keychainError = error;
}
}];
[[FIRInstanceIDKeychain sharedInstance] removeItemWithQuery:queryPrivateKey
handler:^(NSError *error) {
if (error) {
keychainError = error;
}
if (handler) {
handler(keychainError);
}
}];
}
- (BOOL)removeKeyPairCreationTimePlistWithError:(NSError *__autoreleasing *)error {
if (![self.plist deleteFile:error]) {
FIRInstanceIDLoggerError(kFIRInstanceIDMessageCodeKeyPairStore008,
@"Unable to delete keypair creation times plist");
return NO;
}
return YES;
}
+ (NSString *)keyStoreFileName {
return kFIRInstanceIDKeyPairStoreFileName;
}
@end
@@ -0,0 +1,36 @@
/*
* 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>
@class FIRInstanceIDKeyPair;
/**
* A web-safe base64 encoded string with no padding.
*
* @param data The data to encode.
*
* @return A web-safe base 64 encoded string with no padding.
*/
FOUNDATION_EXPORT NSString *FIRInstanceIDWebSafeBase64(NSData *data);
FOUNDATION_EXPORT NSData *FIRInstanceIDSHA1(NSData *data);
FOUNDATION_EXPORT NSDictionary *FIRInstanceIDKeyPairQuery(NSString *tag,
BOOL addReturnAttr,
BOOL returnData);
FOUNDATION_EXPORT NSString *FIRInstanceIDAppIdentity(FIRInstanceIDKeyPair *keyPair);
@@ -0,0 +1,83 @@
/*
* 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 "FIRInstanceIDKeyPairUtilities.h"
#import <CommonCrypto/CommonDigest.h>
#import "FIRInstanceIDKeyPair.h"
#import "FIRInstanceIDLogger.h"
#import "FIRInstanceIDStringEncoding.h"
NSString *FIRInstanceIDWebSafeBase64(NSData *data) {
// Websafe encoding with no padding.
FIRInstanceIDStringEncoding *encoding =
[FIRInstanceIDStringEncoding rfc4648Base64WebsafeStringEncoding];
[encoding setDoPad:NO];
return [encoding encode:data];
}
NSData *FIRInstanceIDSHA1(NSData *data) {
unsigned int outputLength = CC_SHA1_DIGEST_LENGTH;
unsigned char output[outputLength];
unsigned int length = (unsigned int)[data length];
CC_SHA1(data.bytes, length, output);
return [NSMutableData dataWithBytes:output length:outputLength];
}
NSDictionary *FIRInstanceIDKeyPairQuery(NSString *tag, BOOL addReturnAttr, BOOL returnData) {
NSMutableDictionary *queryKey = [NSMutableDictionary dictionary];
NSData *tagData = [tag dataUsingEncoding:NSUTF8StringEncoding];
queryKey[(__bridge id)kSecClass] = (__bridge id)kSecClassKey;
queryKey[(__bridge id)kSecAttrApplicationTag] = tagData;
queryKey[(__bridge id)kSecAttrKeyType] = (__bridge id)kSecAttrKeyTypeRSA;
if (addReturnAttr) {
if (returnData) {
queryKey[(__bridge id)kSecReturnData] = @(YES);
} else {
queryKey[(__bridge id)kSecReturnRef] = @(YES);
}
}
return queryKey;
}
NSString *FIRInstanceIDAppIdentity(FIRInstanceIDKeyPair *keyPair) {
// An Instance-ID is a 64 bit (8 byte) integer with a fixed 4-bit header of 0111 (=^ 0x7).
// The variable 60 bits are obtained by truncating the SHA1 of the app-instance's public key.
SecKeyRef publicKeyRef = [keyPair publicKey];
if (!publicKeyRef) {
FIRInstanceIDLoggerError(kFIRInstanceIDMessageCodeKeyPair002,
@"Unable to create a valid asymmetric crypto key");
return nil;
}
NSData *publicKeyData = keyPair.publicKeyData;
NSData *publicKeySHA1 = FIRInstanceIDSHA1(publicKeyData);
const uint8_t *bytes = publicKeySHA1.bytes;
NSMutableData *identityData = [NSMutableData dataWithData:publicKeySHA1];
uint8_t b0 = bytes[0];
// Take the first byte and make the initial four 7 by initially making the initial 4 bits 0
// and then adding 0x70 to it.
b0 = 0x70 + (0xF & b0);
// failsafe should give you back b0 itself
b0 = (b0 & 0xFF);
[identityData replaceBytesInRange:NSMakeRange(0, 1) withBytes:&b0];
NSData *data = [identityData subdataWithRange:NSMakeRange(0, 8 * sizeof(Byte))];
return FIRInstanceIDWebSafeBase64(data);
}
@@ -0,0 +1,76 @@
/*
* 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>
/* The Keychain error domain */
extern NSString *const kFIRInstanceIDKeychainErrorDomain;
@class FIRInstanceIDKeyPair;
/*
* Wrapping the keychain operations in a serialize queue. This is to avoid keychain operation
* blocking main queue.
*/
@interface FIRInstanceIDKeychain : NSObject
/**
* FIRInstanceIDKeychain.
*
* @return A shared instance of FIRInstanceIDKeychain.
*/
+ (instancetype)sharedInstance;
/**
* Get keychain items matching the given a query.
*
* @param keychainQuery The keychain query.
*
* @return An CFTypeRef result matching the provided inputs.
*/
- (CFTypeRef)itemWithQuery:(NSDictionary *)keychainQuery;
/**
* Remove the cached items from the keychain matching the query.
*
* @param keychainQuery The keychain query.
* @param handler The callback handler which is invoked when the remove operation is
* complete, with an error if there is any.
*/
- (void)removeItemWithQuery:(NSDictionary *)keychainQuery handler:(void (^)(NSError *error))handler;
/**
* Add the item with a given query.
*
* @param keychainQuery The keychain query.
* @param handler The callback handler which is invoked when the add operation is
* complete, with an error if there is any.
*/
- (void)addItemWithQuery:(NSDictionary *)keychainQuery handler:(void (^)(NSError *))handler;
#pragma mark - Keypair
/**
* Generate a public/private key pair given their tags.
*
* @param privateTag The private tag associated with the private key.
* @param publicTag The public tag associated with the public key.
*
* @return A new FIRInstanceIDKeyPair object.
*/
- (FIRInstanceIDKeyPair *)generateKeyPairWithPrivateTag:(NSString *)privateTag
publicTag:(NSString *)publicTag;
@end
@@ -0,0 +1,174 @@
/*
* 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 "FIRInstanceIDKeychain.h"
#import "FIRInstanceIDKeyPair.h"
#import "FIRInstanceIDKeyPairUtilities.h"
#import "FIRInstanceIDLogger.h"
NSString *const kFIRInstanceIDKeychainErrorDomain = @"com.google.iid";
static const NSUInteger kRSA2048KeyPairSize = 2048;
@interface FIRInstanceIDKeychain () {
dispatch_queue_t _keychainOperationQueue;
}
@end
@implementation FIRInstanceIDKeychain
+ (instancetype)sharedInstance {
static FIRInstanceIDKeychain *sharedInstance;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedInstance = [[FIRInstanceIDKeychain alloc] init];
});
return sharedInstance;
}
- (instancetype)init {
self = [super init];
if (self) {
_keychainOperationQueue =
dispatch_queue_create("com.google.FirebaseInstanceID.Keychain", DISPATCH_QUEUE_SERIAL);
}
return self;
}
- (CFTypeRef)itemWithQuery:(NSDictionary *)keychainQuery {
__block SecKeyRef keyRef = NULL;
dispatch_sync(_keychainOperationQueue, ^{
OSStatus status =
SecItemCopyMatching((__bridge CFDictionaryRef)keychainQuery, (CFTypeRef *)&keyRef);
if (status != noErr) {
if (keyRef) {
CFRelease(keyRef);
}
FIRInstanceIDLoggerDebug(kFIRInstanceIDKeychainReadItemError,
@"Info is not found in Keychain. OSStatus: %d. Keychain query: %@",
(int)status, keychainQuery);
}
});
return keyRef;
}
- (void)removeItemWithQuery:(NSDictionary *)keychainQuery
handler:(void (^)(NSError *error))handler {
dispatch_async(_keychainOperationQueue, ^{
OSStatus status = SecItemDelete((__bridge CFDictionaryRef)keychainQuery);
if (status != noErr) {
FIRInstanceIDLoggerDebug(
kFIRInstanceIDKeychainDeleteItemError,
@"Couldn't delete item from Keychain OSStatus: %d with the keychain query %@",
(int)status, keychainQuery);
}
if (handler) {
NSError *error;
// When item is not found, it should NOT be considered as an error. The operation should
// continue.
if (status != noErr && status != errSecItemNotFound) {
error = [NSError errorWithDomain:kFIRInstanceIDKeychainErrorDomain
code:status
userInfo:nil];
}
dispatch_async(dispatch_get_main_queue(), ^{
handler(error);
});
}
});
}
- (void)addItemWithQuery:(NSDictionary *)keychainQuery handler:(void (^)(NSError *))handler {
dispatch_async(_keychainOperationQueue, ^{
OSStatus status = SecItemAdd((__bridge CFDictionaryRef)keychainQuery, NULL);
if (handler) {
NSError *error;
if (status != noErr) {
FIRInstanceIDLoggerWarning(kFIRInstanceIDKeychainAddItemError,
@"Couldn't add item to Keychain OSStatus: %d", (int)status);
error = [NSError errorWithDomain:kFIRInstanceIDKeychainErrorDomain
code:status
userInfo:nil];
}
dispatch_async(dispatch_get_main_queue(), ^{
handler(error);
});
}
});
}
- (FIRInstanceIDKeyPair *)generateKeyPairWithPrivateTag:(NSString *)privateTag
publicTag:(NSString *)publicTag {
// TODO(chliangGoogle) this is called by appInstanceID, which is an internal API used by other
// Firebase teams, will see if we can make it async.
NSData *publicTagData = [publicTag dataUsingEncoding:NSUTF8StringEncoding];
NSData *privateTagData = [privateTag dataUsingEncoding:NSUTF8StringEncoding];
NSDictionary *privateKeyAttr = @{
(__bridge id)kSecAttrIsPermanent : @YES,
(__bridge id)kSecAttrApplicationTag : privateTagData,
(__bridge id)kSecAttrLabel : @"Firebase InstanceID Key Pair Private Key",
(__bridge id)kSecAttrAccessible : (__bridge id)kSecAttrAccessibleAlwaysThisDeviceOnly,
};
NSDictionary *publicKeyAttr = @{
(__bridge id)kSecAttrIsPermanent : @YES,
(__bridge id)kSecAttrApplicationTag : publicTagData,
(__bridge id)kSecAttrLabel : @"Firebase InstanceID Key Pair Public Key",
(__bridge id)kSecAttrAccessible : (__bridge id)kSecAttrAccessibleAlwaysThisDeviceOnly,
};
NSDictionary *keyPairAttributes = @{
(__bridge id)kSecAttrKeyType : (__bridge id)kSecAttrKeyTypeRSA,
(__bridge id)kSecAttrLabel : @"Firebase InstanceID Key Pair",
(__bridge id)kSecAttrKeySizeInBits : @(kRSA2048KeyPairSize),
(__bridge id)kSecPrivateKeyAttrs : privateKeyAttr,
(__bridge id)kSecPublicKeyAttrs : publicKeyAttr,
};
__block SecKeyRef privateKey = NULL;
__block SecKeyRef publicKey = NULL;
dispatch_sync(_keychainOperationQueue, ^{
// SecKeyGeneratePair does not allow you to set kSetAttrAccessible on the keys. We need the keys
// to be accessible even when the device is locked (i.e. app is woken up during a push
// notification, or some background refresh).
OSStatus status =
SecKeyGeneratePair((__bridge CFDictionaryRef)keyPairAttributes, &publicKey, &privateKey);
if (status != noErr || publicKey == NULL || privateKey == NULL) {
FIRInstanceIDLoggerWarning(kFIRInstanceIDKeychainCreateKeyPairError,
@"Couldn't create keypair from Keychain OSStatus: %d",
(int)status);
}
});
// Extract the actual public and private key data from the Keychain
NSDictionary *publicKeyDataQuery = FIRInstanceIDKeyPairQuery(publicTag, YES, YES);
NSDictionary *privateKeyDataQuery = FIRInstanceIDKeyPairQuery(privateTag, YES, YES);
NSData *publicKeyData = (__bridge NSData *)[self itemWithQuery:publicKeyDataQuery];
NSData *privateKeyData = (__bridge NSData *)[self itemWithQuery:privateKeyDataQuery];
return [[FIRInstanceIDKeyPair alloc] initWithPrivateKey:privateKey
publicKey:publicKey
publicKeyData:publicKeyData
privateKeyData:privateKeyData];
}
@end
@@ -0,0 +1,66 @@
/*
* 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 "FIRIMessageCode.h"
// The convenience macros are only defined if they haven't already been defined.
#ifndef FIRInstanceIDLoggerInfo
// Convenience macros that log to the shared GTMLogger instance. These macros
// are how users should typically log to FIRInstanceIDLogger.
#define FIRInstanceIDLoggerDebug(code, ...) \
[FIRInstanceIDSharedLogger() logFuncDebug:__func__ messageCode:code msg:__VA_ARGS__]
#define FIRInstanceIDLoggerInfo(code, ...) \
[FIRInstanceIDSharedLogger() logFuncInfo:__func__ messageCode:code msg:__VA_ARGS__]
#define FIRInstanceIDLoggerNotice(code, ...) \
[FIRInstanceIDSharedLogger() logFuncNotice:__func__ messageCode:code msg:__VA_ARGS__]
#define FIRInstanceIDLoggerWarning(code, ...) \
[FIRInstanceIDSharedLogger() logFuncWarning:__func__ messageCode:code msg:__VA_ARGS__]
#define FIRInstanceIDLoggerError(code, ...) \
[FIRInstanceIDSharedLogger() logFuncError:__func__ messageCode:code msg:__VA_ARGS__]
#endif // !defined(FIRInstanceIDLoggerInfo)
@interface FIRInstanceIDLogger : NSObject
- (void)logFuncDebug:(const char *)func
messageCode:(FIRInstanceIDMessageCode)messageCode
msg:(NSString *)fmt, ... NS_FORMAT_FUNCTION(3, 4);
- (void)logFuncInfo:(const char *)func
messageCode:(FIRInstanceIDMessageCode)messageCode
msg:(NSString *)fmt, ... NS_FORMAT_FUNCTION(3, 4);
- (void)logFuncNotice:(const char *)func
messageCode:(FIRInstanceIDMessageCode)messageCode
msg:(NSString *)fmt, ... NS_FORMAT_FUNCTION(3, 4);
- (void)logFuncWarning:(const char *)func
messageCode:(FIRInstanceIDMessageCode)messageCode
msg:(NSString *)fmt, ... NS_FORMAT_FUNCTION(3, 4);
- (void)logFuncError:(const char *)func
messageCode:(FIRInstanceIDMessageCode)messageCode
msg:(NSString *)fmt, ... NS_FORMAT_FUNCTION(3, 4);
@end
/**
* Instantiates and/or returns a shared GTMLogger used exclusively
* for InstanceID log messages.
* @return the shared GTMLogger instance
*/
FIRInstanceIDLogger *FIRInstanceIDSharedLogger(void);
@@ -0,0 +1,92 @@
/*
* 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 "FIRInstanceIDLogger.h"
#import <FirebaseCore/FIRLogger.h>
// Re-definition of FIRLogger service, as it is not included in :FIRAppHeaders target
NSString *const kFIRInstanceIDLoggerService = @"[Firebase/InstanceID]";
@implementation FIRInstanceIDLogger
#pragma mark - Log Helpers
+ (NSString *)formatMessageCode:(FIRInstanceIDMessageCode)messageCode {
return [NSString stringWithFormat:@"I-IID%06ld", (long)messageCode];
}
- (void)logFuncDebug:(const char *)func
messageCode:(FIRInstanceIDMessageCode)messageCode
msg:(NSString *)fmt, ... {
va_list args;
va_start(args, fmt);
FIRLogBasic(FIRLoggerLevelDebug, kFIRInstanceIDLoggerService,
[FIRInstanceIDLogger formatMessageCode:messageCode], fmt, args);
va_end(args);
}
- (void)logFuncInfo:(const char *)func
messageCode:(FIRInstanceIDMessageCode)messageCode
msg:(NSString *)fmt, ... {
va_list args;
va_start(args, fmt);
FIRLogBasic(FIRLoggerLevelInfo, kFIRInstanceIDLoggerService,
[FIRInstanceIDLogger formatMessageCode:messageCode], fmt, args);
va_end(args);
}
- (void)logFuncNotice:(const char *)func
messageCode:(FIRInstanceIDMessageCode)messageCode
msg:(NSString *)fmt, ... {
va_list args;
va_start(args, fmt);
FIRLogBasic(FIRLoggerLevelNotice, kFIRInstanceIDLoggerService,
[FIRInstanceIDLogger formatMessageCode:messageCode], fmt, args);
va_end(args);
}
- (void)logFuncWarning:(const char *)func
messageCode:(FIRInstanceIDMessageCode)messageCode
msg:(NSString *)fmt, ... {
va_list args;
va_start(args, fmt);
FIRLogBasic(FIRLoggerLevelWarning, kFIRInstanceIDLoggerService,
[FIRInstanceIDLogger formatMessageCode:messageCode], fmt, args);
va_end(args);
}
- (void)logFuncError:(const char *)func
messageCode:(FIRInstanceIDMessageCode)messageCode
msg:(NSString *)fmt, ... {
va_list args;
va_start(args, fmt);
FIRLogBasic(FIRLoggerLevelError, kFIRInstanceIDLoggerService,
[FIRInstanceIDLogger formatMessageCode:messageCode], fmt, args);
va_end(args);
}
@end
FIRInstanceIDLogger *FIRInstanceIDSharedLogger() {
static dispatch_once_t onceToken;
static FIRInstanceIDLogger *logger;
dispatch_once(&onceToken, ^{
logger = [[FIRInstanceIDLogger alloc] init];
});
return logger;
}
@@ -0,0 +1,183 @@
/*
* 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
@class FIRInstanceIDBackupExcludedPlist;
@class FIRInstanceIDCheckinPreferences;
@class FIRInstanceIDCheckinStore;
@class FIRInstanceIDTokenInfo;
@class FIRInstanceIDTokenStore;
@class FIRInstanceIDStore;
@protocol FIRInstanceIDStoreDelegate <NSObject>
/**
* This is called when the store has decided to invalide its tokens associated with the
* previous checkin credentials. After deleting the tokens locally, it calls this method
* to notify the delegate of the change. If possible, the delegate should use this time
* to request the invalidation of the tokens on the server as well.
*/
- (void)store:(FIRInstanceIDStore *)store
didDeleteFCMScopedTokensForCheckin:(FIRInstanceIDCheckinPreferences *)checkin;
@end
/**
* Used to persist the InstanceID tokens. This is also used to cache the Checkin
* credentials. The store also checks for stale entries in the store and
* let's us know if things in the store are stale or not. It does not however
* acts on stale entries in anyway.
*/
@interface FIRInstanceIDStore : NSObject
/**
* The delegate set in the initializer which is notified of changes in the store.
*/
@property(nonatomic, readonly, weak) NSObject<FIRInstanceIDStoreDelegate> *delegate;
- (instancetype)init __attribute__((unavailable("Use initWithDelegate: instead.")));
/**
* Initialize a default store to persist InstanceID tokens and options.
*
* @param delegate The delegate with which to be notified of changes in the store.
* @return Store to persist InstanceID tokens.
*/
- (instancetype)initWithDelegate:(NSObject<FIRInstanceIDStoreDelegate> *)delegate;
/**
* Initialize a store with the token store used to persist tokens, and a checkin store.
* Used for testing.
*
* @param checkinStore Persistent store that persists checkin preferences.
* @param tokenStore Persistent store that persists tokens.
*
* @return Store to persist InstanceID tokens and options.
*/
- (instancetype)initWithCheckinStore:(FIRInstanceIDCheckinStore *)checkinStore
tokenStore:(FIRInstanceIDTokenStore *)tokenStore
delegate:(NSObject<FIRInstanceIDStoreDelegate> *)delegate
NS_DESIGNATED_INITIALIZER;
#pragma mark - Save
/**
* Save the instanceID token info to the store.
*
* @param tokenInfo The token info to store.
* @param handler The callback handler which is invoked when the operation is complete,
* with an error if there is any.
*/
- (void)saveTokenInfo:(FIRInstanceIDTokenInfo *)tokenInfo handler:(void (^)(NSError *))handler;
#pragma mark - Get
/**
* Get the cached token info.
*
* @param authorizedEntity The authorized entity for which we want the token.
* @param scope The scope for which we want the token.
*
* @return The cached token info if any for the given authorizedEntity and scope else
* returns nil.
*/
- (nullable FIRInstanceIDTokenInfo *)tokenInfoWithAuthorizedEntity:(NSString *)authorizedEntity
scope:(NSString *)scope;
/**
* Return all cached token infos from the Keychain.
*
* @return The cached token infos, if any, that are stored in the Keychain.
*/
- (NSArray<FIRInstanceIDTokenInfo *> *)cachedTokenInfos;
#pragma mark - Delete
/**
* Remove the cached token for a given authorizedEntity and scope. If the token was never
* cached or deleted from the cache before this is a no-op.
*
* @param authorizedEntity The authorizedEntity for the cached token.
* @param scope The scope for the cached token
*/
- (void)removeCachedTokenWithAuthorizedEntity:(NSString *)authorizedEntity scope:(NSString *)scope;
/**
* Removes all cached tokens from the persistent store. In case deleting the cached tokens
* fails we try to delete the backup excluded plist that stores the tokens.
*
* @param handler The callback handler which is invoked when the operation is complete,
* with an error if there is any.
*
*/
- (void)removeAllCachedTokensWithHandler:(nullable void (^)(NSError *error))handler;
#pragma mark - Persisting Checkin Preferences
/**
* Save the checkin preferences
*
* @param preferences Checkin preferences to save.
* @param handler The callback handler which is invoked when the operation is complete,
* with an error if there is any.
*/
- (void)saveCheckinPreferences:(FIRInstanceIDCheckinPreferences *)preferences
handler:(nullable void (^)(NSError *error))handler;
/**
* Return the cached checkin preferences.
*
* @return Checkin preferences.
*/
- (FIRInstanceIDCheckinPreferences *)cachedCheckinPreferences;
/**
* Remove the cached checkin preferences from the store.
*
* @param handler The callback handler which is invoked when the operation is complete,
* with an error if there is any.
*/
- (void)removeCheckinPreferencesWithHandler:(nullable void (^)(NSError *error))handler;
#pragma mark - Standard Directory sub-directory
/**
* Check if supported directory has InstanceID subdirectory
*
* @return YES if the Application Support directory has InstanceID subdirectory else NO.
*/
+ (BOOL)hasSubDirectory:(NSString *)subDirectoryName;
/**
* Create InstanceID subdirectory in Application support directory.
*
* @return YES if the subdirectory was created successfully else NO.
*/
+ (BOOL)createSubDirectory:(NSString *)subDirectoryName;
/**
* Removes Application Support subdirectory for InstanceID.
*
* @param error The error object if any while trying to delete the sub-directory.
*
* @return YES if the deletion was successful else NO.
*/
+ (BOOL)removeSubDirectory:(NSString *)subDirectoryName error:(NSError **)error;
@end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,242 @@
/*
* 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<FIRInstanceIDStoreDelegate> *)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<FIRInstanceIDStoreDelegate> *)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<FIRInstanceIDTokenInfo *> *)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
@@ -0,0 +1,66 @@
//
// GTMStringEncoding.h
//
// Copyright 2010 Google Inc.
//
// 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.
//
// This is a copy of GTMStringEncoding. FIRInstanceID wants to avoid
// a CocoaPods GTM dependency. Hence we use our own version of StringEncoding.
#import <Foundation/Foundation.h>
// A generic class for arbitrary base-2 to 128 string encoding and decoding.
@interface FIRInstanceIDStringEncoding : NSObject {
@private
NSData *charMapData_;
char *charMap_;
int reverseCharMap_[128];
int shift_;
unsigned int mask_;
BOOL doPad_;
char paddingChar_;
int padLen_;
}
+ (id)rfc4648Base64WebsafeStringEncoding;
// Create a new, autoreleased GTMStringEncoding object with the given string,
// as described below.
+ (id)stringEncodingWithString:(NSString *)string;
// Initialize a new GTMStringEncoding object with the string.
//
// The length of the string must be a power of 2, at least 2 and at most 128.
// Only 7-bit ASCII characters are permitted in the string.
//
// These characters are the canonical set emitted during encoding.
// If the characters have alternatives (e.g. case, easily transposed) then use
// addDecodeSynonyms: to configure them.
- (id)initWithString:(NSString *)string;
// Indicates whether padding is performed during encoding.
- (BOOL)doPad;
- (void)setDoPad:(BOOL)doPad;
// Sets the padding character to use during encoding.
- (void)setPaddingChar:(char)c;
// Encode a raw binary buffer to a 7-bit ASCII string.
- (NSString *)encode:(NSData *)data;
// Decode a 7-bit ASCII string to a raw binary buffer.
- (NSData *)decode:(NSString *)string;
@end
@@ -0,0 +1,208 @@
//
// FIRInstanceIDStringEncoding.m
//
// Copyright 2009 Google Inc.
//
// 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.
//
// This is a copy of GTMStringEncoding. FIRInstanceID wants to avoid
// a CocoaPods GTM dependency. Hence we use our own version of StringEncoding.
#import "FIRInstanceIDStringEncoding.h"
#import "FIRInstanceIDLogger.h"
enum { kUnknownChar = -1, kPaddingChar = -2, kIgnoreChar = -3 };
@implementation FIRInstanceIDStringEncoding
+ (id)rfc4648Base64WebsafeStringEncoding {
FIRInstanceIDStringEncoding *ret = [self
stringEncodingWithString:@"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"];
[ret setPaddingChar:'='];
[ret setDoPad:YES];
return ret;
}
static inline int lcm(int a, int b) {
for (int aa = a, bb = b;;) {
if (aa == bb)
return aa;
else if (aa < bb)
aa += a;
else
bb += b;
}
}
+ (id)stringEncodingWithString:(NSString *)string {
return [[FIRInstanceIDStringEncoding alloc] initWithString:string];
}
- (id)initWithString:(NSString *)string {
if ((self = [super init])) {
charMapData_ = [string dataUsingEncoding:NSASCIIStringEncoding];
if (!charMapData_) {
// Unable to convert string to ASCII
return nil;
}
charMap_ = (char *)[charMapData_ bytes];
NSUInteger length = [charMapData_ length];
if (length < 2 || length > 128 || length & (length - 1)) {
// Length not a power of 2 between 2 and 128
return nil;
}
memset(reverseCharMap_, kUnknownChar, sizeof(reverseCharMap_));
for (unsigned int i = 0; i < length; i++) {
if (reverseCharMap_[(int)charMap_[i]] != kUnknownChar) {
// Duplicate character at |i|
return nil;
}
reverseCharMap_[(int)charMap_[i]] = i;
}
for (NSUInteger i = 1; i < length; i <<= 1) shift_++;
mask_ = (1 << shift_) - 1;
padLen_ = lcm(8, shift_) / shift_;
}
return self;
}
- (NSString *)description {
return [NSString stringWithFormat:@"<Base%d StringEncoder: %@>", 1 << shift_, charMapData_];
}
- (BOOL)doPad {
return doPad_;
}
- (void)setDoPad:(BOOL)doPad {
doPad_ = doPad;
}
- (void)setPaddingChar:(char)c {
paddingChar_ = c;
reverseCharMap_[(int)c] = kPaddingChar;
}
- (NSString *)encode:(NSData *)inData {
NSUInteger inLen = [inData length];
if (inLen <= 0) {
// Empty input
return @"";
}
unsigned char *inBuf = (unsigned char *)[inData bytes];
NSUInteger inPos = 0;
NSUInteger outLen = (inLen * 8 + shift_ - 1) / shift_;
if (doPad_) {
outLen = ((outLen + padLen_ - 1) / padLen_) * padLen_;
}
NSMutableData *outData = [NSMutableData dataWithLength:outLen];
unsigned char *outBuf = (unsigned char *)[outData mutableBytes];
NSUInteger outPos = 0;
unsigned int buffer = inBuf[inPos++];
int bitsLeft = 8;
while (bitsLeft > 0 || inPos < inLen) {
if (bitsLeft < shift_) {
if (inPos < inLen) {
buffer <<= 8;
buffer |= (inBuf[inPos++] & 0xff);
bitsLeft += 8;
} else {
int pad = shift_ - bitsLeft;
buffer <<= pad;
bitsLeft += pad;
}
}
unsigned int idx = (buffer >> (bitsLeft - shift_)) & mask_;
bitsLeft -= shift_;
outBuf[outPos++] = charMap_[idx];
}
if (doPad_) {
while (outPos < outLen) outBuf[outPos++] = paddingChar_;
}
if (outPos != outLen) {
FIRInstanceIDLoggerError(kFIRInstanceIDStringEncodingBufferUnderflow,
@"Underflowed output buffer");
return nil;
}
[outData setLength:outPos];
return [[NSString alloc] initWithData:outData encoding:NSASCIIStringEncoding];
}
- (NSData *)decode:(NSString *)inString {
char *inBuf = (char *)[inString cStringUsingEncoding:NSASCIIStringEncoding];
if (!inBuf) {
// Unable to convert buffer to ASCII
return nil;
}
NSUInteger inLen = strlen(inBuf);
NSUInteger outLen = inLen * shift_ / 8;
NSMutableData *outData = [NSMutableData dataWithLength:outLen];
unsigned char *outBuf = (unsigned char *)[outData mutableBytes];
NSUInteger outPos = 0;
int buffer = 0;
int bitsLeft = 0;
BOOL expectPad = NO;
for (NSUInteger i = 0; i < inLen; i++) {
int val = reverseCharMap_[(int)inBuf[i]];
switch (val) {
case kIgnoreChar:
break;
case kPaddingChar:
expectPad = YES;
break;
case kUnknownChar:
// Unexpected data at input pos |i|
return nil;
default:
if (expectPad) {
// Expected further padding characters
return nil;
}
buffer <<= shift_;
buffer |= val & mask_;
bitsLeft += shift_;
if (bitsLeft >= 8) {
outBuf[outPos++] = (unsigned char)(buffer >> (bitsLeft - 8));
bitsLeft -= 8;
}
break;
}
}
if (bitsLeft && buffer & ((1 << bitsLeft) - 1)) {
// Incomplete trailing data
return nil;
}
// Shorten buffer if needed due to padding chars
if (outPos > outLen) {
FIRInstanceIDLoggerError(kFIRInstanceIDStringEncodingBufferOverflow, @"Overflowed buffer");
}
[outData setLength:outPos];
return outData;
}
@end
@@ -0,0 +1,31 @@
/*
* 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 "FIRInstanceIDTokenOperation.h"
NS_ASSUME_NONNULL_BEGIN
@interface FIRInstanceIDTokenDeleteOperation : FIRInstanceIDTokenOperation
- (instancetype)initWithAuthorizedEntity:(nullable NSString *)authorizedEntity
scope:(nullable NSString *)scope
checkinPreferences:(FIRInstanceIDCheckinPreferences *)checkinPreferences
keyPair:(nullable FIRInstanceIDKeyPair *)keyPair
action:(FIRInstanceIDTokenAction)action;
@end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,120 @@
/*
* 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 "FIRInstanceIDTokenDeleteOperation.h"
#import "FIRInstanceIDCheckinPreferences.h"
#import "FIRInstanceIDDefines.h"
#import "FIRInstanceIDLogger.h"
#import "FIRInstanceIDTokenOperation+Private.h"
#import "FIRInstanceIDURLQueryItem.h"
#import "FIRInstanceIDUtilities.h"
#import "NSError+FIRInstanceID.h"
@implementation FIRInstanceIDTokenDeleteOperation
- (instancetype)initWithAuthorizedEntity:(NSString *)authorizedEntity
scope:(NSString *)scope
checkinPreferences:(FIRInstanceIDCheckinPreferences *)checkinPreferences
keyPair:(FIRInstanceIDKeyPair *)keyPair
action:(FIRInstanceIDTokenAction)action {
self = [super initWithAction:action
forAuthorizedEntity:authorizedEntity
scope:scope
options:nil
checkinPreferences:checkinPreferences
keyPair:keyPair];
if (self) {
}
return self;
}
- (void)performTokenOperation {
NSString *authHeader =
[FIRInstanceIDTokenOperation HTTPAuthHeaderFromCheckin:self.checkinPreferences];
NSMutableURLRequest *request = [FIRInstanceIDTokenOperation requestWithAuthHeader:authHeader];
// Build form-encoded body
NSString *deviceAuthID = self.checkinPreferences.deviceID;
NSMutableArray<FIRInstanceIDURLQueryItem *> *queryItems =
[FIRInstanceIDTokenOperation standardQueryItemsWithDeviceID:deviceAuthID scope:self.scope];
[queryItems addObject:[FIRInstanceIDURLQueryItem queryItemWithName:@"delete" value:@"true"]];
if (self.action == FIRInstanceIDTokenActionDeleteTokenAndIID) {
[queryItems addObject:[FIRInstanceIDURLQueryItem queryItemWithName:@"iid-operation"
value:@"delete"]];
}
if (self.authorizedEntity) {
[queryItems addObject:[FIRInstanceIDURLQueryItem queryItemWithName:@"sender"
value:self.authorizedEntity]];
}
// Typically we include our public key-signed url items, but in some cases (like deleting all FCM
// tokens), we don't.
if (self.keyPair != nil) {
[queryItems addObjectsFromArray:[self queryItemsWithKeyPair:self.keyPair]];
}
NSString *content = FIRInstanceIDQueryFromQueryItems(queryItems);
request.HTTPBody = [content dataUsingEncoding:NSUTF8StringEncoding];
FIRInstanceIDLoggerDebug(kFIRInstanceIDMessageCodeTokenDeleteOperationFetchRequest,
@"Unregister request to %@ content: %@", FIRInstanceIDRegisterServer(),
content);
FIRInstanceID_WEAKIFY(self);
void (^requestHandler)(NSData *, NSURLResponse *, NSError *) =
^(NSData *data, NSURLResponse *response, NSError *error) {
FIRInstanceID_STRONGIFY(self);
[self handleResponseWithData:data response:response error:error];
};
// Test block
if (self.testBlock) {
self.testBlock(request, requestHandler);
return;
}
NSURLSession *session = [FIRInstanceIDTokenOperation sharedURLSession];
self.dataTask = [session dataTaskWithRequest:request completionHandler:requestHandler];
[self.dataTask resume];
}
- (void)handleResponseWithData:(NSData *)data
response:(NSURLResponse *)response
error:(NSError *)error {
if (error) {
FIRInstanceIDLoggerDebug(kFIRInstanceIDMessageCodeTokenDeleteOperationRequestError,
@"Device unregister HTTP fetch error. Error code: %ld",
_FIRInstanceID_L(error.code));
[self finishWithResult:FIRInstanceIDTokenOperationError token:nil error:error];
return;
}
NSString *dataResponse = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
if (dataResponse.length == 0) {
NSError *error = [NSError errorWithFIRInstanceIDErrorCode:kFIRInstanceIDErrorCodeUnknown];
[self finishWithResult:FIRInstanceIDTokenOperationError token:nil error:error];
return;
}
if (![dataResponse hasPrefix:@"deleted="] && ![dataResponse hasPrefix:@"token="]) {
FIRInstanceIDLoggerDebug(kFIRInstanceIDMessageCodeTokenDeleteOperationBadResponse,
@"Invalid unregister response %@", response);
NSError *error = [NSError errorWithFIRInstanceIDErrorCode:kFIRInstanceIDErrorCodeUnknown];
[self finishWithResult:FIRInstanceIDTokenOperationError token:nil error:error];
return;
}
[self finishWithResult:FIRInstanceIDTokenOperationSucceeded token:nil error:nil];
}
@end
@@ -0,0 +1,32 @@
/*
* 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 "FIRInstanceIDTokenOperation.h"
NS_ASSUME_NONNULL_BEGIN
FOUNDATION_EXPORT NSString *const kFIRInstanceIDFirebaseUserAgentKey;
@interface FIRInstanceIDTokenFetchOperation : FIRInstanceIDTokenOperation
- (instancetype)initWithAuthorizedEntity:(NSString *)authorizedEntity
scope:(NSString *)scope
options:(nullable NSDictionary<NSString *, NSString *> *)options
checkinPreferences:(FIRInstanceIDCheckinPreferences *)checkinPreferences
keyPair:(FIRInstanceIDKeyPair *)keyPair;
@end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,204 @@
/*
* 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 "FIRInstanceIDTokenFetchOperation.h"
#import "FIRInstanceIDCheckinPreferences.h"
#import "FIRInstanceIDConstants.h"
#import "FIRInstanceIDDefines.h"
#import "FIRInstanceIDLogger.h"
#import "FIRInstanceIDTokenOperation+Private.h"
#import "FIRInstanceIDURLQueryItem.h"
#import "FIRInstanceIDUtilities.h"
#import "NSError+FIRInstanceID.h"
#import <FirebaseCore/FIRAppInternal.h>
// We can have a static int since this error should theoretically only
// happen once (for the first time). If it repeats there is something
// else that is wrong.
static int phoneRegistrationErrorRetryCount = 0;
static const int kMaxPhoneRegistrationErrorRetryCount = 10;
NSString *const kFIRInstanceIDFirebaseUserAgentKey = @"X-firebase-client";
@implementation FIRInstanceIDTokenFetchOperation
- (instancetype)initWithAuthorizedEntity:(NSString *)authorizedEntity
scope:(NSString *)scope
options:(nullable NSDictionary<NSString *, NSString *> *)options
checkinPreferences:(FIRInstanceIDCheckinPreferences *)checkinPreferences
keyPair:(FIRInstanceIDKeyPair *)keyPair {
self = [super initWithAction:FIRInstanceIDTokenActionFetch
forAuthorizedEntity:authorizedEntity
scope:scope
options:options
checkinPreferences:checkinPreferences
keyPair:keyPair];
if (self) {
}
return self;
}
- (void)performTokenOperation {
NSString *authHeader =
[FIRInstanceIDTokenOperation HTTPAuthHeaderFromCheckin:self.checkinPreferences];
NSMutableURLRequest *request = [[self class] requestWithAuthHeader:authHeader];
NSString *checkinVersionInfo = self.checkinPreferences.versionInfo;
[request setValue:checkinVersionInfo forHTTPHeaderField:@"info"];
[request setValue:[FIRApp firebaseUserAgent]
forHTTPHeaderField:kFIRInstanceIDFirebaseUserAgentKey];
// Build form-encoded body
NSString *deviceAuthID = self.checkinPreferences.deviceID;
NSMutableArray<FIRInstanceIDURLQueryItem *> *queryItems =
[[self class] standardQueryItemsWithDeviceID:deviceAuthID scope:self.scope];
[queryItems addObject:[FIRInstanceIDURLQueryItem queryItemWithName:@"sender"
value:self.authorizedEntity]];
[queryItems addObject:[FIRInstanceIDURLQueryItem queryItemWithName:@"X-subtype"
value:self.authorizedEntity]];
[queryItems addObjectsFromArray:[self queryItemsWithKeyPair:self.keyPair]];
// Create query items from passed-in options
id apnsTokenData = self.options[kFIRInstanceIDTokenOptionsAPNSKey];
id apnsSandboxValue = self.options[kFIRInstanceIDTokenOptionsAPNSIsSandboxKey];
if ([apnsTokenData isKindOfClass:[NSData class]] &&
[apnsSandboxValue isKindOfClass:[NSNumber class]]) {
NSString *APNSString = FIRInstanceIDAPNSTupleStringForTokenAndServerType(
apnsTokenData, ((NSNumber *)apnsSandboxValue).boolValue);
// The name of the query item happens to be the same as the dictionary key
FIRInstanceIDURLQueryItem *item =
[FIRInstanceIDURLQueryItem queryItemWithName:kFIRInstanceIDTokenOptionsAPNSKey
value:APNSString];
[queryItems addObject:item];
}
id firebaseAppID = self.options[kFIRInstanceIDTokenOptionsFirebaseAppIDKey];
if ([firebaseAppID isKindOfClass:[NSString class]]) {
// The name of the query item happens to be the same as the dictionary key
FIRInstanceIDURLQueryItem *item =
[FIRInstanceIDURLQueryItem queryItemWithName:kFIRInstanceIDTokenOptionsFirebaseAppIDKey
value:(NSString *)firebaseAppID];
[queryItems addObject:item];
}
NSString *content = FIRInstanceIDQueryFromQueryItems(queryItems);
request.HTTPBody = [content dataUsingEncoding:NSUTF8StringEncoding];
FIRInstanceIDLoggerDebug(kFIRInstanceIDMessageCodeTokenFetchOperationFetchRequest,
@"Register request to %@ content: %@", FIRInstanceIDRegisterServer(),
content);
FIRInstanceID_WEAKIFY(self);
void (^requestHandler)(NSData *, NSURLResponse *, NSError *) =
^(NSData *data, NSURLResponse *response, NSError *error) {
FIRInstanceID_STRONGIFY(self);
[self handleResponseWithData:data response:response error:error];
};
// Test block
if (self.testBlock) {
self.testBlock(request, requestHandler);
return;
}
NSURLSession *session = [FIRInstanceIDTokenOperation sharedURLSession];
self.dataTask = [session dataTaskWithRequest:request completionHandler:requestHandler];
[self.dataTask resume];
}
#pragma mark - Request Handling
- (void)handleResponseWithData:(NSData *)data
response:(NSURLResponse *)response
error:(NSError *)error {
if (error) {
FIRInstanceIDLoggerDebug(kFIRInstanceIDMessageCodeTokenFetchOperationRequestError,
@"Token fetch HTTP error. Error Code: %ld", (long)error.code);
[self finishWithResult:FIRInstanceIDTokenOperationError token:nil error:error];
return;
}
NSString *dataResponse = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
if (dataResponse.length == 0) {
NSError *error = [NSError errorWithFIRInstanceIDErrorCode:kFIRInstanceIDErrorCodeUnknown];
[self finishWithResult:FIRInstanceIDTokenOperationError token:nil error:error];
return;
}
NSDictionary *parsedResponse = [self parseFetchTokenResponse:dataResponse];
if ([parsedResponse[@"token"] length]) {
[self finishWithResult:FIRInstanceIDTokenOperationSucceeded
token:parsedResponse[@"token"]
error:nil];
return;
}
NSString *errorValue = parsedResponse[@"Error"];
NSError *responseError;
if (errorValue.length) {
NSArray *errorComponents = [errorValue componentsSeparatedByString:@":"];
// HACK (Kansas replication delay), PHONE_REGISTRATION_ERROR on App
// uninstall and reinstall.
if ([errorComponents containsObject:@"PHONE_REGISTRATION_ERROR"]) {
// Encountered issue http://b/27043795
// Retry register until successful or another error encountered or a
// certain number of tries are over.
if (phoneRegistrationErrorRetryCount < kMaxPhoneRegistrationErrorRetryCount) {
const int nextRetryInterval = 1 << phoneRegistrationErrorRetryCount;
FIRInstanceID_WEAKIFY(self);
dispatch_after(
dispatch_time(DISPATCH_TIME_NOW, (int64_t)(nextRetryInterval * NSEC_PER_SEC)),
dispatch_get_main_queue(), ^{
FIRInstanceID_STRONGIFY(self);
phoneRegistrationErrorRetryCount++;
[self performTokenOperation];
});
return;
}
} else if ([errorComponents containsObject:kFIRInstanceID_CMD_RST]) {
// Server detected the identity we use is no longer valid.
NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
[center postNotificationName:kFIRInstanceIDIdentityInvalidatedNotification object:nil];
FIRInstanceIDLoggerDebug(kFIRInstanceIDMessageCodeInternal001,
@"Identity is invalid. Server request identity reset.");
responseError =
[NSError errorWithFIRInstanceIDErrorCode:kFIRInstanceIDErrorCodeInvalidIdentity];
}
}
if (!responseError) {
FIRInstanceIDLoggerDebug(kFIRInstanceIDMessageCodeTokenFetchOperationBadResponse,
@"Invalid fetch response, expected 'token' or 'Error' key");
responseError = [NSError errorWithFIRInstanceIDErrorCode:kFIRInstanceIDErrorCodeUnknown];
}
[self finishWithResult:FIRInstanceIDTokenOperationError token:nil error:responseError];
}
// expect a response e.g. "token=<reg id>\nGOOG.ttl=123"
- (NSDictionary *)parseFetchTokenResponse:(NSString *)response {
NSArray *lines = [response componentsSeparatedByString:@"\n"];
NSMutableDictionary *parsedResponse = [NSMutableDictionary dictionary];
for (NSString *line in lines) {
NSArray *keyAndValue = [line componentsSeparatedByString:@"="];
if ([keyAndValue count] > 1) {
parsedResponse[keyAndValue[0]] = keyAndValue[1];
}
}
return parsedResponse;
}
@end
@@ -0,0 +1,82 @@
/*
* 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>
#import "FIRInstanceIDAPNSInfo.h"
NS_ASSUME_NONNULL_BEGIN
/**
* Represents an Instance ID token, and all of the relevant information
* associated with it. It can read from and write to an NSDictionary object, for
* simple serialization.
*/
@interface FIRInstanceIDTokenInfo : NSObject <NSCoding>
/// The authorized entity (also known as Sender ID), associated with the token.
@property(nonatomic, readonly, copy) NSString *authorizedEntity;
/// The scope associated with the token. This is an arbitrary string, typically "*".
@property(nonatomic, readonly, copy) NSString *scope;
/// The token value itself, with which all other properties are associated.
@property(nonatomic, readonly, copy) NSString *token;
// These properties are nullable because they might not exist for tokens fetched from
// legacy storage formats.
/// The app version that this token represents.
@property(nonatomic, readonly, copy, nullable) NSString *appVersion;
/// The Firebase app ID (also known as GMP App ID), that this token is associated with.
@property(nonatomic, readonly, copy, nullable) NSString *firebaseAppID;
/// Tokens may not always be associated with an APNs token, and may be associated after
/// being created.
@property(nonatomic, strong, nullable) FIRInstanceIDAPNSInfo *APNSInfo;
/// The time that this token info was updated. The cache time is writeable, since in
/// some cases the token info may be refreshed from the server. In those situations,
/// the cacheTime would be updated.
@property(nonatomic, copy, nullable) NSDate *cacheTime;
/**
* Initializes a FIRInstanceIDTokenInfo object with the required parameters. These
* parameters represent all the relevant associated data with a token.
*
* @param authorizedEntity The authorized entity (also known as Sender ID).
* @param scope The scope of the token, typically "*" meaning
* it's a "default scope".
* @param token The token value itself.
* @param appVersion The application version that this token is associated with.
* @param firebaseAppID The Firebase app ID which this token is associated with.
* @return An instance of FIRInstanceIDTokenInfo.
*/
- (instancetype)initWithAuthorizedEntity:(NSString *)authorizedEntity
scope:(NSString *)scope
token:(NSString *)token
appVersion:(nullable NSString *)appVersion
firebaseAppID:(nullable NSString *)firebaseAppID;
/**
* Check whether the token is still fresh based on:
* 1. Last fetch token is within the 7 days.
* 2. Language setting is not changed.
* 3. App version is current.
* 4. GMP App ID is current.
*/
- (BOOL)isFresh;
@end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,198 @@
/*
* 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 "FIRInstanceIDTokenInfo.h"
#import "FIRInstanceIDLogger.h"
#import "FIRInstanceIDUtilities.h"
/**
* @enum Token Info Dictionary Key Constants
* @discussion The keys that are checked when a token info is
* created from a dictionary. The same keys are used
* when decoding/encoding an archive.
*/
/// Specifies a dictonary key whose value represents the authorized entity, or
/// Sender ID for the token.
static NSString *const kFIRInstanceIDAuthorizedEntityKey = @"authorized_entity";
/// Specifies a dictionary key whose value represents the scope of the token,
/// typically "*".
static NSString *const kFIRInstanceIDScopeKey = @"scope";
/// Specifies a dictionary key which represents the token value itself.
static NSString *const kFIRInstanceIDTokenKey = @"token";
/// Specifies a dictionary key which represents the app version associated
/// with the token.
static NSString *const kFIRInstanceIDAppVersionKey = @"app_version";
/// Specifies a dictionary key which represents the GMP App ID associated with
/// the token.
static NSString *const kFIRInstanceIDFirebaseAppIDKey = @"firebase_app_id";
/// Specifies a dictionary key representing an archive for a
/// `FIRInstanceIDAPNSInfo` object.
static NSString *const kFIRInstanceIDAPNSInfoKey = @"apns_info";
/// Specifies a dictionary key representing the "last cached" time for the token.
static NSString *const kFIRInstanceIDCacheTimeKey = @"cache_time";
/// Default interval that token stays fresh.
const NSTimeInterval kDefaultFetchTokenInterval = 7 * 24 * 60 * 60; // 7 days.
@implementation FIRInstanceIDTokenInfo
- (instancetype)initWithAuthorizedEntity:(NSString *)authorizedEntity
scope:(NSString *)scope
token:(NSString *)token
appVersion:(NSString *)appVersion
firebaseAppID:(NSString *)firebaseAppID {
self = [super init];
if (self) {
_authorizedEntity = [authorizedEntity copy];
_scope = [scope copy];
_token = [token copy];
_appVersion = [appVersion copy];
_firebaseAppID = [firebaseAppID copy];
}
return self;
}
- (BOOL)isFresh {
// Last fetch token cache time could be null if token is from legacy storage format. Then token is
// considered not fresh and should be refreshed and overwrite with the latest storage format.
if (!_cacheTime) {
return NO;
}
// Check if app has just been updated to a new version.
NSString *currentAppVersion = FIRInstanceIDCurrentAppVersion();
if (!_appVersion || ![_appVersion isEqualToString:currentAppVersion]) {
FIRInstanceIDLoggerDebug(kFIRInstanceIDMessageCodeTokenManager004,
@"Invalidating cached token for %@ (%@) due to app version change.",
_authorizedEntity, _scope);
return NO;
}
// Check if GMP App ID has changed
NSString *currentFirebaseAppID = FIRInstanceIDFirebaseAppID();
if (!_firebaseAppID || ![_firebaseAppID isEqualToString:currentFirebaseAppID]) {
FIRInstanceIDLoggerDebug(
kFIRInstanceIDMessageCodeTokenInfoFirebaseAppIDChanged,
@"Invalidating cached token due to Firebase App IID change from %@ to %@", _firebaseAppID,
currentFirebaseAppID);
return NO;
}
// Check whether locale has changed, if yes, token needs to be updated with server for locale
// information.
if (FIRInstanceIDHasLocaleChanged()) {
FIRInstanceIDLoggerDebug(kFIRInstanceIDMessageCodeTokenInfoLocaleChanged,
@"Invalidating cached token due to locale change");
return NO;
}
// Locale is not changed, check whether token has been fetched within 7 days.
NSTimeInterval lastFetchTokenTimestamp = [_cacheTime timeIntervalSince1970];
NSTimeInterval currentTimestamp = FIRInstanceIDCurrentTimestampInSeconds();
NSTimeInterval timeSinceLastFetchToken = currentTimestamp - lastFetchTokenTimestamp;
return (timeSinceLastFetchToken < kDefaultFetchTokenInterval);
}
#pragma mark - NSCoding
- (nullable instancetype)initWithCoder:(NSCoder *)aDecoder {
// These value cannot be nil
id authorizedEntity = [aDecoder decodeObjectForKey:kFIRInstanceIDAuthorizedEntityKey];
if (![authorizedEntity isKindOfClass:[NSString class]]) {
return nil;
}
id scope = [aDecoder decodeObjectForKey:kFIRInstanceIDScopeKey];
if (![scope isKindOfClass:[NSString class]]) {
return nil;
}
id token = [aDecoder decodeObjectForKey:kFIRInstanceIDTokenKey];
if (![token isKindOfClass:[NSString class]]) {
return nil;
}
// These values are nullable, so only fail the decode if the type does not match
id appVersion = [aDecoder decodeObjectForKey:kFIRInstanceIDAppVersionKey];
if (appVersion && ![appVersion isKindOfClass:[NSString class]]) {
return nil;
}
id firebaseAppID = [aDecoder decodeObjectForKey:kFIRInstanceIDFirebaseAppIDKey];
if (firebaseAppID && ![firebaseAppID isKindOfClass:[NSString class]]) {
return nil;
}
id rawAPNSInfo = [aDecoder decodeObjectForKey:kFIRInstanceIDAPNSInfoKey];
if (rawAPNSInfo && ![rawAPNSInfo isKindOfClass:[NSData class]]) {
return nil;
}
FIRInstanceIDAPNSInfo *APNSInfo = nil;
if (rawAPNSInfo) {
// TODO(chliangGoogle: Use the new API and secureCoding protocol.
@try {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
APNSInfo = [NSKeyedUnarchiver unarchiveObjectWithData:rawAPNSInfo];
#pragma clang diagnostic pop
} @catch (NSException *exception) {
FIRInstanceIDLoggerInfo(kFIRInstanceIDMessageCodeTokenInfoBadAPNSInfo,
@"Could not parse raw APNS Info while parsing archived token info.");
APNSInfo = nil;
} @finally {
}
}
id cacheTime = [aDecoder decodeObjectForKey:kFIRInstanceIDCacheTimeKey];
if (cacheTime && ![cacheTime isKindOfClass:[NSDate class]]) {
return nil;
}
self = [super init];
if (self) {
_authorizedEntity = authorizedEntity;
_scope = scope;
_token = token;
_appVersion = appVersion;
_firebaseAppID = firebaseAppID;
_APNSInfo = APNSInfo;
_cacheTime = cacheTime;
}
return self;
}
- (void)encodeWithCoder:(NSCoder *)aCoder {
[aCoder encodeObject:self.authorizedEntity forKey:kFIRInstanceIDAuthorizedEntityKey];
[aCoder encodeObject:self.scope forKey:kFIRInstanceIDScopeKey];
[aCoder encodeObject:self.token forKey:kFIRInstanceIDTokenKey];
[aCoder encodeObject:self.appVersion forKey:kFIRInstanceIDAppVersionKey];
[aCoder encodeObject:self.firebaseAppID forKey:kFIRInstanceIDFirebaseAppIDKey];
NSData *rawAPNSInfo;
if (self.APNSInfo) {
// TODO(chliangGoogle: Use the new API and secureCoding protocol.
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
rawAPNSInfo = [NSKeyedArchiver archivedDataWithRootObject:self.APNSInfo];
#pragma clang diagnostic pop
[aCoder encodeObject:rawAPNSInfo forKey:kFIRInstanceIDAPNSInfoKey];
}
[aCoder encodeObject:self.cacheTime forKey:kFIRInstanceIDCacheTimeKey];
}
@end
@@ -0,0 +1,149 @@
/*
* 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 "FIRInstanceID.h"
@class FIRInstanceIDAuthService;
@class FIRInstanceIDCheckinPreferences;
@class FIRInstanceIDKeyPair;
@class FIRInstanceIDTokenInfo;
@class FIRInstanceIDStore;
typedef NS_OPTIONS(NSUInteger, FIRInstanceIDInvalidTokenReason) {
FIRInstanceIDInvalidTokenReasonNone = 0, // 0
FIRInstanceIDInvalidTokenReasonAppVersion = (1 << 0), // 0...00001
FIRInstanceIDInvalidTokenReasonAPNSToken = (1 << 1), // 0...00010
};
/**
* Manager for the InstanceID token requests i.e `newToken` and `deleteToken`. This
* manages the overall interaction of the `InstanceIDStore`, the token register
* service and the callbacks associated with `GCMInstanceID`.
*/
@interface FIRInstanceIDTokenManager : NSObject
/// Expose the auth service, so it can be used by others
@property(nonatomic, readonly, strong) FIRInstanceIDAuthService *authService;
/**
* Fetch new token for the given authorizedEntity and scope. This makes an
* asynchronous request to the InstanceID backend to create a new token for
* the service and returns it. This will replace any old token for the given
* authorizedEntity and scope that has been cached before.
*
* @param authorizedEntity The authorized entity for the token, should not be nil.
* @param scope The scope for the token, should not be nil.
* @param keyPair The keyPair that represents the app identity.
* @param options The options to be added to the fetch request.
* @param handler The handler to be invoked once we have the token or the
* fetch request to InstanceID backend results in an error. Also
* since it's a public handler it should always be called
* asynchronously. This should be non-nil.
*/
- (void)fetchNewTokenWithAuthorizedEntity:(NSString *)authorizedEntity
scope:(NSString *)scope
keyPair:(FIRInstanceIDKeyPair *)keyPair
options:(NSDictionary *)options
handler:(FIRInstanceIDTokenHandler)handler;
/**
* Return the cached token info, if one exists, for the given authorizedEntity and scope.
*
* @param authorizedEntity The authorized entity for the token.
* @param scope The scope for the token.
*
* @return The cached token info, if available, matching the parameters.
*/
- (FIRInstanceIDTokenInfo *)cachedTokenInfoWithAuthorizedEntity:(NSString *)authorizedEntity
scope:(NSString *)scope;
/**
* Delete the token for the given authorizedEntity and scope. If the token has
* been cached, it will be deleted from the store. It will also make an
* asynchronous request to the InstanceID backend to invalidate the token.
*
* @param authorizedEntity The authorized entity for the token, should not be nil.
* @param scope The scope for the token, should not be nil.
* @param keyPair The keyPair that represents the app identity.
* @param handler The handler to be invoked once the delete request to
* InstanceID backend has returned. If the request was
* successful we invoke the handler with a nil error;
* otherwise we call it with an appropriate error. Also since
* it's a public handler it should always be called
* asynchronously. This should be non-nil.
*/
- (void)deleteTokenWithAuthorizedEntity:(NSString *)authorizedEntity
scope:(NSString *)scope
keyPair:(FIRInstanceIDKeyPair *)keyPair
handler:(FIRInstanceIDDeleteTokenHandler)handler;
/**
* Deletes all cached tokens from the persistent store. This method should only be triggered
* when InstanceID is deleted
*
* @param keyPair The keyPair for the given app.
* @param handler The handler to be invoked once the delete request to InstanceID backend
* has returned. If the request was successful we invoke the handler with
* a nil error; else we pass in an appropriate error. This should be non-nil
* and be called asynchronously.
*/
- (void)deleteAllTokensWithKeyPair:(FIRInstanceIDKeyPair *)keyPair
handler:(FIRInstanceIDDeleteHandler)handler;
/**
* Deletes all cached tokens from the persistent store.
* @param handler The callback handler which is invoked when tokens deletion is complete,
* with an error if there is any.
*
*/
- (void)deleteAllTokensLocallyWithHandler:(void (^)(NSError *error))handler;
/**
* Stop any ongoing token operations.
*/
- (void)stopAllTokenOperations;
#pragma mark - Invalidating Cached Tokens
/**
* Invalidate any cached tokens, if the app version has changed since last launch or if the token
* is cached for more than 7 days.
*
* @return Whether we should fetch default token from server.
*
* @discussion This should safely be called prior to any tokens being retrieved from
* the cache or being fetched from the network.
*/
- (BOOL)checkForTokenRefreshPolicy;
/**
* Upon being provided with different APNs or sandbox, any locally cached tokens
* should be deleted, and the new APNs token should be cached.
*
* @discussion It is possible for this method to be called while token operations are
* in-progress or queued. In this case, the in-flight token operations will have stale
* APNs information. The default token is checked for being out-of-date by Instance ID,
* and re-fetched. Custom tokens are not currently checked.
*
* @param deviceToken The APNS device token, provided by the operating system.
* @param isSandbox YES if the device token is for the sandbox environment, NO otherwise.
*
* @return The array of FIRInstanceIDTokenInfo objects which were invalidated.
*/
- (NSArray<FIRInstanceIDTokenInfo *> *)updateTokensToAPNSDeviceToken:(NSData *)deviceToken
isSandbox:(BOOL)isSandbox;
@end
@@ -0,0 +1,341 @@
/*
* 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 "FIRInstanceIDTokenManager.h"
#import "FIRInstanceIDAuthKeyChain.h"
#import "FIRInstanceIDAuthService.h"
#import "FIRInstanceIDCheckinPreferences.h"
#import "FIRInstanceIDConstants.h"
#import "FIRInstanceIDDefines.h"
#import "FIRInstanceIDLogger.h"
#import "FIRInstanceIDStore.h"
#import "FIRInstanceIDTokenDeleteOperation.h"
#import "FIRInstanceIDTokenFetchOperation.h"
#import "FIRInstanceIDTokenInfo.h"
#import "FIRInstanceIDTokenOperation.h"
#import "NSError+FIRInstanceID.h"
@interface FIRInstanceIDTokenManager () <FIRInstanceIDStoreDelegate>
@property(nonatomic, readwrite, strong) FIRInstanceIDStore *instanceIDStore;
@property(nonatomic, readwrite, strong) FIRInstanceIDAuthService *authService;
@property(nonatomic, readonly, strong) NSOperationQueue *tokenOperations;
@property(nonatomic, readwrite, strong) FIRInstanceIDAPNSInfo *currentAPNSInfo;
@end
@implementation FIRInstanceIDTokenManager
- (instancetype)init {
self = [super init];
if (self) {
_instanceIDStore = [[FIRInstanceIDStore alloc] initWithDelegate:self];
_authService = [[FIRInstanceIDAuthService alloc] initWithStore:_instanceIDStore];
[self configureTokenOperations];
}
return self;
}
- (void)dealloc {
[self stopAllTokenOperations];
}
- (void)configureTokenOperations {
_tokenOperations = [[NSOperationQueue alloc] init];
_tokenOperations.name = @"com.google.iid-token-operations";
// For now, restrict the operations to be serial, because in some cases (like if the
// authorized entity and scope are the same), order matters.
// If we have to deal with several different token requests simultaneously, it would be a good
// idea to add some better intelligence around this (performing unrelated token operations
// simultaneously, etc.).
_tokenOperations.maxConcurrentOperationCount = 1;
if ([_tokenOperations respondsToSelector:@selector(qualityOfService)]) {
_tokenOperations.qualityOfService = NSOperationQualityOfServiceUtility;
}
}
- (void)fetchNewTokenWithAuthorizedEntity:(NSString *)authorizedEntity
scope:(NSString *)scope
keyPair:(FIRInstanceIDKeyPair *)keyPair
options:(NSDictionary *)options
handler:(FIRInstanceIDTokenHandler)handler {
FIRInstanceIDLoggerDebug(kFIRInstanceIDMessageCodeTokenManager000,
@"Fetch new token for authorizedEntity: %@, scope: %@", authorizedEntity,
scope);
FIRInstanceIDTokenFetchOperation *operation =
[self createFetchOperationWithAuthorizedEntity:authorizedEntity
scope:scope
options:options
keyPair:keyPair];
FIRInstanceID_WEAKIFY(self);
FIRInstanceIDTokenOperationCompletion completion =
^(FIRInstanceIDTokenOperationResult result, NSString *_Nullable token,
NSError *_Nullable error) {
FIRInstanceID_STRONGIFY(self);
if (error) {
handler(nil, error);
return;
}
NSString *firebaseAppID = options[kFIRInstanceIDTokenOptionsFirebaseAppIDKey];
FIRInstanceIDTokenInfo *tokenInfo = [[FIRInstanceIDTokenInfo alloc]
initWithAuthorizedEntity:authorizedEntity
scope:scope
token:token
appVersion:FIRInstanceIDCurrentAppVersion()
firebaseAppID:firebaseAppID];
tokenInfo.APNSInfo = [[FIRInstanceIDAPNSInfo alloc] initWithTokenOptionsDictionary:options];
[self.instanceIDStore
saveTokenInfo:tokenInfo
handler:^(NSError *error) {
if (!error) {
// Do not send the token back in case the save was unsuccessful. Since with
// the new asychronous fetch mechanism this can lead to infinite loops, for
// example, we will return a valid token even though we weren't able to store
// it in our cache. The first token will lead to a onTokenRefresh callback
// wherein the user again calls `getToken` but since we weren't able to save
// it we won't hit the cache but hit the server again leading to an infinite
// loop.
FIRInstanceIDLoggerDebug(
kFIRInstanceIDMessageCodeTokenManager001,
@"Token fetch successful, token: %@, authorizedEntity: %@, scope:%@",
token, authorizedEntity, scope);
if (handler) {
handler(token, nil);
}
} else {
if (handler) {
handler(nil, error);
}
}
}];
};
// Add completion handler, and ensure it's called on the main queue
[operation addCompletionHandler:^(FIRInstanceIDTokenOperationResult result,
NSString *_Nullable token, NSError *_Nullable error) {
dispatch_async(dispatch_get_main_queue(), ^{
completion(result, token, error);
});
}];
[self.tokenOperations addOperation:operation];
}
- (FIRInstanceIDTokenInfo *)cachedTokenInfoWithAuthorizedEntity:(NSString *)authorizedEntity
scope:(NSString *)scope {
return [self.instanceIDStore tokenInfoWithAuthorizedEntity:authorizedEntity scope:scope];
}
- (void)deleteTokenWithAuthorizedEntity:(NSString *)authorizedEntity
scope:(NSString *)scope
keyPair:(FIRInstanceIDKeyPair *)keyPair
handler:(FIRInstanceIDDeleteTokenHandler)handler {
if ([self.instanceIDStore tokenInfoWithAuthorizedEntity:authorizedEntity scope:scope]) {
[self.instanceIDStore removeCachedTokenWithAuthorizedEntity:authorizedEntity scope:scope];
}
// Does not matter if we cannot find it in the cache. Still make an effort to unregister
// from the server.
FIRInstanceIDCheckinPreferences *checkinPreferences = self.authService.checkinPreferences;
FIRInstanceIDTokenDeleteOperation *operation =
[self createDeleteOperationWithAuthorizedEntity:authorizedEntity
scope:scope
checkinPreferences:checkinPreferences
keyPair:keyPair
action:FIRInstanceIDTokenActionDeleteToken];
if (handler) {
[operation addCompletionHandler:^(FIRInstanceIDTokenOperationResult result,
NSString *_Nullable token, NSError *_Nullable error) {
dispatch_async(dispatch_get_main_queue(), ^{
handler(error);
});
}];
}
[self.tokenOperations addOperation:operation];
}
- (void)deleteAllTokensWithKeyPair:(FIRInstanceIDKeyPair *)keyPair
handler:(FIRInstanceIDDeleteHandler)handler {
// delete all tokens
FIRInstanceIDCheckinPreferences *checkinPreferences = self.authService.checkinPreferences;
if (!checkinPreferences) {
// The checkin is already deleted. No need to trigger the token delete operation as client no
// longer has the checkin information for server to delete.
dispatch_async(dispatch_get_main_queue(), ^{
handler(nil);
});
return;
}
FIRInstanceIDTokenDeleteOperation *operation =
[self createDeleteOperationWithAuthorizedEntity:kFIRInstanceIDKeychainWildcardIdentifier
scope:kFIRInstanceIDKeychainWildcardIdentifier
checkinPreferences:checkinPreferences
keyPair:keyPair
action:FIRInstanceIDTokenActionDeleteTokenAndIID];
if (handler) {
[operation addCompletionHandler:^(FIRInstanceIDTokenOperationResult result,
NSString *_Nullable token, NSError *_Nullable error) {
dispatch_async(dispatch_get_main_queue(), ^{
handler(error);
});
}];
}
[self.tokenOperations addOperation:operation];
}
- (void)deleteAllTokensLocallyWithHandler:(void (^)(NSError *error))handler {
[self.instanceIDStore removeAllCachedTokensWithHandler:handler];
}
- (void)stopAllTokenOperations {
[self.authService stopCheckinRequest];
[self.tokenOperations cancelAllOperations];
}
#pragma mark - FIRInstanceIDStoreDelegate
- (void)store:(FIRInstanceIDStore *)store
didDeleteFCMScopedTokensForCheckin:(FIRInstanceIDCheckinPreferences *)checkin {
// Make a best effort try to delete the old client related state on the FCM server. This is
// required to delete old pubusb registrations which weren't cleared when the app was deleted.
//
// This is only a one time effort. If this call fails the client would still receive duplicate
// pubsub notifications if he is again subscribed to the same topic.
//
// The client state should be cleared on the server for the provided checkin preferences.
FIRInstanceIDTokenDeleteOperation *operation =
[self createDeleteOperationWithAuthorizedEntity:nil
scope:nil
checkinPreferences:checkin
keyPair:nil
action:FIRInstanceIDTokenActionDeleteToken];
[operation addCompletionHandler:^(FIRInstanceIDTokenOperationResult result,
NSString *_Nullable token, NSError *_Nullable error) {
if (error) {
FIRInstanceIDMessageCode code =
kFIRInstanceIDMessageCodeTokenManagerErrorDeletingFCMTokensOnAppReset;
FIRInstanceIDLoggerDebug(code, @"Failed to delete GCM server registrations on app reset.");
} else {
FIRInstanceIDLoggerDebug(kFIRInstanceIDMessageCodeTokenManagerDeletedFCMTokensOnAppReset,
@"Successfully deleted GCM server registrations on app reset");
}
}];
[self.tokenOperations addOperation:operation];
}
#pragma mark - Unit Testing Stub Helpers
// We really have this method so that we can more easily stub it out for unit testing
- (FIRInstanceIDTokenFetchOperation *)
createFetchOperationWithAuthorizedEntity:(NSString *)authorizedEntity
scope:(NSString *)scope
options:(NSDictionary<NSString *, NSString *> *)options
keyPair:(FIRInstanceIDKeyPair *)keyPair {
FIRInstanceIDCheckinPreferences *checkinPreferences = self.authService.checkinPreferences;
FIRInstanceIDTokenFetchOperation *operation =
[[FIRInstanceIDTokenFetchOperation alloc] initWithAuthorizedEntity:authorizedEntity
scope:scope
options:options
checkinPreferences:checkinPreferences
keyPair:keyPair];
return operation;
}
// We really have this method so that we can more easily stub it out for unit testing
- (FIRInstanceIDTokenDeleteOperation *)
createDeleteOperationWithAuthorizedEntity:(NSString *)authorizedEntity
scope:(NSString *)scope
checkinPreferences:(FIRInstanceIDCheckinPreferences *)checkinPreferences
keyPair:(FIRInstanceIDKeyPair *)keyPair
action:(FIRInstanceIDTokenAction)action {
FIRInstanceIDTokenDeleteOperation *operation =
[[FIRInstanceIDTokenDeleteOperation alloc] initWithAuthorizedEntity:authorizedEntity
scope:scope
checkinPreferences:checkinPreferences
keyPair:keyPair
action:action];
return operation;
}
#pragma mark - Invalidating Cached Tokens
- (BOOL)checkForTokenRefreshPolicy {
// We know at least one cached token exists.
BOOL shouldFetchDefaultToken = NO;
NSArray<FIRInstanceIDTokenInfo *> *tokenInfos = [self.instanceIDStore cachedTokenInfos];
NSMutableArray<FIRInstanceIDTokenInfo *> *tokenInfosToDelete =
[NSMutableArray arrayWithCapacity:tokenInfos.count];
for (FIRInstanceIDTokenInfo *tokenInfo in tokenInfos) {
BOOL isTokenFresh = [tokenInfo isFresh];
if (isTokenFresh) {
// Token is fresh, do nothing.
continue;
}
if ([tokenInfo.scope isEqualToString:kFIRInstanceIDDefaultTokenScope]) {
// Default token is expired, do not mark for deletion. Fetch directly from server to
// replace the current one.
shouldFetchDefaultToken = YES;
} else {
// Non-default token is expired, mark for deletion.
[tokenInfosToDelete addObject:tokenInfo];
}
FIRInstanceIDLoggerDebug(
kFIRInstanceIDMessageCodeTokenManagerInvalidateStaleToken,
@"Invalidating cached token for %@ (%@) due to token is no longer fresh.",
tokenInfo.authorizedEntity, tokenInfo.scope);
}
for (FIRInstanceIDTokenInfo *tokenInfoToDelete in tokenInfosToDelete) {
[self.instanceIDStore removeCachedTokenWithAuthorizedEntity:tokenInfoToDelete.authorizedEntity
scope:tokenInfoToDelete.scope];
}
return shouldFetchDefaultToken;
}
- (NSArray<FIRInstanceIDTokenInfo *> *)updateTokensToAPNSDeviceToken:(NSData *)deviceToken
isSandbox:(BOOL)isSandbox {
// Each cached IID token that is missing an APNSInfo, or has an APNSInfo associated should be
// checked and invalidated if needed.
FIRInstanceIDAPNSInfo *APNSInfo = [[FIRInstanceIDAPNSInfo alloc] initWithDeviceToken:deviceToken
isSandbox:isSandbox];
if ([self.currentAPNSInfo isEqualToAPNSInfo:APNSInfo]) {
return @[];
}
self.currentAPNSInfo = APNSInfo;
NSArray<FIRInstanceIDTokenInfo *> *tokenInfos = [self.instanceIDStore cachedTokenInfos];
NSMutableArray<FIRInstanceIDTokenInfo *> *tokenInfosToDelete =
[NSMutableArray arrayWithCapacity:tokenInfos.count];
for (FIRInstanceIDTokenInfo *cachedTokenInfo in tokenInfos) {
// Check if the cached APNSInfo is nil, or if it is an old APNSInfo.
if (!cachedTokenInfo.APNSInfo ||
![cachedTokenInfo.APNSInfo isEqualToAPNSInfo:self.currentAPNSInfo]) {
// Mark for invalidation.
[tokenInfosToDelete addObject:cachedTokenInfo];
}
}
for (FIRInstanceIDTokenInfo *tokenInfoToDelete in tokenInfosToDelete) {
FIRInstanceIDLoggerDebug(kFIRInstanceIDMessageCodeTokenManagerAPNSChangedTokenInvalidated,
@"Invalidating cached token for %@ (%@) due to APNs token change.",
tokenInfoToDelete.authorizedEntity, tokenInfoToDelete.scope);
[self.instanceIDStore removeCachedTokenWithAuthorizedEntity:tokenInfoToDelete.authorizedEntity
scope:tokenInfoToDelete.scope];
}
return tokenInfosToDelete;
}
@end
@@ -0,0 +1,67 @@
/*
* 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 "FIRInstanceIDTokenOperation.h"
#import "FIRInstanceIDUtilities.h"
@class FIRInstanceIDKeyPair;
@class FIRInstanceIDURLQueryItem;
NS_ASSUME_NONNULL_BEGIN
@interface FIRInstanceIDTokenOperation (Private)
@property(atomic, strong) NSURLSessionDataTask *dataTask;
@property(readonly, strong)
NSMutableArray<FIRInstanceIDTokenOperationCompletion> *completionHandlers;
// For testing only
@property(nonatomic, readwrite, copy) FIRInstanceIDURLRequestTestBlock testBlock;
+ (NSURLSession *)sharedURLSession;
#pragma mark - Initialization
- (instancetype)initWithAction:(FIRInstanceIDTokenAction)action
forAuthorizedEntity:(nullable NSString *)authorizedEntity
scope:(NSString *)scope
options:(nullable NSDictionary<NSString *, NSString *> *)options
checkinPreferences:(FIRInstanceIDCheckinPreferences *)checkinPreferences
keyPair:(FIRInstanceIDKeyPair *)keyPair;
#pragma mark - Request Construction
+ (NSMutableURLRequest *)requestWithAuthHeader:(NSString *)authHeaderString;
+ (NSMutableArray<FIRInstanceIDURLQueryItem *> *)standardQueryItemsWithDeviceID:(NSString *)deviceID
scope:(NSString *)scope;
- (NSArray<FIRInstanceIDURLQueryItem *> *)queryItemsWithKeyPair:(FIRInstanceIDKeyPair *)keyPair;
#pragma mark - HTTP Headers
/**
* Given a valid checkin preferences object, it will return a string that can be used
* in the "Authorization" HTTP header to authenticate this request.
*
* @param checkin The valid checkin preferences object, with a deviceID and secretToken.
*/
+ (NSString *)HTTPAuthHeaderFromCheckin:(FIRInstanceIDCheckinPreferences *)checkin;
#pragma mark - Result
- (void)finishWithResult:(FIRInstanceIDTokenOperationResult)result
token:(nullable NSString *)token
error:(nullable NSError *)error;
@end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,73 @@
/*
* 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>
@class FIRInstanceIDKeyPair;
@class FIRInstanceIDCheckinPreferences;
NS_ASSUME_NONNULL_BEGIN
/**
* Represents the action taken on an FCM token.
*/
typedef NS_ENUM(NSInteger, FIRInstanceIDTokenAction) {
FIRInstanceIDTokenActionFetch,
FIRInstanceIDTokenActionDeleteToken,
FIRInstanceIDTokenActionDeleteTokenAndIID,
};
/**
* Represents the possible results of a token operation.
*/
typedef NS_ENUM(NSInteger, FIRInstanceIDTokenOperationResult) {
FIRInstanceIDTokenOperationSucceeded,
FIRInstanceIDTokenOperationError,
FIRInstanceIDTokenOperationCancelled,
};
/**
* Callback to invoke once the HTTP call to FIRMessaging backend for updating
* subscription finishes.
*
* @param result The result of the operation.
* @param token If the action for fetching a token and the request was successful, this will hold
* the value of the token. Otherwise nil.
* @param error The error which occurred while performing the token operation. This will be nil
* in case the operation was successful, or if the operation was cancelled.
*/
typedef void (^FIRInstanceIDTokenOperationCompletion)(FIRInstanceIDTokenOperationResult result,
NSString *_Nullable token,
NSError *_Nullable error);
@interface FIRInstanceIDTokenOperation : NSOperation
@property(nonatomic, readonly) FIRInstanceIDTokenAction action;
@property(nonatomic, readonly, nullable) NSString *authorizedEntity;
@property(nonatomic, readonly, nullable) NSString *scope;
@property(nonatomic, readonly, nullable) NSDictionary<NSString *, NSString *> *options;
@property(nonatomic, readonly, strong) FIRInstanceIDCheckinPreferences *checkinPreferences;
@property(nonatomic, readonly, strong) FIRInstanceIDKeyPair *keyPair;
@property(nonatomic, readonly) FIRInstanceIDTokenOperationResult result;
- (instancetype)init NS_UNAVAILABLE;
- (void)addCompletionHandler:(FIRInstanceIDTokenOperationCompletion)handler;
@end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,241 @@
/*
* 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 "FIRInstanceIDTokenOperation.h"
#import "FIRInstanceIDCheckinPreferences.h"
#import "FIRInstanceIDKeyPair.h"
#import "FIRInstanceIDKeyPairUtilities.h"
#import "FIRInstanceIDLogger.h"
#import "FIRInstanceIDURLQueryItem.h"
#import "FIRInstanceIDUtilities.h"
#import "NSError+FIRInstanceID.h"
static const NSInteger kFIRInstanceIDPlatformVersionIOS = 2;
static NSString *const kFIRInstanceIDParamInstanceID = @"appid";
// Scope parameter that defines the service using the token
static NSString *const kFIRInstanceIDParamScope = @"X-scope";
// Defines the SDK version
static NSString *const kFIRInstanceIDParamFCMLibVersion = @"X-cliv";
@interface FIRInstanceIDTokenOperation () {
BOOL _isFinished;
BOOL _isExecuting;
}
@property(nonatomic, readwrite, strong) FIRInstanceIDCheckinPreferences *checkinPreferences;
@property(nonatomic, readwrite, strong) FIRInstanceIDKeyPair *keyPair;
@property(atomic, strong) NSURLSessionDataTask *dataTask;
@property(readonly, strong)
NSMutableArray<FIRInstanceIDTokenOperationCompletion> *completionHandlers;
// For testing only
@property(nonatomic, readwrite, copy) FIRInstanceIDURLRequestTestBlock testBlock;
@end
@implementation FIRInstanceIDTokenOperation
+ (NSURLSession *)sharedURLSession {
static NSURLSession *tokenOperationSharedSession;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];
config.timeoutIntervalForResource = 60.0f; // 1 minute
tokenOperationSharedSession = [NSURLSession sessionWithConfiguration:config];
tokenOperationSharedSession.sessionDescription = @"com.google.iid.tokens.session";
});
return tokenOperationSharedSession;
}
- (instancetype)initWithAction:(FIRInstanceIDTokenAction)action
forAuthorizedEntity:(NSString *)authorizedEntity
scope:(NSString *)scope
options:(NSDictionary<NSString *, NSString *> *)options
checkinPreferences:(FIRInstanceIDCheckinPreferences *)checkinPreferences
keyPair:(FIRInstanceIDKeyPair *)keyPair {
self = [super init];
if (self) {
_action = action;
_authorizedEntity = [authorizedEntity copy];
_scope = [scope copy];
_options = [options copy];
_checkinPreferences = checkinPreferences;
_keyPair = keyPair;
_completionHandlers = [NSMutableArray array];
_isExecuting = NO;
_isFinished = NO;
}
return self;
}
- (void)dealloc {
_testBlock = nil;
_authorizedEntity = nil;
_scope = nil;
_options = nil;
_checkinPreferences = nil;
_keyPair = nil;
[_completionHandlers removeAllObjects];
_completionHandlers = nil;
}
- (void)addCompletionHandler:(FIRInstanceIDTokenOperationCompletion)handler {
[self.completionHandlers addObject:handler];
}
- (BOOL)isAsynchronous {
return YES;
}
- (BOOL)isExecuting {
return _isExecuting;
}
- (void)setExecuting:(BOOL)executing {
[self willChangeValueForKey:@"isExecuting"];
_isExecuting = executing;
[self didChangeValueForKey:@"isExecuting"];
}
- (BOOL)isFinished {
return _isFinished;
}
- (void)setFinished:(BOOL)finished {
[self willChangeValueForKey:@"isFinished"];
_isFinished = finished;
[self didChangeValueForKey:@"isFinished"];
}
- (void)start {
if (self.isCancelled) {
[self finishWithResult:FIRInstanceIDTokenOperationCancelled token:nil error:nil];
return;
}
// Quickly validate whether or not the operation has all it needs to begin
BOOL checkinfoAvailable = [self.checkinPreferences hasCheckinInfo];
if (!checkinfoAvailable) {
FIRInstanceIDErrorCode errorCode = kFIRInstanceIDErrorCodeRegistrarFailedToCheckIn;
[self finishWithResult:FIRInstanceIDTokenOperationError
token:nil
error:[NSError errorWithFIRInstanceIDErrorCode:errorCode]];
return;
}
[self setExecuting:YES];
[self performTokenOperation];
}
- (void)finishWithResult:(FIRInstanceIDTokenOperationResult)result
token:(nullable NSString *)token
error:(nullable NSError *)error {
// Add a check to prevent this finish from being called more than once.
if (self.isFinished) {
return;
}
self.dataTask = nil;
_result = result;
// TODO(chliangGoogle): Call these in the main thread?
for (FIRInstanceIDTokenOperationCompletion completionHandler in self.completionHandlers) {
completionHandler(result, token, error);
}
[self setExecuting:NO];
[self setFinished:YES];
}
- (void)cancel {
[super cancel];
[self.dataTask cancel];
[self finishWithResult:FIRInstanceIDTokenOperationCancelled token:nil error:nil];
}
- (void)performTokenOperation {
}
#pragma mark - Request Construction
+ (NSMutableURLRequest *)requestWithAuthHeader:(NSString *)authHeaderString {
NSURL *url = [NSURL URLWithString:FIRInstanceIDRegisterServer()];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
// Add HTTP headers
[request setValue:authHeaderString forHTTPHeaderField:@"Authorization"];
[request setValue:FIRInstanceIDAppIdentifier() forHTTPHeaderField:@"app"];
request.HTTPMethod = @"POST";
return request;
}
+ (NSMutableArray<FIRInstanceIDURLQueryItem *> *)standardQueryItemsWithDeviceID:(NSString *)deviceID
scope:(NSString *)scope {
NSMutableArray<FIRInstanceIDURLQueryItem *> *queryItems = [NSMutableArray arrayWithCapacity:8];
// E.g. X-osv=10.2.1
NSString *systemVersion = FIRInstanceIDOperatingSystemVersion();
[queryItems addObject:[FIRInstanceIDURLQueryItem queryItemWithName:@"X-osv" value:systemVersion]];
// E.g. device=
if (deviceID) {
[queryItems addObject:[FIRInstanceIDURLQueryItem queryItemWithName:@"device" value:deviceID]];
}
// E.g. X-scope=fcm
if (scope) {
[queryItems addObject:[FIRInstanceIDURLQueryItem queryItemWithName:kFIRInstanceIDParamScope
value:scope]];
}
// E.g. plat=2
NSString *platform = [NSString stringWithFormat:@"%ld", (long)kFIRInstanceIDPlatformVersionIOS];
[queryItems addObject:[FIRInstanceIDURLQueryItem queryItemWithName:@"plat" value:platform]];
// E.g. app=com.myapp.foo
NSString *appIdentifier = FIRInstanceIDAppIdentifier();
[queryItems addObject:[FIRInstanceIDURLQueryItem queryItemWithName:@"app" value:appIdentifier]];
// E.g. app_ver=1.5
NSString *appVersion = FIRInstanceIDCurrentAppVersion();
[queryItems addObject:[FIRInstanceIDURLQueryItem queryItemWithName:@"app_ver" value:appVersion]];
// E.g. X-cliv=fiid-1.2.3
NSString *fcmLibraryVersion =
[NSString stringWithFormat:@"fiid-%@", FIRInstanceIDCurrentGCMVersion()];
if (fcmLibraryVersion.length) {
FIRInstanceIDURLQueryItem *gcmLibVersion =
[FIRInstanceIDURLQueryItem queryItemWithName:kFIRInstanceIDParamFCMLibVersion
value:fcmLibraryVersion];
[queryItems addObject:gcmLibVersion];
}
return queryItems;
}
- (NSArray<FIRInstanceIDURLQueryItem *> *)queryItemsWithKeyPair:(FIRInstanceIDKeyPair *)keyPair {
NSMutableArray<FIRInstanceIDURLQueryItem *> *items = [NSMutableArray arrayWithCapacity:3];
// appid=
NSString *instanceID = FIRInstanceIDAppIdentity(keyPair);
[items addObject:[FIRInstanceIDURLQueryItem queryItemWithName:kFIRInstanceIDParamInstanceID
value:instanceID]];
return items;
}
#pragma mark - HTTP Header
+ (NSString *)HTTPAuthHeaderFromCheckin:(FIRInstanceIDCheckinPreferences *)checkin {
NSString *deviceID = checkin.deviceID;
NSString *secret = checkin.secretToken;
return [NSString stringWithFormat:@"AidLogin %@:%@", deviceID, secret];
}
@end
@@ -0,0 +1,106 @@
/*
* 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>
@class FIRInstanceIDAPNSInfo;
@class FIRInstanceIDAuthKeychain;
@class FIRInstanceIDTokenInfo;
/**
* This class is responsible for retrieving and saving `FIRInstanceIDTokenInfo` objects from the
* keychain. The keychain keys that are used are:
* Account: <Main App Bundle ID> (e.g. com.mycompany.myapp)
* Service: <Sender ID>:<Scope> (e.g. 1234567890:*)
*/
@interface FIRInstanceIDTokenStore : NSObject
NS_ASSUME_NONNULL_BEGIN
/**
* Create a default InstanceID token store. Uses a valid Keychain object as it's
* persistent backing store.
*
* @return A valid token store object.
*/
+ (instancetype)defaultStore;
- (instancetype)init __attribute__((unavailable("Use -initWithKeychain: instead.")));
/**
* Initialize a token store object with a Keychain object. Used for testing.
*
* @param keychain The Keychain object to use as the backing store for tokens.
*
* @return A valid token store object with the given Keychain as backing store.
*/
- (instancetype)initWithKeychain:(FIRInstanceIDAuthKeychain *)keychain;
#pragma mark - Get
/**
* Get the cached token from the Keychain.
*
* @param authorizedEntity The authorized entity for the token.
* @param scope The scope for the token.
*
* @return The cached token info if any for the given authorizedEntity and scope else
* nil.
*/
- (nullable FIRInstanceIDTokenInfo *)tokenInfoWithAuthorizedEntity:(NSString *)authorizedEntity
scope:(NSString *)scope;
/**
* Return all cached token infos from the Keychain.
*
* @return The cached token infos, if any, that are stored in the Keychain.
*/
- (NSArray<FIRInstanceIDTokenInfo *> *)cachedTokenInfos;
#pragma mark - Save
/**
* Save the instanceID token info to the persistent store.
*
* @param tokenInfo The token info to store.
* @param handler The callback handler which is invoked when token saving is complete,
* with an error if there is any.
*/
- (void)saveTokenInfo:(FIRInstanceIDTokenInfo *)tokenInfo
handler:(nullable void (^)(NSError *))handler;
#pragma mark - Delete
/**
* Remove the cached token from Keychain.
*
* @param authorizedEntity The authorized entity for the token.
* @param scope The scope for the token.
*
*/
- (void)removeTokenWithAuthorizedEntity:(NSString *)authorizedEntity scope:(NSString *)scope;
/**
* Remove all the cached tokens from the Keychain.
* @param handler The callback handler which is invoked when tokens deletion is complete,
* with an error if there is any.
*
*/
- (void)removeAllTokensWithHandler:(nullable void (^)(NSError *))handler;
NS_ASSUME_NONNULL_END
@end
@@ -0,0 +1,147 @@
/*
* 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 "FIRInstanceIDTokenStore.h"
#import "FIRInstanceIDAuthKeyChain.h"
#import "FIRInstanceIDConstants.h"
#import "FIRInstanceIDLogger.h"
#import "FIRInstanceIDTokenInfo.h"
#import "FIRInstanceIDUtilities.h"
static NSString *const kFIRInstanceIDTokenKeychainId = @"com.google.iid-tokens";
@interface FIRInstanceIDTokenStore ()
@property(nonatomic, readwrite, strong) FIRInstanceIDAuthKeychain *keychain;
@end
@implementation FIRInstanceIDTokenStore
+ (instancetype)defaultStore {
FIRInstanceIDAuthKeychain *tokenKeychain =
[[FIRInstanceIDAuthKeychain alloc] initWithIdentifier:kFIRInstanceIDTokenKeychainId];
return [[FIRInstanceIDTokenStore alloc] initWithKeychain:tokenKeychain];
}
- (instancetype)initWithKeychain:(FIRInstanceIDAuthKeychain *)keychain {
self = [super init];
if (self) {
_keychain = keychain;
}
return self;
}
#pragma mark - Get
+ (NSString *)serviceKeyForAuthorizedEntity:(NSString *)authorizedEntity scope:(NSString *)scope {
return [NSString stringWithFormat:@"%@:%@", authorizedEntity, scope];
}
- (nullable FIRInstanceIDTokenInfo *)tokenInfoWithAuthorizedEntity:(NSString *)authorizedEntity
scope:(NSString *)scope {
NSString *account = FIRInstanceIDAppIdentifier();
NSString *service = [[self class] serviceKeyForAuthorizedEntity:authorizedEntity scope:scope];
NSData *item = [self.keychain dataForService:service account:account];
if (!item) {
return nil;
}
// Token infos created from legacy storage don't have appVersion, firebaseAppID, or APNSInfo.
FIRInstanceIDTokenInfo *tokenInfo = [[self class] tokenInfoFromKeychainItem:item];
return tokenInfo;
}
- (NSArray<FIRInstanceIDTokenInfo *> *)cachedTokenInfos {
NSString *account = FIRInstanceIDAppIdentifier();
NSArray<NSData *> *items =
[self.keychain itemsMatchingService:kFIRInstanceIDKeychainWildcardIdentifier account:account];
NSMutableArray<FIRInstanceIDTokenInfo *> *tokenInfos =
[NSMutableArray arrayWithCapacity:items.count];
for (NSData *item in items) {
FIRInstanceIDTokenInfo *tokenInfo = [[self class] tokenInfoFromKeychainItem:item];
if (tokenInfo) {
[tokenInfos addObject:tokenInfo];
}
}
return tokenInfos;
}
+ (nullable FIRInstanceIDTokenInfo *)tokenInfoFromKeychainItem:(NSData *)item {
// Check if it is saved as an archived FIRInstanceIDTokenInfo, otherwise return nil.
FIRInstanceIDTokenInfo *tokenInfo = nil;
// NOTE: Passing in nil to unarchiveObjectWithData will result in an iOS error logged
// in the console on iOS 10 and below. Avoid by checking item.data's existence.
if (item) {
// TODO(chliangGoogle: Use the new API and secureCoding protocol.
@try {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
tokenInfo = [NSKeyedUnarchiver unarchiveObjectWithData:item];
#pragma clang diagnostic pop
} @catch (NSException *exception) {
FIRInstanceIDLoggerDebug(kFIRInstanceIDMessageCodeTokenStoreExceptionUnarchivingTokenInfo,
@"Unable to parse token info from Keychain item; item was in an "
@"invalid format");
tokenInfo = nil;
} @finally {
}
}
return tokenInfo;
}
#pragma mark - Save
// Token Infos will be saved under these Keychain keys:
// Account: <Main App Bundle ID> (e.g. com.mycompany.myapp)
// Service: <Sender ID>:<Scope> (e.g. 1234567890:*)
- (void)saveTokenInfo:(FIRInstanceIDTokenInfo *)tokenInfo
handler:(void (^)(NSError *))handler { // Keep the cachetime up-to-date.
tokenInfo.cacheTime = [NSDate date];
// Always write to the Keychain, so that the cacheTime is up-to-date.
NSData *tokenInfoData;
// TODO(chliangGoogle: Use the new API and secureCoding protocol.
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
tokenInfoData = [NSKeyedArchiver archivedDataWithRootObject:tokenInfo];
#pragma clang diagnostic pop
NSString *account = FIRInstanceIDAppIdentifier();
NSString *service = [[self class] serviceKeyForAuthorizedEntity:tokenInfo.authorizedEntity
scope:tokenInfo.scope];
[self.keychain setData:tokenInfoData
forService:service
accessibility:NULL
account:account
handler:handler];
}
#pragma mark - Delete
- (void)removeTokenWithAuthorizedEntity:(nonnull NSString *)authorizedEntity
scope:(nonnull NSString *)scope {
NSString *account = FIRInstanceIDAppIdentifier();
NSString *service = [[self class] serviceKeyForAuthorizedEntity:authorizedEntity scope:scope];
[self.keychain removeItemsMatchingService:service account:account handler:nil];
}
- (void)removeAllTokensWithHandler:(void (^)(NSError *error))handler {
NSString *account = FIRInstanceIDAppIdentifier();
[self.keychain removeItemsMatchingService:kFIRInstanceIDKeychainWildcardIdentifier
account:account
handler:handler];
}
@end
@@ -0,0 +1,39 @@
/*
* 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
// Stand-in for NSURLQueryItem, which is only available on iOS 8.0 and up.
@interface FIRInstanceIDURLQueryItem : NSObject
@property(nonatomic, readonly) NSString *name;
@property(nonatomic, readonly) NSString *value;
+ (instancetype)queryItemWithName:(NSString *)name value:(NSString *)value;
- (instancetype)initWithName:(NSString *)name value:(NSString *)value;
@end
/**
* Given an array of query items, construct a URL query. On iOS 8.0 and above, this will use
* NSURLQueryItems internally to perform the string creation, and will be done manually in iOS
* 7 and below.
*/
NSString *FIRInstanceIDQueryFromQueryItems(NSArray<FIRInstanceIDURLQueryItem *> *queryItems);
NS_ASSUME_NONNULL_END
@@ -0,0 +1,55 @@
/*
* 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 "FIRInstanceIDURLQueryItem.h"
@implementation FIRInstanceIDURLQueryItem
+ (instancetype)queryItemWithName:(NSString *)name value:(NSString *)value {
return [[[self class] alloc] initWithName:name value:value];
}
- (instancetype)initWithName:(NSString *)name value:(NSString *)value {
self = [super init];
if (self) {
_name = [name copy];
_value = [value copy];
}
return self;
}
@end
NSString *FIRInstanceIDQueryFromQueryItems(NSArray<FIRInstanceIDURLQueryItem *> *queryItems) {
if ([NSURLQueryItem class]) {
// We are iOS 8.0 and above. Convert to NSURLQueryItems and get query that way
// to take advantage of any automatic encoding
NSMutableArray<NSURLQueryItem *> *urlItems =
[NSMutableArray arrayWithCapacity:queryItems.count];
for (FIRInstanceIDURLQueryItem *queryItem in queryItems) {
[urlItems addObject:[NSURLQueryItem queryItemWithName:queryItem.name value:queryItem.value]];
}
NSURLComponents *components = [[NSURLComponents alloc] init];
components.queryItems = urlItems;
return components.query;
} else {
// We are on iOS 7.0. Manually create the query string
NSMutableArray<NSString *> *pairs = [NSMutableArray arrayWithCapacity:queryItems.count];
for (FIRInstanceIDURLQueryItem *queryItem in queryItems) {
[pairs addObject:[NSString stringWithFormat:@"%@=%@", queryItem.name, queryItem.value]];
}
return [pairs componentsJoinedByString:@"&"];
}
}
@@ -0,0 +1,85 @@
/*
* 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>
/// FIRMessaging Class that responds to the FIRMessaging SDK version selector.
/// Verify at runtime if the class exists and implements the required method.
FOUNDATION_EXPORT NSString *const kFIRInstanceIDFCMSDKClassString;
/// locale key stored in GULUserDefaults
FOUNDATION_EXPORT NSString *const kFIRInstanceIDUserDefaultsKeyLocale;
#pragma mark - Test Blocks
/**
* Response block for mock registration requests made during tests.
*
* @param data The data as returned by the mock request.
* @param response The response as returned by the mock request.
* @param error The error if any as returned by the mock request.
*/
typedef void (^FIRInstanceIDURLRequestTestResponseBlock)(NSData *data,
NSURLResponse *response,
NSError *error);
/**
* Test block to mock registration requests response.
*
* @param request The request to mock response for.
* @param response The response block for the mocked request.
*/
typedef void (^FIRInstanceIDURLRequestTestBlock)(NSURLRequest *request,
FIRInstanceIDURLRequestTestResponseBlock response);
#pragma mark - URL Helpers
FOUNDATION_EXPORT NSString *FIRInstanceIDRegisterServer(void);
#pragma mark - Time
FOUNDATION_EXPORT int64_t FIRInstanceIDCurrentTimestampInSeconds(void);
FOUNDATION_EXPORT int64_t FIRInstanceIDCurrentTimestampInMilliseconds(void);
#pragma mark - App Info
FOUNDATION_EXPORT NSString *FIRInstanceIDCurrentAppVersion(void);
FOUNDATION_EXPORT NSString *FIRInstanceIDAppIdentifier(void);
FOUNDATION_EXPORT NSString *FIRInstanceIDFirebaseAppID(void);
#pragma mark - Device Info
FOUNDATION_EXPORT NSString *FIRInstanceIDDeviceModel(void);
FOUNDATION_EXPORT NSString *FIRInstanceIDOperatingSystemVersion(void);
FOUNDATION_EXPORT BOOL FIRInstanceIDHasLocaleChanged(void);
#pragma mark - Helpers
FOUNDATION_EXPORT BOOL FIRInstanceIDIsValidGCMScope(NSString *scope);
FOUNDATION_EXPORT NSString *FIRInstanceIDStringForAPNSDeviceToken(NSData *deviceToken);
FOUNDATION_EXPORT NSString *FIRInstanceIDAPNSTupleStringForTokenAndServerType(NSData *deviceToken,
BOOL isSandbox);
#pragma mark - GCM Helpers
/// Returns the current GCM version if GCM library is found else returns nil.
FOUNDATION_EXPORT NSString *FIRInstanceIDCurrentGCMVersion(void);
/// Returns the current locale. If GCM is present it queries GCM for a
/// Context Manager specific locale. Otherwise, it returns the system's first
/// preferred language (which may be set independently from locale). If the
/// system returns no preferred languages, this method returns the most common
/// language for the user's given locale. Guaranteed to return a nonnull value.
FOUNDATION_EXPORT NSString *FIRInstanceIDCurrentLocale(void);
@@ -0,0 +1,195 @@
/*
* 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 "FIRInstanceIDUtilities.h"
#if TARGET_OS_IOS || TARGET_OS_TV
#import <UIKit/UIKit.h>
#endif
#import <sys/utsname.h>
#import <FirebaseCore/FIROptions.h>
#import <GoogleUtilities/GULUserDefaults.h>
#import "FIRInstanceID.h"
#import "FIRInstanceIDConstants.h"
#import "FIRInstanceIDLogger.h"
// Convert the macro to a string
#define STR_EXPAND(x) #x
#define STR(x) STR_EXPAND(x)
static NSString *const kFIRInstanceIDAPNSSandboxPrefix = @"s_";
static NSString *const kFIRInstanceIDAPNSProdPrefix = @"p_";
/// FIRMessaging Class that responds to the FIRMessaging SDK version selector.
/// Verify at runtime if the class exists and implements the required method.
NSString *const kFIRInstanceIDFCMSDKClassString = @"FIRMessaging";
/// FIRMessaging selector that returns the current FIRMessaging library version.
static NSString *const kFIRInstanceIDFCMSDKVersionSelectorString = @"FIRMessagingSDKVersion";
/// FIRMessaging selector that returns the current device locale.
static NSString *const kFIRInstanceIDFCMSDKLocaleSelectorString = @"FIRMessagingSDKCurrentLocale";
NSString *const kFIRInstanceIDUserDefaultsKeyLocale =
@"com.firebase.instanceid.user_defaults.locale"; // locale key stored in GULUserDefaults
/// Static values which will be populated once retrieved using
/// |FIRInstanceIDRetrieveEnvironmentInfoFromFirebaseCore|.
static NSString *operatingSystemVersion;
static NSString *hardwareDeviceModel;
#pragma mark - URL Helpers
NSString *FIRInstanceIDRegisterServer() {
return @"https://fcmtoken.googleapis.com/register";
}
#pragma mark - Time
int64_t FIRInstanceIDCurrentTimestampInSeconds() {
return (int64_t)[[NSDate date] timeIntervalSince1970];
}
int64_t FIRInstanceIDCurrentTimestampInMilliseconds() {
return (int64_t)(FIRInstanceIDCurrentTimestampInSeconds() * 1000.0);
}
#pragma mark - App Info
NSString *FIRInstanceIDCurrentAppVersion() {
NSString *version = [[NSBundle mainBundle] infoDictionary][@"CFBundleShortVersionString"];
if (![version length]) {
return @"";
}
return version;
}
NSString *FIRInstanceIDAppIdentifier() {
NSString *bundleIdentifier = [[NSBundle mainBundle] bundleIdentifier];
if (!bundleIdentifier.length) {
FIRInstanceIDLoggerError(kFIRInstanceIDMessageCodeUtilitiesMissingBundleIdentifier,
@"The mainBundle's bundleIdentifier returned '%@'. Bundle identifier "
@"expected to be non-empty.",
bundleIdentifier);
return @"";
}
return bundleIdentifier;
}
NSString *FIRInstanceIDFirebaseAppID() {
return [FIROptions defaultOptions].googleAppID;
}
#pragma mark - Device Info
// Get the device model from Firebase Core's App Environment Util
NSString *FIRInstanceIDDeviceModel() {
static dispatch_once_t once;
dispatch_once(&once, ^{
struct utsname systemInfo;
if (uname(&systemInfo) == 0) {
hardwareDeviceModel = [NSString stringWithUTF8String:systemInfo.machine];
}
});
return hardwareDeviceModel;
}
// Get the system version from Firebase Core's App Environment Util
NSString *FIRInstanceIDOperatingSystemVersion() {
#if TARGET_OS_IOS || TARGET_OS_TV
return [UIDevice currentDevice].systemVersion;
#elif TARGET_OS_OSX
return [NSProcessInfo processInfo].operatingSystemVersionString;
#endif
}
BOOL FIRInstanceIDHasLocaleChanged() {
NSString *lastLocale =
[[GULUserDefaults standardUserDefaults] stringForKey:kFIRInstanceIDUserDefaultsKeyLocale];
NSString *currentLocale = FIRInstanceIDCurrentLocale();
if (lastLocale) {
if ([currentLocale isEqualToString:lastLocale]) {
return NO;
}
}
return YES;
}
#pragma mark - Helpers
BOOL FIRInstanceIDIsValidGCMScope(NSString *scope) {
return [scope compare:kFIRInstanceIDScopeFirebaseMessaging
options:NSCaseInsensitiveSearch] == NSOrderedSame;
}
NSString *FIRInstanceIDStringForAPNSDeviceToken(NSData *deviceToken) {
NSMutableString *APNSToken = [NSMutableString string];
unsigned char *bytes = (unsigned char *)[deviceToken bytes];
for (int i = 0; i < (int)deviceToken.length; i++) {
[APNSToken appendFormat:@"%02x", bytes[i]];
}
return APNSToken;
}
NSString *FIRInstanceIDAPNSTupleStringForTokenAndServerType(NSData *deviceToken, BOOL isSandbox) {
if (deviceToken == nil) {
// A nil deviceToken leads to an invalid tuple string, so return nil.
return nil;
}
NSString *prefix = isSandbox ? kFIRInstanceIDAPNSSandboxPrefix : kFIRInstanceIDAPNSProdPrefix;
NSString *APNSString = FIRInstanceIDStringForAPNSDeviceToken(deviceToken);
NSString *APNSTupleString = [NSString stringWithFormat:@"%@%@", prefix, APNSString];
return APNSTupleString;
}
#pragma mark - GCM Helpers
NSString *FIRInstanceIDCurrentGCMVersion() {
Class versionClass = NSClassFromString(kFIRInstanceIDFCMSDKClassString);
SEL versionSelector = NSSelectorFromString(kFIRInstanceIDFCMSDKVersionSelectorString);
if ([versionClass respondsToSelector:versionSelector]) {
IMP getVersionIMP = [versionClass methodForSelector:versionSelector];
NSString *(*getVersion)(id, SEL) = (void *)getVersionIMP;
return getVersion(versionClass, versionSelector);
}
return nil;
}
NSString *FIRInstanceIDCurrentLocale() {
Class localeClass = NSClassFromString(kFIRInstanceIDFCMSDKClassString);
SEL localeSelector = NSSelectorFromString(kFIRInstanceIDFCMSDKLocaleSelectorString);
if ([localeClass respondsToSelector:localeSelector]) {
IMP getLocaleIMP = [localeClass methodForSelector:localeSelector];
NSString *(*getLocale)(id, SEL) = (void *)getLocaleIMP;
NSString *fcmLocale = getLocale(localeClass, localeSelector);
if (fcmLocale != nil) {
return fcmLocale;
}
}
NSString *systemLanguage = [[NSLocale preferredLanguages] firstObject];
if (systemLanguage != nil) {
return systemLanguage;
}
if (@available(macOS 10.12, iOS 10.0, *)) {
return [NSLocale currentLocale].languageCode;
} else {
return nil;
}
}
@@ -0,0 +1,35 @@
/*
* 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>
/**
* Parsing utility for InstanceID Library versions. InstanceID lib follows semantic versioning.
* This provides utilities to parse the library versions to enable features and do
* updates based on appropriate library versions.
*
* Some example semantic versions are 1.0.1, 2.1.0, 2.1.1, 2.2.0-alpha1, 2.2.1-beta1
*/
FOUNDATION_EXPORT NSString *FIRInstanceIDCurrentLibraryVersion(void);
/// Returns the current Major version of GCM library.
FOUNDATION_EXPORT int FIRInstanceIDCurrentLibraryVersionMajor(void);
/// Returns the current Minor version of GCM library.
FOUNDATION_EXPORT int FIRInstanceIDCurrentLibraryVersionMinor(void);
/// Returns the current Patch version of GCM library.
FOUNDATION_EXPORT int FIRInstanceIDCurrentLibraryVersionPatch(void);
/// Returns YES if current library version is `beta` else NO.
FOUNDATION_EXPORT BOOL FIRInstanceIDCurrentLibraryVersionIsBeta(void);
@@ -0,0 +1,85 @@
/*
* 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 "FIRInstanceIDVersionUtilities.h"
// Convert the macro to a string
#define STR(x) STR_EXPAND(x)
#define STR_EXPAND(x) #x
static NSString *const kSemanticVersioningSeparator = @".";
static NSString *const kBetaVersionPrefix = @"-beta";
static NSString *libraryVersion;
static int majorVersion;
static int minorVersion;
static int patchVersion;
static int betaVersion;
void FIRInstanceIDParseCurrentLibraryVersion() {
static NSArray *allVersions;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSMutableString *daylightVersion =
[NSMutableString stringWithUTF8String:STR(FIRInstanceID_LIB_VERSION)];
// Parse versions
// major, minor, patch[-beta#]
allVersions = [daylightVersion componentsSeparatedByString:kSemanticVersioningSeparator];
if (allVersions.count == 3) {
majorVersion = [allVersions[0] intValue];
minorVersion = [allVersions[1] intValue];
// Parse patch and beta versions
NSArray *patchAndBetaVersion =
[allVersions[2] componentsSeparatedByString:kBetaVersionPrefix];
if (patchAndBetaVersion.count == 2) {
patchVersion = [patchAndBetaVersion[0] intValue];
betaVersion = [patchAndBetaVersion[1] intValue];
} else if (patchAndBetaVersion.count == 1) {
patchVersion = [patchAndBetaVersion[0] intValue];
}
}
// Copy library version
libraryVersion = [daylightVersion copy];
});
}
NSString *FIRInstanceIDCurrentLibraryVersion() {
FIRInstanceIDParseCurrentLibraryVersion();
return libraryVersion;
}
int FIRInstanceIDCurrentLibraryVersionMajor() {
FIRInstanceIDParseCurrentLibraryVersion();
return majorVersion;
}
int FIRInstanceIDCurrentLibraryVersionMinor() {
FIRInstanceIDParseCurrentLibraryVersion();
return minorVersion;
}
int FIRInstanceIDCurrentLibraryVersionPatch() {
FIRInstanceIDParseCurrentLibraryVersion();
return patchVersion;
}
BOOL FIRInstanceIDCurrentLibraryVersionIsBeta() {
FIRInstanceIDParseCurrentLibraryVersion();
return betaVersion > 0;
}
@@ -0,0 +1,70 @@
/*
* 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>
FOUNDATION_EXPORT NSString *const kFIRInstanceIDDomain;
typedef NS_ENUM(NSUInteger, FIRInstanceIDErrorCode) {
// Unknown error.
kFIRInstanceIDErrorCodeUnknown = 0,
// Http related errors.
kFIRInstanceIDErrorCodeAuthentication = 1,
kFIRInstanceIDErrorCodeNoAccess = 2,
kFIRInstanceIDErrorCodeTimeout = 3,
kFIRInstanceIDErrorCodeNetwork = 4,
// Another operation is in progress.
kFIRInstanceIDErrorCodeOperationInProgress = 5,
// Failed to perform device check in.
kFIRInstanceIDErrorCodeRegistrarFailedToCheckIn = 6,
kFIRInstanceIDErrorCodeInvalidRequest = 7,
// InstanceID generic errors
kFIRInstanceIDErrorCodeMissingDeviceID = 501,
// InstanceID Token specific errors
kFIRInstanceIDErrorCodeMissingAPNSToken = 1001,
kFIRInstanceIDErrorCodeMissingAPNSServerType = 1002,
kFIRInstanceIDErrorCodeInvalidAuthorizedEntity = 1003,
kFIRInstanceIDErrorCodeInvalidScope = 1004,
kFIRInstanceIDErrorCodeInvalidStart = 1005,
kFIRInstanceIDErrorCodeInvalidKeyPair = 1006,
// InstanceID Identity specific errors
// Generic InstanceID keypair error
kFIRInstanceIDErrorCodeMissingKeyPair = 2001,
kFIRInstanceIDErrorCodeInvalidKeyPairTags = 2002,
kFIRInstanceIDErrorCodeInvalidKeyPairCreationTime = 2005,
kFIRInstanceIDErrorCodeInvalidIdentity = 2006,
};
@interface NSError (FIRInstanceID)
@property(nonatomic, readonly) FIRInstanceIDErrorCode instanceIDErrorCode;
+ (NSError *)errorWithFIRInstanceIDErrorCode:(FIRInstanceIDErrorCode)errorCode;
+ (NSError *)errorWithFIRInstanceIDErrorCode:(FIRInstanceIDErrorCode)errorCode
userInfo:(NSDictionary *)userInfo;
+ (NSError *)FIRInstanceIDErrorMissingCheckin;
@end
@@ -0,0 +1,44 @@
/*
* 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 "NSError+FIRInstanceID.h"
NSString *const kFIRInstanceIDDomain = @"com.firebase.iid";
@implementation NSError (FIRInstanceID)
- (FIRInstanceIDErrorCode)instanceIDErrorCode {
return (FIRInstanceIDErrorCode)self.code;
}
+ (NSError *)errorWithFIRInstanceIDErrorCode:(FIRInstanceIDErrorCode)errorCode {
return [NSError errorWithFIRInstanceIDErrorCode:errorCode userInfo:nil];
}
+ (NSError *)errorWithFIRInstanceIDErrorCode:(FIRInstanceIDErrorCode)errorCode
userInfo:(NSDictionary *)userInfo {
return [NSError errorWithDomain:kFIRInstanceIDDomain code:errorCode userInfo:userInfo];
}
+ (NSError *)FIRInstanceIDErrorMissingCheckin {
NSDictionary *userInfo = @{@"msg" : @"Missing device credentials. Retry later."};
return [NSError errorWithDomain:kFIRInstanceIDDomain
code:kFIRInstanceIDErrorCodeMissingDeviceID
userInfo:userInfo];
}
@end
@@ -0,0 +1,67 @@
/*
* 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 <FirebaseInstanceID/FIRInstanceID.h>
NS_ASSUME_NONNULL_BEGIN
@class FIRInstanceIDCheckinPreferences;
/**
* Private API used by other Firebase SDKs.
*/
@interface FIRInstanceID ()
@property(nonatomic, readonly, strong) NSString *deviceAuthID;
@property(nonatomic, readonly, strong) NSString *secretToken;
@property(nonatomic, readonly, strong) NSString *versionInfo;
/**
* Private initializer.
*/
- (instancetype)initPrivately;
/**
* Returns a Firebase Messaging scoped token for the firebase app.
*
* @return Returns the stored token if the device has registered with Firebase Messaging, otherwise
* returns nil.
*/
- (nullable NSString *)token;
/**
* Verify if valid checkin preferences have been loaded in memory.
*
* @return YES if valid checkin preferences exist in memory else NO.
*/
- (BOOL)hasValidCheckinInfo;
/**
* Try to load prefetched checkin preferences from the cache. This supports the use case where
* InstanceID library has already obtained a valid checkin and we should be using that.
*
* This should be used as a last gasp effort to retreive any cached checkin preferences before
* hitting the FIRMessaging backend to retrieve new preferences.
*
* Note this is only required because InstanceID and FIRMessaging both require checkin preferences
* which need to be synced with each other.
*
* @return YES if successfully loaded cached checkin preferences into memory else NO.
*/
- (BOOL)tryToLoadValidCheckinInfo;
@end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,312 @@
/*
* 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
@class FIRInstanceIDResult;
/**
* @memberof FIRInstanceID
*
* The scope to be used when fetching/deleting a token for Firebase Messaging.
*/
FOUNDATION_EXPORT NSString *const kFIRInstanceIDScopeFirebaseMessaging
NS_SWIFT_NAME(InstanceIDScopeFirebaseMessaging);
#if defined(__IPHONE_10_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0
/**
* Called when the system determines that tokens need to be refreshed.
* This method is also called if Instance ID has been reset in which
* case, tokens and FCM topic subscriptions also need to be refreshed.
*
* Instance ID service will throttle the refresh event across all devices
* to control the rate of token updates on application servers.
*/
FOUNDATION_EXPORT const NSNotificationName kFIRInstanceIDTokenRefreshNotification
NS_SWIFT_NAME(InstanceIDTokenRefresh);
#else
/**
* Called when the system determines that tokens need to be refreshed.
* This method is also called if Instance ID has been reset in which
* case, tokens and FCM topic subscriptions also need to be refreshed.
*
* Instance ID service will throttle the refresh event across all devices
* to control the rate of token updates on application servers.
*/
FOUNDATION_EXPORT NSString *const kFIRInstanceIDTokenRefreshNotification
NS_SWIFT_NAME(InstanceIDTokenRefreshNotification);
#endif // defined(__IPHONE_10_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0
/**
* @related FIRInstanceID
*
* The completion handler invoked when the InstanceID token returns. If
* the call fails we return the appropriate `error code` as described below.
*
* @param token The valid token as returned by InstanceID backend.
*
* @param error The error describing why generating a new token
* failed. See the error codes below for a more detailed
* description.
*/
typedef void (^FIRInstanceIDTokenHandler)(NSString *__nullable token, NSError *__nullable error)
NS_SWIFT_NAME(InstanceIDTokenHandler);
/**
* @related FIRInstanceID
*
* The completion handler invoked when the InstanceID `deleteToken` returns. If
* the call fails we return the appropriate `error code` as described below
*
* @param error The error describing why deleting the token failed.
* See the error codes below for a more detailed description.
*/
typedef void (^FIRInstanceIDDeleteTokenHandler)(NSError *error)
NS_SWIFT_NAME(InstanceIDDeleteTokenHandler);
/**
* @related FIRInstanceID
*
* The completion handler invoked when the app identity is created. If the
* identity wasn't created for some reason we return the appropriate error code.
*
* @param identity A valid identity for the app instance, nil if there was an error
* while creating an identity.
* @param error The error if fetching the identity fails else nil.
*/
typedef void (^FIRInstanceIDHandler)(NSString *__nullable identity, NSError *__nullable error)
NS_SWIFT_NAME(InstanceIDHandler);
/**
* @related FIRInstanceID
*
* The completion handler invoked when the app identity and all the tokens associated
* with it are deleted. Returns a valid error object in case of failure else nil.
*
* @param error The error if deleting the identity and all the tokens associated with
* it fails else nil.
*/
typedef void (^FIRInstanceIDDeleteHandler)(NSError *__nullable error)
NS_SWIFT_NAME(InstanceIDDeleteHandler);
/**
* @related FIRInstanceID
*
* The completion handler invoked when the app identity and token are fetched. If the
* identity wasn't created for some reason we return the appropriate error code.
*
* @param result The result containing an identity for the app instance and a valid token,
* nil if there was an error while creating the result.
* @param error The error if fetching the identity or token fails else nil.
*/
typedef void (^FIRInstanceIDResultHandler)(FIRInstanceIDResult *__nullable result,
NSError *__nullable error)
NS_SWIFT_NAME(InstanceIDResultHandler);
/**
* Public errors produced by InstanceID.
*/
typedef NS_ENUM(NSUInteger, FIRInstanceIDError) {
// Http related errors.
/// Unknown error.
FIRInstanceIDErrorUnknown = 0,
/// Auth Error -- GCM couldn't validate request from this client.
FIRInstanceIDErrorAuthentication = 1,
/// NoAccess -- InstanceID service cannot be accessed.
FIRInstanceIDErrorNoAccess = 2,
/// Timeout -- Request to InstanceID backend timed out.
FIRInstanceIDErrorTimeout = 3,
/// Network -- No network available to reach the servers.
FIRInstanceIDErrorNetwork = 4,
/// OperationInProgress -- Another similar operation in progress,
/// bailing this one.
FIRInstanceIDErrorOperationInProgress = 5,
/// InvalidRequest -- Some parameters of the request were invalid.
FIRInstanceIDErrorInvalidRequest = 7,
} NS_SWIFT_NAME(InstanceIDError);
/**
* A class contains the results of InstanceID and token query.
*/
NS_SWIFT_NAME(InstanceIDResult)
@interface FIRInstanceIDResult : NSObject <NSCopying>
/**
* An instanceID uniquely identifies the app instance.
*/
@property(nonatomic, readonly, copy) NSString *instanceID;
/*
* Returns a Firebase Messaging scoped token for the firebase app.
*/
@property(nonatomic, readonly, copy) NSString *token;
@end
/**
* Instance ID provides a unique identifier for each app instance and a mechanism
* to authenticate and authorize actions (for example, sending an FCM message).
*
* Once an InstanceID is generated, the library periodically sends information about the
* application and the device where it's running to the Firebase backend. To stop this. see
* `[FIRInstanceID deleteIDWithHandler:]`.
*
* Instance ID is long lived but, may be reset if the device is not used for
* a long time or the Instance ID service detects a problem.
* If Instance ID is reset, the app will be notified via
* `kFIRInstanceIDTokenRefreshNotification`.
*
* If the Instance ID has become invalid, the app can request a new one and
* send it to the app server.
* To prove ownership of Instance ID and to allow servers to access data or
* services associated with the app, call
* `[FIRInstanceID tokenWithAuthorizedEntity:scope:options:handler]`.
*/
NS_SWIFT_NAME(InstanceID)
@interface FIRInstanceID : NSObject
/**
* FIRInstanceID.
*
* @return A shared instance of FIRInstanceID.
*/
+ (instancetype)instanceID NS_SWIFT_NAME(instanceID());
/**
* Unavailable. Use +instanceID instead.
*/
- (instancetype)init __attribute__((unavailable("Use +instanceID instead.")));
#pragma mark - Tokens
/**
* Returns a result of app instance identifier InstanceID and a Firebase Messaging scoped token.
* param handler The callback handler invoked when an app instanceID and a default token
* are generated and returned. If instanceID and token fetching fail for some
* reason the callback is invoked with nil `result` and the appropriate error.
*/
- (void)instanceIDWithHandler:(FIRInstanceIDResultHandler)handler;
/**
* Returns a token that authorizes an Entity (example: cloud service) to perform
* an action on behalf of the application identified by Instance ID.
*
* This is similar to an OAuth2 token except, it applies to the
* application instance instead of a user.
*
* This is an asynchronous call. If the token fetching fails for some reason
* we invoke the completion callback with nil `token` and the appropriate
* error.
*
* This generates an Instance ID if it does not exist yet, which starts periodically sending
* information to the Firebase backend (see `[FIRInstanceID getIDWithHandler:]`).
*
* Note, you can only have one `token` or `deleteToken` call for a given
* authorizedEntity and scope at any point of time. Making another such call with the
* same authorizedEntity and scope before the last one finishes will result in an
* error with code `OperationInProgress`.
*
* @see FIRInstanceID deleteTokenWithAuthorizedEntity:scope:handler:
*
* @param authorizedEntity Entity authorized by the token.
* @param scope Action authorized for authorizedEntity.
* @param options The extra options to be sent with your token request. The
* value for the `apns_token` should be the NSData object
* passed to the UIApplicationDelegate's
* `didRegisterForRemoteNotificationsWithDeviceToken` method.
* The value for `apns_sandbox` should be a boolean (or an
* NSNumber representing a BOOL in Objective-C) set to true if
* your app is a debug build, which means that the APNs
* device token is for the sandbox environment. It should be
* set to false otherwise. If the `apns_sandbox` key is not
* provided, an automatically-detected value shall be used.
* @param handler The callback handler which is invoked when the token is
* successfully fetched. In case of success a valid `token` and
* `nil` error are returned. In case of any error the `token`
* is nil and a valid `error` is returned. The valid error
* codes have been documented above.
*/
- (void)tokenWithAuthorizedEntity:(NSString *)authorizedEntity
scope:(NSString *)scope
options:(nullable NSDictionary *)options
handler:(FIRInstanceIDTokenHandler)handler;
/**
* Revokes access to a scope (action) for an entity previously
* authorized by `[FIRInstanceID tokenWithAuthorizedEntity:scope:options:handler]`.
*
* This is an asynchronous call. Call this on the main thread since InstanceID lib
* is not thread safe. In case token deletion fails for some reason we invoke the
* `handler` callback passed in with the appropriate error code.
*
* Note, you can only have one `token` or `deleteToken` call for a given
* authorizedEntity and scope at a point of time. Making another such call with the
* same authorizedEntity and scope before the last one finishes will result in an error
* with code `OperationInProgress`.
*
* @param authorizedEntity Entity that must no longer have access.
* @param scope Action that entity is no longer authorized to perform.
* @param handler The handler that is invoked once the unsubscribe call ends.
* In case of error an appropriate error object is returned
* else error is nil.
*/
- (void)deleteTokenWithAuthorizedEntity:(NSString *)authorizedEntity
scope:(NSString *)scope
handler:(FIRInstanceIDDeleteTokenHandler)handler;
#pragma mark - Identity
/**
* Asynchronously fetch a stable identifier that uniquely identifies the app
* instance. If the identifier has been revoked or has expired, this method will
* return a new identifier.
*
* Once an InstanceID is generated, the library periodically sends information about the
* application and the device where it's running to the Firebase backend. To stop this. see
* `[FIRInstanceID deleteIDWithHandler:]`.
*
* @param handler The handler to invoke once the identifier has been fetched.
* In case of error an appropriate error object is returned else
* a valid identifier is returned and a valid identifier for the
* application instance.
*/
- (void)getIDWithHandler:(FIRInstanceIDHandler)handler NS_SWIFT_NAME(getID(handler:));
/**
* Resets Instance ID and revokes all tokens.
*
* This method also triggers a request to fetch a new Instance ID and Firebase Messaging scope
* token. Please listen to kFIRInstanceIDTokenRefreshNotification when the new ID and token are
* ready.
*
* This stops the periodic sending of data to the Firebase backend that began when the Instance ID
* was generated. No more data is sent until another library calls Instance ID internally again
* (like FCM, RemoteConfig or Analytics) or user explicitly calls Instance ID APIs to get an
* Instance ID and token again.
*/
- (void)deleteIDWithHandler:(FIRInstanceIDDeleteHandler)handler NS_SWIFT_NAME(deleteID(handler:));
@end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,17 @@
/*
* 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 "FIRInstanceID.h"