You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

525 lines
22 KiB

/*
* 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