# 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

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

### iOS Setup

```bash
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`:**
```objc
// 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`:

```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`:

```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.

```typescript
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.

```typescript
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).

```typescript
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.

```typescript
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.

```typescript
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.)

```typescript
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`

```typescript
const deviceId = await DeviceBinding.getDeviceId();
// "550e8400-e29b-41d4-a716-446655440000"
```

---

### `resetDeviceId(): Promise<string>`

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

```typescript
const newDeviceId = await DeviceBinding.resetDeviceId();
```

---

## Usage Example: Login with Device Binding

```typescript
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:

```javascript
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

```bash
# 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
