Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down Expand Up @@ -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();

Expand Down Expand Up @@ -918,6 +934,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) {
Expand Down
20 changes: 16 additions & 4 deletions fingerprint-scanner-test/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@
<module>common</module>
</modules>
<properties>
<cn1.plugin.version>7.0.228</cn1.plugin.version>
<cn1.version>7.0.228</cn1.version>
<cn1.plugin.version>7.0.281</cn1.plugin.version>
<cn1.version>7.0.281</cn1.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>1.8</java.version>
<java-tests.version>11</java-tests.version>
Expand All @@ -32,6 +32,20 @@
<cn1app.name>myappname</cn1app.name>
<fingerprint.scanner.version>1.1.4</fingerprint.scanner.version>
</properties>
<!-- Codename One releases are published to repo.codenameone.com, not Maven Central,
which only has versions from before the cutover. -->
<repositories>
<repository>
<id>codenameone</id>
<url>https://repo.codenameone.com/maven2</url>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>codenameone</id>
<url>https://repo.codenameone.com/maven2</url>
</pluginRepository>
</pluginRepositories>
<dependencyManagement>
<dependencies>
<dependency>
Expand Down Expand Up @@ -107,8 +121,6 @@
</plugin>
</plugins>
</build>
<repositories/>
<pluginRepositories/>
<profiles>
<profile>
<id>javascript</id>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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];

Expand Down Expand Up @@ -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);
Expand Down
Loading