skip to content

Device Integrity Checks

Jailbreak and root checks run on a device the attacker controls and get hooked away; App Attest and Play Integrity only count when a server verifies them. Interviewers ask what a client check proves.

part ofReact Nativeoverview, primer and where to startread it →
on this pageshow

explore

questions

4

What does a Play Integrity or App Attest result prove about a request from a React Native app, and why must your server verify it?

level: seniorimportance: must knowfreq 40%

answer

  1. evidence signed by Google or Apple
  2. server challenge makes it fresh
  3. genuine app, genuine device, this request
  4. the app cannot judge its own token
  5. @expo/app-integrity, alpha

basics

~20 s

They give your server platform-signed evidence that a request comes from your genuine, unmodified app on a genuine device, bound to a fresh challenge. Only a server can check that evidence; a verdict read on the device can be forged.

solid answer

~50 s

Both services let the **platform vouch** for the app. On Android, **Play Integrity** returns a token the server decodes into a verdict about the app (is this the binary Play recognises?), the device (does it pass integrity checks?) and the account. On iOS, **App Attest** creates a hardware-backed key for the app, has Apple **attest** that key once, and then **signs assertions** over later requests. In React Native these come from a native module or library — Expo offers the alpha `@expo/app-integrity`. What they prove is narrow: this request came from your real app on real hardware, recently, bound to a challenge or request hash. Verification must happen on the server, because the whole point is evidence the device cannot forge; an app that checks its own token is back to trusting the attacker. They do not prove the user is honest.

code

typescript · 26 lines
typescript
import { Platform } from 'react-native';
import * as AppIntegrity from '@expo/app-integrity';

// Android: call once, well before the sensitive action.
export async function prepareAndroid(cloudProjectNumber: string) {
  await AppIntegrity.prepareIntegrityTokenProviderAsync(cloudProjectNumber);
}

// Android: the token is bound to a hash of the transfer and sent to the server,
// which decodes and verifies it. The app never judges the result itself.
export async function integrityTokenForTransfer(requestHash: string) {
  if (Platform.OS !== 'android') {
    throw new Error('Play Integrity is Android only');
  }
  return AppIntegrity.requestIntegrityCheckAsync(requestHash);
}

// iOS: attest a new key once, against a one-time challenge from the server.
export async function attestKeyIOS(challenge: string) {
  if (!AppIntegrity.isSupported) {
    return null; // e.g. the simulator: fall back to other server-side controls
  }
  const keyId = await AppIntegrity.generateKeyAsync();
  const attestation = await AppIntegrity.attestKeyAsync(keyId, challenge);
  return { keyId, attestation }; // send both to the server; persist keyId locally
}

go deeper

for a junior

Recall that Play Integrity and App Attest let Google or Apple vouch for your app, and that your server, not the app, checks the result.

for a middle

Explain the flows: token provider and request hash on Android, key generation, attestation and per-request assertions on iOS, and why a challenge is needed.

for a senior

Design enforcement: which actions require attestation, how the server verifies and stores keys, and how unsupported devices and failures degrade to other controls instead of lockouts.

for a principal

Weigh attestation's coverage and platform dependency against fraud losses, and set how verdicts combine with risk scoring, limits and support processes.

