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-device-attestation

v1.0.0

Published

Device attestation for React Native — Apple App Attest (iOS) and Google Play Integrity (Android). Built on the New Architecture (Turbo Native Modules).

Readme

react-native-device-attestation

Device attestation for React Native apps.

  • iOS — Apple App Attest (DeviceCheck framework, iOS 14+)
  • Android — Google Play Integrity API

Built on the New Architecture (Turbo Native Modules) following the official React Native Turbo Module spec. No third-party wrappers — uses Apple and Google APIs directly.


What this solves

Any secret bundled into an app binary can be extracted by decompiling the APK/IPA. Device attestation proves to your backend that:

  • The request is coming from your genuine, unmodified app
  • The app is running on a real device (not an emulator or script)
  • The app binary has not been tampered with or repackaged

Your backend can then safely return secrets (API keys, tokens, signed credentials) only to verified requests.


Requirements

| Platform | Requirement | |---|---| | iOS | iOS 14+, Xcode 12+, App Attest capability enabled | | Android | minSdkVersion 24+, Google Play Services, app published on Google Play | | React Native | 0.73+ (New Architecture) |


Installation

npm install react-native-device-attestation

iOS

cd ios && pod install

Enable the App Attest capability in Xcode:

  1. Open your project in Xcode
  2. Select your app target
  3. Go to Signing & Capabilities
  4. Click + Capability and add App Attest

Set the environment for each build scheme:

| Scheme | Value | |---|---| | Debug | development | | Release | production |

In your Info.plist:

<key>com.apple.developer.devicecheck.appattest-environment</key>
<string>production</string>

Use development for debug builds. Apple's servers treat development and production attestations separately.

Android

Add the Play Integrity dependency to android/app/build.gradle:

dependencies {
    implementation 'com.google.android.play:integrity:1.3.0'
}

Register the package in MainApplication.kt:

import com.deviceattestation.DeviceAttestationPackage

// inside getPackages():
packages.add(DeviceAttestationPackage())

Enable Play Integrity in Google Play Console:

  1. Go to Release → Setup → App Integrity
  2. Enable the Play Integrity API
  3. Link your Google Cloud project

During development, use the Play Integrity testing tool to get test tokens. Tokens from sideloaded or unsigned APKs will return UNEVALUATED.


Usage

Cross-platform (recommended)

The attest() helper handles the full flow for the current platform.

import { attest } from 'react-native-device-attestation';

// 1. Get a one-time nonce from your backend
const { nonce } = await fetch('/attestation/nonce').then(r => r.json());

// 2. Run attestation
const result = await attest(nonce);
//  iOS    → { token: '<base64 attestation>', keyId: '<key id>' }
//  Android → { token: '<play integrity token>' }

// 3. Send token to your backend for verification
await fetch('/attestation/verify', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    token: result.token,
    keyId: result.keyId,   // iOS only, undefined on Android
    platform: Platform.OS,
  }),
});

iOS — Step by step

import {
  isSupported,
  generateKey,
  attestKey,
  generateAssertion,
} from 'react-native-device-attestation';
import { Platform } from 'react-native';

// Check support first
const supported = await isSupported();
if (!supported) return; // simulator or unsupported device

// Step 1 — Generate and persist the key (once per install)
// Store keyId in Keychain via react-native-keychain
const keyId = await generateKey();
await Keychain.setGenericPassword('attestKeyId', keyId);

// Step 2 — Attest the key (once per install)
const { nonce } = await fetch('/attestation/nonce').then(r => r.json());
const attestationToken = await attestKey(keyId, nonce);

// Send attestation to backend
await fetch('/attestation/verify', {
  method: 'POST',
  body: JSON.stringify({ token: attestationToken, keyId, platform: 'ios' }),
});

// Step 3 — Generate assertion for every sensitive request thereafter
const { nonce: reqNonce } = await fetch('/attestation/nonce').then(r => r.json());
const requestPayload = JSON.stringify({ secretRequest: true, nonce: reqNonce });
const hash = sha256Base64(requestPayload); // see Helpers section below
const assertion = await generateAssertion(keyId, hash);

