diff --git a/Sources/SpeziSecureStorage/SecureStorage.swift b/Sources/SpeziSecureStorage/SecureStorage.swift
index 8efa083..410a9ae 100644
--- a/Sources/SpeziSecureStorage/SecureStorage.swift
+++ b/Sources/SpeziSecureStorage/SecureStorage.swift
@@ -178,6 +178,27 @@ public final class SecureStorage: Module, DefaultInitializable {
try execute(SecItemDelete(query as CFDictionary))
}
+ /// Delete all existing credentials stored in the Keychain.
+ /// - Parameters:
+ /// - accessGroup: The access group associated with the credentials.
+ public func deleteAllCredentials(itemTypes: SecureStorageItemTypes = .all, accessGroup: String? = nil) throws {
+ for kSecClassType in itemTypes.kSecClass {
+ do {
+ var query: [String: Any] = [kSecClass as String: kSecClassType]
+ // Only append the accessGroup attribute if the `CredentialsStore` is configured to use KeyChain access groups
+ if let accessGroup {
+ query[kSecAttrAccessGroup as String] = accessGroup
+ }
+ try execute(SecItemDelete(query as CFDictionary))
+ } catch SecureStorageError.notFound {
+ // We are fine it no keychain items have been found and therefore non had been deleted.
+ continue
+ } catch {
+ print(error)
+ }
+ }
+ }
+
/// Update existing credentials found in the Keychain.
/// - Parameters:
/// - username: The username associated with the old credentials.
@@ -207,11 +228,23 @@ public final class SecureStorage: Module, DefaultInitializable {
/// - accessGroup: The access group associated with the credentials.
/// - Returns: Returns the credentials stored in the Keychain identified by the `username`, `server`, and `accessGroup`.
public func retrieveCredentials(_ username: String, server: String? = nil, accessGroup: String? = nil) throws -> Credentials? {
+ try retrieveAllCredentials(forServer: server, accessGroup: accessGroup)
+ .first { credentials in
+ credentials.username == username
+ }
+ }
+
+ /// Retrieve all existing credentials stored in the Keychain for a specific server.
+ /// - Parameters:
+ /// - server: The server associated with the credentials.
+ /// - accessGroup: The access group associated with the credentials.
+ /// - Returns: Returns all existing credentials stored in the Keychain identified by the `server` and `accessGroup`.
+ public func retrieveAllCredentials(forServer server: String? = nil, accessGroup: String? = nil) throws -> [Credentials] {
// This method uses code provided by the Apple Developer documentation at
// https://developer.apple.com/documentation/security/keychain_services/keychain_items/searching_for_keychain_items
- var query: [String: Any] = queryFor(username, server: server, accessGroup: accessGroup)
- query[kSecMatchLimit as String] = kSecMatchLimitOne
+ var query: [String: Any] = queryFor(nil, server: server, accessGroup: accessGroup)
+ query[kSecMatchLimit as String] = kSecMatchLimitAll
query[kSecReturnAttributes as String] = true
query[kSecReturnData as String] = true
@@ -219,19 +252,28 @@ public final class SecureStorage: Module, DefaultInitializable {
do {
try execute(SecItemCopyMatching(query as CFDictionary, &item))
} catch SecureStorageError.notFound {
- return nil
+ return []
} catch {
throw error
}
- guard let existingItem = item as? [String: Any],
- let passwordData = existingItem[kSecValueData as String] as? Data,
- let password = String(data: passwordData, encoding: String.Encoding.utf8),
- let account = existingItem[kSecAttrAccount as String] as? String else {
+ guard let existingItems = item as? [[String: Any]] else {
throw SecureStorageError.unexpectedCredentialsData
}
- return Credentials(username: account, password: password)
+ var credentials: [Credentials] = []
+
+ for existingItem in existingItems {
+ guard let passwordData = existingItem[kSecValueData as String] as? Data,
+ let password = String(data: passwordData, encoding: String.Encoding.utf8),
+ let account = existingItem[kSecAttrAccount as String] as? String else {
+ continue
+ }
+
+ credentials.append(Credentials(username: account, password: password))
+ }
+
+ return credentials
}
@@ -249,12 +291,14 @@ public final class SecureStorage: Module, DefaultInitializable {
}
}
- private func queryFor(_ account: String, server: String?, accessGroup: String?) -> [String: Any] {
+ private func queryFor(_ account: String?, server: String?, accessGroup: String?) -> [String: Any] {
// This method uses code provided by the Apple Developer documentation at
// https://developer.apple.com/documentation/security/keychain_services/keychain_items/using_the_keychain_to_manage_user_secrets
var query: [String: Any] = [:]
- query[kSecAttrAccount as String] = account
+ if let account {
+ query[kSecAttrAccount as String] = account
+ }
// Only append the accessGroup attribute if the `CredentialsStore` is configured to use KeyChain access groups
if let accessGroup {
diff --git a/Sources/SpeziSecureStorage/SecureStorageItemTypes.swift b/Sources/SpeziSecureStorage/SecureStorageItemTypes.swift
new file mode 100644
index 0000000..dbf5020
--- /dev/null
+++ b/Sources/SpeziSecureStorage/SecureStorageItemTypes.swift
@@ -0,0 +1,48 @@
+//
+// This source file is part of the Stanford Spezi open-source project
+//
+// SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md)
+//
+// SPDX-License-Identifier: MIT
+//
+
+import Security
+
+
+/// Types of items that can be stored in the secure storage.
+public struct SecureStorageItemTypes: OptionSet {
+ /// Keys as created with (``SecureStorage/createKey(_:size:storageScope:)``).
+ public static let keys = SecureStorageItemTypes(rawValue: 1 << 0)
+ /// Credentials as created with (``SecureStorage/store(credentials:server:removeDuplicate:storageScope:)``) by passing in a server name.
+ public static let serverCredentials = SecureStorageItemTypes(rawValue: 1 << 1)
+ /// Credentials as created with (``SecureStorage/store(credentials:server:removeDuplicate:storageScope:)``) by omitting a server name.
+ public static let nonServerCredentials = SecureStorageItemTypes(rawValue: 1 << 2)
+
+ /// Credentials as created with (``SecureStorage/store(credentials:server:removeDuplicate:storageScope:)``).
+ public static let credentials: SecureStorageItemTypes = [.serverCredentials, .serverCredentials]
+ /// All types of items that can be handled by the secure storage component.
+ public static let all: SecureStorageItemTypes = [.keys, .serverCredentials, .nonServerCredentials]
+
+
+ public let rawValue: Int
+
+
+ var kSecClass: [CFString] {
+ var kSecClass: [CFString] = []
+ if self.contains(.keys) {
+ kSecClass.append(kSecClassKey)
+ }
+ if self.contains(.serverCredentials) {
+ kSecClass.append(kSecClassGenericPassword)
+ }
+ if self.contains(.nonServerCredentials) {
+ kSecClass.append(kSecClassInternetPassword)
+ }
+ return kSecClass
+ }
+
+
+ public init(rawValue: Int) {
+ self.rawValue = rawValue
+ }
+}
diff --git a/Tests/UITests/TestApp/SecureStorageTests/SecureStorageTests.swift b/Tests/UITests/TestApp/SecureStorageTests/SecureStorageTests.swift
index e8e3ef2..82ab3ea 100644
--- a/Tests/UITests/TestApp/SecureStorageTests/SecureStorageTests.swift
+++ b/Tests/UITests/TestApp/SecureStorageTests/SecureStorageTests.swift
@@ -24,12 +24,34 @@ final class SecureStorageTests: TestAppTestCase {
func runTests() async throws {
+ try testDeleteAllCredentials()
try testCredentials()
try testInternetCredentials()
+ try testMultipleInternetCredentials()
+ try testMultipleCredentials()
try testKeys()
}
+ func testDeleteAllCredentials() throws {
+ let serverCredentials1 = Credentials(username: "@Schmiedmayer", password: "SpeziInventor")
+ try secureStorage.store(credentials: serverCredentials1, server: "apple.com")
+
+ let serverCredentials2 = Credentials(username: "Stanford Spezi", password: "Paul")
+ try secureStorage.store(credentials: serverCredentials2)
+
+ try secureStorage.createKey("DeleteKeyTest", storageScope: .keychain)
+
+ try secureStorage.deleteAllCredentials()
+
+ try XCTAssertEqual(try XCTUnwrap(secureStorage.retrieveAllCredentials(forServer: "apple.com")).count, 0)
+ try XCTAssertEqual(try XCTUnwrap(secureStorage.retrieveAllCredentials()).count, 0)
+ try XCTAssertNil(secureStorage.retrievePrivateKey(forTag: "DeleteKeyTest"))
+ try XCTAssertNil(secureStorage.retrievePublicKey(forTag: "DeleteKeyTest"))
+ }
+
func testCredentials() throws {
+ try secureStorage.deleteAllCredentials(itemTypes: .credentials)
+
var serverCredentials = Credentials(username: "@PSchmiedmayer", password: "SpeziInventor")
try secureStorage.store(credentials: serverCredentials)
try secureStorage.store(credentials: serverCredentials, storageScope: .keychainSynchronizable)
@@ -52,6 +74,8 @@ final class SecureStorageTests: TestAppTestCase {
}
func testInternetCredentials() throws {
+ try secureStorage.deleteAllCredentials(itemTypes: .credentials)
+
var serverCredentials = Credentials(username: "@PSchmiedmayer", password: "SpeziInventor")
try secureStorage.store(credentials: serverCredentials, server: "twitter.com")
try secureStorage.store(credentials: serverCredentials, server: "twitter.com") // Overwrite existing credentials.
@@ -76,8 +100,48 @@ final class SecureStorageTests: TestAppTestCase {
try XCTAssertNil(try secureStorage.retrieveCredentials("@Spezi", server: "stanford.edu"))
}
+ func testMultipleInternetCredentials() throws {
+ try secureStorage.deleteAllCredentials(itemTypes: .credentials)
+
+ let serverCredentials1 = Credentials(username: "Paul Schmiedmayer", password: "SpeziInventor")
+ try secureStorage.store(credentials: serverCredentials1, server: "linkedin.com")
+
+ let serverCredentials2 = Credentials(username: "Stanford Spezi", password: "Paul")
+ try secureStorage.store(credentials: serverCredentials2, server: "linkedin.com")
+
+ let retrievedCredentials = try XCTUnwrap(secureStorage.retrieveAllCredentials(forServer: "linkedin.com"))
+ try XCTAssertEqual(retrievedCredentials.count, 2)
+ try XCTAssert(retrievedCredentials.contains(where: { $0 == serverCredentials1 }))
+ try XCTAssert(retrievedCredentials.contains(where: { $0 == serverCredentials2 }))
+
+ try secureStorage.deleteCredentials("Paul Schmiedmayer", server: "linkedin.com")
+ try secureStorage.deleteCredentials("Stanford Spezi", server: "linkedin.com")
+
+ try XCTAssertEqual(try XCTUnwrap(secureStorage.retrieveAllCredentials(forServer: "linkedin.com")).count, 0)
+ }
+
+ func testMultipleCredentials() throws {
+ try secureStorage.deleteAllCredentials(itemTypes: .credentials)
+
+ let serverCredentials1 = Credentials(username: "Paul Schmiedmayer", password: "SpeziInventor")
+ try secureStorage.store(credentials: serverCredentials1)
+
+ let serverCredentials2 = Credentials(username: "Stanford Spezi", password: "Paul")
+ try secureStorage.store(credentials: serverCredentials2)
+
+ let retrievedCredentials = try XCTUnwrap(secureStorage.retrieveAllCredentials())
+ try XCTAssertEqual(retrievedCredentials.count, 2)
+ try XCTAssert(retrievedCredentials.contains(where: { $0 == serverCredentials1 }))
+ try XCTAssert(retrievedCredentials.contains(where: { $0 == serverCredentials2 }))
+
+ try secureStorage.deleteCredentials("Paul Schmiedmayer")
+ try secureStorage.deleteCredentials("Stanford Spezi")
+
+ try XCTAssertEqual(try XCTUnwrap(secureStorage.retrieveAllCredentials()).count, 0)
+ }
+
func testKeys() throws {
- try secureStorage.deleteKeys(forTag: "MyKey")
+ try secureStorage.deleteAllCredentials(itemTypes: .keys)
try XCTAssertNil(try secureStorage.retrievePublicKey(forTag: "MyKey"))
try secureStorage.createKey("MyKey", storageScope: .keychain)
diff --git a/Tests/UITests/UITests.xcodeproj/xcshareddata/xcschemes/TestApp.xcscheme b/Tests/UITests/UITests.xcodeproj/xcshareddata/xcschemes/TestApp.xcscheme
index a5d3573..4e9d11c 100644
--- a/Tests/UITests/UITests.xcodeproj/xcshareddata/xcschemes/TestApp.xcscheme
+++ b/Tests/UITests/UITests.xcodeproj/xcshareddata/xcschemes/TestApp.xcscheme
@@ -57,7 +57,8 @@
shouldUseLaunchSchemeArgsEnv = "YES">
+ reference = "container:TestApp.xctestplan"
+ default = "YES">