Skip to content

Commit 60e59cd

Browse files
authored
Merge pull request #53 from mendix/moo/MOO-2391-improve-enc-storage
[MOO-2391] Improve SessionCookieStore
2 parents d728e57 + ae80d69 commit 60e59cd

7 files changed

Lines changed: 80 additions & 30 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
- 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.
11+
1012
## [v0.5.1] - 2026-06-22
1113

1214
- We fixed an issue that could cause iOS apps to restart repeatedly after an OTA update.

MendixNative.podspec

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ Pod::Spec.new do |s|
1414
s.source = { :git => "https://github.com/mendix/mendix-native.git", :tag => "#{s.version}" }
1515

1616
s.source_files = "ios/**/*.{h,m,mm,cpp,swift}"
17-
s.public_header_files = "ios/Modules/Helper/ReactHostHelper.h"
17+
s.public_header_files = ["ios/Modules/Helper/ReactHostHelper.h", "ios/Modules/NativeCookieModule/ObjCExceptionCatcher.h"]
1818
s.private_header_files = "ios/TurboModules/**/*.h"
1919

2020
s.dependency "SSZipArchive"

ios/Modules/Encryption/EncryptedStorage.swift

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,15 @@ import Foundation
3333
kSecClass: kSecClassGenericPassword,
3434
kSecAttrAccount: key,
3535
kSecReturnData: kCFBooleanTrue as Any,
36-
kSecMatchLimit: kSecMatchLimitOne
36+
kSecMatchLimit: kSecMatchLimitOne,
37+
kSecUseAuthenticationUI: kSecUseAuthenticationUIFail
3738
] as CFDictionary
3839
var dataRef: CFTypeRef?
3940
let status = SecItemCopyMatching(query, &dataRef)
40-
if status == errSecSuccess {
41+
if status == errSecInteractionNotAllowed {
42+
// Device locked or item requires auth — don't delete, resolve as absent
43+
promise.resolve(NSNull())
44+
} else if status == errSecSuccess {
4145
guard let data = dataRef as? Data, let value = String(data: data, encoding: .utf8) else {
4246
promise.reject("An error occured while retrieving value", errorCode: Int(status))
4347
return

ios/Modules/NativeCookieModule/NativeCookieModule.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ public class NativeCookieModule: NSObject {
66
NativeCookieModule.clearAll()
77
promise.resolve(nil)
88
}
9-
9+
1010
static func clearAll() {
1111
let storage = HTTPCookieStorage.shared
1212
for cookie in (storage.cookies ?? []) {
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
#import <Foundation/Foundation.h>
2+
3+
NS_ASSUME_NONNULL_BEGIN
4+
5+
@interface ObjCExceptionCatcher : NSObject
6+
7+
/// Executes the block and returns its result. If an NSException is raised,
8+
/// catches it and returns nil instead of crashing.
9+
+ (nullable id)catchException:(id _Nullable (NS_NOESCAPE ^)(void))block;
10+
11+
@end
12+
13+
NS_ASSUME_NONNULL_END
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
#import "ObjCExceptionCatcher.h"
2+
3+
@implementation ObjCExceptionCatcher
4+
5+
+ (nullable id)catchException:(id _Nullable (NS_NOESCAPE ^)(void))block {
6+
@try {
7+
return block();
8+
} @catch (NSException *exception) {
9+
NSLog(@"ObjCExceptionCatcher: caught %@%@", exception.name, exception.reason);
10+
return nil;
11+
}
12+
}
13+
14+
@end

ios/Modules/NativeCookieModule/SessionCookieStore.swift

Lines changed: 43 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,27 @@
11
import Foundation
22

33
public class SessionCookieStore {
4-
4+
55
// MARK: - Private properties
66
private static let bundleIdentifier = Bundle.main.bundleIdentifier ?? "com.mendix.app"
77
private static let storageKey = bundleIdentifier + "sessionCookies"
88
private static let queue = DispatchQueue(label: bundleIdentifier + ".session-cookie-store", qos: .utility)
9-
9+
1010
// MARK: - Public API
1111
public static func restore() {
12-
12+
1313
guard let cookies = get(key: storageKey) else {
1414
NSLog("SessionCookieStore: No cookies to restore")
1515
return
1616
}
17-
17+
1818
let storage = HTTPCookieStorage.shared
1919
let existing = Set(storage.cookies ?? [])
2020
cookies.filter { !existing.contains($0) }.forEach { storage.setCookie($0) }
21-
22-
clear() // Clear stored cookies after restoration to avoid any side effects
21+
22+
clear()
2323
}
24-
24+
2525
public static func persist() {
2626
queue.async {
2727
let sessionCookies = HTTPCookieStorage.shared.cookies?.filter { isSessionCookie($0) } ?? []
@@ -33,21 +33,21 @@ public class SessionCookieStore {
3333
set(key: storageKey, cookies: sessionCookies)
3434
}
3535
}
36-
36+
3737
public static func clear() {
3838
clear(key: storageKey)
3939
}
40-
40+
4141
// MARK: - Private API
4242
private static func isSessionCookie(_ cookie: HTTPCookie) -> Bool {
4343
return cookie.expiresDate == nil
4444
}
45-
45+
4646
private static func set(key: String, cookies: [HTTPCookie]) {
4747
do {
4848
let data = try NSKeyedArchiver.archivedData(withRootObject: cookies, requiringSecureCoding: false)
49+
clear(key: key)
4950
let storeQuery = [kSecClass: kSecClassGenericPassword, kSecAttrAccount: key, kSecValueData: data] as CFDictionary
50-
SecItemDelete(storeQuery)
5151
let status = SecItemAdd(storeQuery, nil)
5252
if status != noErr {
5353
NSLog("SessionCookieStore: Failed to persist session cookies with status: \(status)")
@@ -56,29 +56,46 @@ public class SessionCookieStore {
5656
NSLog("SessionCookieStore: Failed to persist session cookies: \(error.localizedDescription)")
5757
}
5858
}
59-
59+
6060
private static func get(key: String) -> [HTTPCookie]? {
61-
do {
62-
let query: [CFString: Any] = [kSecClass: kSecClassGenericPassword, kSecAttrAccount: key, kSecReturnData: true]
63-
var item: CFTypeRef?
64-
let status = SecItemCopyMatching(query as CFDictionary, &item)
65-
if status == errSecSuccess, let data = item as? Data {
66-
let cookies = try NSKeyedUnarchiver.unarchivedObject(ofClasses: [NSArray.self, HTTPCookie.self], from: data) as? [HTTPCookie]
67-
return cookies
68-
} else {
69-
NSLog("SessionCookieStore: No session cookies found with status: \(status)")
61+
let query: [CFString: Any] = [
62+
kSecClass: kSecClassGenericPassword,
63+
kSecAttrAccount: key,
64+
kSecReturnData: true,
65+
kSecUseAuthenticationUI: kSecUseAuthenticationUIFail
66+
]
67+
var item: CFTypeRef?
68+
let status = SecItemCopyMatching(query as CFDictionary, &item)
69+
70+
if status == errSecInteractionNotAllowed {
71+
NSLog("SessionCookieStore: Item inaccessible (device locked), skipping restore")
72+
return nil
73+
} else if status == errSecSuccess, let data = item as? Data {
74+
let result = ObjCExceptionCatcher.catchException {
75+
try? NSKeyedUnarchiver.unarchivedObject(ofClasses: [NSArray.self, HTTPCookie.self], from: data)
76+
}
77+
78+
guard let cookies = result as? [HTTPCookie] else {
79+
NSLog("SessionCookieStore: Failed to deserialize cookies, clearing")
80+
clear(key: key)
7081
return nil
7182
}
72-
} catch {
73-
NSLog("SessionCookieStore: Failed to retrieve session cookies: \(error.localizedDescription)")
83+
return cookies
84+
} else if status != errSecItemNotFound {
85+
// Any other unreadable state — delete to prevent repeated failures
86+
NSLog("SessionCookieStore: Unreadable legacy item (status: \(status)), clearing")
87+
clear(key: key)
88+
return nil
89+
} else {
90+
NSLog("SessionCookieStore: No session cookies found")
7491
return nil
7592
}
7693
}
77-
94+
7895
private static func clear(key: String) {
79-
let query = [kSecClass: kSecClassGenericPassword, kSecAttrAccount: key, kSecReturnData: true] as CFDictionary
96+
let query = [kSecClass: kSecClassGenericPassword, kSecAttrAccount: key] as CFDictionary
8097
let status = SecItemDelete(query)
81-
if status != errSecSuccess {
98+
if status != errSecSuccess && status != errSecItemNotFound {
8299
NSLog("SessionCookieStore: Failed to clear cookies with status: \(status)")
83100
}
84101
}

0 commit comments

Comments
 (0)