@umutcansu/react-native-pinvault 2.3.2 → 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +29 -0
- package/README.md +65 -14
- package/RNPinVault.podspec +1 -1
- package/android/build.gradle +1 -1
- package/android/src/main/java/io/github/umutcansu/pinvault/reactnative/ConfigParser.kt +120 -24
- package/android/src/main/java/io/github/umutcansu/pinvault/reactnative/NativeSecurity.kt +120 -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 +91 -8
- package/ios/Core/ConfigParser.swift +188 -27
- package/ios/Core/NativeSecurity.swift +112 -6
- package/ios/PinVaultBridge.swift +16 -4
- 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 +1 -1
- package/src/index.ts +15 -2
- package/src/types.ts +14 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,34 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2.4.0 — 2026-10-09
|
|
4
|
+
|
|
5
|
+
### Breaking
|
|
6
|
+
|
|
7
|
+
- **Release builds need the native security file** when the config has Config
|
|
8
|
+
APIs or static pins (Android manifest meta-data
|
|
9
|
+
`io.github.umutcansu.pinvault.ALLOW_NO_NATIVE_SECURITY_FILE` / iOS Info.plist
|
|
10
|
+
`PinVaultAllowNoNativeSecurityFile` = YES opts out).
|
|
11
|
+
- **`requirePinnedReactNativeNetworking` is on by default in release builds.**
|
|
12
|
+
- **`expiredConfigGrace` is at most 7 days in release builds.**
|
|
13
|
+
- **Vault file keys** must match `[A-Za-z0-9._-]{1,64}` (not only dots).
|
|
14
|
+
|
|
15
|
+
### Security
|
|
16
|
+
|
|
17
|
+
- The native security file may also fix a block's `url`, `tokenHosts`,
|
|
18
|
+
`clientCertHosts` and `attestation`, carry a `require` section (protections
|
|
19
|
+
JS cannot switch off) and `vaultFiles` (signature key, encryption, lock).
|
|
20
|
+
- `start` answers `nativeSecurityApplied`.
|
|
21
|
+
- `proofOfPossession` on a Config API (JS or the native security file, where
|
|
22
|
+
it cannot be switched off): the native clients add a `PinVault-Proof` signed
|
|
23
|
+
by the device key to every request that carries the token (ATTESTATION.md §5.1).
|
|
24
|
+
- Android: React Native's long-lived clients (WebSocket, images) run the
|
|
25
|
+
current start's network interceptors (the per-request pin check) and their
|
|
26
|
+
pooled connections are closed on every start and reset; a hook installed only
|
|
27
|
+
by `start()` is reported as not pinned; a hook replaced after start refuses
|
|
28
|
+
https while `requirePinnedReactNativeNetworking` is on.
|
|
29
|
+
- The JS guard of a start the native side refuses is put back.
|
|
30
|
+
- `LOAD_FILE` is a guarded operation (`loadFile`).
|
|
31
|
+
|
|
3
32
|
## 2.3.2 — 2026-10-08 — first release
|
|
4
33
|
|
|
5
34
|
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,7 +13,7 @@ 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
18
|
- React Native **0.87+**, New Architecture only (TurboModule, codegen), Hermes.
|
|
17
19
|
- Android minSdk 24; iOS 16+.
|
|
@@ -19,11 +21,18 @@ ones (`InitResult`, `ClientCertEnrollmentResult`, `VaultFileResult`, …), so th
|
|
|
19
21
|
|
|
20
22
|
## Install
|
|
21
23
|
|
|
24
|
+
From npm ([`@umutcansu/react-native-pinvault`](https://www.npmjs.com/package/@umutcansu/react-native-pinvault)):
|
|
25
|
+
|
|
22
26
|
```bash
|
|
23
27
|
npm install @umutcansu/react-native-pinvault
|
|
24
28
|
cd ios && pod install
|
|
25
29
|
```
|
|
26
30
|
|
|
31
|
+
The native libraries come with it, at the package's own version: npm 2.3.2
|
|
32
|
+
uses `io.github.umutcansu:pinvault:2.3.2` from Maven Central and the Swift
|
|
33
|
+
package at tag `v2.3.2`. The New Architecture must stay on (React Native
|
|
34
|
+
0.87's default).
|
|
35
|
+
|
|
27
36
|
**iOS.** The pod depends on the PinVault Swift package through React Native's
|
|
28
37
|
`spm_dependency` (git URL `https://github.com/umutcansu/PinVault.git`, exact
|
|
29
38
|
version = this package's version, product `PinVault`). Info.plist, as for the
|
|
@@ -93,7 +102,7 @@ names), enum values are the Kotlin constant names (`'TOKEN_MTLS'`,
|
|
|
93
102
|
```ts
|
|
94
103
|
{
|
|
95
104
|
configApis: [{ id, url, bootstrapPins, signaturePublicKeys, requiredSignatures, recoveryPublicKeys,
|
|
96
|
-
serverScope, clientCaPins, clientCertHosts, renewalUrl, attestation, tokenHosts, … }],
|
|
105
|
+
serverScope, clientCaPins, clientCertHosts, renewalUrl, attestation, tokenHosts, proofOfPossession, … }],
|
|
97
106
|
vaultFiles: [{ key, endpoint, configApi, accessPolicy, encryption, userAuth, storage,
|
|
98
107
|
maxOfflineAge: { amount: 7, unit: 'DAYS' }, … }],
|
|
99
108
|
requireCaTrust, requireUnlockedDevice, requireHardwareBackedKeys, expectedSignerSha256,
|
|
@@ -167,9 +176,25 @@ signature. Ship them natively as well, in `pinvault_security.json`:
|
|
|
167
176
|
"signaturePublicKeys": ["…", "…", "…"], "requiredSignatures": 2,
|
|
168
177
|
"recoveryPublicKeys": ["…"], "requiredRecoverySignatures": 1,
|
|
169
178
|
"serverScope": "default-tls", "clientCaPins": ["…"],
|
|
179
|
+
"url": "https://config.example.com:8081/", "attestation": true,
|
|
180
|
+
"tokenHosts": ["api.example.com"], "proofOfPossession": true, "clientCertHosts": ["api.example.com:443"],
|
|
181
|
+
"enrollmentUrl": "https://enroll.example.com/", "renewalUrl": "https://renew.example.com/",
|
|
170
182
|
"allowUnsigned": false, "allowUnpinnedConfigApi": false, "allowServerGeneratedKey": false
|
|
171
183
|
}],
|
|
172
|
-
"staticPins": { "pins": [ … ], "version": 1 }
|
|
184
|
+
"staticPins": { "pins": [ … ], "version": 1 },
|
|
185
|
+
"require": {
|
|
186
|
+
"requireUnlockedDevice": true, "requireHardwareBackedKeys": true,
|
|
187
|
+
"managedTrustRoots": true, "wipeVaultFilesOnRevocation": true,
|
|
188
|
+
"requireCaTrust": ["api.example.com"],
|
|
189
|
+
"expectedSignerSha256": ["…"],
|
|
190
|
+
"expectedBundleIds": ["com.example.app"], "expectedTeamIds": ["ABCDE12345"],
|
|
191
|
+
"expiredConfigGraceSeconds": 0, "vaultFileMaxOfflineAgeSeconds": 604800,
|
|
192
|
+
"userAuthStrength": "BIOMETRIC_CURRENT_SET"
|
|
193
|
+
},
|
|
194
|
+
"vaultFiles": [
|
|
195
|
+
{ "key": "statement", "signaturePublicKey": "…", "encryption": "USER_AUTH", "userAuth": "REQUIRED",
|
|
196
|
+
"maxOfflineAgeSeconds": 86400 }
|
|
197
|
+
]
|
|
173
198
|
}
|
|
174
199
|
```
|
|
175
200
|
|
|
@@ -183,11 +208,32 @@ parsed as strictly; a broken file rejects `start` with `E_INVALID_CONFIG`
|
|
|
183
208
|
applies) or repeat it (lists in any order); a different value rejects `start`
|
|
184
209
|
with `E_INVALID_CONFIG` naming the field;
|
|
185
210
|
- `allowUnsigned`, `allowUnpinnedConfigApi` and `allowServerGeneratedKey` from
|
|
186
|
-
JS are refused unless the file allows them for that block
|
|
211
|
+
JS are refused unless the file allows them for that block;
|
|
212
|
+
- a block's `url`, `enrollmentUrl`, `renewalUrl`, `tokenHosts` and
|
|
213
|
+
`clientCertHosts` are fixed where the file gives them (where the block talks to, and who gets its token and identity),
|
|
214
|
+
and `attestation: true` / `proofOfPossession: true` there cannot be turned off from JS;
|
|
215
|
+
- `require` only tightens: a protection it turns on stays on whatever JS says,
|
|
216
|
+
its `requireCaTrust` hosts are added to JS's, the expected signer / bundle /
|
|
217
|
+
team ids and iOS `userAuthStrength` are fixed, and `expiredConfigGraceSeconds`
|
|
218
|
+
/ `vaultFileMaxOfflineAgeSeconds` are the most JS may ask for (the value when
|
|
219
|
+
JS asks for none)
|
|
220
|
+
(Android reads `expectedSignerSha256`, iOS the bundle and team ids; one file
|
|
221
|
+
can serve both);
|
|
222
|
+
- `vaultFiles` fixes those files' signature key, encryption and screen lock, and
|
|
223
|
+
`maxOfflineAgeSeconds` is the longest offline lifetime JS may give them;
|
|
224
|
+
with the section present, a JS vault file it does not name is refused.
|
|
225
|
+
|
|
226
|
+
`start` answers `nativeSecurityApplied: true` when the file was applied.
|
|
187
227
|
|
|
188
228
|
Without the file, **release builds** (Android: the app is not
|
|
189
|
-
`android:debuggable`; iOS: compiled without `DEBUG`) refuse
|
|
190
|
-
|
|
229
|
+
`android:debuggable`; iOS: compiled without `DEBUG`) refuse to start a config
|
|
230
|
+
with Config APIs or static pins: its trust anchors would come from JS alone.
|
|
231
|
+
An app that accepts that says so natively — Android:
|
|
232
|
+
`<meta-data android:name="io.github.umutcansu.pinvault.ALLOW_NO_NATIVE_SECURITY_FILE" android:value="true"/>`
|
|
233
|
+
in the manifest's `<application>`; iOS: `PinVaultAllowNoNativeSecurityFile` =
|
|
234
|
+
YES in Info.plist — and the three relaxations stay refused from JS then. Debug
|
|
235
|
+
builds take everything as before. Whatever the file says, a release build
|
|
236
|
+
takes an `expiredConfigGrace` of at most 7 days. The sample app writes
|
|
191
237
|
the file for its release builds from `sample-host.properties`
|
|
192
238
|
(`scripts/gen-host-config.js --native-out`).
|
|
193
239
|
|
|
@@ -214,9 +260,11 @@ signer check of attestation (`expectedSignerSha256`, `expectedTeamIds`) reports.
|
|
|
214
260
|
- Hosts without a pin entry are refused like everywhere else in PinVault.
|
|
215
261
|
- No `https` → `http` redirects; no disk HTTP cache and no cookie jar (Android:
|
|
216
262
|
`android.keepReactNativeHttpCache` / `keepReactNativeCookies` keep RN's own).
|
|
217
|
-
- `requirePinnedReactNativeNetworking
|
|
218
|
-
`E_NETWORKING_NOT_PINNED` when RN's networking does
|
|
219
|
-
(below);
|
|
263
|
+
- `requirePinnedReactNativeNetworking` (on by default in release builds)
|
|
264
|
+
makes `start` fail with `E_NETWORKING_NOT_PINNED` when RN's networking does
|
|
265
|
+
not go through PinVault (below); on Android a hook replaced after start
|
|
266
|
+
then refuses RN's https too (checked every few seconds). With it off a
|
|
267
|
+
warning is logged.
|
|
220
268
|
- **Android** (two hooks of RN 0.87, read from its sources), installed by the
|
|
221
269
|
plugin's content provider before `Application.onCreate`, so every client RN
|
|
222
270
|
builds is covered:
|
|
@@ -277,7 +325,10 @@ value until the garbage collector drops it, and if it went through React
|
|
|
277
325
|
state, a form field or a store, it lives there too. Pass it straight from where
|
|
278
326
|
it came from, clear the field, and never persist or log it. For an attestation
|
|
279
327
|
`PinVault-Token`, prefer `tokenHosts` + `PinVault.fetch` (the native client adds
|
|
280
|
-
it) over `fetchAttestationToken`, which hands the token to JS.
|
|
328
|
+
it) over `fetchAttestationToken`, which hands the token to JS. With
|
|
329
|
+
`proofOfPossession` the native client also adds a `PinVault-Proof` signed by the
|
|
330
|
+
device key on every such request, and a backend that requires it refuses the
|
|
331
|
+
token alone — a token in JS is then of no use without the native client. Forget vault
|
|
281
332
|
tokens on a revocation:
|
|
282
333
|
|
|
283
334
|
```ts
|
|
@@ -307,10 +358,10 @@ environmentGuard: async (operation) => !(await myRaspSaysCompromised()),
|
|
|
307
358
|
environmentGuardTimeoutMs: 5000,
|
|
308
359
|
```
|
|
309
360
|
|
|
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
|
|
361
|
+
Asked before `INIT`, `ENROLL`, `FETCH_FILE`, `UNLOCK_FILE` and `LOAD_FILE`. On iOS
|
|
362
|
+
the bridge asks before it calls the library (`start`: `INIT` and `ENROLL`;
|
|
363
|
+
enrollment calls: `ENROLL`; `fetchFile` / `syncAllFiles`: `FETCH_FILE`;
|
|
364
|
+
`unlockFile`: `UNLOCK_FILE`; `loadFile`: `LOAD_FILE`), so the wait for JS holds no thread; an operation the library
|
|
314
365
|
starts on its own (a background refresh) still waits on its own thread,
|
|
315
366
|
bounded by the timeout. **Fail closed:**
|
|
316
367
|
a timeout (native deadline 100–30 000 ms, default 5000), a thrown error, a
|
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.0);
|
|
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
|
@@ -55,11 +55,16 @@ internal object ConfigParser {
|
|
|
55
55
|
const val MAX_PINS = 256
|
|
56
56
|
const val MAX_PINS_PER_HOST = 16
|
|
57
57
|
const val KEY_LENGTH = 4096
|
|
58
|
+
private val VAULT_KEY = Regex("^[A-Za-z0-9._-]{1,64}$")
|
|
58
59
|
|
|
59
60
|
/**
|
|
60
61
|
* @param native the app's native security file ([NativeSecurity]); null = none.
|
|
61
|
-
* @param release a release build of the app: without a native file,
|
|
62
|
-
*
|
|
62
|
+
* @param release a release build of the app: without a native file, a
|
|
63
|
+
* config with Config APIs or static pins is refused (unless
|
|
64
|
+
* [noFileAllowed]) and the relaxations (`allowUnsigned`, …) are refused
|
|
65
|
+
* from JS; `expiredConfigGrace` is capped.
|
|
66
|
+
* @param noFileAllowed the app's manifest accepts a release start without
|
|
67
|
+
* a native security file ([NativeSecurity.NO_FILE_META_DATA]).
|
|
63
68
|
*/
|
|
64
69
|
fun parse(
|
|
65
70
|
json: String,
|
|
@@ -68,27 +73,64 @@ internal object ConfigParser {
|
|
|
68
73
|
listener: PinVaultConnectionListener?,
|
|
69
74
|
native: NativeSecurity? = null,
|
|
70
75
|
release: Boolean = false,
|
|
76
|
+
noFileAllowed: Boolean = false,
|
|
71
77
|
): ParsedConfig {
|
|
72
78
|
val root = StrictJson.parseObject(json, "config")
|
|
73
79
|
val builder = PinVaultConfig.Builder()
|
|
80
|
+
val nativeRequire = native?.require ?: NativeSecurity.Require()
|
|
74
81
|
|
|
75
|
-
root.objList("configApis", MAX_CONFIG_APIS)
|
|
76
|
-
root.objList("vaultFiles", MAX_VAULT_FILES)
|
|
82
|
+
val configApis = root.objList("configApis", MAX_CONFIG_APIS)
|
|
83
|
+
val vaultFiles = root.objList("vaultFiles", MAX_VAULT_FILES)
|
|
77
84
|
val staticPins = root.obj("staticPins")?.let(::staticPins)
|
|
85
|
+
if (release && native == null && !noFileAllowed && (!configApis.isNullOrEmpty() || staticPins != null)) {
|
|
86
|
+
throw BridgeInputException(
|
|
87
|
+
"config: a release build takes its trust anchors only from the app's native security file " +
|
|
88
|
+
"(assets/${NativeSecurity.ASSET_NAME}, README \"Native security file\"); none is shipped. " +
|
|
89
|
+
"An app that accepts JS-only anchors says so in its manifest: meta-data ${NativeSecurity.NO_FILE_META_DATA}=true"
|
|
90
|
+
)
|
|
91
|
+
}
|
|
92
|
+
configApis?.forEach { addConfigApi(builder, it, native, release) }
|
|
93
|
+
vaultFiles?.forEach { addVaultFile(builder, it, tokens, native) }
|
|
78
94
|
SecurityPolicy.staticPins("config.staticPins", staticPins, native)?.let { builder.staticPins(it) }
|
|
79
95
|
|
|
80
96
|
root.int("maxRetryCount", 0, 10)?.let { builder.maxRetryCount(it) }
|
|
81
97
|
root.long("updateIntervalHours", 1, 24L * 30)?.let { builder.updateIntervalHours(it) }
|
|
82
98
|
root.long("updateIntervalMinutes", 15, 60L * 24 * 30)?.let { builder.updateIntervalMinutes(it) }
|
|
83
99
|
root.string("deviceAlias", 128)?.let { builder.deviceAlias(it) }
|
|
84
|
-
root.obj("expiredConfigGrace")?.let { d ->
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
100
|
+
root.obj("expiredConfigGrace")?.let { d ->
|
|
101
|
+
val (amount, unit) = duration(d)
|
|
102
|
+
val ms = unit.toMillis(amount)
|
|
103
|
+
if (release && ms > NativeSecurity.MAX_RELEASE_GRACE_MS) {
|
|
104
|
+
throw BridgeInputException("${d.path}: a release build takes at most 7 days")
|
|
105
|
+
}
|
|
106
|
+
nativeRequire.expiredConfigGraceMs?.let { max ->
|
|
107
|
+
if (ms > max) throw BridgeInputException("${d.path}: longer than the app's native security file allows (${max / 1000} s)")
|
|
108
|
+
}
|
|
109
|
+
builder.expiredConfigGrace(amount, unit)
|
|
110
|
+
} ?: nativeRequire.expiredConfigGraceMs?.let { builder.expiredConfigGrace(it, TimeUnit.MILLISECONDS) }
|
|
111
|
+
// `require` hosts are added to JS's: JS can name more, never fewer.
|
|
112
|
+
val caTrust = (root.stringList("requireCaTrust", 64, 255) ?: emptyList()) + (nativeRequire.requireCaTrust ?: emptyList())
|
|
113
|
+
if (caTrust.isNotEmpty()) builder.requireCaTrust(*caTrust.distinct().toTypedArray())
|
|
114
|
+
if (root.bool("wipeVaultFilesOnRevocation") == true || nativeRequire.wipeVaultFilesOnRevocation) builder.wipeVaultFilesOnRevocation()
|
|
115
|
+
root.obj("vaultFileMaxOfflineAge")?.let { d ->
|
|
116
|
+
val (amount, unit) = duration(d)
|
|
117
|
+
nativeRequire.vaultFileMaxOfflineAgeMs?.let { max ->
|
|
118
|
+
val ms = unit.toMillis(amount)
|
|
119
|
+
// 0 = no limit: longer than any.
|
|
120
|
+
if (ms == 0L || ms > max) throw BridgeInputException("${d.path}: longer than the app's native security file allows (${max / 1000} s)")
|
|
121
|
+
}
|
|
122
|
+
builder.vaultFileMaxOfflineAge(amount, unit)
|
|
123
|
+
} ?: nativeRequire.vaultFileMaxOfflineAgeMs?.let { builder.vaultFileMaxOfflineAge(it, TimeUnit.MILLISECONDS) }
|
|
124
|
+
val requireUnlocked = root.bool("requireUnlockedDevice") == true || nativeRequire.requireUnlockedDevice
|
|
125
|
+
if (root.bool("requireHardwareBackedKeys") == true || nativeRequire.requireHardwareBackedKeys) builder.requireHardwareBackedKeys()
|
|
126
|
+
if (root.bool("managedTrustRoots") == true || nativeRequire.managedTrustRoots) builder.managedTrustRoots()
|
|
127
|
+
val signers = root.stringList("expectedSignerSha256", 16, 128)
|
|
128
|
+
nativeRequire.expectedSignerSha256?.let { fixed ->
|
|
129
|
+
if (signers != null && !NativeSecurity.sameKeys(signers.map(String::lowercase), fixed.map(String::lowercase))) {
|
|
130
|
+
throw BridgeInputException("config.expectedSignerSha256: differs from the app's native security file (${native?.source}); the native value is fixed")
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
(nativeRequire.expectedSignerSha256 ?: signers)?.let { builder.expectedSignerSha256(it) }
|
|
92
134
|
|
|
93
135
|
var guardTimeout: Long? = null
|
|
94
136
|
root.obj("environmentGuard")?.let { g ->
|
|
@@ -109,7 +151,8 @@ internal object ConfigParser {
|
|
|
109
151
|
keepCookies = a.bool("keepReactNativeCookies") == true
|
|
110
152
|
a.finish()
|
|
111
153
|
}
|
|
112
|
-
|
|
154
|
+
// On by default in a release build: RN's https must go through PinVault there.
|
|
155
|
+
val requirePinned = root.bool("requirePinnedReactNativeNetworking") ?: (release && pinGlobal)
|
|
113
156
|
if (requirePinned && !pinGlobal) {
|
|
114
157
|
throw BridgeInputException("config.requirePinnedReactNativeNetworking: contradicts android.pinGlobalNetworking: false")
|
|
115
158
|
}
|
|
@@ -206,11 +249,34 @@ internal object ConfigParser {
|
|
|
206
249
|
val enrollmentUrl = httpsUrl(b, "enrollmentUrl")
|
|
207
250
|
val threshold = b.double("clientCertRenewalThreshold", 0.0, 1.0)
|
|
208
251
|
val disableRenewal = b.bool("disableClientCertRenewal") == true
|
|
209
|
-
val
|
|
252
|
+
val jsAttestation = b.bool("attestation")
|
|
210
253
|
val attestationInterval = b.obj("attestationInterval")?.let(::duration)
|
|
211
|
-
val
|
|
254
|
+
val jsTokenHosts = b.stringList("tokenHosts", 64, 255)
|
|
255
|
+
val jsProof = b.bool("proofOfPossession")
|
|
212
256
|
b.finish()
|
|
213
257
|
|
|
258
|
+
// Where the block talks to and who gets its token and identity: fixed where the file says.
|
|
259
|
+
val nativeBlock = native?.blocks?.get(id)
|
|
260
|
+
fun fixed(key: String): Nothing =
|
|
261
|
+
throw BridgeInputException("${b.path}.$key: differs from the app's native security file (${native?.source}); the native value is fixed")
|
|
262
|
+
val blockUrl = nativeBlock?.url?.also { if (it != url) fixed("url") } ?: url
|
|
263
|
+
val fixedEnrollmentUrl = nativeBlock?.enrollmentUrl?.also { if (enrollmentUrl != null && it != enrollmentUrl) fixed("enrollmentUrl") } ?: enrollmentUrl
|
|
264
|
+
val fixedRenewalUrl = nativeBlock?.renewalUrl?.also { if (renewalUrl != null && it != renewalUrl) fixed("renewalUrl") } ?: renewalUrl
|
|
265
|
+
if (nativeBlock?.attestation == true && jsAttestation == false) {
|
|
266
|
+
throw BridgeInputException("${b.path}.attestation: the app's native security file turns it on; JS cannot turn it off")
|
|
267
|
+
}
|
|
268
|
+
val attestation = jsAttestation == true || nativeBlock?.attestation == true
|
|
269
|
+
if (nativeBlock?.proofOfPossession == true && jsProof == false) {
|
|
270
|
+
throw BridgeInputException("${b.path}.proofOfPossession: the app's native security file turns it on; JS cannot turn it off")
|
|
271
|
+
}
|
|
272
|
+
val proof = jsProof == true || nativeBlock?.proofOfPossession == true
|
|
273
|
+
val tokenHosts = nativeBlock?.tokenHosts?.also { n ->
|
|
274
|
+
if (jsTokenHosts != null && !NativeSecurity.sameKeys(jsTokenHosts.map(String::lowercase), n.map(String::lowercase))) fixed("tokenHosts")
|
|
275
|
+
} ?: jsTokenHosts
|
|
276
|
+
val certHosts = nativeBlock?.clientCertHosts?.also { n ->
|
|
277
|
+
if (clientCertHosts != null && !NativeSecurity.sameKeys(clientCertHosts.map(String::lowercase), n.map(String::lowercase))) fixed("clientCertHosts")
|
|
278
|
+
} ?: clientCertHosts
|
|
279
|
+
|
|
214
280
|
// The trust anchors and relaxations: the native security file decides, JS only repeats.
|
|
215
281
|
val sec = SecurityPolicy.apply(
|
|
216
282
|
b.path, id,
|
|
@@ -221,7 +287,7 @@ internal object ConfigParser {
|
|
|
221
287
|
native, release,
|
|
222
288
|
)
|
|
223
289
|
|
|
224
|
-
builder.configApi(id,
|
|
290
|
+
builder.configApi(id, blockUrl) {
|
|
225
291
|
sec.bootstrapPins?.let { this.bootstrapPins(it) }
|
|
226
292
|
configEndpoint?.let { this.configEndpoint(it) }
|
|
227
293
|
healthEndpoint?.let { this.healthEndpoint(it) }
|
|
@@ -236,36 +302,66 @@ internal object ConfigParser {
|
|
|
236
302
|
if (sec.allowServerKey) this.allowServerGeneratedKey()
|
|
237
303
|
sec.clientCaPins?.let { this.clientCaPins(it) }
|
|
238
304
|
maxLifetime?.let { this.maxClientCertLifetimeDays(it) }
|
|
239
|
-
|
|
305
|
+
certHosts?.let { this.clientCertHosts(it) }
|
|
240
306
|
enrollmentEndpoint?.let { this.enrollmentEndpoint(it) }
|
|
241
307
|
clientCertEndpoint?.let { this.clientCertEndpoint(it) }
|
|
242
308
|
vaultReportEndpoint?.let { this.vaultReportEndpoint(it) }
|
|
243
309
|
clientCertLabel?.let { this.clientCertLabel(it) }
|
|
244
310
|
wantPinsFor?.let { this.wantPinsFor(*it.toTypedArray()) }
|
|
245
|
-
|
|
246
|
-
|
|
311
|
+
fixedRenewalUrl?.let { this.renewalUrl(it) }
|
|
312
|
+
fixedEnrollmentUrl?.let { this.enrollmentUrl(it) }
|
|
247
313
|
threshold?.let { this.clientCertRenewalThreshold(it) }
|
|
248
314
|
if (disableRenewal) this.disableClientCertRenewal()
|
|
249
315
|
if (attestation) this.attestation()
|
|
250
316
|
attestationInterval?.let { this.attestationInterval(it.first, it.second) }
|
|
251
317
|
tokenHosts?.let { this.tokenHosts(it) }
|
|
318
|
+
if (proof) this.proofOfPossession()
|
|
252
319
|
}
|
|
253
320
|
}
|
|
254
321
|
|
|
255
|
-
private fun addVaultFile(builder: PinVaultConfig.Builder, f: Fields, tokens: VaultTokenStore) {
|
|
322
|
+
private fun addVaultFile(builder: PinVaultConfig.Builder, f: Fields, tokens: VaultTokenStore, native: NativeSecurity?) {
|
|
256
323
|
val key = f.requireString("key", 128)
|
|
324
|
+
// The same rule as the library's (the key names the stored copy's file).
|
|
325
|
+
if (!VAULT_KEY.matches(key) || key.all { it == '.' }) {
|
|
326
|
+
throw BridgeInputException("${f.path}.key: must match [A-Za-z0-9._-]{1,64} and not be only dots")
|
|
327
|
+
}
|
|
257
328
|
val endpoint = f.requireString("endpoint", 512)
|
|
258
|
-
val
|
|
329
|
+
val jsSignatureKey = f.string("signaturePublicKey", KEY_LENGTH, multiline = true)
|
|
259
330
|
val updateWithPins = f.bool("updateWithPins")
|
|
260
331
|
val storage = f.enum("storage", StorageStrategy.values())
|
|
261
332
|
val configApi = f.string("configApi", 128)
|
|
262
333
|
val policy = f.enum("accessPolicy", VaultFileAccessPolicy.values())
|
|
263
|
-
val
|
|
264
|
-
val
|
|
334
|
+
val jsEncryption = f.enum("encryption", VaultFileEncryption.values())
|
|
335
|
+
val jsUserAuth = f.enum("userAuth", UserAuth.values())
|
|
265
336
|
val maxOfflineAge = f.obj("maxOfflineAge")?.let(::duration)
|
|
266
337
|
val wipeWhenStale = f.bool("wipeWhenStale") == true
|
|
267
338
|
f.finish()
|
|
268
339
|
|
|
340
|
+
// A file the native security file names keeps its signature key, encryption and lock.
|
|
341
|
+
val declared = native?.vaultFiles?.let { files ->
|
|
342
|
+
files[key] ?: throw BridgeInputException("${f.path}: vault file '$key' is not declared in the app's native security file (${native.source})")
|
|
343
|
+
}
|
|
344
|
+
fun fixed(field: String): Nothing =
|
|
345
|
+
throw BridgeInputException("${f.path}.$field: differs from the app's native security file (${native?.source}); the native value is fixed")
|
|
346
|
+
val signatureKey = declared?.signaturePublicKey?.also { n ->
|
|
347
|
+
if (jsSignatureKey != null && jsSignatureKey.trim() != n.trim()) fixed("signaturePublicKey")
|
|
348
|
+
} ?: jsSignatureKey
|
|
349
|
+
val encryption = declared?.encryption?.also { if (jsEncryption != null && jsEncryption != it) fixed("encryption") } ?: jsEncryption
|
|
350
|
+
val userAuth = declared?.userAuth?.also { if (jsUserAuth != null && jsUserAuth != it) fixed("userAuth") } ?: jsUserAuth
|
|
351
|
+
// The tighter of the file's own cap and the config-wide one: a per-file value
|
|
352
|
+
// overrides the config's, so it must not escape the config-wide cap either.
|
|
353
|
+
val cap = listOfNotNull(declared?.maxOfflineAgeMs, native?.require?.vaultFileMaxOfflineAgeMs).minOrNull()
|
|
354
|
+
cap?.let { max ->
|
|
355
|
+
maxOfflineAge?.let { (amount, unit) ->
|
|
356
|
+
val ms = unit.toMillis(amount)
|
|
357
|
+
// 0 = no limit: longer than any.
|
|
358
|
+
if (ms == 0L || ms > max) {
|
|
359
|
+
throw BridgeInputException("${f.path}.maxOfflineAge: longer than the app's native security file allows (${max / 1000} s)")
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
val offlineAge = maxOfflineAge ?: declared?.maxOfflineAgeMs?.let { it to TimeUnit.MILLISECONDS }
|
|
364
|
+
|
|
269
365
|
builder.vaultFile(key) {
|
|
270
366
|
this.endpoint(endpoint)
|
|
271
367
|
signatureKey?.let { this.signaturePublicKey(it) }
|
|
@@ -279,7 +375,7 @@ internal object ConfigParser {
|
|
|
279
375
|
}
|
|
280
376
|
encryption?.let { this.encryption(it) }
|
|
281
377
|
userAuth?.let { this.userAuth(it) }
|
|
282
|
-
|
|
378
|
+
offlineAge?.let { this.maxOfflineAge(it.first, it.second) }
|
|
283
379
|
if (wipeWhenStale) this.wipeWhenStale()
|
|
284
380
|
}
|
|
285
381
|
}
|
|
@@ -4,6 +4,8 @@ import android.content.Context
|
|
|
4
4
|
import android.content.pm.ApplicationInfo
|
|
5
5
|
import io.github.umutcansu.pinvault.model.CertificateConfig
|
|
6
6
|
import io.github.umutcansu.pinvault.model.HostPin
|
|
7
|
+
import io.github.umutcansu.pinvault.model.UserAuth
|
|
8
|
+
import io.github.umutcansu.pinvault.model.VaultFileEncryption
|
|
7
9
|
import java.io.FileNotFoundException
|
|
8
10
|
import java.io.IOException
|
|
9
11
|
|
|
@@ -22,9 +24,18 @@ import java.io.IOException
|
|
|
22
24
|
* "signaturePublicKeys": ["…", "…"], "requiredSignatures": 2,
|
|
23
25
|
* "recoveryPublicKeys": ["…"], "requiredRecoverySignatures": 1,
|
|
24
26
|
* "serverScope": "default-tls", "clientCaPins": ["…"],
|
|
27
|
+
* "url": "https://config.example.com/", "attestation": true,
|
|
28
|
+
* "tokenHosts": ["api.example.com"], "proofOfPossession": true, "clientCertHosts": ["api.example.com:443"],
|
|
29
|
+
* "enrollmentUrl": "https://enroll.example.com/", "renewalUrl": "https://renew.example.com/",
|
|
25
30
|
* "allowUnsigned": false, "allowUnpinnedConfigApi": false, "allowServerGeneratedKey": false
|
|
26
31
|
* }],
|
|
27
|
-
* "staticPins": { "pins": [ … ], "version": 1 }
|
|
32
|
+
* "staticPins": { "pins": [ … ], "version": 1 },
|
|
33
|
+
* "require": {
|
|
34
|
+
* "requireUnlockedDevice": true, "requireHardwareBackedKeys": true, "managedTrustRoots": true,
|
|
35
|
+
* "wipeVaultFilesOnRevocation": true, "requireCaTrust": ["api.example.com"],
|
|
36
|
+
* "expectedSignerSha256": ["…"], "expiredConfigGraceSeconds": 0, "vaultFileMaxOfflineAgeSeconds": 604800
|
|
37
|
+
* },
|
|
38
|
+
* "vaultFiles": [{ "key": "statement", "signaturePublicKey": "…", "encryption": "USER_AUTH", "userAuth": "REQUIRED", "maxOfflineAgeSeconds": 86400 }]
|
|
28
39
|
* }
|
|
29
40
|
* ```
|
|
30
41
|
*
|
|
@@ -35,18 +46,57 @@ import java.io.IOException
|
|
|
35
46
|
* - a field the file declares is the value: JS may leave it out or repeat it,
|
|
36
47
|
* a different value is refused (`E_INVALID_CONFIG`);
|
|
37
48
|
* - `allowUnsigned`, `allowUnpinnedConfigApi`, `allowServerGeneratedKey` from
|
|
38
|
-
* JS are refused unless the file allows them for that block
|
|
49
|
+
* JS are refused unless the file allows them for that block;
|
|
50
|
+
* - `url`, `tokenHosts` and `clientCertHosts` of a block are fixed where the
|
|
51
|
+
* file gives them, and `attestation: true` / `proofOfPossession: true` there
|
|
52
|
+
* cannot be switched off;
|
|
53
|
+
* - `require` only tightens: a protection it turns on stays on whatever JS
|
|
54
|
+
* says, `requireCaTrust` hosts are added to JS's, `expectedSignerSha256` is
|
|
55
|
+
* fixed, and `expiredConfigGraceSeconds` is the most JS may ask for;
|
|
56
|
+
* - `vaultFiles` fixes those files' signature key, encryption and screen lock;
|
|
57
|
+
* with the section present, a JS vault file it does not name is refused.
|
|
39
58
|
*
|
|
40
|
-
* Without the file, release builds (the app is not debuggable) refuse
|
|
41
|
-
*
|
|
59
|
+
* Without the file, release builds (the app is not debuggable) refuse to start
|
|
60
|
+
* a config with Config APIs or static pins: the trust anchors would come from
|
|
61
|
+
* JS alone. An app that accepts that says so natively, in its manifest
|
|
62
|
+
* (`<meta-data android:name="io.github.umutcansu.pinvault.ALLOW_NO_NATIVE_SECURITY_FILE" android:value="true"/>`);
|
|
63
|
+
* the three relaxations stay refused from JS then. Debug builds take
|
|
64
|
+
* everything as before. Whatever the file says, a release build caps
|
|
65
|
+
* `expiredConfigGrace` at [MAX_RELEASE_GRACE_MS].
|
|
42
66
|
*/
|
|
43
67
|
internal class NativeSecurity(
|
|
44
68
|
/** Where the file came from, for messages. */
|
|
45
69
|
val source: String,
|
|
46
70
|
val blocks: Map<String, Block>,
|
|
47
71
|
val staticPins: CertificateConfig?,
|
|
72
|
+
val require: Require = Require(),
|
|
73
|
+
/** Null when the file has no `vaultFiles` section. */
|
|
74
|
+
val vaultFiles: Map<String, VaultFilePolicy>? = null,
|
|
48
75
|
) {
|
|
49
76
|
|
|
77
|
+
/** The `require` section: protections JS cannot switch off. */
|
|
78
|
+
class Require(
|
|
79
|
+
val requireUnlockedDevice: Boolean = false,
|
|
80
|
+
val requireHardwareBackedKeys: Boolean = false,
|
|
81
|
+
val managedTrustRoots: Boolean = false,
|
|
82
|
+
val wipeVaultFilesOnRevocation: Boolean = false,
|
|
83
|
+
val requireCaTrust: List<String>? = null,
|
|
84
|
+
val expectedSignerSha256: List<String>? = null,
|
|
85
|
+
/** The most `expiredConfigGrace` JS may set; null = only the release cap. */
|
|
86
|
+
val expiredConfigGraceMs: Long? = null,
|
|
87
|
+
/** The most `vaultFileMaxOfflineAge` JS may set (and the value when JS sets none); null = no rule. */
|
|
88
|
+
val vaultFileMaxOfflineAgeMs: Long? = null,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
/** One `vaultFiles` entry: what JS cannot change about that file. */
|
|
92
|
+
class VaultFilePolicy(
|
|
93
|
+
val signaturePublicKey: String?,
|
|
94
|
+
val encryption: VaultFileEncryption?,
|
|
95
|
+
val userAuth: UserAuth?,
|
|
96
|
+
/** The most `maxOfflineAge` JS may give this file (and the value when JS gives none). */
|
|
97
|
+
val maxOfflineAgeMs: Long? = null,
|
|
98
|
+
)
|
|
99
|
+
|
|
50
100
|
class Block(
|
|
51
101
|
val bootstrapPins: List<HostPin>?,
|
|
52
102
|
val signaturePublicKeys: List<String>?,
|
|
@@ -58,10 +108,25 @@ internal class NativeSecurity(
|
|
|
58
108
|
val allowUnsigned: Boolean,
|
|
59
109
|
val allowUnpinnedConfigApi: Boolean,
|
|
60
110
|
val allowServerGeneratedKey: Boolean,
|
|
111
|
+
val url: String? = null,
|
|
112
|
+
val attestation: Boolean = false,
|
|
113
|
+
val tokenHosts: List<String>? = null,
|
|
114
|
+
val proofOfPossession: Boolean = false,
|
|
115
|
+
val clientCertHosts: List<String>? = null,
|
|
116
|
+
val enrollmentUrl: String? = null,
|
|
117
|
+
val renewalUrl: String? = null,
|
|
61
118
|
)
|
|
62
119
|
|
|
63
120
|
companion object {
|
|
64
121
|
const val ASSET_NAME = "pinvault_security.json"
|
|
122
|
+
const val NO_FILE_META_DATA = "io.github.umutcansu.pinvault.ALLOW_NO_NATIVE_SECURITY_FILE"
|
|
123
|
+
/** The longest `expiredConfigGrace` a release build takes: an expired pin set must not live on for months. */
|
|
124
|
+
const val MAX_RELEASE_GRACE_MS = 7L * 24 * 3600 * 1000
|
|
125
|
+
private const val MAX_OFFLINE_AGE_S = 10L * 365 * 24 * 3600
|
|
126
|
+
|
|
127
|
+
private fun httpsOnly(b: Fields, key: String): String? = b.string(key, 2048)?.also {
|
|
128
|
+
if (!it.startsWith("https://")) throw BridgeInputException("${b.path}.$key: must be an https:// URL")
|
|
129
|
+
}
|
|
65
130
|
private const val MAX_FILE_CHARS = 256 * 1024
|
|
66
131
|
|
|
67
132
|
/** The file from the app's assets; null when the app ships none. A broken file throws [BridgeInputException]. */
|
|
@@ -77,6 +142,15 @@ internal class NativeSecurity(
|
|
|
77
142
|
return parse(text, "assets/$ASSET_NAME")
|
|
78
143
|
}
|
|
79
144
|
|
|
145
|
+
/** True when the app's manifest says it starts without a native security file in release. */
|
|
146
|
+
fun noFileAllowed(context: Context): Boolean = try {
|
|
147
|
+
@Suppress("DEPRECATION")
|
|
148
|
+
context.packageManager.getApplicationInfo(context.packageName, android.content.pm.PackageManager.GET_META_DATA)
|
|
149
|
+
.metaData?.getBoolean(NO_FILE_META_DATA, false) == true
|
|
150
|
+
} catch (_: Exception) {
|
|
151
|
+
false
|
|
152
|
+
}
|
|
153
|
+
|
|
80
154
|
/** True for a release build of the app (not `android:debuggable`). */
|
|
81
155
|
fun isReleaseBuild(context: Context): Boolean =
|
|
82
156
|
(context.applicationInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE) == 0
|
|
@@ -103,15 +177,56 @@ internal class NativeSecurity(
|
|
|
103
177
|
allowUnsigned = b.bool("allowUnsigned") == true,
|
|
104
178
|
allowUnpinnedConfigApi = b.bool("allowUnpinnedConfigApi") == true,
|
|
105
179
|
allowServerGeneratedKey = b.bool("allowServerGeneratedKey") == true,
|
|
180
|
+
url = httpsOnly(b, "url"),
|
|
181
|
+
attestation = b.bool("attestation") == true,
|
|
182
|
+
tokenHosts = b.stringList("tokenHosts", 64, 255),
|
|
183
|
+
proofOfPossession = b.bool("proofOfPossession") == true,
|
|
184
|
+
clientCertHosts = b.stringList("clientCertHosts", 64, 2048),
|
|
185
|
+
enrollmentUrl = httpsOnly(b, "enrollmentUrl"),
|
|
186
|
+
renewalUrl = httpsOnly(b, "renewalUrl"),
|
|
106
187
|
)
|
|
107
188
|
b.finish()
|
|
108
189
|
}
|
|
109
190
|
val staticPins = root.obj("staticPins")?.let(ConfigParser::staticPins)
|
|
191
|
+
val require = root.obj("require")?.let { r ->
|
|
192
|
+
Require(
|
|
193
|
+
requireUnlockedDevice = r.bool("requireUnlockedDevice") == true,
|
|
194
|
+
requireHardwareBackedKeys = r.bool("requireHardwareBackedKeys") == true,
|
|
195
|
+
managedTrustRoots = r.bool("managedTrustRoots") == true,
|
|
196
|
+
wipeVaultFilesOnRevocation = r.bool("wipeVaultFilesOnRevocation") == true,
|
|
197
|
+
requireCaTrust = r.stringList("requireCaTrust", 64, 255),
|
|
198
|
+
expectedSignerSha256 = r.stringList("expectedSignerSha256", 16, 128),
|
|
199
|
+
expiredConfigGraceMs = r.long("expiredConfigGraceSeconds", 0, MAX_RELEASE_GRACE_MS / 1000)?.let { it * 1000 },
|
|
200
|
+
vaultFileMaxOfflineAgeMs = r.long("vaultFileMaxOfflineAgeSeconds", 1, MAX_OFFLINE_AGE_S)?.let { it * 1000 },
|
|
201
|
+
).also {
|
|
202
|
+
// iOS-only (screen-lock strength): read for the shape, used by the iOS side.
|
|
203
|
+
r.string("userAuthStrength", 32)
|
|
204
|
+
// iOS-only (bundle and team ids): read for the shape, used by the iOS side.
|
|
205
|
+
r.stringList("expectedBundleIds", 16, 255)
|
|
206
|
+
r.stringList("expectedTeamIds", 16, 32)
|
|
207
|
+
r.finish()
|
|
208
|
+
}
|
|
209
|
+
} ?: Require()
|
|
210
|
+
val vaultFiles = root.objList("vaultFiles", ConfigParser.MAX_VAULT_FILES)?.let { list ->
|
|
211
|
+
val map = LinkedHashMap<String, VaultFilePolicy>()
|
|
212
|
+
list.forEach { f ->
|
|
213
|
+
val key = f.requireString("key", 128)
|
|
214
|
+
if (key in map) throw BridgeInputException("${f.path}.key: '$key' is declared twice")
|
|
215
|
+
map[key] = VaultFilePolicy(
|
|
216
|
+
signaturePublicKey = f.string("signaturePublicKey", ConfigParser.KEY_LENGTH, multiline = true),
|
|
217
|
+
encryption = f.enum("encryption", VaultFileEncryption.values()),
|
|
218
|
+
userAuth = f.enum("userAuth", UserAuth.values()),
|
|
219
|
+
maxOfflineAgeMs = f.long("maxOfflineAgeSeconds", 1, MAX_OFFLINE_AGE_S)?.let { it * 1000 },
|
|
220
|
+
)
|
|
221
|
+
f.finish()
|
|
222
|
+
}
|
|
223
|
+
map
|
|
224
|
+
}
|
|
110
225
|
root.finish()
|
|
111
226
|
if (blocks.isEmpty() && staticPins == null) {
|
|
112
227
|
throw BridgeInputException("$source: declares neither configApis nor staticPins")
|
|
113
228
|
}
|
|
114
|
-
return NativeSecurity(source, blocks, staticPins)
|
|
229
|
+
return NativeSecurity(source, blocks, staticPins, require, vaultFiles)
|
|
115
230
|
} catch (e: BridgeInputException) {
|
|
116
231
|
val message = e.message ?: ""
|
|
117
232
|
throw BridgeInputException(if (message.startsWith("native security file")) message else "native security file $message")
|