From 2a68657bf11b9429f8e9aa948d62d8487233792d Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Thu, 12 Nov 2026 14:00:52 +0200 Subject: [PATCH 1/8] Raise KeyRevokedException when re-enrolment removes or invalidates the key Android: a key deleted after all biometrics were removed was reported as 'No key found'. iOS: an item invalidated by a Face ID/Touch ID change was reported as a generic error; iOS never raised KeyRevokedException. Co-Authored-By: Claude Opus 5.5 --- .../impl/InternalFingerprintImpl.java | 8 +++++ ...fingerprint_impl_InternalFingerprintImpl.m | 36 +++++++++++++++++++ 2 files changed, 44 insertions(+) diff --git a/android/src/main/java/com/codename1/fingerprint/impl/InternalFingerprintImpl.java b/android/src/main/java/com/codename1/fingerprint/impl/InternalFingerprintImpl.java index a7798a2..06239e0 100644 --- a/android/src/main/java/com/codename1/fingerprint/impl/InternalFingerprintImpl.java +++ b/android/src/main/java/com/codename1/fingerprint/impl/InternalFingerprintImpl.java @@ -918,6 +918,14 @@ private SecretKey getSecretKey() { try { mKeyStore().load(null); key = (SecretKey) mKeyStore.getKey(KEY_ID, null); + if (key == null) { + // Android deletes a biometric-bound key outright when the user removes + // EVERY enrolled biometric, instead of failing with + // KeyPermanentlyInvalidatedException. Both callers that read keyRevoked + // return first when no password is stored, so an absent key there means + // a stored password whose key was taken away: revoked, not "No key found". + keyRevoked = true; + } } catch (KeyStoreException e) { errorMessage = getSecretKeyExceptionErrorPrefix + "KeyStoreException"; } catch (CertificateException e) { 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 7bf9c4e..2ddfb33 100644 --- a/ios/src/main/objectivec/com_codename1_fingerprint_impl_InternalFingerprintImpl.m +++ b/ios/src/main/objectivec/com_codename1_fingerprint_impl_InternalFingerprintImpl.m @@ -257,6 +257,27 @@ -(void)deletePassword:(int)requestId param1:(NSString*)reason param2:(NSString*) } }); } +// Attributes only: not guarded by the access-control list, so no prompt, and an item whose +// biometric binding was invalidated is still found. +-(BOOL)itemExists:(NSString*)account { + NSMutableDictionary *q = [NSMutableDictionary dictionary]; + [q setObject:(__bridge id)kSecClassGenericPassword forKey:(__bridge id)kSecClass]; + [q setObject:account forKey:(__bridge id)kSecAttrAccount]; + [q setObject:[self getAppName] forKey:(__bridge id)kSecAttrService]; + [q setObject:@YES forKey:(__bridge id)kSecReturnAttributes]; + [q setObject:(__bridge id)kSecMatchLimitOne forKey:(__bridge id)kSecMatchLimit]; + NSString* accessGroup = [self getKeychainAccessGroup]; + if (accessGroup != nil) { + [q setObject:accessGroup forKey:(__bridge id)kSecAttrAccessGroup]; + } + CFTypeRef attrs = NULL; + OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)q, &attrs); + if (attrs != NULL) { + CFRelease(attrs); + } + return status == errSecSuccess; +} + -(void)getPassword:(int)requestId param1:(NSString*)reason param2:(NSString*)account { NSMutableDictionary *dict = [NSMutableDictionary dictionary]; @@ -284,6 +305,21 @@ -(void)getPassword:(int)requestId param1:(NSString*)reason param2:(NSString*)acc } else if (status == errSecUserCanceled) { JAVA_OBJECT jErrorMessage = fromNSString(getThreadLocalData(), @"__CANCELLED__"); com_codename1_fingerprint_impl_InternalCallback_requestError___int_java_lang_String(getThreadLocalData(), requestId, jErrorMessage); + } else if (status == errSecItemNotFound && [self itemExists:account]) { + // Stored under kSecAccessControlTouchIDCurrentSet: a change to the enrolled + // Face ID / Touch ID set invalidates the item. Its attributes remain but the + // data read answers errSecItemNotFound. Report it as revoked, as Android does, + // and delete it, since it can never be read again and would break a later add. + NSMutableDictionary *del = [NSMutableDictionary dictionary]; + [del setObject:(__bridge id)kSecClassGenericPassword forKey:(__bridge id)kSecClass]; + [del setObject:account forKey:(__bridge id)kSecAttrAccount]; + [del setObject:[self getAppName] forKey:(__bridge id)kSecAttrService]; + if (accessGroup != nil) { + [del setObject:accessGroup forKey:(__bridge id)kSecAttrAccessGroup]; + } + SecItemDelete((__bridge CFDictionaryRef)del); + JAVA_OBJECT jErrorMessage = fromNSString(getThreadLocalData(), @"Key has been invalidated"); + com_codename1_fingerprint_impl_InternalCallback_requestKeyRevokedError___int_java_lang_String(getThreadLocalData(), requestId, jErrorMessage); } else { NSString* errorMessage = [self errorString:status]; JAVA_OBJECT jErrorMessage = fromNSString(getThreadLocalData(), errorMessage); From 5e48561c9e234883e271764b08c807ea09ae1283 Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Thu, 12 Nov 2026 14:04:14 +0200 Subject: [PATCH 2/8] Report a revoked key even when no biometric is enrolled any more Co-Authored-By: Claude Opus 5.5 --- .../impl/InternalFingerprintImpl.java | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/android/src/main/java/com/codename1/fingerprint/impl/InternalFingerprintImpl.java b/android/src/main/java/com/codename1/fingerprint/impl/InternalFingerprintImpl.java index 06239e0..f2cf538 100644 --- a/android/src/main/java/com/codename1/fingerprint/impl/InternalFingerprintImpl.java +++ b/android/src/main/java/com/codename1/fingerprint/impl/InternalFingerprintImpl.java @@ -604,6 +604,14 @@ private boolean getPassword29(final int requestId, final String reason, final St InternalCallback.requestError(requestId,"The specified item could not be found in the keychain."); return false; } + // Before the availability check: when the user deleted EVERY biometric, nothing + // is enrolled, so that check fails and the password would be reported as "not + // available" -- although Android has already deleted its key and it can never be + // read again, even after re-enrolment. A stored password with no key is revoked. + if (getSecretKey() == null && keyRevoked) { + InternalCallback.requestKeyRevokedError(requestId, "Key has been invalidated"); + return false; + } if (isBiometricAuthAvailable()) { SecretKey secretKey = getSecretKey(); @@ -736,6 +744,14 @@ private boolean getPassword(final int requestId, final String reason, final Stri InternalCallback.requestError(requestId,"The specified item could not be found in the keychain."); return false; } + // Before the availability check: when the user deleted EVERY biometric, nothing + // is enrolled, so that check fails and the password would be reported as "not + // available" -- although Android has already deleted its key and it can never be + // read again, even after re-enrolment. A stored password with no key is revoked. + if (getSecretKey() == null && keyRevoked) { + InternalCallback.requestKeyRevokedError(requestId, "Key has been invalidated"); + return false; + } if (isFingerprintAuthAvailable()) { SecretKey secretKey = getSecretKey(); From d2f71b72dafbfb01e9fff64e0932f20a6b8e1d17 Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Thu, 12 Nov 2026 14:10:13 +0200 Subject: [PATCH 3/8] ci: stop installing the removed legacy 'tools' SDK package Co-Authored-By: Claude Opus 5.5 --- .github/workflows/ci.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 01012e3..2ef7c50 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,6 +23,10 @@ jobs: - name: Set up Android SDK uses: android-actions/setup-android@v3 + with: + # The action's default list includes the legacy 'tools' package, which current + # SDK repositories no longer carry, so sdkmanager exits 1 before anything builds. + packages: platform-tools - name: Set up Codename One Build Client shell: bash From 52b2e82767dbb1819f0c7ae48dd9fad6cd1080e0 Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Thu, 12 Nov 2026 15:15:41 +0200 Subject: [PATCH 4/8] ci: install the API 37 platform the generated project compiles against Co-Authored-By: Claude Opus 5.5 --- .github/workflows/ci.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2ef7c50..a422b60 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,7 +26,9 @@ jobs: with: # The action's default list includes the legacy 'tools' package, which current # SDK repositories no longer carry, so sdkmanager exits 1 before anything builds. - packages: platform-tools + # The generated project compiles against API 37, which sdkmanager only offers with + # a minor suffix: a bare 'platforms;android-37' exits 0 and installs nothing. + packages: platform-tools platforms;android-37.0 - name: Set up Codename One Build Client shell: bash From 58b000201f88db8eb41826e03dfc9dbe6ac16dc8 Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Thu, 12 Nov 2026 15:35:41 +0200 Subject: [PATCH 5/8] Revert "ci: install the API 37 platform the generated project compiles against" This reverts commit 52b2e82767dbb1819f0c7ae48dd9fad6cd1080e0. --- .github/workflows/ci.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a422b60..2ef7c50 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,9 +26,7 @@ jobs: with: # The action's default list includes the legacy 'tools' package, which current # SDK repositories no longer carry, so sdkmanager exits 1 before anything builds. - # The generated project compiles against API 37, which sdkmanager only offers with - # a minor suffix: a bare 'platforms;android-37' exits 0 and installs nothing. - packages: platform-tools platforms;android-37.0 + packages: platform-tools - name: Set up Codename One Build Client shell: bash From 6ea4c30f60135f953fba8c53d15e60fdebd1581a Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Thu, 12 Nov 2026 15:35:42 +0200 Subject: [PATCH 6/8] ci: build the test app with the current Codename One release 7.0.228 predates the builder's Gradle 9 support: it picked the runner's API 37 platform but generated a Gradle 8.1 project that cannot resolve it. Co-Authored-By: Claude Opus 5.5 --- fingerprint-scanner-test/pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fingerprint-scanner-test/pom.xml b/fingerprint-scanner-test/pom.xml index 1f418ac..a3a7418 100644 --- a/fingerprint-scanner-test/pom.xml +++ b/fingerprint-scanner-test/pom.xml @@ -19,8 +19,8 @@ common - 7.0.228 - 7.0.228 + 7.0.281 + 7.0.281 UTF-8 1.8 11 From 03f7f2577c73f9d1444a25c798d6b4313ecd5b21 Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Thu, 12 Nov 2026 15:46:53 +0200 Subject: [PATCH 7/8] ci: resolve Codename One releases from repo.codenameone.com Co-Authored-By: Claude Opus 5.5 --- fingerprint-scanner-test/pom.xml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/fingerprint-scanner-test/pom.xml b/fingerprint-scanner-test/pom.xml index a3a7418..4a18a12 100644 --- a/fingerprint-scanner-test/pom.xml +++ b/fingerprint-scanner-test/pom.xml @@ -32,6 +32,20 @@ myappname 1.1.4 + + + + codenameone + https://repo.codenameone.com/maven2 + + + + + codenameone + https://repo.codenameone.com/maven2 + + From 5def8479142144cf61c32e2bbeccf907d3e7161e Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Thu, 12 Nov 2026 16:15:00 +0200 Subject: [PATCH 8/8] ci: drop the empty repository tags the new ones duplicate Co-Authored-By: Claude Opus 5.5 --- fingerprint-scanner-test/pom.xml | 2 -- 1 file changed, 2 deletions(-) diff --git a/fingerprint-scanner-test/pom.xml b/fingerprint-scanner-test/pom.xml index 4a18a12..fb86418 100644 --- a/fingerprint-scanner-test/pom.xml +++ b/fingerprint-scanner-test/pom.xml @@ -121,8 +121,6 @@ - - javascript