mirror of
https://github.com/xweiba/location-spoofer.git
synced 2026-09-26 16:41:54 +08:00
fix: 优化启动引导、坐标处理和日志管理
This commit is contained in:
@@ -1,46 +1,178 @@
|
||||
import Foundation
|
||||
import Security
|
||||
|
||||
protocol CertificateAuthorityKeychain {
|
||||
func load() throws -> CertificateAuthority?
|
||||
func save(_ authority: CertificateAuthority) throws
|
||||
func remove() throws
|
||||
}
|
||||
|
||||
enum CertificateAuthorityStoreError: LocalizedError {
|
||||
case invalidAuthority
|
||||
case keychain(OSStatus)
|
||||
|
||||
var errorDescription: String? {
|
||||
switch self {
|
||||
case .invalidAuthority: return "本地 CA 证书或私钥无效"
|
||||
case let .keychain(status): return "无法写入设备钥匙串(\(status))"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final class DeviceCertificateAuthorityKeychain: CertificateAuthorityKeychain {
|
||||
private enum Item {
|
||||
static let service = "com.paopaolabs.location-spoofer.certificate-authority"
|
||||
static let certificateAccount = "root-ca-certificate"
|
||||
static let keyAccount = "root-ca-private-key"
|
||||
}
|
||||
|
||||
func load() throws -> CertificateAuthority? {
|
||||
guard let certPEM = try load(account: Item.certificateAccount),
|
||||
let keyPEM = try load(account: Item.keyAccount) else {
|
||||
return nil
|
||||
}
|
||||
return CertificateAuthority(certPEM: certPEM, keyPEM: keyPEM)
|
||||
}
|
||||
|
||||
func save(_ authority: CertificateAuthority) throws {
|
||||
try remove()
|
||||
do {
|
||||
try save(authority.certPEM, account: Item.certificateAccount)
|
||||
try save(authority.keyPEM, account: Item.keyAccount)
|
||||
} catch {
|
||||
try? remove()
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
func remove() throws {
|
||||
let query: [String: Any] = [
|
||||
kSecClass as String: kSecClassGenericPassword,
|
||||
kSecAttrService as String: Item.service,
|
||||
kSecAttrSynchronizable as String: kCFBooleanFalse as Any,
|
||||
]
|
||||
let status = SecItemDelete(query as CFDictionary)
|
||||
guard status == errSecSuccess || status == errSecItemNotFound else {
|
||||
throw CertificateAuthorityStoreError.keychain(status)
|
||||
}
|
||||
}
|
||||
|
||||
private func load(account: String) throws -> String? {
|
||||
let query: [String: Any] = [
|
||||
kSecClass as String: kSecClassGenericPassword,
|
||||
kSecAttrService as String: Item.service,
|
||||
kSecAttrAccount as String: account,
|
||||
kSecAttrSynchronizable as String: kCFBooleanFalse as Any,
|
||||
kSecReturnData as String: true,
|
||||
kSecMatchLimit as String: kSecMatchLimitOne,
|
||||
]
|
||||
var result: CFTypeRef?
|
||||
let status = SecItemCopyMatching(query as CFDictionary, &result)
|
||||
if status == errSecItemNotFound { return nil }
|
||||
guard status == errSecSuccess, let data = result as? Data,
|
||||
let value = String(data: data, encoding: .utf8) else {
|
||||
throw CertificateAuthorityStoreError.keychain(status)
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
private func save(_ value: String, account: String) throws {
|
||||
guard let data = value.data(using: .utf8) else {
|
||||
throw CocoaError(.fileWriteInapplicableStringEncoding)
|
||||
}
|
||||
let query: [String: Any] = [
|
||||
kSecClass as String: kSecClassGenericPassword,
|
||||
kSecAttrService as String: Item.service,
|
||||
kSecAttrAccount as String: account,
|
||||
kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly,
|
||||
kSecAttrSynchronizable as String: kCFBooleanFalse as Any,
|
||||
kSecValueData as String: data,
|
||||
]
|
||||
let status = SecItemAdd(query as CFDictionary, nil)
|
||||
guard status == errSecSuccess else { throw CertificateAuthorityStoreError.keychain(status) }
|
||||
}
|
||||
}
|
||||
|
||||
final class CertificateAuthorityStore {
|
||||
private let directory: URL
|
||||
private let generator: () throws -> CertificateAuthority
|
||||
private let validator: (CertificateAuthority) -> Bool
|
||||
private let keychain: CertificateAuthorityKeychain
|
||||
private let certificateURL: URL
|
||||
private let keyURL: URL
|
||||
|
||||
init(directory: URL = AppGroup.containerURL.appendingPathComponent("CertificateAuthority", isDirectory: true), generator: @escaping () throws -> CertificateAuthority = CoreBridge.generateCertificateAuthority) {
|
||||
init(
|
||||
directory: URL = AppGroup.containerURL.appendingPathComponent("CertificateAuthority", isDirectory: true),
|
||||
keychain: CertificateAuthorityKeychain = DeviceCertificateAuthorityKeychain(),
|
||||
generator: @escaping () throws -> CertificateAuthority = CoreBridge.generateCertificateAuthority,
|
||||
validator: @escaping (CertificateAuthority) -> Bool = CoreBridge.isValidCertificateAuthority
|
||||
) {
|
||||
self.directory = directory
|
||||
self.keychain = keychain
|
||||
self.generator = generator
|
||||
self.validator = validator
|
||||
self.certificateURL = directory.appendingPathComponent("ca-cert.pem")
|
||||
self.keyURL = directory.appendingPathComponent("ca-key.pem")
|
||||
}
|
||||
|
||||
func ensure() throws -> CertificateAuthority {
|
||||
if let current = try load() {
|
||||
RuntimeLogger.debug("SHARED", "Certificate.store", "读取已有 CA 文件", details: ["directory": directory.path])
|
||||
return current
|
||||
if let authority = try loadValidKeychainAuthority() {
|
||||
removeLegacyFilesBestEffort()
|
||||
RuntimeLogger.debug("SHARED", "Certificate.store", "复用设备钥匙串中的 CA")
|
||||
return authority
|
||||
}
|
||||
RuntimeLogger.info("SHARED", "Certificate.store", "未找到 CA 文件,开始生成", details: ["directory": directory.path])
|
||||
|
||||
if let legacy = try loadLegacyAuthority(), validator(legacy) {
|
||||
try keychain.save(legacy)
|
||||
removeLegacyFilesBestEffort()
|
||||
RuntimeLogger.info("SHARED", "Certificate.store", "旧 CA 已迁移到设备钥匙串")
|
||||
return legacy
|
||||
}
|
||||
|
||||
let authority = try generator()
|
||||
try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)
|
||||
guard let certificateData = authority.certPEM.data(using: .utf8),
|
||||
let keyData = authority.keyPEM.data(using: .utf8) else {
|
||||
throw CocoaError(.fileWriteInapplicableStringEncoding)
|
||||
}
|
||||
try certificateData.write(to: certificateURL, options: .atomic)
|
||||
try keyData.write(to: keyURL, options: .atomic)
|
||||
applyCompleteProtection(to: certificateURL)
|
||||
applyCompleteProtection(to: keyURL)
|
||||
RuntimeLogger.info("SHARED", "Certificate.store", "CA 文件写入完成", details: ["directory": directory.path])
|
||||
guard validator(authority) else { throw CertificateAuthorityStoreError.invalidAuthority }
|
||||
try keychain.save(authority)
|
||||
removeLegacyFilesBestEffort()
|
||||
RuntimeLogger.info("SHARED", "Certificate.store", "已生成并保存设备专属 CA")
|
||||
return authority
|
||||
}
|
||||
|
||||
func load() throws -> CertificateAuthority? {
|
||||
guard FileManager.default.fileExists(atPath: certificateURL.path), FileManager.default.fileExists(atPath: keyURL.path) else { return nil }
|
||||
return CertificateAuthority(certPEM: try String(contentsOf: certificateURL, encoding: .utf8), keyPEM: try String(contentsOf: keyURL, encoding: .utf8))
|
||||
try loadValidKeychainAuthority()
|
||||
}
|
||||
|
||||
private func applyCompleteProtection(to url: URL) {
|
||||
#if os(iOS)
|
||||
try? FileManager.default.setAttributes([.protectionKey: FileProtectionType.complete], ofItemAtPath: url.path)
|
||||
#endif
|
||||
private func loadValidKeychainAuthority() throws -> CertificateAuthority? {
|
||||
guard let authority = try keychain.load() else { return nil }
|
||||
guard validator(authority) else {
|
||||
RuntimeLogger.warning("SHARED", "Certificate.store", "钥匙串中的 CA 无效,准备回退")
|
||||
try? keychain.remove()
|
||||
return nil
|
||||
}
|
||||
return authority
|
||||
}
|
||||
|
||||
private func loadLegacyAuthority() throws -> CertificateAuthority? {
|
||||
guard FileManager.default.fileExists(atPath: certificateURL.path),
|
||||
FileManager.default.fileExists(atPath: keyURL.path) else {
|
||||
return nil
|
||||
}
|
||||
return CertificateAuthority(
|
||||
certPEM: try String(contentsOf: certificateURL, encoding: .utf8),
|
||||
keyPEM: try String(contentsOf: keyURL, encoding: .utf8)
|
||||
)
|
||||
}
|
||||
|
||||
private func removeLegacyFilesBestEffort() {
|
||||
let fileManager = FileManager.default
|
||||
// Remove private material first. Each item is retried on later launches
|
||||
// when a valid Keychain authority is available.
|
||||
for url in [keyURL, certificateURL] where fileManager.fileExists(atPath: url.path) {
|
||||
do {
|
||||
try fileManager.removeItem(at: url)
|
||||
} catch {
|
||||
RuntimeLogger.error("SHARED", "Certificate.store", "删除旧 CA 文件失败,将在下次启动重试", error: error)
|
||||
}
|
||||
}
|
||||
try? fileManager.removeItem(at: directory)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user