Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -1580,7 +1580,19 @@ private SecretKey getSecretKey() {
keyRevoked = false;
try {
keyStore().load(null);
return (SecretKey) keyStore.getKey(KEY_ID, null);
SecretKey k = (SecretKey) keyStore.getKey(KEY_ID, null);
if (k == null) {
// Android deletes a biometric-bound key outright when the user removes
// EVERY enrolled biometric (and on some devices when the set changes),
// rather than leaving it to fail with KeyPermanentlyInvalidatedException.
// The only caller that reads this flag is the decrypt path, and get()
// returns before reaching it when no entry is stored -- so an absent key
// there always means an entry whose key was taken away: revoked. Reporting
// it as a generic failure made apps treat a re-enrolment as an error
// instead of asking the user to set biometric login up again.
keyRevoked = true;
}
return k;
} catch (UnrecoverableKeyException e) {
keyRevoked = true;
} catch (KeyStoreException e) {
Expand Down
44 changes: 44 additions & 0 deletions Ports/iOSPort/nativeSources/IOSNative.m
Original file line number Diff line number Diff line change
Expand Up @@ -23045,6 +23045,31 @@ static BOOL cn1_secureStorageHasLegacyService(NSString *current, NSString *legac
return legacy != nil && current != nil && ![legacy isEqualToString:current];
}

// Not an OSStatus: Security.framework codes are all in -25xxx/-34xxx/-67xxx and -128, so this
// cannot collide. Must match IOSSecureStorage.KEY_REVOKED_CODE.
#define CN1_SECURE_STORAGE_KEY_REVOKED -990001

// Whether the biometric item is in the keychain at all. Asks for attributes only, which an
// access-control list does not guard, so it never prompts and still sees an item whose
// biometric binding was invalidated.
static BOOL cn1_secureStorageBiometricItemExists(NSString *account, NSString *appName, NSString *accessGroup) {
NSMutableDictionary *q = [NSMutableDictionary dictionary];
[q setObject:(__bridge id)kSecClassGenericPassword forKey:(__bridge id)kSecClass];
[q setObject:account forKey:(__bridge id)kSecAttrAccount];
[q setObject:appName forKey:(__bridge id)kSecAttrService];
[q setObject:@YES forKey:(__bridge id)kSecReturnAttributes];
[q setObject:(__bridge id)kSecMatchLimitOne forKey:(__bridge id)kSecMatchLimit];
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 com_codename1_impl_ios_IOSNative_secureStorageGet___int_java_lang_String_java_lang_String(CN1_THREAD_STATE_MULTI_ARG JAVA_OBJECT me, JAVA_INT requestId, JAVA_OBJECT reason, JAVA_OBJECT account) {
POOL_BEGIN();
NSString *nsReason = (reason == JAVA_NULL) ? @"Authenticate" : toNSString(CN1_THREAD_STATE_PASS_ARG reason);
Expand Down Expand Up @@ -23075,6 +23100,25 @@ void com_codename1_impl_ios_IOSNative_secureStorageGet___int_java_lang_String_ja
NSString *value = [[[NSString alloc] initWithData:d encoding:NSUTF8StringEncoding] autorelease];
JAVA_OBJECT jv = fromNSString(getThreadLocalData(), value);
com_codename1_impl_ios_IOSSecureStorage_nativeStorageStringResult___int_java_lang_String(getThreadLocalData(), requestId, jv);
} else if (status == errSecItemNotFound && cn1_secureStorageBiometricItemExists(nsAccount, appName, accessGroup)) {
// The item is stored under kSecAccessControlTouchIDCurrentSet, so enrolling a
// different Face ID / Touch ID set invalidates it: its attributes are still
// there, but the data read answers errSecItemNotFound. That is a revoked key,
// the same case Android reports as KEY_REVOKED, and it used to surface as a
// generic failure -- iOS apps could never tell re-enrolment from an error.
// The item can never be read again, and leaving it makes a later set() fall
// into SecItemUpdate on an invalidated item, so delete it, as Android deletes
// its invalidated key.
NSMutableDictionary *del = [NSMutableDictionary dictionary];
[del setObject:(__bridge id)kSecClassGenericPassword forKey:(__bridge id)kSecClass];
[del setObject:nsAccount forKey:(__bridge id)kSecAttrAccount];
[del setObject:appName forKey:(__bridge id)kSecAttrService];
if (accessGroup != nil) {
[del setObject:accessGroup forKey:(__bridge id)kSecAttrAccessGroup];
}
SecItemDelete((__bridge CFDictionaryRef)del);
JAVA_OBJECT jmsg = fromNSString(getThreadLocalData(), @"Key has been invalidated");
com_codename1_impl_ios_IOSSecureStorage_nativeStorageError___int_int_java_lang_String(getThreadLocalData(), requestId, CN1_SECURE_STORAGE_KEY_REVOKED, jmsg);
} else {
JAVA_OBJECT jmsg = fromNSString(getThreadLocalData(), [NSString stringWithFormat:@"OSStatus %d", (int)status]);
com_codename1_impl_ios_IOSSecureStorage_nativeStorageError___int_int_java_lang_String(getThreadLocalData(), requestId, (int)status, jmsg);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -278,10 +278,17 @@ private static AsyncResource<?> take(int requestId) {
}
}

/// Reported by the native get when the item survives but its Face ID / Touch ID binding
/// was invalidated by a change to the enrolled set. Not an OSStatus; must match
/// CN1_SECURE_STORAGE_KEY_REVOKED in IOSNative.m.
static final int KEY_REVOKED_CODE = -990001;

private static BiometricError mapStorageError(int code) {
// Reuse the LAError mapping (negative values) and the dominant
// OSStatus values returned by Security.framework.
switch (code) {
case KEY_REVOKED_CODE:
return BiometricError.KEY_REVOKED;
case -128: // errSecUserCanceled
return BiometricError.USER_CANCELED;
case -25291: // errSecNotAvailable
Expand Down
Loading