@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 @@
1
+ {"version":3,"file":"plugin.js","sources":["esm/definitions.js","esm/index.js","esm/web.js"],"sourcesContent":["/// <reference types=\"@capacitor/cli\" />\n// -- Enums --\n/**\n * Standardized error codes for programmatic handling of ssl pinning failures.\n * @since 0.0.15\n */\nexport var SSLPinningErrorCode;\n(function (SSLPinningErrorCode) {\n /** The device does not have the requested hardware. */\n SSLPinningErrorCode[\"UNAVAILABLE\"] = \"UNAVAILABLE\";\n /** The user denied the permission or the feature is disabled in settings. */\n SSLPinningErrorCode[\"PERMISSION_DENIED\"] = \"PERMISSION_DENIED\";\n /** The ssl pinning failed to initialize (e.g., runtime error or Looper failure). */\n SSLPinningErrorCode[\"INIT_FAILED\"] = \"INIT_FAILED\";\n /** The requested ssl pinning type is not valid or not supported by the plugin. */\n SSLPinningErrorCode[\"UNKNOWN_TYPE\"] = \"UNKNOWN_TYPE\";\n})(SSLPinningErrorCode || (SSLPinningErrorCode = {}));\n//# sourceMappingURL=definitions.js.map","/**\n * Import the `registerPlugin` method from the Capacitor core library.\n * This method is used to register a custom plugin.\n */\nimport { registerPlugin } from '@capacitor/core';\n/**\n * The SSLPinning plugin instance.\n * It automatically lazy-loads the web implementation if running in a browser environment.\n * Use this instance to access all ssl pinning functionality.\n */\nconst SSLPinning = registerPlugin('SSLPinning', {\n web: () => import('./web').then((m) => new m.SSLPinningWeb()),\n});\nexport * from './definitions';\nexport { SSLPinning };\n//# sourceMappingURL=index.js.map","/**\n * This module provides a web implementation of the SSLPinningPlugin.\n * The functionality is limited in a web context due to the lack of SSL certificate inspection capabilities in browsers.\n *\n * The implementation adheres to the SSLPinningPlugin interface but provides fallback behavior\n * because browsers do not allow direct inspection of SSL certificate details.\n */\nimport { CapacitorException, ExceptionCode, WebPlugin } from '@capacitor/core';\n/**\n * Web implementation of the SSLPinningPlugin interface.\n *\n * This class is intended to be used in a browser environment and handles scenarios where SSL certificate\n * checking is unsupported. It implements the methods defined by the SSLPinningPlugin\n * interface but returns standardized error responses to indicate the lack of functionality in web contexts.\n */\nexport class SSLPinningWeb extends WebPlugin {\n /**\n * Checks a single SSL certificate against the expected fingerprint.\n * @return A promise that resolves to the result of the certificate check.\n * @throws CapacitorException indicating unimplemented functionality.\n */\n async checkCertificate() {\n throw this.createUnimplementedError();\n }\n /**\n * Checks multiple SSL certificates against their expected fingerprints.\n * @return A promise that resolves to an array of results for each certificate check.\n * @throws CapacitorException indicating unimplemented functionality.\n */\n async checkCertificates() {\n throw this.createUnimplementedError();\n }\n // --- Plugin Info ---\n /**\n * Returns the plugin version.\n *\n * @returns The current plugin version.\n */\n async getPluginVersion() {\n return { version: 'web' };\n }\n /**\n * Creates a standardized exception for unimplemented methods.\n *\n * This utility method centralizes the creation of exceptions for functionality that is not supported\n * on the current platform, ensuring consistency in error reporting.\n *\n * @returns {CapacitorException} An exception with the code `Unimplemented` and a descriptive message.\n */\n createUnimplementedError() {\n return new CapacitorException('This plugin method is not implemented on this platform.', ExceptionCode.Unimplemented);\n }\n}\n//# sourceMappingURL=web.js.map"],"names":["SSLPinningErrorCode","registerPlugin","WebPlugin","CapacitorException","ExceptionCode"],"mappings":";;;IAAA;IACA;IACA;IACA;IACA;IACA;AACWA;IACX,CAAC,UAAU,mBAAmB,EAAE;IAChC;IACA,IAAI,mBAAmB,CAAC,aAAa,CAAC,GAAG,aAAa;IACtD;IACA,IAAI,mBAAmB,CAAC,mBAAmB,CAAC,GAAG,mBAAmB;IAClE;IACA,IAAI,mBAAmB,CAAC,aAAa,CAAC,GAAG,aAAa;IACtD;IACA,IAAI,mBAAmB,CAAC,cAAc,CAAC,GAAG,cAAc;IACxD,CAAC,EAAEA,2BAAmB,KAAKA,2BAAmB,GAAG,EAAE,CAAC,CAAC;;IChBrD;IACA;IACA;IACA;IAEA;IACA;IACA;IACA;IACA;AACK,UAAC,UAAU,GAAGC,mBAAc,CAAC,YAAY,EAAE;IAChD,IAAI,GAAG,EAAE,MAAM,mDAAe,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,aAAa,EAAE,CAAC;IACjE,CAAC;;ICZD;IACA;IACA;IACA;IACA;IACA;IACA;IAEA;IACA;IACA;IACA;IACA;IACA;IACA;IACO,MAAM,aAAa,SAASC,cAAS,CAAC;IAC7C;IACA;IACA;IACA;IACA;IACA,IAAI,MAAM,gBAAgB,GAAG;IAC7B,QAAQ,MAAM,IAAI,CAAC,wBAAwB,EAAE;IAC7C,IAAI;IACJ;IACA;IACA;IACA;IACA;IACA,IAAI,MAAM,iBAAiB,GAAG;IAC9B,QAAQ,MAAM,IAAI,CAAC,wBAAwB,EAAE;IAC7C,IAAI;IACJ;IACA;IACA;IACA;IACA;IACA;IACA,IAAI,MAAM,gBAAgB,GAAG;IAC7B,QAAQ,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE;IACjC,IAAI;IACJ;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA,IAAI,wBAAwB,GAAG;IAC/B,QAAQ,OAAO,IAAIC,uBAAkB,CAAC,yDAAyD,EAAEC,kBAAa,CAAC,aAAa,CAAC;IAC7H,IAAI;IACJ;;;;;;;;;;;;;;;"}
@@ -0,0 +1,79 @@
1
+ import Foundation
2
+ import Capacitor
3
+
4
+ /**
5
+ Helper struct to manage the SSLPinning plugin configuration.
6
+
7
+ This struct reads static configuration values from `capacitor.config.ts`
8
+ using the Capacitor plugin instance's built-in config access.
9
+
10
+ IMPORTANT:
11
+ - These values are READ-ONLY at runtime.
12
+ - JavaScript MUST NOT access them directly.
13
+ - Actual behavior is implemented in native code only.
14
+ */
15
+ public struct SSLPinningConfig {
16
+
17
+ // MARK: - Configuration Keys
18
+
19
+ private struct Keys {
20
+ static let verboseLogging = "verboseLogging"
21
+ static let fingerprint = "fingerprint"
22
+ static let fingerprints = "fingerprints"
23
+ }
24
+
25
+ // MARK: - Public Config Values
26
+
27
+ /**
28
+ Enables verbose native logging.
29
+
30
+ When enabled, additional debug information is printed
31
+ to the Xcode console via the plugin logger.
32
+
33
+ Default: false
34
+ */
35
+ public let verboseLogging: Bool
36
+
37
+ /**
38
+ Default SHA-256 fingerprint used by `checkCertificate()`
39
+ when no fingerprint is provided at runtime.
40
+ */
41
+ public let fingerprint: String?
42
+
43
+ /**
44
+ Default SHA-256 fingerprints used by `checkCertificates()`
45
+ when no fingerprints are provided at runtime.
46
+ */
47
+ public let fingerprints: [String]
48
+
49
+ // MARK: - Private Defaults
50
+
51
+ private let defaultVerboseLogging = false
52
+ private let defaultFingerprint: String? = nil
53
+ private let defaultFingerprints: [String] = []
54
+
55
+ // MARK: - Init
56
+
57
+ /**
58
+ Initializes the configuration by reading values from the Capacitor bridge.
59
+
60
+ - Parameter plugin: The CAPPlugin instance used to access typed configuration.
61
+ */
62
+ init(plugin: CAPPlugin) {
63
+ // Use getConfigValue(key) to bypass SPM visibility issues and ensure stability.
64
+
65
+ // Bool - Verbose Logging
66
+ verboseLogging = plugin.getConfigValue(Keys.verboseLogging) as? Bool ?? defaultVerboseLogging
67
+
68
+ // Optional String - Single Fingerprint
69
+ // We validate that it is not empty after casting to avoid using empty strings as valid fingerprints.
70
+ if let fprt = plugin.getConfigValue(Keys.fingerprint) as? String, !fprt.isEmpty {
71
+ fingerprint = fprt
72
+ } else {
73
+ fingerprint = defaultFingerprint
74
+ }
75
+
76
+ // Array of Strings - Multiple Fingerprints
77
+ fingerprints = plugin.getConfigValue(Keys.fingerprints) as? [String] ?? defaultFingerprints
78
+ }
79
+ }
@@ -0,0 +1,81 @@
1
+ import Foundation
2
+ import Security
3
+
4
+ final class SSLPinningDelegate: NSObject, URLSessionDelegate {
5
+
6
+ /// Initializes the delegate with optional pinned certificates.
7
+ private let expectedFingerprints: [String]
8
+
9
+ /// Completion handler to return the result.
10
+ private let completion: ([String: Any]) -> Void
11
+
12
+ /// Verbose logging flag.
13
+ private let verboseLogging: Bool
14
+
15
+ /// Initializes the SSLPinningDelegate.
16
+ init(
17
+ expectedFingerprints: [String],
18
+ completion: @escaping ([String: Any]) -> Void,
19
+ verboseLogging: Bool
20
+ ) {
21
+ self.expectedFingerprints =
22
+ expectedFingerprints.map {
23
+ SSLPinningUtils.normalizeFingerprint($0)
24
+ }
25
+ self.completion = completion
26
+ self.verboseLogging = verboseLogging
27
+ }
28
+
29
+ // MARK: - URLSessionDelegate
30
+
31
+ /// Intercepts the TLS authentication challenge to perform
32
+ /// manual SSL pinning based on certificate fingerprint.
33
+ ///
34
+ /// The connection is accepted or rejected solely based on
35
+ /// fingerprint comparison, not on system trust evaluation.
36
+ func urlSession(
37
+ _ session: URLSession,
38
+ didReceive challenge: URLAuthenticationChallenge,
39
+ completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void
40
+ ) {
41
+ // =========================================================
42
+ // FINGERPRINT MODE (unchanged behavior)
43
+ // =========================================================
44
+ guard let trust = challenge.protectionSpace.serverTrust,
45
+ let cert = SSLPinningUtils.leafCertificate(from: trust) else {
46
+
47
+ completionHandler(.cancelAuthenticationChallenge, nil)
48
+ completion([
49
+ "fingerprintMatched": false,
50
+ "error": "Unable to extract certificate",
51
+ "errorCode": "INIT_FAILED"
52
+ ])
53
+ return
54
+ }
55
+
56
+ let actualFingerprint =
57
+ SSLPinningUtils.normalizeFingerprint(
58
+ SSLPinningUtils.sha256Fingerprint(from: cert)
59
+ )
60
+
61
+ let matchedFingerprint =
62
+ expectedFingerprints.first {
63
+ $0 == actualFingerprint
64
+ }
65
+
66
+ let matched = matchedFingerprint != nil
67
+
68
+ SSLPinningLogger.debug("SSLPinning matched:", "\(matched)")
69
+
70
+ completion([
71
+ "actualFingerprint": actualFingerprint,
72
+ "fingerprintMatched": matched,
73
+ "matchedFingerprint": matchedFingerprint as Any
74
+ ])
75
+
76
+ completionHandler(
77
+ matched ? .useCredential : .cancelAuthenticationChallenge,
78
+ matched ? URLCredential(trust: trust) : nil
79
+ )
80
+ }
81
+ }
@@ -0,0 +1,111 @@
1
+ import Foundation
2
+ import Security
3
+
4
+ final class SSLPinningImpl {
5
+
6
+ /// Cached configuration.
7
+ private var config: SSLPinningConfig?
8
+
9
+ /// Applies the given configuration.
10
+ func applyConfig(_ config: SSLPinningConfig) {
11
+ self.config = config
12
+ }
13
+
14
+ // MARK: - Single fingerprint
15
+
16
+ /// Validates multiple SSL certificates for a given URL.
17
+ func checkCertificate(
18
+ urlString: String,
19
+ fingerprintFromArgs: String?,
20
+ completion: @escaping ([String: Any]) -> Void
21
+ ) {
22
+ let fingerprint =
23
+ fingerprintFromArgs ??
24
+ config?.fingerprint
25
+
26
+ guard let expectedFingerprint = fingerprint else {
27
+ completion([
28
+ "fingerprintMatched": false,
29
+ "error": "No fingerprint provided (args or config)",
30
+ "errorCode": "UNAVAILABLE"
31
+ ])
32
+ return
33
+ }
34
+
35
+ performCheck(
36
+ urlString: urlString,
37
+ fingerprints: [expectedFingerprint],
38
+ completion: completion
39
+ )
40
+ }
41
+
42
+ // MARK: - Multiple fingerprints
43
+
44
+ /// Validates multiple SSL certificates for a given URL.
45
+ func checkCertificates(
46
+ urlString: String,
47
+ fingerprintsFromArgs: [String]?,
48
+ completion: @escaping ([String: Any]) -> Void
49
+ ) {
50
+ let fingerprints =
51
+ fingerprintsFromArgs ??
52
+ config?.fingerprints
53
+
54
+ guard let fingerprints, !fingerprints.isEmpty else {
55
+ completion([
56
+ "fingerprintMatched": false,
57
+ "error": "No fingerprints provided (args or config)",
58
+ "errorCode": "UNAVAILABLE"
59
+ ])
60
+ return
61
+ }
62
+
63
+ performCheck(
64
+ urlString: urlString,
65
+ fingerprints: fingerprints,
66
+ completion: completion
67
+ )
68
+ }
69
+
70
+ // MARK: - Shared implementation
71
+
72
+ /// Performs the actual SSL pinning validation.
73
+ ///
74
+ /// This method:
75
+ /// - Creates an ephemeral URLSession
76
+ /// - Intercepts the TLS handshake via URLSessionDelegate
77
+ /// - Extracts the server leaf certificate
78
+ /// - Compares its SHA-256 fingerprint against the expected ones
79
+ ///
80
+ /// IMPORTANT:
81
+ /// - The system trust chain is NOT evaluated
82
+ /// - Only fingerprint matching determines acceptance
83
+ private func performCheck(
84
+ urlString: String,
85
+ fingerprints: [String],
86
+ completion: @escaping ([String: Any]) -> Void
87
+ ) {
88
+ guard let url = SSLPinningUtils.httpsURL(from: urlString) else {
89
+ completion([
90
+ "fingerprintMatched": false,
91
+ "error": "Invalid HTTPS URL",
92
+ "errorCode": "UNKNOWN_TYPE"
93
+ ])
94
+ return
95
+ }
96
+
97
+ let delegate = SSLPinningDelegate(
98
+ expectedFingerprints: fingerprints,
99
+ completion: completion,
100
+ verboseLogging: config?.verboseLogging ?? false
101
+ )
102
+
103
+ let session = URLSession(
104
+ configuration: .ephemeral,
105
+ delegate: delegate,
106
+ delegateQueue: nil
107
+ )
108
+
109
+ session.dataTask(with: url).resume()
110
+ }
111
+ }
@@ -0,0 +1,116 @@
1
+ import Foundation
2
+ import Capacitor
3
+
4
+ /**
5
+ * @file SSLPinningPlugin.swift
6
+ * This file defines the implementation of the Capacitor plugin `SSLPinningPlugin` for iOS.
7
+ * The plugin provides an interface between JavaScript and native iOS code, allowing Capacitor applications
8
+ * to interact with SSL certificate verification functionality.
9
+ *
10
+ * Documentation Reference: https://capacitorjs.com/docs/plugins/ios
11
+ */
12
+
13
+ @objc(SSLPinningPlugin)
14
+ public class SSLPinningPlugin: CAPPlugin, CAPBridgedPlugin {
15
+
16
+ // Configuration instance
17
+ private var config: SSLPinningConfig?
18
+
19
+ /// An instance of the implementation class that contains the plugin's core functionality.
20
+ private let implementation = SSLPinningImpl()
21
+
22
+ /// The unique identifier for the plugin, used by Capacitor's internal mechanisms.
23
+ public let identifier = "SSLPinningPlugin"
24
+
25
+ /// The JavaScript name used to reference this plugin in Capacitor applications.
26
+ public let jsName = "SSLPinning"
27
+
28
+ /**
29
+ * A list of methods exposed by this plugin. These methods can be called from the JavaScript side.
30
+ * - `checkCertificate`: Validates an SSL certificate for a given URL.
31
+ * - `checkCertificates`: Validates multiple SSL certificates for given URLs.
32
+ * - `getPluginVersion`: Retrieves the current version of the plugin.
33
+ */
34
+ public let pluginMethods: [CAPPluginMethod] = [
35
+ CAPPluginMethod(name: "checkCertificate", returnType: CAPPluginReturnPromise),
36
+ CAPPluginMethod(name: "checkCertificates", returnType: CAPPluginReturnPromise),
37
+ CAPPluginMethod(name: "getPluginVersion", returnType: CAPPluginReturnPromise)
38
+ ]
39
+
40
+ /**
41
+ Plugin initialization.
42
+ Loads config and sets up lifecycle observers.
43
+ */
44
+ override public func load() {
45
+ let cfg = SSLPinningConfig(plugin: self)
46
+ self.config = cfg
47
+ implementation.applyConfig(cfg)
48
+ }
49
+
50
+ // MARK: - SSL Pinning Methods
51
+
52
+ /**
53
+ Validates an SSL certificate for a given URL.
54
+ - Parameters:
55
+ - call: The CAPPluginCall object containing the call details from JavaScript.
56
+ */
57
+ @objc func checkCertificate(_ call: CAPPluginCall) {
58
+ let url = call.getString("url", "")
59
+ let fingerprintValue = call.getString("fingerprint", "")
60
+ let fingerprintArg = fingerprintValue.isEmpty ? nil : fingerprintValue
61
+
62
+ if url.isEmpty {
63
+ call.resolve([
64
+ "fingerprintMatched": false,
65
+ "error": "Missing url"
66
+ ])
67
+ return
68
+ }
69
+
70
+ implementation.checkCertificate(
71
+ urlString: url,
72
+ fingerprintFromArgs: fingerprintArg
73
+ ) { result in
74
+ call.resolve(result)
75
+ }
76
+ }
77
+
78
+ /**
79
+ Validates multiple SSL certificates for given URLs.
80
+ - Parameters:
81
+ - call: The CAPPluginCall object containing the call details from JavaScript.
82
+ */
83
+ @objc func checkCertificates(_ call: CAPPluginCall) {
84
+ let url = call.getString("url", "")
85
+
86
+ let fingerprints = call
87
+ .getArray("fingerprints", [])
88
+ .compactMap { $0 as? String }
89
+
90
+ if url.isEmpty {
91
+ call.resolve([
92
+ "fingerprintMatched": false,
93
+ "error": "Missing url"
94
+ ])
95
+ return
96
+ }
97
+
98
+ implementation.checkCertificates(
99
+ urlString: url,
100
+ fingerprintsFromArgs: fingerprints.isEmpty ? nil : fingerprints
101
+ ) { result in
102
+ call.resolve(result)
103
+ }
104
+ }
105
+
106
+ // MARK: - Version
107
+
108
+ /// Retrieves the plugin version synchronized from package.json.
109
+ @objc func getPluginVersion(_ call: CAPPluginCall) {
110
+ // Standardized enum name across all CapKit plugins
111
+ call.resolve([
112
+ "version": PluginVersion.number
113
+ ])
114
+ }
115
+
116
+ }
@@ -0,0 +1,57 @@
1
+ import Capacitor
2
+
3
+ /**
4
+ Centralized logger for the SSLPinning plugin.
5
+
6
+ This logger mirrors the Android Logger pattern and provides:
7
+ - a single logging entry point
8
+ - runtime-controlled verbose logging
9
+ - consistent log formatting
10
+
11
+ Business logic MUST NOT perform configuration checks directly.
12
+ */
13
+ enum SSLPinningLogger {
14
+
15
+ /**
16
+ Controls whether debug logs are printed.
17
+
18
+ This value is set once during plugin load
19
+ based on configuration values.
20
+ */
21
+ static var verbose: Bool = false
22
+
23
+ /**
24
+ Prints a verbose / debug log message.
25
+
26
+ This method is intended for development-time diagnostics
27
+ and is automatically silenced when verbose logging is disabled.
28
+ */
29
+ static func debug(_ items: Any...) {
30
+ guard verbose else { return }
31
+ log(items)
32
+ }
33
+
34
+ /**
35
+ Prints an error log message.
36
+
37
+ Error logs are always printed regardless of verbosity.
38
+ */
39
+ static func error(_ items: Any...) {
40
+ log(items)
41
+ }
42
+ }
43
+
44
+ /**
45
+ Low-level log printer with a consistent prefix.
46
+
47
+ This function should not be used directly outside this file.
48
+ */
49
+ private func log(_ items: Any..., separator: String = " ", terminator: String = "\n") {
50
+ CAPLog.print("⚡️ SSLPinning -", terminator: separator)
51
+ for (itemIndex, item) in items.enumerated() {
52
+ CAPLog.print(
53
+ item,
54
+ terminator: itemIndex == items.count - 1 ? terminator : separator
55
+ )
56
+ }
57
+ }
@@ -0,0 +1,47 @@
1
+ import Foundation
2
+ import CryptoKit
3
+ import Security
4
+
5
+ /// Utility helpers for SSL Pinning logic.
6
+ /// Pure Swift — no Capacitor dependency.
7
+ struct SSLPinningUtils {
8
+
9
+ /// Validates and returns a HTTPS URL.
10
+ static func httpsURL(from value: String) -> URL? {
11
+ guard let url = URL(string: value),
12
+ url.scheme?.lowercased() == "https" else {
13
+ return nil
14
+ }
15
+ return url
16
+ }
17
+
18
+ /// Normalizes a fingerprint string (removes colons, lowercases).
19
+ /// Example: "AA:BB:CC" → "aabbcc"
20
+ static func normalizeFingerprint(_ value: String) -> String {
21
+ value
22
+ .replacingOccurrences(of: ":", with: "")
23
+ .lowercased()
24
+ }
25
+
26
+ /// Computes the SHA-256 fingerprint from a SecCertificate.
27
+ /// Output format: "aa:bb:cc:dd"
28
+ static func sha256Fingerprint(from certificate: SecCertificate) -> String {
29
+ let data = SecCertificateCopyData(certificate) as Data
30
+ let hash = SHA256.hash(data: data)
31
+ return hash
32
+ .map { String(format: "%02x", $0) }
33
+ .joined(separator: ":")
34
+ }
35
+
36
+ /// Extracts the leaf certificate from a SecTrust, handling iOS <15 and ≥15.
37
+ static func leafCertificate(from trust: SecTrust) -> SecCertificate? {
38
+ if #available(iOS 15.0, *) {
39
+ if let chain = SecTrustCopyCertificateChain(trust) as? [SecCertificate] {
40
+ return chain.first
41
+ }
42
+ return nil
43
+ } else {
44
+ return SecTrustGetCertificateAtIndex(trust, 0)
45
+ }
46
+ }
47
+ }
@@ -0,0 +1,16 @@
1
+ // This file is automatically generated. Do not modify manually.
2
+ import Foundation
3
+
4
+ /**
5
+ Container for the plugin's version information.
6
+ This enum provides a centralized, single source of truth for the native
7
+ version string, synchronized directly from the project's 'package.json'.
8
+ It ensures parity across JavaScript, Android, and iOS platforms.
9
+ */
10
+ public enum PluginVersion {
11
+ /**
12
+ The semantic version string of the plugin.
13
+ Value synchronized from package.json: "8.0.0-next.0"
14
+ */
15
+ public static let number = "8.0.0-next.0"
16
+ }
@@ -0,0 +1,5 @@
1
+ import XCTest
2
+ @testable import SSLPinningPlugin
3
+
4
+ class SSLPinningTests: XCTestCase {
5
+ }
package/package.json ADDED
@@ -0,0 +1,117 @@
1
+ {
2
+ "name": "@cap-kit/ssl-pinning",
3
+ "version": "8.0.0-next.0",
4
+ "description": "Capacitor plugin for runtime SSL certificate fingerprint pinning on iOS and Android",
5
+ "type": "module",
6
+ "private": false,
7
+ "engines": {
8
+ "node": ">=24.0.0",
9
+ "pnpm": ">=10.0.0"
10
+ },
11
+ "main": "dist/plugin.cjs.js",
12
+ "module": "dist/esm/index.js",
13
+ "types": "dist/esm/index.d.ts",
14
+ "unpkg": "dist/plugin.js",
15
+ "files": [
16
+ "android/src/main/",
17
+ "android/build.gradle",
18
+ "dist/",
19
+ "ios/Sources",
20
+ "ios/Tests",
21
+ "Package.swift",
22
+ "CapKitSSLPinning.podspec",
23
+ "LICENSE",
24
+ "bin/",
25
+ "scripts/"
26
+ ],
27
+ "author": "CapKit Team",
28
+ "license": "MIT",
29
+ "repository": {
30
+ "type": "git",
31
+ "url": "git+https://github.com/cap-kit/capacitor-plugins.git",
32
+ "directory": "packages/ssl-pinning"
33
+ },
34
+ "bugs": {
35
+ "url": "https://github.com/cap-kit/capacitor-plugins/issues"
36
+ },
37
+ "homepage": "https://github.com/cap-kit/capacitor-plugins/tree/main/packages/ssl-pinning#readme",
38
+ "keywords": [
39
+ "capacitor",
40
+ "capacitor-plugin",
41
+ "mobile",
42
+ "native",
43
+ "cap-kit",
44
+ "ios",
45
+ "android",
46
+ "ssl",
47
+ "https",
48
+ "ssl-pinning",
49
+ "certificate-fingerprint",
50
+ "fingerprint-pinning",
51
+ "runtime-security",
52
+ "mitm"
53
+ ],
54
+ "bin": {
55
+ "ssl-fingerprint": "./dist/cli/fingerprint.js",
56
+ "capacitor-ssl-fingerprint": "./dist/cli/fingerprint.js"
57
+ },
58
+ "publishConfig": {
59
+ "access": "public"
60
+ },
61
+ "dependencies": {
62
+ "yargs": "^18.0.0"
63
+ },
64
+ "devDependencies": {
65
+ "@capacitor/android": "^8.0.2",
66
+ "@capacitor/cli": "^8.0.2",
67
+ "@capacitor/core": "^8.0.2",
68
+ "@capacitor/docgen": "^0.3.1",
69
+ "@capacitor/ios": "^8.0.2",
70
+ "@eslint/eslintrc": "^3.3.3",
71
+ "@eslint/js": "^9.39.2",
72
+ "eslint": "^9.39.2",
73
+ "eslint-plugin-import": "^2.32.0",
74
+ "@types/node": "^25.0.10",
75
+ "@types/yargs": "^17.0.35",
76
+ "globals": "^17.2.0",
77
+ "prettier": "^3.8.1",
78
+ "prettier-plugin-java": "^2.8.1",
79
+ "rimraf": "^6.1.2",
80
+ "rollup": "^4.57.0",
81
+ "swiftlint": "^2.0.0",
82
+ "typescript": "~5.9.3",
83
+ "typescript-eslint": "^8.54.0"
84
+ },
85
+ "peerDependencies": {
86
+ "@capacitor/core": "^8.0.2"
87
+ },
88
+ "capacitor": {
89
+ "ios": {
90
+ "src": "ios"
91
+ },
92
+ "android": {
93
+ "src": "android"
94
+ }
95
+ },
96
+ "scripts": {
97
+ "verify": "pnpm run verify:ios && pnpm run verify:android && pnpm run verify:web",
98
+ "verify:ios": "node ./scripts/sync-version.js && xcodebuild -scheme CapKitSslPinning -destination generic/platform=iOS",
99
+ "verify:android": "cd android && ./gradlew clean build && cd ..",
100
+ "verify:web": "pnpm run build",
101
+ "lint:android": "cd android && ./gradlew ktlintCheck",
102
+ "fmt:android": "cd android && ./gradlew ktlintFormat",
103
+ "lint": "pnpm run eslint . && pnpm run swiftlint lint --strict || true && pnpm run lint:android || true",
104
+ "format:check": "prettier --check \"**/*.{css,html,ts,js,java}\" --plugin=prettier-plugin-java",
105
+ "format": "eslint --fix . && prettier --write \"**/*.{css,html,ts,js,java}\" --plugin=prettier-plugin-java && pnpm run swiftlint --fix --format && pnpm run fmt:android",
106
+ "eslint": "eslint",
107
+ "prettier": "prettier \"**/*.{css,html,ts,js,java}\" --plugin=prettier-plugin-java",
108
+ "swiftlint": "node-swiftlint lint ios/Sources",
109
+ "docgen": "docgen --api SSLPinningPlugin --output-readme README.md --output-json dist/docs.json",
110
+ "build": "node ./scripts/sync-version.js && pnpm run clean && pnpm run docgen && tsc && rollup -c rollup.config.mjs && node scripts/chmod.js",
111
+ "clean": "rimraf ./dist",
112
+ "watch": "tsc --watch",
113
+ "test": "pnpm run verify",
114
+ "removePacked": "rimraf -g cap-kit-ssl-pinning-*.tgz",
115
+ "publish:locally": "pnpm run removePacked && pnpm run build && pnpm pack"
116
+ }
117
+ }
@@ -0,0 +1,34 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+
5
+ // Reconstruct __dirname, which does not exist in ESM
6
+ const __filename = fileURLToPath(import.meta.url);
7
+ const __dirname = path.dirname(__filename);
8
+
9
+ // Correct path to the compiled CLI
10
+ const cliPath = path.resolve(__dirname, '../dist/cli/fingerprint.js');
11
+
12
+ try {
13
+ if (fs.existsSync(cliPath)) {
14
+ // 1. Read the file contents
15
+ let content = fs.readFileSync(cliPath, 'utf8');
16
+
17
+ // 2. Add shebang if missing
18
+ if (!content.startsWith('#!/usr/bin/env node')) {
19
+ content = '#!/usr/bin/env node\n' + content;
20
+ fs.writeFileSync(cliPath, content);
21
+ console.log('✅ Shebang added to CLI.');
22
+ }
23
+
24
+ // 3. Make the file executable
25
+ fs.chmodSync(cliPath, '755');
26
+ console.log('✅ CLI permissions set to 755.');
27
+ } else {
28
+ console.error(`❌ CLI file not found at: ${cliPath}`);
29
+ // Do not fail the build if the file does not exist yet, just warn
30
+ }
31
+ } catch (err) {
32
+ console.error('❌ Error setting permissions:', err);
33
+ process.exit(1);
34
+ }