velody/packages/apple/VelodyUtilities/Sources/VelodyUtilities/KeychainService.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,
]
}
}