@cap-kit/ssl-pinning 8.0.0-next.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 (42) hide show
  1. package/CapKitSSLPinning.podspec +17 -0
  2. package/LICENSE +21 -0
  3. package/Package.swift +25 -0
  4. package/README.md +750 -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/sslpinning/SSLPinningConfig.kt +22 -0
  8. package/android/src/main/java/io/capkit/sslpinning/SSLPinningImpl.kt +188 -0
  9. package/android/src/main/java/io/capkit/sslpinning/SSLPinningPlugin.kt +82 -0
  10. package/android/src/main/java/io/capkit/sslpinning/utils/SSLPinningLogger.kt +85 -0
  11. package/android/src/main/java/io/capkit/sslpinning/utils/SSLPinningUtils.kt +44 -0
  12. package/android/src/main/res/.gitkeep +0 -0
  13. package/dist/cli/fingerprint.js +163 -0
  14. package/dist/cli/fingerprint.js.map +1 -0
  15. package/dist/docs.json +430 -0
  16. package/dist/esm/cli/fingerprint.d.ts +1 -0
  17. package/dist/esm/cli/fingerprint.js +161 -0
  18. package/dist/esm/cli/fingerprint.js.map +1 -0
  19. package/dist/esm/definitions.d.ts +285 -0
  20. package/dist/esm/definitions.js +18 -0
  21. package/dist/esm/definitions.js.map +1 -0
  22. package/dist/esm/index.d.ts +15 -0
  23. package/dist/esm/index.js +16 -0
  24. package/dist/esm/index.js.map +1 -0
  25. package/dist/esm/web.d.ts +58 -0
  26. package/dist/esm/web.js +54 -0
  27. package/dist/esm/web.js.map +1 -0
  28. package/dist/plugin.cjs.js +95 -0
  29. package/dist/plugin.cjs.js.map +1 -0
  30. package/dist/plugin.js +98 -0
  31. package/dist/plugin.js.map +1 -0
  32. package/ios/Sources/SSLPinningPlugin/SSLPinningConfig.swift +79 -0
  33. package/ios/Sources/SSLPinningPlugin/SSLPinningDelegate.swift +81 -0
  34. package/ios/Sources/SSLPinningPlugin/SSLPinningImpl.swift +111 -0
  35. package/ios/Sources/SSLPinningPlugin/SSLPinningPlugin.swift +116 -0
  36. package/ios/Sources/SSLPinningPlugin/Utils/SSLPinningLogger.swift +57 -0
  37. package/ios/Sources/SSLPinningPlugin/Utils/SSLPinningUtils.swift +47 -0
  38. package/ios/Sources/SSLPinningPlugin/Version.swift +16 -0
  39. package/ios/Tests/SSLPinningPluginTests/SSLPinningPluginTests.swift +5 -0
  40. package/package.json +117 -0
  41. package/scripts/chmod.js +34 -0
  42. package/scripts/sync-version.js +49 -0
