Skip to content

[MOO-2391] Improve SessionCookieStore#53

Open
MxKevinBeqo wants to merge 8 commits into
mx/11.12.xfrom
moo/MOO-2391-improve-enc-storage
Open

[MOO-2391] Improve SessionCookieStore#53
MxKevinBeqo wants to merge 8 commits into
mx/11.12.xfrom
moo/MOO-2391-improve-enc-storage

Conversation

@MxKevinBeqo

@MxKevinBeqo MxKevinBeqo commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Addresses the keychain-related password dialog that appeared on the splash screen for some users on iOS.


What was happening

  1. A boot-time keychain migration (migrateKeychainAccessibility) runs early during startup and, in its original form, requests the secret data of every generic-password keychain item in one batch query. Decrypting an oversized/blocked item as part of that batch invokes LocalAuthentication, producing the "Enter password" dialog on the splash screen. This is the confirmed direct trigger — the dialog began appearing only when this migration was added, even though the oversized item already existed.
  2. The oversized item itself is written by SessionCookieStore.persist(), which serialises all no-expiry session cookies into a single keychain item that can grow to several hundred KB.
  3. The migration did not set its completion flag on a read error, so dismissing the dialog left it un-migrated and the dialog reappeared on the next launch (the "random"/repeating behaviour).

The fix addresses both the direct trigger (the migration read) and the underlying condition (oversized items), so neither relies on the other.


Changes

1. MendixNative/Encryption/MendixEncryptedStorageModule.mmigrateKeychainAccessibility

Remove kSecReturnData from the batch query. The migration only needs kSecAttrAccount to issue SecItemUpdate calls; fetching the actual secret data was unnecessary and was the direct trigger of the dialog.

Switch to kSecUseAuthenticationUISkip. With kSecMatchLimitAll, this silently skips any item that would require user interaction and returns the rest — so a single blocked/oversized item can neither show a dialog nor abort the batch. (The single-item reads in SessionCookieStore use kSecUseAuthenticationUIFail, which is the correct variant for a one-item query.)

Always set the done-flag. Every error path now writes NSUserDefaults before returning, breaking the infinite retry loop. Previously only a fully successful read set the flag.

// Before
kSecReturnData:     kCFBooleanTrue,   // fetched secret data of every item (the trigger)
kSecMatchLimit:     kSecMatchLimitAll
// + error path that did NOT set the done-flag → retry loop

// After
kSecReturnAttributes:    kCFBooleanTrue,
kSecMatchLimit:          kSecMatchLimitAll,
kSecUseAuthenticationUI: kSecUseAuthenticationUISkip  // skip blocked items, never prompt
// + all error paths now set [[NSUserDefaults standardUserDefaults] setBool:YES ...]

2. SessionCookieStore.swiftget(key:)

Locked-device reads no longer delete valid data. errSecInteractionNotAllowed now returns nil without calling clear(key:). The stored item is left intact so a later read after the device unlocks can still succeed. Previously this status was treated as corruption and the item was deleted — causing data loss on background launches before first unlock.

NSException from NSKeyedUnarchiver caught safely. The unarchive call is wrapped in a new ObjC @try/@catch trampoline (ObjCExceptionCatcher) so that malformed/truncated archive data triggers the self-heal path (delete + return nil) instead of crashing the app. Swift's do/catch cannot intercept Objective-C exceptions raised by NSCoder's default decodingFailurePolicy.

Self-heal on genuinely corrupt data. If deserialization returns nil (whether from a caught exception or a Swift-level failure), the item is deleted and nil returned — the same cleanup behaviour as before, but now it cannot crash.

// Locked device — leave item intact:
if status == errSecInteractionNotAllowed {
    return nil  // no clear()
}

// Successful read — safe unarchive via ObjC trampoline:
let result = ObjCExceptionCatcher.catchException {
    try? NSKeyedUnarchiver.unarchivedObject(ofClasses: [...], from: data)
}
guard let cookies = result as? [HTTPCookie] else {
    clear(key: key)  // genuinely corrupt — self-heal
    return nil
}

3. EncryptedStorage.swiftgetItem

Add kSecUseAuthenticationUIFail to the read query. Prevents the same auth-dialog-loop that affected SessionCookieStore from occurring for any consumer of EncryptedStorage.

On errSecInteractionNotAllowed, resolve as null without deleting. Consistent with the SessionCookieStore fix — a locked-device read is transient, not corruption. The item will be accessible on a subsequent read after unlock.

// Query gains:
kSecUseAuthenticationUI: kSecUseAuthenticationUIFail

// Locked device — resolve as absent, don't delete:
if status == errSecInteractionNotAllowed {
    promise.resolve(NSNull())
}

4. ObjCExceptionCatcher.h/.m (new) & MendixNative.podspec

Small Objective-C utility that wraps a block in @try/@catch and returns nil on NSException. Added to public_header_files in the podspec so Swift code within the module can use it.


Self-healing for already-affected devices

No manual intervention needed. On the first launch after this fix ships:

  1. The fixed migrateKeychainAccessibility no longer reads item data, so it does not prompt even while the oversized item still exists.
  2. restore() calls get(key:) → if the item is blocked/corrupt it is removed → user re-authenticates once.
  3. If the device is locked at restore time, the item is left intact and read succeeds on the next unlocked launch.
  4. Subsequent launches are clean — no oversized item, no migration prompt.

@MxKevinBeqo MxKevinBeqo self-assigned this Jul 9, 2026
@MxKevinBeqo MxKevinBeqo added the enhancement New feature or request label Jul 9, 2026
@MxKevinBeqo MxKevinBeqo changed the base branch from main to mx/11.12.x July 9, 2026 13:33
@MxKevinBeqo MxKevinBeqo force-pushed the moo/MOO-2391-improve-enc-storage branch from dccc1e6 to 69c6314 Compare July 9, 2026 13:39
@MxKevinBeqo MxKevinBeqo closed this Jul 9, 2026
@MxKevinBeqo MxKevinBeqo reopened this Jul 13, 2026
Comment thread ios/Modules/NativeCookieModule/SessionCookieStore.swift
Comment thread ios/Modules/NativeCookieModule/SessionCookieStore.swift Outdated
Comment thread ios/Modules/NativeCookieModule/SessionCookieStore.swift
@YogendraShelke

Copy link
Copy Markdown
Collaborator

EncryptedStorage.getItem performs an equivalent SecItemCopyMatching call but doesn't set kSecUseAuthenticationUI/kSecUseAuthenticationUIFail like the fix applied to SessionCookieStore.get(key:) in this PR. Any customer storing a large or blocked item via EncryptedStorage rather than SessionCookieStore can hit the same startup Auth-dialog-loop bug. Worth a follow-up to apply the same guard there.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants