npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

react-native-biometric-signature

v2.0.0

Published

React Native biometric key creation, signing, authentication prompts, and decryption for iOS and Android.

Readme

react-native-biometric-signature

Biometric-gated cryptographic signing, key creation, decryption, and authentication prompts for React Native on iOS and Android.

npm version License: MIT Types: included Platforms: iOS | Android Architecture: New + Legacy

Private keys are generated inside the iOS Secure Enclave / Keychain or the Android Keystore (with optional StrongBox), and never leave the device. Cryptographic operations only run after the user authenticates with Face ID, Touch ID, fingerprint, iris, or — when explicitly allowed — the device passcode.

The public API mirrors Flutter's biometric_signature package, with idiomatic TypeScript types, React hooks, and full TurboModule + legacy-bridge support. Expo Go is not supported; use a development build or bare React Native.


Contents


Highlights

  • 🔐 Hardware-backed keys. ECDSA P-256 in the iOS Secure Enclave or the Android Keystore (StrongBox-eligible on API 28+).
  • ✍️ Biometric-gated signing. createSignature issues a single Face ID / Touch ID / fingerprint prompt and signs the exact UTF-8 bytes of your payload — perfect for challenge-response auth.
  • 🔓 Optional asymmetric decryption. Pair the signing key with a decrypting key (RSA-OAEP-SHA256 on Android, hybrid Secure Enclave wrapping on iOS) for encrypted server-to-app payloads.
  • New + legacy arch. Ships a Codegen TurboModule spec and a NativeModules fallback — works in both architectures with no app-side branching.
  • 🧩 Expo config plugin. app.json declares the package once; the plugin injects NSFaceIDUsageDescription and Android USE_BIOMETRIC / USE_FINGERPRINT automatically on prebuild / EAS.
  • 🪝 First-class React hooks. useBiometricAvailability and useBiometricSignature expose loading, error, and last-result state for UI components.
  • 🛡️ Normalized errors. Cancellation, lockout, missing enrollment, key invalidation, and missing passcode are mapped to a stable BiometricError union on both platforms — no platform-specific error code matching.
  • 📦 Typed end-to-end. Full .d.ts declarations exported from the package; types map to the runtime API one-to-one.

Installation

npm install react-native-biometric-signature
# or
yarn add react-native-biometric-signature
# or
pnpm add react-native-biometric-signature

Peer requirements:

| Peer | Range | Notes | | -------------------------- | ---------------- | --------------------------------------------------------------------- | | react-native | >=0.74 <1.0 | Recommended: 0.83.6 (matches example app + CI). | | react | >=18.2.0 | | | @expo/config-plugins | >=9 (optional) | Only required if you use the Expo plugin via app.json / app.config.js. |


Quick start

import {
  biometricAuthAvailable,
  createKeys,
  createSignature,
} from 'react-native-biometric-signature';

async function signLogin(challenge: string): Promise<string | null> {
  const availability = await biometricAuthAvailable();
  if (!availability.canAuthenticate) {
    return null; // Surface availability.reason to the user.
  }

  await createKeys('login-key', {
    signatureType: 'ecdsa',
    setInvalidatedByBiometricEnrollment: true,
  });

  const result = await createSignature(challenge, 'login-key');
  return result.signature ?? null;
}

That's the whole imperative path. createKeys is idempotent — calling it again with the same alias returns the existing key (unless you pass failIfExists: true).

For a complete server-issued-challenge flow including verification, see End-to-end signing flow.


Platform setup

Bare React Native

Autolinking handles registration on both platforms. After install:

cd ios && pod install

Add the Face ID usage string to ios/<YourApp>/Info.plist:

<key>NSFaceIDUsageDescription</key>
<string>Use Face ID to authorize sensitive actions.</string>

On Android, declare the biometric permissions in android/app/src/main/AndroidManifest.xml if you're not using the Expo plugin:

<uses-permission android:name="android.permission.USE_BIOMETRIC" />
<uses-permission android:name="android.permission.USE_FINGERPRINT" />

Expo (development builds + EAS)

Expo Go does not ship the native module — use a development build (expo-dev-client) or EAS Build.

Reference the plugin from app.json:

{
  "expo": {
    "plugins": [
      [
        "react-native-biometric-signature",
        {
          "faceIDUsageDescription": "Use Face ID to authorize sensitive actions."
        }
      ]
    ]
  }
}

Then:

npx expo prebuild --clean
npx expo run:ios
npx expo run:android
# or
eas build --platform ios
eas build --platform android

