@cap-kit/tls-fingerprint 8.0.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.
Files changed (51) hide show
  1. package/CapKitTlsFingerprint.podspec +17 -0
  2. package/LICENSE +21 -0
  3. package/Package.swift +25 -0
  4. package/README.md +427 -0
  5. package/android/build.gradle +103 -0
  6. package/android/src/main/AndroidManifest.xml +3 -0
  7. package/android/src/main/java/io/capkit/settings/TLSFingerprintImpl.kt +333 -0
  8. package/android/src/main/java/io/capkit/settings/TLSFingerprintPlugin.kt +342 -0
  9. package/android/src/main/java/io/capkit/settings/config/TLSFingerprintConfig.kt +102 -0
  10. package/android/src/main/java/io/capkit/settings/error/TLSFingerprintError.kt +114 -0
  11. package/android/src/main/java/io/capkit/settings/error/TLSFingerprintErrorMessages.kt +27 -0
  12. package/android/src/main/java/io/capkit/settings/logger/TLSFingerprintLogger.kt +85 -0
  13. package/android/src/main/java/io/capkit/settings/model/TLSFingerprintResultModel.kt +32 -0
  14. package/android/src/main/java/io/capkit/settings/utils/TLSFingerprintUtils.kt +91 -0
  15. package/android/src/main/res/.gitkeep +0 -0
  16. package/dist/cli/fingerprint.js +163 -0
  17. package/dist/cli/fingerprint.js.map +1 -0
  18. package/dist/docs.json +386 -0
  19. package/dist/esm/cli/fingerprint.d.ts +1 -0
  20. package/dist/esm/cli/fingerprint.js +161 -0
  21. package/dist/esm/cli/fingerprint.js.map +1 -0
  22. package/dist/esm/definitions.d.ts +244 -0
  23. package/dist/esm/definitions.js +42 -0
  24. package/dist/esm/definitions.js.map +1 -0
  25. package/dist/esm/index.d.ts +13 -0
  26. package/dist/esm/index.js +11 -0
  27. package/dist/esm/index.js.map +1 -0
  28. package/dist/esm/version.d.ts +1 -0
  29. package/dist/esm/version.js +3 -0
  30. package/dist/esm/version.js.map +1 -0
  31. package/dist/esm/web.d.ts +33 -0
  32. package/dist/esm/web.js +47 -0
  33. package/dist/esm/web.js.map +1 -0
  34. package/dist/plugin.cjs +107 -0
  35. package/dist/plugin.cjs.map +1 -0
  36. package/dist/plugin.js +110 -0
  37. package/dist/plugin.js.map +1 -0
  38. package/ios/Sources/TLSFingerprintPlugin/TLSFingerprintDelegate.swift +365 -0
  39. package/ios/Sources/TLSFingerprintPlugin/TLSFingerprintImpl.swift +275 -0
  40. package/ios/Sources/TLSFingerprintPlugin/TLSFingerprintPlugin.swift +219 -0
  41. package/ios/Sources/TLSFingerprintPlugin/Version.swift +16 -0
  42. package/ios/Sources/TLSFingerprintPlugin/config/TLSFingerprintConfig.swift +114 -0
  43. package/ios/Sources/TLSFingerprintPlugin/error/TLSFingerprintError.swift +107 -0
  44. package/ios/Sources/TLSFingerprintPlugin/error/TLSFingerprintErrorMessages.swift +30 -0
  45. package/ios/Sources/TLSFingerprintPlugin/logger/TLSFingerprintLogger.swift +69 -0
  46. package/ios/Sources/TLSFingerprintPlugin/model/TLSFingerprintResult.swift +76 -0
  47. package/ios/Sources/TLSFingerprintPlugin/utils/TLSFingerprintUtils.swift +79 -0
  48. package/ios/Tests/TLSFingerprintPluginTests/TLSFingerprintPluginTests.swift +15 -0
  49. package/package.json +131 -0
  50. package/scripts/chmod.mjs +34 -0
  51. package/scripts/sync-version.mjs +68 -0
