Skip to content

Commit fe5e146

Browse files
committed
iOS: migrate TLS fingerprints from UserDefaults to Keychain
1 parent cf122ce commit fe5e146

1 file changed

Lines changed: 57 additions & 10 deletions

File tree

apps/shared/OpenClawKit/Sources/OpenClawKit/GatewayTLSPinning.swift

Lines changed: 57 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,23 +17,70 @@ public struct GatewayTLSParams: Sendable {
1717
}
1818

1919
public enum GatewayTLSStore {
20-
private static let suiteName = "ai.openclaw.shared"
21-
private static let keyPrefix = "gateway.tls."
20+
private static let keychainService = "ai.openclaw.tls-pinning"
2221

23-
private static var defaults: UserDefaults {
24-
UserDefaults(suiteName: suiteName) ?? .standard
25-
}
22+
// Legacy UserDefaults location used before Keychain migration.
23+
private static let legacySuiteName = "ai.openclaw.shared"
24+
private static let legacyKeyPrefix = "gateway.tls."
2625

2726
public static func loadFingerprint(stableID: String) -> String? {
28-
let key = self.keyPrefix + stableID
29-
let raw = self.defaults.string(forKey: key)?.trimmingCharacters(in: .whitespacesAndNewlines)
27+
self.migrateFromUserDefaultsIfNeeded(stableID: stableID)
28+
let raw = self.keychainLoad(account: stableID)?.trimmingCharacters(in: .whitespacesAndNewlines)
3029
if raw?.isEmpty == false { return raw }
3130
return nil
3231
}
3332

3433
public static func saveFingerprint(_ value: String, stableID: String) {
35-
let key = self.keyPrefix + stableID
36-
self.defaults.set(value, forKey: key)
34+
self.keychainSave(value, account: stableID)
35+
}
36+
37+
// MARK: - Migration
38+
39+
/// On first Keychain read for a given stableID, move any legacy UserDefaults
40+
/// fingerprint into Keychain and remove the old entry.
41+
private static func migrateFromUserDefaultsIfNeeded(stableID: String) {
42+
guard let defaults = UserDefaults(suiteName: self.legacySuiteName) else { return }
43+
let legacyKey = self.legacyKeyPrefix + stableID
44+
guard let existing = defaults.string(forKey: legacyKey)?
45+
.trimmingCharacters(in: .whitespacesAndNewlines),
46+
!existing.isEmpty
47+
else { return }
48+
if self.keychainLoad(account: stableID) == nil {
49+
self.keychainSave(existing, account: stableID)
50+
}
51+
defaults.removeObject(forKey: legacyKey)
52+
}
53+
54+
// MARK: - Self-contained Keychain helpers (OpenClawKit can't import iOS KeychainStore)
55+
56+
private static func keychainLoad(account: String) -> String? {
57+
let query: [String: Any] = [
58+
kSecClass as String: kSecClassGenericPassword,
59+
kSecAttrService as String: self.keychainService,
60+
kSecAttrAccount as String: account,
61+
kSecReturnData as String: true,
62+
kSecMatchLimit as String: kSecMatchLimitOne,
63+
]
64+
var item: CFTypeRef?
65+
let status = SecItemCopyMatching(query as CFDictionary, &item)
66+
guard status == errSecSuccess, let data = item as? Data else { return nil }
67+
return String(data: data, encoding: .utf8)
68+
}
69+
70+
@discardableResult
71+
private static func keychainSave(_ value: String, account: String) -> Bool {
72+
let data = Data(value.utf8)
73+
let query: [String: Any] = [
74+
kSecClass as String: kSecClassGenericPassword,
75+
kSecAttrService as String: self.keychainService,
76+
kSecAttrAccount as String: account,
77+
]
78+
// Delete-then-add to enforce accessibility attribute.
79+
SecItemDelete(query as CFDictionary)
80+
var insert = query
81+
insert[kSecValueData as String] = data
82+
insert[kSecAttrAccessible as String] = kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly
83+
return SecItemAdd(insert as CFDictionary, nil) == errSecSuccess
3784
}
3885
}
3986

@@ -52,7 +99,7 @@ public final class GatewayTLSPinningSession: NSObject, WebSocketSessioning, URLS
5299

53100
public func makeWebSocketTask(url: URL) -> WebSocketTaskBox {
54101
let task = self.session.webSocketTask(with: url)
55-
task.maximumMessageSize = 16 * 1024 * 1024
102+
task.maximumMessageSize = 4 * 1024 * 1024
56103
return WebSocketTaskBox(task: task)
57104
}
58105

0 commit comments

Comments
 (0)