What the plugin does:

  • iOS — writes NSFaceIDUsageDescription into the generated Info.plist and patches the Podfile to expose React-jsi header paths for hermes-engine and the package's pod (works around a Hermes JSI header-search edge case on some RN minor versions).
  • Android — injects android.permission.USE_BIOMETRIC and android.permission.USE_FINGERPRINT into the merged AndroidManifest.xml.

iOS notes

  • ECDSA P-256 keys are generated inside the Secure Enclave and tied to the user's biometric set.
  • For signatureType: 'rsa', the private key is generated in software, wrapped by a Secure Enclave EC key, stored as ciphertext in the Keychain, and unwrapped only after biometric authentication (transient unwrap, never persisted in plaintext). Use this when your server's verification stack expects RSA.
  • setInvalidatedByBiometricEnrollment: true invalidates the key on biometric enrollment changes — adds defense-in-depth against Face ID enrollment hijacking, at the cost of forcing key re-enrollment when the user adds a fingerprint.
  • NSFaceIDUsageDescription is required by Apple for any Face ID usage. Without it the prompt is silently denied.

Android notes

  • ECDSA P-256 / RSA-2048 keys are generated in the Android Keystore via KeyPairGenerator, bound to setUserAuthenticationRequired(true) and the device's strong biometric class.
  • useStrongBox: true requests StrongBox-backed key material on API 28+; if the device's StrongBox can't satisfy the request, you get code: 'notAvailable' (no silent fallback).
  • The biometric prompt itself runs on the main thread via AndroidX BiometricPrompt. Key generation, file I/O, and post-auth crypto are dispatched to Dispatchers.IO via Kotlin coroutines so they don't block UI.
  • simplePrompt accepts biometricStrength: 'weak' for prompt-only auth (e.g., unlocking a screen) where weak biometrics are acceptable; key-bound signing always requires strong.
  • For enableDecryption: true with ECDSA, the signing key stays in the Keystore and a software ECIES decrypting key is wrapped with an Android Keystore (or StrongBox) AES key.

Security model

| Concern | Behavior | | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | | Where private keys live | iOS Secure Enclave / Keychain. Android Keystore (optionally StrongBox). | | Can the app read the key? | No. Private key material is never exposed to JS or to the native bridge; only the public key is returned. | | When does the prompt fire? | On every key-bound signing / decryption call. simplePrompt triggers a standalone prompt with no key operation. | | Key invalidation | If setInvalidatedByBiometricEnrollment: true (default off for now — opt-in), adding or removing biometrics invalidates the key. | | Backup / sync | Keys are non-exportable and non-syncable. Replacing a device means re-enrolling. This is by design — it makes possession of the key device-bound. | | Default key alias | biometric_key (matches the Flutter biometric_signature plugin for cross-plugin key sharing under the same applicationID / Team ID + Bundle ID). Override with a per-feature alias (e.g. auth-key, payment-key) for clean revocation. | | Algorithms | ECDSA P-256 with SHA-256 (default), or RSA-2048 PKCS#1 v1.5 + RSA-OAEP-SHA256 for decryption (no SHA-1 allowed). |

You're responsible for the verification side. This package gives you cryptographically strong proof of "the user authenticated on the device that holds this key". Pair it with a challenge-response handshake on your server — see the next section.


End-to-end signing flow

The library produces a signature; your server verifies it against the registered public key. Don't sign static strings — always use server-issued single-use challenges.

import {
  biometricAuthAvailable,
  createKeys,
  createSignature,
  getKeyInfo,
} from 'react-native-biometric-signature';

const ALIAS = 'login-key';

// 1) Enrollment — called once after the user opts in to biometric login.
async function enrollDevice(userId: string): Promise<void> {
  const availability = await biometricAuthAvailable();
  if (!availability.canAuthenticate) {
    throw new Error(availability.reason ?? 'Biometrics unavailable');
  }

  const created = await createKeys(ALIAS, {
    signatureType: 'ecdsa',
    setInvalidatedByBiometricEnrollment: true,
    failIfExists: false,
  });

  if (created.error) {
    throw new Error(created.error);
  }

  // Send the PEM public key + a stable device id to your server, keyed by user.
  await fetch('/api/biometric/enroll', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      userId,
      publicKey: created.publicKey,
      algorithm: created.algorithm,
    }),
  });
}

