Files
e-party-iOS/yana/Utils/Security/KeychainManager.swift
edwinQQQ 128bf36c88 feat: 更新依赖和项目配置,优化代码结构
- 在Package.swift中注释掉旧的swift-composable-architecture依赖,并添加swift-case-paths依赖。
- 在Podfile中将iOS平台版本更新至16.0,并移除QCloudCOSXML/Transfer依赖,改为使用QCloudCOSXML。
- 更新Podfile.lock以反映依赖变更,确保项目依赖的准确性。
- 新增架构分析需求文档,明确项目架构评估和改进建议。
- 在多个文件中实现async/await语法,提升异步操作的可读性和性能。
- 更新日志输出方法,确保在调试模式下提供一致的调试信息。
- 优化多个视图组件,提升用户体验和代码可维护性。
2025-07-17 18:47:09 +08:00

364 lines
12 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import Foundation
import Security
/// Keychain
///
/// UserDefaults
/// Codable
///
///
/// - iOS Keychain
/// - Codable
/// -
/// - 线
/// - 访
@MainActor
final class KeychainManager {
// MARK: -
@MainActor static let shared = KeychainManager()
private init() {}
// MARK: -
private let service: String = {
return Bundle.main.bundleIdentifier ?? "com.yana.app"
}()
private let accessGroup: String? = nil // App Group
// MARK: -
enum KeychainError: Error, LocalizedError {
case dataConversionFailed
case encodingFailed(Error)
case decodingFailed(Error)
case keychainOperationFailed(OSStatus)
case itemNotFound
case duplicateItem
case invalidParameters
var errorDescription: String? {
switch self {
case .dataConversionFailed:
return "数据转换失败"
case .encodingFailed(let error):
return "编码失败: \(error.localizedDescription)"
case .decodingFailed(let error):
return "解码失败: \(error.localizedDescription)"
case .keychainOperationFailed(let status):
return "Keychain 操作失败: \(status)"
case .itemNotFound:
return "未找到指定项目"
case .duplicateItem:
return "项目已存在"
case .invalidParameters:
return "无效参数"
}
}
}
// MARK: - 访
enum AccessLevel {
case whenUnlocked // 访
case whenUnlockedThisDeviceOnly // 访
case afterFirstUnlock // 访
case afterFirstUnlockThisDeviceOnly // 访
var attribute: CFString {
switch self {
case .whenUnlocked:
return kSecAttrAccessibleWhenUnlocked
case .whenUnlockedThisDeviceOnly:
return kSecAttrAccessibleWhenUnlockedThisDeviceOnly
case .afterFirstUnlock:
return kSecAttrAccessibleAfterFirstUnlock
case .afterFirstUnlockThisDeviceOnly:
return kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly
}
}
}
// MARK: -
/// Codable Keychain
/// - Parameters:
/// - object: Codable
/// - key:
/// - accessLevel: 访访
/// - Throws: KeychainError
func store<T: Codable>(_ object: T, forKey key: String, accessLevel: AccessLevel = .whenUnlockedThisDeviceOnly) throws {
// 1. Data
let data: Data
do {
data = try JSONEncoder().encode(object)
} catch {
throw KeychainError.encodingFailed(error)
}
// 2.
var query = baseQuery(forKey: key)
query[kSecValueData] = data
query[kSecAttrAccessible] = accessLevel.attribute
// 3.
SecItemDelete(query as CFDictionary)
// 4.
let status = SecItemAdd(query as CFDictionary, nil)
guard status == errSecSuccess else {
throw KeychainError.keychainOperationFailed(status)
}
debugInfoSync("🔐 Keychain 存储成功: \(key)")
}
/// Keychain Codable
/// - Parameters:
/// - type:
/// - key:
/// - Returns: nil
/// - Throws: KeychainError
func retrieve<T: Codable>(_ type: T.Type, forKey key: String) throws -> T? {
// 1.
var query = baseQuery(forKey: key)
query[kSecReturnData] = true
query[kSecMatchLimit] = kSecMatchLimitOne
// 2.
var result: AnyObject?
let status = SecItemCopyMatching(query as CFDictionary, &result)
// 3.
switch status {
case errSecSuccess:
guard let data = result as? Data else {
throw KeychainError.dataConversionFailed
}
// 4.
do {
let object = try JSONDecoder().decode(type, from: data)
debugInfoSync("🔐 Keychain 读取成功: \(key)")
return object
} catch {
throw KeychainError.decodingFailed(error)
}
case errSecItemNotFound:
return nil
default:
throw KeychainError.keychainOperationFailed(status)
}
}
/// Keychain
/// - Parameters:
/// - object:
/// - key:
/// - Throws: KeychainError
func update<T: Codable>(_ object: T, forKey key: String) throws {
// 1.
let data: Data
do {
data = try JSONEncoder().encode(object)
} catch {
throw KeychainError.encodingFailed(error)
}
// 2.
let query = baseQuery(forKey: key)
let updateAttributes: [CFString: Any] = [
kSecValueData: data
]
// 3.
let status = SecItemUpdate(query as CFDictionary, updateAttributes as CFDictionary)
switch status {
case errSecSuccess:
debugInfoSync("🔐 Keychain 更新成功: \(key)")
case errSecItemNotFound:
//
try store(object, forKey: key)
default:
throw KeychainError.keychainOperationFailed(status)
}
}
/// Keychain
/// - Parameter key:
/// - Throws: KeychainError
func delete(forKey key: String) throws {
let query = baseQuery(forKey: key)
let status = SecItemDelete(query as CFDictionary)
switch status {
case errSecSuccess:
debugInfoSync("🔐 Keychain 删除成功: \(key)")
case errSecItemNotFound:
//
break
default:
throw KeychainError.keychainOperationFailed(status)
}
}
/// Keychain
/// - Parameter key:
/// - Returns:
func exists(forKey key: String) -> Bool {
var query = baseQuery(forKey: key)
query[kSecReturnData] = false
query[kSecMatchLimit] = kSecMatchLimitOne
let status = SecItemCopyMatching(query as CFDictionary, nil)
return status == errSecSuccess
}
/// Keychain
/// - Throws: KeychainError
func clearAll() throws {
let query: [CFString: Any] = [
kSecClass: kSecClassGenericPassword,
kSecAttrService: service
]
let status = SecItemDelete(query as CFDictionary)
switch status {
case errSecSuccess, errSecItemNotFound:
debugInfoSync("🔐 Keychain 清除完成")
default:
throw KeychainError.keychainOperationFailed(status)
}
}
// MARK: -
///
/// - Parameter key:
/// - Returns:
private func baseQuery(forKey key: String) -> [CFString: Any] {
var query: [CFString: Any] = [
kSecClass: kSecClassGenericPassword,
kSecAttrService: service,
kSecAttrAccount: key
]
if let accessGroup = accessGroup {
query[kSecAttrAccessGroup] = accessGroup
}
return query
}
}
// MARK: - 便
extension KeychainManager {
/// Keychain
/// - Parameters:
/// - string:
/// - key:
/// - accessLevel: 访
/// - Throws: KeychainError
func storeString(_ string: String, forKey key: String, accessLevel: AccessLevel = .whenUnlockedThisDeviceOnly) throws {
try store(string, forKey: key, accessLevel: accessLevel)
}
/// Keychain
/// - Parameter key:
/// - Returns:
/// - Throws: KeychainError
func retrieveString(forKey key: String) throws -> String? {
return try retrieve(String.self, forKey: key)
}
/// Keychain
/// - Parameters:
/// - data:
/// - key:
/// - accessLevel: 访
/// - Throws: KeychainError
func storeData(_ data: Data, forKey key: String, accessLevel: AccessLevel = .whenUnlockedThisDeviceOnly) throws {
var query = baseQuery(forKey: key)
query[kSecValueData] = data
query[kSecAttrAccessible] = accessLevel.attribute
SecItemDelete(query as CFDictionary)
let status = SecItemAdd(query as CFDictionary, nil)
guard status == errSecSuccess else {
throw KeychainError.keychainOperationFailed(status)
}
}
/// Keychain
/// - Parameter key:
/// - Returns:
/// - Throws: KeychainError
func retrieveData(forKey key: String) throws -> Data? {
var query = baseQuery(forKey: key)
query[kSecReturnData] = true
query[kSecMatchLimit] = kSecMatchLimitOne
var result: AnyObject?
let status = SecItemCopyMatching(query as CFDictionary, &result)
switch status {
case errSecSuccess:
return result as? Data
case errSecItemNotFound:
return nil
default:
throw KeychainError.keychainOperationFailed(status)
}
}
}
// MARK: -
#if DEBUG
extension KeychainManager {
///
/// - Returns:
func debugListAllKeys() -> [String] {
let query: [CFString: Any] = [
kSecClass: kSecClassGenericPassword,
kSecAttrService: service,
kSecReturnAttributes: true,
kSecMatchLimit: kSecMatchLimitAll
]
var result: AnyObject?
let status = SecItemCopyMatching(query as CFDictionary, &result)
guard status == errSecSuccess,
let items = result as? [[CFString: Any]] else {
return []
}
return items.compactMap { item in
item[kSecAttrAccount] as? String
}
}
///
func debugPrintAllKeys() {
let keys = debugListAllKeys()
debugInfoSync("🔐 Keychain 中存储的键:")
for key in keys {
debugInfoSync(" - \(key)")
}
}
}
#endif