@necessarysdk/react-native 0.0.0-stage → 0.1.5

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.
Files changed (114) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/LICENSE +201 -0
  3. package/NOTICE +2 -0
  4. package/NecessaryReactNative.podspec +23 -0
  5. package/README.md +429 -2
  6. package/android/build.gradle +83 -0
  7. package/android/src/main/AndroidManifest.xml +41 -0
  8. package/android/src/main/cpp/CMakeLists.txt +7 -0
  9. package/android/src/main/cpp/code_integrity.cpp +484 -0
  10. package/android/src/main/java/com/necessarysdk/reactnative/Causes.kt +49 -0
  11. package/android/src/main/java/com/necessarysdk/reactnative/InstallationKeyManager.kt +388 -0
  12. package/android/src/main/java/com/necessarysdk/reactnative/IntegrityProbeManager.kt +1152 -0
  13. package/android/src/main/java/com/necessarysdk/reactnative/KeyPresence.kt +62 -0
  14. package/android/src/main/java/com/necessarysdk/reactnative/NecessaryModules.kt +94 -0
  15. package/android/src/main/java/com/necessarysdk/reactnative/NecessaryNative.kt +323 -0
  16. package/android/src/main/java/com/necessarysdk/reactnative/NecessaryPackage.kt +38 -0
  17. package/android/src/main/java/com/necessarysdk/reactnative/ProbeReads.kt +314 -0
  18. package/android/src/main/java/com/necessarysdk/reactnative/ScreenGuard.kt +56 -0
  19. package/android/src/main/java/com/necessarysdk/reactnative/SecureStore.kt +127 -0
  20. package/android/src/main/java/com/necessarysdk/reactnative/StepUpKeyEntry.kt +57 -0
  21. package/android/src/main/java/com/necessarysdk/reactnative/StepUpKeyManager.kt +762 -0
  22. package/contract/report-1/android-8-ios-5/RULES.md +149 -0
  23. package/contract/report-1/android-8-ios-5/report.schema.json +2753 -0
  24. package/contract/report-1/android-8-ios-5/samples/android-8-complete.json +205 -0
  25. package/contract/report-1/android-8-ios-5/samples/android-8-incomplete.json +205 -0
  26. package/contract/report-1/android-8-ios-5/samples/android-8-invalid.json +205 -0
  27. package/contract/report-1/android-8-ios-5/samples/android-8-location-one-fix.json +228 -0
  28. package/contract/report-1/android-8-ios-5/samples/android-8-location-two-fixes.json +228 -0
  29. package/contract/report-1/android-8-ios-5/samples/android-8-refused.json +208 -0
  30. package/contract/report-1/android-8-ios-5/samples/ios-5-complete.json +106 -0
  31. package/contract/report-1/android-8-ios-5/samples/ios-5-incomplete.json +105 -0
  32. package/contract/report-1/android-8-ios-5/samples/ios-5-invalid.json +107 -0
  33. package/contract/report-1/android-8-ios-5/samples/ios-5-partial.json +110 -0
  34. package/contract/report-1/android-8-ios-5/samples/ios-5-refused.json +109 -0
  35. package/doc/integration_guide.md +771 -0
  36. package/doc/measurements.md +774 -0
  37. package/ios/InstallationKeyManager.swift +694 -0
  38. package/ios/IntegrityProbeManager.swift +960 -0
  39. package/ios/NecessaryModules.h +34 -0
  40. package/ios/NecessaryModules.mm +208 -0
  41. package/ios/NecessaryNative.swift +327 -0
  42. package/ios/PrivacyInfo.xcprivacy +14 -0
  43. package/ios/ProbeReads.swift +353 -0
  44. package/lib/client.d.ts +105 -0
  45. package/lib/client.js +176 -0
  46. package/lib/collector.d.ts +18 -0
  47. package/lib/collector.js +49 -0
  48. package/lib/deviceTrust.d.ts +253 -0
  49. package/lib/deviceTrust.js +690 -0
  50. package/lib/errors.d.ts +43 -0
  51. package/lib/errors.js +56 -0
  52. package/lib/index.d.ts +18 -0
  53. package/lib/index.js +34 -0
  54. package/lib/installationKey.d.ts +62 -0
  55. package/lib/installationKey.js +185 -0
  56. package/lib/installationStore.d.ts +49 -0
  57. package/lib/installationStore.js +127 -0
  58. package/lib/native/NativeNecessaryInstallationKey.d.ts +10 -0
  59. package/lib/native/NativeNecessaryInstallationKey.js +4 -0
  60. package/lib/native/NativeNecessaryIntegrity.d.ts +8 -0
  61. package/lib/native/NativeNecessaryIntegrity.js +4 -0
  62. package/lib/native/NativeNecessarySecureScreen.d.ts +7 -0
  63. package/lib/native/NativeNecessarySecureScreen.js +4 -0
  64. package/lib/native/NativeNecessaryStepUpKey.d.ts +9 -0
  65. package/lib/native/NativeNecessaryStepUpKey.js +4 -0
  66. package/lib/native/NativeNecessaryStorage.d.ts +14 -0
  67. package/lib/native/NativeNecessaryStorage.js +4 -0
  68. package/lib/protocol.d.ts +24 -0
  69. package/lib/protocol.js +50 -0
  70. package/lib/reinstallHint.d.ts +23 -0
  71. package/lib/reinstallHint.js +65 -0
  72. package/lib/report.d.ts +62 -0
  73. package/lib/report.js +140 -0
  74. package/lib/requestProof.d.ts +116 -0
  75. package/lib/requestProof.js +220 -0
  76. package/lib/secureScreen.d.ts +15 -0
  77. package/lib/secureScreen.js +32 -0
  78. package/lib/stepUpKey.d.ts +105 -0
  79. package/lib/stepUpKey.js +187 -0
  80. package/lib/transport.d.ts +39 -0
  81. package/lib/transport.js +117 -0
  82. package/lib/util/bytes.d.ts +18 -0
  83. package/lib/util/bytes.js +207 -0
  84. package/lib/util/native.d.ts +20 -0
  85. package/lib/util/native.js +49 -0
  86. package/lib/util/release.d.ts +2 -0
  87. package/lib/util/release.js +7 -0
  88. package/lib/util/target.d.ts +18 -0
  89. package/lib/util/target.js +128 -0
  90. package/package.json +81 -4
  91. package/src/client.ts +221 -0
  92. package/src/collector.ts +64 -0
  93. package/src/deviceTrust.ts +842 -0
  94. package/src/errors.ts +71 -0
  95. package/src/index.ts +19 -0
  96. package/src/installationKey.ts +223 -0
  97. package/src/installationStore.ts +143 -0
  98. package/src/native/NativeNecessaryInstallationKey.ts +12 -0
  99. package/src/native/NativeNecessaryIntegrity.ts +15 -0
  100. package/src/native/NativeNecessarySecureScreen.ts +9 -0
  101. package/src/native/NativeNecessaryStepUpKey.ts +11 -0
  102. package/src/native/NativeNecessaryStorage.ts +16 -0
  103. package/src/protocol.ts +52 -0
  104. package/src/reinstallHint.ts +69 -0
  105. package/src/report.ts +174 -0
  106. package/src/requestProof.ts +295 -0
  107. package/src/secureScreen.ts +29 -0
  108. package/src/stepUpKey.ts +238 -0
  109. package/src/transport.ts +145 -0
  110. package/src/util/bytes.ts +194 -0
  111. package/src/util/native.ts +56 -0
  112. package/src/util/release.ts +6 -0
  113. package/src/util/target.ts +132 -0
  114. package/vectors/client_server.json +264 -0
