@@ -17,23 +17,70 @@ public struct GatewayTLSParams: Sendable {
1717}
1818
1919public 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