@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.
- package/CapKitTlsFingerprint.podspec +17 -0
- package/LICENSE +21 -0
- package/Package.swift +25 -0
- package/README.md +427 -0
- package/android/build.gradle +103 -0
- package/android/src/main/AndroidManifest.xml +3 -0
- package/android/src/main/java/io/capkit/settings/TLSFingerprintImpl.kt +333 -0
- package/android/src/main/java/io/capkit/settings/TLSFingerprintPlugin.kt +342 -0
- package/android/src/main/java/io/capkit/settings/config/TLSFingerprintConfig.kt +102 -0
- package/android/src/main/java/io/capkit/settings/error/TLSFingerprintError.kt +114 -0
- package/android/src/main/java/io/capkit/settings/error/TLSFingerprintErrorMessages.kt +27 -0
- package/android/src/main/java/io/capkit/settings/logger/TLSFingerprintLogger.kt +85 -0
- package/android/src/main/java/io/capkit/settings/model/TLSFingerprintResultModel.kt +32 -0
- package/android/src/main/java/io/capkit/settings/utils/TLSFingerprintUtils.kt +91 -0
- package/android/src/main/res/.gitkeep +0 -0
- package/dist/cli/fingerprint.js +163 -0
- package/dist/cli/fingerprint.js.map +1 -0
- package/dist/docs.json +386 -0
- package/dist/esm/cli/fingerprint.d.ts +1 -0
- package/dist/esm/cli/fingerprint.js +161 -0
- package/dist/esm/cli/fingerprint.js.map +1 -0
- package/dist/esm/definitions.d.ts +244 -0
- package/dist/esm/definitions.js +42 -0
- package/dist/esm/definitions.js.map +1 -0
- package/dist/esm/index.d.ts +13 -0
- package/dist/esm/index.js +11 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/version.d.ts +1 -0
- package/dist/esm/version.js +3 -0
- package/dist/esm/version.js.map +1 -0
- package/dist/esm/web.d.ts +33 -0
- package/dist/esm/web.js +47 -0
- package/dist/esm/web.js.map +1 -0
- package/dist/plugin.cjs +107 -0
- package/dist/plugin.cjs.map +1 -0
- package/dist/plugin.js +110 -0
- package/dist/plugin.js.map +1 -0
- package/ios/Sources/TLSFingerprintPlugin/TLSFingerprintDelegate.swift +365 -0
- package/ios/Sources/TLSFingerprintPlugin/TLSFingerprintImpl.swift +275 -0
- package/ios/Sources/TLSFingerprintPlugin/TLSFingerprintPlugin.swift +219 -0
- package/ios/Sources/TLSFingerprintPlugin/Version.swift +16 -0
- package/ios/Sources/TLSFingerprintPlugin/config/TLSFingerprintConfig.swift +114 -0
- package/ios/Sources/TLSFingerprintPlugin/error/TLSFingerprintError.swift +107 -0
- package/ios/Sources/TLSFingerprintPlugin/error/TLSFingerprintErrorMessages.swift +30 -0
- package/ios/Sources/TLSFingerprintPlugin/logger/TLSFingerprintLogger.swift +69 -0
- package/ios/Sources/TLSFingerprintPlugin/model/TLSFingerprintResult.swift +76 -0
- package/ios/Sources/TLSFingerprintPlugin/utils/TLSFingerprintUtils.swift +79 -0
- package/ios/Tests/TLSFingerprintPluginTests/TLSFingerprintPluginTests.swift +15 -0
- package/package.json +131 -0
- package/scripts/chmod.mjs +34 -0
- 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
|
+
}
|