package/README.md CHANGED
@@ -1,3 +1,430 @@
1
- # Temporary Holding Version
1
+ # Necessary for React Native
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Device integrity for React Native apps on Android and iOS. The SDK gives your app a key it never
4
+ exports — made in the phone's keystore, in secure hardware where the phone has it and in software where
5
+ it does not — signed measurements of the phone and of the app running on it, and a proof on each request
6
+ your app sends — and, if you turn them on, a second key the person must unlock to approve a request, and
7
+ a PIN your backend checks. **It never scores and never decides: your backend does.**
8
+
9
+ Android 7.0 (API 24) and later; iOS 15 and later; React Native 0.76 or later with the New Architecture
10
+ (the default since 0.76, the only one since 0.82). It is built and tested with React Native 0.88, which
11
+ the example app runs.
12
+
13
+ ## Documentation
14
+
15
+ - [The integration guide](https://github.com/merajhasan88/necessary_reactnative/blob/main/doc/integration_guide.md)
16
+ (`doc/integration_guide.md`) — the protocol: what the SDK sends, and how your backend checks it.
17
+ - [The measurements](https://github.com/merajhasan88/necessary_reactnative/blob/main/doc/measurements.md)
18
+ (`doc/measurements.md`) — everything the SDK measures on the phone, field by field: what each
19
+ measurement is, how it is taken, and what a complete answer holds.
20
+ - [The measurement contract](https://github.com/merajhasan88/necessary_reactnative/tree/main/contract/report-1/android-8-ios-5)
21
+ (`contract/report-1/android-8-ios-5/`) — the report's schema, its rules and sample reports. The
22
+ folder's name is two versions: `report-1` is the report's format, and `android-8-ios-5` the lowest
23
+ versions of the measuring code, the collector, that the contract accepts — 8 on Android and 5 on iOS
24
+ (`collector_version`; this SDK's are 10 and 8). They are not the Android or iOS versions the SDK runs
25
+ on, which are above.
26
+ - [The example app](https://github.com/merajhasan88/necessary_reactnative/tree/main/example)
27
+ (`example/`) puts every piece below in a working app, against the
28
+ [sample backends](https://github.com/merajhasan88/necessary_reactnative/tree/main/backend)
29
+ (in the repository, not the package). [Try the example app](#try-the-example-app), below, installs it
30
+ and tests it step by step, measurement by measurement.
31
+
32
+ ## Install
33
+
34
+ ```sh
35
+ npm install @necessarysdk/react-native
36
+ cd ios && pod install
37
+ ```
38
+
39
+ The SDK's native modules link themselves (autolinking); rebuild the app after installing. The SDK
40
+ declares what its own features need. Your app adds only what you choose to use:
41
+
42
+ - **Location measurements**: on Android `ACCESS_COARSE_LOCATION` (and `ACCESS_FINE_LOCATION` for a
43
+ precise location) in your manifest; on iOS `NSLocationWhenInUseUsageDescription` in `Info.plist`.
44
+ The SDK never asks by itself: call `trust.requestLocationPermission()` from your own screen.
45
+ - **Biometric step-up on iOS**: `NSFaceIDUsageDescription` in `Info.plist`.
46
+
47
+ ## Start
48
+
49
+ ```ts
50
+ import { DeviceTrust, PersonalMeasurement } from '@necessarysdk/react-native';
51
+
52
+ const trust = new DeviceTrust({
53
+ baseUrl: 'https://api.example.com',
54
+ // The personal measurements your app allows; your backend asks for what it needs of them.
55
+ personalMeasurements: [PersonalMeasurement.screenCapture],
56
+ });
57
+
58
+ await trust.start(); // loads or makes the key, registers it, proves it holds it
59
+ ```
60
+
61
+ `start()` makes the guide's first three calls under `baseUrl`: register the installation, ask for a key
62
+ challenge, answer it. Your backend answers each as the guide says. A new key is a new installation:
63
+ `trust.identity?.createdThisLaunch` tells your app to forget what it kept for the old one.
64
+
65
+ ## Scan
66
+
67
+ ```ts
68
+ const scan = await trust.scan();
69
+ const answer = scan.answer; // your backend's answer, whole
70
+ ```
71
+
72
+ The SDK measures what your backend's challenge asks for — and, of the personal measurements it asks
73
+ for, the ones your app enabled — signs the report with the installation key and sends it. It acts on the
74
+ guide's protocol errors once (a challenge expired, the installation session expired, the backend lost
75
+ the installation, a short wait); any other refusal reaches your app as an `ApiException`.
76
+
77
+ ## Personal measurements
78
+
79
+ Seven measurements identify a person's phone, network or habits. Each is collected only when your app
80
+ enables it **and** your backend's challenge asks for it (`optional_probes`); otherwise the report says
81
+ `not_enabled`. None is on unless your app turns it on:
82
+
83
+ ```ts
84
+ trust.personalMeasurements = new Set([
85
+ PersonalMeasurement.screenCapture, // the screen recorded, mirrored or cast
86
+ PersonalMeasurement.location, // the last location the phone already knows
87
+ ]);
88
+ ```
89
+
90
+ Set them in the constructor, or change them whenever the person does — in a privacy setting: the next
91
+ scan follows, and the installation and its step-up key stay as they are.
92
+
93
+ | `PersonalMeasurement` | What it reports | Platforms |
94
+ |---|---|---|
95
+ | `localAddresses` | the phone's own IP addresses, link-local ones left out | both |
96
+ | `accessibility` | the Accessibility services that are on, and the keyboard in use | Android |
97
+ | `notificationAccess` | the apps allowed to read notifications | Android |
98
+ | `otherAccess` | device-administrator apps, and known remote-control apps installed | Android |
99
+ | `screenCapture` | the screen being recorded, mirrored or cast | both |
100
+ | `location` | the last location the phone already knows — never a fresh fix, never a prompt | both |
101
+ | `mockLocation` | whether that location was faked | both |
102
+
103
+ The location needs the person's permission as well: declare it (Install, above) and ask from your own
104
+ screen with `await trust.requestLocationPermission()`, which answers `precise`, `approximate` or
105
+ `none`. On iOS the three Android-only measurements answer `unsupported`.
106
+
107
+ ## Prove your own requests
108
+
109
+ Every request your app sends to your own API can carry a proof that this installation's key signed it,
110
+ so a copied session token is useless on another phone:
111
+
112
+ ```ts
113
+ const body = JSON.stringify({ amount: 25, to: 'payee-7' });
114
+ const proof = await trust.signRequest({
115
+ method: 'POST',
116
+ path: '/v1/transfer',
117
+ bearer: accountSession, // your backend's session for this installation
118
+ body, // sent exactly as signed
119
+ });
120
+ await fetch('https://api.example.com/v1/transfer', {
121
+ method: 'POST',
122
+ headers: {
123
+ Authorization: `Bearer ${accountSession}`,
124
+ 'Content-Type': 'application/json',
125
+ ...proof.headers, // X-Access-Proof, X-Access-Signature
126
+ },
127
+ body,
128
+ });
129
+ ```
130
+
131
+ Each proof serves its request once. `trust.send(proof)` sends it through the SDK's transport instead.
132
+
133
+ ## A scan for one action
134
+
135
+ For a request that matters — a payment, a new payee — have the scan name it, under the account session
136
+ that will send it, and send the scan's report with the request:
137
+
138
+ ```ts
139
+ import { describeOperation } from '@necessarysdk/react-native';
140
+
141
+ const operation = describeOperation('POST', '/v1/transfer', body);
142
+ const scan = await trust.scan({ session: accountSession, operation });
143
+ const proof = await trust.signRequest({
144
+ method: 'POST',
145
+ path: '/v1/transfer',
146
+ bearer: accountSession,
147
+ body,
148
+ reportId: scan.reportId, // X-Integrity-Report
149
+ });
150
+ ```
151
+
152
+ ## Step-up: the person approves one request
153
+
154
+ Turn it on, and choose how the key is unlocked:
155
+
156
+ ```ts
157
+ import { DeviceTrust, StepUpOptions } from '@necessarysdk/react-native';
158
+
159
+ const trust = new DeviceTrust({
160
+ baseUrl: 'https://api.example.com',
161
+ stepUp: new StepUpOptions({ factor: 'passcode', mode: 'per_use' }),
162
+ });
163
+ await trust.start(); // registers the step-up key with the installation
164
+
165
+ if (trust.stepUpUnavailable !== null) {
166
+ // This phone cannot make the key -- no screen lock, say. Step-up is off here.
167
+ }
168
+
169
+ const proof = await trust.signRequest({
170
+ method: 'POST',
171
+ path: '/v1/transfer',
172
+ bearer: accountSession,
173
+ body,
174
+ stepUp: true,
175
+ reason: 'Approve the transfer of 25', // what the phone shows the person
176
+ });
177
+ ```
178
+
179
+ | Option | Values |
180
+ |---|---|
181
+ | `factor` | `passcode` (the phone's PIN, pattern or password) or `biometric` (a fingerprint or face enrolled now) |
182
+ | `mode` | `per_use` (unlocked for every signature) or `windowed` with `windowSeconds` (one unlock for a while; Android only) |
183
+
184
+ Android 9 and 10 cannot ask for the passcode at every signature: there a passcode key is windowed, and
185
+ says so to your backend. iOS makes per-use keys only. An app whose backend sets the policy assigns
186
+ `trust.stepUp` before `start()`.
187
+
188
+ Removing the screen lock, or enrolling another biometric for a biometric key, makes the key unusable:
189
+ an approval then fails with `NativeKeyException` `STEPUP_KEY_INVALIDATED`. On Android the SDK deletes
190
+ the key only when Android itself says so; when the keystore cannot establish whether the key is usable,
191
+ or cannot be reached at all — a failure it may recover from — the approval fails with
192
+ `STEPUP_KEY_UNAVAILABLE` and the key is kept: try again later. Android 9 does not say that a key the
193
+ screen lock killed is dead, so there such a key answers `STEPUP_KEY_UNAVAILABLE`. The person re-enrols a
194
+ new key with their own re-authentication, which also replaces a key that stays unavailable:
195
+
196
+ ```ts
197
+ await trust.reenrolStepUp({
198
+ session: accountSession,
199
+ reauthentication: { password }, // as your backend requires
200
+ });
201
+ ```
202
+
203
+ A new key is made, the person unlocks it to sign its proof, and your backend takes it for that account
204
+ alone; approvals use it from then on. The call goes to `/v1/installations/stepup-key`, or to your API's
205
+ own path: `new StepUpOptions({ reenrolPath: ... })`. Until your backend has taken the new key — the
206
+ person cancelled the prompt, the password was wrong, the answer was lost — approvals fail with
207
+ `STEPUP_REENROL_PENDING`, across restarts too: call `reenrolStepUp` again. The same holds for a key the
208
+ phone made after the installation was registered — the screen lock was off at the first `start()` and
209
+ set later, say — since your backend takes a step-up key only at an installation's first registration:
210
+ `start()` reports `stepUpUnavailable` `STEPUP_REENROL_PENDING`, and re-enrolment registers the key.
211
+
212
+ ## The app PIN: the person's PIN for one request
213
+
214
+ Turn it on; the two paths default to the guide's and can be your API's own:
215
+
216
+ ```ts
217
+ import { AppPinOptions, DeviceTrust } from '@necessarysdk/react-native';
218
+
219
+ const trust = new DeviceTrust({
220
+ baseUrl: 'https://api.example.com',
221
+ appPin: new AppPinOptions(), // path: '/v1/account/app-pin', proofPath: '/v1/account/app-pin/proof'
222
+ });
223
+
224
+ // Setting it: with the person's own re-authentication, as your backend requires.
225
+ await trust.setAppPin({ session: accountSession, pin, reauthentication: { password } });
226
+
227
+ // Using it: your backend checks the PIN for this one request, and the request carries its proof.
228
+ const proof = await trust.signRequest({
229
+ method: 'POST',
230
+ path: '/v1/transfer',
231
+ bearer: accountSession,
232
+ body,
233
+ appPin: pin,
234
+ });
235
+ ```
236
+
237
+ The PIN travels once, in a signed body, never in a header, and the SDK never stores it. Protect the
238
+ screen while it is typed:
239
+
240
+ ```ts
241
+ import { SecureScreen } from '@necessarysdk/react-native';
242
+
243
+ await SecureScreen.protect(true); // Android: no screenshots or recording; iOS: covered while captured
244
+ // ... the PIN is typed ...
245
+ await SecureScreen.protect(false);
246
+ ```
247
+
248
+ Calls nest — each `protect(true)` needs its `protect(false)` — and when the last one ends the window gets
249
+ back what it had: a window your app keeps secure itself stays secure. On iOS the cover is over your app's
250
+ window, not the system keyboard. Opening Control Center to start a recording closes the keyboard, so
251
+ protect whenever the field has focus, as the example does.
252
+
253
+ Your backend's refusals reach your app as `ApiException` with the guide's codes: `app_pin_not_set`,
254
+ `app_pin_invalid` (it counts), `app_pin_locked` (a new PIN, set with the re-authentication, unlocks it).
255
+ Step-up and the app PIN combine: your backend decides which requests need which.
256
+
257
+ ## What happens when
258
+
259
+ | When | What the SDK does |
260
+ |---|---|
261
+ | The key is lost — an Android reinstall, a cleared keystore | `start()` makes a new key and a new installation ID; `trust.identity?.createdThisLaunch` is true, so forget what you kept for the old installation. Your backend links the two only provisionally, by the reinstall hint |
262
+ | Android's keystore cannot be reached | its lookups answer "no such key" then too, so the SDK makes a key only where the keystore, answering, holds none: `start()` fails with `NativeKeyException` `KEY_UNAVAILABLE`, sends nothing and keeps the installation as it is — call it again later; if it persists, `reset()` starts a new installation. Signing answers the same, and a step-up key `STEPUP_KEY_UNAVAILABLE` |
263
+ | iOS keeps the key across a reinstall | with its ID, the same installation comes back; without it, the SDK proposes a new ID, your backend answers with the one it holds for the key, and the SDK keeps that one |
264
+ | The app's process restarts | nothing is kept in memory: `start()` again finds the same key and installation, and proves it for a new installation session |
265
+ | The activity, a scene or a window is replaced | screen protection follows: Android sets it on each new window; on iOS the cover is placed again, on the window the person sees in each scene, whenever a window becomes key, visible or hidden or a scene comes to the foreground — and protection asked for before any window exists covers the first one. A step-up prompt the activity's end cancelled answers `STEPUP_CANCELLED`: ask again |
266
+ | Two calls at once | `start()`, `prove()`, `loadIdentity()` and the same `scan()` run once, and the second caller gets the first's result; scans for different operations, or asked after the personal measurements changed, run apart |
267
+ | A step-up prompt is open | on iOS the SDK's other calls — a scan, a request's signature — wait until the person answers it; Android runs them meanwhile |
268
+ | `reset()` or `close()` while calls run | the calls end with `InterruptedCallException` and keep nothing — no session, no installation ID, no report sent, no request proof or app PIN answer handed out; a reset first waits for every write and key change still running, so none outlives it; after a reset, new calls wait for the deletion and never join one made before. A proof handed out before the reset, or a request already sent, is beyond it: reset is local |
269
+ | A key cannot be deleted | `reset()` fails with that `NativeKeyException` (`KEY_DELETE_FAILED`): call it again. If it is the installation key, the installation stays as it was, its ID with it; if it is the step-up key, the installation itself is already gone. None to delete, or no step-up on the phone, is no failure |
270
+ | A native call fails on Android | the `NativeKeyException`'s message says what failed, then everything beneath it — `[cause: …]`, each failure's kind and words down to the keystore's own answer, and from Android 13 the keystore's numeric code wherever Android passes it on — so a key that reads as unavailable or dead says what the keystore answered. A probe's `error` and each `unread` reason carry the same, within the contract's lengths |
271
+ | A call abandoned, or its answer lost | a scan is simply asked for again — a challenge serves once; the SDK never sends a signed request twice: sign it again, and let your backend decide whether the first one reached it |
272
+ | A report over 65,536 bytes | it is never sent: the app lists the contract lets a collector shorten are shortened, and say `truncated`; anything else answers an `ApiException` `report_too_large` |
273
+ | The stored installation ID cannot be read | `start()` fails with `NativeKeyException` `STORAGE_UNREADABLE` and makes nothing new over it: call it again; `reset()` starts a new installation |
274
+
275
+ ## Your own transport
276
+
277
+ ```ts
278
+ const trust = DeviceTrust.over(new MyTransport()); // implements DeviceTrustTransport
279
+ ```
280
+
281
+ A transport sends each message the SDK builds and answers your backend's JSON object, or throws an
282
+ `ApiException` with its `code`, `message` and `details`. To carry the messages over an API of your own
283
+ shape instead, use the pieces directly — `trust.client`, `collectReport`, `signRequest` — and make the
284
+ guide's checks in your backend.
285
+
286
+ ## What the SDK never does
287
+
288
+ It never scores, decides, or tells your app a score; never stores your account session; never sends the
289
+ PIN in a header; and uses no platform attestation (Play Integrity, SafetyNet, App Attest, DeviceCheck).
290
+ It never exports a private key, but it cannot prove to your backend where a key is kept: the
291
+ registration's `key_security` is the phone's own word, and where the secure hardware declines — the iOS
292
+ Simulator, some configurations — the key is a software key, which a compromised phone can copy. A fully
293
+ compromised phone can falsify what it measures and can use a genuine key to sign what it likes: the
294
+ measurements raise the cost of fraud, and your backend weighs them with its own business logic.
295
+
296
+ ## Try the example app
297
+
298
+ The example app (`example/`) puts every piece above in a working app, against a sample backend.
299
+ Its own README carries the same steps.
300
+
301
+ ### Install it
302
+
303
+ The sample backends live in the repository, not in the package, so start from a clone.
304
+
305
+ 1. Clone the repository:
306
+
307
+ ```sh
308
+ git clone https://github.com/merajhasan88/necessary_reactnative.git
309
+ cd necessary_reactnative
310
+ ```
311
+
312
+ 2. Start a sample backend on your computer. It answers on `127.0.0.1` only. In Python 3.10 or later:
313
+
314
+ ```sh
315
+ pip install --require-hashes -r backend/python/requirements.txt
316
+ python3 backend/python/server.py --port 8787 --schema contract/report-1/android-8-ios-5/report.schema.json
317
+ ```
318
+
319
+ The TypeScript and C# samples run the same way; their commands are in
320
+ [`backend/README.md`](https://github.com/merajhasan88/necessary_reactnative/blob/main/backend/README.md).
321
+ 3. Install the example's packages (Node.js 22 or later), from `example/`:
322
+
323
+ ```sh
324
+ cd example
325
+ npm ci
326
+ ```
327
+
328
+ 4. Name the backend in `example/src/backends.json`, then run the app:
329
+ - **On an Android emulator**, a debug build reaches your computer at `10.0.2.2`:
330
+ `{"Sample": "http://10.0.2.2:8787"}`, then `npx react-native run-android`.
331
+ - **On an Android phone** connected by USB with USB debugging on, forward the port first:
332
+ `adb reverse tcp:8787 tcp:8787`, name `{"Sample": "http://127.0.0.1:8787"}`, then
333
+ `npx react-native run-android`.
334
+ - **A release build**, on Android or iOS, talks only HTTPS: the SDK refuses an `http://` address in
335
+ a release build. Put the sample behind an HTTPS proxy on a name the phone can resolve, with a
336
+ certificate the phone trusts, and name that address:
337
+ `{"Sample": "https://backend.example.com"}`, then
338
+ `npx react-native run-android --mode release`.
339
+ - **On an iPhone**, run `pod install` in `example/ios`, open `example/ios/NecessaryExample.xcworkspace`
340
+ in Xcode on a Mac, choose your team under Signing & Capabilities, and run with the iPhone
341
+ connected. An iPhone reaches your computer over your network, so name the address of the HTTPS
342
+ proxy.
343
+
344
+ To offer several backends, name each: `{"Python": "https://…", "TypeScript": "https://…"}`. A debug
345
+ build measures itself as one — `debuggable` in `app_identity` — so judge the measurements on a release
346
+ build.
347
+
348
+ ### Test it on a phone, step by step
349
+
350
+ With several backends, tap one at the top and run the steps once for each. Every result line carries a
351
+ ✓ when the backend did what it should, a red ! when it did not.
352
+
353
+ 1. **Installation → Start.** Expect: `Installation …, proved; step-up on.` On a phone without a screen
354
+ lock, step-up is off: `step-up off here (…)`, and step 7 cannot pass.
355
+ 2. **Measurements:** switch on **All of them**, tap **Ask for the location permission** and allow it.
356
+ Once is enough; the switches reset when the app restarts.
357
+ 3. **Scan.** Expect: `The backend answered next: allow, report …` — the sample backends answer
358
+ `scan_again` when a measurement could not be read, and `review` when its figures contradict each
359
+ other. Under **Hooks, gadgets and memory changes** every row reads `measured`, none `not asked`. Open
360
+ any row for the fields the phone sent.
361
+ 4. **Account → Sign in.** Expect: `An account session for this installation.`
362
+ 5. **Requests → Protected request.** Expect: ✓ `{"done":"/v1/demo/echo"}`.
363
+ 6. **Sensitive, without approval.** Expect: ✓ `Refused, as it should be: 403 needs_approval`.
364
+ 7. **Sensitive, with step-up**, then unlock with the phone's passcode. Expect: ✓
365
+ `{"done":"/v1/demo/sensitive"}`.
366
+ 8. Type 6 digits in **App PIN**, then **Set the app PIN**. Expect: ✓ `Set, with the account password.`
367
+ 9. **Sensitive, with the app PIN.** Expect: ✓ `{"done":"/v1/demo/sensitive"}`.
368
+ 10. **Requests a backend must refuse:** tap each of the four. Expect a ✓ on each: `Refused, as it should
369
+ be: 401 proof_reused` for the request sent twice, `401 proof_not_accepted` for the other three.
370
+
371
+ If the phone's screen lock is removed and set again, the step-up key dies: step 7 then shows `The phone
372
+ said STEPUP_KEY_INVALIDATED` (on Android 9, `STEPUP_KEY_UNAVAILABLE`). Tap **Re-enrol the step-up key**
373
+ and unlock with the passcode — expect ✓ `{"stepup_key_registered":true}` — and step 7 passes again. The
374
+ new key is that account's alone. If the lock was off at step 1 and set afterwards, **Start again** says
375
+ `step-up off here (STEPUP_REENROL_PENDING)`: the backend took the installation without a step-up key, so
376
+ re-enrol the same way after step 4.
377
+
378
+ The app PIN belongs to the account, not to the phone. Phones signed in with the same handle share it: a
379
+ PIN set on one unlocks requests on the other, and before any is set, step 9 answers `app_pin_not_set`.
380
+ Sign in with another handle for an account of its own.
381
+
382
+ ### A session copied to another phone
383
+
384
+ A session token copied off one phone must be useless on another. You need two phones running the
385
+ example against the same backend.
386
+
387
+ 1. **Phone A:** steps 1 and 4 above, then **Requests a backend must refuse → Share this phone's
388
+ session**, and send the text to yourself.
389
+ 2. Get the text to phone B, for example in a note or a message to yourself. It holds phone A's session
390
+ and installation ID; the sample's sessions last ten minutes.
391
+ 3. **Phone B:** step 1 above, on the same backend.
392
+ 4. **Phone B:** paste the text into **A session from another phone**, then tap **Use it here**. Expect: ✓
393
+ `Refused, as it should be: 401 invalid_installation_signature`. Phone B's key cannot sign for phone A's
394
+ installation, even when the request names it.
395
+
396
+ If step 4 reads `The session is no longer valid: sign in again`, phone A's session has expired: sign in
397
+ again on phone A and share it again.
398
+
399
+ ### Measurement by measurement
400
+
401
+ What each measurement is and every field it carries:
402
+ [`doc/measurements.md`](https://github.com/merajhasan88/necessary_reactnative/blob/main/doc/measurements.md).
403
+ On a phone of your own you can watch these change, one at a time: make the change, tap **Scan**, open the
404
+ measurement's row, and change it back afterwards.
405
+
406
+ | Measurement | Change | The row then shows |
407
+ |---|---|---|
408
+ | `developer_settings` (Android) | Settings → About phone → tap Build number seven times; then Developer options → USB debugging | `developer_options_enabled`, then `adb_enabled`, true |
409
+ | `network` | turn on a VPN app; or set a manual proxy on your Wi-Fi network | `vpn_active` true, the tunnel's interface in `interfaces` (Android) or `scoped_interfaces` (iOS); `http_proxy_configured` true |
410
+ | `local_addresses` | switch between Wi-Fi and mobile data | the phone's own addresses, per interface |
411
+ | `screen_capture` | start the screen recorder (Android 15 and later; on iOS from Control Center); connect an external display | `recording` true; `external_displays` 1 |
412
+ | `location` | tap **Ask for the location permission** and allow it | the `permission`, the coordinates and their `age_seconds` |
413
+ | `mock_location` | Android: Developer options → Select mock location app, and run a mock-location app; iOS: simulate a location from Xcode while the app runs from it | `mock` true |
414
+ | `accessibility` (Android) | Settings → Accessibility → turn on a service such as Select to Speak; or choose another keyboard | the service in `services`; the keyboard in `keyboard` |
415
+ | `notification_access` (Android) | Settings → Notification access (under Special app access) → allow an app | the app in `listeners` |
416
+ | `other_access` (Android) | install AnyDesk, TeamViewer QuickSupport, RustDesk or AirDroid; or make an app a device administrator | the app in `remote_access_apps`; in `device_admins` |
417
+ | `app_identity` | install a build of the example made with other code | other hashes; a debug build reads `debuggable` true |
418
+ | `debug_state` (Android) | in Android Studio, Run → Attach Debugger to Android Process, on a debug build | `debugger_connected` true |
419
+ | `code_signing` (iOS) | run a development build from Xcode | `get_task_allow` true |
420
+
421
+ The rest — root and jailbreak traces, Frida and other hooking tools, code changed in memory, an emulator
422
+ or the Simulator — needs a device you are allowed to change: never a phone you depend on. On an Android
423
+ emulator `emulator.suspected` is true, and a `userdebug` image shows a debuggable, test-keys build in
424
+ `system_properties` and `root_files`; on the iOS Simulator `simulator.is_simulator` is true. Frida on a
425
+ test device shows in `runtime_maps`, `frida_ports`, `instrumentation_threads`, `exec_mappings` and
426
+ `code_integrity` on Android, and in `dyld_images` and `code_integrity` on iOS.
427
+
428
+ ## Licence
429
+
430
+ Copyright 2026 Meraj Hasan. Licensed under the Apache License, Version 2.0 (`LICENSE`, `NOTICE`).
@@ -0,0 +1,83 @@
1
+ // The SDK's native side on Android, built inside the app that links it: the app's root project
2
+ // provides the Android and React Native Gradle plugins, and the plugin of the native modules'
3
+ // language, and the versions below where it sets none.
4
+
5
+ def extOrDefault(String name, Object fallback) {
6
+ return rootProject.ext.has(name) ? rootProject.ext.get(name) : fallback
7
+ }
8
+
9
+ apply plugin: "com.android.library"
10
+ apply plugin: "kotlin-android"
11
+ apply plugin: "com.facebook.react"
12
+
13
+ android {
14
+ namespace "com.necessarysdk.reactnative"
15
+ compileSdkVersion extOrDefault("compileSdkVersion", 36)
16
+ if (rootProject.ext.has("ndkVersion")) {
17
+ ndkVersion rootProject.ext.get("ndkVersion")
18
+ }
19
+
20
+ defaultConfig {
21
+ minSdkVersion Math.max(24, extOrDefault("minSdkVersion", 24) as int)
22
+ targetSdkVersion extOrDefault("targetSdkVersion", 36)
23
+ externalNativeBuild {
24
+ cmake {
25
+ cppFlags "-std=c++17"
26
+ }
27
+ }
28
+ }
29
+
30
+ externalNativeBuild {
31
+ cmake {
32
+ path "src/main/cpp/CMakeLists.txt"
33
+ }
34
+ }
35
+
36
+ compileOptions {
37
+ sourceCompatibility JavaVersion.VERSION_17
38
+ targetCompatibility JavaVersion.VERSION_17
39
+ }
40
+
41
+ buildTypes {
42
+ release {
43
+ minifyEnabled false
44
+ }
45
+ }
46
+
47
+ testOptions {
48
+ unitTests {
49
+ includeAndroidResources = true
50
+ all {
51
+ outputs.upToDateWhen { false }
52
+ testLogging {
53
+ events "passed", "skipped", "failed", "standardOut", "standardError"
54
+ showStandardStreams = true
55
+ }
56
+ }
57
+ }
58
+ }
59
+ }
60
+
61
+ kotlin {
62
+ compilerOptions {
63
+ jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17
64
+ }
65
+ }
66
+
67
+ repositories {
68
+ google()
69
+ mavenCentral()
70
+ }
71
+
72
+ dependencies {
73
+ implementation "com.facebook.react:react-android"
74
+ // The collectors' failure handling, the step-up key's verdicts and the key lookups, tested on
75
+ // the JVM.
76
+ testImplementation "junit:junit:4.13.2"
77
+ }
78
+
79
+ react {
80
+ jsRootDir = file("../src/")
81
+ libraryName = "NecessarySpec"
82
+ codegenJavaPackageName = "com.necessarysdk.reactnative"
83
+ }
@@ -0,0 +1,41 @@
1
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android">
2
+ <!-- What the SDK's own features need; all normal permissions, granted at
3
+ install. The location permissions are the app's to declare and ask for:
4
+ the SDK measures the location only where the app has them. -->
5
+ <uses-permission android:name="android.permission.INTERNET" />
6
+ <!-- The network probe asks the connectivity service whether a VPN or a proxy
7
+ is in use. -->
8
+ <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
9
+ <!-- Step-up prompts (BiometricPrompt, device credential or biometric). -->
10
+ <uses-permission android:name="android.permission.USE_BIOMETRIC" />
11
+ <!-- screen_capture on Android 15 and later: whether this app's window is being
12
+ recorded. -->
13
+ <uses-permission android:name="android.permission.DETECT_SCREEN_RECORDING" />
14
+ <!-- Screen protection: other apps' windows are not drawn over the protected
15
+ window (Android 12 and later). -->
16
+ <uses-permission android:name="android.permission.HIDE_OVERLAY_WINDOWS" />
17
+ <queries>
18
+ <!-- The apps that can hold Accessibility, a keyboard, notification access
19
+ or device administration, so the accessibility, notification_access
20
+ and other_access measurements can read who installed each. -->
21
+ <intent>
22
+ <action android:name="android.accessibilityservice.AccessibilityService"/>
23
+ </intent>
24
+ <intent>
25
+ <action android:name="android.view.InputMethod"/>
26
+ </intent>
27
+ <intent>
28
+ <action android:name="android.service.notification.NotificationListenerService"/>
29
+ </intent>
30
+ <intent>
31
+ <action android:name="android.app.action.DEVICE_ADMIN_ENABLED"/>
32
+ </intent>
33
+ <!-- Remote-control apps scammers have victims install; the same list as
34
+ IntegrityProbeManager.remoteAccessPackages. -->
35
+ <package android:name="com.anydesk.anydeskandroid"/>
36
+ <package android:name="com.teamviewer.quicksupport.market"/>
37
+ <!-- RustDesk, by its own package name. -->
38
+ <package android:name="com.carriez.flutter_hbb"/>
39
+ <package android:name="com.sand.airdroid"/>
40
+ </queries>
41
+ </manifest>
@@ -0,0 +1,7 @@
1
+ cmake_minimum_required(VERSION 3.22.1)
2
+ project(codeintegrity)
3
+
4
+ add_library(codeintegrity SHARED code_integrity.cpp)
5
+
6
+ find_library(log-lib log)
7
+ target_link_libraries(codeintegrity ${log-lib})