@cap-kit/tls-fingerprint 8.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/CapKitTlsFingerprint.podspec +17 -0
  2. package/LICENSE +21 -0
  3. package/Package.swift +25 -0
  4. package/README.md +427 -0
  5. package/android/build.gradle +103 -0
  6. package/android/src/main/AndroidManifest.xml +3 -0
  7. package/android/src/main/java/io/capkit/settings/TLSFingerprintImpl.kt +333 -0
  8. package/android/src/main/java/io/capkit/settings/TLSFingerprintPlugin.kt +342 -0
  9. package/android/src/main/java/io/capkit/settings/config/TLSFingerprintConfig.kt +102 -0
  10. package/android/src/main/java/io/capkit/settings/error/TLSFingerprintError.kt +114 -0
  11. package/android/src/main/java/io/capkit/settings/error/TLSFingerprintErrorMessages.kt +27 -0
  12. package/android/src/main/java/io/capkit/settings/logger/TLSFingerprintLogger.kt +85 -0
  13. package/android/src/main/java/io/capkit/settings/model/TLSFingerprintResultModel.kt +32 -0
  14. package/android/src/main/java/io/capkit/settings/utils/TLSFingerprintUtils.kt +91 -0
  15. package/android/src/main/res/.gitkeep +0 -0
  16. package/dist/cli/fingerprint.js +163 -0
  17. package/dist/cli/fingerprint.js.map +1 -0
  18. package/dist/docs.json +386 -0
  19. package/dist/esm/cli/fingerprint.d.ts +1 -0
  20. package/dist/esm/cli/fingerprint.js +161 -0
  21. package/dist/esm/cli/fingerprint.js.map +1 -0
  22. package/dist/esm/definitions.d.ts +244 -0
  23. package/dist/esm/definitions.js +42 -0
  24. package/dist/esm/definitions.js.map +1 -0
  25. package/dist/esm/index.d.ts +13 -0
  26. package/dist/esm/index.js +11 -0
  27. package/dist/esm/index.js.map +1 -0
  28. package/dist/esm/version.d.ts +1 -0
  29. package/dist/esm/version.js +3 -0
  30. package/dist/esm/version.js.map +1 -0
  31. package/dist/esm/web.d.ts +33 -0
  32. package/dist/esm/web.js +47 -0
  33. package/dist/esm/web.js.map +1 -0
  34. package/dist/plugin.cjs +107 -0
  35. package/dist/plugin.cjs.map +1 -0
  36. package/dist/plugin.js +110 -0
  37. package/dist/plugin.js.map +1 -0
  38. package/ios/Sources/TLSFingerprintPlugin/TLSFingerprintDelegate.swift +365 -0
  39. package/ios/Sources/TLSFingerprintPlugin/TLSFingerprintImpl.swift +275 -0
  40. package/ios/Sources/TLSFingerprintPlugin/TLSFingerprintPlugin.swift +219 -0
  41. package/ios/Sources/TLSFingerprintPlugin/Version.swift +16 -0
  42. package/ios/Sources/TLSFingerprintPlugin/config/TLSFingerprintConfig.swift +114 -0
  43. package/ios/Sources/TLSFingerprintPlugin/error/TLSFingerprintError.swift +107 -0
  44. package/ios/Sources/TLSFingerprintPlugin/error/TLSFingerprintErrorMessages.swift +30 -0
  45. package/ios/Sources/TLSFingerprintPlugin/logger/TLSFingerprintLogger.swift +69 -0
  46. package/ios/Sources/TLSFingerprintPlugin/model/TLSFingerprintResult.swift +76 -0
  47. package/ios/Sources/TLSFingerprintPlugin/utils/TLSFingerprintUtils.swift +79 -0
  48. package/ios/Tests/TLSFingerprintPluginTests/TLSFingerprintPluginTests.swift +15 -0
  49. package/package.json +131 -0
  50. package/scripts/chmod.mjs +34 -0
  51. package/scripts/sync-version.mjs +68 -0
@@ -0,0 +1,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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"}