Skip to content

piroposantosdev/react-native-device-binding

Repository files navigation

react-native-device-binding

Hardware-backed ECDSA P-256 device binding for React Native. Private keys are stored in the Secure Enclave (iOS) and Android Keystore (with StrongBox support). The private key never leaves the hardware.

Features

  • ECDSA P-256 (secp256r1) key pair generation
  • Secure Enclave storage on iOS (with software fallback for simulators)
  • Android Keystore with StrongBox support (auto-fallback when unavailable)
  • Sign arbitrary challenges with the hardware-stored private key
  • Public key exported in SPKI/DER Base64 format (compatible with most backends)
  • Persistent device ID (UUID) management
  • Full TypeScript support
  • Example app for testing all operations

Requirements

Platform Minimum Version
iOS 13.0
Android API 23 (6.0)
React Native 0.70+

Important: Secure Enclave / Android Keystore features require a physical device. Emulators and simulators will use software-backed keys.

Installation

npm install react-native-device-binding
# or
yarn add react-native-device-binding

iOS Setup

cd ios && pod install

Your iOS project needs a Swift bridging header. If you don't have one yet, Xcode will prompt you to create it when you add the first Swift file to an Objective-C project. Alternatively, create these files manually:

YourApp-Bridging-Header.h:

// Leave empty or add other bridge headers

Make sure in your Xcode project settings → Build Settings → "Objective-C Bridging Header" points to this file.

Android Setup

1. Register the package

For React Native 0.73+ (auto-linking): No manual registration needed. It works out of the box.

For older versions, add to android/app/src/main/java/.../MainApplication.java:

import com.devicebinding.DeviceBindingPackage;

@Override
protected List<ReactPackage> getPackages() {
    List<ReactPackage> packages = new PackageList(this).getPackages();
    packages.add(new DeviceBindingPackage()); // Add this line
    return packages;
}

2. Kotlin support

Make sure your project has Kotlin enabled. In android/build.gradle:

buildscript {
    ext {
        kotlinVersion = '1.9.22' // or your preferred version
    }
    dependencies {
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion"
    }
}

API Reference

generateKeyPair(alias: string): Promise<KeyPair>

Generates a new ECDSA P-256 key pair stored in hardware. If a key with the same alias already exists, it is replaced.

import DeviceBinding from 'react-native-device-binding';

const { publicKey, keyAttestation } = await DeviceBinding.generateKeyPair('my-key');
// publicKey: Base64-encoded SPKI/DER public key
// keyAttestation: "secure_enclave_ios" | "android_keystore_strongbox" | "android_keystore"

Parameters:

Parameter Type Description
alias string Unique identifier for the key pair

Returns: Promise<{ publicKey: string; keyAttestation: string }>


getPublicKey(alias: string): Promise<string>

Retrieves the public key for an existing key pair.

const publicKey = await DeviceBinding.getPublicKey('my-key');

Returns: Base64-encoded SPKI/DER public key.

Throws: KEY_NOT_FOUND if no key exists for the alias.


signChallenge(alias: string, challenge: string): Promise<string>

Signs a challenge string using the private key stored in hardware. Uses SHA256withECDSA (DER-encoded signature).

const signature = await DeviceBinding.signChallenge('my-key', 'random-challenge-from-server');
// signature: Base64-encoded DER ECDSA signature

Parameters:

Parameter Type Description
alias string Key pair alias
challenge string UTF-8 string to sign

Returns: Base64-encoded signature.

Throws: KEY_NOT_FOUND if no key exists, SIGN_ERROR on failure.


deleteKeyPair(alias: string): Promise<boolean>

Permanently deletes a key pair from the hardware store.

const deleted = await DeviceBinding.deleteKeyPair('my-key');
// true if deleted, false if key didn't exist

hasKeyPair(alias: string): Promise<boolean>

Checks if a key pair exists for the given alias.

const exists = await DeviceBinding.hasKeyPair('my-key');

isStrongBoxAvailable(): Promise<boolean>

Checks if hardware-backed key storage is available.

  • iOS: Always returns true (Secure Enclave available on A7+ chips)
  • Android: Returns true if StrongBox Keymaster is available (Pixel 3+, Samsung S10+, etc.)
const available = await DeviceBinding.isStrongBoxAvailable();

getDeviceId(): Promise<string>

Returns a persistent UUID for this device. Generated on first call and persisted across app launches.

  • iOS: Stored in UserDefaults
  • Android: Stored in SharedPreferences
const deviceId = await DeviceBinding.getDeviceId();
// "550e8400-e29b-41d4-a716-446655440000"

resetDeviceId(): Promise<string>

Generates and persists a new device UUID, replacing the old one.

const newDeviceId = await DeviceBinding.resetDeviceId();

Usage Example: Login with Device Binding

import DeviceBinding from 'react-native-device-binding';
import { Platform } from 'react-native';

const KEY_ALIAS = 'auth-device-key';

// 1. During registration — generate key and send public key to backend
async function registerDevice() {
  const deviceId = await DeviceBinding.getDeviceId();
  const { publicKey, keyAttestation } = await DeviceBinding.generateKeyPair(KEY_ALIAS);

  await fetch('https://api.example.com/auth/register', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      email: 'user@example.com',
      password: 'SecurePassword@123',
      deviceBinding: {
        deviceId,
        publicKey,
        keyAttestation,
        platform: Platform.OS,
      },
    }),
  });
}