@@ -0,0 +1,333 @@
1
+ package io.capkit.tlsfingerprint
2
+
3
+ import android.content.Context
4
+ import io.capkit.tlsfingerprint.config.TLSFingerprintConfig
5
+ import io.capkit.tlsfingerprint.error.TLSFingerprintError
6
+ import io.capkit.tlsfingerprint.error.TLSFingerprintErrorMessages
7
+ import io.capkit.tlsfingerprint.logger.TLSFingerprintLogger
8
+ import io.capkit.tlsfingerprint.model.TLSFingerprintResultModel
9
+ import io.capkit.tlsfingerprint.utils.TLSFingerprintUtils
10
+ import java.io.IOException
11
+ import java.net.SocketTimeoutException
12
+ import java.net.URL
13
+ import java.security.cert.Certificate
14
+ import java.security.cert.X509Certificate
15
+ import javax.net.ssl.HttpsURLConnection
16
+ import javax.net.ssl.SSLContext
17
+ import javax.net.ssl.TrustManager
18
+ import javax.net.ssl.X509TrustManager
19
+
20
+ /**
21
+ * Native Android implementation for the TLSFingerprint plugin.
22
+ *
23
+ * Responsibilities:
24
+ * - Perform platform-specific SSL pinning logic
25
+ * - Interact with system networking APIs
26
+ * - Throw typed TLSFingerprintError values on failure
27
+ *
28
+ * Forbidden:
29
+ * - Accessing PluginCall
30
+ * - Referencing Capacitor APIs
31
+ * - Constructing JavaScript payloads
32
+ */
33
+ class TLSFingerprintImpl(
34
+ private val context: Context,
35
+ ) {
36
+ // -----------------------------------------------------------------------------
37
+ // Constants
38
+ // -----------------------------------------------------------------------------
39
+
40
+ companion object {
41
+ private const val TIMEOUT_MS = 10_000
42
+ }
43
+
44
+ // -----------------------------------------------------------------------------
45
+ // Properties
46
+ // -----------------------------------------------------------------------------
47
+
48
+ /**
49
+ * Cached plugin configuration.
50
+ * Injected once during plugin initialization.
51
+ */
52
+ private lateinit var config: TLSFingerprintConfig
53
+
54
+ // -----------------------------------------------------------------------------
55
+ // Configuration
56
+ // -----------------------------------------------------------------------------
57
+
58
+ /**
59
+ * Applies plugin configuration.
60
+ *
61
+ * This method MUST be called exactly once from the plugin's load() method.
62
+ * It translates static configuration into runtime behavior
63
+ * (e.g. enabling verbose logging).
64
+ */
65
+ fun updateConfig(newConfig: TLSFingerprintConfig) {
66
+ this.config = newConfig
67
+ TLSFingerprintLogger.verbose = newConfig.verboseLogging
68
+
69
+ TLSFingerprintLogger.debug(
70
+ "Configuration applied. Verbose logging:",
71
+ newConfig.verboseLogging.toString(),
72
+ )
73
+ }
74
+
75
+ // -----------------------------------------------------------------------------
76
+ // Single fingerprint
77
+ // -----------------------------------------------------------------------------
78
+
79
+ /**
80
+ * Validates the SSL certificate of a HTTPS endpoint
81
+ * using a single SHA-256 fingerprint.
82
+ *
83
+ * Resolution order:
84
+ * 1. Runtime fingerprint argument
85
+ * 2. Static configuration fingerprint
86
+ *
87
+ * @throws TLSFingerprintError.Unavailable if no fingerprint available.
88
+ */
89
+ @Throws(TLSFingerprintError::class)
90
+ fun checkCertificate(
91
+ urlString: String,
92
+ fingerprintFromArgs: String?,
93
+ ): TLSFingerprintResultModel {
94
+ val fingerprint =
95
+ fingerprintFromArgs ?: config.fingerprint
96
+
97
+ if (fingerprint != null) {
98
+ return performCheck(
99
+ urlString = urlString,
100
+ fingerprints = listOf(fingerprint),
101
+ )
102
+ }
103
+
104
+ throw TLSFingerprintError.Unavailable(
105
+ TLSFingerprintErrorMessages.NO_FINGERPRINTS_PROVIDED,
106
+ )
107
+ }
108
+
109
+ // -----------------------------------------------------------------------------
110
+ // Multiple fingerprints
111
+ // -----------------------------------------------------------------------------
112
+
113
+ /**
114
+ * Validates the SSL certificate of a HTTPS endpoint
115
+ * using multiple allowed SHA-256 fingerprints.
116
+ *
117
+ * A match is considered valid if ANY provided fingerprint matches.
118
+ *
119
+ * @throws TLSFingerprintError.Unavailable if no fingerprints available.
120
+ */
121
+ @Throws(TLSFingerprintError::class)
122
+ fun checkCertificates(
123
+ urlString: String,
124
+ fingerprintsFromArgs: List<String>?,
125
+ ): TLSFingerprintResultModel {
126
+ val fingerprints =
127
+ fingerprintsFromArgs?.takeIf { it.isNotEmpty() }
128
+ ?: config.fingerprints.takeIf { it.isNotEmpty() }
129
+
130
+ if (fingerprints != null) {
131
+ return performCheck(
132
+ urlString = urlString,
133
+ fingerprints = fingerprints,
134
+ )
135
+ }
136
+
137
+ throw TLSFingerprintError.Unavailable(
138
+ TLSFingerprintErrorMessages.NO_FINGERPRINTS_PROVIDED,
139
+ )
140
+ }
141
+
142
+ // -----------------------------------------------------------------------------
143
+ // Shared implementation
144
+ // -----------------------------------------------------------------------------
145
+
146
+ /**
147
+ * Determines and executes the SSL fingerprint validation for the given request.
148
+ *
149
+ * Evaluation order:
150
+ *
151
+ * 1. Excluded domain → bypass validation entirely.
152
+ * 2. Fingerprint-based validation.
153
+ *
154
+ * @throws TLSFingerprintError when validation fails.
155
+ */
156
+ @Throws(TLSFingerprintError::class)
157
+ private fun performCheck(
158
+ urlString: String,
159
+ fingerprints: List<String>,
160
+ ): TLSFingerprintResultModel {
161
+ val url =
162
+ TLSFingerprintUtils.httpsUrl(urlString)
163
+ ?: throw TLSFingerprintError.UnknownType(
164
+ TLSFingerprintErrorMessages.INVALID_URL_MUST_BE_HTTPS,
165
+ )
166
+
167
+ val host = url.host
168
+
169
+ // -----------------------------------------------------------------------------
170
+ // EXCLUDED DOMAIN MODE
171
+ // -----------------------------------------------------------------------------
172
+
173
+ /**
174
+ * If the request host matches an excluded domain,
175
+ * fingerprint validation is bypassed.
176
+ * The connection uses a permissive TrustManager.
177
+ * System trust chain is NOT explicitly evaluated.
178
+ *
179
+ * Matching rules:
180
+ * - Exact match
181
+ * - Subdomain match
182
+ */
183
+ if (config.excludedDomains.any { excluded ->
184
+ val excludedLower = excluded.lowercase().trim()
185
+ val hostLower = host.lowercase().trim()
186
+ // Match exact domain or any subdomain (e.g., api.example.com matches example.com)
187
+ hostLower == excludedLower || hostLower.endsWith(".$excludedLower")
188
+ }
189
+ ) {
190
+ TLSFingerprintLogger.debug("TLSFingerprint excluded domain:", host)
191
+
192
+ try {
193
+ val certificate = getCertificate(url)
194
+ val actualFingerprint =
195
+ TLSFingerprintUtils.normalizeFingerprint(
196
+ TLSFingerprintUtils.sha256Fingerprint(certificate),
197
+ )
198
+
199
+ return TLSFingerprintResultModel(
200
+ actualFingerprint = actualFingerprint,
201
+ fingerprintMatched = true,
202
+ excludedDomain = true,
203
+ mode = "excluded",
204
+ errorCode = "EXCLUDED_DOMAIN",
205
+ error = TLSFingerprintErrorMessages.EXCLUDED_DOMAIN,
206
+ )
207
+ } catch (e: SocketTimeoutException) {
208
+ throw TLSFingerprintError.Timeout(TLSFingerprintErrorMessages.TIMEOUT)
209
+ } catch (e: IOException) {
210
+ throw TLSFingerprintError.NetworkError(TLSFingerprintErrorMessages.NETWORK_ERROR)
211
+ }
212
+ }
213
+
214
+ // -----------------------------------------------------------------------------
215
+ // FINGERPRINT MODE
216
+ // -----------------------------------------------------------------------------
217
+
218
+ /**
219
+ * Fingerprint-based pinning.
220
+ *
221
+ * Only the leaf certificate fingerprint is compared.
222
+ * The system trust chain is NOT evaluated in this mode.
223
+ */
224
+ val certificate: Certificate
225
+ try {
226
+ certificate = getCertificate(url)
227
+ } catch (e: SocketTimeoutException) {
228
+ throw TLSFingerprintError.Timeout(TLSFingerprintErrorMessages.TIMEOUT)
229
+ } catch (e: IOException) {
230
+ throw TLSFingerprintError.NetworkError(TLSFingerprintErrorMessages.NETWORK_ERROR)
231
+ }
232
+
233
+ val actualFingerprint =
234
+ TLSFingerprintUtils.normalizeFingerprint(
235
+ TLSFingerprintUtils.sha256Fingerprint(certificate),
236
+ )
237
+
238
+ val normalizedExpected =
239
+ fingerprints.map {
240
+ TLSFingerprintUtils.normalizeFingerprint(it)
241
+ }
242
+
243
+ val matchedFingerprint =
244
+ normalizedExpected.firstOrNull {
245
+ it == actualFingerprint
246
+ }
247
+
248
+ val matched = matchedFingerprint != null
249
+
250
+ val errorCode = if (matched) "" else "PINNING_FAILED"
251
+ val errorMessage = if (matched) "" else TLSFingerprintErrorMessages.PINNING_FAILED
252
+
253
+ TLSFingerprintLogger.debug(
254
+ "TLSFingerprint matched:",
255
+ matched.toString(),
256
+ )
257
+
258
+ return TLSFingerprintResultModel(
259
+ actualFingerprint = actualFingerprint,
260
+ fingerprintMatched = matched,
261
+ matchedFingerprint = matchedFingerprint,
262
+ mode = "fingerprint",
263
+ errorCode = errorCode,
264
+ error = errorMessage,
265
+ )
266
+ }
267
+
268
+ // -----------------------------------------------------------------------------
269
+ // Certificate retrieval
270
+ // -----------------------------------------------------------------------------
271
+
272
+ /**
273
+ * Opens a TLS connection and extracts
274
+ * the server leaf certificate.
275
+ *
276
+ * SECURITY MODEL:
277
+ * - A permissive TrustManager is used intentionally.
278
+ * - The system trust chain is NOT validated.
279
+ * - Validation is performed manually via fingerprint comparison.
280
+ */
281
+ @Throws(Exception::class)
282
+ private fun getCertificate(url: URL): Certificate {
283
+ val trustManagers =
284
+ arrayOf<TrustManager>(
285
+ object : X509TrustManager {
286
+ override fun getAcceptedIssuers() = emptyArray<X509Certificate>()
287
+
288
+ override fun checkClientTrusted(
289
+ certs: Array<X509Certificate>,
290
+ authType: String,
291
+ ) {}
292
+
293
+ override fun checkServerTrusted(
294
+ certs: Array<X509Certificate>,
295
+ authType: String,
296
+ ) {}
297
+ },
298
+ )
299
+
300
+ val sslContext =
301
+ SSLContext.getInstance("TLS")
302
+
303
+ sslContext.init(
304
+ null,
305
+ trustManagers,
306
+ java.security.SecureRandom(),
307
+ )
308
+
309
+ val connection =
310
+ url.openConnection() as HttpsURLConnection
311
+
312
+ try {
313
+ connection.sslSocketFactory =
314
+ sslContext.socketFactory
315
+
316
+ connection.connectTimeout = TIMEOUT_MS
317
+ connection.readTimeout = TIMEOUT_MS
318
+
319
+ connection.connect()
320
+
321
+ val certificate =
322
+ connection.serverCertificates.first()
323
+
324
+ return certificate
325
+ } finally {
326
+ try {
327
+ connection.disconnect()
328
+ } catch (_: Exception) {
329
+ // Ignore disconnect errors
330
+ }
331
+ }
332
+ }
333
+ }
@@ -0,0 +1,342 @@
1
+ package io.capkit.tlsfingerprint
2
+
3
+ import com.getcapacitor.JSArray
4
+ import com.getcapacitor.JSObject
5
+ import com.getcapacitor.Plugin
6
+ import com.getcapacitor.PluginCall
7
+ import com.getcapacitor.PluginMethod
8
+ import com.getcapacitor.annotation.CapacitorPlugin
9
+ import com.getcapacitor.annotation.Permission
10
+ import io.capkit.tlsfingerprint.config.TLSFingerprintConfig
11
+ import io.capkit.tlsfingerprint.error.TLSFingerprintError
12
+ import io.capkit.tlsfingerprint.error.TLSFingerprintErrorMessages
13
+ import io.capkit.tlsfingerprint.logger.TLSFingerprintLogger
14
+ import io.capkit.tlsfingerprint.model.TLSFingerprintResultModel
15
+ import io.capkit.tlsfingerprint.utils.TLSFingerprintUtils
16
+ import java.net.UnknownHostException
17
+ import javax.net.ssl.SSLException
18
+
19
+ /**
20
+ * Capacitor bridge for the TLSFingerprint plugin (Android).
21
+ *
22
+ * Responsibilities:
23
+ * - Parse JavaScript input
24
+ * - Call the native implementation
25
+ * - Resolve or reject PluginCall
26
+ * - Map native errors to JS-facing error codes
27
+ */
28
+ @CapacitorPlugin(
29
+ name = "TLSFingerprint",
30
+ permissions = [
31
+ Permission(
32
+ alias = "network",
33
+ strings = [android.Manifest.permission.INTERNET],
34
+ ),
35
+ ],
36
+ )
37
+ class TLSFingerprintPlugin : Plugin() {
38
+ // -----------------------------------------------------------------------------
39
+ // Properties
40
+ // -----------------------------------------------------------------------------
41
+
42
+ /**
43
+ * Immutable plugin configuration read from capacitor.config.ts.
44
+ * * CONTRACT:
45
+ * - Initialized exactly once in `load()`.
46
+ * - Treated as read-only afterwards.
47
+ */
48
+ private lateinit var config: TLSFingerprintConfig
49
+
50
+ /**
51
+ * Native implementation layer containing core Android logic.
52
+ *
53
+ * CONTRACT:
54
+ * - Owned by the Plugin layer.
55
+ * - MUST NOT access PluginCall or Capacitor bridge APIs directly.
56
+ */
57
+ private lateinit var implementation: TLSFingerprintImpl
58
+
59
+ // -----------------------------------------------------------------------------
60
+ // Companion Object
61
+ // -----------------------------------------------------------------------------
62
+
63
+ private companion object {
64
+ /**
65
+ * Account type identifier for internal plugin identification.
66
+ */
67
+ const val ACCOUNT_TYPE = "io.capkit.tlsfingerprint"
68
+
69
+ /**
70
+ * Human-readable account name for the plugin.
71
+ */
72
+ const val ACCOUNT_NAME = "TLSFingerprint"
73
+ }
74
+
75
+ // -----------------------------------------------------------------------------
76
+ // Lifecycle
77
+ // -----------------------------------------------------------------------------
78
+
79
+ /**
80
+ * Called once when the plugin is loaded by the Capacitor bridge.
81
+ *
82
+ * This method initializes the configuration container and the native
83
+ * implementation layer, ensuring all dependencies are injected.
84
+ */
85
+ override fun load() {
86
+ super.load()
87
+
88
+ config = TLSFingerprintConfig(this)
89
+ implementation = TLSFingerprintImpl(context)
90
+ implementation.updateConfig(config)
91
+
92
+ TLSFingerprintLogger.debug("Plugin loaded. Version: ", BuildConfig.PLUGIN_VERSION)
93
+ }
94
+
95
+ // -----------------------------------------------------------------------------
96
+ // Error Mapping
97
+ // -----------------------------------------------------------------------------
98
+
99
+ /**
100
+ * Rejects the call with a message and a standardized error code.
101
+ * Ensure consistency with the JS TLSFingerprintErrorCode enum.
102
+ */
103
+ private fun reject(
104
+ call: PluginCall,
105
+ error: TLSFingerprintError,
106
+ ) {
107
+ val code =
108
+ when (error) {
109
+ is TLSFingerprintError.Unavailable -> "UNAVAILABLE"
110
+ is TLSFingerprintError.Cancelled -> "CANCELLED"
111
+ is TLSFingerprintError.PermissionDenied -> "PERMISSION_DENIED"
112
+ is TLSFingerprintError.InitFailed -> "INIT_FAILED"
113
+ is TLSFingerprintError.InvalidInput -> "INVALID_INPUT"
114
+ is TLSFingerprintError.UnknownType -> "UNKNOWN_TYPE"
115
+ is TLSFingerprintError.NotFound -> "NOT_FOUND"
116
+ is TLSFingerprintError.Conflict -> "CONFLICT"
117
+ is TLSFingerprintError.Timeout -> "TIMEOUT"
118
+ is TLSFingerprintError.NetworkError -> "NETWORK_ERROR"
119
+ is TLSFingerprintError.InvalidConfig -> "INVALID_INPUT"
120
+ is TLSFingerprintError.SslError -> "SSL_ERROR"
121
+ }
122
+
123
+ // Always use the message from the TLSFingerprintError instance
124
+ val message = error.message ?: "Unknown native error"
125
+ call.reject(message, code)
126
+ }
127
+
128
+ // -----------------------------------------------------------------------------
129
+ // SSL Pinning (single fingerprint)
130
+ // -----------------------------------------------------------------------------
131
+
132
+ /**
133
+ * Validates the SSL certificate of a HTTPS endpoint
134
+ * using a single fingerprint.
135
+ */
136
+ @PluginMethod
137
+ fun checkCertificate(call: PluginCall) {
138
+ val url: String? = call.getString("url")
139
+ val fingerprint: String? = call.getString("fingerprint")
140
+
141
+ if (url.isNullOrBlank()) {
142
+ call.reject(TLSFingerprintErrorMessages.URL_REQUIRED, "INVALID_INPUT")
143
+ return
144
+ }
145
+
146
+ val parsedUrl =
147
+ try {
148
+ java.net.URL(url)
149
+ } catch (e: java.net.MalformedURLException) {
150
+ call.reject(TLSFingerprintErrorMessages.INVALID_URL, "INVALID_INPUT")
151
+ return
152
+ }
153
+
154
+ if (parsedUrl.host.isNullOrBlank()) {
155
+ call.reject(TLSFingerprintErrorMessages.NO_HOST_FOUND_IN_URL, "INVALID_INPUT")
156
+ return
157
+ }
158
+
159
+ if (fingerprint != null && !TLSFingerprintUtils.isValidFingerprintFormat(fingerprint)) {
160
+ call.reject(TLSFingerprintErrorMessages.INVALID_FINGERPRINT_FORMAT, "INVALID_INPUT")
161
+ return
162
+ }
163
+
164
+ try {
165
+ execute {
166
+ try {
167
+ val result: TLSFingerprintResultModel =
168
+ implementation.checkCertificate(
169
+ urlString = url ?: "",
170
+ fingerprintFromArgs = fingerprint,
171
+ )
172
+
173
+ val jsResult = JSObject()
174
+ jsResult.put("actualFingerprint", result.actualFingerprint)
175
+ jsResult.put("fingerprintMatched", result.fingerprintMatched)
176
+ jsResult.put("matchedFingerprint", result.matchedFingerprint)
177
+ jsResult.put("excludedDomain", result.excludedDomain)
178
+ jsResult.put("mode", result.mode)
179
+ jsResult.put("error", result.error)
180
+ jsResult.put("errorCode", result.errorCode)
181
+
182
+ call.resolve(jsResult)
183
+ } catch (error: TLSFingerprintError) {
184
+ reject(call, error)
185
+ } catch (error: IllegalArgumentException) {
186
+ call.reject(
187
+ error.message ?: "Invalid input",
188
+ "INVALID_INPUT",
189
+ )
190
+ } catch (error: SSLException) {
191
+ reject(call, TLSFingerprintError.SslError(error.message ?: "SSL/TLS error"))
192
+ } catch (error: UnknownHostException) {
193
+ call.reject(
194
+ error.message ?: "Unknown host",
195
+ "NETWORK_ERROR",
196
+ )
197
+ } catch (error: Exception) {
198
+ call.reject(
199
+ error.message ?: TLSFingerprintErrorMessages.INTERNAL_ERROR,
200
+ "INIT_FAILED",
201
+ )
202
+ }
203
+ }
204
+ } catch (error: IllegalArgumentException) {
205
+ call.reject(
206
+ error.message ?: "Invalid input",
207
+ "INVALID_INPUT",
208
+ )
209
+ } catch (error: Exception) {
210
+ call.reject(
211
+ error.message ?: TLSFingerprintErrorMessages.INTERNAL_ERROR,
212
+ "INIT_FAILED",
213
+ )
214
+ }
215
+ }
216
+
217
+ // -----------------------------------------------------------------------------
218
+ // SSL Pinning (multiple fingerprints)
219
+ // -----------------------------------------------------------------------------
220
+
221
+ /**
222
+ * Validates the SSL certificate of a HTTPS endpoint
223
+ * using multiple allowed fingerprints.
224
+ */
225
+ @PluginMethod
226
+ fun checkCertificates(call: PluginCall) {
227
+ val url: String? = call.getString("url")
228
+
229
+ val jsArray: JSArray? = call.getArray("fingerprints")
230
+
231
+ // Parsing JSArray to a clean Kotlin List
232
+ val fingerprints: List<String>? =
233
+ if (jsArray != null && jsArray.length() > 0) {
234
+ val list = ArrayList<String>()
235
+ for (i in 0 until jsArray.length()) {
236
+ val value = jsArray.getString(i)
237
+ if (!value.isNullOrBlank()) {
238
+ list.add(value)
239
+ }
240
+ }
241
+ if (list.isNotEmpty()) list else null
242
+ } else {
243
+ null
244
+ }
245
+
246
+ if (url.isNullOrBlank()) {
247
+ call.reject(TLSFingerprintErrorMessages.URL_REQUIRED, "INVALID_INPUT")
248
+ return
249
+ }
250
+
251
+ val parsedUrl =
252
+ try {
253
+ java.net.URL(url)
254
+ } catch (e: java.net.MalformedURLException) {
255
+ call.reject(TLSFingerprintErrorMessages.INVALID_URL, "INVALID_INPUT")
256
+ return
257
+ }
258
+
259
+ if (parsedUrl.host.isNullOrBlank()) {
260
+ call.reject(TLSFingerprintErrorMessages.NO_HOST_FOUND_IN_URL, "INVALID_INPUT")
261
+ return
262
+ }
263
+
264
+ fingerprints?.forEach { fp ->
265
+ if (!TLSFingerprintUtils.isValidFingerprintFormat(fp)) {
266
+ call.reject(TLSFingerprintErrorMessages.INVALID_FINGERPRINT_FORMAT, "INVALID_INPUT")
267
+ return
268
+ }
269
+ }
270
+
271
+ try {
272
+ execute {
273
+ try {
274
+ val result: TLSFingerprintResultModel =
275
+ implementation.checkCertificates(
276
+ urlString = url ?: "",
277
+ fingerprintsFromArgs = fingerprints,
278
+ )
279
+
280
+ val jsResult = JSObject()
281
+ jsResult.put("actualFingerprint", result.actualFingerprint)
282
+ jsResult.put("fingerprintMatched", result.fingerprintMatched)
283
+ jsResult.put("matchedFingerprint", result.matchedFingerprint)
284
+ jsResult.put("excludedDomain", result.excludedDomain)
285
+ jsResult.put("mode", result.mode)
286
+ jsResult.put("error", result.error)
287
+ jsResult.put("errorCode", result.errorCode)
288
+
289
+ call.resolve(jsResult)
290
+ } catch (error: TLSFingerprintError) {
291
+ reject(call, error)
292
+ } catch (error: IllegalArgumentException) {
293
+ call.reject(
294
+ error.message ?: "Invalid input",
295
+ "INVALID_INPUT",
296
+ )
297
+ } catch (error: SSLException) {
298
+ reject(call, TLSFingerprintError.SslError(error.message ?: "SSL/TLS error"))
299
+ } catch (error: UnknownHostException) {
300
+ call.reject(
301
+ error.message ?: "Unknown host",
302
+ "NETWORK_ERROR",
303
+ )
304
+ } catch (error: Exception) {
305
+ call.reject(
306
+ error.message ?: TLSFingerprintErrorMessages.INTERNAL_ERROR,
307
+ "INIT_FAILED",
308
+ )
309
+ }
310
+ }
311
+ } catch (error: IllegalArgumentException) {
312
+ call.reject(
313
+ error.message ?: "Invalid input",
314
+ "INVALID_INPUT",
315
+ )
316
+ } catch (error: Exception) {
317
+ call.reject(
318
+ error.message ?: TLSFingerprintErrorMessages.INTERNAL_ERROR,
319
+ "INIT_FAILED",
320
+ )
321
+ }
322
+ }
323
+
324
+ // -----------------------------------------------------------------------------
325
+ // Version
326
+ // -----------------------------------------------------------------------------
327
+
328
+ /**
329
+ * Returns the native plugin version.
330
+ *
331
+ * NOTE:
332
+ * - This method is guaranteed not to fail
333
+ * - Therefore it does NOT use TestError
334
+ * - Version is injected at build time from package.json
335
+ */
336
+ @PluginMethod
337
+ fun getPluginVersion(call: PluginCall) {
338
+ val ret = JSObject()
339
+ ret.put("version", BuildConfig.PLUGIN_VERSION)
340
+ call.resolve(ret)
341
+ }
342
+ }