@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.
- package/CapKitSSLPinning.podspec +17 -0
- package/LICENSE +21 -0
- package/Package.swift +25 -0
- package/README.md +750 -0
- package/android/build.gradle +103 -0
- package/android/src/main/AndroidManifest.xml +3 -0
- package/android/src/main/java/io/capkit/sslpinning/SSLPinningConfig.kt +22 -0
- package/android/src/main/java/io/capkit/sslpinning/SSLPinningImpl.kt +188 -0
- package/android/src/main/java/io/capkit/sslpinning/SSLPinningPlugin.kt +82 -0
- package/android/src/main/java/io/capkit/sslpinning/utils/SSLPinningLogger.kt +85 -0
- package/android/src/main/java/io/capkit/sslpinning/utils/SSLPinningUtils.kt +44 -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 +430 -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 +285 -0
- package/dist/esm/definitions.js +18 -0
- package/dist/esm/definitions.js.map +1 -0
- package/dist/esm/index.d.ts +15 -0
- package/dist/esm/index.js +16 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/web.d.ts +58 -0
- package/dist/esm/web.js +54 -0
- package/dist/esm/web.js.map +1 -0
- package/dist/plugin.cjs.js +95 -0
- package/dist/plugin.cjs.js.map +1 -0
- package/dist/plugin.js +98 -0
- package/dist/plugin.js.map +1 -0
- package/ios/Sources/SSLPinningPlugin/SSLPinningConfig.swift +79 -0
- package/ios/Sources/SSLPinningPlugin/SSLPinningDelegate.swift +81 -0
- package/ios/Sources/SSLPinningPlugin/SSLPinningImpl.swift +111 -0
- package/ios/Sources/SSLPinningPlugin/SSLPinningPlugin.swift +116 -0
- package/ios/Sources/SSLPinningPlugin/Utils/SSLPinningLogger.swift +57 -0
- package/ios/Sources/SSLPinningPlugin/Utils/SSLPinningUtils.swift +47 -0
- package/ios/Sources/SSLPinningPlugin/Version.swift +16 -0
- package/ios/Tests/SSLPinningPluginTests/SSLPinningPluginTests.swift +5 -0
- package/package.json +117 -0
- package/scripts/chmod.js +34 -0
- 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,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
|