// 2) Sign-in — called each time the user authenticates.
async function biometricSignIn(userId: string): Promise<string> {
  // The server issues a short-lived single-use challenge.
  const { challenge } = await (
    await fetch(`/api/biometric/challenge?userId=${userId}`)
  ).json();

  // Confirm the key still exists and is valid; recreate if invalidated.
  const info = await getKeyInfo(ALIAS, true);
  if (!info.exists || !info.isValid) {
    await enrollDevice(userId); // re-enroll if biometrics changed
  }

  const result = await createSignature(challenge, ALIAS);
  if (result.error || !result.signature) {
    throw new Error(result.error ?? 'Signing failed');
  }

  // Server verifies the signature against the stored public key + challenge.
  const { sessionToken } = await (
    await fetch('/api/biometric/verify', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        userId,
        challenge,
        signature: result.signature,
      }),
    })
  ).json();

  return sessionToken;
}

Server-side verification (Node.js example) is a single crypto.verify call:

import { createVerify, createPublicKey } from 'node:crypto';

function verifyEcdsaSignature(
  publicKeyPem: string,
  challenge: string,
  signatureBase64: string
): boolean {
  const verifier = createVerify('SHA256');
  verifier.update(challenge);
  verifier.end();
  return verifier.verify(
    createPublicKey(publicKeyPem),
    Buffer.from(signatureBase64, 'base64')
  );
}

Challenges should expire fast (≤2 min) and be single-use — store them in Redis or a short-lived JWT.


Hooks

Hooks never trigger biometric prompts on mount; you call the prompting methods explicitly. State (isLoading, error, lastSignature, etc.) reflects the last operation.

import { Button, Text, View } from 'react-native';
import { useBiometricSignature } from 'react-native-biometric-signature';

export function LoginScreen({ challenge }: { challenge: string }) {
  const bio = useBiometricSignature({ keyAlias: 'login-key' });

  return (
    <View>
      {bio.error ? <Text>Error: {String(bio.error)}</Text> : null}
      {bio.lastSignature?.signature ? (
        <Text>Signed: {bio.lastSignature.signature.slice(0, 16)}…</Text>
      ) : null}

      <Button
        title={bio.isLoading ? 'Authenticating…' : 'Sign in with biometrics'}
        disabled={bio.isLoading}
        onPress={() => {
          void bio.createSignature(challenge);
        }}
      />
    </View>
  );
}

For availability checks (without prompting), use useBiometricAvailability:

import { useBiometricAvailability } from 'react-native-biometric-signature';

const { availability, isLoading, refreshAvailability } = useBiometricAvailability({
  refreshOnMount: true,
});

API reference

Availability

biometricAuthAvailable(): Promise<BiometricAvailability>

Reports whether biometric authentication is available, enrolled, and which modalities the device supports. Never prompts.

type BiometricAvailability = {
  canAuthenticate?: boolean;
  hasEnrolledBiometrics?: boolean;
  availableBiometrics?: BiometricType[]; // 'face' | 'fingerprint' | 'iris' | 'multiple' | 'unavailable'
  reason?: string;
};

isDeviceLockSet(): Promise<boolean>

True if the device has any secure lock (passcode, PIN, pattern). Android uses KeyguardManager.isDeviceSecure(); iOS uses LAContext.canEvaluatePolicy(.deviceOwnerAuthentication). Useful as a precondition before prompting for sensitive flows.

isSensorAvailable()

Alias for biometricAuthAvailable(). Convenience export for codebases coming from react-native-biometrics.

Key lifecycle

createKeys(keyAlias?, config?, keyFormat?, promptMessage?): Promise<KeyCreationResult>

Creates (or returns) a biometric-bound signing keypair. Optionally creates a paired decrypting key.

Defaults: keyAlias = "biometric_key", keyFormat = "pem", signatureType = "ecdsa".

Config (CreateKeysConfig):

| Option | Default | Effect | | -------------------------------------- | --------- | ---------------------------------------------------------------------------------------------- | | signatureType | 'ecdsa' | 'ecdsa' for P-256 / SHA-256, 'rsa' for RSA-2048 PKCS#1 v1.5. | | enableDecryption | false | Also create a paired decrypting key (ECIES on iOS/Android-EC, RSA-OAEP-SHA256 elsewhere). | | setInvalidatedByBiometricEnrollment | false | Invalidate key on biometric enrollment changes. Strong defense, friction tradeoff. | | useStrongBox (Android) | false | Require StrongBox HSM backing on API 28+. Fails with 'notAvailable' if unsupported. | | useDeviceCredentials (Android) | false | Allow PIN/pattern/password fallback for biometric-bound operations. | | enforceBiometric | false | Require biometric auth specifically (Android: strong class only). | | failIfExists | false | Reject with keyAlreadyExists instead of returning the existing key. | | promptSubtitle / promptDescription | | Override Android BiometricPrompt text. iOS uses the iOS-native LocalizedReason. | | cancelButtonText | | Android BiometricPrompt negative button text. |

