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"
+202
View File
@@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
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.
+223
View File
@@ -0,0 +1,223 @@
# Firebase iOS Open Source Development [![Build Status](https://travis-ci.org/firebase/firebase-ios-sdk.svg?branch=master)](https://travis-ci.org/firebase/firebase-ios-sdk)
This repository contains a subset of the Firebase iOS SDK source. It currently
includes FirebaseCore, FirebaseAuth, FirebaseDatabase, FirebaseFirestore,
FirebaseFunctions, FirebaseInstanceID, FirebaseInAppMessaging,
FirebaseInAppMessagingDisplay, FirebaseMessaging and FirebaseStorage.
The repository also includes GoogleUtilities source. The
[GoogleUtilities](GoogleUtilities/README.md) pod is
a set of utilities used by Firebase and other Google products.
Firebase is an app development platform with tools to help you build, grow and
monetize your app. More information about Firebase can be found at
[https://firebase.google.com](https://firebase.google.com).
## Installation
See the three subsections for details about three different installation methods.
1. [Standard pod install](README.md#standard-pod-install)
1. [Installing from the GitHub repo](README.md#installing-from-github)
1. [Experimental Carthage](README.md#carthage-ios-only)
### Standard pod install
Go to
[https://firebase.google.com/docs/ios/setup](https://firebase.google.com/docs/ios/setup).
### Installing from GitHub
For releases starting with 5.0.0, the source for each release is also deployed
to CocoaPods master and available via standard
[CocoaPods Podfile syntax](https://guides.cocoapods.org/syntax/podfile.html#pod).
These instructions can be used to access the Firebase repo at other branches,
tags, or commits.
#### Background
See
[the Podfile Syntax Reference](https://guides.cocoapods.org/syntax/podfile.html#pod)
for instructions and options about overriding pod source locations.
#### Accessing Firebase Source Snapshots
All of the official releases are tagged in this repo and available via CocoaPods. To access a local
source snapshot or unreleased branch, use Podfile directives like the following:
To access FirebaseFirestore via a branch:
```
pod 'FirebaseCore', :git => 'https://github.com/firebase/firebase-ios-sdk.git', :branch => 'master'
pod 'FirebaseFirestore', :git => 'https://github.com/firebase/firebase-ios-sdk.git', :branch => 'master'
```
To access FirebaseMessaging via a checked out version of the firebase-ios-sdk repo do:
```
pod 'FirebaseCore', :path => '/path/to/firebase-ios-sdk'
pod 'FirebaseMessaging', :path => '/path/to/firebase-ios-sdk'
```
### Carthage (iOS only)
Instructions for the experimental Carthage distribution are at
[Carthage](Carthage.md).
### Rome
Instructions for installing binary frameworks via
[Rome](https://github.com/CocoaPods/Rome) are at [Rome](Rome.md).
## Development
To develop Firebase software in this repository, ensure that you have at least
the following software:
* Xcode 10.1 (or later)
* CocoaPods 1.7.2 (or later)
For the pod that you want to develop:
`pod gen Firebase{name here}.podspec --local-sources=./ --auto-open`
Firestore and Functions have self contained Xcode projects. See
[Firestore/README.md](Firestore/README.md) and
[Functions/README.md](Functions/README.md).
### Adding a New Firebase Pod
See [AddNewPod.md](AddNewPod.md).
### Code Formatting
To ensure that the code is formatted consistently, run the script
[./scripts/style.sh](https://github.com/firebase/firebase-ios-sdk/blob/master/scripts/style.sh)
before creating a PR.
Travis will verify that any code changes are done in a style compliant way. Install
`clang-format` and `swiftformat`.
These commands will get the right versions:
```
brew install https://raw.githubusercontent.com/Homebrew/homebrew-core/773cb75d360b58f32048f5964038d09825a507c8/Formula/clang-format.rb
brew install https://raw.githubusercontent.com/Homebrew/homebrew-core/3dfea1004e0736754bbf49673cca8aaed8a94089/Formula/swiftformat.rb
```
Note: if you already have a newer version of these installed you may need to
`brew switch` to this version.
### Running Unit Tests
Select a scheme and press Command-u to build a component and run its unit tests.
#### Viewing Code Coverage
First, make sure that [xcov](https://github.com/nakiostudio/xcov) is installed with `gem install xcov`.
After running the `AllUnitTests_iOS` scheme in Xcode, execute
`xcov --workspace Firebase.xcworkspace --scheme AllUnitTests_iOS --output_directory xcov_output`
at Example/ in the terminal. This will aggregate the coverage, and you can run `open xcov_output/index.html` to see the results.
### Running Sample Apps
In order to run the sample apps and integration tests, you'll need valid
`GoogleService-Info.plist` files for those samples. The Firebase Xcode project contains dummy plist
files without real values, but can be replaced with real plist files. To get your own
`GoogleService-Info.plist` files:
1. Go to the [Firebase Console](https://console.firebase.google.com/)
2. Create a new Firebase project, if you don't already have one
3. For each sample app you want to test, create a new Firebase app with the sample app's bundle
identifier (e.g. `com.google.Database-Example`)
4. Download the resulting `GoogleService-Info.plist` and replace the appropriate dummy plist file
(e.g. in [Example/Database/App/](Example/Database/App/));
Some sample apps like Firebase Messaging ([Example/Messaging/App](Example/Messaging/App)) require
special Apple capabilities, and you will have to change the sample app to use a unique bundle
identifier that you can control in your own Apple Developer account.
## Specific Component Instructions
See the sections below for any special instructions for those components.
### Firebase Auth
If you're doing specific Firebase Auth development, see
[the Auth Sample README](Example/Auth/README.md) for instructions about
building and running the FirebaseAuth pod along with various samples and tests.
### Firebase Database
To run the Database Integration tests, make your database authentication rules
[public](https://firebase.google.com/docs/database/security/quickstart).
### Firebase Storage
To run the Storage Integration tests, follow the instructions in
[FIRStorageIntegrationTests.m](Example/Storage/Tests/Integration/FIRStorageIntegrationTests.m).
#### Push Notifications
Push notifications can only be delivered to specially provisioned App IDs in the developer portal.
In order to actually test receiving push notifications, you will need to:
1. Change the bundle identifier of the sample app to something you own in your Apple Developer
account, and enable that App ID for push notifications.
2. You'll also need to
[upload your APNs Provider Authentication Key or certificate to the Firebase Console](https://firebase.google.com/docs/cloud-messaging/ios/certs)
at **Project Settings > Cloud Messaging > [Your Firebase App]**.
3. Ensure your iOS device is added to your Apple Developer portal as a test device.
#### iOS Simulator
The iOS Simulator cannot register for remote notifications, and will not receive push notifications.
In order to receive push notifications, you'll have to follow the steps above and run the app on a
physical device.
## Community Supported Efforts
We've seen an amazing amount of interest and contributions to improve the Firebase SDKs, and we are
very grateful! We'd like to empower as many developers as we can to be able to use Firebase and
participate in the Firebase community.
### macOS and tvOS
Thanks to contributions from the community, FirebaseAuth, FirebaseCore, FirebaseDatabase, FirebaseMessaging,
FirebaseFirestore, FirebaseFunctions and FirebaseStorage now compile, run unit tests, and work on
macOS and tvOS.
For tvOS, checkout the [Sample](Example/tvOSSample).
Keep in mind that macOS and tvOS are not officially supported by Firebase, and this repository is
actively developed primarily for iOS. While we can catch basic unit test issues with Travis, there
may be some changes where the SDK no longer works as expected on macOS or tvOS. If you encounter
this, please [file an issue](https://github.com/firebase/firebase-ios-sdk/issues).
Note that the Firebase pod is not available for macOS and tvOS.
To install, add a subset of the following to the Podfile:
```
pod 'FirebaseAuth'
pod 'FirebaseCore'
pod 'FirebaseDatabase'
pod 'FirebaseFirestore'
pod 'FirebaseFunctions'
pod 'FirebaseMessaging'
pod 'FirebaseStorage'
```
## Roadmap
See [Roadmap](ROADMAP.md) for more about the Firebase iOS SDK Open Source
plans and directions.
## Contributing
See [Contributing](CONTRIBUTING.md) for more information on contributing to the Firebase
iOS SDK.
## License
The contents of this repository is licensed under the
[Apache License, version 2.0](http://www.apache.org/licenses/LICENSE-2.0).
Your use of Firebase is governed by the
[Terms of Service for Firebase Services](https://firebase.google.com/terms/).