diff --git a/Sift/SiftKeychain.m b/Sift/SiftKeychain.m index 79fd657..2348172 100644 --- a/Sift/SiftKeychain.m +++ b/Sift/SiftKeychain.m @@ -32,6 +32,7 @@ + (NSString *)getStoredIFVString { (__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword, (__bridge id)kSecAttrAccount: kSiftVendorIFVKeychainKey, (__bridge id)kSecReturnData: (__bridge id)kCFBooleanTrue, + (__bridge id)kSecReturnAttributes: (__bridge id)kCFBooleanTrue, (__bridge id)kSecMatchLimit: (__bridge id)kSecMatchLimitOne }; @@ -40,22 +41,43 @@ + (NSString *)getStoredIFVString { NSString *storedIFVString = nil; if (result) { - NSData *data = (__bridge_transfer NSData *)result; + NSDictionary *attributes = (__bridge_transfer NSDictionary *)result; + NSData *data = attributes[(__bridge id)kSecValueData]; storedIFVString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; + + if (storedIFVString != nil && [self attributesNeedMigration:attributes]) { + [self storeIFVString:storedIFVString]; + } } return storedIFVString; } ++ (BOOL)attributesNeedMigration:(NSDictionary *)attributes { + BOOL isDeviceOnly = [attributes[(__bridge id)kSecAttrAccessible] + isEqual:(__bridge id)kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly]; + BOOL isSynchronizable = [attributes[(__bridge id)kSecAttrSynchronizable] boolValue]; + return !isDeviceOnly || isSynchronizable; +} + + (void)storeIFVString:(NSString *)ifv { + NSDictionary *query = [self keychainQueryForIFV:ifv]; + SecItemDelete((__bridge CFDictionaryRef)query); + SecItemAdd((__bridge CFDictionaryRef)query, NULL); +} + ++ (NSDictionary *)keychainQueryForIFV:(NSString *)ifv { NSData *data = [ifv dataUsingEncoding:NSUTF8StringEncoding]; - NSDictionary *query = @{ + return @{ (__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword, (__bridge id)kSecAttrAccount: kSiftVendorIFVKeychainKey, - (__bridge id)kSecValueData: data + (__bridge id)kSecValueData: data, + (__bridge id)kSecAttrAccessible: (__bridge id)kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly, + (__bridge id)kSecAttrSynchronizable: (__bridge id)kCFBooleanFalse }; +} - SecItemDelete((__bridge CFDictionaryRef)query); - SecItemAdd((__bridge CFDictionaryRef)query, NULL); ++ (NSString *)vendorIFVKeychainKey { + return kSiftVendorIFVKeychainKey; } @end diff --git a/SiftTests/SiftKeychain+Testing.h b/SiftTests/SiftKeychain+Testing.h index d40133b..8983472 100644 --- a/SiftTests/SiftKeychain+Testing.h +++ b/SiftTests/SiftKeychain+Testing.h @@ -11,4 +11,7 @@ @interface SiftKeychain () + (NSString *)getStoredIFVString; + (void)storeIFVString:(NSString *)ifv; ++ (NSString *)vendorIFVKeychainKey; ++ (NSDictionary *)keychainQueryForIFV:(NSString *)ifv; ++ (BOOL)attributesNeedMigration:(NSDictionary *)attributes; @end diff --git a/SiftTests/SiftKeychainTests.m b/SiftTests/SiftKeychainTests.m index 5ca5a97..7c1bb49 100644 --- a/SiftTests/SiftKeychainTests.m +++ b/SiftTests/SiftKeychainTests.m @@ -71,6 +71,48 @@ - (void)testProcessDeviceIFV_changedDeviceIFV { [self swizzleMethod:mockGetStoredDeviceIFV withMethod:getStoredDeviceIFV]; } +- (void)testKeychainQueryForIFV_setsDeviceOnlyAccessibilityAndNotSynchronizable { + NSString *ifv = @"TEST-DEVICE-ONLY-IFV"; + NSDictionary *query = [SiftKeychain keychainQueryForIFV:ifv]; + + XCTAssertEqualObjects(query[(__bridge id)kSecAttrAccount], [SiftKeychain vendorIFVKeychainKey]); + XCTAssertEqualObjects(query[(__bridge id)kSecValueData], [ifv dataUsingEncoding:NSUTF8StringEncoding]); + XCTAssertEqualObjects(query[(__bridge id)kSecAttrAccessible], + (__bridge id)kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly); + XCTAssertEqualObjects(query[(__bridge id)kSecAttrSynchronizable], @NO); +} + +- (void)testAttributesNeedMigration_returnsNoWhenAlreadyDeviceOnlyAndNotSynchronizable { + NSDictionary *attributes = @{ + (__bridge id)kSecAttrAccessible: (__bridge id)kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly, + (__bridge id)kSecAttrSynchronizable: @NO + }; + + XCTAssertFalse([SiftKeychain attributesNeedMigration:attributes]); +} + +- (void)testAttributesNeedMigration_returnsYesWhenAccessibleIsMissingOrNotDeviceOnly { + NSDictionary *missingAccessible = @{ + (__bridge id)kSecAttrSynchronizable: @NO + }; + NSDictionary *legacyAccessible = @{ + (__bridge id)kSecAttrAccessible: (__bridge id)kSecAttrAccessibleWhenUnlocked, + (__bridge id)kSecAttrSynchronizable: @NO + }; + + XCTAssertTrue([SiftKeychain attributesNeedMigration:missingAccessible]); + XCTAssertTrue([SiftKeychain attributesNeedMigration:legacyAccessible]); +} + +- (void)testAttributesNeedMigration_returnsYesWhenSynchronizable { + NSDictionary *attributes = @{ + (__bridge id)kSecAttrAccessible: (__bridge id)kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly, + (__bridge id)kSecAttrSynchronizable: @YES + }; + + XCTAssertTrue([SiftKeychain attributesNeedMigration:attributes]); +} + // MARK: Mocks + (NSString *)mockNilStoredDeviceIFV {