Files
ShadowsocksX-NG/ShadowsocksX-NG2/Domain/Credentials/MasterKeyStore.swift
T
Qiu Yuzhou f9e95474e5 refactor: adopt age configuration storage
Replace the custom encrypted container with AgeSwift 0.1.0 so standard
age tools can inspect sensitive configuration independently of the app.

Use independent Keychain X25519 identities for catalog.age and
proxy-listening.age. Preserve atomic commits and confirmed recovery;
reject historical NG2 formats without migration or cleanup.
2026-10-21 14:31:32 +08:00

104 lines
3.8 KiB
Swift

import Foundation
import Security
enum MasterKeyStoreError: Error, Equatable {
case keychainStatus(OSStatus)
}
/// 主密钥的二进制存储;交互只由显式解锁或重置命令开启。
protocol MasterKeyStoring {
func ids() throws -> [UUID]
func read(_ id: UUID, allowingInteraction: Bool) throws -> Data?
func create(_ data: Data, id: UUID, allowingInteraction: Bool) throws
}
struct KeychainMasterKeyStore: MasterKeyStoring {
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 MasterKeyStoreError.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 MasterKeyStoreError.keychainStatus(status)
}
return data
}
}
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 MasterKeyStoreError.keychainStatus(errSecParam) }
var access: SecAccess?
try check(
SecAccessCreate(
"ShadowsocksX-NG2 encrypted configuration" as CFString,
[application] as CFArray, &access))
guard let access else { throw MasterKeyStoreError.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 MasterKeyStoreError.keychainStatus(status) }
}
}
/// 经典钥匙串使用进程级交互开关。同步调用结束后恢复原值,不把交互许可留给后台操作。
private func withClassicKeychainInteraction<T>(_ allowed: Bool, _ operation: () throws -> T) throws
-> T
{
var previous: DarwinBoolean = false
let getStatus = SecKeychainGetUserInteractionAllowed(&previous)
guard getStatus == errSecSuccess else { throw MasterKeyStoreError.keychainStatus(getStatus) }
let setStatus = SecKeychainSetUserInteractionAllowed(allowed)
guard setStatus == errSecSuccess else { throw MasterKeyStoreError.keychainStatus(setStatus) }
defer { SecKeychainSetUserInteractionAllowed(previous.boolValue) }
return try operation()
}