Skip to content

Commit

Permalink
Delete Secure Storage & Improve Getting and Loading Credentials (#13)
Browse files Browse the repository at this point in the history
# Delete Secure Storage & Improve Getting and Loading Credentials

## ♻️ Current situation & Problem
- The API currently does not provide a functionality to retrieve all
credentials for a web service or generally stored credentials.
- There is no API to delete credentials.


## ⚙️ Release Notes 
- Provides an API endpoint to retrieve all credentials and filter by
server.
- Adds a mechanism to delete credentials.


## 📚 Documentation
- Added documentation and additional context.


## ✅ Testing
- Added tests for all new functions.


## 📝 Code of Conduct & Contributing Guidelines 

By submitting creating this pull request, you agree to follow our [Code
of
Conduct](https://github.com/StanfordSpezi/.github/blob/main/CODE_OF_CONDUCT.md)
and [Contributing
Guidelines](https://github.com/StanfordSpezi/.github/blob/main/CONTRIBUTING.md):
- [x] I agree to follow the [Code of
Conduct](https://github.com/StanfordSpezi/.github/blob/main/CODE_OF_CONDUCT.md)
and [Contributing
Guidelines](https://github.com/StanfordSpezi/.github/blob/main/CONTRIBUTING.md).
  • Loading branch information
PSchmiedmayer committed Aug 31, 2023
1 parent f940006 commit 739ee1e
Show file tree
Hide file tree
Showing 4 changed files with 169 additions and 12 deletions.
64 changes: 54 additions & 10 deletions Sources/SpeziSecureStorage/SecureStorage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -207,31 +228,52 @@ 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

var item: CFTypeRef?
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
}


Expand All @@ -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 {
Expand Down
48 changes: 48 additions & 0 deletions Sources/SpeziSecureStorage/SecureStorageItemTypes.swift
Original file line number Diff line number Diff line change
@@ -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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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.
Expand All @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,8 @@
shouldUseLaunchSchemeArgsEnv = "YES">
<TestPlans>
<TestPlanReference
reference = "container:TestApp.xctestplan">
reference = "container:TestApp.xctestplan"
default = "YES">
</TestPlanReference>
</TestPlans>
<Testables>
Expand Down

0 comments on commit 739ee1e

Please sign in to comment.