react-native-ssl-manager 1.0.3 → 1.1.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/README.md +199 -0
- package/android/src/main/java/com/usesslpinning/PinnedOkHttpClient.kt +106 -0
- package/android/ssl-pinning-setup.gradle +185 -8
- package/app.plugin.js +99 -0
- package/lib/NativeUseSslPinning.d.ts +8 -0
- package/lib/NativeUseSslPinning.d.ts.map +1 -0
- package/lib/NativeUseSslPinning.js +4 -0
- package/lib/UseSslPinning.types.d.ts +17 -0
- package/lib/UseSslPinning.types.d.ts.map +1 -0
- package/lib/UseSslPinning.types.js +2 -0
- package/lib/index.d.ts +15 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +58 -0
- package/package.json +1 -11
- package/scripts/nsc-utils.js +82 -0
- package/scripts/postinstall.js +72 -0
package/README.md
CHANGED
|
@@ -223,6 +223,118 @@ Your `ssl_config.json` should follow this structure:
|
|
|
223
223
|
- ❌ **Don't rename** the file - it must be exactly `ssl_config.json`
|
|
224
224
|
- ❌ **Don't place** in subdirectories - must be in project root
|
|
225
225
|
|
|
226
|
+
## Supported Networking Stacks
|
|
227
|
+
|
|
228
|
+
This library provides SSL pinning coverage across both platforms. The table below shows which networking stacks are covered and by which mechanism.
|
|
229
|
+
|
|
230
|
+
| Stack | Platform | Covered | Mechanism |
|
|
231
|
+
|-------|----------|---------|-----------|
|
|
232
|
+
| `fetch` / `axios` (React Native) | iOS | Yes | TrustKit swizzling (`kTSKSwizzleNetworkDelegates`) |
|
|
233
|
+
| `URLSession` (Foundation) | iOS | Yes | TrustKit swizzling |
|
|
234
|
+
| `SDWebImage` | iOS | Yes | TrustKit swizzling (uses URLSession) |
|
|
235
|
+
| `Alamofire` | iOS | Yes | TrustKit swizzling (uses URLSession) |
|
|
236
|
+
| Most URLSession-based libraries | iOS | Yes* | TrustKit swizzling |
|
|
237
|
+
| `fetch` / `axios` (React Native) | Android | Yes | OkHttpClientFactory + Network Security Config |
|
|
238
|
+
| OkHttp (direct) | Android | Yes | Network Security Config |
|
|
239
|
+
| Cronet (`react-native-nitro-fetch`) | Android | Best-effort* | Network Security Config |
|
|
240
|
+
| Android WebView | Android | Yes | Network Security Config |
|
|
241
|
+
| Coil / Ktor with OkHttp engine | Android | Yes | Network Security Config |
|
|
242
|
+
| Glide / OkHttp3 (`react-native-fast-image`) | Android | Yes | Network Security Config |
|
|
243
|
+
| `HttpURLConnection` | Android | Yes | Network Security Config |
|
|
244
|
+
|
|
245
|
+
### How It Works
|
|
246
|
+
|
|
247
|
+
**iOS**: TrustKit is initialized with `kTSKSwizzleNetworkDelegates: true`, which swizzles most `URLSession` delegates at the OS level. This means most libraries that use `URLSession` under the hood (including SDWebImage, Alamofire, and React Native's networking layer) are automatically covered without any additional configuration.
|
|
248
|
+
|
|
249
|
+
**Android**: The library auto-generates `network_security_config.xml` from `ssl_config.json` at build time and patches `AndroidManifest.xml` to reference it. Android's Network Security Config is enforced at the platform level for all networking stacks that use the default `TrustManager` — including OkHttp, WebView, Coil, Glide, and `HttpURLConnection`. Cronet coverage is best-effort and depends on whether Cronet uses the platform default TrustManager.
|
|
250
|
+
|
|
251
|
+
### Known Limitations
|
|
252
|
+
|
|
253
|
+
- **iOS**: Libraries that implement custom TLS stacks (not using `URLSession`) are NOT covered by TrustKit swizzling.
|
|
254
|
+
- **iOS**: Apps with complex custom `URLSessionDelegate` implementations or other method-swizzling libraries may experience conflicts with TrustKit's swizzling. TrustKit's own documentation notes swizzling is designed for "simple apps".
|
|
255
|
+
- **Android**: Libraries that build OkHttp with a custom `TrustManager` that bypasses the system default may bypass Network Security Config.
|
|
256
|
+
- **Android (Cronet)**: Cronet may use its own TLS stack rather than the platform default `TrustManager`, in which case NSC pin-sets are not enforced. For authoritative Cronet pinning, use `CronetEngine.Builder.addPublicKeyPins()` directly.
|
|
257
|
+
|
|
258
|
+
### PinnedOkHttpClient (Android)
|
|
259
|
+
|
|
260
|
+
For native module authors who need a pinned OkHttp client (e.g., custom Glide modules, Ktor engines), the library exposes a public singleton:
|
|
261
|
+
|
|
262
|
+
```kotlin
|
|
263
|
+
import com.usesslpinning.PinnedOkHttpClient
|
|
264
|
+
|
|
265
|
+
// Get a pinned OkHttpClient instance
|
|
266
|
+
val client = PinnedOkHttpClient.getInstance(context)
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
#### Glide Integration
|
|
270
|
+
|
|
271
|
+
```kotlin
|
|
272
|
+
@GlideModule
|
|
273
|
+
class MyAppGlideModule : AppGlideModule() {
|
|
274
|
+
override fun registerComponents(context: Context, glide: Glide, registry: Registry) {
|
|
275
|
+
val client = PinnedOkHttpClient.getInstance(context)
|
|
276
|
+
registry.replace(
|
|
277
|
+
GlideUrl::class.java,
|
|
278
|
+
InputStream::class.java,
|
|
279
|
+
OkHttpUrlLoader.Factory(client)
|
|
280
|
+
)
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
#### Coil Integration
|
|
286
|
+
|
|
287
|
+
```kotlin
|
|
288
|
+
val imageLoader = ImageLoader.Builder(context)
|
|
289
|
+
.okHttpClient { PinnedOkHttpClient.getInstance(context) }
|
|
290
|
+
.build()
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
#### Ktor OkHttp Engine
|
|
294
|
+
|
|
295
|
+
```kotlin
|
|
296
|
+
val httpClient = HttpClient(OkHttp) {
|
|
297
|
+
engine {
|
|
298
|
+
preconfigured = PinnedOkHttpClient.getInstance(context)
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
#### Ktor CIO Engine (Manual Pinning)
|
|
304
|
+
|
|
305
|
+
The CIO engine uses its own TLS stack and is **not** covered by Network Security Config or `PinnedOkHttpClient`. You must configure a custom `TrustManager` manually:
|
|
306
|
+
|
|
307
|
+
```kotlin
|
|
308
|
+
import io.ktor.client.*
|
|
309
|
+
import io.ktor.client.engine.cio.*
|
|
310
|
+
import java.security.cert.X509Certificate
|
|
311
|
+
import javax.net.ssl.X509TrustManager
|
|
312
|
+
|
|
313
|
+
val httpClient = HttpClient(CIO) {
|
|
314
|
+
engine {
|
|
315
|
+
https {
|
|
316
|
+
trustManager = object : X509TrustManager {
|
|
317
|
+
override fun checkClientTrusted(chain: Array<X509Certificate>, authType: String) {}
|
|
318
|
+
override fun checkServerTrusted(chain: Array<X509Certificate>, authType: String) {
|
|
319
|
+
// Validate the leaf certificate's public key pin against
|
|
320
|
+
// your expected SHA-256 hashes from ssl_config.json
|
|
321
|
+
val leafCert = chain[0]
|
|
322
|
+
val publicKeyHash = java.security.MessageDigest
|
|
323
|
+
.getInstance("SHA-256")
|
|
324
|
+
.digest(leafCert.publicKey.encoded)
|
|
325
|
+
val pin = android.util.Base64.encodeToString(publicKeyHash, android.util.Base64.NO_WRAP)
|
|
326
|
+
val expectedPins = listOf("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=") // from ssl_config.json
|
|
327
|
+
if (pin !in expectedPins) {
|
|
328
|
+
throw javax.net.ssl.SSLPeerUnverifiedException("Certificate pin mismatch for CIO engine")
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
override fun getAcceptedIssuers(): Array<X509Certificate> = arrayOf()
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
```
|
|
337
|
+
|
|
226
338
|
## Important Notes ⚠️
|
|
227
339
|
|
|
228
340
|
### Restarting After SSL Pinning Changes
|
|
@@ -305,6 +417,90 @@ const handleSSLToggle = async (enabled: boolean) => {
|
|
|
305
417
|
- Regularly update certificates before expiration
|
|
306
418
|
- Maintain multiple backup certificates
|
|
307
419
|
|
|
420
|
+
## Supported Networking Stacks
|
|
421
|
+
|
|
422
|
+
This library provides platform-level SSL pinning coverage across both iOS and Android. The table below shows which networking stacks are covered on each platform and the mechanism used.
|
|
423
|
+
|
|
424
|
+
| Stack | Platform | Covered | Mechanism |
|
|
425
|
+
|-------|----------|---------|-----------|
|
|
426
|
+
| `fetch` / `axios` (React Native) | iOS | Yes | TrustKit URLSession swizzling |
|
|
427
|
+
| `fetch` / `axios` (React Native) | Android | Yes | OkHttpClientFactory + Network Security Config |
|
|
428
|
+
| `URLSession` (Foundation) | iOS | Yes | TrustKit swizzling (`kTSKSwizzleNetworkDelegates`) |
|
|
429
|
+
| `SDWebImage` | iOS | Yes | TrustKit swizzling (uses URLSession internally) |
|
|
430
|
+
| `Alamofire` | iOS | Yes | TrustKit swizzling (uses URLSession internally) |
|
|
431
|
+
| Most URLSession-based libraries | iOS | Yes* | TrustKit swizzling |
|
|
432
|
+
| OkHttp | Android | Yes | Network Security Config + CertificatePinner |
|
|
433
|
+
| Cronet (`react-native-nitro-fetch`) | Android | Best-effort* | Network Security Config |
|
|
434
|
+
| Android WebView | Android | Yes | Network Security Config |
|
|
435
|
+
| Coil / Ktor with OkHttp engine | Android | Yes | Network Security Config |
|
|
436
|
+
| Glide / OkHttp3 (`react-native-fast-image`) | Android | Yes | Network Security Config |
|
|
437
|
+
| `HttpURLConnection` | Android | Yes | Network Security Config |
|
|
438
|
+
|
|
439
|
+
### iOS Coverage
|
|
440
|
+
|
|
441
|
+
On iOS, TrustKit is initialized with `kTSKSwizzleNetworkDelegates: true`, which automatically swizzles most `URLSession` delegates. This means **most libraries that use `URLSession` under the hood** are covered without additional configuration — including `SDWebImage`, `Alamofire`, and React Native's built-in networking.
|
|
442
|
+
|
|
443
|
+
**Known limitations:**
|
|
444
|
+
- Custom TLS stacks that do not use `URLSession` (e.g., custom OpenSSL bindings) are NOT covered by TrustKit swizzling.
|
|
445
|
+
- Apps with complex custom `URLSessionDelegate` implementations or other method-swizzling libraries may experience conflicts with TrustKit's swizzling.
|
|
446
|
+
|
|
447
|
+
### Android Coverage
|
|
448
|
+
|
|
449
|
+
On Android, the library generates a `network_security_config.xml` at build time from your `ssl_config.json`. This is enforced at the OS level for all networking stacks that use the platform default `TrustManager`, covering OkHttp, WebView, Coil, Glide, and `HttpURLConnection` without per-library configuration. Cronet coverage is best-effort (see below).
|
|
450
|
+
|
|
451
|
+
**Known limitations:**
|
|
452
|
+
- Libraries that build OkHttp with a custom `TrustManager` that bypasses the system default may not be covered by Network Security Config.
|
|
453
|
+
- **Cronet**: No authoritative documentation confirms Cronet always respects NSC `<pin-set>` directives. Cronet has its own pinning API (`CronetEngine.Builder.addPublicKeyPins()`), which should be used for guaranteed Cronet pinning.
|
|
454
|
+
|
|
455
|
+
### PinnedOkHttpClient API (Android)
|
|
456
|
+
|
|
457
|
+
For native module authors who need a pre-configured pinned OkHttp client, the library exposes `PinnedOkHttpClient`:
|
|
458
|
+
|
|
459
|
+
```kotlin
|
|
460
|
+
// Get the singleton pinned client
|
|
461
|
+
val client = PinnedOkHttpClient.getInstance(context)
|
|
462
|
+
```
|
|
463
|
+
|
|
464
|
+
The client reads `ssl_config.json` and configures `CertificatePinner` when SSL pinning is enabled. It returns a plain `OkHttpClient` when pinning is disabled. The singleton is automatically invalidated when the pinning state changes.
|
|
465
|
+
|
|
466
|
+
#### Glide Integration
|
|
467
|
+
|
|
468
|
+
```kotlin
|
|
469
|
+
@GlideModule
|
|
470
|
+
class MyAppGlideModule : AppGlideModule() {
|
|
471
|
+
override fun registerComponents(context: Context, glide: Glide, registry: Registry) {
|
|
472
|
+
val client = PinnedOkHttpClient.getInstance(context)
|
|
473
|
+
registry.replace(
|
|
474
|
+
GlideUrl::class.java,
|
|
475
|
+
InputStream::class.java,
|
|
476
|
+
OkHttpUrlLoader.Factory(client)
|
|
477
|
+
)
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
```
|
|
481
|
+
|
|
482
|
+
#### Coil Integration
|
|
483
|
+
|
|
484
|
+
```kotlin
|
|
485
|
+
val imageLoader = ImageLoader.Builder(context)
|
|
486
|
+
.okHttpClient { PinnedOkHttpClient.getInstance(context) }
|
|
487
|
+
.build()
|
|
488
|
+
```
|
|
489
|
+
|
|
490
|
+
#### Ktor OkHttp Engine Integration
|
|
491
|
+
|
|
492
|
+
```kotlin
|
|
493
|
+
val httpClient = HttpClient(OkHttp) {
|
|
494
|
+
engine {
|
|
495
|
+
preconfigured = PinnedOkHttpClient.getInstance(context)
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
```
|
|
499
|
+
|
|
500
|
+
#### Ktor CIO Engine (Manual Pinning)
|
|
501
|
+
|
|
502
|
+
The CIO engine uses its own TLS stack and is **not** covered by Network Security Config or `PinnedOkHttpClient`. See the [Ktor CIO Engine manual pinning example](#ktor-cio-engine-manual-pinning) above for a complete code sample using a custom `TrustManager`.
|
|
503
|
+
|
|
308
504
|
## ✅ Completed Roadmap
|
|
309
505
|
|
|
310
506
|
### Recently Completed Features
|
|
@@ -344,6 +540,9 @@ const handleSSLToggle = async (enabled: boolean) => {
|
|
|
344
540
|
- Web support for React Native Web
|
|
345
541
|
- Additional certificate formats support
|
|
346
542
|
|
|
543
|
+
- 📦 **Optional Library Artifacts**
|
|
544
|
+
- `react-native-ssl-manager-glide` — first-class Glide integration with pre-configured `AppGlideModule` (currently manual via `PinnedOkHttpClient`)
|
|
545
|
+
|
|
347
546
|
## 🧪 Testing Your SSL Implementation
|
|
348
547
|
|
|
349
548
|
### Using the Example App
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
package com.usesslpinning
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import okhttp3.CertificatePinner
|
|
5
|
+
import okhttp3.OkHttpClient
|
|
6
|
+
import org.json.JSONObject
|
|
7
|
+
import java.io.IOException
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Public singleton providing a pinned OkHttpClient for native module authors.
|
|
11
|
+
*
|
|
12
|
+
* Usage:
|
|
13
|
+
* val client = PinnedOkHttpClient.getInstance(context)
|
|
14
|
+
*
|
|
15
|
+
* The client is configured with CertificatePinner from ssl_config.json when
|
|
16
|
+
* SSL pinning is enabled. When disabled, a plain OkHttpClient is returned.
|
|
17
|
+
*
|
|
18
|
+
* The singleton is invalidated when the pinning state changes via setUseSSLPinning.
|
|
19
|
+
*/
|
|
20
|
+
object PinnedOkHttpClient {
|
|
21
|
+
|
|
22
|
+
@Volatile
|
|
23
|
+
private var instance: OkHttpClient? = null
|
|
24
|
+
|
|
25
|
+
@Volatile
|
|
26
|
+
private var lastPinningState: Boolean? = null
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Returns a singleton OkHttpClient configured with certificate pinning
|
|
30
|
+
* from ssl_config.json (when SSL pinning is enabled).
|
|
31
|
+
*/
|
|
32
|
+
@JvmStatic
|
|
33
|
+
fun getInstance(context: Context): OkHttpClient {
|
|
34
|
+
val prefs = context.getSharedPreferences("AppSettings", Context.MODE_PRIVATE)
|
|
35
|
+
val useSSLPinning = prefs.getBoolean("useSSLPinning", true)
|
|
36
|
+
|
|
37
|
+
// Invalidate if pinning state changed
|
|
38
|
+
if (useSSLPinning != lastPinningState) {
|
|
39
|
+
instance = null
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return instance ?: synchronized(this) {
|
|
43
|
+
instance ?: buildClient(context, useSSLPinning).also {
|
|
44
|
+
instance = it
|
|
45
|
+
lastPinningState = useSSLPinning
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Invalidate the cached client. Called when setUseSSLPinning changes state.
|
|
52
|
+
*/
|
|
53
|
+
@JvmStatic
|
|
54
|
+
fun invalidate() {
|
|
55
|
+
synchronized(this) {
|
|
56
|
+
instance = null
|
|
57
|
+
lastPinningState = null
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
private fun buildClient(context: Context, useSSLPinning: Boolean): OkHttpClient {
|
|
62
|
+
val builder = OkHttpClient.Builder()
|
|
63
|
+
|
|
64
|
+
if (useSSLPinning) {
|
|
65
|
+
try {
|
|
66
|
+
val configJson = readSslConfig(context)
|
|
67
|
+
if (configJson != null) {
|
|
68
|
+
val pinnerBuilder = CertificatePinner.Builder()
|
|
69
|
+
val sha256Keys = configJson.getJSONObject("sha256Keys")
|
|
70
|
+
val hostnames = sha256Keys.keys()
|
|
71
|
+
while (hostnames.hasNext()) {
|
|
72
|
+
val hostname = hostnames.next()
|
|
73
|
+
val keysArray = sha256Keys.getJSONArray(hostname)
|
|
74
|
+
for (i in 0 until keysArray.length()) {
|
|
75
|
+
pinnerBuilder.add(hostname, keysArray.getString(i))
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
builder.certificatePinner(pinnerBuilder.build())
|
|
79
|
+
}
|
|
80
|
+
} catch (_: Exception) {
|
|
81
|
+
// SSL pinning setup failed — return plain client
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return builder.build()
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
private fun readSslConfig(context: Context): JSONObject? {
|
|
89
|
+
// Check runtime config first (set via JS API)
|
|
90
|
+
val prefs = context.getSharedPreferences("AppSettings", Context.MODE_PRIVATE)
|
|
91
|
+
val runtimeConfig = prefs.getString("sslConfig", null)
|
|
92
|
+
if (!runtimeConfig.isNullOrEmpty()) {
|
|
93
|
+
return JSONObject(runtimeConfig)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Fall back to bundled asset
|
|
97
|
+
return try {
|
|
98
|
+
val inputStream = context.assets.open("ssl_config.json")
|
|
99
|
+
val bytes = inputStream.readBytes()
|
|
100
|
+
inputStream.close()
|
|
101
|
+
JSONObject(String(bytes, Charsets.UTF_8))
|
|
102
|
+
} catch (_: IOException) {
|
|
103
|
+
null
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
@@ -20,6 +20,146 @@ def ensureAssetsDir() {
|
|
|
20
20
|
def assetsDir = file("${projectDir}/src/main/assets")
|
|
21
21
|
def destFile = file("${projectDir}/src/main/assets/ssl_config.json")
|
|
22
22
|
|
|
23
|
+
import groovy.xml.XmlParser
|
|
24
|
+
import groovy.xml.XmlNodePrinter
|
|
25
|
+
import groovy.xml.MarkupBuilder
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Generate network_security_config.xml from ssl_config.json
|
|
29
|
+
* Returns true if XML was generated, false otherwise
|
|
30
|
+
*/
|
|
31
|
+
def generateNetworkSecurityConfigXml(File sslConfigFile, File xmlOutputFile) {
|
|
32
|
+
if (!sslConfigFile || !sslConfigFile.exists()) {
|
|
33
|
+
println "⚠️ SSL Config not found, skipping Network Security Config XML generation"
|
|
34
|
+
return false
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
def json = new groovy.json.JsonSlurper().parse(sslConfigFile)
|
|
38
|
+
def sha256Keys = json?.sha256Keys
|
|
39
|
+
if (!sha256Keys || sha256Keys.isEmpty()) {
|
|
40
|
+
println "⚠️ No sha256Keys found in ssl_config.json, skipping XML generation"
|
|
41
|
+
return false
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Calculate default expiration: 1 year from now
|
|
45
|
+
def cal = Calendar.getInstance()
|
|
46
|
+
cal.add(Calendar.YEAR, 1)
|
|
47
|
+
def expirationDate = String.format("%tF", cal) // YYYY-MM-DD format
|
|
48
|
+
|
|
49
|
+
// Ensure output directory exists
|
|
50
|
+
def xmlDir = xmlOutputFile.parentFile
|
|
51
|
+
if (!xmlDir.exists()) xmlDir.mkdirs()
|
|
52
|
+
|
|
53
|
+
// Check for existing NSC and merge if present
|
|
54
|
+
if (xmlOutputFile.exists()) {
|
|
55
|
+
println "🔄 Existing network_security_config.xml found, merging pin entries"
|
|
56
|
+
mergeNetworkSecurityConfigXml(xmlOutputFile, sha256Keys, expirationDate)
|
|
57
|
+
} else {
|
|
58
|
+
writeNewNetworkSecurityConfigXml(xmlOutputFile, sha256Keys, expirationDate)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return true
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Write a fresh network_security_config.xml
|
|
66
|
+
*/
|
|
67
|
+
def writeNewNetworkSecurityConfigXml(File xmlFile, Map sha256Keys, String expirationDate) {
|
|
68
|
+
def writer = new StringWriter()
|
|
69
|
+
def xml = new MarkupBuilder(writer)
|
|
70
|
+
xml.setDoubleQuotes(true)
|
|
71
|
+
|
|
72
|
+
writer.write('<?xml version="1.0" encoding="utf-8"?>\n')
|
|
73
|
+
xml.'network-security-config' {
|
|
74
|
+
sha256Keys.each { domain, pins ->
|
|
75
|
+
'domain-config'('cleartextTrafficPermitted': 'false') {
|
|
76
|
+
'domain'('includeSubdomains': 'true', domain)
|
|
77
|
+
'pin-set'('expiration': expirationDate) {
|
|
78
|
+
pins.each { pinValue ->
|
|
79
|
+
def cleanPin = pinValue.replaceFirst('^sha256/', '')
|
|
80
|
+
'pin'('digest': 'SHA-256', cleanPin)
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
xmlFile.text = writer.toString()
|
|
88
|
+
println "✅ Generated network_security_config.xml with ${sha256Keys.size()} domain(s)"
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Merge pin-set entries into an existing network_security_config.xml
|
|
93
|
+
* Preserves existing config (debug-overrides, base-config, other domain-configs)
|
|
94
|
+
* Replaces pin-set for domains that already exist, adds new domain-configs otherwise
|
|
95
|
+
*/
|
|
96
|
+
def mergeNetworkSecurityConfigXml(File xmlFile, Map sha256Keys, String expirationDate) {
|
|
97
|
+
def parser = new XmlParser()
|
|
98
|
+
def root = parser.parse(xmlFile)
|
|
99
|
+
|
|
100
|
+
sha256Keys.each { domain, pins ->
|
|
101
|
+
// Find existing domain-config for this domain
|
|
102
|
+
def existingDomainConfig = root.'domain-config'.find { dc ->
|
|
103
|
+
dc.'domain'.any { d -> d.text() == domain }
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (existingDomainConfig) {
|
|
107
|
+
println "⚠️ Replacing existing pin-set for domain: ${domain}"
|
|
108
|
+
// Remove old pin-set(s)
|
|
109
|
+
existingDomainConfig.'pin-set'.each { existingDomainConfig.remove(it) }
|
|
110
|
+
// Add new pin-set
|
|
111
|
+
def pinSetNode = new Node(existingDomainConfig, 'pin-set', [expiration: expirationDate])
|
|
112
|
+
pins.each { pinValue ->
|
|
113
|
+
def cleanPin = pinValue.replaceFirst('^sha256/', '')
|
|
114
|
+
new Node(pinSetNode, 'pin', [digest: 'SHA-256'], cleanPin)
|
|
115
|
+
}
|
|
116
|
+
} else {
|
|
117
|
+
// Add new domain-config
|
|
118
|
+
def domainConfigNode = new Node(root, 'domain-config', [cleartextTrafficPermitted: 'false'])
|
|
119
|
+
new Node(domainConfigNode, 'domain', [includeSubdomains: 'true'], domain)
|
|
120
|
+
def pinSetNode = new Node(domainConfigNode, 'pin-set', [expiration: expirationDate])
|
|
121
|
+
pins.each { pinValue ->
|
|
122
|
+
def cleanPin = pinValue.replaceFirst('^sha256/', '')
|
|
123
|
+
new Node(pinSetNode, 'pin', [digest: 'SHA-256'], cleanPin)
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Write merged XML back
|
|
129
|
+
def writer = new StringWriter()
|
|
130
|
+
writer.write('<?xml version="1.0" encoding="utf-8"?>\n')
|
|
131
|
+
def printer = new XmlNodePrinter(new PrintWriter(writer))
|
|
132
|
+
printer.setPreserveWhitespace(true)
|
|
133
|
+
printer.print(root)
|
|
134
|
+
xmlFile.text = writer.toString()
|
|
135
|
+
println "✅ Merged pin entries into existing network_security_config.xml"
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Patch AndroidManifest.xml to reference network_security_config if not already set
|
|
140
|
+
*/
|
|
141
|
+
def patchAndroidManifest(File manifestFile) {
|
|
142
|
+
if (!manifestFile || !manifestFile.exists()) {
|
|
143
|
+
println "⚠️ AndroidManifest.xml not found, skipping manifest patching"
|
|
144
|
+
return false
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
def content = manifestFile.text
|
|
148
|
+
if (content.contains('android:networkSecurityConfig')) {
|
|
149
|
+
println "ℹ️ AndroidManifest already has networkSecurityConfig reference, preserving existing"
|
|
150
|
+
return false
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Add networkSecurityConfig attribute to <application> tag
|
|
154
|
+
content = content.replaceFirst(
|
|
155
|
+
'(<application\\b[^>]*)(>)',
|
|
156
|
+
'$1 android:networkSecurityConfig="@xml/network_security_config"$2'
|
|
157
|
+
)
|
|
158
|
+
manifestFile.text = content
|
|
159
|
+
println "✅ Added networkSecurityConfig reference to AndroidManifest.xml"
|
|
160
|
+
return true
|
|
161
|
+
}
|
|
162
|
+
|
|
23
163
|
plugins.withId('com.android.application') {
|
|
24
164
|
|
|
25
165
|
// ===== AGP 7/8: androidComponents DSL =====
|
|
@@ -64,10 +204,29 @@ plugins.withId('com.android.application') {
|
|
|
64
204
|
}
|
|
65
205
|
}
|
|
66
206
|
|
|
67
|
-
//
|
|
68
|
-
|
|
69
|
-
tasks.
|
|
70
|
-
|
|
207
|
+
// ===== Generate Network Security Config XML =====
|
|
208
|
+
def nscTaskName = "generateNetworkSecurityConfig${vNameCap}"
|
|
209
|
+
tasks.register(nscTaskName) {
|
|
210
|
+
group = "SSL Pinning"
|
|
211
|
+
description = "Generate network_security_config.xml for ${vName}"
|
|
212
|
+
|
|
213
|
+
doLast {
|
|
214
|
+
def sourceFile = findSslConfigFile()
|
|
215
|
+
def xmlDir = file("${projectDir}/src/main/res/xml")
|
|
216
|
+
def xmlFile = file("${xmlDir}/network_security_config.xml")
|
|
217
|
+
if (generateNetworkSecurityConfigXml(sourceFile, xmlFile)) {
|
|
218
|
+
// Patch manifest
|
|
219
|
+
def manifestFile = file("${projectDir}/src/main/AndroidManifest.xml")
|
|
220
|
+
patchAndroidManifest(manifestFile)
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Hook both tasks before merge/assemble/install
|
|
226
|
+
tasks.matching { it.name == "merge${vNameCap}Assets" }.configureEach { dependsOn taskName; dependsOn nscTaskName }
|
|
227
|
+
tasks.matching { it.name == "install${vNameCap}" }.configureEach { dependsOn taskName; dependsOn nscTaskName }
|
|
228
|
+
tasks.matching { it.name == "assemble${vNameCap}" }.configureEach { dependsOn taskName; dependsOn nscTaskName }
|
|
229
|
+
tasks.matching { it.name == "merge${vNameCap}Resources" }.configureEach { dependsOn nscTaskName }
|
|
71
230
|
}
|
|
72
231
|
}
|
|
73
232
|
|
|
@@ -114,15 +273,33 @@ plugins.withId('com.android.application') {
|
|
|
114
273
|
}
|
|
115
274
|
}
|
|
116
275
|
|
|
276
|
+
// ===== Generate Network Security Config XML (legacy path) =====
|
|
277
|
+
def nscTaskName = "generateNetworkSecurityConfig${vNameCap}"
|
|
278
|
+
def nscTask = tasks.create(nscTaskName) {
|
|
279
|
+
group = "SSL Pinning"
|
|
280
|
+
description = "Generate network_security_config.xml for ${vNameCap}"
|
|
281
|
+
|
|
282
|
+
doLast {
|
|
283
|
+
def sourceFile = findSslConfigFile()
|
|
284
|
+
def xmlDir = file("${projectDir}/src/main/res/xml")
|
|
285
|
+
def xmlFile = file("${xmlDir}/network_security_config.xml")
|
|
286
|
+
if (generateNetworkSecurityConfigXml(sourceFile, xmlFile)) {
|
|
287
|
+
def manifestFile = file("${projectDir}/src/main/AndroidManifest.xml")
|
|
288
|
+
patchAndroidManifest(manifestFile)
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
117
293
|
try {
|
|
118
294
|
if (variant.hasProperty("mergeAssetsProvider")) {
|
|
119
|
-
variant.mergeAssetsProvider.configure { dependsOn copyTask }
|
|
295
|
+
variant.mergeAssetsProvider.configure { dependsOn copyTask; dependsOn nscTask }
|
|
120
296
|
} else {
|
|
121
|
-
tasks.named("merge${vNameCap}Assets").configure { dependsOn copyTask }
|
|
297
|
+
tasks.named("merge${vNameCap}Assets").configure { dependsOn copyTask; dependsOn nscTask }
|
|
122
298
|
}
|
|
123
299
|
} catch (ignored) { }
|
|
124
|
-
try { tasks.named("
|
|
125
|
-
try { tasks.named("
|
|
300
|
+
try { tasks.named("merge${vNameCap}Resources").configure { dependsOn nscTask } } catch (ignored) { }
|
|
301
|
+
try { tasks.named("install${vNameCap}").configure { dependsOn copyTask; dependsOn nscTask } } catch (ignored) { }
|
|
302
|
+
try { tasks.named("assemble${vNameCap}").configure { dependsOn copyTask; dependsOn nscTask } } catch (ignored) { }
|
|
126
303
|
}
|
|
127
304
|
}
|
|
128
305
|
}
|
package/app.plugin.js
CHANGED
|
@@ -21,6 +21,8 @@ function withSslManager(config, options = {}) {
|
|
|
21
21
|
config = withAndroidSslPinning(config);
|
|
22
22
|
config = withAndroidMainApplication(config);
|
|
23
23
|
config = withAndroidAssets(config, { sslConfigPath });
|
|
24
|
+
config = withAndroidNetworkSecurityConfig(config, { sslConfigPath });
|
|
25
|
+
config = withAndroidNscManifest(config);
|
|
24
26
|
}
|
|
25
27
|
|
|
26
28
|
// Add iOS configuration
|
|
@@ -287,4 +289,101 @@ function withIosAssets(config, options) {
|
|
|
287
289
|
return config;
|
|
288
290
|
}
|
|
289
291
|
|
|
292
|
+
const { generateNscXml, mergeNscXml } = require('./scripts/nsc-utils');
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Read ssl_config.json and return parsed sha256Keys, or null if not found
|
|
296
|
+
*/
|
|
297
|
+
function readSslConfig(projectRoot, sslConfigPath) {
|
|
298
|
+
const sourceConfigPath = path.resolve(projectRoot, sslConfigPath);
|
|
299
|
+
if (!fs.existsSync(sourceConfigPath)) {
|
|
300
|
+
return null;
|
|
301
|
+
}
|
|
302
|
+
try {
|
|
303
|
+
const config = JSON.parse(fs.readFileSync(sourceConfigPath, 'utf8'));
|
|
304
|
+
return config.sha256Keys || null;
|
|
305
|
+
} catch {
|
|
306
|
+
return null;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Generate network_security_config.xml during Expo prebuild
|
|
312
|
+
*/
|
|
313
|
+
function withAndroidNetworkSecurityConfig(config, options) {
|
|
314
|
+
return withDangerousMod(config, [
|
|
315
|
+
'android',
|
|
316
|
+
async (config) => {
|
|
317
|
+
const { sslConfigPath = 'ssl_config.json' } = options;
|
|
318
|
+
const projectRoot = config.modRequest.projectRoot;
|
|
319
|
+
const sha256Keys = readSslConfig(projectRoot, sslConfigPath);
|
|
320
|
+
|
|
321
|
+
if (!sha256Keys || Object.keys(sha256Keys).length === 0) {
|
|
322
|
+
console.warn(
|
|
323
|
+
'⚠️ No SSL pins found, skipping network_security_config.xml generation'
|
|
324
|
+
);
|
|
325
|
+
return config;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const xmlDir = path.join(
|
|
329
|
+
config.modRequest.platformProjectRoot,
|
|
330
|
+
'app/src/main/res/xml'
|
|
331
|
+
);
|
|
332
|
+
const xmlPath = path.join(xmlDir, 'network_security_config.xml');
|
|
333
|
+
|
|
334
|
+
if (!fs.existsSync(xmlDir)) {
|
|
335
|
+
fs.mkdirSync(xmlDir, { recursive: true });
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
if (fs.existsSync(xmlPath)) {
|
|
339
|
+
// Merge with existing
|
|
340
|
+
const existingXml = fs.readFileSync(xmlPath, 'utf8');
|
|
341
|
+
const mergedXml = mergeNscXml(existingXml, sha256Keys);
|
|
342
|
+
fs.writeFileSync(xmlPath, mergedXml);
|
|
343
|
+
console.log(
|
|
344
|
+
'✅ Merged SSL pins into existing network_security_config.xml'
|
|
345
|
+
);
|
|
346
|
+
} else {
|
|
347
|
+
// Generate new
|
|
348
|
+
const xml = generateNscXml(sha256Keys);
|
|
349
|
+
fs.writeFileSync(xmlPath, xml);
|
|
350
|
+
console.log('✅ Generated network_security_config.xml');
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
return config;
|
|
354
|
+
},
|
|
355
|
+
]);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Patch AndroidManifest to reference network_security_config.xml
|
|
360
|
+
*/
|
|
361
|
+
function withAndroidNscManifest(config) {
|
|
362
|
+
return withAndroidManifest(config, (config) => {
|
|
363
|
+
const manifest = config.modResults;
|
|
364
|
+
const application = manifest.manifest.application?.[0];
|
|
365
|
+
|
|
366
|
+
if (!application) {
|
|
367
|
+
return config;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
if (!application.$) {
|
|
371
|
+
application.$ = {};
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
if (application.$['android:networkSecurityConfig']) {
|
|
375
|
+
console.log(
|
|
376
|
+
'ℹ️ AndroidManifest already has networkSecurityConfig, preserving existing'
|
|
377
|
+
);
|
|
378
|
+
return config;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
application.$['android:networkSecurityConfig'] =
|
|
382
|
+
'@xml/network_security_config';
|
|
383
|
+
console.log('✅ Added networkSecurityConfig to AndroidManifest.xml');
|
|
384
|
+
|
|
385
|
+
return config;
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
|
|
290
389
|
module.exports = withSslManager;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { TurboModule } from 'react-native';
|
|
2
|
+
export interface Spec extends TurboModule {
|
|
3
|
+
readonly setUseSSLPinning: (usePinning: boolean) => Promise<void>;
|
|
4
|
+
readonly getUseSSLPinning: () => Promise<boolean>;
|
|
5
|
+
}
|
|
6
|
+
declare const _default: Spec;
|
|
7
|
+
export default _default;
|
|
8
|
+
//# sourceMappingURL=NativeUseSslPinning.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"NativeUseSslPinning.d.ts","sourceRoot":"","sources":["../src/NativeUseSslPinning.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAGhD,MAAM,WAAW,IAAK,SAAQ,WAAW;IACvC,QAAQ,CAAC,gBAAgB,EAAE,CAAC,UAAU,EAAE,OAAO,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAClE,QAAQ,CAAC,gBAAgB,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,CAAC;CACnD;;AAED,wBAAuE"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SSL Pinning Configuration Interface
|
|
3
|
+
* Defines the structure for SSL pinning configuration
|
|
4
|
+
*/
|
|
5
|
+
export interface SslPinningConfig {
|
|
6
|
+
sha256Keys: {
|
|
7
|
+
[domain: string]: string[];
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* SSL Pinning Error Interface
|
|
12
|
+
*/
|
|
13
|
+
export interface SslPinningError extends Error {
|
|
14
|
+
code?: string;
|
|
15
|
+
message: string;
|
|
16
|
+
}
|
|
17
|
+
//# sourceMappingURL=UseSslPinning.types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"UseSslPinning.types.d.ts","sourceRoot":"","sources":["../src/UseSslPinning.types.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,MAAM,WAAW,gBAAgB;IAC/B,UAAU,EAAE;QACV,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;KAC5B,CAAC;CACH;AAED;;GAEG;AACH,MAAM,WAAW,eAAgB,SAAQ,KAAK;IAC5C,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;CACjB"}
|
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export type { SslPinningConfig, SslPinningError } from './UseSslPinning.types';
|
|
2
|
+
/**
|
|
3
|
+
* Sets whether SSL pinning should be used.
|
|
4
|
+
*
|
|
5
|
+
* @param {boolean} usePinning - Whether to enable SSL pinning
|
|
6
|
+
* @returns {Promise<void>} A promise that resolves when the setting is saved
|
|
7
|
+
*/
|
|
8
|
+
export declare const setUseSSLPinning: (usePinning: boolean) => Promise<void>;
|
|
9
|
+
/**
|
|
10
|
+
* Retrieves the current state of SSL pinning usage.
|
|
11
|
+
*
|
|
12
|
+
* @returns A promise that resolves to a boolean indicating whether SSL pinning is being used.
|
|
13
|
+
*/
|
|
14
|
+
export declare const getUseSSLPinning: () => Promise<boolean>;
|
|
15
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.tsx"],"names":[],"mappings":"AAGA,YAAY,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AA0C/E;;;;;GAKG;AACH,eAAO,MAAM,gBAAgB,eAAgB,OAAO,KAAG,QAAQ,IAAI,CAElE,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,gBAAgB,QAAa,QAAQ,OAAO,CAExD,CAAC"}
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Remove unused Platform import since we no longer check OS
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.getUseSSLPinning = exports.setUseSSLPinning = void 0;
|
|
5
|
+
// New Architecture and Legacy Architecture support
|
|
6
|
+
let UseSslPinning;
|
|
7
|
+
try {
|
|
8
|
+
// Try Legacy NativeModules first (more reliable)
|
|
9
|
+
const { NativeModules } = require('react-native');
|
|
10
|
+
// Look for our universal module (works in both CLI and Expo)
|
|
11
|
+
UseSslPinning = NativeModules.UseSslPinning;
|
|
12
|
+
if (UseSslPinning) {
|
|
13
|
+
}
|
|
14
|
+
else {
|
|
15
|
+
// Fallback to TurboModule if available
|
|
16
|
+
try {
|
|
17
|
+
UseSslPinning = require('./NativeUseSslPinning').default;
|
|
18
|
+
}
|
|
19
|
+
catch (turboModuleError) {
|
|
20
|
+
console.log('❌ TurboModule failed:', turboModuleError.message);
|
|
21
|
+
UseSslPinning = null;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
console.log('❌ Overall module loading failed:', error.message);
|
|
27
|
+
UseSslPinning = null;
|
|
28
|
+
}
|
|
29
|
+
// Fallback implementation if native module is not available
|
|
30
|
+
if (!UseSslPinning) {
|
|
31
|
+
UseSslPinning = {
|
|
32
|
+
setUseSSLPinning: (_usePinning) => {
|
|
33
|
+
return Promise.resolve();
|
|
34
|
+
},
|
|
35
|
+
getUseSSLPinning: () => {
|
|
36
|
+
return Promise.resolve(true);
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Sets whether SSL pinning should be used.
|
|
42
|
+
*
|
|
43
|
+
* @param {boolean} usePinning - Whether to enable SSL pinning
|
|
44
|
+
* @returns {Promise<void>} A promise that resolves when the setting is saved
|
|
45
|
+
*/
|
|
46
|
+
const setUseSSLPinning = (usePinning) => {
|
|
47
|
+
return UseSslPinning.setUseSSLPinning(usePinning);
|
|
48
|
+
};
|
|
49
|
+
exports.setUseSSLPinning = setUseSSLPinning;
|
|
50
|
+
/**
|
|
51
|
+
* Retrieves the current state of SSL pinning usage.
|
|
52
|
+
*
|
|
53
|
+
* @returns A promise that resolves to a boolean indicating whether SSL pinning is being used.
|
|
54
|
+
*/
|
|
55
|
+
const getUseSSLPinning = async () => {
|
|
56
|
+
return await UseSslPinning.getUseSSLPinning();
|
|
57
|
+
};
|
|
58
|
+
exports.getUseSSLPinning = getUseSSLPinning;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-native-ssl-manager",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "React Native SSL Pinning provides seamless SSL certificate pinning integration for enhanced network security in React Native apps. This module enables developers to easily implement and manage certificate pinning, protecting applications against man-in-the-middle (MITM) attacks. With dynamic configuration options and the ability to toggle SSL pinning, it's particularly useful for development and testing scenarios.",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"module": "lib/index.js",
|
|
@@ -137,11 +137,6 @@
|
|
|
137
137
|
"react-native": "*"
|
|
138
138
|
}
|
|
139
139
|
},
|
|
140
|
-
"codegenConfig": {
|
|
141
|
-
"name": "RNUseSslPinningSpec",
|
|
142
|
-
"type": "modules",
|
|
143
|
-
"jsSrcsDir": "src"
|
|
144
|
-
},
|
|
145
140
|
"jest": {
|
|
146
141
|
"preset": "react-native",
|
|
147
142
|
"modulePathIgnorePatterns": [
|
|
@@ -204,11 +199,6 @@
|
|
|
204
199
|
"trailingComma": "es5",
|
|
205
200
|
"useTabs": false
|
|
206
201
|
},
|
|
207
|
-
"codegenConfig": {
|
|
208
|
-
"name": "RNUseSslPinningSpec",
|
|
209
|
-
"type": "modules",
|
|
210
|
-
"jsSrcsDir": "src"
|
|
211
|
-
},
|
|
212
202
|
"dependencies": {
|
|
213
203
|
"@babel/runtime": "^7.23.0",
|
|
214
204
|
"postinstall": "^0.10.3"
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared utilities for Network Security Config XML generation.
|
|
3
|
+
* Used by app.plugin.js, postinstall.js, and tests.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Generate network_security_config.xml content from sha256Keys
|
|
8
|
+
*/
|
|
9
|
+
function generateNscXml(sha256Keys) {
|
|
10
|
+
// Default expiration: 1 year from now
|
|
11
|
+
const expDate = new Date();
|
|
12
|
+
expDate.setFullYear(expDate.getFullYear() + 1);
|
|
13
|
+
const expiration = expDate.toISOString().split('T')[0]; // YYYY-MM-DD
|
|
14
|
+
|
|
15
|
+
let xml = '<?xml version="1.0" encoding="utf-8"?>\n';
|
|
16
|
+
xml += '<network-security-config>\n';
|
|
17
|
+
|
|
18
|
+
for (const [domain, pins] of Object.entries(sha256Keys)) {
|
|
19
|
+
xml += ' <domain-config cleartextTrafficPermitted="false">\n';
|
|
20
|
+
xml += ` <domain includeSubdomains="true">${domain}</domain>\n`;
|
|
21
|
+
xml += ` <pin-set expiration="${expiration}">\n`;
|
|
22
|
+
for (const pin of pins) {
|
|
23
|
+
const cleanPin = pin.replace(/^sha256\//, '');
|
|
24
|
+
xml += ` <pin digest="SHA-256">${cleanPin}</pin>\n`;
|
|
25
|
+
}
|
|
26
|
+
xml += ' </pin-set>\n';
|
|
27
|
+
xml += ' </domain-config>\n';
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
xml += '</network-security-config>\n';
|
|
31
|
+
return xml;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Merge pin-set entries into existing NSC XML string.
|
|
36
|
+
* Preserves existing config, replaces pin-set for matching domains, adds new ones.
|
|
37
|
+
*/
|
|
38
|
+
function mergeNscXml(existingXml, sha256Keys) {
|
|
39
|
+
const expDate = new Date();
|
|
40
|
+
expDate.setFullYear(expDate.getFullYear() + 1);
|
|
41
|
+
const expiration = expDate.toISOString().split('T')[0];
|
|
42
|
+
|
|
43
|
+
for (const [domain, pins] of Object.entries(sha256Keys)) {
|
|
44
|
+
const pinSetXml = pins
|
|
45
|
+
.map((pin) => {
|
|
46
|
+
const cleanPin = pin.replace(/^sha256\//, '');
|
|
47
|
+
return ` <pin digest="SHA-256">${cleanPin}</pin>`;
|
|
48
|
+
})
|
|
49
|
+
.join('\n');
|
|
50
|
+
|
|
51
|
+
const domainConfigBlock =
|
|
52
|
+
` <domain-config cleartextTrafficPermitted="false">\n` +
|
|
53
|
+
` <domain includeSubdomains="true">${domain}</domain>\n` +
|
|
54
|
+
` <pin-set expiration="${expiration}">\n` +
|
|
55
|
+
`${pinSetXml}\n` +
|
|
56
|
+
` </pin-set>\n` +
|
|
57
|
+
` </domain-config>`;
|
|
58
|
+
|
|
59
|
+
// Check if domain already exists in the XML
|
|
60
|
+
const domainRegex = new RegExp(
|
|
61
|
+
`<domain-config[^>]*>\\s*<domain[^>]*>${domain.replace(/\./g, '\\.')}</domain>[\\s\\S]*?</domain-config>`,
|
|
62
|
+
'g'
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
if (domainRegex.test(existingXml)) {
|
|
66
|
+
console.warn(`⚠️ Replacing existing pin-set for domain: ${domain}`);
|
|
67
|
+
// Reset regex lastIndex since test() advanced it
|
|
68
|
+
domainRegex.lastIndex = 0;
|
|
69
|
+
existingXml = existingXml.replace(domainRegex, domainConfigBlock);
|
|
70
|
+
} else {
|
|
71
|
+
// Insert before closing </network-security-config>
|
|
72
|
+
existingXml = existingXml.replace(
|
|
73
|
+
'</network-security-config>',
|
|
74
|
+
`${domainConfigBlock}\n</network-security-config>`
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return existingXml;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
module.exports = { generateNscXml, mergeNscXml };
|
package/scripts/postinstall.js
CHANGED
|
@@ -105,4 +105,76 @@ if (fs.existsSync(sslConfigPath)) {
|
|
|
105
105
|
);
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
+
// Generate Network Security Config XML for Android
|
|
109
|
+
const { generateNscXml, mergeNscXml } = require('./nsc-utils');
|
|
110
|
+
const androidDir = path.join(projectRoot, 'android');
|
|
111
|
+
if (fs.existsSync(androidDir) && fs.existsSync(sslConfigPath)) {
|
|
112
|
+
console.log('🔄 Generating Android Network Security Config XML...');
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
const sslConfig = JSON.parse(fs.readFileSync(sslConfigPath, 'utf8'));
|
|
116
|
+
const sha256Keys = sslConfig.sha256Keys;
|
|
117
|
+
|
|
118
|
+
if (sha256Keys && Object.keys(sha256Keys).length > 0) {
|
|
119
|
+
const xmlDir = path.join(
|
|
120
|
+
androidDir,
|
|
121
|
+
'app',
|
|
122
|
+
'src',
|
|
123
|
+
'main',
|
|
124
|
+
'res',
|
|
125
|
+
'xml'
|
|
126
|
+
);
|
|
127
|
+
const xmlPath = path.join(xmlDir, 'network_security_config.xml');
|
|
128
|
+
|
|
129
|
+
if (fs.existsSync(xmlPath)) {
|
|
130
|
+
// Merge with existing NSC
|
|
131
|
+
const existingXml = fs.readFileSync(xmlPath, 'utf8');
|
|
132
|
+
const mergedXml = mergeNscXml(existingXml, sha256Keys);
|
|
133
|
+
fs.writeFileSync(xmlPath, mergedXml);
|
|
134
|
+
console.log(
|
|
135
|
+
'✅ Merged SSL pins into existing network_security_config.xml'
|
|
136
|
+
);
|
|
137
|
+
} else {
|
|
138
|
+
// Generate new XML
|
|
139
|
+
if (!fs.existsSync(xmlDir)) {
|
|
140
|
+
fs.mkdirSync(xmlDir, { recursive: true });
|
|
141
|
+
}
|
|
142
|
+
const xml = generateNscXml(sha256Keys);
|
|
143
|
+
fs.writeFileSync(xmlPath, xml);
|
|
144
|
+
console.log('✅ Generated network_security_config.xml');
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Patch AndroidManifest.xml
|
|
148
|
+
const manifestPath = path.join(
|
|
149
|
+
androidDir,
|
|
150
|
+
'app',
|
|
151
|
+
'src',
|
|
152
|
+
'main',
|
|
153
|
+
'AndroidManifest.xml'
|
|
154
|
+
);
|
|
155
|
+
if (fs.existsSync(manifestPath)) {
|
|
156
|
+
let manifestContent = fs.readFileSync(manifestPath, 'utf8');
|
|
157
|
+
if (!manifestContent.includes('android:networkSecurityConfig')) {
|
|
158
|
+
manifestContent = manifestContent.replace(
|
|
159
|
+
/(<application\b[^>]*)(>)/,
|
|
160
|
+
'$1 android:networkSecurityConfig="@xml/network_security_config"$2'
|
|
161
|
+
);
|
|
162
|
+
fs.writeFileSync(manifestPath, manifestContent);
|
|
163
|
+
console.log('✅ Added networkSecurityConfig to AndroidManifest.xml');
|
|
164
|
+
} else {
|
|
165
|
+
console.log(
|
|
166
|
+
'ℹ️ AndroidManifest already has networkSecurityConfig reference'
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
} else {
|
|
171
|
+
console.log('⚠️ No sha256Keys in ssl_config.json, skipping XML generation');
|
|
172
|
+
}
|
|
173
|
+
} catch (error) {
|
|
174
|
+
console.warn('⚠️ Failed to generate Network Security Config XML:', error.message);
|
|
175
|
+
}
|
|
176
|
+
} else if (!fs.existsSync(androidDir)) {
|
|
177
|
+
console.log('ℹ️ No android/ directory found, skipping NSC XML generation');
|
|
178
|
+
}
|
|
179
|
+
|
|
108
180
|
console.log('🎉 React Native SSL Manager setup complete!');
|