getKeyInfo(keyAlias?, checkValidity?, keyFormat?): Promise<KeyInfo>

Returns key existence, validity, algorithm, key size, public key (in the requested format), and hybrid-mode metadata. checkValidity: true performs a real Keystore/Keychain probe rather than a presence check.

deleteKeys(keyAlias?): Promise<boolean>

Deletes the signing key and any paired decrypting key for one alias. Returns false if the native call errors (e.g., Keychain locked).

deleteAllKeys(): Promise<boolean>

Deletes every key this package created and tracks.

Signing

createSignature(payload, keyAlias?, config?, signatureFormat?, keyFormat?, promptMessage?): Promise<SignatureResult>

Prompts the user and signs the exact UTF-8 bytes of payload.

Defaults: signatureFormat = "base64", keyFormat = "pem".

type SignatureResult = {
  signature?: string;       // base64 | hex string, depending on signatureFormat
  signatureBytes?: number[]; // populated when signatureFormat === 'raw'
  publicKey?: string;
  error?: string;
  code?: BiometricError;
  algorithm?: string;
  keySize?: number;
  authenticationType?: AuthenticationType; // 'biometric' | 'credential' | 'unknown'
};

createBiometricSignature(...)

Alias for createSignature(...). Compatible with codebases coming from react-native-biometrics.

Decryption

decrypt(payload, keyAlias?, payloadFormat?, config?, promptMessage?): Promise<DecryptResult>

Prompts the user and decrypts ciphertext using the key created by createKeys(..., { enableDecryption: true }). Default payloadFormat = "base64". See Decryption for the full flow.

Authentication prompts

simplePrompt(promptMessage, config?): Promise<SimplePromptResult>

Standalone biometric prompt with no cryptographic operation. Use this for unlock-style flows where you don't need a signature. config.biometricStrength: 'weak' is accepted on Android.

Convenience

biometricKeyExists({ keyAlias?, checkValidity?, keyFormat? }): Promise<BiometricKeyExistsResult>

Thin wrapper over getKeyInfo that returns { exists, isValid? }.

Hooks

useBiometricAvailability(options?: { refreshOnMount?: boolean })

Returns { availability, isLoading, error, refreshAvailability }.

useBiometricSignature(options?: { keyAlias?, refreshAvailabilityOnMount? })

Returns the full imperative API bound to a keyAlias, plus stateful lastSignature, lastDecryptResult, lastKeyInfo, lastPromptResult, isLoading, error, and availability.

Error utilities

import {
  getErrorMessage,
  isBiometricError,
  normalizeBiometricError,
} from 'react-native-biometric-signature';
  • getErrorMessage(err) — safe message extraction from unknown.
  • isBiometricError(value) — type guard for the BiometricError union.
  • normalizeBiometricError(value) — maps native LAError / Keystore exception strings into the normalized BiometricError union. Useful when surfacing errors from third-party native modules through the same code path.

Types

All types are exported from the package entrypoint:

import type {
  AuthenticationType,
  BiometricAvailability,
  BiometricError,
  BiometricKeyExistsOptions,
  BiometricKeyExistsResult,
  BiometricStrength,
  BiometricType,
  CreateKeysConfig,
  CreateSignatureConfig,
  DecryptConfig,
  DecryptResult,
  KeyCreationResult,
  KeyFormat,
  KeyInfo,
  PayloadFormat,
  SignatureFormat,
  SignatureResult,
  SignatureType,
  SimplePromptConfig,
  SimplePromptResult,
  UseBiometricAvailabilityOptions,
  UseBiometricAvailabilityResult,
  UseBiometricSignatureOptions,
  UseBiometricSignatureResult,
} from 'react-native-biometric-signature';

The BiometricError union:

type BiometricError =
  | 'success'
  | 'userCanceled'
  | 'notAvailable'
  | 'notEnrolled'
  | 'lockedOut'
  | 'lockedOutPermanent'
  | 'keyNotFound'
  | 'keyInvalidated'
  | 'unknown'
  | 'invalidInput'
  | 'securityUpdateRequired' // Android-only
  | 'notSupported'
  | 'systemCanceled'
  | 'promptError'
  | 'keyAlreadyExists'
  | 'passcodeNotSet';

lockedOutPermanent fires natively on Android (BiometricPrompt.ERROR_LOCKOUT_PERMANENT) and is synthesized on iOS when an LAError.biometryLockout recurs on a follow-up canEvaluatePolicy check. Treat both as "biometrics require device passcode unlock to recover."

securityUpdateRequired is Android-only.


Error handling

Expected biometric outcomes (cancellation, lockout, no enrollment, key invalidation, missing passcode) are returned as result objects with code and error fields, not thrown:

