diff --git a/MyPassCore/Sources/MyPassCore/Keychain/KeychainError.swift b/MyPassCore/Sources/MyPassCore/Keychain/KeychainError.swift new file mode 100644 index 0000000..f6c1745 --- /dev/null +++ b/MyPassCore/Sources/MyPassCore/Keychain/KeychainError.swift @@ -0,0 +1,7 @@ +import Foundation + +public enum KeychainError: Error, Equatable { + case saveFailed(OSStatus) + case loadFailed(OSStatus) + case notFound +} diff --git a/MyPassCore/Sources/MyPassCore/Keychain/KeychainStore.swift b/MyPassCore/Sources/MyPassCore/Keychain/KeychainStore.swift new file mode 100644 index 0000000..8b507e9 --- /dev/null +++ b/MyPassCore/Sources/MyPassCore/Keychain/KeychainStore.swift @@ -0,0 +1,68 @@ +import Foundation +import Security + +public struct KeychainStore { + private let accessGroup: String + private let service: String + + public init(accessGroup: String, service: String = "com.christophevila.mypass") { + self.accessGroup = accessGroup + self.service = service + } + + public func save(password: String, for account: String) throws { + let data = Data(password.utf8) + let query: [CFString: Any] = [ + kSecClass: kSecClassGenericPassword, + kSecAttrService: service, + kSecAttrAccount: account, + kSecAttrAccessGroup: accessGroup, + kSecAttrAccessible: kSecAttrAccessibleWhenUnlockedThisDeviceOnly, + kSecValueData: data, + ] + SecItemDelete(query as CFDictionary) + let status = SecItemAdd(query as CFDictionary, nil) + guard status == errSecSuccess else { throw KeychainError.saveFailed(status) } + } + + public func load(for account: String) throws -> String { + let query: [CFString: Any] = [ + kSecClass: kSecClassGenericPassword, + kSecAttrService: service, + kSecAttrAccount: account, + kSecAttrAccessGroup: accessGroup, + kSecReturnData: true, + kSecMatchLimit: kSecMatchLimitOne, + ] + var result: AnyObject? + let status = SecItemCopyMatching(query as CFDictionary, &result) + guard status == errSecSuccess else { + throw status == errSecItemNotFound ? KeychainError.notFound : KeychainError.loadFailed(status) + } + guard let data = result as? Data, let password = String(data: data, encoding: .utf8) else { + throw KeychainError.loadFailed(errSecInvalidData) + } + return password + } + + public func delete(for account: String) { + let query: [CFString: Any] = [ + kSecClass: kSecClassGenericPassword, + kSecAttrService: service, + kSecAttrAccount: account, + kSecAttrAccessGroup: accessGroup, + ] + SecItemDelete(query as CFDictionary) + } + + public func exists(for account: String) -> Bool { + let query: [CFString: Any] = [ + kSecClass: kSecClassGenericPassword, + kSecAttrService: service, + kSecAttrAccount: account, + kSecAttrAccessGroup: accessGroup, + kSecMatchLimit: kSecMatchLimitOne, + ] + return SecItemCopyMatching(query as CFDictionary, nil) == errSecSuccess + } +}