@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 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
+ [![npm](https://img.shields.io/npm/v/@umutcansu/react-native-pinvault)](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/README.md) applies as it is.
16
+ [library documentation](https://github.com/umutcansu/PinVault/blob/main/GUIDE.md) applies as it is.
15
17
 
16
- - React Native **0.87+**, New Architecture only (TurboModule, codegen), Hermes.
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`). Info.plist, as for the
30
- native library:
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 those three
190
- relaxations from JS; debug builds take them as before. The sample app writes
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` and `<Image>` are pinned on
208
- both platforms** (WebSocket on Android only). The rules are the same on both:
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: true` makes `start` fail with
218
- `E_NETWORKING_NOT_PINNED` when RN's networking does not go through PinVault
219
- (below); without it a warning is logged.
220
- - **Android** (two hooks of RN 0.87, read from its sources), installed by the
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
- Another library that calls either setter after PinVault (a network inspector,
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 both (`PinVaultNetworking.status()`) and log a
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. **WebSocket is not pinned on iOS:** RN 0.87's `RCTWebSocketModule`
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. Forget vault
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 `UNLOCK_FILE`. On iOS the bridge
311
- asks before it calls the library (`start`: `INIT` and `ENROLL`; enrollment
312
- calls: `ENROLL`; `fetchFile` / `syncAllFiles`: `FETCH_FILE`; `unlockFile`:
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 both
326
- platforms (WebSocket on Android), fail closed before `start`, and a replaced
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 WebSocket is not pinned, and RN's https answers are delivered after
382
- the whole (bounded) body is read (above).
383
- - Android: the networking hooks are detected by reading two private fields of
384
- React Native 0.87 (`OkHttpClientProvider.factory`,
385
- `NetworkingModule.customClientBuilder`; the consumer R8 rules keep them). A
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.
@@ -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.3.2);
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"
@@ -9,7 +9,7 @@ buildscript {
9
9
  kotlinVersion: "2.2.0",
10
10
  minSdkVersion: 24,
11
11
  compileSdkVersion: 36,
12
- pinvaultVersion: "2.3.2",
12
+ pinvaultVersion: "2.4.1",
13
13
  ]
14
14
  ext.pinvaultRnExt = { prop ->
15
15
  rootProject.ext.has(prop) ? rootProject.ext.get(prop) : PinVaultRN[prop]
@@ -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.