// Send assertion in header
await fetch('/config/secrets', {
  headers: { 'X-Attestation-Assertion': assertion },
});

Android — Step by step

import { requestIntegrityToken } from 'react-native-device-attestation';

// Get nonce from your backend
const { nonce } = await fetch('/attestation/nonce').then(r => r.json());

// Request integrity token
const token = await requestIntegrityToken(nonce);

// Send to backend for verification
await fetch('/attestation/verify', {
  method: 'POST',
  body: JSON.stringify({ token, platform: 'android' }),
});

API

isSupported(): Promise<boolean>

Returns true if the device supports attestation.

  • iOS: checks DCAppAttestService.shared.isSupported — returns false on simulators and unsupported devices
  • Android: always returns true on GMS devices

generateKey(): Promise<string> — iOS only

Generates a new App Attest key pair. Returns the keyId.

Call once per device install. Store the keyId in the iOS Keychain. Calling this again generates a new key, invalidating the previous attestation.

const keyId = await generateKey();

attestKey(keyId: string, challenge: string): Promise<string> — iOS only

Attests the key with Apple's servers. Returns a base64-encoded attestation object.

  • keyId — returned from generateKey()
  • challenge — base64-encoded nonce from your backend

Send the returned token to your backend. Your backend verifies it with Apple's API. Call once per install (not on every request — use generateAssertion for subsequent requests).

const attestation = await attestKey(keyId, nonce);

generateAssertion(keyId: string, requestHash: string): Promise<string> — iOS only

Generates a cryptographic assertion for a specific request. Use this for every sensitive API call after the initial attestation.

  • keyId — the attested key ID
  • requestHash — base64-encoded SHA256 of your request body + nonce
const assertion = await generateAssertion(keyId, requestHash);

requestIntegrityToken(nonce: string): Promise<string> — Android only

Requests a Play Integrity token.

  • nonce — base64-encoded, URL-safe string (max 500 bytes) from your backend
const token = await requestIntegrityToken(nonce);

attest(nonce: string, keyId?: string): Promise<AttestationResult> — cross-platform

Runs the full attestation flow for the current platform.

type AttestationResult = {
  token: string;
  keyId?: string; // iOS only
};

Pass a stored keyId on iOS to skip generateKey().


Backend — Verification

Nonce endpoint

// GET /attestation/nonce
app.get('/attestation/nonce', async (req, res) => {
  const nonce = crypto.randomBytes(32).toString('base64url');
  await redis.setex(`nonce:${nonce}`, 120, '1'); // 2 min TTL, single use
  res.json({ nonce });
});

iOS — Verify attestation with Apple

import { AppAttest } from 'apple-appattest'; // or use your own cbor parser

app.post('/attestation/verify', async (req, res) => {
  const { token, keyId, platform, nonce } = req.body;

  // Consume nonce (prevent replay)
  const valid = await redis.getdel(`nonce:${nonce}`);
  if (!valid) return res.status(401).json({ error: 'Invalid or expired nonce' });

  if (platform === 'ios') {
    const attestation = Buffer.from(token, 'base64');
    const result = await AppAttest.verifyAttestation({
      attestation,
      challenge: Buffer.from(nonce, 'base64'),
      bundleId: 'com.your.bundleid',
      teamId: 'YOUR_APPLE_TEAM_ID',
      // set to true for development environment
      isDevelopment: process.env.NODE_ENV !== 'production',
    });

    if (!result.verified) {
      return res.status(403).json({ error: 'Attestation failed' });
    }

    // Store the public key for assertion verification
    await db.upsert('device_keys', { keyId, publicKey: result.publicKey });
  }

  if (platform === 'android') {
    const { GoogleAuth } = require('google-auth-library');
    const auth = new GoogleAuth({ scopes: 'https://www.googleapis.com/auth/playintegrity' });
    const client = await auth.getClient();
    const accessToken = await client.getAccessToken();

    const response = await fetch(
      `https://playintegrity.googleapis.com/v1/${PACKAGE_NAME}:decodeIntegrityToken`,
      {
        method: 'POST',
        headers: { Authorization: `Bearer ${accessToken.token}` },
        body: JSON.stringify({ integrityToken: token }),
      }
    );

    const data = await response.json();
    const verdict = data.tokenPayloadExternal?.appIntegrity?.appRecognitionVerdict;

    if (verdict !== 'PLAY_RECOGNIZED') {
      return res.status(403).json({ error: `Integrity check failed: ${verdict}` });
    }
  }

  // Return secrets from AWS Secrets Manager
  const secrets = await secretsManager.getSecretValue({ SecretId: 'app/config' });
  res.json(JSON.parse(secrets.SecretString));
});

