From 9e72b6da7b9ec61a0848fff0de12d1fe307c04f8 Mon Sep 17 00:00:00 2001 From: Steve Hannah Date: Sat, 11 Oct 2025 15:29:01 -0700 Subject: [PATCH] Support for keychain access groups on iOS --- .../Maven__com_jhlabs_filters_2_0_235_1.xml | 13 -- ...aven__org_xerial_sqlite_jdbc_3_7_15_M1.xml | 13 -- .idea/modules.xml | 18 -- README.adoc | 155 +++++++++++++++++- ...fingerprint_impl_InternalFingerprintImpl.m | 47 ++++++ pom.xml | 1 - .../fingerprint-scanner-tests-common.iml | 19 --- .../fingerprint-scanner-tests-javase.iml | 25 --- 8 files changed, 201 insertions(+), 90 deletions(-) delete mode 100644 .idea/libraries/Maven__com_jhlabs_filters_2_0_235_1.xml delete mode 100644 .idea/libraries/Maven__org_xerial_sqlite_jdbc_3_7_15_M1.xml delete mode 100644 .idea/modules.xml delete mode 100644 tests/common/fingerprint-scanner-tests-common.iml delete mode 100644 tests/javase/fingerprint-scanner-tests-javase.iml diff --git a/.idea/libraries/Maven__com_jhlabs_filters_2_0_235_1.xml b/.idea/libraries/Maven__com_jhlabs_filters_2_0_235_1.xml deleted file mode 100644 index f674d8e..0000000 --- a/.idea/libraries/Maven__com_jhlabs_filters_2_0_235_1.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/libraries/Maven__org_xerial_sqlite_jdbc_3_7_15_M1.xml b/.idea/libraries/Maven__org_xerial_sqlite_jdbc_3_7_15_M1.xml deleted file mode 100644 index 574c7a0..0000000 --- a/.idea/libraries/Maven__org_xerial_sqlite_jdbc_3_7_15_M1.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml deleted file mode 100644 index 57274cf..0000000 --- a/.idea/modules.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/README.adoc b/README.adoc index 5e8cb8a..295879a 100644 --- a/README.adoc +++ b/README.adoc @@ -170,7 +170,7 @@ The security settings on the passwords are: ---- SecAccessControlRef sacRef = SecAccessControlCreateWithFlags(kCFAllocatorDefault, kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly, - kSecAccessControlTouchIDCurrentSet, + kSecAccessControlTouchIDCurrentSet, nil ); ---- @@ -181,6 +181,159 @@ For more details on what these mean, see the following documentation pages: . https://developer.apple.com/documentation/security/ksecattraccessiblewhenpasscodesetthisdeviceonly?language=objc[kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly] . https://developer.apple.com/documentation/security/secaccesscontrolcreateflags/ksecaccesscontroltouchidcurrentset?language=objc[kSecAccessControlTouchIDCurrentSet] +==== Sharing Keychain Items with App Extensions + +By default, keychain items stored by the Fingerprint API are only accessible from the main app. To share keychain items with app extensions (such as Action Extensions, Share Extensions, or other extension types), you can specify a shared keychain access group. + +This is particularly useful for scenarios like: + +* Apple Wallet provisioning flows that require an Action Extension to access authentication tokens +* Share Extensions that need to authenticate with stored credentials +* Today Widget Extensions that display authenticated content + +===== Step 1: Configure Keychain Access Group Entitlement + +Add the keychain access group to your Codename One project using the `ios.keychainAccessGroup` build hint. This can be set in your project's `codenameone_settings.properties` file or through the Codename One Settings GUI: + +---- +ios.keychainAccessGroup=TEAMID123.group.com.example.myapp +---- + +Replace `TEAMID123.group.com.example.myapp` with your full keychain access group identifier, which **must** include your Team ID (also called App Identifier Prefix). The access group format is: + +---- +[TEAM_ID].[group_name] +---- + +For example: `ABCDE12345.group.com.example.myapp` + +**Finding Your Team ID:** + +You can find your Team ID in: + +* Your Apple Developer account (under Membership details) +* Xcode → Project Settings → Signing & Capabilities → Team (shown in parentheses) +* Certificates, Identifiers & Profiles section of developer.apple.com + +The access group must be consistent across your main app and all extensions that need access. + +[IMPORTANT] +==== +The Team ID prefix is **required** for keychain access groups to work properly. Without it, keychain operations will fail with errSecMissingEntitlement. +==== + +===== Step 2: Configure Extensions + +If you have app extensions, ensure they also have the same keychain access group entitlement configured. For native iOS extensions, add the keychain-access-groups entitlement in the extension's Info.plist or entitlements file: + +[source,xml] +---- +keychain-access-groups + + $(AppIdentifierPrefix)group.com.example.myapp + +---- + +===== Step 3: Set the Display Property in Your Code + +Before using the Fingerprint API to store or retrieve passwords, set the keychain access group display property: + +[source,java] +---- +import com.codename1.ui.Display; +import com.codename1.fingerprint.Fingerprint; + +// Set this early in your app initialization, before using Fingerprint API +Display.getInstance().setProperty( + "Fingerprint.ios.keychainAccessGroup", + "TEAMID123.group.com.example.myapp" +); +---- + +[IMPORTANT] +==== +* Set the property **before** calling any Fingerprint password methods (addPassword, getPassword, deletePassword) +* Use the **exact same access group identifier including the Team ID prefix** that you configured in the build hint +* The property must be set in both your main app and your extensions +* The Team ID prefix is **required** - omitting it will cause keychain operations to fail +==== + +===== Complete Example + +Here's a complete example for a main app that shares credentials with an Action Extension: + +[source,java] +---- +// In your main app's start() method or before using Fingerprint +public void start() { + if (current != null) { + current.show(); + return; + } + + // Configure shared keychain access group (must include Team ID prefix) + Display.getInstance().setProperty( + "Fingerprint.ios.keychainAccessGroup", + "TEAMID123.group.com.example.myapp" + ); + + // Now store credentials that will be accessible from extensions + String account = "user@example.com"; + String token = "auth_token_12345"; + + Fingerprint.addPassword( + "Storing credentials for extension access", + account, + token + ).onResult((success, err) -> { + if (err != null) { + Log.e(err); + ToastBar.showErrorMessage("Failed to store credentials: " + err.getMessage()); + } else { + Log.p("Credentials stored in shared keychain group"); + ToastBar.showInfoMessage("Credentials saved"); + } + }); +} +---- + +In your extension code (whether native or using Codename One): + +[source,java] +---- +// Set the same property in your extension (must include Team ID prefix) +Display.getInstance().setProperty( + "Fingerprint.ios.keychainAccessGroup", + "TEAMID123.group.com.example.myapp" +); + +// Retrieve credentials stored by the main app +String account = "user@example.com"; + +Fingerprint.getPassword( + "Authenticating from extension", + account +).onResult((token, err) -> { + if (err != null) { + Log.e(err); + ToastBar.showErrorMessage("Failed to retrieve credentials: " + err.getMessage()); + } else { + Log.p("Retrieved token: " + token); + // Use token for authentication in extension + performAuthentication(token); + } +}); +---- + +===== Important Notes + +* **Backward Compatibility**: If you don't set the `Fingerprint.ios.keychainAccessGroup` property, keychain items will be stored in the default app-specific keychain group (current behavior) +* **Migration**: Items stored without an access group cannot be accessed when an access group is specified, and vice versa. If you're adding this feature to an existing app, users may need to re-authenticate +* **Security**: All targets (app and extensions) with the shared access group entitlement can access the keychain items. Only share with trusted extensions +* **Access Group Format**: Both the `ios.keychainAccessGroup` build hint AND the `Fingerprint.ios.keychainAccessGroup` property must include the full access group identifier with the Team ID prefix (e.g., `TEAMID123.group.com.example.myapp`) +* **Team ID is Required**: The Team ID prefix is mandatory in both the build hint and the runtime property. Omitting it will cause keychain operations to fail with errSecMissingEntitlement +* **Error Handling**: If your app attempts to use an access group it's not entitled to, keychain operations will fail with an error (errSecMissingEntitlement). Ensure the build hint and property match exactly + == Working with the Sources diff --git a/ios/src/main/objectivec/com_codename1_fingerprint_impl_InternalFingerprintImpl.m b/ios/src/main/objectivec/com_codename1_fingerprint_impl_InternalFingerprintImpl.m index e85f844..7bf9c4e 100644 --- a/ios/src/main/objectivec/com_codename1_fingerprint_impl_InternalFingerprintImpl.m +++ b/ios/src/main/objectivec/com_codename1_fingerprint_impl_InternalFingerprintImpl.m @@ -121,6 +121,30 @@ -(NSString*)getAppName { return nsres; } +-(NSString*)getKeychainAccessGroup { + struct ThreadLocalData* threadStateData = getThreadLocalData(); + enteringNativeAllocations(); + + JAVA_OBJECT key = fromNSString(CN1_THREAD_GET_STATE_PASS_ARG @"Fingerprint.ios.keychainAccessGroup"); + JAVA_OBJECT defaultVal = fromNSString(CN1_THREAD_GET_STATE_PASS_ARG @""); + + JAVA_OBJECT res = com_codename1_ui_CN_getProperty___java_lang_String_java_lang_String_R_java_lang_String( + CN1_THREAD_GET_STATE_PASS_ARG + key, + defaultVal + ); + finishedNativeAllocations(); + + NSString *nsres = toNSString(CN1_THREAD_GET_STATE_PASS_ARG res); + + // Return nil if empty string (to maintain default behavior) + if (nsres == nil || [nsres length] == 0) { + return nil; + } + + return nsres; +} + -(void)updatePassword:(int)requestId reason:(NSString*)reason account:(NSString*)account password:(NSString*)password { NSMutableDictionary *query = [NSMutableDictionary dictionary]; @@ -132,6 +156,11 @@ -(void)updatePassword:(int)requestId reason:(NSString*)reason account:(NSString* [query setObject:[self getAppName] forKey:(__bridge id) kSecAttrService]; [query setObject:reason forKey:(__bridge id)kSecUseOperationPrompt]; + // Add custom access group if specified + NSString* accessGroup = [self getKeychainAccessGroup]; + if (accessGroup != nil) { + [query setObject:accessGroup forKey:(__bridge id)kSecAttrAccessGroup]; + } NSMutableDictionary *changes = [NSMutableDictionary dictionary]; [changes setObject:[password dataUsingEncoding:NSUTF8StringEncoding] forKey:(__bridge id)kSecValueData]; @@ -165,6 +194,12 @@ -(void)addPassword:(int)requestId param1:(NSString*)reason param2:(NSString*)acc [dict setObject:(__bridge id)sacRef forKey:(__bridge id)kSecAttrAccessControl]; [dict setObject:reason forKey:(__bridge id)kSecUseOperationPrompt]; + // Add custom access group if specified + NSString* accessGroup = [self getKeychainAccessGroup]; + if (accessGroup != nil) { + [dict setObject:accessGroup forKey:(__bridge id)kSecAttrAccessGroup]; + } + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ OSStatus status = SecItemAdd((__bridge CFDictionaryRef)dict, nil); if (status == errSecDuplicateItem) { @@ -201,6 +236,12 @@ -(void)deletePassword:(int)requestId param1:(NSString*)reason param2:(NSString*) [dict setObject:[self getAppName] forKey:(__bridge id) kSecAttrService]; //[dict setObject:reason forKey:(__bridge id)kSecUseOperationPrompt]; + // Add custom access group if specified + NSString* accessGroup = [self getKeychainAccessGroup]; + if (accessGroup != nil) { + [dict setObject:accessGroup forKey:(__bridge id)kSecAttrAccessGroup]; + } + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ OSStatus status = SecItemDelete((__bridge CFDictionaryRef)dict); @@ -226,6 +267,12 @@ -(void)getPassword:(int)requestId param1:(NSString*)reason param2:(NSString*)acc [dict setObject:[self getAppName] forKey:(__bridge id) kSecAttrService]; [dict setObject:reason forKey:(__bridge id)kSecUseOperationPrompt]; + // Add custom access group if specified + NSString* accessGroup = [self getKeychainAccessGroup]; + if (accessGroup != nil) { + [dict setObject:accessGroup forKey:(__bridge id)kSecAttrAccessGroup]; + } + dispatch_async(dispatch_get_main_queue(), ^{ CFTypeRef dataRef = NULL; OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)dict, &dataRef); diff --git a/pom.xml b/pom.xml index 9b488d6..093281e 100644 --- a/pom.xml +++ b/pom.xml @@ -67,7 +67,6 @@ javase win lib - tests diff --git a/tests/common/fingerprint-scanner-tests-common.iml b/tests/common/fingerprint-scanner-tests-common.iml deleted file mode 100644 index d266478..0000000 --- a/tests/common/fingerprint-scanner-tests-common.iml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/tests/javase/fingerprint-scanner-tests-javase.iml b/tests/javase/fingerprint-scanner-tests-javase.iml deleted file mode 100644 index 553a563..0000000 --- a/tests/javase/fingerprint-scanner-tests-javase.iml +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file