mirror of
https://github.com/shadowsocks/ShadowsocksX-NG.git
synced 2026-11-02 00:04:46 +00:00
112 lines
4.1 KiB
Swift
112 lines
4.1 KiB
Swift
import Foundation
|
|
import Security
|
|
|
|
enum CredentialKeyStoreError: Error, Equatable {
|
|
case keychainStatus(OSStatus)
|
|
}
|
|
|
|
/// 主密钥的二进制存储;交互只由显式解锁或重置命令开启。
|
|
protocol CredentialKeyStoring {
|
|
func ids() throws -> [UUID]
|
|
func read(_ id: UUID, allowingInteraction: Bool) throws -> Data?
|
|
func create(_ data: Data, id: UUID, allowingInteraction: Bool) throws
|
|
}
|
|
|
|
struct KeychainCredentialKeyStore: CredentialKeyStoring {
|
|
let service: String
|
|
|
|
init(namespace: String) {
|
|
self.service = "com.qiuyuzhou.ShadowsocksX-NG2.\(namespace)"
|
|
}
|
|
|
|
func ids() throws -> [UUID] {
|
|
try withClassicKeychainInteraction(false) {
|
|
var query = baseQuery
|
|
query[kSecReturnAttributes as String] = true
|
|
query[kSecMatchLimit as String] = kSecMatchLimitAll
|
|
var result: AnyObject?
|
|
let status = SecItemCopyMatching(query as CFDictionary, &result)
|
|
if status == errSecItemNotFound { return [] }
|
|
guard status == errSecSuccess, let items = result as? [[String: Any]] else {
|
|
throw CredentialKeyStoreError.keychainStatus(status)
|
|
}
|
|
return items.compactMap { item in
|
|
(item[kSecAttrAccount as String] as? String).flatMap(UUID.init(uuidString:))
|
|
}.sorted { $0.uuidString < $1.uuidString }
|
|
}
|
|
}
|
|
|
|
func read(_ id: UUID, allowingInteraction: Bool) throws -> Data? {
|
|
try withClassicKeychainInteraction(allowingInteraction) {
|
|
var query = query(id)
|
|
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, let data = result as? Data else {
|
|
throw CredentialKeyStoreError.keychainStatus(status)
|
|
}
|
|
return data
|
|
}
|
|
}
|
|
|
|
@diagnose(
|
|
DeprecatedDeclaration, as: ignored,
|
|
reason: "经典登录钥匙串使用 SecAccess ACL 限制获准应用访问"
|
|
)
|
|
func create(_ data: Data, id: UUID, allowingInteraction: Bool) throws {
|
|
try withClassicKeychainInteraction(allowingInteraction) {
|
|
var application: SecTrustedApplication?
|
|
try check(SecTrustedApplicationCreateFromPath(nil, &application))
|
|
guard let application else { throw CredentialKeyStoreError.keychainStatus(errSecParam) }
|
|
var access: SecAccess?
|
|
try check(
|
|
SecAccessCreate(
|
|
"ShadowsocksX-NG2 encrypted configuration" as CFString,
|
|
[application] as CFArray, &access))
|
|
guard let access else { throw CredentialKeyStoreError.keychainStatus(errSecParam) }
|
|
var item = query(id)
|
|
item[kSecValueData as String] = data
|
|
item[kSecAttrAccess as String] = access
|
|
try check(SecItemAdd(item as CFDictionary, nil))
|
|
}
|
|
}
|
|
|
|
private func query(_ id: UUID) -> [String: Any] {
|
|
var query = baseQuery
|
|
query[kSecAttrAccount as String] = id.uuidString
|
|
return query
|
|
}
|
|
|
|
private var baseQuery: [String: Any] {
|
|
[
|
|
kSecClass as String: kSecClassGenericPassword,
|
|
kSecAttrService as String: service,
|
|
kSecAttrSynchronizable as String: false,
|
|
kSecUseDataProtectionKeychain as String: false,
|
|
]
|
|
}
|
|
|
|
private func check(_ status: OSStatus) throws {
|
|
guard status == errSecSuccess else { throw CredentialKeyStoreError.keychainStatus(status) }
|
|
}
|
|
}
|
|
|
|
/// 经典钥匙串使用进程级交互开关。同步调用结束后恢复原值,不把交互许可留给后台操作。
|
|
@diagnose(
|
|
DeprecatedDeclaration, as: ignored,
|
|
reason: "经典登录钥匙串需要显式控制交互许可,并在同步操作结束后恢复原值"
|
|
)
|
|
private func withClassicKeychainInteraction<T>(_ allowed: Bool, _ operation: () throws -> T) throws
|
|
-> T
|
|
{
|
|
var previous: DarwinBoolean = false
|
|
let getStatus = SecKeychainGetUserInteractionAllowed(&previous)
|
|
guard getStatus == errSecSuccess else { throw CredentialKeyStoreError.keychainStatus(getStatus) }
|
|
let setStatus = SecKeychainSetUserInteractionAllowed(allowed)
|
|
guard setStatus == errSecSuccess else { throw CredentialKeyStoreError.keychainStatus(setStatus) }
|
|
defer { SecKeychainSetUserInteractionAllowed(previous.boolValue) }
|
|
return try operation()
|
|
}
|