## The problem attestation solves A server receives a request that says it comes from your React Native wallet app. It cannot see the device. The request could come from your app, a repackaged copy, a script replaying captured traffic, or an emulator farm. Client-side checks cannot settle it, because the device can lie. **Attestation** adds a third party the attacker does not control — Google or Apple — that signs a statement about the app and device, which your server verifies. ## Play Integrity on Android In the **standard request** flow, as the Expo docs describe it: 1. The app **prepares a token provider** once, ahead of time (`prepareIntegrityTokenProviderAsync` in `@expo/app-integrity`). 2. For a sensitive action, the app computes a **request hash** of what it is about to do and requests a token bound to it (`requestIntegrityCheckAsync`). 3. The app sends the token with the request; your **server** has it decoded and verified, and reads the verdict. 4. The server checks the verdict: the app is the one Play recognises, the device meets the integrity level you require, and the request hash matches the request it received. A provider used for too long can expire; the library then reports `ERR_APP_INTEGRITY_PROVIDER_INVALID`, and the app prepares a new one. ## App Attest on iOS App Attest works in two phases: 1. **Attest once.** The app checks `isSupported` (the simulator is not), generates a hardware-backed key with `generateKeyAsync`, obtains a one-time **challenge** from the server and calls `attestKeyAsync(keyId, challenge)`. The server verifies the attestation object against Apple's certificate chain and stores the key. 2. **Assert per request.** For sensitive requests, the app gets a new challenge and calls `generateAssertionAsync(keyId, clientData)`; the server checks the signature with the stored key. The private key lives in the **Secure Enclave** and cannot be read by any process. Keys survive app updates but not reinstallation, device migration or restore from backup, so the server must expect new keys at those moments. ## What the evidence proves | Claim | Play Integrity | App Attest | |---|---|---| | The binary is your genuine app | yes — Play-recognised app verdict | yes — the key is bound to your App ID | | The hardware is genuine | yes — device verdict | yes — the key lives in genuine Apple hardware | | The evidence is fresh and bound to this action | yes — request hash | yes — server challenge per assertion | | The OS is not jailbroken or rooted | partly — device verdict levels | not definitively | | The user is not committing fraud | no | no | ## Why only the server may decide - A token the **app** inspects and then reports as "passed" is just another client claim, and hooking can fake the report. - Play Integrity tokens are meant for your server to decode and verify, not for the app. - The **challenge** and **request hash** only work if the server generated or recomputes them; otherwise a captured token can be replayed. - The server can combine the verdict with account history, limits and step-up authentication. ## Setting it up in a React Native project Neither service is part of React Native core. The app reaches them through a native module: - **Expo** — `@expo/app-integrity` wraps both services behind one JavaScript API. - **Android** — Play Integrity must be enabled for the app, and the provider is prepared with the app's Google Cloud project number. - **iOS** — the **App Attest** capability must be added under Signing & Capabilities, which adds the entitlement, and the app needs an App ID registered with Apple. The server side is ordinary backend work: generating challenges, calling or verifying against the platform, storing App Attest keys per user and device, and recording verdicts for monitoring. ## Using it well - Attest at **sensitive moments** — adding a card, sending money, changing the payout account — rather than on every request. - Plan for **unsupported or failing** devices: rate-limit, ask for more authentication, or degrade features instead of blocking outright. - **Roll out gradually** and monitor verdict distributions before enforcing. - Treat `@expo/app-integrity` as **alpha**: the Expo docs warn it will frequently experience breaking changes. ## Traps - Believing attestation stops a user on a genuine device from abusing your API by hand. - Skipping the challenge, which turns a valid token into a replayable one. - Forgetting that reinstall creates new App Attest keys, then flagging honest users.

  • Why must the server issue the App Attest challenge instead of the React Native app generating a random one?
    The challenge proves freshness. If the app chose it, an attacker could replay an old attestation or assertion together with the challenge it was made for, and the server would have nothing to compare against. A server-issued, single-use challenge ties the evidence to this moment and this request.
  • How should a React Native wallet treat a device where attestation is unsupported or fails?
    Not as proof of fraud. App Attest is unavailable on the simulator and some contexts, Play Integrity calls can fail on weak networks, and keys reset on reinstall. The server should fall back to other controls — lower limits, step-up authentication, manual review — and reserve hard refusals for verdicts that positively show a modified app.
  • Does a passing attestation mean the request body is safe to trust?
    No. It shows the request came from your genuine app on genuine hardware and, via the request hash or signed client data, that it was not altered in transit. A real user can still send a malicious value through the real app, so input validation and authorization still apply.

saying these in an interview costs you the question

  • The app can check the integrity verdict itself and skip the server.
  • A passing attestation proves the user is not committing fraud.
  • App Attest definitively detects a jailbroken iPhone.
  • A fixed challenge is fine because the token is signed anyway.
  • Attestation failures should always block the user outright.
open as a page

How do you keep a React Native wallet's card-details screen out of screenshots and screen recordings on Android and iOS?

level: juniorimportance: should knowfreq 36%

basics

~20 s

On Android, set FLAG_SECURE on the Activity window while the screen is visible, for example with expo-screen-capture's usePreventScreenCapture. iOS has no equivalent public flag, so rely on the library's iOS support, detect screenshots and recording, and hide sensitive content.

open as a page

In a React Native app, why can't a jailbreak or root check running on the device be trusted, and how do attackers bypass it?

level: middleimportance: should knowfreq 42%

basics

~20 s

A root or jailbreak check is a heuristic running on a device the attacker controls, so root-hiding tools, runtime hooking or a patched bundle make it return false. Treat its result as a risk signal, never as a security decision.

open as a page

Your React Native mobile-wallet app must handle rooted and jailbroken devices; would you block them outright, and how would you enforce the policy?

level: seniorimportance: should knowfreq 30%

basics

~20 s

Usually not outright: a client-side block stops honest users while attackers bypass it. Enforce on the server instead, requiring verified attestation for money movement and applying limits, step-up authentication and monitoring when signals are weak.

open as a page