Helpers

SHA256 base64 in React Native

import { Buffer } from '@craftzdog/react-native-buffer';
import Crypto from 'react-native-quick-crypto';

export function sha256Base64(input: string): string {
  const hash = Crypto.createHash('sha256');
  hash.update(input);
  return hash.digest('base64');
}

Or using crypto-js (already in many RN projects):

import CryptoJS from 'crypto-js';

export function sha256Base64(input: string): string {
  return CryptoJS.SHA256(input).toString(CryptoJS.enc.Base64);
}

Storing the keyId (iOS)

The keyId must survive app restarts but should not be in AsyncStorage (clearable). Use the Keychain:

npm install react-native-keychain
import * as Keychain from 'react-native-keychain';

export const saveKeyId = (keyId: string) =>
  Keychain.setGenericPassword('device_attestation', keyId, {
    service: 'com.yourapp.attestation',
    accessible: Keychain.ACCESSIBLE.WHEN_UNLOCKED_THIS_DEVICE_ONLY,
  });

export const getKeyId = async (): Promise<string | null> => {
  const creds = await Keychain.getGenericPassword({ service: 'com.yourapp.attestation' });
  return creds ? creds.password : null;
};

Full integration example

import { Platform } from 'react-native';
import { attest, isSupported } from 'react-native-device-attestation';
import { getKeyId, saveKeyId } from './keychain';

let _secureConfig: Record<string, string> | null = null;

export const initSecureConfig = async () => {
  try {
    const supported = await isSupported();
    if (!supported) return; // simulator — skip in dev

    const { nonce } = await fetch('/attestation/nonce').then(r => r.json());

    let keyId: string | undefined;
    if (Platform.OS === 'ios') {
      keyId = (await getKeyId()) ?? undefined;
    }

    const result = await attest(nonce, keyId);

    if (Platform.OS === 'ios' && result.keyId) {
      await saveKeyId(result.keyId); // persist for next launch
    }

    const res = await fetch('/attestation/verify', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        token: result.token,
        keyId: result.keyId,
        platform: Platform.OS,
      }),
    });

    if (!res.ok) throw new Error('Attestation rejected by server');

    _secureConfig = await res.json();
  } catch (err) {
    if (__DEV__) console.log('[Attestation] Error:', err);
  }
};

export const getSecureConfig = () => _secureConfig;

Call initSecureConfig() once in App.tsx before any API calls:

useEffect(() => {
  initSecureConfig();
}, []);

Error codes

| Code | Platform | Meaning | |---|---|---| | NOT_SUPPORTED | iOS | Device does not support App Attest (simulator or old device) | | GENERATE_KEY_FAILED | iOS | DCAppAttestService failed to generate a key | | ATTEST_KEY_FAILED | iOS | Apple server rejected the attestation | | ASSERTION_FAILED | iOS | Assertion generation failed | | INTEGRITY_ERROR | Android | Play Integrity API returned an error | | PLATFORM_ERROR | Both | Method called on wrong platform |


Security notes

  • Never persist secrets returned from your backend in AsyncStorage, MMKV, or any unencrypted store. Keep them in memory only.
  • Nonces must be single-use. Store them in Redis with a short TTL and delete on consumption to prevent replay attacks.
  • Rooted/jailbroken devices — App Attest will fail on jailbroken iOS devices. Play Integrity returns a MEETS_BASIC_INTEGRITY verdict on rooted Android devices. Your backend should decide how to handle these verdicts based on your policy.
  • Development builds — App Attest works in the development environment on real devices. Simulators are not supported.
  • Production rollout — Apple rate-limits App Attest attestations per device per day. The key should be generated and attested once per install, not on every launch.

License

MIT