@umutcansu/react-native-pinvault 0.0.0-stage → 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 +84 -0
- package/LICENSE +21 -0
- package/README.md +437 -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 +396 -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 +354 -0
- package/android/src/main/java/io/github/umutcansu/pinvault/reactnative/PinVaultModule.kt +419 -0
- package/android/src/main/java/io/github/umutcansu/pinvault/reactnative/PinVaultNetworking.kt +379 -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 +437 -0
- package/ios/Core/JSEnvironmentGuard.swift +129 -0
- package/ios/Core/NativeSecurity.swift +303 -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 +424 -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 +331 -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 +359 -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 +398 -0
- package/src/types.ts +362 -0
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
package io.github.umutcansu.pinvault.reactnative
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.content.pm.ApplicationInfo
|
|
5
|
+
import io.github.umutcansu.pinvault.model.CertificateConfig
|
|
6
|
+
import io.github.umutcansu.pinvault.model.HostPin
|
|
7
|
+
import io.github.umutcansu.pinvault.model.UserAuth
|
|
8
|
+
import io.github.umutcansu.pinvault.model.VaultFileEncryption
|
|
9
|
+
import java.io.FileNotFoundException
|
|
10
|
+
import java.io.IOException
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* The app's native security file: the trust anchors and relaxations of
|
|
14
|
+
* `start(config)` declared outside the JS bundle, which an OTA update
|
|
15
|
+
* (CodePush, Expo Updates) or an edited bundle can change. The file ships in
|
|
16
|
+
* the APK (`src/main/assets/pinvault_security.json`, or a build type's own
|
|
17
|
+
* `src/release/assets/`), which the APK signature seals.
|
|
18
|
+
*
|
|
19
|
+
* ```json
|
|
20
|
+
* {
|
|
21
|
+
* "configApis": [{
|
|
22
|
+
* "id": "default-tls",
|
|
23
|
+
* "bootstrapPins": [{ "hostname": "api.example.com", "sha256": ["…", "…"] }],
|
|
24
|
+
* "signaturePublicKeys": ["…", "…"], "requiredSignatures": 2,
|
|
25
|
+
* "recoveryPublicKeys": ["…"], "requiredRecoverySignatures": 1,
|
|
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/",
|
|
30
|
+
* "allowUnsigned": false, "allowUnpinnedConfigApi": false, "allowServerGeneratedKey": false
|
|
31
|
+
* }],
|
|
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 }]
|
|
39
|
+
* }
|
|
40
|
+
* ```
|
|
41
|
+
*
|
|
42
|
+
* When the file is there ([apply]):
|
|
43
|
+
* - every Config API of the JS config must be declared in it (an OTA update
|
|
44
|
+
* cannot add a block with keys of its own), and JS `staticPins` must be
|
|
45
|
+
* declared too;
|
|
46
|
+
* - a field the file declares is the value: JS may leave it out or repeat it,
|
|
47
|
+
* a different value is refused (`E_INVALID_CONFIG`);
|
|
48
|
+
* - `allowUnsigned`, `allowUnpinnedConfigApi`, `allowServerGeneratedKey` from
|
|
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.
|
|
58
|
+
*
|
|
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].
|
|
66
|
+
*/
|
|
67
|
+
internal class NativeSecurity(
|
|
68
|
+
/** Where the file came from, for messages. */
|
|
69
|
+
val source: String,
|
|
70
|
+
val blocks: Map<String, Block>,
|
|
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,
|
|
75
|
+
) {
|
|
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
|
+
|
|
100
|
+
class Block(
|
|
101
|
+
val bootstrapPins: List<HostPin>?,
|
|
102
|
+
val signaturePublicKeys: List<String>?,
|
|
103
|
+
val requiredSignatures: Int?,
|
|
104
|
+
val recoveryPublicKeys: List<String>?,
|
|
105
|
+
val requiredRecoverySignatures: Int?,
|
|
106
|
+
val serverScope: String?,
|
|
107
|
+
val clientCaPins: List<String>?,
|
|
108
|
+
val allowUnsigned: Boolean,
|
|
109
|
+
val allowUnpinnedConfigApi: Boolean,
|
|
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,
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
companion object {
|
|
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
|
+
}
|
|
130
|
+
private const val MAX_FILE_CHARS = 256 * 1024
|
|
131
|
+
|
|
132
|
+
/** The file from the app's assets; null when the app ships none. A broken file throws [BridgeInputException]. */
|
|
133
|
+
fun load(context: Context): NativeSecurity? {
|
|
134
|
+
val text = try {
|
|
135
|
+
context.assets.open(ASSET_NAME).bufferedReader(Charsets.UTF_8).use { it.readText() }
|
|
136
|
+
} catch (_: FileNotFoundException) {
|
|
137
|
+
return null
|
|
138
|
+
} catch (e: IOException) {
|
|
139
|
+
// There, but unreadable: fail closed.
|
|
140
|
+
throw BridgeInputException("native security file assets/$ASSET_NAME: cannot be read (${e.javaClass.simpleName})")
|
|
141
|
+
}
|
|
142
|
+
return parse(text, "assets/$ASSET_NAME")
|
|
143
|
+
}
|
|
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
|
+
|
|
154
|
+
/** True for a release build of the app (not `android:debuggable`). */
|
|
155
|
+
fun isReleaseBuild(context: Context): Boolean =
|
|
156
|
+
(context.applicationInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE) == 0
|
|
157
|
+
|
|
158
|
+
fun parse(text: String, source: String): NativeSecurity {
|
|
159
|
+
val root = try {
|
|
160
|
+
StrictJson.parseObject(text, source, maxChars = MAX_FILE_CHARS)
|
|
161
|
+
} catch (e: BridgeInputException) {
|
|
162
|
+
throw BridgeInputException("native security file ${e.message}")
|
|
163
|
+
}
|
|
164
|
+
val blocks = LinkedHashMap<String, Block>()
|
|
165
|
+
try {
|
|
166
|
+
root.objList("configApis", ConfigParser.MAX_CONFIG_APIS)?.forEach { b ->
|
|
167
|
+
val id = b.requireString("id", 128)
|
|
168
|
+
if (id in blocks) throw BridgeInputException("${b.path}.id: '$id' is declared twice")
|
|
169
|
+
blocks[id] = Block(
|
|
170
|
+
bootstrapPins = b.objList("bootstrapPins", ConfigParser.MAX_PINS)?.map(ConfigParser::hostPin),
|
|
171
|
+
signaturePublicKeys = b.stringList("signaturePublicKeys", 16, ConfigParser.KEY_LENGTH, multiline = true),
|
|
172
|
+
requiredSignatures = b.int("requiredSignatures", 1, 16),
|
|
173
|
+
recoveryPublicKeys = b.stringList("recoveryPublicKeys", 16, ConfigParser.KEY_LENGTH, multiline = true),
|
|
174
|
+
requiredRecoverySignatures = b.int("requiredRecoverySignatures", 1, 16),
|
|
175
|
+
serverScope = b.string("serverScope", 128),
|
|
176
|
+
clientCaPins = b.stringList("clientCaPins", 16, 128),
|
|
177
|
+
allowUnsigned = b.bool("allowUnsigned") == true,
|
|
178
|
+
allowUnpinnedConfigApi = b.bool("allowUnpinnedConfigApi") == true,
|
|
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"),
|
|
187
|
+
)
|
|
188
|
+
b.finish()
|
|
189
|
+
}
|
|
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
|
+
}
|
|
225
|
+
root.finish()
|
|
226
|
+
if (blocks.isEmpty() && staticPins == null) {
|
|
227
|
+
throw BridgeInputException("$source: declares neither configApis nor staticPins")
|
|
228
|
+
}
|
|
229
|
+
return NativeSecurity(source, blocks, staticPins, require, vaultFiles)
|
|
230
|
+
} catch (e: BridgeInputException) {
|
|
231
|
+
val message = e.message ?: ""
|
|
232
|
+
throw BridgeInputException(if (message.startsWith("native security file")) message else "native security file $message")
|
|
233
|
+
} catch (e: IllegalArgumentException) {
|
|
234
|
+
// A builder / constructor rule (two pins per host, …).
|
|
235
|
+
throw BridgeInputException("native security file $source: ${e.message}")
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// ── Comparison (order of lists does not matter) ─────────────────────
|
|
240
|
+
|
|
241
|
+
fun sameKeys(a: List<String>, b: List<String>): Boolean = a.map(String::trim).toSet() == b.map(String::trim).toSet()
|
|
242
|
+
|
|
243
|
+
private fun pinKey(p: HostPin) =
|
|
244
|
+
listOf(p.hostname.lowercase(), p.sha256.sorted().joinToString(","), p.version, p.forceUpdate, p.mtls, p.clientCertVersion)
|
|
245
|
+
.joinToString("|")
|
|
246
|
+
|
|
247
|
+
fun samePins(a: List<HostPin>, b: List<HostPin>): Boolean = a.map(::pinKey).sorted() == b.map(::pinKey).sorted()
|
|
248
|
+
|
|
249
|
+
fun sameStaticPins(a: CertificateConfig, b: CertificateConfig): Boolean =
|
|
250
|
+
a.version == b.version && a.forceUpdate == b.forceUpdate && samePins(a.pins, b.pins)
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/** What JS asked for in one `configApis[i]` entry, before the native policy is applied. */
|
|
255
|
+
internal class SecurityFields(
|
|
256
|
+
val bootstrapPins: List<HostPin>?,
|
|
257
|
+
/** `signaturePublicKey` (one) or `signaturePublicKeys`; never both. */
|
|
258
|
+
val oneKey: String?,
|
|
259
|
+
val keys: List<String>?,
|
|
260
|
+
val requiredSignatures: Int?,
|
|
261
|
+
val recoveryKeys: List<String>?,
|
|
262
|
+
val requiredRecovery: Int?,
|
|
263
|
+
val serverScope: String?,
|
|
264
|
+
val clientCaPins: List<String>?,
|
|
265
|
+
val allowUnsigned: Boolean,
|
|
266
|
+
val allowUnpinned: Boolean,
|
|
267
|
+
val allowServerKey: Boolean,
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
/** The native policy applied to one block: the values that go to the builder, or a refusal. */
|
|
271
|
+
internal object SecurityPolicy {
|
|
272
|
+
|
|
273
|
+
private const val RELAXATIONS_HINT =
|
|
274
|
+
"a release build takes it only from the app's native security file (assets/${NativeSecurity.ASSET_NAME}, README \"Native security file\")"
|
|
275
|
+
|
|
276
|
+
fun apply(path: String, id: String, js: SecurityFields, native: NativeSecurity?, release: Boolean): SecurityFields {
|
|
277
|
+
if (native == null) {
|
|
278
|
+
if (release) {
|
|
279
|
+
if (js.allowUnsigned) throw BridgeInputException("$path.allowUnsigned: refused from JS; $RELAXATIONS_HINT")
|
|
280
|
+
if (js.allowUnpinned) throw BridgeInputException("$path.allowUnpinnedConfigApi: refused from JS; $RELAXATIONS_HINT")
|
|
281
|
+
if (js.allowServerKey) throw BridgeInputException("$path.allowServerGeneratedKey: refused from JS; $RELAXATIONS_HINT")
|
|
282
|
+
}
|
|
283
|
+
return js
|
|
284
|
+
}
|
|
285
|
+
val block = native.blocks[id]
|
|
286
|
+
?: throw BridgeInputException("$path: Config API '$id' is not declared in the app's native security file (${native.source})")
|
|
287
|
+
|
|
288
|
+
fun fixed(key: String): Nothing =
|
|
289
|
+
throw BridgeInputException("$path.$key: differs from the app's native security file (${native.source}); the native value is fixed")
|
|
290
|
+
|
|
291
|
+
fun relaxation(key: String, js: Boolean, allowed: Boolean) {
|
|
292
|
+
if (js && !allowed) throw BridgeInputException("$path.$key: refused; the app's native security file (${native.source}) does not allow it")
|
|
293
|
+
}
|
|
294
|
+
relaxation("allowUnsigned", js.allowUnsigned, block.allowUnsigned)
|
|
295
|
+
relaxation("allowUnpinnedConfigApi", js.allowUnpinned, block.allowUnpinnedConfigApi)
|
|
296
|
+
relaxation("allowServerGeneratedKey", js.allowServerKey, block.allowServerGeneratedKey)
|
|
297
|
+
|
|
298
|
+
val pins = block.bootstrapPins?.also { n ->
|
|
299
|
+
if (js.bootstrapPins != null && !NativeSecurity.samePins(js.bootstrapPins, n)) fixed("bootstrapPins")
|
|
300
|
+
} ?: js.bootstrapPins
|
|
301
|
+
|
|
302
|
+
val oneKey = js.oneKey
|
|
303
|
+
var keys = js.keys
|
|
304
|
+
block.signaturePublicKeys?.let { n ->
|
|
305
|
+
val jsKeys = js.oneKey?.let { listOf(it) } ?: js.keys
|
|
306
|
+
if (jsKeys == null) {
|
|
307
|
+
keys = n
|
|
308
|
+
} else if (!NativeSecurity.sameKeys(jsKeys, n)) {
|
|
309
|
+
fixed(if (js.oneKey != null) "signaturePublicKey" else "signaturePublicKeys")
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
fun <T> one(key: String, nativeValue: T?, jsValue: T?): T? {
|
|
314
|
+
if (nativeValue == null) return jsValue
|
|
315
|
+
if (jsValue != null && jsValue != nativeValue) fixed(key)
|
|
316
|
+
return nativeValue
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
fun list(key: String, nativeValue: List<String>?, jsValue: List<String>?): List<String>? {
|
|
320
|
+
if (nativeValue == null) return jsValue
|
|
321
|
+
if (jsValue != null && !NativeSecurity.sameKeys(jsValue, nativeValue)) fixed(key)
|
|
322
|
+
return nativeValue
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
return SecurityFields(
|
|
326
|
+
bootstrapPins = pins,
|
|
327
|
+
oneKey = oneKey,
|
|
328
|
+
keys = keys,
|
|
329
|
+
requiredSignatures = one("requiredSignatures", block.requiredSignatures, js.requiredSignatures),
|
|
330
|
+
recoveryKeys = list("recoveryPublicKeys", block.recoveryPublicKeys, js.recoveryKeys),
|
|
331
|
+
requiredRecovery = one("requiredRecoverySignatures", block.requiredRecoverySignatures, js.requiredRecovery),
|
|
332
|
+
serverScope = one("serverScope", block.serverScope, js.serverScope),
|
|
333
|
+
clientCaPins = list("clientCaPins", block.clientCaPins, js.clientCaPins),
|
|
334
|
+
// The file is the source of truth for the relaxations too.
|
|
335
|
+
allowUnsigned = block.allowUnsigned,
|
|
336
|
+
allowUnpinned = block.allowUnpinnedConfigApi,
|
|
337
|
+
allowServerKey = block.allowServerGeneratedKey,
|
|
338
|
+
)
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/** JS `staticPins` against the file: declared there or refused; the file's apply when JS has none. */
|
|
342
|
+
fun staticPins(path: String, js: CertificateConfig?, native: NativeSecurity?): CertificateConfig? {
|
|
343
|
+
if (native == null) return js
|
|
344
|
+
val n = native.staticPins
|
|
345
|
+
if (n == null) {
|
|
346
|
+
if (js != null) throw BridgeInputException("$path: not declared in the app's native security file (${native.source})")
|
|
347
|
+
return null
|
|
348
|
+
}
|
|
349
|
+
if (js != null && !NativeSecurity.sameStaticPins(js, n)) {
|
|
350
|
+
throw BridgeInputException("$path: differs from the app's native security file (${native.source}); the native value is fixed")
|
|
351
|
+
}
|
|
352
|
+
return n
|
|
353
|
+
}
|
|
354
|
+
}
|