diff --git a/Examples/Example-macOS/Example-macOS.entitlements b/Examples/Example-macOS/Example-macOS.entitlements new file mode 100644 index 00000000..92a2b454 --- /dev/null +++ b/Examples/Example-macOS/Example-macOS.entitlements @@ -0,0 +1,10 @@ + + + + + keychain-access-groups + + $(AppIdentifierPrefix)com.example.GTMAppAuth.Example-macOS.test-group + + + diff --git a/Examples/Example-macOS/Example-macOS.xcodeproj/project.pbxproj b/Examples/Example-macOS/Example-macOS.xcodeproj/project.pbxproj index 32b944ef..f7586115 100644 --- a/Examples/Example-macOS/Example-macOS.xcodeproj/project.pbxproj +++ b/Examples/Example-macOS/Example-macOS.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 52; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ @@ -21,6 +21,7 @@ /* Begin PBXFileReference section */ 7355833228AFFC0B00AC24DC /* GTMAppAuth */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = GTMAppAuth; path = ../..; sourceTree = ""; }; + 73B03D4C2B5EF66300EEA5DC /* Example-macOS.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "Example-macOS.entitlements"; sourceTree = ""; }; C1AF3AE62818785B003BAEFF /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; F6016E701D2AC11F003497D7 /* Example-macOS.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Example-macOS.app"; sourceTree = BUILT_PRODUCTS_DIR; }; F6016E8B1D2BD988003497D7 /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = AppDelegate.m; path = Source/AppDelegate.m; sourceTree = ""; }; @@ -66,6 +67,7 @@ F6016E671D2AC11E003497D7 = { isa = PBXGroup; children = ( + 73B03D4C2B5EF66300EEA5DC /* Example-macOS.entitlements */, 7355833128AFFC0B00AC24DC /* Packages */, C1AF3AE62818785B003BAEFF /* README.md */, F6016E8A1D2BD973003497D7 /* Source */, @@ -292,7 +294,11 @@ isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_ENTITLEMENTS = "Example-macOS.entitlements"; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; + DEVELOPMENT_TEAM = ""; INFOPLIST_FILE = "$(SRCROOT)/Source/Info.plist"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -300,6 +306,7 @@ ); PRODUCT_BUNDLE_IDENTIFIER = "com.example.GTMAppAuth.Example-macOS"; PRODUCT_NAME = "Example-macOS"; + PROVISIONING_PROFILE_SPECIFIER = ""; }; name = Debug; }; @@ -307,7 +314,11 @@ isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_ENTITLEMENTS = "Example-macOS.entitlements"; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; + DEVELOPMENT_TEAM = ""; INFOPLIST_FILE = "$(SRCROOT)/Source/Info.plist"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -315,6 +326,7 @@ ); PRODUCT_BUNDLE_IDENTIFIER = "com.example.GTMAppAuth.Example-macOS"; PRODUCT_NAME = "Example-macOS"; + PROVISIONING_PROFILE_SPECIFIER = ""; }; name = Release; }; diff --git a/Examples/Example-macOS/Source/GTMAppAuthExampleViewController.m b/Examples/Example-macOS/Source/GTMAppAuthExampleViewController.m index cad65ca9..acee3ead 100644 --- a/Examples/Example-macOS/Source/GTMAppAuthExampleViewController.m +++ b/Examples/Example-macOS/Source/GTMAppAuthExampleViewController.m @@ -30,6 +30,18 @@ #import "AppDelegate.h" +/*! @brief The bundle ID will use in constructing the app group string for keychain queries. + @discussion The string here is a combination of this example app's bundle ID and the keychain + access group name added in the app's entitlements file. + */ +static NSString *kBundleIDAccessGroup = @"com.example.GTMAppAuth.Example-macOS.test-group"; + +/*! @brief The team ID you will use in constructing the app group string for keychain queries. + @discussion The team ID you will use can be found in your developer team profile page on + developer.apple.com. + */ +static NSString *const kTeamIDPrefix = @"YOUR_TEAM_ID"; + /*! @brief The OIDC issuer from which the configuration will be discovered. */ static NSString *const kIssuer = @"https://accounts.google.com"; @@ -65,9 +77,17 @@ @implementation GTMAppAuthExampleViewController - (void)viewDidLoad { [super viewDidLoad]; - self.keychainStore = [[GTMKeychainStore alloc] initWithItemName:kExampleAuthorizerKey]; + GTMKeychainAttribute *dataProtection = [GTMKeychainAttribute useDataProtectionKeychain]; + NSString *testGroup = [NSString stringWithFormat:@"%@.%@", kTeamIDPrefix, kBundleIDAccessGroup]; + GTMKeychainAttribute *accessGroup = [GTMKeychainAttribute keychainAccessGroupWithName:testGroup]; + NSSet *attributes = [NSSet setWithArray:@[dataProtection, accessGroup]]; + self.keychainStore = [[GTMKeychainStore alloc] initWithItemName:kExampleAuthorizerKey + keychainAttributes:attributes]; #if !defined(NS_BLOCK_ASSERTIONS) + NSAssert(![kTeamIDPrefix isEqualToString:@"YOUR_TEAM_ID"], + @"Update kTeamIDPrefix with your own team ID."); + // NOTE: // // To run this sample, you need to register your own Google API client at diff --git a/GTMAppAuth/Sources/KeychainStore/KeychainAttribute.swift b/GTMAppAuth/Sources/KeychainStore/KeychainAttribute.swift index bfc1d243..634aefbb 100644 --- a/GTMAppAuth/Sources/KeychainStore/KeychainAttribute.swift +++ b/GTMAppAuth/Sources/KeychainStore/KeychainAttribute.swift @@ -24,6 +24,7 @@ public final class KeychainAttribute: NSObject { /// Indicates whether to treat macOS keychain items like iOS keychain items. /// /// This attribute will set `kSecUseDataProtectionKeychain` as true in the Keychain query. + @available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) case useDataProtectionKeychain /// The `String` name for the access group to use in the Keychain query. case accessGroup(String) @@ -32,9 +33,13 @@ public final class KeychainAttribute: NSObject { public var keyName: String { switch self { case .useDataProtectionKeychain: - return "kSecUseDataProtectionKeychain" + if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) { + return kSecUseDataProtectionKeychain as String + } else { + fatalError("`KeychainAttribute.Attribute.useDataProtectionKeychain is only available on macOS 10.15 and greater") + } case .accessGroup: - return "kSecKeychainAccessGroup" + return kSecAttrAccessGroup as String } } } @@ -53,7 +58,7 @@ public final class KeychainAttribute: NSObject { /// Creates an instance of `KeychainAttribute` whose attribute is set to /// `.useDataProtectionKeychain`. /// - Returns: An instance of `KeychainAttribute`. - @available(macOS 10.15, *) + @available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) @objc public static let useDataProtectionKeychain = KeychainAttribute( attribute: .useDataProtectionKeychain ) diff --git a/GTMAppAuth/Sources/KeychainStore/KeychainHelper.swift b/GTMAppAuth/Sources/KeychainStore/KeychainHelper.swift index 1017745e..6b9425c8 100644 --- a/GTMAppAuth/Sources/KeychainStore/KeychainHelper.swift +++ b/GTMAppAuth/Sources/KeychainStore/KeychainHelper.swift @@ -26,6 +26,7 @@ public protocol KeychainHelper { func password(forService service: String) throws -> String func passwordData(forService service: String) throws -> Data func removePassword(forService service: String) throws + func setPassword(_ password: String, forService service: String) throws func setPassword(_ password: String, forService service: String, accessibility: CFTypeRef) throws func setPassword(data: Data, forService service: String, accessibility: CFTypeRef?) throws } @@ -34,11 +35,6 @@ public protocol KeychainHelper { final class KeychainWrapper: KeychainHelper { let accountName = "OAuth" let keychainAttributes: Set - @available(macOS 10.15, *) - private var isMaxMacOSVersionGreaterThanTenOneFive: Bool { - let tenOneFive = OperatingSystemVersion(majorVersion: 10, minorVersion: 15, patchVersion: 0) - return ProcessInfo().isOperatingSystemAtLeast(tenOneFive) - } init(keychainAttributes: Set = []) { self.keychainAttributes = keychainAttributes @@ -54,11 +50,7 @@ final class KeychainWrapper: KeychainHelper { keychainAttributes.forEach { configuration in switch configuration.attribute { case .useDataProtectionKeychain: -#if os(macOS) && isMaxMacOSVersionGreaterThanTenOneFive - if #available(macOS 10.15, *) { - query[configuration.attribute.keyName] = kCFBooleanTrue - } -#endif + query[configuration.attribute.keyName] = kCFBooleanTrue case .accessGroup(let name): query[configuration.attribute.keyName] = name } @@ -109,6 +101,11 @@ final class KeychainWrapper: KeychainHelper { throw KeychainStore.Error.failedToDeletePassword(forItemName: service) } } + + func setPassword(_ password: String, forService service: String) throws { + let passwordData = Data(password.utf8) + try setPassword(data: passwordData, forService: service, accessibility: nil) + } func setPassword( _ password: String, diff --git a/GTMAppAuth/Sources/KeychainStore/KeychainStore.swift b/GTMAppAuth/Sources/KeychainStore/KeychainStore.swift index 63af95af..885fdadf 100644 --- a/GTMAppAuth/Sources/KeychainStore/KeychainStore.swift +++ b/GTMAppAuth/Sources/KeychainStore/KeychainStore.swift @@ -62,10 +62,11 @@ public final class KeychainStore: NSObject, AuthSessionStore { /// - Parameters: /// - itemName: The `String` name for the credential to store in the keychain. /// - keychainHelper: An instance conforming to `KeychainHelper`. + /// - Note: The `KeychainHelper`'s `keychainAttributes` are used if present. @objc public convenience init(itemName: String, keychainHelper: KeychainHelper) { self.init( itemName: itemName, - keychainAttributes: [], + keychainAttributes: keychainHelper.keychainAttributes, keychainHelper: keychainHelper ) } @@ -100,12 +101,7 @@ public final class KeychainStore: NSObject, AuthSessionStore { @objc(saveAuthSession:error:) public func save(authSession: AuthSession) throws { - let authSessionData: Data = try authSessionData(fromAuthSession: authSession) - try keychainHelper.setPassword( - data: authSessionData, - forService: itemName, - accessibility: kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly - ) + try save(authSession: authSession, withItemName: itemName) } /// Saves the provided `AuthSession` using the provided item name. @@ -118,10 +114,22 @@ public final class KeychainStore: NSObject, AuthSessionStore { @objc(saveAuthSession:withItemName:error:) public func save(authSession: AuthSession, withItemName itemName: String) throws { let authSessionData = try authSessionData(fromAuthSession: authSession) + + var maybeAccessibility: CFString? = kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly + if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) { + // On macOS, we must use `kSecUseDataProtectionKeychain` if using `kSecAttrAccessible` + // (https://developer.apple.com/documentation/security/ksecattraccessible?language=objc) +#if os(macOS) + if !keychainAttributes.contains(.useDataProtectionKeychain) { + maybeAccessibility = nil + } +#endif + } + try keychainHelper.setPassword( data: authSessionData, forService: itemName, - accessibility: kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly + accessibility: maybeAccessibility ) } @@ -268,6 +276,18 @@ public final class KeychainStore: NSObject, AuthSessionStore { .persistenceResponseString(forAuthSession: authSession) else { throw KeychainStore.Error.failedToCreateResponseStringFromAuthSession(authSession) } + + if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) { + // On macOS, we must use `kSecUseDataProtectionKeychain` if using `kSecAttrAccessible` + // (https://developer.apple.com/documentation/security/ksecattraccessible?language=objc) +#if os(macOS) + if !keychainAttributes.contains(.useDataProtectionKeychain) { + try keychainHelper.setPassword(persistence, forService: itemName) + return + } +#endif + } + try keychainHelper.setPassword( persistence, forService: itemName, diff --git a/GTMAppAuth/Tests/Helpers/GTMAppAuthTestingHelpers.swift b/GTMAppAuth/Tests/Helpers/GTMAppAuthTestingHelpers.swift index 71421f73..1008cc57 100644 --- a/GTMAppAuth/Tests/Helpers/GTMAppAuthTestingHelpers.swift +++ b/GTMAppAuth/Tests/Helpers/GTMAppAuthTestingHelpers.swift @@ -26,6 +26,7 @@ public protocol Testing { @objc(GTMTestingConstants) public class TestingConstants: NSObject { + @objc public static let testAccessGroup = "testAccessGroup" @objc public static let testAccessToken = "access_token" @objc public static let accessTokenExpiresIn = 3600 @objc public static let testRefreshToken = "refresh_token" diff --git a/GTMAppAuth/Tests/Helpers/KeychainHelperFake.swift b/GTMAppAuth/Tests/Helpers/KeychainHelperFake.swift index 6a7cc87a..7a17da94 100644 --- a/GTMAppAuth/Tests/Helpers/KeychainHelperFake.swift +++ b/GTMAppAuth/Tests/Helpers/KeychainHelperFake.swift @@ -88,6 +88,21 @@ public class KeychainHelperFake: NSObject, KeychainHelper { } } + @objc public func setPassword(_ password: String, forService service: String) throws { + do { + try removePassword(forService: service) + } catch KeychainStore.Error.failedToDeletePasswordBecauseItemNotFound { + // No need to throw this error since we are setting a new password + } catch { + throw error + } + + guard let passwordData = password.data(using: .utf8) else { + throw KeychainStore.Error.unexpectedPasswordData(forItemName: service) + } + try setPassword(data: passwordData, forService: service, accessibility: nil) + } + @objc public func setPassword( _ password: String, forService service: String, @@ -104,7 +119,7 @@ public class KeychainHelperFake: NSObject, KeychainHelper { guard let passwordData = password.data(using: .utf8) else { throw KeychainStore.Error.unexpectedPasswordData(forItemName: service) } - try setPassword(data: passwordData, forService: service, accessibility: nil) + try setPassword(data: passwordData, forService: service, accessibility: accessibility) } @objc public func setPassword( diff --git a/GTMAppAuth/Tests/Unit/KeychainStore/GTMOAuth2CompatibilityTests.swift b/GTMAppAuth/Tests/Unit/KeychainStore/GTMOAuth2CompatibilityTests.swift index 49dfb521..5245d9f5 100644 --- a/GTMAppAuth/Tests/Unit/KeychainStore/GTMOAuth2CompatibilityTests.swift +++ b/GTMAppAuth/Tests/Unit/KeychainStore/GTMOAuth2CompatibilityTests.swift @@ -29,6 +29,20 @@ class GTMOAuth2CompatibilityTests: XCTestCase { private lazy var testPersistenceString: String = { return "access_token=\(TestingConstants.testAccessToken)&refresh_token=\(TestingConstants.testRefreshToken)&scope=\(TestingConstants.testScope2)&serviceProvider=\(TestingConstants.testServiceProvider)&userEmail=foo%40foo.com&userEmailIsVerified=y&userID=\(TestingConstants.testUserID)" }() + private lazy var keychainStoreWithAttributes: KeychainStore = { + let attributes: Set + if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) { + attributes = [.useDataProtectionKeychain, + .keychainAccessGroup(name: TestingConstants.testAccessGroup)] + } else { + attributes = [.keychainAccessGroup(name: TestingConstants.testAccessGroup)] + } + let keychainHelperWithAttributes = KeychainHelperFake(keychainAttributes: attributes) + return KeychainStore( + itemName: TestingConstants.testKeychainItemName, + keychainHelper: keychainHelperWithAttributes + ) + }() private let keychainHelper = KeychainHelperFake(keychainAttributes: []) private lazy var keychainStore: KeychainStore = { return KeychainStore( @@ -63,6 +77,9 @@ class GTMOAuth2CompatibilityTests: XCTestCase { func testSaveOAuth2AuthSession() throws { try keychainStore.saveWithGTMOAuth2Format(forAuthSession: expectedAuthSession) + // Save with keychain attributes to simulate macOS environment with + // `kSecUseDataProtectionKeychain` + try keychainStoreWithAttributes.saveWithGTMOAuth2Format(forAuthSession: expectedAuthSession) } func testSaveGTMOAuth2AuthSessionThrowsError() { @@ -80,7 +97,34 @@ class GTMOAuth2CompatibilityTests: XCTestCase { func testRemoveOAuth2AuthSession() throws { try keychainStore.saveWithGTMOAuth2Format(forAuthSession: expectedAuthSession) + let _ = try keychainStore.retrieveAuthSessionInGTMOAuth2Format( + tokenURL: TestingConstants.testTokenURL, + redirectURI: TestingConstants.testRedirectURI, + clientID: TestingConstants.testClientID, + clientSecret: TestingConstants.testClientSecret + ) try keychainStore.removeAuthSession() + XCTAssertThrowsError(try keychainStore.retrieveAuthSession()) { thrownError in + XCTAssertEqual( + thrownError as? KeychainStore.Error, + KeychainStore.Error.passwordNotFound(forItemName: TestingConstants.testKeychainItemName) + ) + } + + try keychainStoreWithAttributes.saveWithGTMOAuth2Format(forAuthSession: expectedAuthSession) + let _ = try keychainStoreWithAttributes.retrieveAuthSessionInGTMOAuth2Format( + tokenURL: TestingConstants.testTokenURL, + redirectURI: TestingConstants.testRedirectURI, + clientID: TestingConstants.testClientID, + clientSecret: TestingConstants.testClientSecret + ) + try keychainStoreWithAttributes.removeAuthSession() + XCTAssertThrowsError(try keychainStoreWithAttributes.retrieveAuthSession()) { thrownError in + XCTAssertEqual( + thrownError as? KeychainStore.Error, + KeychainStore.Error.passwordNotFound(forItemName: TestingConstants.testKeychainItemName) + ) + } } func testRemoveOAuth2AuthSessionhrowsError() { @@ -116,6 +160,29 @@ class GTMOAuth2CompatibilityTests: XCTestCase { XCTAssertEqual(authSession.userEmailIsVerified, expectedAuthSession.userEmailIsVerified) XCTAssertEqual(authSession.canAuthorize, expectedAuthSession.canAuthorize) } + + func testAuthSessionFromKeychainWithAttributesForName() throws { + try keychainStoreWithAttributes.saveWithGTMOAuth2Format(forAuthSession: expectedAuthSession) + let authSession = try keychainStoreWithAttributes.retrieveAuthSessionInGTMOAuth2Format( + tokenURL: TestingConstants.testTokenURL, + redirectURI: TestingConstants.testRedirectURI, + clientID: TestingConstants.testClientID, + clientSecret: TestingConstants.testClientID + ) + + XCTAssertEqual(authSession.authState.scope, expectedAuthSession.authState.scope) + XCTAssertEqual( + authSession.authState.lastTokenResponse?.accessToken, + expectedAuthSession.authState.lastTokenResponse?.accessToken + ) + XCTAssertEqual(authSession.authState.refreshToken, expectedAuthSession.authState.refreshToken) + XCTAssertEqual(authSession.authState.isAuthorized, expectedAuthSession.authState.isAuthorized) + XCTAssertEqual(authSession.serviceProvider, expectedAuthSession.serviceProvider) + XCTAssertEqual(authSession.userID, expectedAuthSession.userID) + XCTAssertEqual(authSession.userEmail, expectedAuthSession.userEmail) + XCTAssertEqual(authSession.userEmailIsVerified, expectedAuthSession.userEmailIsVerified) + XCTAssertEqual(authSession.canAuthorize, expectedAuthSession.canAuthorize) + } func testAuthSessionFromKeychainForNameThrowsError() throws { try keychainStore.saveWithGTMOAuth2Format(forAuthSession: expectedAuthSession) diff --git a/GTMAppAuth/Tests/Unit/KeychainStore/KeychainStoreTests.swift b/GTMAppAuth/Tests/Unit/KeychainStore/KeychainStoreTests.swift index 4b621884..5043d053 100644 --- a/GTMAppAuth/Tests/Unit/KeychainStore/KeychainStoreTests.swift +++ b/GTMAppAuth/Tests/Unit/KeychainStore/KeychainStoreTests.swift @@ -48,8 +48,41 @@ class KeychainStoreTests: XCTestCase { keychainHelper.passwordStore.removeAll() keychainHelper.generatedKeychainQuery = nil } + + @available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) + func testKeychainAttributeKeysHaveCorrectNames() throws { + let expectedAccessGroup = "unit-test-group" + let attributes: Set = [ + KeychainAttribute.useDataProtectionKeychain, + KeychainAttribute.keychainAccessGroup(name: expectedAccessGroup) + ] + let keychainHelperFake = KeychainHelperFake(keychainAttributes: attributes) + let store = KeychainStore( + itemName: TestingConstants.testKeychainItemName, + keychainHelper: keychainHelperFake + ) + + try store.save(authSession: authSession) + guard let testQuery = keychainHelperFake.generatedKeychainQuery as? [String: AnyHashable] else { + XCTFail("`keychainHelperFake` missing keychain query attributes") + return + } + + let comparisonQuery = comparisonKeychainQuery( + withAttributes: attributes, + accountName: keychainHelperFake.accountName, + service: TestingConstants.testKeychainItemName + ) + XCTAssertEqual(testQuery, comparisonQuery) + XCTAssertNotNil(testQuery[kSecUseDataProtectionKeychain as String]) + guard let testAccessGroup = testQuery[kSecAttrAccessGroup as String] as? String else { + XCTFail("`testQuery` should have a keychain access group") + return + } + XCTAssertEqual(testAccessGroup, expectedAccessGroup) + } - @available(macOS 10.15, *) + @available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) func testKeychainQueryHasDataProtectionAttributeOnSave() throws { let useDataProtectionAttributeSet: Set = [.useDataProtectionKeychain] let fakeWithDataProtection = KeychainHelperFake( @@ -109,7 +142,7 @@ class KeychainStoreTests: XCTestCase { XCTAssertEqual(expectedGroupName, testGroupName) } - @available(macOS 10.15, *) + @available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) func testKeychainQueryHasDataProtectionAndAccessGroupAttributesOnSave() throws { let expectedGroupName = "testGroup" let accessGroupAttributeSet: Set = [ @@ -154,7 +187,7 @@ class KeychainStoreTests: XCTestCase { XCTAssertEqual(testAccessGroupName, expectedGroupName) } - @available(macOS 10.15, *) + @available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) func testKeychainQueryHasDataProtectionAttributeOnRead() throws { let useDataProtectionAttributeSet: Set = [.useDataProtectionKeychain] let fakeWithDataProtection = KeychainHelperFake( @@ -216,7 +249,7 @@ class KeychainStoreTests: XCTestCase { XCTAssertEqual(expectedGroupName, testGroupName) } - @available(macOS 10.15, *) + @available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) func testKeychainQueryHasDataProtectionAndAccessGroupAttributesOnRead() throws { let expectedGroupName = "testGroup" let accessGroupAttributeSet: Set = [ @@ -262,7 +295,7 @@ class KeychainStoreTests: XCTestCase { XCTAssertEqual(testAccessGroupName, expectedGroupName) } - @available(macOS 10.15, *) + @available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) func testKeychainQueryHasDataProtectionAttributeOnRemove() throws { let useDataProtectionAttributeSet: Set = [.useDataProtectionKeychain] let fakeWithDataProtection = KeychainHelperFake( @@ -324,7 +357,7 @@ class KeychainStoreTests: XCTestCase { XCTAssertEqual(expectedGroupName, testGroupName) } - @available(macOS 10.15, *) + @available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) func testKeychainQueryHasDataProtectionAndAccessGroupAttributesOnRemove() throws { let expectedGroupName = "testGroup" let accessGroupAttributeSet: Set = [