mirror of
https://github.com/shadowsocks/ShadowsocksX-NG.git
synced 2026-11-08 10:44:50 +00:00
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.
63 lines
2.3 KiB
Swift
63 lines
2.3 KiB
Swift
import Foundation
|
|
|
|
/// 活动目标持久化:落盘 `~/Library/Application Support/
|
|
/// ShadowsocksX-NG2/configuration/activation.json`,权限与原子替换基线同 catalog.age。
|
|
/// 只含目标身份,永不落秘密明文。损坏或版本未知按安全侧恢复为「无活动目标」
|
|
/// (代理保持停止,由状态机重校验同步);文件系统级错误仍抛出。
|
|
struct ActivationStateFileStore {
|
|
enum PersistenceError: Error, Equatable {
|
|
/// 读取或写入时的文件系统错误。
|
|
case ioFailure(detail: String)
|
|
}
|
|
|
|
private static let currentVersion = 1
|
|
private static let jsonEncoder: JSONEncoder = {
|
|
let encoder = JSONEncoder()
|
|
encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
|
|
return encoder
|
|
}()
|
|
|
|
let fileURL: URL
|
|
|
|
/// 默认位置:`~/Library/Application Support/ShadowsocksX-NG2/configuration/activation.json`。
|
|
static func defaultFileURL() -> URL {
|
|
ApplicationSupportPaths.current.activationStateFileURL
|
|
}
|
|
|
|
/// 文件缺失/损坏/版本未知 → `nil`(安全侧:无活动目标)。
|
|
func loadActiveTargetID() throws -> NodeID? {
|
|
guard FileManager.default.fileExists(atPath: fileURL.path) else { return nil }
|
|
let data: Data
|
|
do {
|
|
data = try Data(contentsOf: fileURL)
|
|
} catch {
|
|
throw PersistenceError.ioFailure(detail: String(describing: error))
|
|
}
|
|
let payload = try? JSONDecoder().decode(ActivationStatePayload.self, from: data)
|
|
guard let payload, payload.version == Self.currentVersion else { return nil }
|
|
return payload.activeTargetID
|
|
}
|
|
|
|
/// 整体重写并原子替换;失败时保留原文件。目录权限强制为 0700(含自愈)。
|
|
func save(activeTargetID: NodeID?) throws {
|
|
let data: Data
|
|
do {
|
|
data = try Self.jsonEncoder.encode(
|
|
ActivationStatePayload(version: Self.currentVersion, activeTargetID: activeTargetID))
|
|
} catch {
|
|
throw PersistenceError.ioFailure(detail: String(describing: error))
|
|
}
|
|
do {
|
|
try AtomicFileWriter.write(data, to: fileURL)
|
|
} catch {
|
|
throw PersistenceError.ioFailure(detail: String(describing: error))
|
|
}
|
|
}
|
|
}
|
|
|
|
/// 落盘文档形态:版本号 + 活动目标身份(可为 null)。
|
|
private struct ActivationStatePayload: Codable {
|
|
var version: Int
|
|
var activeTargetID: NodeID?
|
|
}
|