// 2. During login — get challenge, sign it, send to backend
async function loginWithDeviceBinding(email: string, password: string) {
  // Step 1: Get challenge from server
  const challengeResponse = await fetch('https://api.example.com/auth/challenge');
  const { challenge } = await challengeResponse.json();

  // Step 2: Sign challenge with hardware key
  const deviceId = await DeviceBinding.getDeviceId();
  const signature = await DeviceBinding.signChallenge(KEY_ALIAS, challenge);

  // Step 3: Send login request
  const loginResponse = await fetch('https://api.example.com/auth/login', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      email,
      password,
      deviceId,
      challenge,
      signature,
    }),
  });

  return loginResponse.json();
}

// 3. Device swap — delete old key, generate new one
async function swapDevice() {
  await DeviceBinding.deleteKeyPair(KEY_ALIAS);
  const { publicKey, keyAttestation } = await DeviceBinding.generateKeyPair(KEY_ALIAS);
  const deviceId = await DeviceBinding.resetDeviceId();

  // Send new keys to backend (after OTP/selfie verification)
  return { deviceId, publicKey, keyAttestation, platform: Platform.OS };
}

Backend Verification

Your backend should verify the ECDSA signature using the stored public key. Example in Node.js:

const crypto = require('crypto');

function verifySignature(publicKeyBase64, challenge, signatureBase64) {
  const publicKey = crypto.createPublicKey({
    key: Buffer.from(publicKeyBase64, 'base64'),
    format: 'der',
    type: 'spki',
  });

  const verify = crypto.createVerify('SHA256');
  verify.update(challenge, 'utf8');
  verify.end();

  return verify.verify(publicKey, Buffer.from(signatureBase64, 'base64'));
}

Running the Example App

# Clone the repo
git clone https://github.com/nicearma/react-native-device-binding.git
cd react-native-device-binding

# Install plugin dependencies
yarn install

# Initialize the example app
cd example
npx @react-native-community/cli init DeviceBindingExample --version 0.76.0

# Copy the example App.tsx
cp ../example/App.tsx DeviceBindingExample/App.tsx

# Install the plugin in the example
cd DeviceBindingExample
yarn add ../../

# iOS
cd ios && pod install && cd ..
npx react-native run-ios --device  # Must use physical device for Secure Enclave

# Android
npx react-native run-android       # Physical device recommended for StrongBox

The example app provides buttons to test every operation: generate keys, sign challenges, delete keys, check StrongBox availability, manage device IDs, and run the full device binding flow.

How It Works

iOS (Secure Enclave)

  1. Key Generation: Uses SecKeyCreateRandomKey with kSecAttrTokenIDSecureEnclave to generate ECDSA P-256 keys directly in the Secure Enclave. Falls back to software-backed keys on simulators.
  2. Key Storage: Private key is stored permanently in the Secure Enclave with kSecAttrIsPermanent: true. Tagged with com.devicebinding.<alias>.
  3. Signing: Uses SecKeyCreateSignature with ecdsaSignatureMessageX962SHA256 algorithm.
  4. Public Key Export: Raw EC point is wrapped with SPKI/DER header for standard Base64 encoding.

Android (Keystore + StrongBox)

  1. Key Generation: Uses KeyPairGenerator with AndroidKeyStore provider and ECGenParameterSpec("secp256r1"). Attempts StrongBox first on API 28+, falls back to TEE-backed Keystore.
  2. Key Storage: Keys are stored in Android Keystore with alias device_binding_<alias>.
  3. Signing: Uses java.security.Signature with SHA256withECDSA algorithm.
  4. Public Key Export: Android Keystore returns keys in standard SPKI/DER format natively.

Security Properties

Property iOS Android
Key stored in hardware Secure Enclave (A7+) StrongBox or TEE
Private key extractable No No
Key survives app reinstall No (Keychain cleared) No (Keystore cleared)
Algorithm ECDSA P-256 ECDSA P-256
Signature format DER (X9.62) DER
Public key format SPKI/DER Base64 SPKI/DER Base64

Error Codes

Code Description
KEY_GEN_ERROR Failed to generate key pair
KEY_NOT_FOUND No key pair found for the given alias
KEY_ERROR Failed to access key
KEY_EXPORT_ERROR Failed to export public key
SIGN_ERROR Failed to sign challenge
DELETE_ERROR Failed to delete key pair
CHECK_ERROR Failed to check key pair existence

Troubleshooting

iOS: "Failed to generate key pair" on Simulator

The Secure Enclave is not available on iOS Simulator. The plugin automatically falls back to software-backed keys, but if you still see errors, ensure you're on iOS 13.0+ simulator.

Android: StrongBox not available

Not all Android devices support StrongBox. The plugin automatically falls back to TEE-backed Keystore. Use isStrongBoxAvailable() to check at runtime.

Android: "The package doesn't seem to be linked"

  1. Run npx react-native clean and rebuild
  2. Verify the package is in node_modules/react-native-device-binding
  3. Check that auto-linking detected it: npx react-native config

iOS: "No matching function for call to RCT_EXTERN_MODULE"

Make sure you have a bridging header. Xcode needs it to bridge Swift and Objective-C.

License

MIT

About

A step-by-step guide to implementing device binding in React Native — link user accounts to trusted devices using secure identifiers, improving authentication and preventing unauthorized access.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages