react-native-app-attestation 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +451 -0
- package/android/build.gradle +31 -0
- package/android/src/main/java/com/reactnativeappattestation/PlayIntegrityModule.kt +38 -0
- package/android/src/main/java/com/reactnativeappattestation/PlayIntegrityPackage.kt +21 -0
- package/ios/AppAttestModule.m +11 -0
- package/ios/AppAttestModule.swift +56 -0
- package/ios/ReactNativeAppAttestation-Bridging-Header.h +1 -0
- package/lib/AttestationService.d.ts +15 -0
- package/lib/AttestationService.js +126 -0
- package/lib/DeviceIDService.d.ts +9 -0
- package/lib/DeviceIDService.js +62 -0
- package/lib/index.d.ts +64 -0
- package/lib/index.js +171 -0
- package/lib/interceptors.d.ts +18 -0
- package/lib/interceptors.js +56 -0
- package/lib/types.d.ts +38 -0
- package/lib/types.js +3 -0
- package/package.json +49 -0
- package/react-native-app-attestation.podspec +27 -0
- package/src/AttestationService.ts +123 -0
- package/src/DeviceIDService.ts +62 -0
- package/src/index.ts +174 -0
- package/src/interceptors.ts +72 -0
- package/src/types.ts +52 -0
package/README.md
ADDED
|
@@ -0,0 +1,451 @@
|
|
|
1
|
+
# react-native-app-attestation
|
|
2
|
+
|
|
3
|
+
A complete mobile app security solution for React Native apps.
|
|
4
|
+
|
|
5
|
+
## The Problem
|
|
6
|
+
|
|
7
|
+
Without this library, anyone can send fake requests to your API:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
# Anyone can do this from their terminal
|
|
11
|
+
curl -X POST https://api.yourapp.com/send-otp \
|
|
12
|
+
-d '{"phone": "9999999999"}'
|
|
13
|
+
|
|
14
|
+
# Or loop it 1000 times
|
|
15
|
+
for i in {1..1000}; do
|
|
16
|
+
curl -X POST https://api.yourapp.com/send-otp \
|
|
17
|
+
-d '{"phone": "9999999999"}'
|
|
18
|
+
done
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
This leads to:
|
|
22
|
+
- OTP bombing — spamming any phone number with OTPs
|
|
23
|
+
- Fake account creation using bots
|
|
24
|
+
- API abuse causing server crashes
|
|
25
|
+
- Data scraping
|
|
26
|
+
|
|
27
|
+
## The Solution
|
|
28
|
+
|
|
29
|
+
This library proves to your server that every request comes from your **genuine, official app** installed on a **real device** — not from scripts or bots.
|
|
30
|
+
|
|
31
|
+
It does this using:
|
|
32
|
+
- **Android**: Google Play Integrity API — Google guarantees the request is from your genuine app
|
|
33
|
+
- **iOS**: Apple App Attest — Apple guarantees the request is from your genuine app
|
|
34
|
+
|
|
35
|
+
Every API request automatically includes security headers that your server can verify.
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## How It Works
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
Your App Your Backend
|
|
43
|
+
| |
|
|
44
|
+
| 1. Get nonce |
|
|
45
|
+
| ─────────────────────────> |
|
|
46
|
+
| |
|
|
47
|
+
| 2. nonce received |
|
|
48
|
+
| <───────────────────────── |
|
|
49
|
+
| |
|
|
50
|
+
| 3. Send nonce to Google/Apple
|
|
51
|
+
| ──────────> Google/Apple |
|
|
52
|
+
| |
|
|
53
|
+
| 4. Signed attestation token|
|
|
54
|
+
| <────────── Google/Apple |
|
|
55
|
+
| |
|
|
56
|
+
| 5. API request with token |
|
|
57
|
+
| ─────────────────────────> |
|
|
58
|
+
| |
|
|
59
|
+
| 6. Server verifies token |
|
|
60
|
+
| with Google/Apple |
|
|
61
|
+
| |
|
|
62
|
+
| 7. Request allowed |
|
|
63
|
+
| <───────────────────────── |
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## What Gets Sent With Every Request
|
|
69
|
+
|
|
70
|
+
```
|
|
71
|
+
POST /api/send-otp
|
|
72
|
+
User-Agent: App/1.0.0 (Android)
|
|
73
|
+
X-App-Platform: android
|
|
74
|
+
X-App-Version: 1.0.0
|
|
75
|
+
X-Device-ID: 8f3c7e9d-21ab-4d5e-b3c2-9a7f1e6d4c8b
|
|
76
|
+
X-Timestamp: 1710249381
|
|
77
|
+
X-Attestation: eyJhbGciOiJSUzI1NiJ9...
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Your server checks all of these on every request.
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## Installation
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
npm install react-native-app-attestation
|
|
88
|
+
cd ios && pod install
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
That's it! Autolinking handles the native module setup automatically.
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## Android Setup
|
|
96
|
+
|
|
97
|
+
These steps are one-time only.
|
|
98
|
+
|
|
99
|
+
### Step 1: Link your app in Google Play Console
|
|
100
|
+
|
|
101
|
+
This tells Google that your app is allowed to use Play Integrity API.
|
|
102
|
+
|
|
103
|
+
```
|
|
104
|
+
1. Go to play.google.com/console
|
|
105
|
+
2. Select your app
|
|
106
|
+
3. Left menu: Release → Setup → App Integrity
|
|
107
|
+
4. Find "Play Integrity API" section
|
|
108
|
+
5. Click "Link"
|
|
109
|
+
6. Select your Google Cloud project
|
|
110
|
+
7. Confirm
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
> Note: Your app must already be uploaded to Play Console at least once.
|
|
114
|
+
|
|
115
|
+
### Step 2: Enable Play Integrity API in Google Cloud
|
|
116
|
+
|
|
117
|
+
```
|
|
118
|
+
1. Go to console.cloud.google.com
|
|
119
|
+
2. Select the same project you linked above
|
|
120
|
+
3. Left menu: APIs & Services → Library
|
|
121
|
+
4. Search for "Play Integrity API"
|
|
122
|
+
5. Click on it → Click "ENABLE"
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### Step 3: Add SDK dependency
|
|
126
|
+
|
|
127
|
+
In `android/app/build.gradle`:
|
|
128
|
+
|
|
129
|
+
```gradle
|
|
130
|
+
dependencies {
|
|
131
|
+
// ... your existing dependencies
|
|
132
|
+
implementation 'com.google.android.play:integrity:1.3.0'
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
## iOS Setup
|
|
139
|
+
|
|
140
|
+
These steps are one-time only.
|
|
141
|
+
|
|
142
|
+
### Step 1: Add App Attest capability in Xcode
|
|
143
|
+
|
|
144
|
+
```
|
|
145
|
+
1. Open your project in Xcode
|
|
146
|
+
2. Click your project name in the left sidebar (blue icon)
|
|
147
|
+
3. Select your app target
|
|
148
|
+
4. Click "Signing & Capabilities" tab
|
|
149
|
+
5. Click "+ Capability" button
|
|
150
|
+
6. Search for "App Attest"
|
|
151
|
+
7. Double click to add it
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### Step 2: Set up Bridging Header
|
|
155
|
+
|
|
156
|
+
```
|
|
157
|
+
1. Open Xcode
|
|
158
|
+
2. Click your project → Select your target
|
|
159
|
+
3. Go to "Build Settings" tab
|
|
160
|
+
4. Search for "Objective-C Bridging Header"
|
|
161
|
+
5. Set the value to:
|
|
162
|
+
YourProjectName/ReactNativeAppAttestation-Bridging-Header.h
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
---
|
|
166
|
+
|
|
167
|
+
## Code Setup
|
|
168
|
+
|
|
169
|
+
### Step 1: Initialize in App.tsx
|
|
170
|
+
|
|
171
|
+
Call `initAttestation` once when your app starts.
|
|
172
|
+
|
|
173
|
+
```typescript
|
|
174
|
+
import { initAttestation } from 'react-native-app-attestation';
|
|
175
|
+
|
|
176
|
+
// With MMKV (recommended — fast synchronous storage)
|
|
177
|
+
import { MMKV } from 'react-native-mmkv';
|
|
178
|
+
const mmkv = new MMKV();
|
|
179
|
+
|
|
180
|
+
initAttestation({
|
|
181
|
+
// Tell the library how to store the device ID
|
|
182
|
+
// You can use any storage — MMKV, AsyncStorage, etc.
|
|
183
|
+
storage: {
|
|
184
|
+
get: (key) => mmkv.getString(key) ?? null,
|
|
185
|
+
set: (key, val) => mmkv.set(key, val),
|
|
186
|
+
delete: (key) => mmkv.delete(key),
|
|
187
|
+
},
|
|
188
|
+
|
|
189
|
+
// Your backend endpoint that returns a nonce
|
|
190
|
+
nonceEndpoint: 'https://api.yourapp.com/auth/nonce',
|
|
191
|
+
|
|
192
|
+
// Your app version — sent in every request header
|
|
193
|
+
appVersion: '1.0.0',
|
|
194
|
+
|
|
195
|
+
// Show debug logs in development
|
|
196
|
+
debug: __DEV__,
|
|
197
|
+
});
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
```typescript
|
|
201
|
+
// With AsyncStorage
|
|
202
|
+
import AsyncStorage from '@react-native-async-storage/async-storage';
|
|
203
|
+
|
|
204
|
+
initAttestation({
|
|
205
|
+
storage: {
|
|
206
|
+
get: (key) => AsyncStorage.getItem(key),
|
|
207
|
+
set: (key, val) => AsyncStorage.setItem(key, val),
|
|
208
|
+
delete: (key) => AsyncStorage.removeItem(key),
|
|
209
|
+
},
|
|
210
|
+
nonceEndpoint: 'https://api.yourapp.com/auth/nonce',
|
|
211
|
+
appVersion: '1.0.0',
|
|
212
|
+
});
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
---
|
|
216
|
+
|
|
217
|
+
### Step 2: Secure your API calls
|
|
218
|
+
|
|
219
|
+
#### With Axios
|
|
220
|
+
|
|
221
|
+
```typescript
|
|
222
|
+
import axios from 'axios';
|
|
223
|
+
import { setupAxios } from 'react-native-app-attestation';
|
|
224
|
+
|
|
225
|
+
const api = axios.create({
|
|
226
|
+
baseURL: 'https://api.yourapp.com',
|
|
227
|
+
timeout: 15000,
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
// One line — all requests secured automatically
|
|
231
|
+
setupAxios(api);
|
|
232
|
+
|
|
233
|
+
// Use api normally — security headers added behind the scenes
|
|
234
|
+
const response = await api.get('/user/profile');
|
|
235
|
+
const response = await api.post('/send-otp', { phone: '9876543210' });
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
#### With Fetch
|
|
239
|
+
|
|
240
|
+
```typescript
|
|
241
|
+
import { secureGet, securePost } from 'react-native-app-attestation';
|
|
242
|
+
|
|
243
|
+
// GET request — replaces fetch()
|
|
244
|
+
const response = await secureGet('https://api.yourapp.com/user');
|
|
245
|
+
const data = await response.json();
|
|
246
|
+
|
|
247
|
+
// POST request — replaces fetch() with method POST
|
|
248
|
+
const response = await securePost(
|
|
249
|
+
'https://api.yourapp.com/login',
|
|
250
|
+
{ phone: '9876543210' }
|
|
251
|
+
);
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
#### With any other HTTP client
|
|
255
|
+
|
|
256
|
+
```typescript
|
|
257
|
+
import { getSecurityHeaders } from 'react-native-app-attestation';
|
|
258
|
+
|
|
259
|
+
// Get all headers as an object and add them manually
|
|
260
|
+
const headers = await getSecurityHeaders();
|
|
261
|
+
|
|
262
|
+
myHttpClient.request({
|
|
263
|
+
url: '/endpoint',
|
|
264
|
+
headers: {
|
|
265
|
+
...headers,
|
|
266
|
+
'Authorization': `Bearer ${token}`,
|
|
267
|
+
}
|
|
268
|
+
});
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
---
|
|
272
|
+
|
|
273
|
+
### Step 3: Sensitive operations
|
|
274
|
+
|
|
275
|
+
For payments or login, always use a fresh token.
|
|
276
|
+
This prevents replay attacks on critical endpoints.
|
|
277
|
+
|
|
278
|
+
```typescript
|
|
279
|
+
import { getFreshAttestationToken } from 'react-native-app-attestation';
|
|
280
|
+
|
|
281
|
+
const makePayment = async (paymentData) => {
|
|
282
|
+
// Force a fresh token — always bypasses cache
|
|
283
|
+
const freshToken = await getFreshAttestationToken();
|
|
284
|
+
|
|
285
|
+
await api.post('/payment', paymentData, {
|
|
286
|
+
headers: { 'X-Attestation': freshToken }
|
|
287
|
+
});
|
|
288
|
+
};
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
---
|
|
292
|
+
|
|
293
|
+
### Step 4: Logout
|
|
294
|
+
|
|
295
|
+
Clear the attestation cache when the user logs out.
|
|
296
|
+
|
|
297
|
+
```typescript
|
|
298
|
+
import { clearAttestationCache } from 'react-native-app-attestation';
|
|
299
|
+
|
|
300
|
+
const logout = () => {
|
|
301
|
+
clearAttestationCache();
|
|
302
|
+
// ... rest of your logout logic
|
|
303
|
+
};
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
---
|
|
307
|
+
|
|
308
|
+
## Backend Setup
|
|
309
|
+
|
|
310
|
+
### Nonce Endpoint
|
|
311
|
+
|
|
312
|
+
Create a `GET /auth/nonce` endpoint that returns a random one-time string.
|
|
313
|
+
|
|
314
|
+
```javascript
|
|
315
|
+
// Node.js example
|
|
316
|
+
const crypto = require('crypto');
|
|
317
|
+
|
|
318
|
+
app.get('/auth/nonce', async (req, res) => {
|
|
319
|
+
const nonce = crypto.randomBytes(32).toString('hex');
|
|
320
|
+
|
|
321
|
+
await db.nonces.create({
|
|
322
|
+
nonce,
|
|
323
|
+
expiresAt: new Date(Date.now() + 5 * 60 * 1000), // 5 minutes
|
|
324
|
+
used: false,
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
res.json({ nonce });
|
|
328
|
+
});
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
### Request Validation
|
|
332
|
+
|
|
333
|
+
Validate these headers on every incoming request:
|
|
334
|
+
|
|
335
|
+
```javascript
|
|
336
|
+
const validateRequest = async (req) => {
|
|
337
|
+
|
|
338
|
+
// 1. Check User-Agent
|
|
339
|
+
if (!req.headers['user-agent']?.includes('App/')) {
|
|
340
|
+
throw new Error('Invalid client');
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// 2. Reject requests older than 5 minutes (prevents replay attacks)
|
|
344
|
+
const timestamp = parseInt(req.headers['x-timestamp']);
|
|
345
|
+
const now = Math.floor(Date.now() / 1000);
|
|
346
|
+
if (Math.abs(now - timestamp) > 300) {
|
|
347
|
+
throw new Error('Request expired');
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// 3. Check Device ID format
|
|
351
|
+
const deviceId = req.headers['x-device-id'];
|
|
352
|
+
if (!/^[0-9a-f-]{36}$/i.test(deviceId)) {
|
|
353
|
+
throw new Error('Invalid device ID');
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// 4. Verify attestation token with Google/Apple
|
|
357
|
+
const isValid = await verifyAttestationToken(
|
|
358
|
+
req.headers['x-attestation'],
|
|
359
|
+
req.headers['x-app-platform']
|
|
360
|
+
);
|
|
361
|
+
if (!isValid) throw new Error('Attestation failed');
|
|
362
|
+
};
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
### Verify Android Token (Play Integrity)
|
|
366
|
+
|
|
367
|
+
```javascript
|
|
368
|
+
const verifyAndroidToken = async (token) => {
|
|
369
|
+
const response = await fetch(
|
|
370
|
+
`https://playintegrity.googleapis.com/v1/${PACKAGE_NAME}:decodeIntegrityToken`,
|
|
371
|
+
{
|
|
372
|
+
method: 'POST',
|
|
373
|
+
headers: { Authorization: `Bearer ${GOOGLE_ACCESS_TOKEN}` },
|
|
374
|
+
body: JSON.stringify({ integrity_token: token }),
|
|
375
|
+
}
|
|
376
|
+
);
|
|
377
|
+
|
|
378
|
+
const data = await response.json();
|
|
379
|
+
|
|
380
|
+
return (
|
|
381
|
+
data.tokenPayloadExternal?.appIntegrity?.appRecognitionVerdict === 'PLAY_RECOGNIZED' &&
|
|
382
|
+
data.tokenPayloadExternal?.deviceIntegrity?.deviceRecognitionVerdict
|
|
383
|
+
?.includes('MEETS_DEVICE_INTEGRITY')
|
|
384
|
+
);
|
|
385
|
+
};
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
### Verify iOS Token (App Attest)
|
|
389
|
+
|
|
390
|
+
```javascript
|
|
391
|
+
const verifyIOSToken = async (token) => {
|
|
392
|
+
// Full guide: https://developer.apple.com/documentation/devicecheck/validating_apps_that_connect_to_your_server
|
|
393
|
+
const attestation = Buffer.from(token, 'base64');
|
|
394
|
+
// ... Apple certificate chain verification
|
|
395
|
+
};
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
---
|
|
399
|
+
|
|
400
|
+
## Token Caching
|
|
401
|
+
|
|
402
|
+
The library automatically caches the attestation token for 10 minutes.
|
|
403
|
+
Google/Apple is only called once every 10 minutes — not on every request.
|
|
404
|
+
|
|
405
|
+
```
|
|
406
|
+
10:00 → App opens — fresh token fetched, cached for 10 min
|
|
407
|
+
10:02 → API call — cached token used (no Google/Apple call)
|
|
408
|
+
10:05 → API call — cached token used (no Google/Apple call)
|
|
409
|
+
10:08 → API call — cached token used (no Google/Apple call)
|
|
410
|
+
10:10 → Cache expired — fresh token fetched automatically
|
|
411
|
+
10:15 → Payment — getFreshAttestationToken() always bypasses cache
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
Customize the cache duration:
|
|
415
|
+
|
|
416
|
+
```typescript
|
|
417
|
+
initAttestation({
|
|
418
|
+
tokenCacheDurationMs: 5 * 60 * 1000, // 5 minutes instead of default 10
|
|
419
|
+
});
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
---
|
|
423
|
+
|
|
424
|
+
## API Reference
|
|
425
|
+
|
|
426
|
+
| Function | Description |
|
|
427
|
+
|----------|-------------|
|
|
428
|
+
| `initAttestation(config)` | Initialize once at app start |
|
|
429
|
+
| `setupAxios(instance)` | Setup axios interceptor — one line secures all requests |
|
|
430
|
+
| `secureGet(url, options?)` | Secure replacement for fetch GET |
|
|
431
|
+
| `securePost(url, body, options?)` | Secure replacement for fetch POST |
|
|
432
|
+
| `getDeviceID()` | Get persistent device UUID |
|
|
433
|
+
| `getAttestationToken(forceRefresh?)` | Get cached or fresh attestation token |
|
|
434
|
+
| `getFreshAttestationToken()` | Always fresh token — use for payments and login |
|
|
435
|
+
| `getSecurityHeaders()` | Get all security headers as an object |
|
|
436
|
+
| `clearAttestationCache()` | Clear token cache — call on logout |
|
|
437
|
+
| `resetDeviceID()` | Delete stored device ID |
|
|
438
|
+
|
|
439
|
+
---
|
|
440
|
+
|
|
441
|
+
## Requirements
|
|
442
|
+
|
|
443
|
+
- React Native >= 0.70
|
|
444
|
+
- iOS >= 14.0
|
|
445
|
+
- Android API level >= 24
|
|
446
|
+
|
|
447
|
+
---
|
|
448
|
+
|
|
449
|
+
## License
|
|
450
|
+
|
|
451
|
+
MIT
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
buildscript {
|
|
2
|
+
repositories {
|
|
3
|
+
google()
|
|
4
|
+
mavenCentral()
|
|
5
|
+
}
|
|
6
|
+
dependencies {
|
|
7
|
+
classpath "com.android.tools.build:gradle:7.4.2"
|
|
8
|
+
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.8.0"
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
apply plugin: "com.android.library"
|
|
13
|
+
apply plugin: "kotlin-android"
|
|
14
|
+
|
|
15
|
+
android {
|
|
16
|
+
compileSdkVersion 33
|
|
17
|
+
defaultConfig {
|
|
18
|
+
minSdkVersion 24
|
|
19
|
+
targetSdkVersion 33
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
repositories {
|
|
24
|
+
google()
|
|
25
|
+
mavenCentral()
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
dependencies {
|
|
29
|
+
implementation "com.facebook.react:react-native:+"
|
|
30
|
+
implementation "com.google.android.play:integrity:1.3.0"
|
|
31
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
package com.reactnativeappattestation
|
|
2
|
+
|
|
3
|
+
import com.facebook.react.bridge.Promise
|
|
4
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
5
|
+
import com.facebook.react.bridge.ReactContextBaseJavaModule
|
|
6
|
+
import com.facebook.react.bridge.ReactMethod
|
|
7
|
+
import com.google.android.play.core.integrity.IntegrityManagerFactory
|
|
8
|
+
import com.google.android.play.core.integrity.IntegrityTokenRequest
|
|
9
|
+
|
|
10
|
+
class PlayIntegrityModule(reactContext: ReactApplicationContext)
|
|
11
|
+
: ReactContextBaseJavaModule(reactContext) {
|
|
12
|
+
|
|
13
|
+
override fun getName() = "PlayIntegrityModule"
|
|
14
|
+
|
|
15
|
+
@ReactMethod
|
|
16
|
+
fun getAttestationToken(nonce: String, promise: Promise) {
|
|
17
|
+
try {
|
|
18
|
+
val integrityManager = IntegrityManagerFactory
|
|
19
|
+
.create(reactApplicationContext)
|
|
20
|
+
|
|
21
|
+
val request = IntegrityTokenRequest.builder()
|
|
22
|
+
.setNonce(nonce)
|
|
23
|
+
.build()
|
|
24
|
+
|
|
25
|
+
integrityManager
|
|
26
|
+
.requestIntegrityToken(request)
|
|
27
|
+
.addOnSuccessListener { response ->
|
|
28
|
+
promise.resolve(response.token())
|
|
29
|
+
}
|
|
30
|
+
.addOnFailureListener { error ->
|
|
31
|
+
promise.reject("INTEGRITY_ERROR", error.message)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
} catch (e: Exception) {
|
|
35
|
+
promise.reject("UNEXPECTED_ERROR", e.message)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
package com.reactnativeappattestation
|
|
2
|
+
|
|
3
|
+
import com.facebook.react.ReactPackage
|
|
4
|
+
import com.facebook.react.bridge.NativeModule
|
|
5
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
6
|
+
import com.facebook.react.uimanager.ViewManager
|
|
7
|
+
|
|
8
|
+
class PlayIntegrityPackage : ReactPackage {
|
|
9
|
+
|
|
10
|
+
override fun createNativeModules(
|
|
11
|
+
reactContext: ReactApplicationContext
|
|
12
|
+
): List<NativeModule> {
|
|
13
|
+
return listOf(PlayIntegrityModule(reactContext))
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
override fun createViewManagers(
|
|
17
|
+
reactContext: ReactApplicationContext
|
|
18
|
+
): List<ViewManager<*, *>> {
|
|
19
|
+
return emptyList()
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import DeviceCheck
|
|
3
|
+
import CryptoKit
|
|
4
|
+
|
|
5
|
+
@objc(AppAttestModule)
|
|
6
|
+
class AppAttestModule: NSObject {
|
|
7
|
+
|
|
8
|
+
@objc static func requiresMainQueueSetup() -> Bool {
|
|
9
|
+
return false
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
@objc func getAttestationToken(
|
|
13
|
+
_ challenge: String,
|
|
14
|
+
resolve: @escaping RCTPromiseResolveBlock,
|
|
15
|
+
reject: @escaping RCTPromiseRejectBlock
|
|
16
|
+
) {
|
|
17
|
+
guard DCAppAttestService.shared.isSupported else {
|
|
18
|
+
reject("NOT_SUPPORTED", "App Attest not supported", nil)
|
|
19
|
+
return
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
DCAppAttestService.shared.generateKey { keyId, error in
|
|
23
|
+
if let error = error {
|
|
24
|
+
reject("KEY_ERROR", error.localizedDescription, error)
|
|
25
|
+
return
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
guard let keyId = keyId else {
|
|
29
|
+
reject("KEY_ERROR", "KeyId nil aaya", nil)
|
|
30
|
+
return
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
let challengeData = challenge.data(using: .utf8)!
|
|
34
|
+
let hash = Data(SHA256.hash(data: challengeData))
|
|
35
|
+
|
|
36
|
+
DCAppAttestService.shared.attestKey(
|
|
37
|
+
keyId,
|
|
38
|
+
clientDataHash: hash
|
|
39
|
+
) { attestation, error in
|
|
40
|
+
|
|
41
|
+
if let error = error {
|
|
42
|
+
reject("ATTEST_ERROR", error.localizedDescription, error)
|
|
43
|
+
return
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
guard let attestation = attestation else {
|
|
47
|
+
reject("ATTEST_ERROR", "Attestation nil aaya", nil)
|
|
48
|
+
return
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
let token = attestation.base64EncodedString()
|
|
52
|
+
resolve(token)
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
#import <React/RCTBridgeModule.h>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { AttestationConfig, AttestationResult } from './types';
|
|
2
|
+
export declare class AttestationService {
|
|
3
|
+
private config;
|
|
4
|
+
private cachedToken;
|
|
5
|
+
private tokenExpiry;
|
|
6
|
+
private cacheDuration;
|
|
7
|
+
constructor(config: AttestationConfig);
|
|
8
|
+
private log;
|
|
9
|
+
private fetchNonce;
|
|
10
|
+
private getAndroidToken;
|
|
11
|
+
private getIOSToken;
|
|
12
|
+
getToken(forceRefresh?: boolean): Promise<AttestationResult>;
|
|
13
|
+
getFreshToken(): Promise<AttestationResult>;
|
|
14
|
+
clearCache(): void;
|
|
15
|
+
}
|