@umutcansu/react-native-pinvault 2.3.2 → 2.4.1
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/CHANGELOG.md +69 -0
- package/README.md +116 -36
- package/RNPinVault.podspec +1 -1
- package/android/build.gradle +1 -1
- package/android/consumer-rules.pro +3 -0
- package/android/src/main/java/io/github/umutcansu/pinvault/reactnative/ConfigParser.kt +150 -29
- package/android/src/main/java/io/github/umutcansu/pinvault/reactnative/NativeSecurity.kt +134 -5
- package/android/src/main/java/io/github/umutcansu/pinvault/reactnative/PinVaultModule.kt +12 -3
- package/android/src/main/java/io/github/umutcansu/pinvault/reactnative/PinVaultNetworking.kt +116 -16
- package/ios/Core/ConfigParser.swift +216 -32
- package/ios/Core/NativeSecurity.swift +127 -6
- package/ios/PinVaultBridge.swift +24 -7
- package/ios/PinVaultReactNetworking.swift +16 -0
- package/ios/RNPinVault.mm +6 -0
- package/ios/RNPinVaultWebSocketPinning.h +21 -0
- package/ios/RNPinVaultWebSocketPinning.mm +170 -0
- package/lib/module/index.js +21 -2
- package/lib/module/index.js.map +1 -1
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/types.d.ts +14 -1
- package/lib/typescript/src/types.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/index.ts +15 -2
- package/src/types.ts +14 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,74 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2.4.1 — 2026-10-10
|
|
4
|
+
|
|
5
|
+
### Security
|
|
6
|
+
|
|
7
|
+
- **React Native's WebSocket is pinned on iOS.** `wss://` went through
|
|
8
|
+
SocketRocket on CFStream with the platform's CA check only, so a socket.io /
|
|
9
|
+
graphql-ws connection could be intercepted by anyone holding a certificate
|
|
10
|
+
the device trusts (a corporate proxy, a user-installed root), and `start`
|
|
11
|
+
still reported RN's networking as pinned. Every SocketRocket socket to a
|
|
12
|
+
`wss` / `https` URL now gets a security policy that asks PinVault (the
|
|
13
|
+
library's new `evaluateServerTrust(_:host:port:)`) before the upgrade
|
|
14
|
+
request is written; subprotocols are kept. `requirePinnedReactNativeNetworking`
|
|
15
|
+
covers it (`E_NETWORKING_NOT_PINNED` when the hook is not in place), and
|
|
16
|
+
Info.plist `PinVaultPinReactNativeNetworking` = NO turns it off with the
|
|
17
|
+
https handler. A WebSocket to a host that requires a client certificate is
|
|
18
|
+
not supported on iOS yet (the handshake fails).
|
|
19
|
+
- **Android: every WebSocket goes through PinVault's hook of its own.** RN's
|
|
20
|
+
`WebSocketModule` has a separate `setCustomClientBuilder`; up to 0.81 it
|
|
21
|
+
builds each socket's client from a bare `OkHttpClient.Builder()`, outside
|
|
22
|
+
`OkHttpClientProvider`, so there the socket kept the system's trust while
|
|
23
|
+
`start` reported RN's networking as pinned. The plugin now installs that hook
|
|
24
|
+
too and checks it with the other two (`HookStatus.webSocketBuilderIsPinVault`),
|
|
25
|
+
so a library replacing it is also caught on 0.87.
|
|
26
|
+
|
|
27
|
+
- **A release build needs the anchors in the native security file:** a declared
|
|
28
|
+
block must carry `bootstrapPins` (or `allowUnpinnedConfigApi`) and
|
|
29
|
+
`signaturePublicKeys` (or `allowUnsigned`). A file with a bare
|
|
30
|
+
`{"id": …}` satisfied the release rule while the bundle supplied the pins
|
|
31
|
+
and keys; it now rejects `start` with `E_INVALID_CONFIG` naming the field.
|
|
32
|
+
- **`clientCertHosts` from JS only where the file names them** (release
|
|
33
|
+
builds): an OTA bundle could otherwise present the device's mTLS identity
|
|
34
|
+
to another pinned host. `tokenHosts` from JS is still taken: it only
|
|
35
|
+
narrows the library's default.
|
|
36
|
+
|
|
37
|
+
### Changed
|
|
38
|
+
|
|
39
|
+
- **React Native 0.81+** (was 0.87+): built and run on 0.81.6 and 0.87.1,
|
|
40
|
+
Android and iOS (start with `requirePinnedReactNativeNetworking`, RN `fetch`
|
|
41
|
+
and `WebSocket` to a pinned and an unpinned host).
|
|
42
|
+
|
|
43
|
+
## 2.4.0 — 2026-10-09
|
|
44
|
+
|
|
45
|
+
### Breaking
|
|
46
|
+
|
|
47
|
+
- **Release builds need the native security file** when the config has Config
|
|
48
|
+
APIs or static pins (Android manifest meta-data
|
|
49
|
+
`io.github.umutcansu.pinvault.ALLOW_NO_NATIVE_SECURITY_FILE` / iOS Info.plist
|
|
50
|
+
`PinVaultAllowNoNativeSecurityFile` = YES opts out).
|
|
51
|
+
- **`requirePinnedReactNativeNetworking` is on by default in release builds.**
|
|
52
|
+
- **`expiredConfigGrace` is at most 7 days in release builds.**
|
|
53
|
+
- **Vault file keys** must match `[A-Za-z0-9._-]{1,64}` (not only dots).
|
|
54
|
+
|
|
55
|
+
### Security
|
|
56
|
+
|
|
57
|
+
- The native security file may also fix a block's `url`, `tokenHosts`,
|
|
58
|
+
`clientCertHosts` and `attestation`, carry a `require` section (protections
|
|
59
|
+
JS cannot switch off) and `vaultFiles` (signature key, encryption, lock).
|
|
60
|
+
- `start` answers `nativeSecurityApplied`.
|
|
61
|
+
- `proofOfPossession` on a Config API (JS or the native security file, where
|
|
62
|
+
it cannot be switched off): the native clients add a `PinVault-Proof` signed
|
|
63
|
+
by the device key to every request that carries the token (ATTESTATION.md §5.1).
|
|
64
|
+
- Android: React Native's long-lived clients (WebSocket, images) run the
|
|
65
|
+
current start's network interceptors (the per-request pin check) and their
|
|
66
|
+
pooled connections are closed on every start and reset; a hook installed only
|
|
67
|
+
by `start()` is reported as not pinned; a hook replaced after start refuses
|
|
68
|
+
https while `requirePinnedReactNativeNetworking` is on.
|
|
69
|
+
- The JS guard of a start the native side refuses is put back.
|
|
70
|
+
- `LOAD_FILE` is a guarded operation (`loadFile`).
|
|
71
|
+
|
|
3
72
|
## 2.3.2 — 2026-10-08 — first release
|
|
4
73
|
|
|
5
74
|
First published version (2.3.1 was never published). Needs PinVault 2.3.2 on both platforms.
|
package/README.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# PinVault for React Native
|
|
2
2
|
|
|
3
|
+
[](https://www.npmjs.com/package/@umutcansu/react-native-pinvault)
|
|
4
|
+
|
|
3
5
|
`@umutcansu/react-native-pinvault` — dynamic certificate pinning with a signed,
|
|
4
6
|
remotely updated pin config, mTLS enrollment with a hardware identity key,
|
|
5
7
|
signed and encrypted vault files, and device attestation, for React Native
|
|
@@ -11,23 +13,34 @@ It is a thin bridge over the two native libraries — Android
|
|
|
11
13
|
Secure Enclave), signature checks and vault decryption all stay native: no
|
|
12
14
|
crypto runs in JavaScript, and JavaScript never sees TLS. Names are the native
|
|
13
15
|
ones (`InitResult`, `ClientCertEnrollmentResult`, `VaultFileResult`, …), so the
|
|
14
|
-
[library documentation](https://github.com/umutcansu/PinVault/blob/main/
|
|
16
|
+
[library documentation](https://github.com/umutcansu/PinVault/blob/main/GUIDE.md) applies as it is.
|
|
15
17
|
|
|
16
|
-
- React Native **0.
|
|
18
|
+
- React Native **0.81+** (built and tested on 0.81.6 and 0.87.1), New
|
|
19
|
+
Architecture only (TurboModule, codegen), Hermes.
|
|
17
20
|
- Android minSdk 24; iOS 16+.
|
|
18
21
|
- No runtime dependencies: `react` and `react-native` are peers.
|
|
19
22
|
|
|
20
23
|
## Install
|
|
21
24
|
|
|
25
|
+
From npm ([`@umutcansu/react-native-pinvault`](https://www.npmjs.com/package/@umutcansu/react-native-pinvault)):
|
|
26
|
+
|
|
22
27
|
```bash
|
|
23
28
|
npm install @umutcansu/react-native-pinvault
|
|
24
29
|
cd ios && pod install
|
|
25
30
|
```
|
|
26
31
|
|
|
32
|
+
The native libraries come with it, at the package's own version: npm 2.4.0
|
|
33
|
+
uses `io.github.umutcansu:pinvault:2.4.0` from Maven Central and the Swift
|
|
34
|
+
package at tag `v2.4.0`. The New Architecture must stay on (React Native's
|
|
35
|
+
default since 0.76).
|
|
36
|
+
|
|
27
37
|
**iOS.** The pod depends on the PinVault Swift package through React Native's
|
|
28
38
|
`spm_dependency` (git URL `https://github.com/umutcansu/PinVault.git`, exact
|
|
29
|
-
version = this package's version, product `PinVault`).
|
|
30
|
-
|
|
39
|
+
version = this package's version, product `PinVault`). On React Native 0.81
|
|
40
|
+
with Xcode 26 or later, React Native's own `fmt` pod does not compile from
|
|
41
|
+
source; install the prebuilt dependencies instead
|
|
42
|
+
(`RCT_USE_RN_DEP=1 RCT_USE_PREBUILT_RNCORE=1 pod install`, 0.87's default).
|
|
43
|
+
Info.plist, as for the native library:
|
|
31
44
|
|
|
32
45
|
| Key | Why |
|
|
33
46
|
|---|---|
|
|
@@ -93,7 +106,7 @@ names), enum values are the Kotlin constant names (`'TOKEN_MTLS'`,
|
|
|
93
106
|
```ts
|
|
94
107
|
{
|
|
95
108
|
configApis: [{ id, url, bootstrapPins, signaturePublicKeys, requiredSignatures, recoveryPublicKeys,
|
|
96
|
-
serverScope, clientCaPins, clientCertHosts, renewalUrl, attestation, tokenHosts, … }],
|
|
109
|
+
serverScope, clientCaPins, clientCertHosts, renewalUrl, attestation, tokenHosts, proofOfPossession, … }],
|
|
97
110
|
vaultFiles: [{ key, endpoint, configApi, accessPolicy, encryption, userAuth, storage,
|
|
98
111
|
maxOfflineAge: { amount: 7, unit: 'DAYS' }, … }],
|
|
99
112
|
requireCaTrust, requireUnlockedDevice, requireHardwareBackedKeys, expectedSignerSha256,
|
|
@@ -167,9 +180,25 @@ signature. Ship them natively as well, in `pinvault_security.json`:
|
|
|
167
180
|
"signaturePublicKeys": ["…", "…", "…"], "requiredSignatures": 2,
|
|
168
181
|
"recoveryPublicKeys": ["…"], "requiredRecoverySignatures": 1,
|
|
169
182
|
"serverScope": "default-tls", "clientCaPins": ["…"],
|
|
183
|
+
"url": "https://config.example.com:8081/", "attestation": true,
|
|
184
|
+
"tokenHosts": ["api.example.com"], "proofOfPossession": true, "clientCertHosts": ["api.example.com:443"],
|
|
185
|
+
"enrollmentUrl": "https://enroll.example.com/", "renewalUrl": "https://renew.example.com/",
|
|
170
186
|
"allowUnsigned": false, "allowUnpinnedConfigApi": false, "allowServerGeneratedKey": false
|
|
171
187
|
}],
|
|
172
|
-
"staticPins": { "pins": [ … ], "version": 1 }
|
|
188
|
+
"staticPins": { "pins": [ … ], "version": 1 },
|
|
189
|
+
"require": {
|
|
190
|
+
"requireUnlockedDevice": true, "requireHardwareBackedKeys": true,
|
|
191
|
+
"managedTrustRoots": true, "wipeVaultFilesOnRevocation": true,
|
|
192
|
+
"requireCaTrust": ["api.example.com"],
|
|
193
|
+
"expectedSignerSha256": ["…"],
|
|
194
|
+
"expectedBundleIds": ["com.example.app"], "expectedTeamIds": ["ABCDE12345"],
|
|
195
|
+
"expiredConfigGraceSeconds": 0, "vaultFileMaxOfflineAgeSeconds": 604800,
|
|
196
|
+
"userAuthStrength": "BIOMETRIC_CURRENT_SET"
|
|
197
|
+
},
|
|
198
|
+
"vaultFiles": [
|
|
199
|
+
{ "key": "statement", "signaturePublicKey": "…", "encryption": "USER_AUTH", "userAuth": "REQUIRED",
|
|
200
|
+
"maxOfflineAgeSeconds": 86400 }
|
|
201
|
+
]
|
|
173
202
|
}
|
|
174
203
|
```
|
|
175
204
|
|
|
@@ -182,12 +211,40 @@ parsed as strictly; a broken file rejects `start` with `E_INVALID_CONFIG`
|
|
|
182
211
|
- a field the file declares is the value: JS may leave it out (the native value
|
|
183
212
|
applies) or repeat it (lists in any order); a different value rejects `start`
|
|
184
213
|
with `E_INVALID_CONFIG` naming the field;
|
|
214
|
+
- in a release build a declared block must carry its anchors: `bootstrapPins`
|
|
215
|
+
(or `allowUnpinnedConfigApi`) and `signaturePublicKeys` (or `allowUnsigned`);
|
|
216
|
+
a block that only names itself rejects `start` — otherwise the bundle would
|
|
217
|
+
supply the pins and keys;
|
|
185
218
|
- `allowUnsigned`, `allowUnpinnedConfigApi` and `allowServerGeneratedKey` from
|
|
186
|
-
JS are refused unless the file allows them for that block
|
|
219
|
+
JS are refused unless the file allows them for that block;
|
|
220
|
+
- a block's `url`, `enrollmentUrl`, `renewalUrl`, `tokenHosts` and
|
|
221
|
+
`clientCertHosts` are fixed where the file gives them (where the block talks to, and who gets its token and identity),
|
|
222
|
+
and `attestation: true` / `proofOfPossession: true` there cannot be turned off from JS;
|
|
223
|
+
in a release build JS may give `clientCertHosts` only where the file names
|
|
224
|
+
them (the bundle cannot hand the device's identity to another host;
|
|
225
|
+
`tokenHosts` from JS only narrows the library's default);
|
|
226
|
+
- `require` only tightens: a protection it turns on stays on whatever JS says,
|
|
227
|
+
its `requireCaTrust` hosts are added to JS's, the expected signer / bundle /
|
|
228
|
+
team ids and iOS `userAuthStrength` are fixed, and `expiredConfigGraceSeconds`
|
|
229
|
+
/ `vaultFileMaxOfflineAgeSeconds` are the most JS may ask for (the value when
|
|
230
|
+
JS asks for none)
|
|
231
|
+
(Android reads `expectedSignerSha256`, iOS the bundle and team ids; one file
|
|
232
|
+
can serve both);
|
|
233
|
+
- `vaultFiles` fixes those files' signature key, encryption and screen lock, and
|
|
234
|
+
`maxOfflineAgeSeconds` is the longest offline lifetime JS may give them;
|
|
235
|
+
with the section present, a JS vault file it does not name is refused.
|
|
236
|
+
|
|
237
|
+
`start` answers `nativeSecurityApplied: true` when the file was applied.
|
|
187
238
|
|
|
188
239
|
Without the file, **release builds** (Android: the app is not
|
|
189
|
-
`android:debuggable`; iOS: compiled without `DEBUG`) refuse
|
|
190
|
-
|
|
240
|
+
`android:debuggable`; iOS: compiled without `DEBUG`) refuse to start a config
|
|
241
|
+
with Config APIs or static pins: its trust anchors would come from JS alone.
|
|
242
|
+
An app that accepts that says so natively — Android:
|
|
243
|
+
`<meta-data android:name="io.github.umutcansu.pinvault.ALLOW_NO_NATIVE_SECURITY_FILE" android:value="true"/>`
|
|
244
|
+
in the manifest's `<application>`; iOS: `PinVaultAllowNoNativeSecurityFile` =
|
|
245
|
+
YES in Info.plist — and the three relaxations stay refused from JS then. Debug
|
|
246
|
+
builds take everything as before. Whatever the file says, a release build
|
|
247
|
+
takes an `expiredConfigGrace` of at most 7 days. The sample app writes
|
|
191
248
|
the file for its release builds from `sample-host.properties`
|
|
192
249
|
(`scripts/gen-host-config.js --native-out`).
|
|
193
250
|
|
|
@@ -204,8 +261,8 @@ signer check of attestation (`expectedSignerSha256`, `expectedTeamIds`) reports.
|
|
|
204
261
|
back) on either platform. Request bodies ≤ 10 MiB, responses ≤ 10 MiB by
|
|
205
262
|
default (`maxResponseBytes`, at most 50 MiB) — enforced while the body is
|
|
206
263
|
read on both platforms (a declared `Content-Length` over it is refused first).
|
|
207
|
-
- **React Native's own `fetch`, `XMLHttpRequest
|
|
208
|
-
|
|
264
|
+
- **React Native's own `fetch`, `XMLHttpRequest`, `<Image>` and `WebSocket`
|
|
265
|
+
are pinned on both platforms.** The rules are the same on both:
|
|
209
266
|
- **Before `start()`** — and after a start that failed — every `https`
|
|
210
267
|
request through RN's networking fails (`PinVault has not started`): fail
|
|
211
268
|
closed, as the native `getClient()` / `session()` before `init`. Plain
|
|
@@ -214,10 +271,12 @@ signer check of attestation (`expectedSignerSha256`, `expectedTeamIds`) reports.
|
|
|
214
271
|
- Hosts without a pin entry are refused like everywhere else in PinVault.
|
|
215
272
|
- No `https` → `http` redirects; no disk HTTP cache and no cookie jar (Android:
|
|
216
273
|
`android.keepReactNativeHttpCache` / `keepReactNativeCookies` keep RN's own).
|
|
217
|
-
- `requirePinnedReactNativeNetworking
|
|
218
|
-
`E_NETWORKING_NOT_PINNED` when RN's networking does
|
|
219
|
-
(below);
|
|
220
|
-
|
|
274
|
+
- `requirePinnedReactNativeNetworking` (on by default in release builds)
|
|
275
|
+
makes `start` fail with `E_NETWORKING_NOT_PINNED` when RN's networking does
|
|
276
|
+
not go through PinVault (below); on Android a hook replaced after start
|
|
277
|
+
then refuses RN's https too (checked every few seconds). With it off a
|
|
278
|
+
warning is logged.
|
|
279
|
+
- **Android** (three hooks, read from RN 0.81 and 0.87's sources), installed by the
|
|
221
280
|
plugin's content provider before `Application.onCreate`, so every client RN
|
|
222
281
|
builds is covered:
|
|
223
282
|
`NetworkingModule.setCustomClientBuilder` is called for every fetch / XHR
|
|
@@ -227,9 +286,14 @@ signer check of attestation (`expectedSignerSha256`, `expectedTeamIds`) reports.
|
|
|
227
286
|
covers the clients RN builds once — the networking base client, the
|
|
228
287
|
WebSocket / dev-support singleton and Fresco's image client: their TLS goes
|
|
229
288
|
through a forwarding socket factory to the current pinned one.
|
|
230
|
-
|
|
289
|
+
`WebSocketModule.setCustomClientBuilder` is called for every `WebSocket`: up
|
|
290
|
+
to 0.81 RN builds each socket's client from a bare `OkHttpClient.Builder()`,
|
|
291
|
+
outside `OkHttpClientProvider`, so without this hook a socket would get the
|
|
292
|
+
system's trust; from 0.87 it derives from the provider's client. Either way
|
|
293
|
+
the socket gets the same forwarding factory.
|
|
294
|
+
Another library that calls any of these setters after PinVault (a network inspector,
|
|
231
295
|
a crash reporter, another pinning package) replaces the hook: `start()` and
|
|
232
|
-
every `PinVault.fetch` check
|
|
296
|
+
every `PinVault.fetch` check all three (`PinVaultNetworking.status()`) and log a
|
|
233
297
|
warning; `requirePinnedReactNativeNetworking` turns it into a failed start.
|
|
234
298
|
Opt out natively by removing the provider in the app's manifest —
|
|
235
299
|
`android.pinGlobalNetworking: false` alone is refused while the hooks are in
|
|
@@ -254,13 +318,21 @@ signer check of attestation (`expectedSignerSha256`, `expectedTeamIds`) reports.
|
|
|
254
318
|
body in chunks and the completion after the body has been read (progress
|
|
255
319
|
events arrive at the end); cancelling a request cancels it. `start` checks
|
|
256
320
|
which handler RCTNetworking picks for an `https` request.
|
|
321
|
+
**WebSocket:** RN's `RCTWebSocketModule` uses SocketRocket on CFStream, not
|
|
322
|
+
URLSession. Every SocketRocket initializer ends in
|
|
323
|
+
`-initWithURLRequest:protocols:securityPolicy:`; the plugin replaces that
|
|
324
|
+
method at load (`RNPinVaultWebSocketPinning.mm`), so a socket to a `wss` /
|
|
325
|
+
`https` URL always gets a security policy that asks
|
|
326
|
+
`PinVault.shared.evaluateServerTrust(_:host:port:)` — the same pins, host
|
|
327
|
+
check and fail-closed rule as the https handler — whatever policy its creator
|
|
328
|
+
passed. SocketRocket judges the trust before it writes the upgrade request,
|
|
329
|
+
so a refused chain sends no header or cookie. Subprotocols are kept
|
|
330
|
+
(`RCTSetCustomSRWebSocketProvider` is not used: its block never sees them).
|
|
331
|
+
`start` checks the hook is still the initializer's implementation. A
|
|
332
|
+
WebSocket to a host that requires a client certificate fails on iOS (no
|
|
333
|
+
identity is presented); use it over TLS, or Android.
|
|
257
334
|
Opt out natively with the Info.plist key `PinVaultPinReactNativeNetworking`
|
|
258
|
-
= NO
|
|
259
|
-
uses SocketRocket on CFStream, not URLSession, so it cannot take PinVault's
|
|
260
|
-
session or delegate; its hook (`RCTSetCustomSRWebSocketProvider` with an
|
|
261
|
-
`SRSecurityPolicy` whose `evaluateServerTrust:forDomain:` decides) would need
|
|
262
|
-
a public trust-evaluation API from the PinVault library, which it does not
|
|
263
|
-
have. Keep secrets off `wss://` on iOS, or send them with `PinVault.fetch`.
|
|
335
|
+
= NO (both the https handler and the WebSocket hook).
|
|
264
336
|
Keep App Transport Security on (no `NSAllowsArbitraryLoads`).
|
|
265
337
|
|
|
266
338
|
## Enrollment and vault tokens
|
|
@@ -277,7 +349,10 @@ value until the garbage collector drops it, and if it went through React
|
|
|
277
349
|
state, a form field or a store, it lives there too. Pass it straight from where
|
|
278
350
|
it came from, clear the field, and never persist or log it. For an attestation
|
|
279
351
|
`PinVault-Token`, prefer `tokenHosts` + `PinVault.fetch` (the native client adds
|
|
280
|
-
it) over `fetchAttestationToken`, which hands the token to JS.
|
|
352
|
+
it) over `fetchAttestationToken`, which hands the token to JS. With
|
|
353
|
+
`proofOfPossession` the native client also adds a `PinVault-Proof` signed by the
|
|
354
|
+
device key on every such request, and a backend that requires it refuses the
|
|
355
|
+
token alone — a token in JS is then of no use without the native client. Forget vault
|
|
281
356
|
tokens on a revocation:
|
|
282
357
|
|
|
283
358
|
```ts
|
|
@@ -307,10 +382,10 @@ environmentGuard: async (operation) => !(await myRaspSaysCompromised()),
|
|
|
307
382
|
environmentGuardTimeoutMs: 5000,
|
|
308
383
|
```
|
|
309
384
|
|
|
310
|
-
Asked before `INIT`, `ENROLL`, `FETCH_FILE` and `
|
|
311
|
-
asks before it calls the library (`start`: `INIT` and `ENROLL`;
|
|
312
|
-
calls: `ENROLL`; `fetchFile` / `syncAllFiles`: `FETCH_FILE`;
|
|
313
|
-
`UNLOCK_FILE`), so the wait for JS holds no thread; an operation the library
|
|
385
|
+
Asked before `INIT`, `ENROLL`, `FETCH_FILE`, `UNLOCK_FILE` and `LOAD_FILE`. On iOS
|
|
386
|
+
the bridge asks before it calls the library (`start`: `INIT` and `ENROLL`;
|
|
387
|
+
enrollment calls: `ENROLL`; `fetchFile` / `syncAllFiles`: `FETCH_FILE`;
|
|
388
|
+
`unlockFile`: `UNLOCK_FILE`; `loadFile`: `LOAD_FILE`), so the wait for JS holds no thread; an operation the library
|
|
314
389
|
starts on its own (a background refresh) still waits on its own thread,
|
|
315
390
|
bounded by the timeout. **Fail closed:**
|
|
316
391
|
a timeout (native deadline 100–30 000 ms, default 5000), a thrown error, a
|
|
@@ -322,8 +397,8 @@ as one more signal. The decisive check is server-side attestation
|
|
|
322
397
|
## Security notes (OWASP MASVS)
|
|
323
398
|
|
|
324
399
|
- **NETWORK** — pinned native path only; no unpinned fallback; `https` → `http`
|
|
325
|
-
redirects are not followed; RN's own fetch / XHR / images are pinned on
|
|
326
|
-
platforms
|
|
400
|
+
redirects are not followed; RN's own fetch / XHR / images / WebSocket are pinned on
|
|
401
|
+
both platforms, fail closed before `start`, and a replaced
|
|
327
402
|
hook is detected.
|
|
328
403
|
- **Trust anchors** — release builds take the relaxations only from the native
|
|
329
404
|
security file; with the file, JS cannot change pins, keys or scopes.
|
|
@@ -369,7 +444,9 @@ repository's `:pinvault` (unsigned) into `android/build/pinvault-maven` at every
|
|
|
369
444
|
build and resolves `io.github.umutcansu:pinvault` only from there — a composite
|
|
370
445
|
build is not possible because PinVault builds with AGP 8.7 and React Native 0.87
|
|
371
446
|
with AGP 9. iOS: the Podfile sets `PINVAULT_IOS_PACKAGE_PATH` to the repository,
|
|
372
|
-
and the podspec hands `spm_dependency` that local path instead of the git tag.
|
|
447
|
+
and the podspec hands `spm_dependency` that local path instead of the git tag. A
|
|
448
|
+
local path needs a React Native whose `spm_dependency` takes one (0.87 does,
|
|
449
|
+
0.81 takes git URLs only); apps on 0.81 use the git tag, as every app outside the repository does.
|
|
373
450
|
|
|
374
451
|
## Known limits
|
|
375
452
|
|
|
@@ -378,10 +455,13 @@ and the podspec hands `spm_dependency` that local path instead of the git tag.
|
|
|
378
455
|
`build_auth_list`); the keystore daemon then keeps a dead connection and every
|
|
379
456
|
Keystore operation fails until it is restarted. Turn `requireUnlockedDevice`
|
|
380
457
|
on for release builds / real devices only (the sample does).
|
|
381
|
-
- iOS: RN's
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
458
|
+
- iOS: RN's https answers are delivered after the whole (bounded) body is read,
|
|
459
|
+
and a WebSocket presents no client certificate (above). The WebSocket hook
|
|
460
|
+
expects SocketRocket 0.7's designated initializer; another shape reads as
|
|
461
|
+
not pinned.
|
|
462
|
+
- Android: the networking hooks are detected by reading three private fields
|
|
463
|
+
of React Native (`OkHttpClientProvider.factory`,
|
|
464
|
+
`NetworkingModule.customClientBuilder`, `WebSocketModule.customClientBuilder`;
|
|
465
|
+
the same in 0.81 and 0.87, the consumer R8 rules keep them). A
|
|
386
466
|
React Native version that renames them reads as "could not be read", which
|
|
387
467
|
counts as not pinned.
|
package/RNPinVault.podspec
CHANGED
|
@@ -4,7 +4,7 @@ package = JSON.parse(File.read(File.join(__dir__, "package.json")))
|
|
|
4
4
|
|
|
5
5
|
# PinVault for iOS is a Swift package (Package.swift at the repository root,
|
|
6
6
|
# product "PinVault"). React Native's spm_dependency adds it to the Pods project:
|
|
7
|
-
# - consumers: the git URL at the tag of this package's version (v2.
|
|
7
|
+
# - consumers: the git URL at the tag of this package's version (v2.4.1);
|
|
8
8
|
# - development: PINVAULT_IOS_PACKAGE_PATH = a PinVault checkout (the sample's
|
|
9
9
|
# Podfile sets it to this repository), the counterpart of pinvault.localPath.
|
|
10
10
|
pinvault_url = "https://github.com/umutcansu/PinVault.git"
|
package/android/build.gradle
CHANGED
|
@@ -14,6 +14,9 @@
|
|
|
14
14
|
-keepclassmembers class com.facebook.react.modules.network.NetworkingModule {
|
|
15
15
|
private static com.facebook.react.modules.network.CustomClientBuilder customClientBuilder;
|
|
16
16
|
}
|
|
17
|
+
-keepclassmembers class com.facebook.react.modules.websocket.WebSocketModule {
|
|
18
|
+
private static com.facebook.react.modules.network.CustomClientBuilder customClientBuilder;
|
|
19
|
+
}
|
|
17
20
|
# Tink (under the PinVault library's encrypted storage) references Error Prone's
|
|
18
21
|
# compile-time annotations, which no React Native app has on its classpath; R8
|
|
19
22
|
# stops a release build on the missing classes otherwise.
|