Files
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

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?
}