@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,102 @@
|
|
|
1
|
+
package io.capkit.tlsfingerprint.config
|
|
2
|
+
|
|
3
|
+
import com.getcapacitor.Plugin
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Plugin configuration container.
|
|
7
|
+
*
|
|
8
|
+
* This class is responsible for reading and exposing
|
|
9
|
+
* static configuration values defined under the
|
|
10
|
+
* `TLSFingerprint` key in capacitor.config.ts.
|
|
11
|
+
*
|
|
12
|
+
* Configuration rules:
|
|
13
|
+
* - Read once during plugin initialization
|
|
14
|
+
* - Treated as immutable runtime input
|
|
15
|
+
* - Accessible only from native code
|
|
16
|
+
*/
|
|
17
|
+
class TLSFingerprintConfig(
|
|
18
|
+
plugin: Plugin,
|
|
19
|
+
) {
|
|
20
|
+
// -----------------------------------------------------------------------------
|
|
21
|
+
// Configuration Keys
|
|
22
|
+
// -----------------------------------------------------------------------------
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Centralized definition of configuration keys.
|
|
26
|
+
* Avoids string duplication and typos.
|
|
27
|
+
*/
|
|
28
|
+
private object Keys {
|
|
29
|
+
const val VERBOSE_LOGGING = "verboseLogging"
|
|
30
|
+
const val FINGERPRINT = "fingerprint"
|
|
31
|
+
const val FINGERPRINTS = "fingerprints"
|
|
32
|
+
const val EXCLUDED_DOMAINS = "excludedDomains"
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// -----------------------------------------------------------------------------
|
|
36
|
+
// Properties
|
|
37
|
+
// -----------------------------------------------------------------------------
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Enables verbose native logging.
|
|
41
|
+
*
|
|
42
|
+
* When enabled, additional debug information
|
|
43
|
+
* is printed to Logcat.
|
|
44
|
+
*
|
|
45
|
+
* Default: false
|
|
46
|
+
*/
|
|
47
|
+
val verboseLogging: Boolean
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Default SHA-256 fingerprint used by checkCertificate()
|
|
51
|
+
* when no fingerprint is provided at runtime.
|
|
52
|
+
*/
|
|
53
|
+
val fingerprint: String?
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Default SHA-256 fingerprints used by checkCertificates()
|
|
57
|
+
* when no fingerprints are provided at runtime.
|
|
58
|
+
*/
|
|
59
|
+
val fingerprints: List<String>
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Domains or URL prefixes excluded from SSL pinning.
|
|
63
|
+
*
|
|
64
|
+
* Any request whose host matches one of these values
|
|
65
|
+
* MUST bypass SSL pinning checks.
|
|
66
|
+
*/
|
|
67
|
+
val excludedDomains: List<String>
|
|
68
|
+
|
|
69
|
+
// -----------------------------------------------------------------------------
|
|
70
|
+
// Initialization
|
|
71
|
+
// -----------------------------------------------------------------------------
|
|
72
|
+
|
|
73
|
+
init {
|
|
74
|
+
val config = plugin.getConfig()
|
|
75
|
+
|
|
76
|
+
// Verbose logging flag
|
|
77
|
+
verboseLogging =
|
|
78
|
+
config.getBoolean(Keys.VERBOSE_LOGGING, false)
|
|
79
|
+
|
|
80
|
+
// Single fingerprint (optional)
|
|
81
|
+
val fp = config.getString(Keys.FINGERPRINT)
|
|
82
|
+
fingerprint =
|
|
83
|
+
if (!fp.isNullOrBlank()) fp else null
|
|
84
|
+
|
|
85
|
+
// Multiple fingerprints (optional)
|
|
86
|
+
fingerprints =
|
|
87
|
+
config
|
|
88
|
+
.getArray(Keys.FINGERPRINTS)
|
|
89
|
+
?.toList()
|
|
90
|
+
?.mapNotNull { it as? String }
|
|
91
|
+
?.filter { it.isNotBlank() }
|
|
92
|
+
?: emptyList()
|
|
93
|
+
|
|
94
|
+
excludedDomains =
|
|
95
|
+
config
|
|
96
|
+
.getArray(Keys.EXCLUDED_DOMAINS)
|
|
97
|
+
?.toList()
|
|
98
|
+
?.mapNotNull { it as? String }
|
|
99
|
+
?.filter { it.isNotBlank() }
|
|
100
|
+
?: emptyList()
|
|
101
|
+
}
|
|
102
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
package io.capkit.tlsfingerprint.error
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Native error model for the TLSFingerprint plugin (Android).
|
|
5
|
+
*
|
|
6
|
+
* Architectural rules:
|
|
7
|
+
* - Must NOT reference Capacitor APIs.
|
|
8
|
+
* - Must NOT reference JavaScript directly.
|
|
9
|
+
* - Must be throwable from the Implementation (Impl) layer.
|
|
10
|
+
* - Mapping to JS-facing error codes happens ONLY in the Plugin layer.
|
|
11
|
+
*/
|
|
12
|
+
sealed class TLSFingerprintError(
|
|
13
|
+
message: String,
|
|
14
|
+
) : Throwable(message) {
|
|
15
|
+
// -----------------------------------------------------------------------------
|
|
16
|
+
// Specific Error Types
|
|
17
|
+
// -----------------------------------------------------------------------------
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Feature or capability is not available due to device or configuration limitations.
|
|
21
|
+
* Maps to the 'UNAVAILABLE' error code in JavaScript.
|
|
22
|
+
*/
|
|
23
|
+
class Unavailable(
|
|
24
|
+
message: String,
|
|
25
|
+
) : TLSFingerprintError(message)
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The user cancelled an interactive flow.
|
|
29
|
+
* Maps to the 'CANCELLED' error code in JavaScript.
|
|
30
|
+
*/
|
|
31
|
+
class Cancelled(
|
|
32
|
+
message: String,
|
|
33
|
+
) : TLSFingerprintError(message)
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Required permission was denied or not granted by the user.
|
|
37
|
+
* Maps to the 'PERMISSION_DENIED' error code in JavaScript.
|
|
38
|
+
*/
|
|
39
|
+
class PermissionDenied(
|
|
40
|
+
message: String,
|
|
41
|
+
) : TLSFingerprintError(message)
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Plugin failed to initialize or perform a required native operation.
|
|
45
|
+
* Maps to the 'INIT_FAILED' error code in JavaScript.
|
|
46
|
+
*/
|
|
47
|
+
class InitFailed(
|
|
48
|
+
message: String,
|
|
49
|
+
) : TLSFingerprintError(message)
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Invalid or malformed input was provided by the caller.
|
|
53
|
+
* Maps to the 'INVALID_INPUT' error code in JavaScript.
|
|
54
|
+
*/
|
|
55
|
+
class InvalidInput(
|
|
56
|
+
message: String,
|
|
57
|
+
) : TLSFingerprintError(message)
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Invalid or unsupported input type was provided to the native implementation.
|
|
61
|
+
* Maps to the 'UNKNOWN_TYPE' error code in JavaScript.
|
|
62
|
+
*/
|
|
63
|
+
class UnknownType(
|
|
64
|
+
message: String,
|
|
65
|
+
) : TLSFingerprintError(message)
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* The requested resource does not exist.
|
|
69
|
+
* Maps to the 'NOT_FOUND' error code in JavaScript.
|
|
70
|
+
*/
|
|
71
|
+
class NotFound(
|
|
72
|
+
message: String,
|
|
73
|
+
) : TLSFingerprintError(message)
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* The operation conflicts with the current state.
|
|
77
|
+
* Maps to the 'CONFLICT' error code in JavaScript.
|
|
78
|
+
*/
|
|
79
|
+
class Conflict(
|
|
80
|
+
message: String,
|
|
81
|
+
) : TLSFingerprintError(message)
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* The operation did not complete within the expected time.
|
|
85
|
+
* Maps to the 'TIMEOUT' error code in JavaScript.
|
|
86
|
+
*/
|
|
87
|
+
class Timeout(
|
|
88
|
+
message: String,
|
|
89
|
+
) : TLSFingerprintError(message)
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Network connectivity or TLS handshake error.
|
|
93
|
+
* Maps to the 'NETWORK_ERROR' error code in JavaScript.
|
|
94
|
+
*/
|
|
95
|
+
class NetworkError(
|
|
96
|
+
message: String,
|
|
97
|
+
) : TLSFingerprintError(message)
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Invalid or malformed configuration.
|
|
101
|
+
* Maps to the 'INVALID_INPUT' error code in JavaScript.
|
|
102
|
+
*/
|
|
103
|
+
class InvalidConfig(
|
|
104
|
+
message: String,
|
|
105
|
+
) : TLSFingerprintError(message)
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* SSL/TLS specific error (certificate expired, handshake failure, etc.).
|
|
109
|
+
* Maps to the 'SSL_ERROR' error code in JavaScript.
|
|
110
|
+
*/
|
|
111
|
+
class SslError(
|
|
112
|
+
message: String,
|
|
113
|
+
) : TLSFingerprintError(message)
|
|
114
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
package io.capkit.tlsfingerprint.error
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Canonical error messages shared across platforms.
|
|
5
|
+
* These strings must remain byte-identical on iOS and Android.
|
|
6
|
+
*/
|
|
7
|
+
object TLSFingerprintErrorMessages {
|
|
8
|
+
const val URL_REQUIRED = "url is required"
|
|
9
|
+
const val INVALID_URL_MUST_BE_HTTPS = "Invalid URL. Must be https."
|
|
10
|
+
const val INVALID_URL = "Invalid URL."
|
|
11
|
+
const val NO_FINGERPRINTS_PROVIDED = "No fingerprints provided"
|
|
12
|
+
const val NO_HOST_FOUND_IN_URL = "No host found in URL"
|
|
13
|
+
const val INVALID_FINGERPRINT_FORMAT = "Invalid fingerprint format"
|
|
14
|
+
const val UNSUPPORTED_HOST = "Unsupported host: %s"
|
|
15
|
+
const val PINNING_FAILED = "Pinning failed"
|
|
16
|
+
const val EXCLUDED_DOMAIN = "Excluded domain"
|
|
17
|
+
const val TIMEOUT = "Timeout"
|
|
18
|
+
const val NETWORK_ERROR = "Network error"
|
|
19
|
+
const val INTERNAL_ERROR = "Internal error"
|
|
20
|
+
const val INVALID_CONFIG = "Invalid configuration: %s"
|
|
21
|
+
|
|
22
|
+
@JvmStatic
|
|
23
|
+
fun unsupportedHost(value: String): String = String.format(UNSUPPORTED_HOST, value)
|
|
24
|
+
|
|
25
|
+
@JvmStatic
|
|
26
|
+
fun invalidConfig(value: String): String = String.format(INVALID_CONFIG, value)
|
|
27
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
package io.capkit.tlsfingerprint.logger
|
|
2
|
+
|
|
3
|
+
import android.util.Log
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Centralized logging utility for the TLSFingerprint 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 TLSFingerprintLogger {
|
|
15
|
+
/**
|
|
16
|
+
* Logcat tag used for all plugin logs.
|
|
17
|
+
* Helps filtering logs during debugging.
|
|
18
|
+
*/
|
|
19
|
+
private const val TAG = "⚡️ TLSFingerprint"
|
|
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,32 @@
|
|
|
1
|
+
package io.capkit.tlsfingerprint.model
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Canonical, nominal result model for TLS fingerprint validation operations on Android.
|
|
5
|
+
* This data class is exchanged between the native implementation (TLSFingerprintImpl)
|
|
6
|
+
* and the bridge (TLSFingerprintPlugin) and serialized to JavaScript via a JSObject.
|
|
7
|
+
*
|
|
8
|
+
* Fields mirror the public JS payload:
|
|
9
|
+
* - actualFingerprint: server fingerprint used for matching
|
|
10
|
+
* - fingerprintMatched: whether the fingerprint check succeeded (true) or not (false)
|
|
11
|
+
* - matchedFingerprint: the fingerprint that matched (only present for fingerprint mode)
|
|
12
|
+
* - excludedDomain: indicates an excluded-domain bypass (true when applicable)
|
|
13
|
+
* - mode: active mode: "fingerprint" | "excluded"
|
|
14
|
+
* - error: human-readable error (empty on success/match)
|
|
15
|
+
* - errorCode: canonical error code string (empty on success)
|
|
16
|
+
*/
|
|
17
|
+
data class TLSFingerprintResultModel(
|
|
18
|
+
/** Actual server fingerprint used for matching (if available). */
|
|
19
|
+
val actualFingerprint: String?,
|
|
20
|
+
/** Indicates whether the fingerprint check succeeded or not. */
|
|
21
|
+
val fingerprintMatched: Boolean,
|
|
22
|
+
/** The fingerprint that matched (if fingerprint mode). */
|
|
23
|
+
val matchedFingerprint: String? = null,
|
|
24
|
+
/** Whether the host is excluded and pinning bypass uses system trust. */
|
|
25
|
+
val excludedDomain: Boolean? = null,
|
|
26
|
+
/** Active mode: "fingerprint" | "excluded". */
|
|
27
|
+
val mode: String? = null,
|
|
28
|
+
/** Human-readable error description, if any. */
|
|
29
|
+
val error: String? = null,
|
|
30
|
+
/** Canonical error code for JS consumption. */
|
|
31
|
+
val errorCode: String? = null,
|
|
32
|
+
)
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
package io.capkit.tlsfingerprint.utils
|
|
2
|
+
|
|
3
|
+
import java.net.URL
|
|
4
|
+
import java.security.MessageDigest
|
|
5
|
+
import java.security.cert.Certificate
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Utility helpers for SSL pinning logic (Android).
|
|
9
|
+
*
|
|
10
|
+
* Pure utilities:
|
|
11
|
+
* - No Capacitor dependency
|
|
12
|
+
* - No side effects
|
|
13
|
+
* - Fully testable
|
|
14
|
+
*/
|
|
15
|
+
object TLSFingerprintUtils {
|
|
16
|
+
/**
|
|
17
|
+
* Validates and returns a HTTPS URL.
|
|
18
|
+
*
|
|
19
|
+
* Non-HTTPS URLs are explicitly rejected
|
|
20
|
+
* to prevent insecure usage.
|
|
21
|
+
*/
|
|
22
|
+
fun httpsUrl(value: String): URL? =
|
|
23
|
+
try {
|
|
24
|
+
val url = URL(value)
|
|
25
|
+
if (url.protocol == "https") url else null
|
|
26
|
+
} catch (_: Exception) {
|
|
27
|
+
null
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Normalizes a fingerprint string by:
|
|
32
|
+
* - Removing colon separators
|
|
33
|
+
* - Removing all whitespace
|
|
34
|
+
* - Converting to lowercase
|
|
35
|
+
*
|
|
36
|
+
* Example:
|
|
37
|
+
* "AA:BB:CC" → "aabbcc"
|
|
38
|
+
* "AA BB CC" → "aabbcc"
|
|
39
|
+
*/
|
|
40
|
+
fun normalizeFingerprint(value: String): String =
|
|
41
|
+
value
|
|
42
|
+
.replace(":", "")
|
|
43
|
+
.replace(" ", "")
|
|
44
|
+
.lowercase()
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Validates that a fingerprint string is a valid SHA-256 hex format.
|
|
48
|
+
*
|
|
49
|
+
* Valid fingerprint:
|
|
50
|
+
* - Exactly 64 hexadecimal characters (after normalization)
|
|
51
|
+
* - Contains only [a-f0-9]
|
|
52
|
+
*/
|
|
53
|
+
fun isValidFingerprintFormat(value: String): Boolean {
|
|
54
|
+
val normalized = normalizeFingerprint(value)
|
|
55
|
+
return normalized.length == 64 && normalized.matches(Regex("^[a-f0-9]+$"))
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Validates a fingerprint and returns an error message if invalid, or null if valid.
|
|
60
|
+
*/
|
|
61
|
+
fun validateFingerprint(value: String): String? {
|
|
62
|
+
if (value.isBlank()) {
|
|
63
|
+
return "Fingerprint cannot be blank"
|
|
64
|
+
}
|
|
65
|
+
val normalized = normalizeFingerprint(value)
|
|
66
|
+
if (normalized.length != 64) {
|
|
67
|
+
return "Invalid fingerprint: must be 64 hex characters"
|
|
68
|
+
}
|
|
69
|
+
if (!normalized.matches(Regex("^[a-f0-9]+$"))) {
|
|
70
|
+
return "Invalid fingerprint: must contain only hex characters [a-f0-9]"
|
|
71
|
+
}
|
|
72
|
+
return null
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Computes the SHA-256 fingerprint of an X.509 certificate.
|
|
77
|
+
*
|
|
78
|
+
* Output format:
|
|
79
|
+
* "aa:bb:cc:dd:..."
|
|
80
|
+
*/
|
|
81
|
+
fun sha256Fingerprint(cert: Certificate): String {
|
|
82
|
+
val digest =
|
|
83
|
+
MessageDigest
|
|
84
|
+
.getInstance("SHA-256")
|
|
85
|
+
.digest(cert.encoded)
|
|
86
|
+
|
|
87
|
+
return digest.joinToString(separator = ":") {
|
|
88
|
+
"%02x".format(it)
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
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
|
+
TLSFingerprint: {
|
|
56
|
+
fingerprint: "${fingerprints[0]}"
|
|
57
|
+
}
|
|
58
|
+
}`;
|
|
59
|
+
}
|
|
60
|
+
return `plugins: {
|
|
61
|
+
TLSFingerprint: {
|
|
62
|
+
fingerprints: ${JSON.stringify(fingerprints, null, 4)}
|
|
63
|
+
}
|
|
64
|
+
}`;
|
|
65
|
+
}
|
|
66
|
+
case 'capacitor-plugin': {
|
|
67
|
+
if (mode === 'single') {
|
|
68
|
+
return `TLSFingerprint: {
|
|
69
|
+
fingerprint: "${fingerprints[0]}"
|
|
70
|
+
}`;
|
|
71
|
+
}
|
|
72
|
+
return `TLSFingerprint: {
|
|
73
|
+
fingerprints: ${JSON.stringify(fingerprints, null, 4)}
|
|
74
|
+
}`;
|
|
75
|
+
}
|
|
76
|
+
case 'capacitor-json': {
|
|
77
|
+
if (mode === 'single') {
|
|
78
|
+
return JSON.stringify({
|
|
79
|
+
plugins: {
|
|
80
|
+
TLSFingerprint: {
|
|
81
|
+
fingerprint: fingerprints[0],
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
}, null, 2);
|
|
85
|
+
}
|
|
86
|
+
return JSON.stringify({
|
|
87
|
+
plugins: {
|
|
88
|
+
TLSFingerprint: {
|
|
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
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fingerprint.js","sources":["../esm/cli/fingerprint.js"],"sourcesContent":["import crypto from 'crypto';\nimport fs from 'fs/promises';\nimport https from 'https';\nimport { createRequire } from 'module';\nimport yargs from 'yargs';\nimport { hideBin } from 'yargs/helpers';\nconst require = createRequire(import.meta.url);\nconst pkg = require('../../package.json');\nasync function getCertificate(domain, insecure) {\n return new Promise((resolve, reject) => {\n const options = {\n host: domain,\n port: 443,\n method: 'GET',\n rejectUnauthorized: !insecure,\n };\n const req = https.request(options, (res) => {\n var _a, _b, _c;\n const socket = res.socket;\n const cert = (_a = socket === null || socket === void 0 ? void 0 : socket.getPeerCertificate) === null || _a === void 0 ? void 0 : _a.call(socket, true);\n if (!(cert === null || cert === void 0 ? void 0 : cert.raw)) {\n reject(new Error('Unable to retrieve peer certificate'));\n return;\n }\n const fingerprint = crypto\n .createHash('sha256')\n .update(cert.raw)\n .digest('hex')\n .match(/.{2}/g)\n .join(':')\n .toUpperCase();\n resolve({\n domain,\n subject: (_b = cert.subject) !== null && _b !== void 0 ? _b : {},\n issuer: (_c = cert.issuer) !== null && _c !== void 0 ? _c : {},\n validFrom: cert.valid_from,\n validTo: cert.valid_to,\n fingerprint,\n });\n });\n req.on('error', reject);\n req.end();\n });\n}\nfunction formatOutput(results, mode, format) {\n const fingerprints = results.map((r) => r.fingerprint);\n switch (format) {\n case 'fingerprints':\n return `export const fingerprints = ${JSON.stringify(fingerprints, null, 2)};`;\n case 'capacitor': {\n if (mode === 'single') {\n return `plugins: {\n TLSFingerprint: {\n fingerprint: \"${fingerprints[0]}\"\n }\n}`;\n }\n return `plugins: {\n TLSFingerprint: {\n fingerprints: ${JSON.stringify(fingerprints, null, 4)}\n }\n}`;\n }\n case 'capacitor-plugin': {\n if (mode === 'single') {\n return `TLSFingerprint: {\n fingerprint: \"${fingerprints[0]}\"\n}`;\n }\n return `TLSFingerprint: {\n fingerprints: ${JSON.stringify(fingerprints, null, 4)}\n}`;\n }\n case 'capacitor-json': {\n if (mode === 'single') {\n return JSON.stringify({\n plugins: {\n TLSFingerprint: {\n fingerprint: fingerprints[0],\n },\n },\n }, null, 2);\n }\n return JSON.stringify({\n plugins: {\n TLSFingerprint: {\n fingerprints,\n },\n },\n }, null, 2);\n }\n case 'json':\n default:\n return JSON.stringify(results, null, 2);\n }\n}\nasync function main() {\n var _a, _b, _c;\n const argv = await yargs(hideBin(process.argv))\n .usage('Usage: $0 <domains...> [options]')\n .version(pkg.version)\n .option('out', {\n alias: 'o',\n type: 'string',\n description: 'Output file path',\n })\n .option('format', {\n alias: 'f',\n type: 'string',\n choices: ['json', 'fingerprints', 'capacitor', 'capacitor-plugin', 'capacitor-json'],\n default: 'json',\n description: 'Output format',\n })\n .option('mode', {\n type: 'string',\n choices: ['single', 'multi'],\n default: 'single',\n description: 'Fingerprint mode (single or multi)',\n })\n .option('insecure', {\n type: 'boolean',\n default: true,\n description: 'Allow insecure TLS connections (disables certificate validation)',\n })\n .demandCommand(1, 'At least one domain is required')\n .help().argv;\n const domains = argv._;\n const results = [];\n console.log('Fetching certificates...\\n');\n for (const domain of domains) {\n try {\n const certInfo = await getCertificate(domain, argv.insecure);\n results.push(certInfo);\n console.log(`Domain: ${certInfo.domain}`);\n console.log(`Subject: ${(_a = certInfo.subject.CN) !== null && _a !== void 0 ? _a : '-'}`);\n console.log(`Issuer: ${(_b = certInfo.issuer.CN) !== null && _b !== void 0 ? _b : '-'}`);\n console.log(`Valid From: ${certInfo.validFrom}`);\n console.log(`Valid To: ${certInfo.validTo}`);\n console.log(`SHA256 Fingerprint: ${certInfo.fingerprint}`);\n console.log('-------------------\\n');\n }\n catch (err) {\n console.error(`Error fetching cert for ${domain}: ${(_c = err === null || err === void 0 ? void 0 : err.message) !== null && _c !== void 0 ? _c : err}`);\n console.log('-------------------\\n');\n }\n }\n const output = formatOutput(results, argv.mode, argv.format);\n if (argv.out) {\n await fs.writeFile(argv.out, output);\n console.log(`Results written to ${argv.out}`);\n }\n else {\n console.log(output);\n }\n process.exit(0);\n}\nmain().catch((err) => {\n console.error(err);\n process.exit(1);\n});\n//# sourceMappingURL=fingerprint.js.map"],"names":["require"],"mappings":"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"}
|