The following sample correctly uses the Keychain API for local authentication (SecAccessControlCreateWithFlags) but it uses the kSecAccessControlUserPresence flag which allows fallback to device passcode when biometric authentication fails or isn't yet configured.
importSwiftUIimportLocalAuthenticationimportSecuritystructMastgTest{staticfuncmastgTest(completion:@escaping(String)->Void){letaccount="com.mastg.sectoken"lettokenData="8767086b9f6f976g-a8df76".data(using:.utf8)!//1.StorethetokenintheKeychainwithACLflags//1a.Createanaccess‐controlobjectrequiringuserpresenceguardletaccessControl=SecAccessControlCreateWithFlags(nil,kSecAttrAccessibleWhenUnlocked,.userPresence,nil)else{completion("❌ Failed to create access control")return}//1b.Buildyouradd‐itemquery//Optional:youmayprovideacustomizedcontexttoalterthedefaultconfig,e.g.toseta"reuse duration".//Seehttps://developer.apple.com/documentation/localauthentication/accessing-keychain-items-with-face-id-or-touch-id#Optionally-Provide-a-Customized-Context//KeychainservicesautomaticallymakesuseoftheLocalAuthenticationframework,evenifyoudon't provide one.////letcontext=LAContext()//context.touchIDAuthenticationAllowableReuseDuration=10letstoreQuery:[String:Any]=[kSecClassasString:kSecClassGenericPassword,kSecAttrAccountasString:account,kSecValueDataasString:tokenData,kSecAttrAccessControlasString:accessControl,//kSecUseAuthenticationContextasString:context,]//Beforeadding,wedeleteanyexistingitemSecItemDelete(storeQueryasCFDictionary)letstoreStatus=SecItemAdd(storeQueryasCFDictionary,nil)guardstoreStatus==errSecSuccesselse{completion("❌ Failed to store token in Keychain (status \(storeStatus))")return}//2.Nowlet's retrieve the token//Optional:youmayprovideacontextwithalocalizedreason.//Seehttps://developer.apple.com/documentation/localauthentication/accessing-keychain-items-with-face-id-or-touch-id#Provide-a-Prompt-When-Reading-the-Item//KeychainserviceswilluseLAContexttoprompttheuserevenifyoudon't provide one.////letcontext=LAContext()//context.localizedReason="Access your token from the keychain"letfetchQuery:[String:Any]=[kSecClassasString:kSecClassGenericPassword,kSecAttrAccountasString:account,kSecReturnDataasString:true,kSecMatchLimitasString:kSecMatchLimitOne,//kSecUseAuthenticationContextasString:context,]varresult:CFTypeRef?letfetchStatus=SecItemCopyMatching(fetchQueryasCFDictionary,&result)iffetchStatus==errSecSuccess,letdata=resultas?Data,lettoken=String(data:data,encoding:.utf8){completion("✅ Retrieved token: \(token)")}else{completion("❌ Authentication failed or token inaccessible (status \(fetchStatus))")}}}
Unzip the app package and locate the main binary file ( Exploring the App Package), which in this case is ./Payload/MASTestApp.app/MASTestApp.
Run run.sh.
biometricAuthenticationFallback.r2
1 2 3 4 5 6 7 8 91011121314
easm.bytes=falseescr.color=falseeasm.var=false?ePrintxrefsto \'Run analysis\"aaa?ePrintxrefsto \'SecAccessControlCreateWithFlags\"axt@sym.imp.SecAccessControlCreateWithFlags?e?ePrintdisassemblyaround \"SecAccessControlCreateWithFlags\" in the functionpdf@0x100004194|grep-C5"SecAccessControlCreateWithFlags"
The output reveals the use of SecAccessControlCreateWithFlags(allocator, protection, flags, error) in the app. In this demo, we focus on the flags argument because it specifies the Access Control. flags is a third argument of the function, so it's at x2/w2 register. By looking at the output, we can see that w2 register holds value of 1.
The flags is an enum of SecAccessControlCreateFlags. 1 corresponds with kSecAccessControlUserPresence (see LAPublicDefines.h). This means that the app invokes SecAccessControlCreateWithFlags(..., kSecAccessControlUserPresence), which means it falls back to device's passcode authentication.
The test fails because the output shows references to biometric verification that falls back to device's passcode authentication, specifically kSecAccessControlUserPresence.