78 lines
2.0 KiB
Swift
78 lines
2.0 KiB
Swift
//
|
||
// KeychainStore.swift
|
||
// punchnet
|
||
//
|
||
// Created by 安礼成 on 2026/1/19.
|
||
//
|
||
|
||
import Foundation
|
||
import Security
|
||
|
||
enum KeychainError: Error {
|
||
case unexpectedStatus(OSStatus)
|
||
}
|
||
|
||
final class KeychainStore {
|
||
|
||
public static var shared: KeychainStore = .init(service: Bundle.main.bundleIdentifier!)
|
||
|
||
private let service: String
|
||
|
||
private init(service: String) {
|
||
self.service = service
|
||
}
|
||
|
||
func save(_ data: Data, account: String) throws {
|
||
let query: [CFString: Any] = [
|
||
kSecClass: kSecClassGenericPassword,
|
||
kSecAttrService: service,
|
||
kSecAttrAccount: account,
|
||
kSecValueData: data
|
||
]
|
||
|
||
// 先删再加,避免重复
|
||
SecItemDelete(query as CFDictionary)
|
||
|
||
let status = SecItemAdd(query as CFDictionary, nil)
|
||
guard status == errSecSuccess else {
|
||
throw KeychainError.unexpectedStatus(status)
|
||
}
|
||
}
|
||
|
||
func load(account: String) throws -> Data? {
|
||
let query: [CFString: Any] = [
|
||
kSecClass: kSecClassGenericPassword,
|
||
kSecAttrService: service,
|
||
kSecAttrAccount: account,
|
||
kSecReturnData: true,
|
||
kSecMatchLimit: kSecMatchLimitOne
|
||
]
|
||
|
||
var result: AnyObject?
|
||
let status = SecItemCopyMatching(query as CFDictionary, &result)
|
||
|
||
if status == errSecItemNotFound {
|
||
return nil
|
||
}
|
||
|
||
guard status == errSecSuccess else {
|
||
throw KeychainError.unexpectedStatus(status)
|
||
}
|
||
|
||
return result as? Data
|
||
}
|
||
|
||
func delete(account: String) throws {
|
||
let query: [CFString: Any] = [
|
||
kSecClass: kSecClassGenericPassword,
|
||
kSecAttrService: service,
|
||
kSecAttrAccount: account
|
||
]
|
||
|
||
let status = SecItemDelete(query as CFDictionary)
|
||
guard status == errSecSuccess || status == errSecItemNotFound else {
|
||
throw KeychainError.unexpectedStatus(status)
|
||
}
|
||
}
|
||
}
|