diff --git a/Cartfile b/Cartfile index 4b3c11b..1b01d39 100644 --- a/Cartfile +++ b/Cartfile @@ -1 +1,2 @@ github "ReactiveX/RxSwift" ~> 6.2.0 +github "wultra/WultraSSLPinning" \ No newline at end of file diff --git a/Example/HCaptcha.xcodeproj/project.pbxproj b/Example/HCaptcha.xcodeproj/project.pbxproj index 81c2e7c..01031c9 100644 --- a/Example/HCaptcha.xcodeproj/project.pbxproj +++ b/Example/HCaptcha.xcodeproj/project.pbxproj @@ -486,16 +486,22 @@ inputPaths = ( "${PODS_ROOT}/Target Support Files/Pods-HCaptcha_Example/Pods-HCaptcha_Example-frameworks.sh", "${BUILT_PRODUCTS_DIR}/HCaptcha-Core-RxSwift/HCaptcha.framework", + "${BUILT_PRODUCTS_DIR}/PowerAuth2/PowerAuth2.framework", "${BUILT_PRODUCTS_DIR}/RxCocoa/RxCocoa.framework", "${BUILT_PRODUCTS_DIR}/RxRelay/RxRelay.framework", "${BUILT_PRODUCTS_DIR}/RxSwift/RxSwift.framework", + "${BUILT_PRODUCTS_DIR}/WultraSSLPinning/WultraSSLPinning.framework", + "${PODS_XCFRAMEWORKS_BUILD_DIR}/PowerAuthCore/PowerAuthCore.framework/PowerAuthCore", ); name = "[CP] Embed Pods Frameworks"; outputPaths = ( "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/HCaptcha.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/PowerAuth2.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RxCocoa.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RxRelay.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RxSwift.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/WultraSSLPinning.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/PowerAuthCore.framework", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; @@ -598,10 +604,16 @@ inputPaths = ( "${PODS_ROOT}/Target Support Files/Pods-HCaptcha_Objc_Example/Pods-HCaptcha_Objc_Example-frameworks.sh", "${BUILT_PRODUCTS_DIR}/HCaptcha-Core/HCaptcha.framework", + "${BUILT_PRODUCTS_DIR}/PowerAuth2/PowerAuth2.framework", + "${BUILT_PRODUCTS_DIR}/WultraSSLPinning/WultraSSLPinning.framework", + "${PODS_XCFRAMEWORKS_BUILD_DIR}/PowerAuthCore/PowerAuthCore.framework/PowerAuthCore", ); name = "[CP] Embed Pods Frameworks"; outputPaths = ( "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/HCaptcha.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/PowerAuth2.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/WultraSSLPinning.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/PowerAuthCore.framework", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; diff --git a/Example/Podfile.lock b/Example/Podfile.lock index bb2668c..f4a546b 100644 --- a/Example/Podfile.lock +++ b/Example/Podfile.lock @@ -1,9 +1,13 @@ PODS: - AppSwizzle (1.3.1) - - HCaptcha/Core (2.0.0) + - HCaptcha/Core (2.0.0): + - WultraSSLPinning/PowerAuthIntegration - HCaptcha/RxSwift (2.0.0): - HCaptcha/Core - RxSwift (~> 6.2.0) + - PowerAuth2 (1.6.2): + - PowerAuthCore (~> 1.6.2) + - PowerAuthCore (1.6.2) - RxBlocking (6.2.0): - RxSwift (= 6.2.0) - RxCocoa (6.2.0): @@ -13,6 +17,10 @@ PODS: - RxSwift (= 6.2.0) - RxSwift (6.2.0) - SwiftLint (0.43.1) + - WultraSSLPinning/Lib (1.4.0) + - WultraSSLPinning/PowerAuthIntegration (1.4.0): + - PowerAuth2 (>= 1.6.0) + - WultraSSLPinning/Lib DEPENDENCIES: - AppSwizzle (~> 1.3) @@ -25,11 +33,14 @@ DEPENDENCIES: SPEC REPOS: trunk: - AppSwizzle + - PowerAuth2 + - PowerAuthCore - RxBlocking - RxCocoa - RxRelay - RxSwift - SwiftLint + - WultraSSLPinning EXTERNAL SOURCES: HCaptcha: @@ -37,12 +48,15 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: AppSwizzle: db36e436f56110d93e5ae0147683435df593cabc - HCaptcha: a4c1efafe5a049c2db09d847736e369645d1079a + HCaptcha: 4ce547fc4e00a50fcad0d1a9811e67703dcdc0a1 + PowerAuth2: 18365eda20a3aaec6e1d0d92679c7e90d646cc05 + PowerAuthCore: 733c68b8f923d49304353ac5d7871ef49f948b38 RxBlocking: 0b29f7d2079109a8de49c411381bed7c33ef1eeb RxCocoa: 4baf94bb35f2c0ab31bc0cb9f1900155f646ba42 RxRelay: e72dbfd157807478401ef1982e1c61c945c94b2f RxSwift: d356ab7bee873611322f134c5f9ef379fa183d8f SwiftLint: 99f82d07b837b942dd563c668de129a03fc3fb52 + WultraSSLPinning: 73a73de4f89b0ac98154c3ef761ed6eee7760749 PODFILE CHECKSUM: 44657674683c7639f916ef6eb6fae5387395d652 diff --git a/HCaptcha.podspec b/HCaptcha.podspec index c786148..aba6282 100644 --- a/HCaptcha.podspec +++ b/HCaptcha.podspec @@ -25,6 +25,8 @@ invisibility is not possible. core.source_files = 'HCaptcha/Classes/*' core.frameworks = ['WebKit', 'JavaScriptCore'] + core.dependency 'WultraSSLPinning/PowerAuthIntegration' + core.resource_bundles = { 'HCaptcha' => ['HCaptcha/Assets/**/*'] } diff --git a/HCaptcha/Classes/HCaptcha.swift b/HCaptcha/Classes/HCaptcha.swift index d7d04bc..cde8f65 100644 --- a/HCaptcha/Classes/HCaptcha.swift +++ b/HCaptcha/Classes/HCaptcha.swift @@ -59,7 +59,8 @@ public class HCaptcha: NSObject { imghost: URL? = nil, host: String? = nil, theme: String = "light", - customTheme: String? = nil + customTheme: String? = nil, + sslPinning: Bool = false ) throws { let infoDict = Bundle.main.infoDictionary @@ -80,7 +81,8 @@ public class HCaptcha: NSObject { imghost: imghost, host: host, theme: theme, - customTheme: customTheme) + customTheme: customTheme, + sslPinning: sslPinning) self.init(manager: HCaptchaWebViewManager( html: config.html, @@ -89,7 +91,8 @@ public class HCaptcha: NSObject { endpoint: config.getEndpointURL(locale: locale), size: config.size, rqdata: config.rqdata, - theme: config.actualTheme + theme: config.actualTheme, + sslPinning: config.sslPinning )) } diff --git a/HCaptcha/Classes/HCaptchaConfig.swift b/HCaptcha/Classes/HCaptchaConfig.swift index 7bd182e..78b1a0c 100644 --- a/HCaptcha/Classes/HCaptchaConfig.swift +++ b/HCaptcha/Classes/HCaptchaConfig.swift @@ -100,6 +100,9 @@ struct HCaptchaConfig { self.customTheme ?? "\"\(theme)\"" } + /// Enable SSL pinning + let sslPinning: Bool + /// The Bundle that holds HCaptcha's assets private static let bundle: Bundle = { #if SWIFT_PACKAGE @@ -144,7 +147,8 @@ struct HCaptchaConfig { imghost: URL?, host: String?, theme: String, - customTheme: String?) throws { + customTheme: String?, + sslPinning: Bool) throws { guard let filePath = HCaptchaConfig.bundle.path(forResource: "hcaptcha", ofType: "html") else { throw HCaptchaError.htmlLoadError } @@ -187,6 +191,7 @@ struct HCaptchaConfig { self.host = host self.theme = theme self.customTheme = customTheme + self.sslPinning = sslPinning } /** diff --git a/HCaptcha/Classes/HCaptchaWebViewManager.swift b/HCaptcha/Classes/HCaptchaWebViewManager.swift index 76fbccf..8d6e754 100644 --- a/HCaptcha/Classes/HCaptchaWebViewManager.swift +++ b/HCaptcha/Classes/HCaptchaWebViewManager.swift @@ -8,6 +8,7 @@ import Foundation import WebKit +import WultraSSLPinning /** Handles comunications with the webview containing the HCaptcha challenge. @@ -89,6 +90,12 @@ internal class HCaptchaWebViewManager { /// Keep error If it happens before validate call fileprivate var lastError: HCaptchaError? + /// Enable SSL pinning + fileprivate var sslPinning: Bool + + /// SSL pinning aware navigation delegate + fileprivate let webViewNavDelegate = HCaptchaWebViewNavDelegate() + /// The webview that executes JS code lazy var webView: WKWebView = { let webview = WKWebView( @@ -99,6 +106,10 @@ internal class HCaptchaWebViewManager { webview.accessibilityTraits = UIAccessibilityTraits.link webview.isHidden = true + if sslPinning { + webview.navigationDelegate = webViewNavDelegate + } + return webview }() @@ -113,8 +124,9 @@ internal class HCaptchaWebViewManager { - theme: Widget theme, value must be valid JS Object or String with brackets */ init(html: String, apiKey: String, baseURL: URL, endpoint: URL, - size: HCaptchaSize, rqdata: String?, theme: String) { + size: HCaptchaSize, rqdata: String?, theme: String, sslPinning: Bool) { self.baseURL = baseURL + self.sslPinning = sslPinning self.decoder = HCaptchaDecoder { [weak self] result in self?.handle(result: result) } diff --git a/HCaptcha/Classes/HCaptchaWebViewNavDelegate.swift b/HCaptcha/Classes/HCaptchaWebViewNavDelegate.swift new file mode 100644 index 0000000..2d7da30 --- /dev/null +++ b/HCaptcha/Classes/HCaptchaWebViewNavDelegate.swift @@ -0,0 +1,30 @@ +// +// HCaptchaWebViewNavDelegate.swift +// HCaptcha +// +// Copyright © 2022 HCaptcha. All rights reserved. +// + +import Foundation +import WebKit +import WultraSSLPinning + +class HCaptchaWebViewNavDelegate: UIResponder, WKNavigationDelegate { + fileprivate static var certStore: CertStore { + let publicKey = "BO47Jqs1wXV9dt1q1hMHuZzUPVzGNaPn9vUqRzleMiHAIYg+zZmktobi3nKVrtExIctL9f8aTdpfwhex21jbI9g=" + let config = CertStoreConfiguration(serviceUrl: URL(string: "https://hcaptcha.com")!, + publicKey: publicKey) + return .powerAuthCertStore(configuration: config) + } + + func webView(_ webView: WKWebView, + didReceive challenge: URLAuthenticationChallenge, + completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) { + switch HCaptchaWebViewNavDelegate.certStore.validate(challenge: challenge) { + case .trusted: + completionHandler(.performDefaultHandling, nil) + case .untrusted, .empty: + completionHandler(.cancelAuthenticationChallenge, nil) + } + } +} diff --git a/Package.swift b/Package.swift index 3512d77..362cfb1 100644 --- a/Package.swift +++ b/Package.swift @@ -21,6 +21,7 @@ let package = Package( ], dependencies: [ .package(url: "https://github.com/ReactiveX/RxSwift", .upToNextMajor(from: "6.2.0")) + .package(url: "https://github.com/wultra/ssl-pinning-ios") ], targets: [ .target(