diff --git a/Amplify/Categories/Auth/Models/AccessGroup.swift b/Amplify/Categories/Auth/Models/AccessGroup.swift new file mode 100644 index 0000000000..7fc1ebb3c6 --- /dev/null +++ b/Amplify/Categories/Auth/Models/AccessGroup.swift @@ -0,0 +1,30 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import Foundation + +public struct AccessGroup { + public let name: String? + public let migrateKeychainItems: Bool + + public init(name: String, migrateKeychainItemsOfUserSession: Bool = false) { + self.init(name: name, migrateKeychainItems: migrateKeychainItemsOfUserSession) + } + + public static func none(migrateKeychainItemsOfUserSession: Bool) -> AccessGroup { + return .init(name: nil, migrateKeychainItems: migrateKeychainItemsOfUserSession) + } + + public static var none: AccessGroup { + return .none(migrateKeychainItemsOfUserSession: false) + } + + private init(name: String?, migrateKeychainItems: Bool) { + self.name = name + self.migrateKeychainItems = migrateKeychainItems + } +} diff --git a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/AWSCognitoAuthPlugin+Configure.swift b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/AWSCognitoAuthPlugin+Configure.swift index fdcfbf9385..ee8bcaf58f 100644 --- a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/AWSCognitoAuthPlugin+Configure.swift +++ b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/AWSCognitoAuthPlugin+Configure.swift @@ -177,7 +177,8 @@ extension AWSCognitoAuthPlugin { } private func makeCredentialStore() -> AmplifyAuthCredentialStoreBehavior { - AWSCognitoAuthCredentialStore(authConfiguration: authConfiguration) + return AWSCognitoAuthCredentialStore(authConfiguration: authConfiguration, accessGroup: secureStoragePreferences?.accessGroup?.name, + migrateKeychainItemsOfUserSession: secureStoragePreferences?.accessGroup?.migrateKeychainItems ?? false) } private func makeLegacyKeychainStore(service: String) -> KeychainStoreBehavior { diff --git a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/AWSCognitoAuthPlugin.swift b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/AWSCognitoAuthPlugin.swift index f32209ba9c..857651ffc3 100644 --- a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/AWSCognitoAuthPlugin.swift +++ b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/AWSCognitoAuthPlugin.swift @@ -35,6 +35,9 @@ public final class AWSCognitoAuthPlugin: AWSCognitoAuthPluginBehavior { /// The user network preferences for timeout and retry let networkPreferences: AWSCognitoNetworkPreferences? + /// The user secure storage preferences for access group + let secureStoragePreferences: AWSCognitoSecureStoragePreferences? + @_spi(InternalAmplifyConfiguration) internal(set) public var jsonConfiguration: JSONValue? @@ -43,15 +46,14 @@ public final class AWSCognitoAuthPlugin: AWSCognitoAuthPluginBehavior { return "awsCognitoAuthPlugin" } - /// Instantiates an instance of the AWSCognitoAuthPlugin. - public init() { - self.networkPreferences = nil - } - - /// Instantiates an instance of the AWSCognitoAuthPlugin with custom network preferences + /// Instantiates an instance of the AWSCognitoAuthPlugin with optionally custom network + /// preferences and custom secure storage preferences /// - Parameters: /// - networkPreferences: network preferences - public init(networkPreferences: AWSCognitoNetworkPreferences) { + /// - secureStoragePreferences: secure storage preferences + public init(networkPreferences: AWSCognitoNetworkPreferences? = nil, + secureStoragePreferences: AWSCognitoSecureStoragePreferences = AWSCognitoSecureStoragePreferences()) { self.networkPreferences = networkPreferences + self.secureStoragePreferences = secureStoragePreferences } } diff --git a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/CredentialStorage/AWSCognitoAuthCredentialStore.swift b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/CredentialStorage/AWSCognitoAuthCredentialStore.swift index 3bb2a2e1bb..a5666de8d7 100644 --- a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/CredentialStorage/AWSCognitoAuthCredentialStore.swift +++ b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/CredentialStorage/AWSCognitoAuthCredentialStore.swift @@ -13,6 +13,7 @@ struct AWSCognitoAuthCredentialStore { // Credential store constants private let service = "com.amplify.awsCognitoAuthPlugin" + private let sharedService = "com.amplify.awsCognitoAuthPluginShared" private let sessionKey = "session" private let deviceMetadataKey = "deviceMetadata" private let deviceASFKey = "deviceASF" @@ -25,14 +26,29 @@ struct AWSCognitoAuthCredentialStore { private var isKeychainConfiguredKey: String { "\(userDefaultsNameSpace).isKeychainConfigured" } + private var accessGroupKey: String { + "\(userDefaultsNameSpace).accessGroup" + } private let authConfiguration: AuthConfiguration private let keychain: KeychainStoreBehavior private let userDefaults = UserDefaults.standard + private let accessGroup: String? - init(authConfiguration: AuthConfiguration, accessGroup: String? = nil) { + init(authConfiguration: AuthConfiguration, accessGroup: String? = nil, migrateKeychainItemsOfUserSession: Bool = false) { self.authConfiguration = authConfiguration - self.keychain = KeychainStore(service: service, accessGroup: accessGroup) + self.accessGroup = accessGroup + if let accessGroup { + self.keychain = KeychainStore(service: sharedService, accessGroup: accessGroup) + } else { + self.keychain = KeychainStore(service: service) + } + + if migrateKeychainItemsOfUserSession { + try? migrateKeychainItemsToAccessGroup() + } + + try? saveStoredAccessGroup() if !userDefaults.bool(forKey: isKeychainConfiguredKey) { try? clearAllCredentials() @@ -182,6 +198,81 @@ extension AWSCognitoAuthCredentialStore: AmplifyAuthCredentialStoreBehavior { private func clearAllCredentials() throws { try keychain._removeAll() } + + private func retrieveStoredAccessGroup() throws -> String? { + return userDefaults.string(forKey: accessGroupKey) + } + + private func saveStoredAccessGroup() throws { + if let accessGroup { + userDefaults.set(accessGroup, forKey: accessGroupKey) + } else { + userDefaults.removeObject(forKey: accessGroupKey) + } + } + + private func migrateKeychainItemsToAccessGroup() throws { + let oldAccessGroup = try? retrieveStoredAccessGroup() + let oldKeychain: KeychainStoreBehavior + + if oldAccessGroup == accessGroup { + log.verbose("[AWSCognitoAuthCredentialStore] Stored access group is the same as current access group, aborting migration") + return + } + + if let oldAccessGroup { + oldKeychain = KeychainStore(service: sharedService, accessGroup: oldAccessGroup) + } else { + oldKeychain = KeychainStore(service: service) + } + + let authCredentialStoreKey = generateSessionKey(for: authConfiguration) + let authCredentialData: Data + let awsCredential: AmplifyCredentials + do { + authCredentialData = try oldKeychain._getData(authCredentialStoreKey) + awsCredential = try decode(data: authCredentialData) + } catch { + log.verbose("[AWSCognitoAuthCredentialStore] Could not retrieve previous credentials in keychain under old access group, nothing to migrate") + return + } + + guard awsCredential.areValid() else { + log.verbose("[AWSCognitoAuthCredentialStore] Credentials found are not valid (expired) in old access group keychain, aborting migration") + return + } + + let oldItems: [(key: String, value: Data)] + do { + oldItems = try oldKeychain._getAll() + } catch { + log.error("[AWSCognitoAuthCredentialStore] Error getting all items from keychain under old access group, aborting migration") + return + } + + if oldItems.isEmpty { + log.verbose("[AWSCognitoAuthCredentialStore] No items in keychain under old access group, clearing keychain items under new access group") + return + } + + for item in oldItems { + do { + try keychain._set(item.value, key: item.key) + } catch { + log.error("[AWSCognitoAuthCredentialStore] Error migrating one of the items, aborting migration: \(error)") + try? clearAllCredentials() + return + } + } + + do { + try oldKeychain._removeAll() + } catch { + log.error("[AWSCognitoAuthCredentialStore] Error deleting all items from keychain under old access group after migration") + } + + log.verbose("[AWSCognitoAuthCredentialStore] Migration of keychain items from old access group to new access group successful") + } } @@ -205,3 +296,11 @@ private extension AWSCognitoAuthCredentialStore { } } + +extension AWSCognitoAuthCredentialStore: DefaultLogger { + public static var log: Logger { + Amplify.Logging.logger(forNamespace: String(describing: self)) + } + + public nonisolated var log: Logger { Self.log } +} diff --git a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Models/AWSCognitoSecureStoragePreferences.swift b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Models/AWSCognitoSecureStoragePreferences.swift new file mode 100644 index 0000000000..c7dcefe42b --- /dev/null +++ b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Models/AWSCognitoSecureStoragePreferences.swift @@ -0,0 +1,19 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import Foundation +import Amplify + +public struct AWSCognitoSecureStoragePreferences { + + /// The access group that the keychain will use for auth items + public let accessGroup: AccessGroup? + + public init(accessGroup: AccessGroup? = nil) { + self.accessGroup = accessGroup + } +} diff --git a/AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/ActionTests/CredentialStore/MockCredentialStoreBehavior.swift b/AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/ActionTests/CredentialStore/MockCredentialStoreBehavior.swift index 97b9201818..1f00f81df3 100644 --- a/AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/ActionTests/CredentialStore/MockCredentialStoreBehavior.swift +++ b/AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/ActionTests/CredentialStore/MockCredentialStoreBehavior.swift @@ -15,12 +15,15 @@ class MockKeychainStoreBehavior: KeychainStoreBehavior { typealias VoidHandler = () -> Void let data: String + let allData: [(key: String, value: Data)] let removeAllHandler: VoidHandler? + let mockKey: String = "mockKey" init(data: String, removeAllHandler: VoidHandler? = nil) { self.data = data self.removeAllHandler = removeAllHandler + self.allData = [(key: mockKey, value: Data(data.utf8))] } func _getString(_ key: String) throws -> String { @@ -41,4 +44,8 @@ class MockKeychainStoreBehavior: KeychainStoreBehavior { func _removeAll() throws { removeAllHandler?() } + + func _getAll() throws -> [(key: String, value: Data)] { + return allData + } } diff --git a/AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/ConfigurationTests/AWSCognitoAuthPluginAmplifyOutputsConfigTests.swift b/AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/ConfigurationTests/AWSCognitoAuthPluginAmplifyOutputsConfigTests.swift index 85eba053d7..23ecfae901 100644 --- a/AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/ConfigurationTests/AWSCognitoAuthPluginAmplifyOutputsConfigTests.swift +++ b/AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/ConfigurationTests/AWSCognitoAuthPluginAmplifyOutputsConfigTests.swift @@ -123,4 +123,84 @@ class AWSCognitoAuthPluginAmplifyOutputsConfigTests: XCTestCase { XCTFail("Should not throw error. \(error)") } } + + /// Test Auth configuration with valid config for user pool and identity pool, with secure storage preferences + /// + /// - Given: Given valid config for user pool and identity pool with secure storage preferences + /// - When: + /// - I configure auth with the given configuration and secure storage preferences + /// - Then: + /// - I should not get any error while configuring auth + /// + func testConfigWithUserPoolAndIdentityPoolWithSecureStoragePreferences() throws { + let plugin = AWSCognitoAuthPlugin( + secureStoragePreferences: .init( + accessGroup: AccessGroup(name: "xx") + ) + ) + try Amplify.add(plugin: plugin) + + let amplifyConfig = AmplifyOutputsData(auth: .init( + awsRegion: "us-east-1", + userPoolId: "xx", + userPoolClientId: "xx", + identityPoolId: "xx")) + + do { + try Amplify.configure(amplifyConfig) + + let escapeHatch = plugin.getEscapeHatch() + guard case .userPoolAndIdentityPool(let userPoolClient, let identityPoolClient) = escapeHatch else { + XCTFail("Expected .userPool, got \(escapeHatch)") + return + } + XCTAssertNotNil(userPoolClient) + XCTAssertNotNil(identityPoolClient) + + } catch { + XCTFail("Should not throw error. \(error)") + } + } + + /// Test Auth configuration with valid config for user pool and identity pool, with network preferences and secure storage preferences + /// + /// - Given: Given valid config for user pool and identity pool, network preferences, and secure storage preferences + /// - When: + /// - I configure auth with the given configuration, network preferences, and secure storage preferences + /// - Then: + /// - I should not get any error while configuring auth + /// + func testConfigWithUserPoolAndIdentityPoolWithNetworkPreferencesAndSecureStoragePreferences() throws { + let plugin = AWSCognitoAuthPlugin( + networkPreferences: .init( + maxRetryCount: 2, + timeoutIntervalForRequest: 60, + timeoutIntervalForResource: 60), + secureStoragePreferences: .init( + accessGroup: AccessGroup(name: "xx") + ) + ) + try Amplify.add(plugin: plugin) + + let amplifyConfig = AmplifyOutputsData(auth: .init( + awsRegion: "us-east-1", + userPoolId: "xx", + userPoolClientId: "xx", + identityPoolId: "xx")) + + do { + try Amplify.configure(amplifyConfig) + + let escapeHatch = plugin.getEscapeHatch() + guard case .userPoolAndIdentityPool(let userPoolClient, let identityPoolClient) = escapeHatch else { + XCTFail("Expected .userPool, got \(escapeHatch)") + return + } + XCTAssertNotNil(userPoolClient) + XCTAssertNotNil(identityPoolClient) + + } catch { + XCTFail("Should not throw error. \(error)") + } + } } diff --git a/AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/ConfigurationTests/AWSCognitoAuthPluginConfigTests.swift b/AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/ConfigurationTests/AWSCognitoAuthPluginConfigTests.swift index 8ba574028e..04cf1c78f9 100644 --- a/AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/ConfigurationTests/AWSCognitoAuthPluginConfigTests.swift +++ b/AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/ConfigurationTests/AWSCognitoAuthPluginConfigTests.swift @@ -235,5 +235,101 @@ class AWSCognitoAuthPluginConfigTests: XCTestCase { XCTFail("Should not throw error. \(error)") } } + + /// Test Auth configuration with valid config for user pool and identity pool, with secure storage preferences + /// + /// - Given: Given valid config for user pool and identity pool, and secure storage preferences + /// - When: + /// - I configure auth with the given configuration and secure storage preferences + /// - Then: + /// - I should not get any error while configuring auth + /// + func testConfigWithUserPoolAndIdentityPoolWithSecureStoragePreferences() throws { + let plugin = AWSCognitoAuthPlugin( + secureStoragePreferences: .init( + accessGroup: AccessGroup(name: "xx") + ) + ) + try Amplify.add(plugin: plugin) + + let categoryConfig = AuthCategoryConfiguration(plugins: [ + "awsCognitoAuthPlugin": [ + "CredentialsProvider": ["CognitoIdentity": ["Default": + ["PoolId": "xx", + "Region": "us-east-1"] + ]], + "CognitoUserPool": ["Default": [ + "PoolId": "xx", + "Region": "us-east-1", + "AppClientId": "xx", + "AppClientSecret": "xx"]] + ] + ]) + let amplifyConfig = AmplifyConfiguration(auth: categoryConfig) + do { + try Amplify.configure(amplifyConfig) + + let escapeHatch = plugin.getEscapeHatch() + guard case .userPoolAndIdentityPool(let userPoolClient, let identityPoolClient) = escapeHatch else { + XCTFail("Expected .userPool, got \(escapeHatch)") + return + } + XCTAssertNotNil(userPoolClient) + XCTAssertNotNil(identityPoolClient) + + } catch { + XCTFail("Should not throw error. \(error)") + } + } + + /// Test Auth configuration with valid config for user pool and identity pool, with network preferences and secure storage preferences + /// + /// - Given: Given valid config for user pool and identity pool, network preferences, and secure storage preferences + /// - When: + /// - I configure auth with the given configuration, network preferences, and secure storage preferences + /// - Then: + /// - I should not get any error while configuring auth + /// + func testConfigWithUserPoolAndIdentityPoolWithNetworkPreferencesAndSecureStoragePreferences() throws { + let plugin = AWSCognitoAuthPlugin( + networkPreferences: .init( + maxRetryCount: 2, + timeoutIntervalForRequest: 60, + timeoutIntervalForResource: 60), + secureStoragePreferences: .init( + accessGroup: AccessGroup(name: "xx") + ) + ) + try Amplify.add(plugin: plugin) + + let categoryConfig = AuthCategoryConfiguration(plugins: [ + "awsCognitoAuthPlugin": [ + "CredentialsProvider": ["CognitoIdentity": ["Default": + ["PoolId": "xx", + "Region": "us-east-1"] + ]], + "CognitoUserPool": ["Default": [ + "PoolId": "xx", + "Region": "us-east-1", + "AppClientId": "xx", + "AppClientSecret": "xx"]] + ] + ]) + let amplifyConfig = AmplifyConfiguration(auth: categoryConfig) + do { + try Amplify.configure(amplifyConfig) + + let escapeHatch = plugin.getEscapeHatch() + guard case .userPoolAndIdentityPool(let userPoolClient, let identityPoolClient) = escapeHatch else { + XCTFail("Expected .userPool, got \(escapeHatch)") + return + } + XCTAssertNotNil(userPoolClient) + XCTAssertNotNil(identityPoolClient) + + } catch { + XCTFail("Should not throw error. \(error)") + } + } } diff --git a/AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/Support/DefaultConfig.swift b/AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/Support/DefaultConfig.swift index 13e5a8781e..ef876129ae 100644 --- a/AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/Support/DefaultConfig.swift +++ b/AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/Support/DefaultConfig.swift @@ -366,6 +366,10 @@ struct MockLegacyStore: KeychainStoreBehavior { func _removeAll() throws { } + + func _getAll() throws -> [(key: String, value: Data)] { + return [] + } } diff --git a/AmplifyPlugins/Auth/Tests/AuthHostApp/AuthHostApp.xcodeproj/project.pbxproj b/AmplifyPlugins/Auth/Tests/AuthHostApp/AuthHostApp.xcodeproj/project.pbxproj index efe5f198a7..3801340b4e 100644 --- a/AmplifyPlugins/Auth/Tests/AuthHostApp/AuthHostApp.xcodeproj/project.pbxproj +++ b/AmplifyPlugins/Auth/Tests/AuthHostApp/AuthHostApp.xcodeproj/project.pbxproj @@ -216,6 +216,8 @@ B43C26C827BC9D54003F3BF7 /* AuthConfirmSignUpTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthConfirmSignUpTests.swift; sourceTree = ""; }; B43C26C927BC9D54003F3BF7 /* AuthResendSignUpCodeTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthResendSignUpCodeTests.swift; sourceTree = ""; }; B4B9F45628F47B7B004F346F /* amplify-ios */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = "amplify-ios"; path = ../../../..; sourceTree = ""; }; + E2A7D1732C5D76CB00B06999 /* AuthHostApp.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = AuthHostApp.entitlements; sourceTree = ""; }; + E2A7D1742C5D774200B06999 /* AuthWatchApp.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = AuthWatchApp.entitlements; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -303,6 +305,7 @@ 485CB53127B614CE006CCEC7 = { isa = PBXGroup; children = ( + E2A7D1742C5D774200B06999 /* AuthWatchApp.entitlements */, 485CB5C627B62C5C006CCEC7 /* Packages */, 485CB53C27B614CE006CCEC7 /* AuthHostApp */, 485CB5A027B61E04006CCEC7 /* AuthIntegrationTests */, @@ -328,6 +331,7 @@ 485CB53C27B614CE006CCEC7 /* AuthHostApp */ = { isa = PBXGroup; children = ( + E2A7D1732C5D76CB00B06999 /* AuthHostApp.entitlements */, 681DFEA728E747B80000C36A /* AsyncTesting */, 485CB53D27B614CE006CCEC7 /* AuthHostAppApp.swift */, 485CB53F27B614CE006CCEC7 /* ContentView.swift */, @@ -1135,6 +1139,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = AuthHostApp/AuthHostApp.entitlements; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_ASSET_PATHS = "\"AuthHostApp/Preview Content\""; @@ -1168,6 +1173,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = AuthHostApp/AuthHostApp.entitlements; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_ASSET_PATHS = "\"AuthHostApp/Preview Content\""; @@ -1245,6 +1251,7 @@ buildSettings = { ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CODE_SIGN_ENTITLEMENTS = AuthWatchApp.entitlements; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_ASSET_PATHS = ""; @@ -1275,6 +1282,7 @@ buildSettings = { ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CODE_SIGN_ENTITLEMENTS = AuthWatchApp.entitlements; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_ASSET_PATHS = ""; diff --git a/AmplifyPlugins/Auth/Tests/AuthHostApp/AuthHostApp/AuthHostApp.entitlements b/AmplifyPlugins/Auth/Tests/AuthHostApp/AuthHostApp/AuthHostApp.entitlements new file mode 100644 index 0000000000..795ed76cfc --- /dev/null +++ b/AmplifyPlugins/Auth/Tests/AuthHostApp/AuthHostApp/AuthHostApp.entitlements @@ -0,0 +1,11 @@ + + + + + keychain-access-groups + + $(AppIdentifierPrefix)com.aws.amplify.auth.AuthHostAppShared + $(AppIdentifierPrefix)com.aws.amplify.auth.AuthHostAppShared2 + + + diff --git a/AmplifyPlugins/Auth/Tests/AuthHostApp/AuthIntegrationTests/AWSAuthBaseTest.swift b/AmplifyPlugins/Auth/Tests/AuthHostApp/AuthIntegrationTests/AWSAuthBaseTest.swift index 69668eee0d..7c8ca70cf8 100644 --- a/AmplifyPlugins/Auth/Tests/AuthHostApp/AuthIntegrationTests/AWSAuthBaseTest.swift +++ b/AmplifyPlugins/Auth/Tests/AuthHostApp/AuthIntegrationTests/AWSAuthBaseTest.swift @@ -30,6 +30,10 @@ class AWSAuthBaseTest: XCTestCase { var amplifyOutputsFile = "testconfiguration/AWSCognitoAuthPluginIntegrationTests-amplify_outputs" let credentialsFile = "testconfiguration/AWSCognitoAuthPluginIntegrationTests-credentials" + let keychainAccessGroup = "94KV3E626L.com.aws.amplify.auth.AuthHostAppShared" + let keychainAccessGroup2 = "94KV3E626L.com.aws.amplify.auth.AuthHostAppShared2" + let keychainAccessGroupWatch = "W3DRXD72QU.com.amazon.aws.amplify.swift.AuthWatchAppShared" + let keychainAccessGroupWatch2 = "W3DRXD72QU.com.amazon.aws.amplify.swift.AuthWatchAppShared2" var amplifyConfiguration: AmplifyConfiguration! var amplifyOutputs: AmplifyOutputsData! diff --git a/AmplifyPlugins/Auth/Tests/AuthHostApp/AuthIntegrationTests/CredentialStore/CredentialStoreConfigurationTests.swift b/AmplifyPlugins/Auth/Tests/AuthHostApp/AuthIntegrationTests/CredentialStore/CredentialStoreConfigurationTests.swift index 0dd8d34e9f..5eaa8a6a9c 100644 --- a/AmplifyPlugins/Auth/Tests/AuthHostApp/AuthIntegrationTests/CredentialStore/CredentialStoreConfigurationTests.swift +++ b/AmplifyPlugins/Auth/Tests/AuthHostApp/AuthIntegrationTests/CredentialStore/CredentialStoreConfigurationTests.swift @@ -210,4 +210,315 @@ class CredentialStoreConfigurationTests: AWSAuthBaseTest { let credentials = try? newCredentialStore.retrieveCredential() XCTAssertNil(credentials) } + + /// Test migrating to a shared access group keeps credentials + /// + /// - Given: A user registered is configured + /// - When: + /// - The credential store is re-initialized with shared access group and migration set to true + /// - Then: + /// - The old credentials should still persist + /// + func testCredentialsRemainOnMigrationToSharedAccessGroup() { + // Given + let identityId = "identityId" + // Migration only happens if credentials are not expired, hence + // the need for nonimmediate expiration test data + let awsCredentials = AuthAWSCognitoCredentials.nonimmediateExpiryTestData + let initialCognitoCredentials = AmplifyCredentials.userPoolAndIdentityPool( + signedInData: .testData, + identityID: identityId, + credentials: awsCredentials) + let initialAuthConfig = AuthConfiguration.userPoolsAndIdentityPools( + Defaults.makeDefaultUserPoolConfigData(), + Defaults.makeIdentityConfigData()) + let credentialStore = AWSCognitoAuthCredentialStore(authConfiguration: initialAuthConfig) + do { + try credentialStore.saveCredential(initialCognitoCredentials) + } catch { + XCTFail("Unable to save credentials") + } + + // When migrating to shared access group with same configuration + #if os(watchOS) + let newCredentialStore = AWSCognitoAuthCredentialStore(authConfiguration: initialAuthConfig, accessGroup: keychainAccessGroupWatch, migrateKeychainItemsOfUserSession: true) + #else + let newCredentialStore = AWSCognitoAuthCredentialStore(authConfiguration: initialAuthConfig, accessGroup: keychainAccessGroup, migrateKeychainItemsOfUserSession: true) + #endif + + // Then + guard let credentials = try? newCredentialStore.retrieveCredential(), + case .userPoolAndIdentityPool(let retrievedTokens, + let retrievedIdentityID, + let retrievedCredentials) = credentials else { + XCTFail("Unable to retrieve Credentials") + return + } + XCTAssertNotNil(credentials) + XCTAssertNotNil(retrievedTokens) + XCTAssertNotNil(retrievedIdentityID) + XCTAssertNotNil(retrievedCredentials) + XCTAssertEqual(retrievedIdentityID, identityId) + XCTAssertEqual(retrievedCredentials, awsCredentials) + } + + /// Test migrating from a shared access group to an unshared access group keeps credentials + /// + /// - Given: A user registered is configured + /// - When: + /// - The credential store is re-initialized with unshared access group and migration set to true + /// - Then: + /// - The old credentials should still persist + /// + func testCredentialsRemainOnMigrationFromSharedAccessGroup() { + // Given + let identityId = "identityId" + let awsCredentials = AuthAWSCognitoCredentials.nonimmediateExpiryTestData + // Migration only happens if credentials are not expired, hence + // the need for nonimmediate expiration test data + let initialCognitoCredentials = AmplifyCredentials.userPoolAndIdentityPool( + signedInData: .testData, + identityID: identityId, + credentials: awsCredentials) + let initialAuthConfig = AuthConfiguration.userPoolsAndIdentityPools( + Defaults.makeDefaultUserPoolConfigData(), + Defaults.makeIdentityConfigData()) + #if os(watchOS) + let credentialStore = AWSCognitoAuthCredentialStore(authConfiguration: initialAuthConfig, accessGroup: keychainAccessGroupWatch) + #else + let credentialStore = AWSCognitoAuthCredentialStore(authConfiguration: initialAuthConfig, accessGroup: keychainAccessGroup) + #endif + do { + try credentialStore.saveCredential(initialCognitoCredentials) + } catch { + XCTFail("Unable to save credentials") + } + + // When migrating to unshared access group with same configuration + let newCredentialStore = AWSCognitoAuthCredentialStore(authConfiguration: initialAuthConfig, migrateKeychainItemsOfUserSession: true) + + // Then + guard let credentials = try? newCredentialStore.retrieveCredential(), + case .userPoolAndIdentityPool(let retrievedTokens, + let retrievedIdentityID, + let retrievedCredentials) = credentials else { + XCTFail("Unable to retrieve Credentials") + return + } + XCTAssertNotNil(credentials) + XCTAssertNotNil(retrievedTokens) + XCTAssertNotNil(retrievedIdentityID) + XCTAssertNotNil(retrievedCredentials) + XCTAssertEqual(retrievedIdentityID, identityId) + XCTAssertEqual(retrievedCredentials, awsCredentials) + } + + /// Test migrating from a shared access group to another shared access group keeps credentials + /// + /// - Given: A user registered is configured + /// - When: + /// - The credential store is re-initialized with another shared access group and migration set to true + /// - Then: + /// - The old credentials should still persist + /// + func testCredentialsRemainOnMigrationFromSharedAccessGroupToAnotherSharedAccessGroup() { + // Given + let identityId = "identityId" + let awsCredentials = AuthAWSCognitoCredentials.nonimmediateExpiryTestData + // Migration only happens if credentials are not expired, hence + // the need for nonimmediate expiration test data + let initialCognitoCredentials = AmplifyCredentials.userPoolAndIdentityPool( + signedInData: .testData, + identityID: identityId, + credentials: awsCredentials) + let initialAuthConfig = AuthConfiguration.userPoolsAndIdentityPools( + Defaults.makeDefaultUserPoolConfigData(), + Defaults.makeIdentityConfigData()) + #if os(watchOS) + let credentialStore = AWSCognitoAuthCredentialStore(authConfiguration: initialAuthConfig, accessGroup: keychainAccessGroupWatch) + #else + let credentialStore = AWSCognitoAuthCredentialStore(authConfiguration: initialAuthConfig, accessGroup: keychainAccessGroup) + #endif + do { + try credentialStore.saveCredential(initialCognitoCredentials) + } catch { + XCTFail("Unable to save credentials") + } + + // When migrating to another shared access group with same configuration + #if os(watchOS) + let newCredentialStore = AWSCognitoAuthCredentialStore(authConfiguration: initialAuthConfig, accessGroup: keychainAccessGroupWatch2, migrateKeychainItemsOfUserSession: true) + #else + let newCredentialStore = AWSCognitoAuthCredentialStore(authConfiguration: initialAuthConfig, accessGroup: keychainAccessGroup2, migrateKeychainItemsOfUserSession: true) + #endif + + // Then + guard let credentials = try? newCredentialStore.retrieveCredential(), + case .userPoolAndIdentityPool(let retrievedTokens, + let retrievedIdentityID, + let retrievedCredentials) = credentials else { + XCTFail("Unable to retrieve Credentials") + return + } + XCTAssertNotNil(credentials) + XCTAssertNotNil(retrievedTokens) + XCTAssertNotNil(retrievedIdentityID) + XCTAssertNotNil(retrievedCredentials) + XCTAssertEqual(retrievedIdentityID, identityId) + XCTAssertEqual(retrievedCredentials, awsCredentials) + } + + /// Test moving to a shared access group without migration should not keep credentials + /// + /// - Given: A user registered is configured + /// - When: + /// - The credential store is re-initialized with shared access group and migration set to false + /// - Then: + /// - The old credentials should not persist + /// + func testCredentialsDoNotRemainOnNonMigrationToSharedAccessGroup() { + // Given + let identityId = "identityId" + let awsCredentials = AuthAWSCognitoCredentials.nonimmediateExpiryTestData + let initialCognitoCredentials = AmplifyCredentials.userPoolAndIdentityPool( + signedInData: .testData, + identityID: identityId, + credentials: awsCredentials) + let initialAuthConfig = AuthConfiguration.userPoolsAndIdentityPools( + Defaults.makeDefaultUserPoolConfigData(), + Defaults.makeIdentityConfigData()) + let credentialStore = AWSCognitoAuthCredentialStore(authConfiguration: initialAuthConfig) + do { + try credentialStore.saveCredential(initialCognitoCredentials) + } catch { + XCTFail("Unable to save credentials") + } + + // When moving to shared access group with same configuration but without migration + #if os(watchOS) + let newCredentialStore = AWSCognitoAuthCredentialStore(authConfiguration: initialAuthConfig, accessGroup: keychainAccessGroupWatch, migrateKeychainItemsOfUserSession: false) + #else + let newCredentialStore = AWSCognitoAuthCredentialStore(authConfiguration: initialAuthConfig, accessGroup: keychainAccessGroup, migrateKeychainItemsOfUserSession: false) + #endif + + // Then + guard let credentials = try? newCredentialStore.retrieveCredential(), + case .userPoolAndIdentityPool(let retrievedTokens, + let retrievedIdentityID, + let retrievedCredentials) = credentials else { + // Expected + return + } + + // If credentials are present, they should not be the same as those that were not migrated + XCTAssertNotNil(credentials) + XCTAssertNotNil(retrievedTokens) + XCTAssertNotNil(retrievedIdentityID) + XCTAssertNotNil(retrievedCredentials) + XCTAssertNotEqual(retrievedCredentials, awsCredentials) + } + + /// Test moving from a shared access group to an unshared access group without migration should not keep credentials + /// + /// - Given: A user registered is configured + /// - When: + /// - The credential store is re-initialized with unshared access group and migration set to false + /// - Then: + /// - The old credentials should not persist + /// + func testCredentialsDoNotRemainOnNonMigrationFromSharedAccessGroup() { + // Given + let identityId = "identityId" + let awsCredentials = AuthAWSCognitoCredentials.nonimmediateExpiryTestData + let initialCognitoCredentials = AmplifyCredentials.userPoolAndIdentityPool( + signedInData: .testData, + identityID: identityId, + credentials: awsCredentials) + let initialAuthConfig = AuthConfiguration.userPoolsAndIdentityPools( + Defaults.makeDefaultUserPoolConfigData(), + Defaults.makeIdentityConfigData()) + #if os(watchOS) + let credentialStore = AWSCognitoAuthCredentialStore(authConfiguration: initialAuthConfig, accessGroup: keychainAccessGroupWatch) + #else + let credentialStore = AWSCognitoAuthCredentialStore(authConfiguration: initialAuthConfig, accessGroup: keychainAccessGroup) + #endif + do { + try credentialStore.saveCredential(initialCognitoCredentials) + } catch { + XCTFail("Unable to save credentials") + } + + // When moving to unshared access group with same configuration but without migration + let newCredentialStore = AWSCognitoAuthCredentialStore(authConfiguration: initialAuthConfig, migrateKeychainItemsOfUserSession: false) + + // Then + guard let credentials = try? newCredentialStore.retrieveCredential(), + case .userPoolAndIdentityPool(let retrievedTokens, + let retrievedIdentityID, + let retrievedCredentials) = credentials else { + // Expected + return + } + + // If credentials are present, they should not be the same as those that were not migrated + XCTAssertNotNil(credentials) + XCTAssertNotNil(retrievedTokens) + XCTAssertNotNil(retrievedIdentityID) + XCTAssertNotNil(retrievedCredentials) + XCTAssertNotEqual(retrievedCredentials, awsCredentials) + } + + /// Test moving from a shared access group to another shared access group without migration should not keep credentials + /// + /// - Given: A user registered is configured + /// - When: + /// - The credential store is re-initialized with another shared access group and migration set to false + /// - Then: + /// - The old credentials should not persist + /// + func testCredentialsDoNotRemainOnNonMigrationFromSharedAccessGroupToAnotherSharedAccessGroup() { + // Given + let identityId = "identityId" + let awsCredentials = AuthAWSCognitoCredentials.nonimmediateExpiryTestData + let initialCognitoCredentials = AmplifyCredentials.userPoolAndIdentityPool( + signedInData: .testData, + identityID: identityId, + credentials: awsCredentials) + let initialAuthConfig = AuthConfiguration.userPoolsAndIdentityPools( + Defaults.makeDefaultUserPoolConfigData(), + Defaults.makeIdentityConfigData()) + #if os(watchOS) + let credentialStore = AWSCognitoAuthCredentialStore(authConfiguration: initialAuthConfig, accessGroup: keychainAccessGroupWatch) + #else + let credentialStore = AWSCognitoAuthCredentialStore(authConfiguration: initialAuthConfig, accessGroup: keychainAccessGroup) + #endif + do { + try credentialStore.saveCredential(initialCognitoCredentials) + } catch { + XCTFail("Unable to save credentials") + } + + // When moving to another shared access group with same configuration but without migration + #if os(watchOS) + let newCredentialStore = AWSCognitoAuthCredentialStore(authConfiguration: initialAuthConfig, accessGroup: keychainAccessGroupWatch2, migrateKeychainItemsOfUserSession: false) + #else + let newCredentialStore = AWSCognitoAuthCredentialStore(authConfiguration: initialAuthConfig, accessGroup: keychainAccessGroup2, migrateKeychainItemsOfUserSession: false) + #endif + + // Then + guard let credentials = try? newCredentialStore.retrieveCredential(), + case .userPoolAndIdentityPool(let retrievedTokens, + let retrievedIdentityID, + let retrievedCredentials) = credentials else { + // Expected + return + } + + // If credentials are present, they should not be the same as those that were not migrated + XCTAssertNotNil(credentials) + XCTAssertNotNil(retrievedTokens) + XCTAssertNotNil(retrievedIdentityID) + XCTAssertNotNil(retrievedCredentials) + XCTAssertNotEqual(retrievedCredentials, awsCredentials) + } } diff --git a/AmplifyPlugins/Auth/Tests/AuthHostApp/AuthIntegrationTests/Helpers/AuthEnvironmentHelper.swift b/AmplifyPlugins/Auth/Tests/AuthHostApp/AuthIntegrationTests/Helpers/AuthEnvironmentHelper.swift index cf495aa7fb..dbe55e8d23 100644 --- a/AmplifyPlugins/Auth/Tests/AuthHostApp/AuthIntegrationTests/Helpers/AuthEnvironmentHelper.swift +++ b/AmplifyPlugins/Auth/Tests/AuthHostApp/AuthIntegrationTests/Helpers/AuthEnvironmentHelper.swift @@ -40,6 +40,14 @@ extension AuthAWSCognitoCredentials { sessionToken: "xx", expiration: Date()) } + + static var nonimmediateExpiryTestData: AuthAWSCognitoCredentials { + return AuthAWSCognitoCredentials( + accessKeyId: "xx", + secretAccessKey: "xx", + sessionToken: "xx", + expiration: Date() + TimeInterval(200)) + } } extension AWSCognitoUserPoolTokens { diff --git a/AmplifyPlugins/Auth/Tests/AuthHostApp/AuthWatchApp.entitlements b/AmplifyPlugins/Auth/Tests/AuthHostApp/AuthWatchApp.entitlements new file mode 100644 index 0000000000..5658c2919e --- /dev/null +++ b/AmplifyPlugins/Auth/Tests/AuthHostApp/AuthWatchApp.entitlements @@ -0,0 +1,11 @@ + + + + + keychain-access-groups + + $(AppIdentifierPrefix)com.amazon.aws.amplify.swift.AuthWatchAppShared + $(AppIdentifierPrefix)com.amazon.aws.amplify.swift.AuthWatchAppShared2 + + + diff --git a/AmplifyPlugins/Core/AWSPluginsCore/Keychain/KeychainStore.swift b/AmplifyPlugins/Core/AWSPluginsCore/Keychain/KeychainStore.swift index 6327676c83..de534d7b67 100644 --- a/AmplifyPlugins/Core/AWSPluginsCore/Keychain/KeychainStore.swift +++ b/AmplifyPlugins/Core/AWSPluginsCore/Keychain/KeychainStore.swift @@ -52,6 +52,11 @@ public protocol KeychainStoreBehavior { /// Removes all key-value pair in the Keychain. /// This System Programming Interface (SPI) may have breaking changes in future updates. func _removeAll() throws + + @_spi(KeychainStore) + /// Retrieves all key-value pairs in the keychain + /// This System Programming Interface (SPI) may have breaking changes in future updates. + func _getAll() throws -> [(key: String, value: Data)] } public struct KeychainStore: KeychainStoreBehavior { @@ -70,14 +75,13 @@ public struct KeychainStore: KeychainStoreBehavior { } public init(service: String) { - self.init(service: service, accessGroup: nil) + attributes = KeychainStoreAttributes(service: service) + log.verbose("[KeychainStore] Initialized keychain with service=\(service), attributes=\(attributes), accessGroup=") } public init(service: String, accessGroup: String? = nil) { - var attributes = KeychainStoreAttributes(service: service) - attributes.accessGroup = accessGroup - self.init(attributes: attributes) - log.verbose("[KeychainStore] Initialized keychain with service=\(service), attributes=\(attributes), accessGroup=\(accessGroup ?? "")") + attributes = KeychainStoreAttributes(service: service, accessGroup: accessGroup) + log.verbose("[KeychainStore] Initialized keychain with service=\(service), attributes=\(attributes), accessGroup=\(attributes.accessGroup ?? "")") } @_spi(KeychainStore) @@ -206,7 +210,7 @@ public struct KeychainStore: KeychainStoreBehavior { let status = SecItemDelete(query as CFDictionary) if status != errSecSuccess && status != errSecItemNotFound { - log.error("[KeychainStore] Error removing itms from keychain with status=\(status)") + log.error("[KeychainStore] Error removing items from keychain with status=\(status)") throw KeychainStoreError.securityError(status) } log.verbose("[KeychainStore] Successfully removed item from keychain") @@ -229,6 +233,48 @@ public struct KeychainStore: KeychainStoreBehavior { } log.verbose("[KeychainStore] Successfully removed all items from keychain") } + + @_spi(KeychainStore) + /// Retrieves all key-value pairs in the keychain + /// This System Programming Interface (SPI) may have breaking changes in future updates. + public func _getAll() throws -> [(key: String, value: Data)] { + log.verbose("[KeychainStore] Starting to retrieve all items from keychain") + var query = attributes.defaultGetQuery() + query[Constants.MatchLimit] = Constants.MatchLimitAll + query[Constants.ReturnData] = kCFBooleanTrue + query[Constants.ReturnAttributes] = kCFBooleanTrue + query[Constants.ReturnRef] = kCFBooleanTrue + + var result: AnyObject? + let status = SecItemCopyMatching(query as CFDictionary, &result) + + switch status { + case errSecSuccess: + guard let items = result as? [[String: Any]] else { + log.error("[KeychainStore] The keychain items retrieved are not the correct type") + throw KeychainStoreError.unknown("The keychain items retrieved are not the correct type") + } + + var keyValuePairs = [(key: String, value: Data)]() + for item in items { + guard let key = item[Constants.AttributeAccount] as? String, + let value = item[Constants.ValueData] as? Data else { + log.error("[KeychainStore] Unable to retrieve key or value from keychain item") + continue + } + keyValuePairs.append((key: key, value: value)) + } + + log.verbose("[KeychainStore] Successfully retrieved \(keyValuePairs.count) items from keychain") + return keyValuePairs + case errSecItemNotFound: + log.verbose("[KeychainStore] No items found in keychain") + return [] + default: + log.error("[KeychainStore] Error of status=\(status) occurred when attempting to retrieve all items from keychain") + throw KeychainStoreError.securityError(status) + } + } } @@ -258,6 +304,7 @@ extension KeychainStore { /** Return Type Key Constants */ static let ReturnData = String(kSecReturnData) static let ReturnAttributes = String(kSecReturnAttributes) + static let ReturnRef = String(kSecReturnRef) /** Value Type Key Constants */ static let ValueData = String(kSecValueData) diff --git a/AmplifyPlugins/Core/AWSPluginsCore/Keychain/KeychainStoreAttributes.swift b/AmplifyPlugins/Core/AWSPluginsCore/Keychain/KeychainStoreAttributes.swift index a638b2879b..bbbab1d275 100644 --- a/AmplifyPlugins/Core/AWSPluginsCore/Keychain/KeychainStoreAttributes.swift +++ b/AmplifyPlugins/Core/AWSPluginsCore/Keychain/KeychainStoreAttributes.swift @@ -24,7 +24,7 @@ extension KeychainStoreAttributes { KeychainStore.Constants.UseDataProtectionKeyChain: kCFBooleanTrue ] - if let accessGroup = accessGroup { + if let accessGroup { query[KeychainStore.Constants.AttributeAccessGroup] = accessGroup } return query diff --git a/AmplifyPlugins/Core/AWSPluginsCoreTests/Keychain/KeychainStoreAttributesTests.swift b/AmplifyPlugins/Core/AWSPluginsCoreTests/Keychain/KeychainStoreAttributesTests.swift index 9c474d3a4f..501d8c8c37 100644 --- a/AmplifyPlugins/Core/AWSPluginsCoreTests/Keychain/KeychainStoreAttributesTests.swift +++ b/AmplifyPlugins/Core/AWSPluginsCoreTests/Keychain/KeychainStoreAttributesTests.swift @@ -45,10 +45,28 @@ class KeychainStoreAttributesTests: XCTestCase { XCTAssertNil(defaultGetAttributes[KeychainStore.Constants.AttributeAccessible] as? String) XCTAssertNil(defaultGetAttributes[KeychainStore.Constants.UseDataProtectionKeyChain] as? String) } + + /// Given: an instance of `KeychainStoreAttributes` + /// When: `keychainStoreAttribute.defaultSetQuery()` is invoked with a required service param + /// Then: Validate if the attributes contain the correct set query params + /// - AttributeService + /// - Class + /// - AttributeAccessible + /// - UseDataProtectionKeyChain + func testDefaultSetQuery() { + keychainStoreAttribute = KeychainStoreAttributes(service: "someService") + + let defaultSetAttributes = keychainStoreAttribute.defaultSetQuery() + XCTAssertEqual(defaultSetAttributes[KeychainStore.Constants.AttributeService] as? String, "someService") + XCTAssertEqual(defaultSetAttributes[KeychainStore.Constants.Class] as? String, KeychainStore.Constants.ClassGenericPassword) + XCTAssertNil(defaultSetAttributes[KeychainStore.Constants.AttributeAccessGroup] as? String) + XCTAssertEqual(defaultSetAttributes[KeychainStore.Constants.AttributeAccessible] as? String, KeychainStore.Constants.AttributeAccessibleAfterFirstUnlockThisDeviceOnly) + XCTAssertEqual(defaultSetAttributes[KeychainStore.Constants.UseDataProtectionKeyChain] as? Bool, true) + } /// Given: an instance of `KeychainStoreAttributes` /// When: `keychainStoreAttribute.defaultSetQuery()` is invoked with a required service param and access group - /// Then: Validate if the attributes contain the correct get query params + /// Then: Validate if the attributes contain the correct set query params /// - AttributeService /// - Class /// - AttributeAccessGroup @@ -57,12 +75,12 @@ class KeychainStoreAttributesTests: XCTestCase { func testDefaultSetQueryWithAccessGroup() { keychainStoreAttribute = KeychainStoreAttributes(service: "someService", accessGroup: "someAccessGroup") - let defaultGetAttributes = keychainStoreAttribute.defaultSetQuery() - XCTAssertEqual(defaultGetAttributes[KeychainStore.Constants.AttributeService] as? String, "someService") - XCTAssertEqual(defaultGetAttributes[KeychainStore.Constants.Class] as? String, KeychainStore.Constants.ClassGenericPassword) - XCTAssertEqual(defaultGetAttributes[KeychainStore.Constants.AttributeAccessGroup] as? String, "someAccessGroup") - XCTAssertEqual(defaultGetAttributes[KeychainStore.Constants.AttributeAccessible] as? String, KeychainStore.Constants.AttributeAccessibleAfterFirstUnlockThisDeviceOnly) - XCTAssertEqual(defaultGetAttributes[KeychainStore.Constants.UseDataProtectionKeyChain] as? Bool, true) + let defaultSetAttributes = keychainStoreAttribute.defaultSetQuery() + XCTAssertEqual(defaultSetAttributes[KeychainStore.Constants.AttributeService] as? String, "someService") + XCTAssertEqual(defaultSetAttributes[KeychainStore.Constants.Class] as? String, KeychainStore.Constants.ClassGenericPassword) + XCTAssertEqual(defaultSetAttributes[KeychainStore.Constants.AttributeAccessGroup] as? String, "someAccessGroup") + XCTAssertEqual(defaultSetAttributes[KeychainStore.Constants.AttributeAccessible] as? String, KeychainStore.Constants.AttributeAccessibleAfterFirstUnlockThisDeviceOnly) + XCTAssertEqual(defaultSetAttributes[KeychainStore.Constants.UseDataProtectionKeyChain] as? Bool, true) } override func tearDown() { diff --git a/AmplifyPlugins/Internal/Tests/InternalAWSPinpointUnitTests/Mocks/MockKeychainStore.swift b/AmplifyPlugins/Internal/Tests/InternalAWSPinpointUnitTests/Mocks/MockKeychainStore.swift index e2c127588e..3596ce8241 100644 --- a/AmplifyPlugins/Internal/Tests/InternalAWSPinpointUnitTests/Mocks/MockKeychainStore.swift +++ b/AmplifyPlugins/Internal/Tests/InternalAWSPinpointUnitTests/Mocks/MockKeychainStore.swift @@ -61,6 +61,20 @@ class MockKeychainStore: KeychainStoreBehavior { stringValues.removeAll() dataValues.removeAll() } + + func _getAll() throws -> [(key: String, value: Data)] { + var allValues: [(key: String, value: Data)] = [] + + for (key, value) in dataValues { + allValues.append((key: key, value: value)) + } + + for (key, value) in stringValues { + allValues.append((key: key, value: value.data(using: .utf8)!)) + } + + return allValues + } func resetCounters() { dataForKeyCount = 0 diff --git a/api-dump/AWSDataStorePlugin.json b/api-dump/AWSDataStorePlugin.json index 47c0e792de..2dc8a457a7 100644 --- a/api-dump/AWSDataStorePlugin.json +++ b/api-dump/AWSDataStorePlugin.json @@ -8205,7 +8205,7 @@ "-module", "AWSDataStorePlugin", "-o", - "\/var\/folders\/hn\/5bx1f4_d4ds5vhwhkxc7vdcr0000gn\/T\/tmp.nAGRifhwH6\/AWSDataStorePlugin.json", + "\/var\/folders\/m_\/cksx93ys47x4621g0zbw_m4m0000gn\/T\/tmp.gb6DUlncxa\/AWSDataStorePlugin.json", "-I", ".build\/debug", "-sdk-version", diff --git a/api-dump/AWSPluginsCore.json b/api-dump/AWSPluginsCore.json index 19b8e2fb57..44d1400cff 100644 --- a/api-dump/AWSPluginsCore.json +++ b/api-dump/AWSPluginsCore.json @@ -5187,6 +5187,55 @@ "throwing": true, "reqNewWitnessTableEntry": true, "funcSelfKind": "NonMutating" + }, + { + "kind": "Function", + "name": "_getAll", + "printedName": "_getAll()", + "children": [ + { + "kind": "TypeNominal", + "name": "Array", + "printedName": "[(key: Swift.String, value: Foundation.Data)]", + "children": [ + { + "kind": "TypeNominal", + "name": "Tuple", + "printedName": "(key: Swift.String, value: Foundation.Data)", + "children": [ + { + "kind": "TypeNominal", + "name": "String", + "printedName": "Swift.String", + "usr": "s:SS" + }, + { + "kind": "TypeNominal", + "name": "Data", + "printedName": "Foundation.Data", + "usr": "s:10Foundation4DataV" + } + ] + } + ], + "usr": "s:Sa" + } + ], + "declKind": "Func", + "usr": "s:14AWSPluginsCore21KeychainStoreBehaviorP7_getAllSaySS3key_10Foundation4DataV5valuetGyKF", + "mangledName": "$s14AWSPluginsCore21KeychainStoreBehaviorP7_getAllSaySS3key_10Foundation4DataV5valuetGyKF", + "moduleName": "AWSPluginsCore", + "genericSig": "", + "protocolReq": true, + "declAttributes": [ + "SPIAccessControl" + ], + "spi_group_names": [ + "KeychainStore" + ], + "throwing": true, + "reqNewWitnessTableEntry": true, + "funcSelfKind": "NonMutating" } ], "declKind": "Protocol", @@ -5468,6 +5517,52 @@ "throwing": true, "funcSelfKind": "NonMutating" }, + { + "kind": "Function", + "name": "_getAll", + "printedName": "_getAll()", + "children": [ + { + "kind": "TypeNominal", + "name": "Array", + "printedName": "[(key: Swift.String, value: Foundation.Data)]", + "children": [ + { + "kind": "TypeNominal", + "name": "Tuple", + "printedName": "(key: Swift.String, value: Foundation.Data)", + "children": [ + { + "kind": "TypeNominal", + "name": "String", + "printedName": "Swift.String", + "usr": "s:SS" + }, + { + "kind": "TypeNominal", + "name": "Data", + "printedName": "Foundation.Data", + "usr": "s:10Foundation4DataV" + } + ] + } + ], + "usr": "s:Sa" + } + ], + "declKind": "Func", + "usr": "s:14AWSPluginsCore13KeychainStoreV7_getAllSaySS3key_10Foundation4DataV5valuetGyKF", + "mangledName": "$s14AWSPluginsCore13KeychainStoreV7_getAllSaySS3key_10Foundation4DataV5valuetGyKF", + "moduleName": "AWSPluginsCore", + "declAttributes": [ + "SPIAccessControl" + ], + "spi_group_names": [ + "KeychainStore" + ], + "throwing": true, + "funcSelfKind": "NonMutating" + }, { "kind": "Var", "name": "log", @@ -24273,7 +24368,7 @@ "-module", "AWSPluginsCore", "-o", - "\/var\/folders\/hn\/5bx1f4_d4ds5vhwhkxc7vdcr0000gn\/T\/tmp.nAGRifhwH6\/AWSPluginsCore.json", + "\/var\/folders\/m_\/cksx93ys47x4621g0zbw_m4m0000gn\/T\/tmp.gb6DUlncxa\/AWSPluginsCore.json", "-I", ".build\/debug", "-sdk-version", diff --git a/api-dump/Amplify.json b/api-dump/Amplify.json index 1bb4d36d2c..2d828de19d 100644 --- a/api-dump/Amplify.json +++ b/api-dump/Amplify.json @@ -18636,6 +18636,220 @@ } ] }, + { + "kind": "TypeDecl", + "name": "AccessGroup", + "printedName": "AccessGroup", + "children": [ + { + "kind": "Var", + "name": "name", + "printedName": "name", + "children": [ + { + "kind": "TypeNominal", + "name": "Optional", + "printedName": "Swift.String?", + "children": [ + { + "kind": "TypeNominal", + "name": "String", + "printedName": "Swift.String", + "usr": "s:SS" + } + ], + "usr": "s:Sq" + } + ], + "declKind": "Var", + "usr": "s:7Amplify11AccessGroupV4nameSSSgvp", + "mangledName": "$s7Amplify11AccessGroupV4nameSSSgvp", + "moduleName": "Amplify", + "declAttributes": [ + "HasStorage" + ], + "isLet": true, + "hasStorage": true, + "accessors": [ + { + "kind": "Accessor", + "name": "Get", + "printedName": "Get()", + "children": [ + { + "kind": "TypeNominal", + "name": "Optional", + "printedName": "Swift.String?", + "children": [ + { + "kind": "TypeNominal", + "name": "String", + "printedName": "Swift.String", + "usr": "s:SS" + } + ], + "usr": "s:Sq" + } + ], + "declKind": "Accessor", + "usr": "s:7Amplify11AccessGroupV4nameSSSgvg", + "mangledName": "$s7Amplify11AccessGroupV4nameSSSgvg", + "moduleName": "Amplify", + "implicit": true, + "declAttributes": [ + "Transparent" + ], + "accessorKind": "get" + } + ] + }, + { + "kind": "Var", + "name": "migrateKeychainItems", + "printedName": "migrateKeychainItems", + "children": [ + { + "kind": "TypeNominal", + "name": "Bool", + "printedName": "Swift.Bool", + "usr": "s:Sb" + } + ], + "declKind": "Var", + "usr": "s:7Amplify11AccessGroupV20migrateKeychainItemsSbvp", + "mangledName": "$s7Amplify11AccessGroupV20migrateKeychainItemsSbvp", + "moduleName": "Amplify", + "declAttributes": [ + "HasStorage" + ], + "isLet": true, + "hasStorage": true, + "accessors": [ + { + "kind": "Accessor", + "name": "Get", + "printedName": "Get()", + "children": [ + { + "kind": "TypeNominal", + "name": "Bool", + "printedName": "Swift.Bool", + "usr": "s:Sb" + } + ], + "declKind": "Accessor", + "usr": "s:7Amplify11AccessGroupV20migrateKeychainItemsSbvg", + "mangledName": "$s7Amplify11AccessGroupV20migrateKeychainItemsSbvg", + "moduleName": "Amplify", + "implicit": true, + "declAttributes": [ + "Transparent" + ], + "accessorKind": "get" + } + ] + }, + { + "kind": "Constructor", + "name": "init", + "printedName": "init(name:migrateKeychainItemsOfUserSession:)", + "children": [ + { + "kind": "TypeNominal", + "name": "AccessGroup", + "printedName": "Amplify.AccessGroup", + "usr": "s:7Amplify11AccessGroupV" + }, + { + "kind": "TypeNominal", + "name": "String", + "printedName": "Swift.String", + "usr": "s:SS" + }, + { + "kind": "TypeNominal", + "name": "Bool", + "printedName": "Swift.Bool", + "hasDefaultArg": true, + "usr": "s:Sb" + } + ], + "declKind": "Constructor", + "usr": "s:7Amplify11AccessGroupV4name33migrateKeychainItemsOfUserSessionACSS_Sbtcfc", + "mangledName": "$s7Amplify11AccessGroupV4name33migrateKeychainItemsOfUserSessionACSS_Sbtcfc", + "moduleName": "Amplify", + "init_kind": "Designated" + }, + { + "kind": "Function", + "name": "none", + "printedName": "none(migrateKeychainItemsOfUserSession:)", + "children": [ + { + "kind": "TypeNominal", + "name": "AccessGroup", + "printedName": "Amplify.AccessGroup", + "usr": "s:7Amplify11AccessGroupV" + }, + { + "kind": "TypeNominal", + "name": "Bool", + "printedName": "Swift.Bool", + "usr": "s:Sb" + } + ], + "declKind": "Func", + "usr": "s:7Amplify11AccessGroupV4none33migrateKeychainItemsOfUserSessionACSb_tFZ", + "mangledName": "$s7Amplify11AccessGroupV4none33migrateKeychainItemsOfUserSessionACSb_tFZ", + "moduleName": "Amplify", + "static": true, + "funcSelfKind": "NonMutating" + }, + { + "kind": "Var", + "name": "none", + "printedName": "none", + "children": [ + { + "kind": "TypeNominal", + "name": "AccessGroup", + "printedName": "Amplify.AccessGroup", + "usr": "s:7Amplify11AccessGroupV" + } + ], + "declKind": "Var", + "usr": "s:7Amplify11AccessGroupV4noneACvpZ", + "mangledName": "$s7Amplify11AccessGroupV4noneACvpZ", + "moduleName": "Amplify", + "static": true, + "accessors": [ + { + "kind": "Accessor", + "name": "Get", + "printedName": "Get()", + "children": [ + { + "kind": "TypeNominal", + "name": "AccessGroup", + "printedName": "Amplify.AccessGroup", + "usr": "s:7Amplify11AccessGroupV" + } + ], + "declKind": "Accessor", + "usr": "s:7Amplify11AccessGroupV4noneACvgZ", + "mangledName": "$s7Amplify11AccessGroupV4noneACvgZ", + "moduleName": "Amplify", + "static": true, + "accessorKind": "get" + } + ] + } + ], + "declKind": "Struct", + "usr": "s:7Amplify11AccessGroupV", + "mangledName": "$s7Amplify11AccessGroupV", + "moduleName": "Amplify" + }, { "kind": "TypeAlias", "name": "AdditionalInfo", @@ -179735,7 +179949,7 @@ "-module", "Amplify", "-o", - "\/var\/folders\/hn\/5bx1f4_d4ds5vhwhkxc7vdcr0000gn\/T\/tmp.nAGRifhwH6\/Amplify.json", + "\/var\/folders\/m_\/cksx93ys47x4621g0zbw_m4m0000gn\/T\/tmp.gb6DUlncxa\/Amplify.json", "-I", ".build\/debug", "-sdk-version", diff --git a/api-dump/CoreMLPredictionsPlugin.json b/api-dump/CoreMLPredictionsPlugin.json index 1a9879407f..284048b4d7 100644 --- a/api-dump/CoreMLPredictionsPlugin.json +++ b/api-dump/CoreMLPredictionsPlugin.json @@ -430,7 +430,7 @@ "-module", "CoreMLPredictionsPlugin", "-o", - "\/var\/folders\/hn\/5bx1f4_d4ds5vhwhkxc7vdcr0000gn\/T\/tmp.nAGRifhwH6\/CoreMLPredictionsPlugin.json", + "\/var\/folders\/m_\/cksx93ys47x4621g0zbw_m4m0000gn\/T\/tmp.gb6DUlncxa\/CoreMLPredictionsPlugin.json", "-I", ".build\/debug", "-sdk-version",