63 lines
2.5 KiB
Swift
63 lines
2.5 KiB
Swift
// Copyright (c) 2026 GiantAccel, LLC
|
|
// XStats modifications Copyright (C) 2026 ysicing
|
|
// SPDX-License-Identifier: AGPL-3.0-or-later AND MIT
|
|
//
|
|
// Upstream portions retain their MIT license.
|
|
// XStats modifications are licensed under AGPL-3.0-or-later.
|
|
// See LICENSE, LICENSING.md and LICENSES/OpenStats-MIT.txt.
|
|
|
|
import Foundation
|
|
import Security
|
|
|
|
/// 连接密码仅存本机钥匙串,读写失败必须明确报告,不能伪装成空密码。
|
|
@MainActor
|
|
public protocol WebDAVPasswordStore {
|
|
func read(account: String) throws -> String?
|
|
func write(_ password: String, account: String) throws
|
|
}
|
|
|
|
/// 使用文件式钥匙串,兼容本机 ad-hoc 签名;各服务器与用户独立存储。
|
|
@MainActor
|
|
public struct KeychainWebDAVPasswordStore: WebDAVPasswordStore {
|
|
public init() {}
|
|
private func query(_ account: String) -> [String: Any] {
|
|
[kSecClass as String: kSecClassGenericPassword,
|
|
kSecAttrService as String: "work.12306.xstats.webdav",
|
|
kSecAttrAccount as String: account]
|
|
}
|
|
|
|
public func read(account: String) throws -> String? {
|
|
var query = query(account)
|
|
query[kSecReturnData as String] = true
|
|
query[kSecMatchLimit as String] = kSecMatchLimitOne
|
|
var result: AnyObject?
|
|
let status = SecItemCopyMatching(query as CFDictionary, &result)
|
|
if status == errSecItemNotFound { return nil }
|
|
guard status == errSecSuccess else { throw WebDAVKeychainError(status: status) }
|
|
guard let data = result as? Data, let password = String(data: data, encoding: .utf8) else {
|
|
throw WebDAVKeychainError(status: errSecDecode)
|
|
}
|
|
return password
|
|
}
|
|
|
|
public func write(_ password: String, account: String) throws {
|
|
let query = query(account)
|
|
let data = Data(password.utf8)
|
|
var status = SecItemUpdate(query as CFDictionary, [kSecValueData as String: data] as CFDictionary)
|
|
if status == errSecItemNotFound {
|
|
var item = query
|
|
item[kSecValueData as String] = data
|
|
item[kSecAttrLabel as String] = "XStats WebDAV"
|
|
status = SecItemAdd(item as CFDictionary, nil)
|
|
}
|
|
guard status == errSecSuccess else { throw WebDAVKeychainError(status: status) }
|
|
}
|
|
}
|
|
|
|
public struct WebDAVKeychainError: Error, LocalizedError {
|
|
public let status: OSStatus
|
|
public var errorDescription: String? {
|
|
(SecCopyErrorMessageString(status, nil) as String?) ?? "Keychain error \(status)"
|
|
}
|
|
}
|