@@ -0,0 +1,103 @@
1
+ buildscript {
2
+ ext.kotlin_version = project.hasProperty("kotlin_version") ? rootProject.ext.kotlin_version : '2.2.20'
3
+ repositories {
4
+ google()
5
+ mavenCentral()
6
+ }
7
+ dependencies {
8
+ classpath 'com.android.tools.build:gradle:8.13.0'
9
+ classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
10
+ }
11
+ }
12
+
13
+ plugins {
14
+ id "org.jlleitschuh.gradle.ktlint" version "12.1.1" apply false
15
+ }
16
+
17
+ ext {
18
+ junitVersion = project.hasProperty('junitVersion') ? rootProject.ext.junitVersion : '4.13.2'
19
+ androidxAppCompatVersion = project.hasProperty('androidxAppCompatVersion') ? rootProject.ext.androidxAppCompatVersion : '1.7.1'
20
+ androidxJunitVersion = project.hasProperty('androidxJunitVersion') ? rootProject.ext.androidxJunitVersion : '1.3.0'
21
+ androidxEspressoCoreVersion = project.hasProperty('androidxEspressoCoreVersion') ? rootProject.ext.androidxEspressoCoreVersion : '3.7.0'
22
+ androidxCoreKTXVersion = project.hasProperty('androidxCoreKTXVersion') ? rootProject.ext.androidxCoreKTXVersion : '1.17.0'
23
+ }
24
+
25
+ apply plugin: 'com.android.library'
26
+ apply plugin: 'kotlin-android'
27
+ apply plugin: 'kotlin-parcelize'
28
+ apply plugin: 'org.jlleitschuh.gradle.ktlint'
29
+
30
+ import groovy.json.JsonSlurper
31
+
32
+ def getPluginVersion() {
33
+ try {
34
+ def packageJsonFile = file('../package.json')
35
+ if (packageJsonFile.exists()) {
36
+ def packageJson = new JsonSlurper().parseText(packageJsonFile.text)
37
+ return packageJson.version
38
+ }
39
+ } catch (Exception e) {
40
+ // Ignore errors and fallback
41
+ }
42
+ return "0.0.15" // Fallback version
43
+ }
44
+
45
+ def pluginVersion = getPluginVersion()
46
+
47
+ android {
48
+ namespace = "io.capkit.sslpinning"
49
+ compileSdk = project.hasProperty('compileSdkVersion') ? rootProject.ext.compileSdkVersion as Integer : 36
50
+
51
+ // AGP 8.0+ disables BuildConfig by default for libraries.
52
+ // We need to enable it to inject the plugin version.
53
+ buildFeatures {
54
+ buildConfig = true
55
+ }
56
+
57
+ defaultConfig {
58
+ minSdkVersion = project.hasProperty('minSdkVersion') ? rootProject.ext.minSdkVersion as Integer : 24
59
+ targetSdkVersion = project.hasProperty('targetSdkVersion') ? rootProject.ext.targetSdkVersion as Integer : 36
60
+ versionCode = 1
61
+
62
+ // Dynamic versioning (feature enabled)
63
+ versionName = pluginVersion
64
+
65
+ // Injects the version into the BuildConfig class ONLY if feature is enabled
66
+ buildConfigField "String", "PLUGIN_VERSION", "\"${pluginVersion}\""
67
+
68
+ testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
69
+ }
70
+ buildTypes {
71
+ release {
72
+ minifyEnabled = false
73
+ proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
74
+ }
75
+ }
76
+ lint {
77
+ abortOnError = false
78
+ }
79
+ compileOptions {
80
+ sourceCompatibility = JavaVersion.VERSION_21
81
+ targetCompatibility = JavaVersion.VERSION_21
82
+ }
83
+ kotlinOptions {
84
+ jvmTarget = JavaVersion.VERSION_21
85
+ }
86
+ }
87
+
88
+ repositories {
89
+ google()
90
+ mavenCentral()
91
+ }
92
+
93
+ dependencies {
94
+ implementation fileTree(dir: 'libs', include: ['*.jar'])
95
+ implementation project(':capacitor-android')
96
+ implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion"
97
+ implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
98
+ implementation "androidx.core:core-ktx:$androidxCoreKTXVersion"
99
+
100
+ testImplementation "junit:junit:$junitVersion"
101
+ androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion"
102
+ androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion"
103
+ }
@@ -0,0 +1,3 @@
1
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android">
2
+ <uses-permission android:name="android.permission.INTERNET"/>
3
+ </manifest>
@@ -0,0 +1,22 @@
1
+ package io.capkit.sslpinning
2
+
3
+ import com.getcapacitor.Plugin
4
+
5
+ class SSLPinningConfig(plugin: Plugin) {
6
+ val verboseLogging: Boolean
7
+ val fingerprint: String?
8
+ val fingerprints: List<String>
9
+
10
+ init {
11
+ val config = plugin.getConfig()
12
+
13
+ verboseLogging = config.getBoolean("verboseLogging", false)
14
+
15
+ val fp = config.getString("fingerprint")
16
+ fingerprint = if (fp.isNullOrBlank()) null else fp
17
+
18
+ fingerprints =
19
+ config.getArray("fingerprints")?.toList()?.mapNotNull { it as? String }
20
+ ?: emptyList()
21
+ }
22
+ }
@@ -0,0 +1,188 @@
1
+ package io.capkit.sslpinning
2
+
3
+ import io.capkit.sslpinning.utils.SSLPinningLogger
4
+ import io.capkit.sslpinning.utils.SSLPinningUtils
5
+ import java.net.URL
6
+ import java.security.cert.Certificate
7
+ import javax.net.ssl.HttpsURLConnection
8
+ import javax.net.ssl.SSLContext
9
+ import javax.net.ssl.TrustManager
10
+ import javax.net.ssl.X509TrustManager
11
+
12
+ class SSLPinningImpl(
13
+ private val config: SSLPinningConfig,
14
+ ) {
15
+ // ---- Single fingerprint ----
16
+
17
+ /**
18
+ * Validates the SSL certificate of a HTTPS endpoint using a single SHA-256 fingerprint.
19
+ *
20
+ * This method:
21
+ * - Opens a TLS connection to the given URL
22
+ * - Extracts the leaf certificate presented by the server
23
+ * - Computes its SHA-256 fingerprint
24
+ * - Compares it against the provided or configured fingerprint
25
+ *
26
+ * NOTE:
27
+ * - No HTTP request body is sent
28
+ * - The certificate trust chain is NOT validated
29
+ * - Only the leaf certificate fingerprint is checked
30
+ */
31
+ fun checkCertificate(
32
+ urlString: String,
33
+ fingerprintFromArgs: String?,
34
+ callback: (Map<String, Any>) -> Unit,
35
+ ) {
36
+ val fingerprint =
37
+ fingerprintFromArgs ?: config.fingerprint
38
+
39
+ if (fingerprint == null) {
40
+ callback(
41
+ mapOf(
42
+ "fingerprintMatched" to false,
43
+ "error" to "No fingerprint provided (args or config)",
44
+ ),
45
+ )
46
+ return
47
+ }
48
+
49
+ performCheck(urlString, listOf(fingerprint), callback)
50
+ }
51
+
52
+ // ---- Multiple fingerprints ----
53
+
54
+ /**
55
+ * Validates the SSL certificate of a HTTPS endpoint using multiple allowed fingerprints.
56
+ *
57
+ * The certificate is considered valid if **any** of the provided fingerprints
58
+ * matches the server's leaf certificate fingerprint.
59
+ *
60
+ * This is typically used to support certificate rotation.
61
+ */
62
+ fun checkCertificates(
63
+ urlString: String,
64
+ fingerprintsFromArgs: List<String>?,
65
+ callback: (Map<String, Any>) -> Unit,
66
+ ) {
67
+ val fingerprints =
68
+ fingerprintsFromArgs?.takeIf { it.isNotEmpty() }
69
+ ?: config.fingerprints.takeIf { it.isNotEmpty() }
70
+
71
+ if (fingerprints == null) {
72
+ callback(
73
+ mapOf(
74
+ "fingerprintMatched" to false,
75
+ "error" to "No fingerprints provided (args or config)",
76
+ ),
77
+ )
78
+ return
79
+ }
80
+
81
+ performCheck(urlString, fingerprints, callback)
82
+ }
83
+
84
+ // ---- Shared implementation ----
85
+
86
+ /**
87
+ * Shared internal implementation for SSL pinning validation.
88
+ *
89
+ * This method performs the actual TLS handshake and fingerprint comparison.
90
+ * It is intentionally isolated to avoid duplication between
91
+ * single and multi fingerprint modes.
92
+ */
93
+ private fun performCheck(
94
+ urlString: String,
95
+ fingerprints: List<String>,
96
+ callback: (Map<String, Any>) -> Unit,
97
+ ) {
98
+ val url = SSLPinningUtils.httpsUrl(urlString)
99
+ if (url == null) {
100
+ callback(
101
+ mapOf(
102
+ "fingerprintMatched" to false,
103
+ "error" to "Invalid HTTPS URL",
104
+ "errorCode" to "UNKNOWN_TYPE",
105
+ ),
106
+ )
107
+ return
108
+ }
109
+
110
+ try {
111
+ val cert = getCertificate(url)
112
+ val actualFingerprint =
113
+ SSLPinningUtils.normalizeFingerprint(
114
+ SSLPinningUtils.sha256Fingerprint(cert),
115
+ )
116
+
117
+ val normalizedExpected =
118
+ fingerprints.map { SSLPinningUtils.normalizeFingerprint(it) }
119
+
120
+ val matchedFingerprint =
121
+ normalizedExpected.firstOrNull { it == actualFingerprint }
122
+
123
+ val matched = matchedFingerprint != null
124
+
125
+ SSLPinningLogger.debug("SSLPinning matched:", matched.toString())
126
+
127
+ callback(
128
+ mapOf(
129
+ "actualFingerprint" to actualFingerprint,
130
+ "fingerprintMatched" to matched,
131
+ "matchedFingerprint" to (matchedFingerprint ?: ""),
132
+ ),
133
+ )
134
+ } catch (e: Exception) {
135
+ SSLPinningLogger.error("Certificate check failed", e)
136
+ callback(
137
+ mapOf(
138
+ "fingerprintMatched" to false,
139
+ "error" to e.message.orEmpty(),
140
+ "errorCode" to "INIT_FAILED",
141
+ ),
142
+ )
143
+ }
144
+ }
145
+
146
+ // ---- Certificate retrieval ----
147
+
148
+ /**
149
+ * Opens a TLS connection and extracts the server leaf certificate.
150
+ *
151
+ * A permissive TrustManager is intentionally used to allow
152
+ * inspection of the certificate without enforcing trust validation.
153
+ *
154
+ * SECURITY NOTE:
155
+ * This does NOT bypass SSL pinning security, because the fingerprint
156
+ * comparison is performed manually after extraction.
157
+ */
158
+ private fun getCertificate(url: URL): Certificate {
159
+ val trustManagers =
160
+ arrayOf<TrustManager>(
161
+ object : X509TrustManager {
162
+ override fun getAcceptedIssuers() = arrayOf<java.security.cert.X509Certificate>()
163
+
164
+ override fun checkClientTrusted(
165
+ certs: Array<java.security.cert.X509Certificate>,
166
+ authType: String,
167
+ ) {}
168
+
169
+ override fun checkServerTrusted(
170
+ certs: Array<java.security.cert.X509Certificate>,
171
+ authType: String,
172
+ ) {}
173
+ },
174
+ )
175
+
176
+ val sslContext = SSLContext.getInstance("TLS")
177
+ sslContext.init(null, trustManagers, java.security.SecureRandom())
178
+
179
+ val connection = url.openConnection() as HttpsURLConnection
180
+ connection.sslSocketFactory = sslContext.socketFactory
181
+ connection.connect()
182
+
183
+ val cert = connection.serverCertificates.first()
184
+ connection.disconnect()
185
+
186
+ return cert
187
+ }
188
+ }
@@ -0,0 +1,82 @@
1
+ package io.capkit.sslpinning
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 io.capkit.sslpinning.utils.SSLPinningLogger
10
+
11
+ @CapacitorPlugin(name = "SSLPinning")
12
+ class SSLPinningPlugin : Plugin() {
13
+ private lateinit var implementation: SSLPinningImpl
14
+
15
+ override fun load() {
16
+ val config = SSLPinningConfig(this)
17
+ SSLPinningLogger.verbose = config.verboseLogging
18
+ implementation = SSLPinningImpl(config)
19
+ }
20
+
21
+ @PluginMethod
22
+ fun checkCertificate(call: PluginCall) {
23
+ val url = call.getString("url") ?: ""
24
+ val fingerprint = call.getString("fingerprint")
25
+
26
+ if (url.isEmpty()) {
27
+ val result = JSObject()
28
+ result.put("fingerprintMatched", false)
29
+ result.put("error", "Missing url")
30
+ call.resolve(result)
31
+ return
32
+ }
33
+
34
+ execute {
35
+ implementation.checkCertificate(url, fingerprint) { data ->
36
+ val result = JSObject()
37
+ for (entry in data.entries) {
38
+ result.put(entry.key, entry.value)
39
+ }
40
+ call.resolve(result)
41
+ }
42
+ }
43
+ }
44
+
45
+ @PluginMethod
46
+ fun checkCertificates(call: PluginCall) {
47
+ val url = call.getString("url") ?: ""
48
+
49
+ val jsArray: JSArray? = call.getArray("fingerprints")
50
+ val fingerprints: List<String>? =
51
+ if (jsArray != null && jsArray.length() > 0) {
52
+ val list = ArrayList<String>()
53
+ for (i in 0 until jsArray.length()) {
54
+ val value = jsArray.getString(i)
55
+ if (!value.isNullOrEmpty()) {
56
+ list.add(value)
57
+ }
58
+ }
59
+ if (list.isNotEmpty()) list else null
60
+ } else {
61
+ null
62
+ }
63
+
64
+ if (url.isEmpty()) {
65
+ val result = JSObject()
66
+ result.put("fingerprintMatched", false)
67
+ result.put("error", "Missing url")
68
+ call.resolve(result)
69
+ return
70
+ }
71
+
72
+ execute {
73
+ implementation.checkCertificates(url, fingerprints) { data ->
74
+ val result = JSObject()
75
+ for (entry in data.entries) {
76
+ result.put(entry.key, entry.value)
77
+ }
78
+ call.resolve(result)
79
+ }
80
+ }
81
+ }
82
+ }
@@ -0,0 +1,85 @@
1
+ package io.capkit.sslpinning.utils
2
+
3
+ import android.util.Log
4
+
5
+ /**
6
+ * Centralized logging utility for the SSLPinning plugin.
7
+ *
8
+ * This logging provides a single entry point for all native logs
9
+ * and supports runtime-controlled verbose logging.
10
+ *
11
+ * The goal is to avoid scattering `if (verbose)` checks across
12
+ * business logic and keep logging behavior consistent.
13
+ */
14
+ object SSLPinningLogger {
15
+ /**
16
+ * Logcat tag used for all plugin logs.
17
+ * Helps filtering logs during debugging.
18
+ */
19
+ private const val TAG = "⚡️ SSLPinning"
20
+
21
+ /**
22
+ * Controls whether debug logs are printed.
23
+ *
24
+ * This flag should be set once during plugin initialization
25
+ * based on configuration values.
26
+ */
27
+ var verbose: Boolean = false
28
+
29
+ /**
30
+ * Prints a debug / verbose log message.
31
+ *
32
+ * This method should be used for development-time diagnostics
33
+ * and is automatically silenced when [verbose] is false.
34
+ *
35
+ * @param messages One or more message fragments to be concatenated.
36
+ */
37
+ fun debug(vararg messages: String) {
38
+ if (verbose) {
39
+ log(TAG, Log.DEBUG, *messages)
40
+ }
41
+ }
42
+
43
+ /**
44
+ * Prints an error log message.
45
+ *
46
+ * Error logs are always printed regardless of [verbose] state.
47
+ *
48
+ * @param message Human-readable error description.
49
+ * @param e Optional exception for stack trace logging.
50
+ */
51
+ fun error(
52
+ message: String,
53
+ e: Throwable? = null,
54
+ ) {
55
+ val sb = StringBuilder(message)
56
+ if (e != null) {
57
+ sb.append(" | Error: ").append(e.message)
58
+ }
59
+ Log.e(TAG, sb.toString(), e)
60
+ }
61
+
62
+ /**
63
+ * Internal low-level log dispatcher.
64
+ *
65
+ * Joins message fragments and forwards them to Android's Log API
66
+ * using the specified priority.
67
+ */
68
+ fun log(
69
+ tag: String,
70
+ level: Int,
71
+ vararg messages: String,
72
+ ) {
73
+ val sb = StringBuilder()
74
+ for (msg in messages) {
75
+ sb.append(msg).append(" ")
76
+ }
77
+ when (level) {
78
+ Log.DEBUG -> Log.d(tag, sb.toString())
79
+ Log.INFO -> Log.i(tag, sb.toString())
80
+ Log.WARN -> Log.w(tag, sb.toString())
81
+ Log.ERROR -> Log.e(tag, sb.toString())
82
+ else -> Log.v(tag, sb.toString())
83
+ }
84
+ }
85
+ }
@@ -0,0 +1,44 @@
1
+ package io.capkit.sslpinning.utils
2
+
3
+ import java.net.URL
4
+ import java.security.MessageDigest
5
+ import java.security.cert.Certificate
6
+
7
+ object SSLPinningUtils {
8
+ /**
9
+ * Validates that the provided string is a valid HTTPS URL.
10
+ *
11
+ * Non-HTTPS URLs are explicitly rejected to prevent insecure usage.
12
+ */
13
+ fun httpsUrl(value: String): URL? {
14
+ return try {
15
+ val url = URL(value)
16
+ if (url.protocol == "https") url else null
17
+ } catch (_: Exception) {
18
+ null
19
+ }
20
+ }
21
+
22
+ /**
23
+ * Normalizes a fingerprint string by:
24
+ * - Removing colon separators
25
+ * - Converting to lowercase
26
+ *
27
+ * This allows consistent comparison across platforms.
28
+ */
29
+ fun normalizeFingerprint(value: String): String {
30
+ return value.replace(":", "").lowercase()
31
+ }
32
+
33
+ /**
34
+ * Computes the SHA-256 fingerprint of an X.509 certificate.
35
+ *
36
+ * The returned format uses colon-separated hexadecimal pairs,
37
+ * matching common OpenSSL representations.
38
+ */
39
+ fun sha256Fingerprint(cert: Certificate): String {
40
+ val md = MessageDigest.getInstance("SHA-256")
41
+ val digest = md.digest(cert.encoded)
42
+ return digest.joinToString(":") { "%02x".format(it) }
43
+ }
44
+ }
File without changes
@@ -0,0 +1,163 @@
1
+ #!/usr/bin/env node
2
+ import crypto from 'crypto';
3
+ import fs from 'fs/promises';
4
+ import https from 'https';
5
+ import { createRequire } from 'module';
6
+ import yargs from 'yargs';
7
+ import { hideBin } from 'yargs/helpers';
8
+
9
+ const require$1 = createRequire(import.meta.url);
10
+ const pkg = require$1('../../package.json');
11
+ async function getCertificate(domain, insecure) {
12
+ return new Promise((resolve, reject) => {
13
+ const options = {
14
+ host: domain,
15
+ port: 443,
16
+ method: 'GET',
17
+ rejectUnauthorized: !insecure,
18
+ };
19
+ const req = https.request(options, (res) => {
20
+ var _a, _b, _c;
21
+ const socket = res.socket;
22
+ const cert = (_a = socket === null || socket === void 0 ? void 0 : socket.getPeerCertificate) === null || _a === void 0 ? void 0 : _a.call(socket, true);
23
+ if (!(cert === null || cert === void 0 ? void 0 : cert.raw)) {
24
+ reject(new Error('Unable to retrieve peer certificate'));
25
+ return;
26
+ }
27
+ const fingerprint = crypto
28
+ .createHash('sha256')
29
+ .update(cert.raw)
30
+ .digest('hex')
31
+ .match(/.{2}/g)
32
+ .join(':')
33
+ .toUpperCase();
34
+ resolve({
35
+ domain,
36
+ subject: (_b = cert.subject) !== null && _b !== void 0 ? _b : {},
37
+ issuer: (_c = cert.issuer) !== null && _c !== void 0 ? _c : {},
38
+ validFrom: cert.valid_from,
39
+ validTo: cert.valid_to,
40
+ fingerprint,
41
+ });
42
+ });
43
+ req.on('error', reject);
44
+ req.end();
45
+ });
46
+ }
47
+ function formatOutput(results, mode, format) {
48
+ const fingerprints = results.map((r) => r.fingerprint);
49
+ switch (format) {
50
+ case 'fingerprints':
51
+ return `export const fingerprints = ${JSON.stringify(fingerprints, null, 2)};`;
52
+ case 'capacitor': {
53
+ if (mode === 'single') {
54
+ return `plugins: {
55
+ SSLPinning: {
56
+ fingerprint: "${fingerprints[0]}"
57
+ }
58
+ }`;
59
+ }
60
+ return `plugins: {
61
+ SSLPinning: {
62
+ fingerprints: ${JSON.stringify(fingerprints, null, 4)}
63
+ }
64
+ }`;
65
+ }
66
+ case 'capacitor-plugin': {
67
+ if (mode === 'single') {
68
+ return `SSLPinning: {
69
+ fingerprint: "${fingerprints[0]}"
70
+ }`;
71
+ }
72
+ return `SSLPinning: {
73
+ fingerprints: ${JSON.stringify(fingerprints, null, 4)}
74
+ }`;
75
+ }
76
+ case 'capacitor-json': {
77
+ if (mode === 'single') {
78
+ return JSON.stringify({
79
+ plugins: {
80
+ SSLPinning: {
81
+ fingerprint: fingerprints[0],
82
+ },
83
+ },
84
+ }, null, 2);
85
+ }
86
+ return JSON.stringify({
87
+ plugins: {
88
+ SSLPinning: {
89
+ fingerprints,
90
+ },
91
+ },
92
+ }, null, 2);
93
+ }
94
+ case 'json':
95
+ default:
96
+ return JSON.stringify(results, null, 2);
97
+ }
98
+ }
99
+ async function main() {
100
+ var _a, _b, _c;
101
+ const argv = await yargs(hideBin(process.argv))
102
+ .usage('Usage: $0 <domains...> [options]')
103
+ .version(pkg.version)
104
+ .option('out', {
105
+ alias: 'o',
106
+ type: 'string',
107
+ description: 'Output file path',
108
+ })
109
+ .option('format', {
110
+ alias: 'f',
111
+ type: 'string',
112
+ choices: ['json', 'fingerprints', 'capacitor', 'capacitor-plugin', 'capacitor-json'],
113
+ default: 'json',
114
+ description: 'Output format',
115
+ })
116
+ .option('mode', {
117
+ type: 'string',
118
+ choices: ['single', 'multi'],
119
+ default: 'single',
120
+ description: 'Fingerprint mode (single or multi)',
121
+ })
122
+ .option('insecure', {
123
+ type: 'boolean',
124
+ default: true,
125
+ description: 'Allow insecure TLS connections (disables certificate validation)',
126
+ })
127
+ .demandCommand(1, 'At least one domain is required')
128
+ .help().argv;
129
+ const domains = argv._;
130
+ const results = [];
131
+ console.log('Fetching certificates...\n');
132
+ for (const domain of domains) {
133
+ try {
134
+ const certInfo = await getCertificate(domain, argv.insecure);
135
+ results.push(certInfo);
136
+ console.log(`Domain: ${certInfo.domain}`);
137
+ console.log(`Subject: ${(_a = certInfo.subject.CN) !== null && _a !== void 0 ? _a : '-'}`);
138
+ console.log(`Issuer: ${(_b = certInfo.issuer.CN) !== null && _b !== void 0 ? _b : '-'}`);
139
+ console.log(`Valid From: ${certInfo.validFrom}`);
140
+ console.log(`Valid To: ${certInfo.validTo}`);
141
+ console.log(`SHA256 Fingerprint: ${certInfo.fingerprint}`);
142
+ console.log('-------------------\n');
143
+ }
144
+ catch (err) {
145
+ console.error(`Error fetching cert for ${domain}: ${(_c = err === null || err === void 0 ? void 0 : err.message) !== null && _c !== void 0 ? _c : err}`);
146
+ console.log('-------------------\n');
147
+ }
148
+ }
149
+ const output = formatOutput(results, argv.mode, argv.format);
150
+ if (argv.out) {
151
+ await fs.writeFile(argv.out, output);
152
+ console.log(`Results written to ${argv.out}`);
153
+ }
154
+ else {
155
+ console.log(output);
156
+ }
157
+ process.exit(0);
158
+ }
159
+ main().catch((err) => {
160
+ console.error(err);
161
+ process.exit(1);
162
+ });
163
+ //# sourceMappingURL=fingerprint.js.map