Files
firebase-ios-sdk/FirebaseMessaging/Sources/Token/FIRMessagingTokenInfo.m

274 lines
12 KiB
Objective-C

/*
* 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 "FirebaseMessaging/Sources/Token/FIRMessagingTokenInfo.h"
#import "FirebaseMessaging/Sources/FIRMessagingConstants.h"
#import "FirebaseMessaging/Sources/FIRMessagingLogger.h"
#import "FirebaseMessaging/Sources/FIRMessagingUtilities.h"
#import "FirebaseMessaging/Sources/Public/FirebaseMessaging/FIRMessaging.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 dictionary 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";
/// Specifies a dictionary key representing the token type (V4 or FID).
static NSString *const kFIRInstanceIDTokenTypeKey = @"token_type";
/// Default interval that token stays fresh.
static const NSTimeInterval kDefaultFetchTokenInterval = 7 * 24 * 60 * 60; // 7 days.
@implementation FIRMessagingTokenInfo
- (instancetype)initWithAuthorizedEntity:(NSString *)authorizedEntity
scope:(NSString *)scope
token:(NSString *)token
appVersion:(NSString *)appVersion
firebaseAppID:(NSString *)firebaseAppID
tokenType:(NSString *)tokenType {
self = [super init];
if (self) {
_authorizedEntity = [authorizedEntity copy];
_scope = [scope copy];
_token = [token copy];
_appVersion = [appVersion copy];
_firebaseAppID = [firebaseAppID copy];
_tokenType = [tokenType copy] ?: @"V4";
}
return self;
}
- (BOOL)isFreshWithIID:(NSString *)IID {
// 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 (!IID) {
return NO;
}
if (!_cacheTime) {
return NO;
}
// Check if token type matches isInstallationIdEnabled
BOOL isInstallationIdEnabled = [FIRMessaging messaging].isInstallationIdEnabled;
if (isInstallationIdEnabled && ![self.tokenType isEqualToString:@"FID"]) {
FIRMessagingLoggerDebug(kFIRMessagingMessageCodeTokenManager004,
@"Invalidating cached token due to isInstallationIdEnabled being YES "
@"but token is not FID.");
return NO;
}
if (!isInstallationIdEnabled && ![self.tokenType isEqualToString:@"V4"]) {
FIRMessagingLoggerDebug(
kFIRMessagingMessageCodeTokenManager004,
@"Invalidating cached token due to isInstallationIdEnabled being NO but token is FID.");
return NO;
}
// Check if it's consistent with IID
if (![self.token hasPrefix:IID]) {
return NO;
}
if ([self hasDenylistedScope]) {
return NO;
}
// Check if app has just been updated to a new version.
NSString *currentAppVersion = FIRMessagingCurrentAppVersion();
if (!_appVersion || ![_appVersion isEqualToString:currentAppVersion]) {
FIRMessagingLoggerDebug(kFIRMessagingMessageCodeTokenManager004,
@"Invalidating cached token for %@ (%@) due to app version change.",
_authorizedEntity, _scope);
return NO;
}
// Check if GMP App ID has changed
NSString *currentFirebaseAppID = FIRMessagingFirebaseAppID();
if (!_firebaseAppID || ![_firebaseAppID isEqualToString:currentFirebaseAppID]) {
FIRMessagingLoggerDebug(
kFIRMessagingMessageCodeTokenInfoFirebaseAppIDChanged,
@"Invalidating cached token due to Firebase App IID change from %@ to %@", _firebaseAppID,
currentFirebaseAppID);
return NO;
}
// Check whether locale has changed. The new FID registration API does not track or send locale
// information (and bypasses the checkin API that persists it in user defaults), so locale
// changes only invalidate cached tokens in legacy checkin-based registration.
if (!isInstallationIdEnabled && FIRMessagingHasLocaleChanged()) {
FIRMessagingLoggerDebug(kFIRMessagingMessageCodeTokenInfoLocaleChanged,
@"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 = FIRMessagingCurrentTimestampInSeconds();
NSTimeInterval timeSinceLastFetchToken = currentTimestamp - lastFetchTokenTimestamp;
return (timeSinceLastFetchToken < kDefaultFetchTokenInterval);
}
- (BOOL)hasDenylistedScope {
/// The token with fiam scope is set by old FIAM SDK(s) which will remain in keychain for ever. So
/// we need to remove these tokens to deny its usage.
if ([self.scope isEqualToString:kFIRMessagingFIAMTokenScope]) {
return YES;
}
return NO;
}
- (BOOL)isDefaultToken {
return [self.scope isEqualToString:kFIRMessagingDefaultTokenScope];
}
#pragma mark - NSSecureCoding
+ (BOOL)supportsSecureCoding {
return YES;
}
- (nullable instancetype)initWithCoder:(NSCoder *)aDecoder {
// These value cannot be nil
NSString *authorizedEntity = [aDecoder decodeObjectOfClass:[NSString class]
forKey:kFIRInstanceIDAuthorizedEntityKey];
if (!authorizedEntity) {
return nil;
}
NSString *scope = [aDecoder decodeObjectOfClass:[NSString class] forKey:kFIRInstanceIDScopeKey];
if (!scope) {
return nil;
}
NSString *token = [aDecoder decodeObjectOfClass:[NSString class] forKey:kFIRInstanceIDTokenKey];
if (!token) {
return nil;
}
// These values are nullable, so don't fail on nil.
NSString *appVersion = [aDecoder decodeObjectOfClass:[NSString class]
forKey:kFIRInstanceIDAppVersionKey];
NSString *firebaseAppID = [aDecoder decodeObjectOfClass:[NSString class]
forKey:kFIRInstanceIDFirebaseAppIDKey];
// `apns_info` has two on-disk shapes. FirebaseMessaging 10.19.0 and later encode APNSInfo
// directly. 10.18.0 and earlier wrote a nested NSKeyedArchiver blob instead. Older versions could
// also persist a mutable token (NSMutableData), so allowlist both NSData and NSMutableData.
NSSet *APNSInfoClasses =
[NSSet setWithObjects:FIRMessagingAPNSInfo.class, NSData.class, NSMutableData.class, nil];
id decodedAPNSInfo = [aDecoder decodeObjectOfClasses:APNSInfoClasses
forKey:kFIRInstanceIDAPNSInfoKey];
FIRMessagingAPNSInfo *rawAPNSInfo = nil;
BOOL needsMigration = NO;
if ([decodedAPNSInfo isKindOfClass:[FIRMessagingAPNSInfo class]]) {
rawAPNSInfo = decodedAPNSInfo;
} else if ([decodedAPNSInfo isKindOfClass:[NSData class]]) {
// A 10.18.0-or-earlier record. The nested blob names the class `FIRInstanceIDAPNSInfo`, so
// map it the same way FIRMessagingTokenStore maps `FIRInstanceIDTokenInfo` on the outer
// archive. Secure coding stays on: the blob was written insecurely, but FIRMessagingAPNSInfo
// conforms to NSSecureCoding, so it can still be read under the strict decoder.
NSError *APNSInfoError = nil;
NSKeyedUnarchiver *APNSInfoUnarchiver = nil;
BOOL caughtException = NO;
@try {
APNSInfoUnarchiver = [[NSKeyedUnarchiver alloc] initForReadingFromData:decodedAPNSInfo
error:&APNSInfoError];
if (APNSInfoUnarchiver) {
APNSInfoUnarchiver.requiresSecureCoding = YES;
[APNSInfoUnarchiver setClass:[FIRMessagingAPNSInfo class]
forClassName:@"FIRInstanceIDAPNSInfo"];
rawAPNSInfo = [APNSInfoUnarchiver decodeObjectOfClass:[FIRMessagingAPNSInfo class]
forKey:NSKeyedArchiveRootObjectKey];
[APNSInfoUnarchiver finishDecoding];
}
} @catch (NSException *exception) {
FIRMessagingLoggerInfo(kFIRMessagingMessageCodeTokenInfoBadAPNSInfo,
@"Exception decoding APNS info archived by FirebaseMessaging 10.18.0 "
@"or earlier: %@",
exception);
rawAPNSInfo = nil;
caughtException = YES;
}
if (!rawAPNSInfo && !caughtException) {
FIRMessagingLoggerInfo(kFIRMessagingMessageCodeTokenInfoBadAPNSInfo,
@"Could not parse APNS info archived by FirebaseMessaging 10.18.0 or "
@"earlier; error: %@",
APNSInfoError ?: APNSInfoUnarchiver.error);
}
// Either way the record is in the retired format, so have the store rewrite it.
needsMigration = YES;
}
NSDate *cacheTime = [aDecoder decodeObjectOfClass:[NSDate class]
forKey:kFIRInstanceIDCacheTimeKey];
NSString *tokenType = [aDecoder decodeObjectOfClass:[NSString class]
forKey:kFIRInstanceIDTokenTypeKey];
self = [super init];
if (self) {
_authorizedEntity = [authorizedEntity copy];
_scope = [scope copy];
_token = [token copy];
_appVersion = [appVersion copy];
_firebaseAppID = [firebaseAppID copy];
_APNSInfo = [rawAPNSInfo copy];
_cacheTime = cacheTime;
_needsMigration = needsMigration;
_tokenType = [tokenType copy] ?: @"V4";
}
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];
if (self.APNSInfo) {
[aCoder encodeObject:self.APNSInfo forKey:kFIRInstanceIDAPNSInfoKey];
}
[aCoder encodeObject:self.cacheTime forKey:kFIRInstanceIDCacheTimeKey];
if (self.tokenType) {
[aCoder encodeObject:self.tokenType forKey:kFIRInstanceIDTokenTypeKey];
}
}
@end