feat: add KeychainStore

This commit is contained in:
2026-05-22 14:49:38 +02:00
parent 3af6c9ad31
commit 7e62c954c4
2 changed files with 75 additions and 0 deletions
@@ -0,0 +1,7 @@
import Foundation
public enum KeychainError: Error, Equatable {
case saveFailed(OSStatus)
case loadFailed(OSStatus)
case notFound
}
@@ -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
}
}