@umutcansu/react-native-pinvault 0.0.0-stage → 2.3.2
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 +55 -0
- package/LICENSE +21 -0
- package/README.md +386 -2
- package/RNPinVault.podspec +43 -0
- package/android/build.gradle +68 -0
- package/android/consumer-rules.pro +25 -0
- package/android/src/main/AndroidManifest.xml +16 -0
- package/android/src/main/java/io/github/umutcansu/pinvault/reactnative/ConfigParser.kt +300 -0
- package/android/src/main/java/io/github/umutcansu/pinvault/reactnative/JsEnvironmentGuard.kt +53 -0
- package/android/src/main/java/io/github/umutcansu/pinvault/reactnative/NativeSecurity.kt +239 -0
- package/android/src/main/java/io/github/umutcansu/pinvault/reactnative/PinVaultModule.kt +410 -0
- package/android/src/main/java/io/github/umutcansu/pinvault/reactnative/PinVaultNetworking.kt +296 -0
- package/android/src/main/java/io/github/umutcansu/pinvault/reactnative/PinVaultPackage.kt +25 -0
- package/android/src/main/java/io/github/umutcansu/pinvault/reactnative/PinnedFetch.kt +131 -0
- package/android/src/main/java/io/github/umutcansu/pinvault/reactnative/ResultMapper.kt +188 -0
- package/android/src/main/java/io/github/umutcansu/pinvault/reactnative/StrictJson.kt +225 -0
- package/android/src/main/java/io/github/umutcansu/pinvault/reactnative/VaultTokenStore.kt +63 -0
- package/ios/Core/ConfigParser.swift +276 -0
- package/ios/Core/JSEnvironmentGuard.swift +129 -0
- package/ios/Core/NativeSecurity.swift +197 -0
- package/ios/Core/PinnedFetch.swift +119 -0
- package/ios/Core/ReactNetworking.swift +172 -0
- package/ios/Core/ResultMapper.swift +163 -0
- package/ios/Core/StrictJSON.swift +214 -0
- package/ios/Core/VaultTokenStore.swift +66 -0
- package/ios/PinVaultBridge.swift +412 -0
- package/ios/PinVaultReactNetworking.swift +47 -0
- package/ios/RNPinVault.h +7 -0
- package/ios/RNPinVault.mm +293 -0
- package/ios/RNPinVaultURLRequestHandler.mm +92 -0
- package/lib/module/NativePinVault.js +13 -0
- package/lib/module/NativePinVault.js.map +1 -0
- package/lib/module/index.js +312 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/types.js +2 -0
- package/lib/module/types.js.map +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/NativePinVault.d.ts +50 -0
- package/lib/typescript/src/NativePinVault.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +150 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/lib/typescript/src/types.d.ts +346 -0
- package/lib/typescript/src/types.d.ts.map +1 -0
- package/package.json +128 -4
- package/src/NativePinVault.ts +72 -0
- package/src/index.ts +385 -0
- package/src/types.ts +351 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 2.3.2 — 2026-10-08 — first release
|
|
4
|
+
|
|
5
|
+
First published version (2.3.1 was never published). Needs PinVault 2.3.2 on both platforms.
|
|
6
|
+
|
|
7
|
+
### MASVS audit fixes
|
|
8
|
+
|
|
9
|
+
- **Native security file** (`pinvault_security.json`: Android assets, iOS app
|
|
10
|
+
bundle): bootstrap pins, signing / recovery keys, `requiredSignatures`,
|
|
11
|
+
`serverScope`, `clientCaPins` and the three relaxations declared outside the
|
|
12
|
+
JS bundle. With it, JS may only repeat them (a different value, an undeclared
|
|
13
|
+
Config API or undeclared `staticPins` → `E_INVALID_CONFIG`); without it,
|
|
14
|
+
release builds refuse `allowUnsigned`, `allowUnpinnedConfigApi` and
|
|
15
|
+
`allowServerGeneratedKey` from JS.
|
|
16
|
+
- **Android networking hook installed automatically** by a content provider
|
|
17
|
+
before `Application.onCreate` (`PinVaultNetworking.install` stays as the
|
|
18
|
+
alternative). `start` and every `fetch` detect a replaced OkHttp factory /
|
|
19
|
+
custom client builder (`PinVaultNetworking.status()`) and warn;
|
|
20
|
+
`requirePinnedReactNativeNetworking: true` fails `start` with
|
|
21
|
+
`E_NETWORKING_NOT_PINNED`. RN's fetch / XHR get no disk cache and no cookie
|
|
22
|
+
jar by default (`android.keepReactNativeHttpCache` / `keepReactNativeCookies`),
|
|
23
|
+
no `https` → `http` redirects; Fresco's image client is covered too.
|
|
24
|
+
`android.pinGlobalNetworking: false` is refused while the hooks are installed
|
|
25
|
+
(opt out natively). Consumer R8 rules for the release build of a React Native
|
|
26
|
+
0.87 app (Error Prone annotations, WorkManager's Room database).
|
|
27
|
+
- **iOS: React Native's own fetch / XHR / `<Image>` pinned** by
|
|
28
|
+
`RNPinVaultURLRequestHandler` (an `RCTURLRequestHandler`, priority over
|
|
29
|
+
`RCTHTTPRequestHandler` for https) on `PinVault.shared.session()`: fail closed
|
|
30
|
+
before `start`, bounded answers (`ios.reactNativeMaxResponseBytes`), no
|
|
31
|
+
`https` → `http` redirects, cancellation. Opt out with the Info.plist key
|
|
32
|
+
`PinVaultPinReactNativeNetworking` = NO. WebSocket stays unpinned on iOS
|
|
33
|
+
(README).
|
|
34
|
+
- iOS `fetch` enforces `maxResponseBytes` while reading (needs the library's new
|
|
35
|
+
`PinnedSession.data(for:maxResponseBytes:)`).
|
|
36
|
+
- Deep nesting refused before the JSON parsers recurse (JS, Kotlin, Swift);
|
|
37
|
+
Android's `StackOverflowError` can no longer escape.
|
|
38
|
+
- A refused second `start` keeps the running `environmentGuard`; the iOS guard
|
|
39
|
+
asks JS before the library call, so no thread waits for JS.
|
|
40
|
+
- README: token handling described as it is (JS values until garbage
|
|
41
|
+
collection; native memory only on the plugin's side).
|
|
42
|
+
|
|
43
|
+
### Initial bridge
|
|
44
|
+
|
|
45
|
+
First release of the React Native plugin, aligned with PinVault 2.3.1
|
|
46
|
+
(Android `io.github.umutcansu:pinvault:2.3.1`, iOS Swift package `v2.3.1`).
|
|
47
|
+
|
|
48
|
+
- TurboModule (New Architecture, React Native 0.87+): `start`, pinned `fetch`,
|
|
49
|
+
enrollment, vault files, attestation and connection events, with the native
|
|
50
|
+
names and result shapes.
|
|
51
|
+
- Strict native config parsing (unknown keys, wrong types and oversized input are
|
|
52
|
+
refused), tokens in native memory only, redacted messages.
|
|
53
|
+
- `environmentGuard` as an async JS callback with a native deadline (fail closed).
|
|
54
|
+
- Android: React Native's own `fetch` / `XMLHttpRequest` / `WebSocket` pinned with
|
|
55
|
+
`PinVault.applyTo` (`PinVaultNetworking`). iOS: not pinned, warned at `start`.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Umut Cansu
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,387 @@
|
|
|
1
|
-
#
|
|
1
|
+
# PinVault for React Native
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
`@umutcansu/react-native-pinvault` — dynamic certificate pinning with a signed,
|
|
4
|
+
remotely updated pin config, mTLS enrollment with a hardware identity key,
|
|
5
|
+
signed and encrypted vault files, and device attestation, for React Native
|
|
6
|
+
apps on Android and iOS.
|
|
7
|
+
|
|
8
|
+
It is a thin bridge over the two native libraries — Android
|
|
9
|
+
[`io.github.umutcansu:pinvault`](https://github.com/umutcansu/PinVault/blob/main/pinvault) and the iOS Swift package
|
|
10
|
+
[`PinVault`](https://github.com/umutcansu/PinVault/blob/main/pinvault-ios/README.md). Pinning, keys (Android Keystore /
|
|
11
|
+
Secure Enclave), signature checks and vault decryption all stay native: no
|
|
12
|
+
crypto runs in JavaScript, and JavaScript never sees TLS. Names are the native
|
|
13
|
+
ones (`InitResult`, `ClientCertEnrollmentResult`, `VaultFileResult`, …), so the
|
|
14
|
+
[library documentation](https://github.com/umutcansu/PinVault/blob/main/README.md) applies as it is.
|
|
15
|
+
|
|
16
|
+
- React Native **0.87+**, New Architecture only (TurboModule, codegen), Hermes.
|
|
17
|
+
- Android minSdk 24; iOS 16+.
|
|
18
|
+
- No runtime dependencies: `react` and `react-native` are peers.
|
|
19
|
+
|
|
20
|
+
## Install
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npm install @umutcansu/react-native-pinvault
|
|
24
|
+
cd ios && pod install
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
**iOS.** The pod depends on the PinVault Swift package through React Native's
|
|
28
|
+
`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:
|
|
31
|
+
|
|
32
|
+
| Key | Why |
|
|
33
|
+
|---|---|
|
|
34
|
+
| `NSFaceIDUsageDescription` | vault files behind the screen lock (`userAuth`) |
|
|
35
|
+
| `BGTaskSchedulerPermittedIdentifiers` = `io.github.umutcansu.pinvault.refresh`, `UIBackgroundModes` = `fetch` | `schedulePeriodicUpdates` |
|
|
36
|
+
|
|
37
|
+
Register the background task in `AppDelegate` before launch ends (the app target
|
|
38
|
+
does not link the Swift package itself, the pod exposes the call):
|
|
39
|
+
|
|
40
|
+
```swift
|
|
41
|
+
import RNPinVault
|
|
42
|
+
// application(_:didFinishLaunchingWithOptions:)
|
|
43
|
+
_ = PinVaultBridge.registerBackgroundTask()
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
**Android.** The module depends on `io.github.umutcansu:pinvault` (same version
|
|
47
|
+
as this package; override with the Gradle property `pinvault.version`). React
|
|
48
|
+
Native's own networking is pinned with no code of yours: the plugin's manifest
|
|
49
|
+
declares a content provider (`PinVaultNetworkingInitializer`) that installs the
|
|
50
|
+
hooks before `Application.onCreate` runs (see [Networking](#networking)).
|
|
51
|
+
|
|
52
|
+
**Release builds.** Put the trust anchors into the app itself, not only into
|
|
53
|
+
the JS bundle: [Native security file](#native-security-file).
|
|
54
|
+
|
|
55
|
+
## Quick start
|
|
56
|
+
|
|
57
|
+
```ts
|
|
58
|
+
import PinVault from '@umutcansu/react-native-pinvault';
|
|
59
|
+
|
|
60
|
+
const result = await PinVault.start({
|
|
61
|
+
configApis: [{
|
|
62
|
+
id: 'default-tls',
|
|
63
|
+
url: 'https://api.example.com:8081/',
|
|
64
|
+
bootstrapPins: [{ hostname: 'api.example.com', sha256: [primaryPin, backupPin] }],
|
|
65
|
+
signaturePublicKeys: [signingKey, backupSigningKey],
|
|
66
|
+
requiredSignatures: 2,
|
|
67
|
+
serverScope: 'default-tls',
|
|
68
|
+
}],
|
|
69
|
+
requireCaTrust: ['api.example.com'],
|
|
70
|
+
});
|
|
71
|
+
if (result.type !== 'ready') {
|
|
72
|
+
// Fail closed: until start() returns ready, every pinned request is refused.
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const res = await PinVault.fetch('https://api.example.com/v1/me', { headers: { Accept: 'application/json' } });
|
|
77
|
+
console.log(res.status, JSON.parse(res.body));
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
`start` fails closed exactly like the native `init` / `start(config:)`. Calling
|
|
81
|
+
`start` again applies the new config (the bridge calls `reset()` first, as the
|
|
82
|
+
native samples do when they restart).
|
|
83
|
+
|
|
84
|
+
## Configuration
|
|
85
|
+
|
|
86
|
+
`start(config)` takes the builders as JSON: the keys are the builder method
|
|
87
|
+
names of `PinVaultConfig.Builder`, `ConfigApiBlock.Builder` and
|
|
88
|
+
`VaultFileConfig.Builder`, durations are `{ amount, unit }` (`TimeUnit`
|
|
89
|
+
names), enum values are the Kotlin constant names (`'TOKEN_MTLS'`,
|
|
90
|
+
`'USER_AUTH'`, …). The full shape is the `PinVaultConfig` type in
|
|
91
|
+
[`src/types.ts`](src/types.ts).
|
|
92
|
+
|
|
93
|
+
```ts
|
|
94
|
+
{
|
|
95
|
+
configApis: [{ id, url, bootstrapPins, signaturePublicKeys, requiredSignatures, recoveryPublicKeys,
|
|
96
|
+
serverScope, clientCaPins, clientCertHosts, renewalUrl, attestation, tokenHosts, … }],
|
|
97
|
+
vaultFiles: [{ key, endpoint, configApi, accessPolicy, encryption, userAuth, storage,
|
|
98
|
+
maxOfflineAge: { amount: 7, unit: 'DAYS' }, … }],
|
|
99
|
+
requireCaTrust, requireUnlockedDevice, requireHardwareBackedKeys, expectedSignerSha256,
|
|
100
|
+
wipeVaultFilesOnRevocation, vaultFileMaxOfflineAge, updateIntervalMinutes, deviceAlias, …
|
|
101
|
+
environmentGuard: async (operation) => boolean, // JS, see below
|
|
102
|
+
requirePinnedReactNativeNetworking, // see Networking
|
|
103
|
+
android: { requireUnlockedDeviceAllowFallback, pinGlobalNetworking,
|
|
104
|
+
keepReactNativeHttpCache, keepReactNativeCookies },
|
|
105
|
+
ios: { resolve: { 'mock-tls.sample': '10.0.0.12' }, expectedBundleIds, expectedTeamIds, userAuthStrength,
|
|
106
|
+
reactNativeMaxResponseBytes },
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
**The native parsers are strict.** An unknown key at any level, a wrong type
|
|
111
|
+
(`"3"` for a number, `1` for a boolean), a value out of range, a non-`https`
|
|
112
|
+
Config API URL, an oversized input (256 KiB config, bounded lists and strings)
|
|
113
|
+
rejects `start` with `PinVaultError` code `E_INVALID_CONFIG` and a message that
|
|
114
|
+
names the path (`config.configApis[0]: unknown key 'bootstrapPinz'`). The
|
|
115
|
+
library's own builder rules still apply on top (two pins per host, a signing
|
|
116
|
+
key for a signed Config API, `userAuth` for `USER_AUTH` files, …). Nothing is
|
|
117
|
+
silently ignored; the other platform's section (`ios` on Android, `android` on
|
|
118
|
+
iOS) is shape-checked and left to that platform.
|
|
119
|
+
|
|
120
|
+
Not exposed, on purpose: `clientKeystore(bytes, password)` (a P12 and its
|
|
121
|
+
password would cross the JS bridge — enroll instead), custom
|
|
122
|
+
`CertificateConfigApi`, storage, integrity-token and verdict providers (native
|
|
123
|
+
objects; iOS adds its App Attest verdict provider by itself), and
|
|
124
|
+
`reportToPinVaultBackend`.
|
|
125
|
+
|
|
126
|
+
## API
|
|
127
|
+
|
|
128
|
+
All functions return promises; `PinVault.x` and the named export `x` are the same.
|
|
129
|
+
|
|
130
|
+
| Area | Functions |
|
|
131
|
+
|---|---|
|
|
132
|
+
| Start / config | `start`, `updateNow`, `currentVersion`, `hostPinVersions`, `pinsForHost`, `signingStatus`, `isForceUpdate`, `reset`, `schedulePeriodicUpdates`, `cancelPeriodicUpdates`, `enableDebugLogging` (debug builds only) |
|
|
133
|
+
| HTTP | `fetch(url, { method, headers, body, bodyEncoding, responseEncoding, timeoutMs, maxResponseBytes, settings })` → `{ status, url, headers, body, bodyEncoding, ok }` |
|
|
134
|
+
| Enrollment | `deviceId`, `enrollForResult(token)`, `autoEnrollForResult`, `checkPendingEnrollment`, `isEnrolled`, `isEnrollmentPending`, `enrollmentVerificationCode`, `enrolledClientCN`, `enrolledClientNotAfter`, `unenroll(label, { wipeVaultFiles })`, `identityKeySecurityLevel` |
|
|
135
|
+
| Vault | `setVaultToken(key, token)`, `clearVaultTokens`, `fetchFile(key, { token })`, `loadFile(key, encoding)`, `fileStatus`, `unlockFile(key, prompt)`, `isFileLocked`, `hasFile`, `fileVersion`, `clearFile`, `syncAllFiles` |
|
|
136
|
+
| Attestation | `attestNow`, `fetchAttestationToken(host)`, `attestationStatus`, `attestationHeaderName` |
|
|
137
|
+
| Events | `addConnectionListener(event => …)` → `{ remove() }` |
|
|
138
|
+
|
|
139
|
+
Results are plain objects with a `type` (the sealed subclass in lowerCamel):
|
|
140
|
+
`{ type: 'ready', version }`, `{ type: 'failed', reason, exception: { name, message } }`,
|
|
141
|
+
`{ type: 'refused', reason: 'INVALID_TOKEN', httpStatus, … }`, `{ type: 'updated', key, version }`, …
|
|
142
|
+
A refused operation is a result, not an exception — the same as natively.
|
|
143
|
+
Rejected promises are `PinVaultError` with `code` (`E_INVALID_CONFIG`,
|
|
144
|
+
`E_INVALID_ARGUMENT`, `E_NOT_STARTED`, `E_FETCH`, `E_NO_ACTIVITY`, `E_NATIVE`,
|
|
145
|
+
`E_NETWORKING_NOT_PINNED`)
|
|
146
|
+
and `exception` (the native class name and message, e.g.
|
|
147
|
+
`SSLPeerUnverifiedException` for a pin mismatch).
|
|
148
|
+
|
|
149
|
+
## Native security file
|
|
150
|
+
|
|
151
|
+
The JS bundle is not a safe home for trust anchors: an OTA update (CodePush,
|
|
152
|
+
Expo Updates) or an edited bundle changes it without touching the app's
|
|
153
|
+
signature. Ship them natively as well, in `pinvault_security.json`:
|
|
154
|
+
|
|
155
|
+
- **Android:** `android/app/src/main/assets/pinvault_security.json` (or a build
|
|
156
|
+
type's own `src/release/assets/`). Assets, not `res/raw`: resource shrinking
|
|
157
|
+
cannot drop them and no `R` reference is needed. The APK signature seals it.
|
|
158
|
+
- **iOS:** a resource of the app bundle named `pinvault_security.json` (add it to
|
|
159
|
+
the app target, or copy it into the bundle in a build phase that runs before
|
|
160
|
+
signing). The code signature seals it.
|
|
161
|
+
|
|
162
|
+
```json
|
|
163
|
+
{
|
|
164
|
+
"configApis": [{
|
|
165
|
+
"id": "default-tls",
|
|
166
|
+
"bootstrapPins": [{ "hostname": "api.example.com", "sha256": ["…", "…"] }],
|
|
167
|
+
"signaturePublicKeys": ["…", "…", "…"], "requiredSignatures": 2,
|
|
168
|
+
"recoveryPublicKeys": ["…"], "requiredRecoverySignatures": 1,
|
|
169
|
+
"serverScope": "default-tls", "clientCaPins": ["…"],
|
|
170
|
+
"allowUnsigned": false, "allowUnpinnedConfigApi": false, "allowServerGeneratedKey": false
|
|
171
|
+
}],
|
|
172
|
+
"staticPins": { "pins": [ … ], "version": 1 }
|
|
173
|
+
}
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
The keys are the JS config's (a subset of `configApis[]` plus `staticPins`),
|
|
177
|
+
parsed as strictly; a broken file rejects `start` with `E_INVALID_CONFIG`
|
|
178
|
+
(fail closed). When the file is there:
|
|
179
|
+
|
|
180
|
+
- every Config API of the JS config must be declared in it (a bundle cannot add
|
|
181
|
+
a block with keys of its own), and JS `staticPins` must be declared too;
|
|
182
|
+
- a field the file declares is the value: JS may leave it out (the native value
|
|
183
|
+
applies) or repeat it (lists in any order); a different value rejects `start`
|
|
184
|
+
with `E_INVALID_CONFIG` naming the field;
|
|
185
|
+
- `allowUnsigned`, `allowUnpinnedConfigApi` and `allowServerGeneratedKey` from
|
|
186
|
+
JS are refused unless the file allows them for that block.
|
|
187
|
+
|
|
188
|
+
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
|
|
191
|
+
the file for its release builds from `sample-host.properties`
|
|
192
|
+
(`scripts/gen-host-config.js --native-out`).
|
|
193
|
+
|
|
194
|
+
A repackaged app can change the file too — but only by re-signing, which the
|
|
195
|
+
signer check of attestation (`expectedSignerSha256`, `expectedTeamIds`) reports.
|
|
196
|
+
|
|
197
|
+
## Networking
|
|
198
|
+
|
|
199
|
+
- **`PinVault.fetch`** goes through the native pinned client on both platforms
|
|
200
|
+
(`PinVault.getClient()` / `PinVault.shared.session()`; `settings` →
|
|
201
|
+
`getClient(settings)` / `session(settings:)`, pinning without pin-mismatch
|
|
202
|
+
recovery): pin check, attestation token, pin-mismatch recovery. HTTPS only;
|
|
203
|
+
a redirect from `https` into plain `http` is not followed (the 3xx comes
|
|
204
|
+
back) on either platform. Request bodies ≤ 10 MiB, responses ≤ 10 MiB by
|
|
205
|
+
default (`maxResponseBytes`, at most 50 MiB) — enforced while the body is
|
|
206
|
+
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:
|
|
209
|
+
- **Before `start()`** — and after a start that failed — every `https`
|
|
210
|
+
request through RN's networking fails (`PinVault has not started`): fail
|
|
211
|
+
closed, as the native `getClient()` / `session()` before `init`. Plain
|
|
212
|
+
`http` is not TLS and stays with RN (Metro in debug builds; ATS / the
|
|
213
|
+
network security config decide).
|
|
214
|
+
- Hosts without a pin entry are refused like everywhere else in PinVault.
|
|
215
|
+
- No `https` → `http` redirects; no disk HTTP cache and no cookie jar (Android:
|
|
216
|
+
`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
|
|
221
|
+
plugin's content provider before `Application.onCreate`, so every client RN
|
|
222
|
+
builds is covered:
|
|
223
|
+
`NetworkingModule.setCustomClientBuilder` is called for every fetch / XHR
|
|
224
|
+
request; each one gets the socket factory and interceptors of one client that
|
|
225
|
+
`PinVault.applyTo(builder)` configured after `start()` (one per start, so
|
|
226
|
+
pooled connections are reused). `OkHttpClientProvider.setOkHttpClientFactory`
|
|
227
|
+
covers the clients RN builds once — the networking base client, the
|
|
228
|
+
WebSocket / dev-support singleton and Fresco's image client: their TLS goes
|
|
229
|
+
through a forwarding socket factory to the current pinned one.
|
|
230
|
+
Another library that calls either setter after PinVault (a network inspector,
|
|
231
|
+
a crash reporter, another pinning package) replaces the hook: `start()` and
|
|
232
|
+
every `PinVault.fetch` check both (`PinVaultNetworking.status()`) and log a
|
|
233
|
+
warning; `requirePinnedReactNativeNetworking` turns it into a failed start.
|
|
234
|
+
Opt out natively by removing the provider in the app's manifest —
|
|
235
|
+
`android.pinGlobalNetworking: false` alone is refused while the hooks are in
|
|
236
|
+
place, so a JS bundle cannot unpin RN's networking:
|
|
237
|
+
|
|
238
|
+
```xml
|
|
239
|
+
<provider android:name="io.github.umutcansu.pinvault.reactnative.PinVaultNetworkingInitializer"
|
|
240
|
+
android:authorities="${applicationId}.pinvault-networking" tools:node="remove" />
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
With the provider removed, `PinVaultNetworking.install(this)` in
|
|
244
|
+
`MainApplication.onCreate` (before `loadReactNative`) is the explicit
|
|
245
|
+
alternative; a hook installed later than that misses the clients RN already built.
|
|
246
|
+
- **iOS:** the plugin's `RNPinVaultURLRequestHandler` (an `RCTURLRequestHandler`)
|
|
247
|
+
answers for `https` with `handlerPriority` 10; RCTNetworking picks the
|
|
248
|
+
highest-priority handler, so RN's `RCTHTTPRequestHandler` (priority 0) only
|
|
249
|
+
sees plain `http`. Codegen registers it (`codegenConfig.ios.
|
|
250
|
+
modulesConformingToProtocol` in this package's `package.json`; `pod install`
|
|
251
|
+
picks it up). Requests run on `PinVault.shared.session()` with the library's
|
|
252
|
+
bounded read (`ios.reactNativeMaxResponseBytes`, default 50 MiB, at most
|
|
253
|
+
256 MiB); the library hands out whole answers, so RN gets the response, the
|
|
254
|
+
body in chunks and the completion after the body has been read (progress
|
|
255
|
+
events arrive at the end); cancelling a request cancels it. `start` checks
|
|
256
|
+
which handler RCTNetworking picks for an `https` request.
|
|
257
|
+
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`.
|
|
264
|
+
Keep App Transport Security on (no `NSAllowsArbitraryLoads`).
|
|
265
|
+
|
|
266
|
+
## Enrollment and vault tokens
|
|
267
|
+
|
|
268
|
+
Tokens cross the bridge once, as an argument, and the plugin keeps them on the
|
|
269
|
+
native side only. The enrollment token is held for the duration of the call
|
|
270
|
+
(`enrollForResult(token)`). Vault access tokens of `TOKEN` / `TOKEN_MTLS` files
|
|
271
|
+
are set with `setVaultToken(key, token)` or passed to `fetchFile(key, { token })`
|
|
272
|
+
and kept **in native memory only**: the native `accessToken { … }` provider
|
|
273
|
+
reads them on every download; the plugin never writes them to disk or the
|
|
274
|
+
Keychain, never hands them back to JS, and they are gone with the process.
|
|
275
|
+
What the plugin cannot control is the JS side: the string you pass is a JS
|
|
276
|
+
value until the garbage collector drops it, and if it went through React
|
|
277
|
+
state, a form field or a store, it lives there too. Pass it straight from where
|
|
278
|
+
it came from, clear the field, and never persist or log it. For an attestation
|
|
279
|
+
`PinVault-Token`, prefer `tokenHosts` + `PinVault.fetch` (the native client adds
|
|
280
|
+
it) over `fetchAttestationToken`, which hands the token to JS. Forget vault
|
|
281
|
+
tokens on a revocation:
|
|
282
|
+
|
|
283
|
+
```ts
|
|
284
|
+
PinVault.addConnectionListener((e) => {
|
|
285
|
+
if (e.type === 'clientCertRenewal' && e.status === 'REENROLL_REQUIRED') PinVault.clearVaultTokens();
|
|
286
|
+
});
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
Every string the bridge hands back (reasons, messages, `lastError`) has the
|
|
290
|
+
tokens it holds replaced by `***`.
|
|
291
|
+
|
|
292
|
+
## Vault content
|
|
293
|
+
|
|
294
|
+
- `fetchFile` results never carry the content (`{ type: 'updated', key, version }`);
|
|
295
|
+
read it with `loadFile(key, 'utf8' | 'base64')`.
|
|
296
|
+
- Files behind the screen lock (`USER_AUTH`) never open with `loadFile`:
|
|
297
|
+
`unlockFile(key, { title, description, negativeButtonText })` shows the
|
|
298
|
+
native prompt (Android `BiometricPrompt`, which needs a `FragmentActivity` —
|
|
299
|
+
`ReactActivity` is one; iOS `LAContext`) and only then returns the content.
|
|
300
|
+
- Once the content is in JS, it is a JS string: keep it only as long as you
|
|
301
|
+
need it, do not put it into state that is persisted, and never log it.
|
|
302
|
+
|
|
303
|
+
## environmentGuard
|
|
304
|
+
|
|
305
|
+
```ts
|
|
306
|
+
environmentGuard: async (operation) => !(await myRaspSaysCompromised()),
|
|
307
|
+
environmentGuardTimeoutMs: 5000,
|
|
308
|
+
```
|
|
309
|
+
|
|
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
|
|
314
|
+
starts on its own (a background refresh) still waits on its own thread,
|
|
315
|
+
bounded by the timeout. **Fail closed:**
|
|
316
|
+
a timeout (native deadline 100–30 000 ms, default 5000), a thrown error, a
|
|
317
|
+
rejected promise or anything but `true` refuses the operation. The guard runs
|
|
318
|
+
in JavaScript, and code that hooks the JS runtime can answer for it: treat it
|
|
319
|
+
as one more signal. The decisive check is server-side attestation
|
|
320
|
+
(`attestation: true` on a Config API) with a strict policy.
|
|
321
|
+
|
|
322
|
+
## Security notes (OWASP MASVS)
|
|
323
|
+
|
|
324
|
+
- **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
|
|
327
|
+
hook is detected.
|
|
328
|
+
- **Trust anchors** — release builds take the relaxations only from the native
|
|
329
|
+
security file; with the file, JS cannot change pins, keys or scopes.
|
|
330
|
+
- **STORAGE** — the plugin stores nothing in JS (no AsyncStorage); tokens live
|
|
331
|
+
in native memory only; vault content leaves native code only through
|
|
332
|
+
`loadFile` / `unlockFile`.
|
|
333
|
+
- **CRYPTO / AUTH** — no crypto in JS; screen-lock prompts are native.
|
|
334
|
+
- **PLATFORM** — every bridge input is validated natively (types, sizes, allowed
|
|
335
|
+
keys); events and errors carry no token, password or file content; no WebView.
|
|
336
|
+
- **CODE** — TypeScript strict; the plugin logs only in development builds
|
|
337
|
+
(`__DEV__`); R8 rules ship in `android/consumer-rules.pro`.
|
|
338
|
+
- **RESILIENCE** — `requireHardwareBackedKeys`, `requireUnlockedDevice` and
|
|
339
|
+
`expectedSignerSha256` are config keys; turn them on in release builds.
|
|
340
|
+
|
|
341
|
+
## Privacy: what leaves the device
|
|
342
|
+
|
|
343
|
+
The same as the native libraries, to your own PinVault server only: the device
|
|
344
|
+
id (`ANDROID_ID` / `identifierForVendor`), manufacturer and model, the
|
|
345
|
+
`deviceAlias` you set, and — with `attestation` — the attestation report (app
|
|
346
|
+
package / bundle id, version, signer digests, installer, OS version and patch
|
|
347
|
+
level, key security level, and the integrity signals: root/jailbreak, emulator,
|
|
348
|
+
debugger, hooking framework, app integrity, cloner, installer, adb, software
|
|
349
|
+
key; see [`ATTESTATION.md`](https://github.com/umutcansu/PinVault/blob/main/ATTESTATION.md) §3). Vault downloads report key,
|
|
350
|
+
version, status, device model, device id and alias. Connection events stay on
|
|
351
|
+
the device unless your listener sends them somewhere.
|
|
352
|
+
|
|
353
|
+
## Tests
|
|
354
|
+
|
|
355
|
+
```bash
|
|
356
|
+
npm test # Jest: the TS layer with the native module mocked
|
|
357
|
+
npx tsc --noEmit
|
|
358
|
+
# Android JVM tests (config parsing, refusals, result mapping, RN networking hooks, fetch):
|
|
359
|
+
cd ../sample-client-rn/android && ./gradlew :umutcansu_react-native-pinvault:testDebugUnitTest
|
|
360
|
+
# iOS XCTest for the Swift core (on the Mac, against this repository's Swift package):
|
|
361
|
+
cd ios/Tests && swift test
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
## Development in this repository
|
|
365
|
+
|
|
366
|
+
The sample app [`sample-client-rn/`](https://github.com/umutcansu/PinVault/blob/main/sample-client-rn) builds the plugin from
|
|
367
|
+
source. Android: `pinvault.localPath` in its `gradle.properties` publishes the
|
|
368
|
+
repository's `:pinvault` (unsigned) into `android/build/pinvault-maven` at every
|
|
369
|
+
build and resolves `io.github.umutcansu:pinvault` only from there — a composite
|
|
370
|
+
build is not possible because PinVault builds with AGP 8.7 and React Native 0.87
|
|
371
|
+
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.
|
|
373
|
+
|
|
374
|
+
## Known limits
|
|
375
|
+
|
|
376
|
+
- Android 9 **emulator** images: their software keymaster (`keymaster@3.0`)
|
|
377
|
+
crashes while attesting a key made with `requireUnlockedDevice` (SIGSEGV in
|
|
378
|
+
`build_auth_list`); the keystore daemon then keeps a dead connection and every
|
|
379
|
+
Keystore operation fails until it is restarted. Turn `requireUnlockedDevice`
|
|
380
|
+
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
|
|
386
|
+
React Native version that renames them reads as "could not be read", which
|
|
387
|
+
counts as not pinned.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
|
|
3
|
+
package = JSON.parse(File.read(File.join(__dir__, "package.json")))
|
|
4
|
+
|
|
5
|
+
# PinVault for iOS is a Swift package (Package.swift at the repository root,
|
|
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);
|
|
8
|
+
# - development: PINVAULT_IOS_PACKAGE_PATH = a PinVault checkout (the sample's
|
|
9
|
+
# Podfile sets it to this repository), the counterpart of pinvault.localPath.
|
|
10
|
+
pinvault_url = "https://github.com/umutcansu/PinVault.git"
|
|
11
|
+
local_package = ENV["PINVAULT_IOS_PACKAGE_PATH"].to_s.strip
|
|
12
|
+
|
|
13
|
+
Pod::Spec.new do |s|
|
|
14
|
+
s.name = "RNPinVault"
|
|
15
|
+
s.version = package["version"]
|
|
16
|
+
s.summary = package["description"]
|
|
17
|
+
s.homepage = package["homepage"]
|
|
18
|
+
s.license = package["license"]
|
|
19
|
+
s.authors = package["author"]
|
|
20
|
+
|
|
21
|
+
s.platforms = { :ios => "16.0" }
|
|
22
|
+
s.source = { :git => pinvault_url, :tag => "v#{s.version}" }
|
|
23
|
+
s.source_files = "ios/*.{h,mm,swift}", "ios/Core/*.swift"
|
|
24
|
+
s.private_header_files = "ios/*.h"
|
|
25
|
+
s.swift_version = "5.9"
|
|
26
|
+
|
|
27
|
+
if local_package.empty?
|
|
28
|
+
spm_dependency(s,
|
|
29
|
+
url: pinvault_url,
|
|
30
|
+
requirement: { kind: "exactVersion", version: s.version.to_s },
|
|
31
|
+
products: ["PinVault"]
|
|
32
|
+
)
|
|
33
|
+
else
|
|
34
|
+
path = File.expand_path(local_package)
|
|
35
|
+
unless File.exist?(File.join(path, "Package.swift"))
|
|
36
|
+
raise "PINVAULT_IOS_PACKAGE_PATH=#{local_package}: no Package.swift there (a PinVault checkout is expected)"
|
|
37
|
+
end
|
|
38
|
+
Pod::UI.puts "[RNPinVault] PinVault Swift package from #{path}"
|
|
39
|
+
spm_dependency(s, url: path, requirement: {}, products: ["PinVault"])
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
install_modules_dependencies(s)
|
|
43
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// @umutcansu/react-native-pinvault — Android TurboModule over
|
|
2
|
+
// io.github.umutcansu:pinvault (Maven Central).
|
|
3
|
+
//
|
|
4
|
+
// Development against an unpublished PinVault: the app's settings.gradle can
|
|
5
|
+
// substitute the Maven artifact with the repository's :pinvault project
|
|
6
|
+
// (sample-client-rn/android/settings.gradle, Gradle property pinvault.localPath).
|
|
7
|
+
buildscript {
|
|
8
|
+
ext.PinVaultRN = [
|
|
9
|
+
kotlinVersion: "2.2.0",
|
|
10
|
+
minSdkVersion: 24,
|
|
11
|
+
compileSdkVersion: 36,
|
|
12
|
+
pinvaultVersion: "2.3.2",
|
|
13
|
+
]
|
|
14
|
+
ext.pinvaultRnExt = { prop ->
|
|
15
|
+
rootProject.ext.has(prop) ? rootProject.ext.get(prop) : PinVaultRN[prop]
|
|
16
|
+
}
|
|
17
|
+
repositories {
|
|
18
|
+
google()
|
|
19
|
+
mavenCentral()
|
|
20
|
+
}
|
|
21
|
+
dependencies {
|
|
22
|
+
classpath "com.android.tools.build:gradle:8.7.2"
|
|
23
|
+
// noinspection DifferentKotlinGradleVersion
|
|
24
|
+
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${pinvaultRnExt('kotlinVersion')}"
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
apply plugin: "com.android.library"
|
|
29
|
+
if (project.extensions.findByName("kotlin") == null) {
|
|
30
|
+
apply plugin: "kotlin-android"
|
|
31
|
+
}
|
|
32
|
+
apply plugin: "com.facebook.react"
|
|
33
|
+
|
|
34
|
+
def pinvaultVersion = project.findProperty("pinvault.version") ?: pinvaultRnExt("pinvaultVersion")
|
|
35
|
+
|
|
36
|
+
android {
|
|
37
|
+
namespace "io.github.umutcansu.pinvault.reactnative"
|
|
38
|
+
compileSdkVersion pinvaultRnExt("compileSdkVersion")
|
|
39
|
+
|
|
40
|
+
defaultConfig {
|
|
41
|
+
minSdkVersion pinvaultRnExt("minSdkVersion")
|
|
42
|
+
consumerProguardFiles "consumer-rules.pro"
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
compileOptions {
|
|
46
|
+
sourceCompatibility JavaVersion.VERSION_17
|
|
47
|
+
targetCompatibility JavaVersion.VERSION_17
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
testOptions {
|
|
51
|
+
unitTests.returnDefaultValues = true
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
dependencies {
|
|
56
|
+
implementation "com.facebook.react:react-android"
|
|
57
|
+
implementation "io.github.umutcansu:pinvault:${pinvaultVersion}"
|
|
58
|
+
implementation "com.squareup.okhttp3:okhttp:4.12.0"
|
|
59
|
+
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.1"
|
|
60
|
+
implementation "androidx.fragment:fragment-ktx:1.8.5"
|
|
61
|
+
|
|
62
|
+
testImplementation "junit:junit:4.13.2"
|
|
63
|
+
// The real org.json (android.jar only has stubs in JVM tests).
|
|
64
|
+
testImplementation "org.json:json:20240303"
|
|
65
|
+
testImplementation "com.squareup.okhttp3:mockwebserver:4.12.0"
|
|
66
|
+
testImplementation "com.squareup.okhttp3:okhttp-tls:4.12.0"
|
|
67
|
+
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.8.1"
|
|
68
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# @umutcansu/react-native-pinvault — R8 rules for apps.
|
|
2
|
+
# The TurboModule is looked up by the generated code (no reflection); the
|
|
3
|
+
# PinVault library ships its own consumer rules. Keep the module and package
|
|
4
|
+
# names readable in crash reports only.
|
|
5
|
+
-keep class io.github.umutcansu.pinvault.reactnative.PinVaultPackage { *; }
|
|
6
|
+
-keep class io.github.umutcansu.pinvault.reactnative.PinVaultModule { *; }
|
|
7
|
+
-keep class io.github.umutcansu.pinvault.reactnative.PinVaultNetworking { public *; }
|
|
8
|
+
-keep class io.github.umutcansu.pinvault.reactnative.PinVaultNetworkingInitializer { <init>(); }
|
|
9
|
+
# PinVaultNetworking.status() reads React Native's two networking hooks by name
|
|
10
|
+
# to detect another library replacing them.
|
|
11
|
+
-keepclassmembers class com.facebook.react.modules.network.OkHttpClientProvider {
|
|
12
|
+
private static com.facebook.react.modules.network.OkHttpClientFactory factory;
|
|
13
|
+
}
|
|
14
|
+
-keepclassmembers class com.facebook.react.modules.network.NetworkingModule {
|
|
15
|
+
private static com.facebook.react.modules.network.CustomClientBuilder customClientBuilder;
|
|
16
|
+
}
|
|
17
|
+
# Tink (under the PinVault library's encrypted storage) references Error Prone's
|
|
18
|
+
# compile-time annotations, which no React Native app has on its classpath; R8
|
|
19
|
+
# stops a release build on the missing classes otherwise.
|
|
20
|
+
-dontwarn com.google.errorprone.annotations.**
|
|
21
|
+
# PinVault's periodic updates use WorkManager, which opens its Room database by
|
|
22
|
+
# reflection (WorkDatabase_Impl's no-argument constructor). In a React Native
|
|
23
|
+
# 0.87 release build (AGP 9, R8 full mode) R8 dropped that constructor and the
|
|
24
|
+
# app died at launch in androidx.startup's InitializationProvider.
|
|
25
|
+
-keep class * extends androidx.room.RoomDatabase { <init>(); }
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
|
2
|
+
<!-- PinVault itself declares what it needs (INTERNET, biometrics). -->
|
|
3
|
+
<application>
|
|
4
|
+
<!--
|
|
5
|
+
Pins React Native's own networking before the app's code runs: Android
|
|
6
|
+
creates content providers before Application.onCreate, so the hooks are
|
|
7
|
+
in place before React Native builds its HTTP clients (README, "Networking").
|
|
8
|
+
Opt out in the app's manifest with tools:node="remove" on this element.
|
|
9
|
+
-->
|
|
10
|
+
<provider
|
|
11
|
+
android:name="io.github.umutcansu.pinvault.reactnative.PinVaultNetworkingInitializer"
|
|
12
|
+
android:authorities="${applicationId}.pinvault-networking"
|
|
13
|
+
android:exported="false"
|
|
14
|
+
android:initOrder="1000" />
|
|
15
|
+
</application>
|
|
16
|
+
</manifest>
|