




















11import CryptoKit
22import Foundation
334+public enum GatewayDeviceIdentityProfile: String, Sendable {
5+case primary
6+case shareExtension
7+8+var identityFileName: String {
9+switch self {
10+case .primary:
11+"device.json"
12+case .shareExtension:
13+"share-device.json"
14+}
15+}
16+17+var authFileName: String {
18+switch self {
19+case .primary:
20+"device-auth.json"
21+case .shareExtension:
22+"share-device-auth.json"
23+}
24+}
25+}
26+427public struct DeviceIdentity: Codable, Sendable {
528public var deviceId: String
629public var publicKey: String
@@ -17,8 +40,40 @@ public struct DeviceIdentity: Codable, Sendable {
17401841enum DeviceIdentityPaths {
1942private static let stateDirEnv = ["OPENCLAW_STATE_DIR"]
43+static let legacyMigrationMarkerFileName = ".legacy-migrated"
20442145static func stateDirURL() -> URL {
46+self.stateDirURL(
47+ overrideURL: self.stateDirOverrideURL(),
48+ legacyStateDirURL: self.legacyStateDirURL(),
49+ appGroupStateDirURL: self.appGroupStateDirURL(),
50+ temporaryDirectory: FileManager.default.temporaryDirectory)
51+}
52+53+static func stateDirURL(
54+ overrideURL: URL?,
55+ legacyStateDirURL: URL?,
56+ appGroupStateDirURL: URL?,
57+ temporaryDirectory: URL) -> URL
58+{
59+if let overrideURL {
60+return overrideURL
61+}
62+if let appGroupStateDirURL {
63+if let legacyStateDirURL {
64+self.migrateLegacyIdentityDirectoryIfNeeded(
65+ from: legacyStateDirURL,
66+ to: appGroupStateDirURL)
67+}
68+return appGroupStateDirURL
69+}
70+if let legacyStateDirURL {
71+return legacyStateDirURL
72+}
73+return temporaryDirectory.appendingPathComponent("openclaw", isDirectory: true)
74+}
75+76+private static func stateDirOverrideURL() -> URL? {
2277for key in self.stateDirEnv {
2378if let raw = getenv(key) {
2479let value = String(cString: raw).trimmingCharacters(in: .whitespacesAndNewlines)
@@ -27,34 +82,93 @@ enum DeviceIdentityPaths {
2782}
2883}
2984}
85+return nil
86+}
308788+private static func legacyStateDirURL() -> URL? {
3189if let appSupport = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first {
3290return appSupport.appendingPathComponent("OpenClaw", isDirectory: true)
3391}
92+return nil
93+}
349435-return FileManager.default.temporaryDirectory.appendingPathComponent("openclaw", isDirectory: true)
95+private static func appGroupStateDirURL() -> URL? {
96+guard
97+let containerURL = FileManager.default
98+.containerURL(forSecurityApplicationGroupIdentifier: OpenClawAppGroup.identifier)
99+else {
100+return nil
101+}
102+return containerURL.appendingPathComponent("OpenClaw", isDirectory: true)
103+}
104+105+private static func identityDirectoryURL(base: URL) -> URL {
106+ base.appendingPathComponent("identity", isDirectory: true)
107+}
108+109+private static func migrateLegacyIdentityDirectoryIfNeeded(from legacyStateDirURL: URL, to sharedStateDirURL: URL) {
110+let fileManager = FileManager.default
111+let legacyIdentityURL = self.identityDirectoryURL(base: legacyStateDirURL)
112+let legacyDeviceURL = legacyIdentityURL.appendingPathComponent("device.json", isDirectory: false)
113+let sharedIdentityURL = self.identityDirectoryURL(base: sharedStateDirURL)
114+let sharedDeviceURL = sharedIdentityURL.appendingPathComponent("device.json", isDirectory: false)
115+let markerURL = sharedIdentityURL.appendingPathComponent(self.legacyMigrationMarkerFileName, isDirectory: false)
116+guard
117+ !fileManager.fileExists(atPath: markerURL.path) || !fileManager.fileExists(atPath: sharedDeviceURL.path),
118+ fileManager.fileExists(atPath: legacyDeviceURL.path)
119+else {
120+return
121+}
122+123+do {
124+ // The share extension can create app-group identity before the app migrates.
125+ // Until this marker exists, the app's legacy paired identity remains authoritative.
126+try fileManager.createDirectory(at: sharedIdentityURL, withIntermediateDirectories: true)
127+let entries = try fileManager.contentsOfDirectory(at: legacyIdentityURL, includingPropertiesForKeys: nil)
128+var copiedNames = Set<String>()
129+for entry in entries {
130+let destination = sharedIdentityURL.appendingPathComponent(entry.lastPathComponent, isDirectory: false)
131+if fileManager.fileExists(atPath: destination.path) {
132+try fileManager.removeItem(at: destination)
133+}
134+try fileManager.copyItem(at: entry, to: destination)
135+ copiedNames.insert(entry.lastPathComponent)
136+}
137+for staleName in ["device.json", "device-auth.json"] where !copiedNames.contains(staleName) {
138+let staleURL = sharedIdentityURL.appendingPathComponent(staleName, isDirectory: false)
139+if fileManager.fileExists(atPath: staleURL.path) {
140+try fileManager.removeItem(at: staleURL)
141+}
142+}
143+try Data("1\n".utf8).write(to: markerURL, options: [.atomic])
144+} catch {
145+ // Device identity migration is best-effort; callers will create fresh state if no readable identity exists.
146+}
36147}
37148}
3814939150public enum DeviceIdentityStore {
40-private static let fileName = "device.json"
41151private static let ed25519SPKIPrefix = Data([
42-0x30, 0x2a, 0x30, 0x05, 0x06, 0x03, 0x2b, 0x65,
152+0x30, 0x2A, 0x30, 0x05, 0x06, 0x03, 0x2B, 0x65,
431530x70, 0x03, 0x21, 0x00,
44154])
45155private static let ed25519PKCS8PrivatePrefix = Data([
46-0x30, 0x2e, 0x02, 0x01, 0x00, 0x30, 0x05, 0x06,
47-0x03, 0x2b, 0x65, 0x70, 0x04, 0x22, 0x04, 0x20,
156+0x30, 0x2E, 0x02, 0x01, 0x00, 0x30, 0x05, 0x06,
157+0x03, 0x2B, 0x65, 0x70, 0x04, 0x22, 0x04, 0x20,
48158])
4915950160public static func loadOrCreate() -> DeviceIdentity {
51-self.loadOrCreate(fileURL: self.fileURL())
161+self.loadOrCreate(profile: .primary)
162+}
163+164+public static func loadOrCreate(profile: GatewayDeviceIdentityProfile) -> DeviceIdentity {
165+self.loadOrCreate(fileURL: self.fileURL(profile: profile))
52166}
5316754168static func loadOrCreate(fileURL url: URL) -> DeviceIdentity {
55169if let data = try? Data(contentsOf: url) {
56170switch self.decodeStoredIdentity(data) {
57-case .identity(let decoded):
171+case let .identity(decoded):
58172return decoded
59173case .recognizedInvalid:
60174return self.generate()
@@ -143,7 +257,7 @@ public enum DeviceIdentityStore {
143257let privateKeyData = Data(base64Encoded: identity.privateKey)
144258else { return nil }
145259146-guard publicKeyData.count == 32 && privateKeyData.count == 32,
260+guard publicKeyData.count == 32, privateKeyData.count == 32,
147261self.keyPairMatches(publicKeyData: publicKeyData, privateKeyData: privateKeyData)
148262else { return nil }
149263return DeviceIdentity(
@@ -211,11 +325,11 @@ public enum DeviceIdentityStore {
211325}
212326}
213327214-private static func fileURL() -> URL {
328+private static func fileURL(profile: GatewayDeviceIdentityProfile) -> URL {
215329let base = DeviceIdentityPaths.stateDirURL()
216330return base
217331.appendingPathComponent("identity", isDirectory: true)
218-.appendingPathComponent(self.fileName, isDirectory: false)
332+.appendingPathComponent(profile.identityFileName, isDirectory: false)
219333}
220334}
221335此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。