98 lines
2.8 KiB
Swift
98 lines
2.8 KiB
Swift
import Foundation
|
|
import Security
|
|
|
|
public protocol KeychainService: Sendable {
|
|
func save(_ value: String, forKey key: String) async throws
|
|
func loadValue(forKey key: String) async throws -> String?
|
|
}
|
|
|
|
public actor MemoryKeychainService: KeychainService {
|
|
private var storage: [String: String] = [:]
|
|
|
|
public init() {}
|
|
|
|
public func save(_ value: String, forKey key: String) async throws {
|
|
storage[key] = value
|
|
}
|
|
|
|
public func loadValue(forKey key: String) async throws -> String? {
|
|
storage[key]
|
|
}
|
|
}
|
|
|
|
public enum KeychainServiceError: LocalizedError, Sendable {
|
|
case invalidValue
|
|
case unexpectedStatus(OSStatus)
|
|
|
|
public var errorDescription: String? {
|
|
switch self {
|
|
case .invalidValue:
|
|
return "The keychain returned invalid data."
|
|
case let .unexpectedStatus(status):
|
|
if let message = SecCopyErrorMessageString(status, nil) as String? {
|
|
return message
|
|
}
|
|
|
|
return "The keychain returned status \(status)."
|
|
}
|
|
}
|
|
}
|
|
|
|
public actor SystemKeychainService: KeychainService {
|
|
private let service: String
|
|
|
|
public init(service: String = "de.diyaa.velody") {
|
|
self.service = service
|
|
}
|
|
|
|
public func save(_ value: String, forKey key: String) async throws {
|
|
let query = baseQuery(forKey: key)
|
|
let deleteStatus = SecItemDelete(query as CFDictionary)
|
|
|
|
guard deleteStatus == errSecSuccess || deleteStatus == errSecItemNotFound else {
|
|
throw KeychainServiceError.unexpectedStatus(deleteStatus)
|
|
}
|
|
|
|
var addQuery = query
|
|
addQuery[kSecValueData as String] = Data(value.utf8)
|
|
|
|
let addStatus = SecItemAdd(addQuery as CFDictionary, nil)
|
|
guard addStatus == errSecSuccess else {
|
|
throw KeychainServiceError.unexpectedStatus(addStatus)
|
|
}
|
|
}
|
|
|
|
public func loadValue(forKey key: String) async throws -> String? {
|
|
var query = baseQuery(forKey: key)
|
|
query[kSecReturnData as String] = kCFBooleanTrue
|
|
query[kSecMatchLimit as String] = kSecMatchLimitOne
|
|
|
|
var result: AnyObject?
|
|
let status = SecItemCopyMatching(query as CFDictionary, &result)
|
|
|
|
switch status {
|
|
case errSecSuccess:
|
|
guard
|
|
let data = result as? Data,
|
|
let value = String(data: data, encoding: .utf8)
|
|
else {
|
|
throw KeychainServiceError.invalidValue
|
|
}
|
|
|
|
return value
|
|
case errSecItemNotFound:
|
|
return nil
|
|
default:
|
|
throw KeychainServiceError.unexpectedStatus(status)
|
|
}
|
|
}
|
|
|
|
private func baseQuery(forKey key: String) -> [String: Any] {
|
|
[
|
|
kSecClass as String: kSecClassGenericPassword,
|
|
kSecAttrService as String: service,
|
|
kSecAttrAccount as String: key,
|
|
]
|
|
}
|
|
}
|