Skip to content

Biometrics: report KEY_REVOKED when re-enrolment removes or invalidates the key - #6221

Open
shai-almog wants to merge 1 commit into
masterfrom
biometrics-key-revoked-on-reenroll
Open

shai-almog wants to merge 1 commit into
masterfrom
biometrics-key-revoked-on-reenroll

Conversation

@shai-almog

Copy link
Copy Markdown
Collaborator

A biometric secure-storage read after the user removed every biometric, or re-enrolled Face ID / Touch ID, failed as BiometricError.UNKNOWN instead of KEY_REVOKED, so apps could not tell a re-enrolment from an error.

  • Android (AndroidSecureStorage.getSecretKey): Android deletes a biometric-bound key when every biometric is removed. get() returns before the decrypt path when nothing is stored, so a missing key there means an entry whose key was taken away: now KEY_REVOKED.
  • iOS (IOSNative.m, IOSSecureStorage): an item bound to kSecAccessControlTouchIDCurrentSet answers errSecItemNotFound to a data read after the enrolled set changes, while an attributes-only query (no ACL, no prompt) still finds it. That combination now reports KEY_REVOKED (a non-OSStatus code, -990001) and deletes the unreadable item, as Android deletes its invalidated key. iOS previously never reported KEY_REVOKED.

🤖 Generated with Claude Code

…es the key

Android deletes a biometric-bound keystore key when every biometric is
removed, so a later read found no key and failed as UNKNOWN. On iOS an item
bound to the current Face ID/Touch ID set answers errSecItemNotFound after
re-enrolment, which also mapped to UNKNOWN; iOS never reported KEY_REVOKED.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@shai-almog

shai-almog commented Nov 12, 2026 •

Copy link
Copy Markdown
Collaborator Author

Compared 195 screenshots: 195 matched.

Native Android coverage

  • 📊 Line coverage: 19.18% (56167/292885 lines covered) [HTML preview] (artifact android-coverage-report, jacocoAndroidReport/html/index.html)
    • Other counters: instruction 18.13% (270598/1492353), branch 12.38% (19677/158943), complexity 10.36% (15846/153017), method 15.73% (11410/72518), class 27.93% (2247/8045)
    • Lowest covered classes
      • kotlin.collections.unsigned.kotlin.collections.unsigned.UArraysKt___UArraysKt – 0.00% (0/2396 lines covered)
      • org.jacoco.agent.rt.internal_0e20598.asm.org.jacoco.agent.rt.internal_0e20598.asm.ClassReader – 0.00% (0/1524 lines covered)
      • androidx.compose.runtime.androidx.compose.runtime.LinkComposer – 0.00% (0/1430 lines covered)
      • org.jacoco.agent.rt.internal_0e20598.asm.org.jacoco.agent.rt.internal_0e20598.asm.MethodWriter – 0.00% (0/922 lines covered)
      • kotlinx.coroutines.channels.kotlinx.coroutines.channels.BufferedChannel – 0.00% (0/863 lines covered)
      • kotlin.sequences.kotlin.sequences.SequencesKt___SequencesKt – 0.00% (0/744 lines covered)
      • com.google.common.cache.com.google.common.cache.LocalCache$Segment – 0.00% (0/726 lines covered)
      • okio.okio.Buffer – 0.00% (0/687 lines covered)
      • kotlin.text.kotlin.text.StringsKt___StringsKt – 0.00% (0/630 lines covered)
      • com.codename1.androidcompat.rt.com.codename1.androidcompat.rt.TextXmlPullParser – 0.00% (0/623 lines covered)

✅ Native Android screenshot tests passed.

Native Android coverage

  • 📊 Line coverage: 19.18% (56167/292885 lines covered) [HTML preview] (artifact android-coverage-report, jacocoAndroidReport/html/index.html)
    • Other counters: instruction 18.13% (270598/1492353), branch 12.38% (19677/158943), complexity 10.36% (15846/153017), method 15.73% (11410/72518), class 27.93% (2247/8045)
    • Lowest covered classes
      • kotlin.collections.unsigned.kotlin.collections.unsigned.UArraysKt___UArraysKt – 0.00% (0/2396 lines covered)
      • org.jacoco.agent.rt.internal_0e20598.asm.org.jacoco.agent.rt.internal_0e20598.asm.ClassReader – 0.00% (0/1524 lines covered)
      • androidx.compose.runtime.androidx.compose.runtime.LinkComposer – 0.00% (0/1430 lines covered)
      • org.jacoco.agent.rt.internal_0e20598.asm.org.jacoco.agent.rt.internal_0e20598.asm.MethodWriter – 0.00% (0/922 lines covered)
      • kotlinx.coroutines.channels.kotlinx.coroutines.channels.BufferedChannel – 0.00% (0/863 lines covered)
      • kotlin.sequences.kotlin.sequences.SequencesKt___SequencesKt – 0.00% (0/744 lines covered)
      • com.google.common.cache.com.google.common.cache.LocalCache$Segment – 0.00% (0/726 lines covered)
      • okio.okio.Buffer – 0.00% (0/687 lines covered)
      • kotlin.text.kotlin.text.StringsKt___StringsKt – 0.00% (0/630 lines covered)
      • com.codename1.androidcompat.rt.com.codename1.androidcompat.rt.TextXmlPullParser – 0.00% (0/623 lines covered)

Benchmark Results

Detailed Performance Metrics

Metric Duration
SIMD kernel backend scalar fallback (no native SIMD)
SIMD int-add (64K x300) java 197ms / native 183ms = 1.0x speedup
SIMD float-mul (64K x300) java 220ms / native 80ms = 2.7x speedup
SIMD kernel correctness PASS (native result == scalar reference)
Base64 payload size 8192 bytes
Base64 benchmark iterations 6000
Base64 SIMD byte path gated to scalar (CPU autovectorizes scalar; explicit SIMD not beneficial here)
Base64 CN1 encode 91.000 ms
Base64 CN1 decode 77.000 ms
Base64 native encode 664.000 ms
Base64 encode ratio (CN1/native) 0.137x (86.3% faster)
Base64 native decode 349.000 ms
Base64 decode ratio (CN1/native) 0.221x (77.9% faster)
Image encode benchmark status skipped (SIMD unsupported)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant