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 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
- // Hook chắc chắn cho assemble/install/mergeAssets
68
- tasks.matching { it.name == "merge${vNameCap}Assets" }.configureEach { dependsOn taskName }
69
- tasks.matching { it.name == "install${vNameCap}" }.configureEach { dependsOn taskName }
70
- tasks.matching { it.name == "assemble${vNameCap}" }.configureEach { dependsOn taskName }
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("install${vNameCap}").configure { dependsOn copyTask } } catch (ignored) { }
125
- try { tasks.named("assemble${vNameCap}").configure { dependsOn copyTask } } catch (ignored) { }
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,4 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const react_native_1 = require("react-native");
4
+ exports.default = react_native_1.TurboModuleRegistry.getEnforcing('UseSslPinning');
@@ -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"}
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
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",
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 };
@@ -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!');