const result = await createSignature(challenge, 'login-key');

if (result.code === 'userCanceled') {
  // User dismissed the prompt — typically a no-op in the UI.
  return;
}

if (result.code === 'keyInvalidated' || result.code === 'keyNotFound') {
  // Re-enroll the device.
  await createKeys('login-key', { signatureType: 'ecdsa' });
  return retry();
}

if (result.code === 'lockedOutPermanent') {
  showHint('Unlock your device with the passcode to re-enable biometrics.');
  return;
}

if (result.error || !result.signature) {
  reportError(result.error ?? 'Unknown biometric error');
  return;
}

submit(result.signature);

Programmer errors do throw: invalid argument types, empty payloads, unsupported format strings, and missing native module initialization all surface as TypeError / Error so they're caught loudly during development.


Decryption

Decryption needs a key created with enableDecryption: true:

await createKeys('payments-key', {
  signatureType: 'ecdsa',
  enableDecryption: true,
});

const info = await getKeyInfo('payments-key');
// info.decryptingPublicKey is the PEM you send to the backend.

// Backend encrypts payload using info.decryptingPublicKey,
// then the app decrypts after a biometric prompt:
const { decryptedData } = await decrypt(
  ciphertextBase64,
  'payments-key',
  'base64' // payload format
);

Platform mapping:

| Platform | Signing | Decryption | | -------- | ------- | ----------- | | iOS — ECDSA | Secure Enclave P-256 | Secure Enclave ECIES (same key) | | iOS — RSA | Hybrid: software RSA wrapped by Secure Enclave EC | Same hybrid structure; RSA-OAEP-SHA256 after unwrap | | Android — ECDSA | Keystore P-256 | Hybrid: software ECIES wrapped by Keystore (or StrongBox) AES | | Android — RSA | Keystore RSA-2048 PKCS#1 v1.5 | Keystore RSA-2048 OAEP-SHA256 (paired key) |

SHA-1 is not permitted on either platform — the OAEP key is created with DIGEST_SHA256 only.


Key and signature formats

Public keys (KeyFormat):

  • pem (default) — PEM-encoded SubjectPublicKeyInfo
  • base64 — Base64 DER
  • hex — Hex DER
  • raw — bytes returned in publicKeyBytes

Signatures (SignatureFormat):

  • base64 (default) — base64 string in signature
  • hex — hex string in signature
  • raw — bytes returned in signatureBytes

There is no private-key export API by design.


New and legacy architecture

The package ships:

Apps don't need to branch — the JS layer auto-selects whichever path is registered.


Troubleshooting

| Symptom | Fix | | ------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------- | | The package does not seem to be linked | Run pod install; rebuild the native app; confirm you're using a development build (not Expo Go). | | Face ID prompt does not appear | Add NSFaceIDUsageDescription to Info.plist (or via the Expo plugin). | | Android prompt returns notEnrolled | Enroll a biometric, or pass useDeviceCredentials: true if your flow allows the device-credential fallback. | | Returns keyInvalidated after enrolling a new face / fingerprint | Re-create the key. This is intentional if setInvalidatedByBiometricEnrollment: true. | | decrypt returns keyNotFound | Create the key with enableDecryption: true before calling decrypt. | | Android emulator can't reach Metro | Run with REACT_NATIVE_PACKAGER_HOSTNAME=127.0.0.1. The example app's npm run example:android script sets this for you. | | lockedOutPermanent on iOS | Tell the user to unlock with the device passcode — Apple disables biometry until then. |


Migration from Flutter biometric_signature

The mirrored method names are intentionally stable:

biometricAuthAvailable · createKeys · createSignature · decrypt · deleteKeys · deleteAllKeys · getKeyInfo · simplePrompt · isDeviceLockSet

React Native-specific additions (isSensorAvailable, biometricKeyExists, createBiometricSignature, hooks) are additive wrappers — they don't replace the mirrored API. If you're porting a Flutter app's biometric layer, the call sites translate one-to-one; only the surrounding state plumbing changes.


Example app

A complete Expo example lives in example/. It exercises every API: availability checks, key creation with both ECDSA and RSA modes, signing, decryption, prompt-only auth, and key info / deletion.

nvm use 24.15.0
npm install

# iOS
npm run example:ios

# Android
npm run example:android

# Metro only
npm run example

The example targets Expo SDK 55.0.24 and React Native 0.83.6.


Contributing

PRs and issues welcome. Before opening a PR:

npm run typecheck
npm run lint
npm test
npm run build

See CONTRIBUTING.md and CODE_OF_CONDUCT.md.


License

MIT © chamodanethra