@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 @@
|
|
|
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
|
+
}
|
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
|
+
}
|
package/scripts/chmod.js
ADDED
|
@@ -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
|
+
}
|