diff --git a/CHANGELOG.md b/CHANGELOG.md index 3baf045..6fada6a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +- We addressed a scenario where, in iOS, an Auth dialogue could continuously popup on app startup due to access of either large keychain blob items or blocked items. + ## [v0.5.1] - 2026-06-22 - We fixed an issue that could cause iOS apps to restart repeatedly after an OTA update. diff --git a/ios/Modules/NativeCookieModule/NativeCookieModule.swift b/ios/Modules/NativeCookieModule/NativeCookieModule.swift index c7985e4..cadb00a 100644 --- a/ios/Modules/NativeCookieModule/NativeCookieModule.swift +++ b/ios/Modules/NativeCookieModule/NativeCookieModule.swift @@ -6,7 +6,7 @@ public class NativeCookieModule: NSObject { NativeCookieModule.clearAll() promise.resolve(nil) } - + static func clearAll() { let storage = HTTPCookieStorage.shared for cookie in (storage.cookies ?? []) { diff --git a/ios/Modules/NativeCookieModule/SessionCookieStore.swift b/ios/Modules/NativeCookieModule/SessionCookieStore.swift index 9312e2c..0fd1bec 100644 --- a/ios/Modules/NativeCookieModule/SessionCookieStore.swift +++ b/ios/Modules/NativeCookieModule/SessionCookieStore.swift @@ -1,27 +1,27 @@ import Foundation public class SessionCookieStore { - + // MARK: - Private properties private static let bundleIdentifier = Bundle.main.bundleIdentifier ?? "com.mendix.app" private static let storageKey = bundleIdentifier + "sessionCookies" private static let queue = DispatchQueue(label: bundleIdentifier + ".session-cookie-store", qos: .utility) - + // MARK: - Public API public static func restore() { - + guard let cookies = get(key: storageKey) else { NSLog("SessionCookieStore: No cookies to restore") return } - + let storage = HTTPCookieStorage.shared let existing = Set(storage.cookies ?? []) cookies.filter { !existing.contains($0) }.forEach { storage.setCookie($0) } - - clear() // Clear stored cookies after restoration to avoid any side effects + + clear() } - + public static func persist() { queue.async { let sessionCookies = HTTPCookieStorage.shared.cookies?.filter { isSessionCookie($0) } ?? [] @@ -33,21 +33,21 @@ public class SessionCookieStore { set(key: storageKey, cookies: sessionCookies) } } - + public static func clear() { clear(key: storageKey) } - + // MARK: - Private API private static func isSessionCookie(_ cookie: HTTPCookie) -> Bool { return cookie.expiresDate == nil } - + private static func set(key: String, cookies: [HTTPCookie]) { do { let data = try NSKeyedArchiver.archivedData(withRootObject: cookies, requiringSecureCoding: false) + clear(key: key) let storeQuery = [kSecClass: kSecClassGenericPassword, kSecAttrAccount: key, kSecValueData: data] as CFDictionary - SecItemDelete(storeQuery) let status = SecItemAdd(storeQuery, nil) if status != noErr { NSLog("SessionCookieStore: Failed to persist session cookies with status: \(status)") @@ -56,29 +56,47 @@ public class SessionCookieStore { NSLog("SessionCookieStore: Failed to persist session cookies: \(error.localizedDescription)") } } - + private static func get(key: String) -> [HTTPCookie]? { - do { - let query: [CFString: Any] = [kSecClass: kSecClassGenericPassword, kSecAttrAccount: key, kSecReturnData: true] - var item: CFTypeRef? - let status = SecItemCopyMatching(query as CFDictionary, &item) - if status == errSecSuccess, let data = item as? Data { + let query: [CFString: Any] = [ + kSecClass: kSecClassGenericPassword, + kSecAttrAccount: key, + kSecReturnData: true, + kSecUseAuthenticationUI: kSecUseAuthenticationUIFail + ] + var item: CFTypeRef? + let status = SecItemCopyMatching(query as CFDictionary, &item) + + if status == errSecInteractionNotAllowed { + // Oversized/blocked item — remove so it never prompts again + NSLog("SessionCookieStore: Blocked legacy item detected, clearing") + clear(key: key) + return nil + } else if status == errSecSuccess, let data = item as? Data { + do { let cookies = try NSKeyedUnarchiver.unarchivedObject(ofClasses: [NSArray.self, HTTPCookie.self], from: data) as? [HTTPCookie] return cookies - } else { - NSLog("SessionCookieStore: No session cookies found with status: \(status)") + } catch { + // Unarchiving failed (corrupt/oversized blob) — self-heal by removing + NSLog("SessionCookieStore: Failed to deserialize legacy cookies, clearing: \(error.localizedDescription)") + clear(key: key) return nil } - } catch { - NSLog("SessionCookieStore: Failed to retrieve session cookies: \(error.localizedDescription)") + } else if status != errSecItemNotFound { + // Any other unreadable state — delete to prevent repeated failures + NSLog("SessionCookieStore: Unreadable legacy item (status: \(status)), clearing") + clear(key: key) + return nil + } else { + NSLog("SessionCookieStore: No session cookies found") return nil } } - + private static func clear(key: String) { - let query = [kSecClass: kSecClassGenericPassword, kSecAttrAccount: key, kSecReturnData: true] as CFDictionary + let query = [kSecClass: kSecClassGenericPassword, kSecAttrAccount: key] as CFDictionary let status = SecItemDelete(query) - if status != errSecSuccess { + if status != errSecSuccess && status != errSecItemNotFound { NSLog("SessionCookieStore: Failed to clear cookies with status: \(status)") } }