importSwiftUIimportLocalAuthenticationimportSecuritystructMastgTest{staticfuncmastgTest(completion:@escaping(String)->Void){letaccount="com.mastg.sectoken"lettokenData="8767086b9f6f976g-a8df76".data(using:.utf8)!//1.StorethetokenintheKeychainwithACLflags//1a.Createanaccess‐controlobjectrequiringuserpresenceguardletaccessControl=SecAccessControlCreateWithFlags(nil,kSecAttrAccessibleWhenUnlocked,.biometryAny,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))")}}}
constAccessControlFlags={kSecAccessControlUserPresence:1<<0,kSecAccessControlBiometryAny:1<<1,kSecAccessControlBiometryCurrentSet:1<<3,kSecAccessControlDevicePasscode:1<<4,kSecAccessControlWatch:1<<5,kSecAccessControlOr:1<<14,kSecAccessControlAnd:1<<15,kSecAccessControlPrivateKeyUsage:1<<30,kSecAccessControlApplicationPassword:1<<31,};Interceptor.attach(Module.getExportByName(null,'SecAccessControlCreateWithFlags'),{/* func SecAccessControlCreateWithFlags( _ allocator: CFAllocator?, _ protection: CFTypeRef, _ flags: SecAccessControlCreateFlags, _ error: UnsafeMutablePointer<Unmanaged<CFError>?>? ) -> SecAccessControl? */onEnter(args){constflags=args[2]constflags_description=parseAccessControlFlags(flags)console.log(`\SecAccessControlCreateWithFlags(..., 0x${flags.toString(16)}) called with ${flags_description}\n`)// Use an arrow function so that `this` remains the same as in onEnterconstprintBacktrace=(maxLines=8)=>{console.log("\nBacktrace:");letbacktrace=Thread.backtrace(this.context,Backtracer.ACCURATE).map(DebugSymbol.fromAddress);for(leti=0;i<Math.min(maxLines,backtrace.length);i++){console.log(backtrace[i]);}}printBacktrace();}});functionparseAccessControlFlags(value){constresult=[];for(const[name,bit]ofObject.entries(AccessControlFlags)){if((value&bit)===bit){result.push(name);}}returnresult;}
SecAccessControlCreateWithFlags(...,0x2)calledwithkSecAccessControlBiometryAnyBacktrace:0x102350198MASTestApp!specializedstaticMastgTest.createAccessControl()0x1023504b0MASTestApp!specializedstaticMastgTest.storeTokenInKeychain(secretToken:)0x102351b98MASTestApp!closure#1 in closure #1 in closure #1 in ContentView.body.getter0x19908bc54SwiftUI!partialapplyforclosure#1 in closure #2 in ContextMenuBridge.contextMenuInteraction(_:willPerformPreviewActionForMenuWith:animator:)0x198f986d0SwiftUI!partialapplyforspecializedthunkfor@callee_guaranteed()->(@outA,@error@ownedError)0x1996657c4SwiftUI!specializedstaticMainActor.assumeIsolated<A>(_:file:line:)0x1996321a8SwiftUI!ButtonAction.callAsFunction()0x198b14c2cSwiftUI!partialapplyforimplicitclosure#2 in implicit closure #1 in PlatformItemListButtonStyle.makeBody(configuration:)
The output reveals the use of SecAccessControlCreateWithFlags in the app and lists all used flags.
The test fails because the output shows the runtime use of SecAccessControlCreateWithFlags with kSecAccessControlBiometryAny (see LAPublicDefines.h), which accepts any additional biometrics added after the Keychain entry was created.
When it is required that the associated keychain item become inaccessible when changes are made to the biometric database (e.g., when a new fingerprint or face is added), the app must use thekSecAccessControlBiometryCurrentSet flag instead.