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.
- 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
| 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.
npm install react-native-device-binding
# or
yarn add react-native-device-bindingcd ios && pod installYour 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 headersMake sure in your Xcode project settings → Build Settings → "Objective-C Bridging Header" points to this file.
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;
}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"
}
}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 }>
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.
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 signatureParameters:
| 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.
Permanently deletes a key pair from the hardware store.
const deleted = await DeviceBinding.deleteKeyPair('my-key');
// true if deleted, false if key didn't existChecks if a key pair exists for the given alias.
const exists = await DeviceBinding.hasKeyPair('my-key');Checks if hardware-backed key storage is available.
- iOS: Always returns
true(Secure Enclave available on A7+ chips) - Android: Returns
trueif StrongBox Keymaster is available (Pixel 3+, Samsung S10+, etc.)
const available = await DeviceBinding.isStrongBoxAvailable();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"Generates and persists a new device UUID, replacing the old one.
const newDeviceId = await DeviceBinding.resetDeviceId();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 };
}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'));
}# 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 StrongBoxThe 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.
- Key Generation: Uses
SecKeyCreateRandomKeywithkSecAttrTokenIDSecureEnclaveto generate ECDSA P-256 keys directly in the Secure Enclave. Falls back to software-backed keys on simulators. - Key Storage: Private key is stored permanently in the Secure Enclave with
kSecAttrIsPermanent: true. Tagged withcom.devicebinding.<alias>. - Signing: Uses
SecKeyCreateSignaturewithecdsaSignatureMessageX962SHA256algorithm. - Public Key Export: Raw EC point is wrapped with SPKI/DER header for standard Base64 encoding.
- Key Generation: Uses
KeyPairGeneratorwithAndroidKeyStoreprovider andECGenParameterSpec("secp256r1"). Attempts StrongBox first on API 28+, falls back to TEE-backed Keystore. - Key Storage: Keys are stored in Android Keystore with alias
device_binding_<alias>. - Signing: Uses
java.security.SignaturewithSHA256withECDSAalgorithm. - Public Key Export: Android Keystore returns keys in standard SPKI/DER format natively.
| 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 |
| 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 |
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.
Not all Android devices support StrongBox. The plugin automatically falls back to TEE-backed Keystore. Use isStrongBoxAvailable() to check at runtime.
- Run
npx react-native cleanand rebuild - Verify the package is in
node_modules/react-native-device-binding - Check that auto-linking detected it:
npx react-native config
Make sure you have a bridging header. Xcode needs it to bridge Swift and Objective-C.
MIT