Skip to content

feat: add authenticationFreshness option to force a fresh auth prompt on read - #798

Open
idanlevi1 wants to merge 1 commit into
oblador:masterfrom
idanlevi1:feat/force-fresh-authentication
Open

feat: add authenticationFreshness option to force a fresh auth prompt on read#798
idanlevi1 wants to merge 1 commit into
oblador:masterfrom
idanlevi1:feat/force-fresh-authentication

Conversation

@idanlevi1

Copy link
Copy Markdown

Motivation

By default a protected keychain read can be satisfied without prompting when the user authenticated (or unlocked the device) very recently:

  • iOS/visionOS — an item protected only by the device passcode is returned silently by SecItemCopyMatching while the device is unlocked (no prompt), and biometric reads honor touchIDAuthenticationAllowableReuseDuration.
  • Android — the underlying Keystore key stays usable for a short validity window (~5s) after the last authentication (validityDuration in getKeyGenSpecBuilder), so a read inside that window decrypts without a new BiometricPrompt.

This is the behavior noted in the AUTH_INVALIDATED docs: "Due to the way this library currently handles user authentication validity duration…".

For high-assurance actions (transaction signing, step-up auth) an app needs a guaranteed fresh prompt on every read. Today there's no way to request that.

What this adds

A new opt-in, typed GetOptions field:

import { getGenericPassword, AUTHENTICATION_FRESHNESS } from 'react-native-keychain'

await getGenericPassword({
  service: 'signing-seed',
  authenticationFreshness: AUTHENTICATION_FRESHNESS.REQUIRE_FRESH,
})
enum AUTHENTICATION_FRESHNESS {
  ALLOW_REUSE = 'AllowReuse',    // default — current behavior
  REQUIRE_FRESH = 'RequireFresh' // force a fresh OS prompt on every read
}

Implementation

  • iOS/visionOS (RNKeychainManager.mm) — when REQUIRE_FRESH, evaluate the device-owner policy (via the existing authPolicy(options), so authenticationType is respected) on a dedicated LAContext with touchIDAuthenticationAllowableReuseDuration = 0, then pass that context via kSecUseAuthenticationContext so the just-granted authorization satisfies the lookup without a second prompt.
  • Android (KeychainModuleResultHandler.requireFreshAuthCipherStorageKeystoreAesGcm / CipherStorageKeystoreRsaEcb) — for auth-gated keys, drive the interactive BiometricPrompt up front instead of taking the optimistic decrypt that would succeed inside the validity window. Non auth-gated keys and all encrypt paths are unchanged.

Compatibility

  • Default behavior is unchanged — the option defaults to ALLOW_REUSE; existing callers are unaffected.
  • TurboModule spec unchanged (options is already a generic Object).
  • TSDoc added on the enum and the GetOptions field (feeds the generated API docs).

Testing

  • tsc passes on the changed TS.
  • Verified on-device: on a passcode-only iPhone the passcode sheet now appears on every gated read; without the flag the read stays silent as before.
  • Would appreciate maintainer guidance on adding coverage to KeychainExample and any Android instrumentation you'd like included.

cc @oblador @DorianMazur — happy to adjust the naming/shape (e.g. fold into an existing option) to match your preferences.

… on read

By default a protected keychain read can be satisfied without prompting when
the user authenticated (or unlocked the device) very recently:

- iOS: an item protected only by the device passcode is returned silently by
  SecItemCopyMatching while the device is unlocked, and biometric reads honor
  touchIDAuthenticationAllowableReuseDuration.
- Android: the underlying key stays usable for a short validity window (~5s,
  validityDuration in getKeyGenSpecBuilder) after the last authentication, so a
  read within that window decrypts without a new BiometricPrompt.

For high-assurance actions (signing, step-up auth) callers need a guaranteed
fresh prompt on every read. This adds an opt-in, typed GetOptions field
`authenticationFreshness: AUTHENTICATION_FRESHNESS`:

- ALLOW_REUSE (default) — unchanged behavior, a recent auth may satisfy the read.
- REQUIRE_FRESH — force a fresh OS authentication prompt on every read.

iOS: evaluate the device-owner policy (respecting authenticationType) on a
dedicated LAContext with a zero reuse duration, then pass it via
kSecUseAuthenticationContext so the granted authorization satisfies the lookup
without a second prompt.

Android: for auth-gated keys, drive the interactive BiometricPrompt up front
instead of the optimistic decrypt that would succeed inside the validity
window. Threaded to the cipher via a ResultHandler.requireFreshAuth flag; non
auth-gated keys and encrypt paths are unaffected.

Default behavior is unchanged; the option is fully backward compatible.
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