@dynamic-labs-sdk/client 1.7.0 → 1.8.1
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/android/build.gradle +231 -0
- package/android/gradle/wrapper/gradle-wrapper.jar +0 -0
- package/android/gradle/wrapper/gradle-wrapper.properties +7 -0
- package/android/gradle.properties +8 -0
- package/android/gradlew +251 -0
- package/android/gradlew.bat +94 -0
- package/android/settings.gradle +30 -0
- package/android/src/main/java/xyz/dynamic/client/DynamicClientPackage.kt +52 -0
- package/android/src/main/java/xyz/dynamic/client/keychain/KeyStoreKeyManager.kt +147 -0
- package/android/src/main/java/xyz/dynamic/client/keychain/KeychainModule.kt +85 -0
- package/android/src/main/java/xyz/dynamic/client/manifest/ReactNativeManifestModule.kt +25 -0
- package/android/src/main/java/xyz/dynamic/client/webview/DynamicWebViewClient.kt +83 -0
- package/android/src/main/java/xyz/dynamic/client/webview/DynamicWebViewHost.kt +518 -0
- package/android/src/main/java/xyz/dynamic/client/webview/DynamicWebViewModule.kt +147 -0
- package/android/src/test/java/xyz/dynamic/client/keychain/KeyStoreKeyManagerTest.kt +288 -0
- package/android/src/test/java/xyz/dynamic/client/webview/DynamicWebViewClientTest.kt +196 -0
- package/android/src/test/java/xyz/dynamic/client/webview/DynamicWebViewHostOpenTest.kt +347 -0
- package/android/src/test/java/xyz/dynamic/client/webview/DynamicWebViewHostTest.kt +171 -0
- package/android/src/test/java/xyz/dynamic/client/webview/DynamicWebViewModuleTest.kt +191 -0
- package/dist/{InvalidParamError-Crwntjl7.native.esm.js → InvalidParamError-BKnC9rcJ.esm.js} +441 -10
- package/dist/InvalidParamError-BKnC9rcJ.esm.js.map +1 -0
- package/dist/{InvalidParamError-fQLJaGBW.esm.js → InvalidParamError-Btv8VGSa.native.esm.js} +3 -3
- package/dist/InvalidParamError-Btv8VGSa.native.esm.js.map +1 -0
- package/dist/{InvalidParamError-CTssOe86.cjs → InvalidParamError-DGBih-LJ.cjs} +550 -4
- package/dist/InvalidParamError-DGBih-LJ.cjs.map +1 -0
- package/dist/{NotWaasWalletAccountError-DF_S7gqo.esm.js → NotWaasWalletAccountError-CHr72wTH.esm.js} +3 -3
- package/dist/{NotWaasWalletAccountError-DF_S7gqo.esm.js.map → NotWaasWalletAccountError-CHr72wTH.esm.js.map} +1 -1
- package/dist/{NotWaasWalletAccountError-BJLGGYmU.native.esm.js → NotWaasWalletAccountError-CRmyVoB3.native.esm.js} +3 -3
- package/dist/{NotWaasWalletAccountError-BJLGGYmU.native.esm.js.map → NotWaasWalletAccountError-CRmyVoB3.native.esm.js.map} +1 -1
- package/dist/{NotWaasWalletAccountError-2RaDipZu.cjs → NotWaasWalletAccountError-i1FozGep.cjs} +3 -3
- package/dist/{NotWaasWalletAccountError-2RaDipZu.cjs.map → NotWaasWalletAccountError-i1FozGep.cjs.map} +1 -1
- package/dist/core.cjs +23 -23
- package/dist/core.cjs.map +1 -1
- package/dist/core.esm.js +5 -5
- package/dist/core.native.esm.js +7 -7
- package/dist/core.native.esm.js.map +1 -1
- package/dist/errors/WebViewLoadFailedError.d.ts +1 -1
- package/dist/errors/WebViewLoadFailedError.d.ts.map +1 -1
- package/dist/exports/index.d.ts +2 -0
- package/dist/exports/index.d.ts.map +1 -1
- package/dist/{getNetworkProviderFromNetworkId-C1p9Rrg3.cjs → getNetworkProviderFromNetworkId-BdgI7g5U.cjs} +11 -317
- package/dist/getNetworkProviderFromNetworkId-BdgI7g5U.cjs.map +1 -0
- package/dist/{getNetworkProviderFromNetworkId-BCSmILlH.native.esm.js → getNetworkProviderFromNetworkId-Bm41knUq.native.esm.js} +6 -6
- package/dist/getNetworkProviderFromNetworkId-Bm41knUq.native.esm.js.map +1 -0
- package/dist/{getNetworkProviderFromNetworkId-BWhaD23J.esm.js → getNetworkProviderFromNetworkId-CRuh5pgc.esm.js} +4 -242
- package/dist/getNetworkProviderFromNetworkId-CRuh5pgc.esm.js.map +1 -0
- package/dist/{getSignedSessionId-CxxYNC_8.cjs → getSignedSessionId-Cwmt4BkY.cjs} +3 -3
- package/dist/{getSignedSessionId-CxxYNC_8.cjs.map → getSignedSessionId-Cwmt4BkY.cjs.map} +1 -1
- package/dist/{getSignedSessionId-c7dS4PQ9.esm.js → getSignedSessionId-D2w_IRdt.esm.js} +3 -3
- package/dist/{getSignedSessionId-c7dS4PQ9.esm.js.map → getSignedSessionId-D2w_IRdt.esm.js.map} +1 -1
- package/dist/{getSignedSessionId-DPOXdh6y.native.esm.js → getSignedSessionId-S6CBWkOn.native.esm.js} +3 -3
- package/dist/{getSignedSessionId-DPOXdh6y.native.esm.js.map → getSignedSessionId-S6CBWkOn.native.esm.js.map} +1 -1
- package/dist/getVerifiedCredentialForWalletAccount-CDuP1kaI.cjs +347 -0
- package/dist/getVerifiedCredentialForWalletAccount-CDuP1kaI.cjs.map +1 -0
- package/dist/{getVerifiedCredentialForWalletAccount-Cahp763h.esm.js → getVerifiedCredentialForWalletAccount-D33r7Drd.native.esm.js} +2 -2
- package/dist/{getVerifiedCredentialForWalletAccount-DuNbORNW.native.esm.js.map → getVerifiedCredentialForWalletAccount-D33r7Drd.native.esm.js.map} +1 -1
- package/dist/getVerifiedCredentialForWalletAccount-QwfkKHfr.esm.js +269 -0
- package/dist/getVerifiedCredentialForWalletAccount-QwfkKHfr.esm.js.map +1 -0
- package/dist/index.cjs +67 -67
- package/dist/index.cjs.map +1 -1
- package/dist/index.esm.js +5 -5
- package/dist/index.esm.js.map +1 -1
- package/dist/index.native.esm.js +5 -5
- package/dist/index.native.esm.js.map +1 -1
- package/dist/isMfaRequiredForAction-5p3i98-Z.esm.js +318 -0
- package/dist/isMfaRequiredForAction-5p3i98-Z.esm.js.map +1 -0
- package/dist/{isMfaRequiredForAction-D8G5PBAd.native.esm.js → isMfaRequiredForAction-cmElIVof.native.esm.js} +2 -2
- package/dist/{isMfaRequiredForAction-D8G5PBAd.native.esm.js.map → isMfaRequiredForAction-cmElIVof.native.esm.js.map} +1 -1
- package/dist/isMfaRequiredForAction-z74isCQ7.cjs +405 -0
- package/dist/isMfaRequiredForAction-z74isCQ7.cjs.map +1 -0
- package/dist/modules/waas/createWaasClient/createWaasClient.d.ts.map +1 -1
- package/dist/modules/waas/createWaasClient/translateNativeOpenError.d.ts.map +1 -1
- package/dist/modules/wallets/walletProvider/events/onWalletProviderEvent/onWalletProviderEvent.d.ts +1 -2
- package/dist/modules/wallets/walletProvider/events/onWalletProviderEvent/onWalletProviderEvent.d.ts.map +1 -1
- package/dist/tsconfig.lib.tsbuildinfo +1 -1
- package/dist/waas.cjs +9 -9
- package/dist/waas.cjs.map +1 -1
- package/dist/waas.esm.js +3 -3
- package/dist/waas.native.esm.js +3 -3
- package/dist/waasCore.cjs +8 -5
- package/dist/waasCore.cjs.map +1 -1
- package/dist/waasCore.esm.js +8 -4
- package/dist/waasCore.esm.js.map +1 -1
- package/dist/waasCore.native.esm.js +9 -8
- package/dist/waasCore.native.esm.js.map +1 -1
- package/dynamic-labs-sdk-client.podspec +27 -0
- package/ios/DynamicClientKeychain.h +4 -0
- package/ios/DynamicClientKeychain.mm +101 -0
- package/ios/DynamicClientManifest.h +4 -0
- package/ios/DynamicClientManifest.mm +45 -0
- package/ios/DynamicClientWebView.h +5 -0
- package/ios/DynamicClientWebView.mm +143 -0
- package/ios/DynamicWebViewImpl.swift +603 -0
- package/ios/LiveSecKeyAPI.swift +86 -0
- package/ios/ReactNativeManifestImpl.swift +20 -0
- package/ios/SecKeyAPI.swift +57 -0
- package/ios/SecureEnclaveKeyManager.swift +191 -0
- package/package.json +14 -5
- package/react-native.config.cjs +14 -0
- package/src/turboModules/NativeDynamicWebView.native.spec.ts +61 -0
- package/src/turboModules/NativeDynamicWebView.ts +40 -0
- package/src/turboModules/NativeKeychain.native.spec.ts +47 -0
- package/src/turboModules/NativeKeychain.ts +19 -0
- package/src/turboModules/NativeReactNativeManifest.native.spec.ts +55 -0
- package/src/turboModules/NativeReactNativeManifest.ts +28 -0
- package/dist/InvalidParamError-CTssOe86.cjs.map +0 -1
- package/dist/InvalidParamError-Crwntjl7.native.esm.js.map +0 -1
- package/dist/InvalidParamError-fQLJaGBW.esm.js.map +0 -1
- package/dist/getNetworkProviderFromNetworkId-BCSmILlH.native.esm.js.map +0 -1
- package/dist/getNetworkProviderFromNetworkId-BWhaD23J.esm.js.map +0 -1
- package/dist/getNetworkProviderFromNetworkId-C1p9Rrg3.cjs.map +0 -1
- package/dist/getVerifiedCredentialForWalletAccount-Cahp763h.esm.js.map +0 -1
- package/dist/getVerifiedCredentialForWalletAccount-Di8dM5Wx.cjs +0 -887
- package/dist/getVerifiedCredentialForWalletAccount-Di8dM5Wx.cjs.map +0 -1
- package/dist/getVerifiedCredentialForWalletAccount-DuNbORNW.native.esm.js +0 -701
- package/dist/isMfaRequiredForAction-BjRvPrU6.esm.js +0 -79
- package/dist/isMfaRequiredForAction-BjRvPrU6.esm.js.map +0 -1
- package/dist/isMfaRequiredForAction-hlhj0qAC.cjs +0 -96
- package/dist/isMfaRequiredForAction-hlhj0qAC.cjs.map +0 -1
|
@@ -0,0 +1,603 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import UIKit
|
|
3
|
+
import WebKit
|
|
4
|
+
|
|
5
|
+
/// Delegate used by the Swift implementation to hand messages
|
|
6
|
+
/// received from the WebView back to the ObjC++ module, which
|
|
7
|
+
/// forwards them to JavaScript via `RCTEventEmitter`.
|
|
8
|
+
@objc public protocol DynamicWebViewImplDelegate: AnyObject {
|
|
9
|
+
func didReceiveMessage(forInstanceId instanceId: String, data: Any)
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/// JS glue injected into every DynamicWebView instance at document start.
|
|
13
|
+
///
|
|
14
|
+
/// Purpose: hosted pages written for the browser iframe transport call
|
|
15
|
+
/// `window.parent.postMessage` to reach the SDK host. In a WKWebView
|
|
16
|
+
/// there is no real parent window, so we:
|
|
17
|
+
/// 1. Intercept the page's outbound `window.parent.postMessage` calls
|
|
18
|
+
/// and forward their payload to the native side via
|
|
19
|
+
/// `webkit.messageHandlers.dynamicBridge.postMessage(data)`.
|
|
20
|
+
/// 2. Expose `window.__dynamicReceive(json)` which re-dispatches a
|
|
21
|
+
/// synthetic `MessageEvent` on `window` so the page's existing
|
|
22
|
+
/// `window.addEventListener('message', ...)` code receives host
|
|
23
|
+
/// messages exactly as it would inside a browser iframe.
|
|
24
|
+
private let bridgeUserScript = """
|
|
25
|
+
(function() {
|
|
26
|
+
if (window.__dynamicBridgeInstalled) { return; }
|
|
27
|
+
window.__dynamicBridgeInstalled = true;
|
|
28
|
+
|
|
29
|
+
var post = function(data) {
|
|
30
|
+
try {
|
|
31
|
+
window.webkit.messageHandlers.dynamicBridge.postMessage(data);
|
|
32
|
+
} catch (e) {}
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
// The hosted iframe page calls `window.parent.postMessage(data, hostOrigin)`.
|
|
36
|
+
// In a top-level WKWebView `window.parent === window`, so `window.parent.postMessage`
|
|
37
|
+
// resolves to `window.postMessage`. The browser's `postMessage` performs a
|
|
38
|
+
// targetOrigin check and silently drops the message when the declared hostOrigin
|
|
39
|
+
// (e.g. the RN app's bundler origin) does not match the WebView's own origin,
|
|
40
|
+
// which is always the case here. We override `window.postMessage` to intercept
|
|
41
|
+
// the call *before* the origin check runs and forward the payload to native.
|
|
42
|
+
try {
|
|
43
|
+
Object.defineProperty(window, 'postMessage', {
|
|
44
|
+
configurable: true,
|
|
45
|
+
writable: true,
|
|
46
|
+
value: function(data, targetOrigin, transfer) {
|
|
47
|
+
post(data);
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
} catch (e) {
|
|
51
|
+
try { window.postMessage = function(data) { post(data); }; } catch (_) {}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Called by native (via evaluateJavaScript) to deliver a host -> webview message.
|
|
55
|
+
// Re-dispatches a synthetic `MessageEvent` on `window` so the page's existing
|
|
56
|
+
// `window.addEventListener('message', ...)` handler fires exactly as it would
|
|
57
|
+
// in a real iframe.
|
|
58
|
+
window.__dynamicReceive = function(json) {
|
|
59
|
+
try {
|
|
60
|
+
var payload = typeof json === 'string' ? JSON.parse(json) : json;
|
|
61
|
+
var evt = new MessageEvent('message', {
|
|
62
|
+
data: payload,
|
|
63
|
+
origin: window.location.origin,
|
|
64
|
+
source: window
|
|
65
|
+
});
|
|
66
|
+
window.dispatchEvent(evt);
|
|
67
|
+
} catch (e) {}
|
|
68
|
+
};
|
|
69
|
+
})();
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
// `internal` (not `private`) so `shouldDeliverScriptMessage` and the bridge
|
|
73
|
+
// origin-check unit tests can reference the handler name without hardcoding it.
|
|
74
|
+
let bridgeHandlerName = "dynamicBridge"
|
|
75
|
+
private let errorDomain = "DynamicWebView"
|
|
76
|
+
|
|
77
|
+
// Keys read out of NSError.userInfo by the ObjC++ bridge when translating a
|
|
78
|
+
// native error into an RCT reject-block code + JS-accessible metadata.
|
|
79
|
+
// The JS wrapper reads `reason` to build a typed `WebViewLoadFailedError`.
|
|
80
|
+
private let errorReasonKey = "DynamicWebViewReason"
|
|
81
|
+
private let errorStatusCodeKey = "DynamicWebViewStatusCode"
|
|
82
|
+
private let errorUnderlyingDescriptionKey = "DynamicWebViewUnderlying"
|
|
83
|
+
|
|
84
|
+
/// Reasons mirror `WebViewLoadFailureReason` in the JS errors module.
|
|
85
|
+
/// Kept as plain strings rather than an enum so the value can cross
|
|
86
|
+
/// the ObjC/JS bridge unchanged via NSError.userInfo.
|
|
87
|
+
private enum Reason {
|
|
88
|
+
static let invalidUrl = "invalid_url"
|
|
89
|
+
static let noHostView = "no_host_view"
|
|
90
|
+
static let httpError = "http_error"
|
|
91
|
+
static let navigationFailed = "navigation_failed"
|
|
92
|
+
static let cancelled = "cancelled"
|
|
93
|
+
static let instanceBusy = "instance_busy"
|
|
94
|
+
static let contentProcessTerminated = "content_process_terminated"
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
private func makeError(
|
|
98
|
+
reason: String,
|
|
99
|
+
message: String,
|
|
100
|
+
statusCode: Int? = nil,
|
|
101
|
+
underlyingDescription: String? = nil
|
|
102
|
+
) -> NSError {
|
|
103
|
+
var userInfo: [String: Any] = [
|
|
104
|
+
NSLocalizedDescriptionKey: message,
|
|
105
|
+
errorReasonKey: reason,
|
|
106
|
+
]
|
|
107
|
+
if let statusCode = statusCode {
|
|
108
|
+
userInfo[errorStatusCodeKey] = statusCode
|
|
109
|
+
}
|
|
110
|
+
if let underlyingDescription = underlyingDescription {
|
|
111
|
+
userInfo[errorUnderlyingDescriptionKey] = underlyingDescription
|
|
112
|
+
}
|
|
113
|
+
return NSError(domain: errorDomain, code: 0, userInfo: userInfo)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// MARK: - Origin / trust helpers (pure, unit-testable)
|
|
117
|
+
|
|
118
|
+
/// `scheme://host[:port]` with the scheme-default https port (443) omitted, or
|
|
119
|
+
/// `nil` when scheme/host are missing. Normalizing makes the pinned origin
|
|
120
|
+
/// comparable to both a `WKSecurityOrigin` and a navigation `URL`, and mirrors
|
|
121
|
+
/// the Android `originOf` helper so the two platforms pin identically.
|
|
122
|
+
func normalizedOrigin(scheme: String?, host: String?, port: Int?) -> String? {
|
|
123
|
+
guard let scheme = scheme?.lowercased(), let host = host, !host.isEmpty else {
|
|
124
|
+
return nil
|
|
125
|
+
}
|
|
126
|
+
if let port = port, !(scheme == "https" && port == 443) {
|
|
127
|
+
return "\(scheme)://\(host):\(port)"
|
|
128
|
+
}
|
|
129
|
+
return "\(scheme)://\(host)"
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/// The normalized origin of a URL.
|
|
133
|
+
func originString(of url: URL) -> String? {
|
|
134
|
+
normalizedOrigin(scheme: url.scheme, host: url.host, port: url.port)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/// Navigation is allowed only to https on the pinned origin — blocking any
|
|
138
|
+
/// main-frame navigation that would re-expose the bridge to another origin.
|
|
139
|
+
func isNavigationAllowed(to url: URL?, expectedOrigin: String) -> Bool {
|
|
140
|
+
guard let url = url, url.scheme?.lowercased() == "https" else { return false }
|
|
141
|
+
return originString(of: url) == expectedOrigin
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/// A bridge message is trusted only from the pinned origin's main frame.
|
|
145
|
+
func isMessageTrusted(
|
|
146
|
+
isMainFrame: Bool,
|
|
147
|
+
messageOrigin: String?,
|
|
148
|
+
expectedOrigin: String
|
|
149
|
+
) -> Bool {
|
|
150
|
+
isMainFrame && messageOrigin == expectedOrigin
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// MARK: - Delegate-decision seams (pure, unit-testable)
|
|
154
|
+
|
|
155
|
+
// The WebKit callback argument types (WKNavigationAction / WKNavigationResponse
|
|
156
|
+
// / WKScriptMessage) have no public initializers, so they cannot be constructed
|
|
157
|
+
// in a unit test. These protocols expose only the fields the trust decisions
|
|
158
|
+
// read, letting the decision logic be tested with plain fakes while the real WK
|
|
159
|
+
// types conform via the extensions below. The protocols are deliberately NOT
|
|
160
|
+
// @objc so they never leak into the generated -Swift.h that sibling .mm files
|
|
161
|
+
// import.
|
|
162
|
+
|
|
163
|
+
/// The fields the navigation gate reads off a `WKNavigationAction`.
|
|
164
|
+
protocol NavigationActionLike {
|
|
165
|
+
var requestURL: URL? { get }
|
|
166
|
+
/// `nil` when the action has no target frame (e.g. a new-window request);
|
|
167
|
+
/// `true`/`false` for a main-frame vs sub-frame target.
|
|
168
|
+
var isTargetMainFrame: Bool? { get }
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/// The fields the HTTP-error gate reads off a `WKNavigationResponse`.
|
|
172
|
+
protocol NavigationResponseLike {
|
|
173
|
+
var isForMainFrame: Bool { get }
|
|
174
|
+
var httpStatusCode: Int? { get }
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/// The fields the bridge origin check reads off a `WKScriptMessage`.
|
|
178
|
+
protocol ScriptMessageLike {
|
|
179
|
+
var messageName: String { get }
|
|
180
|
+
var isMainFrame: Bool { get }
|
|
181
|
+
var sourceOrigin: String? { get }
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
extension WKNavigationAction: NavigationActionLike {
|
|
185
|
+
var requestURL: URL? { request.url }
|
|
186
|
+
var isTargetMainFrame: Bool? { targetFrame?.isMainFrame }
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
extension WKNavigationResponse: NavigationResponseLike {
|
|
190
|
+
// `isForMainFrame` is already a stored property on WKNavigationResponse and
|
|
191
|
+
// satisfies the protocol requirement directly.
|
|
192
|
+
var httpStatusCode: Int? { (response as? HTTPURLResponse)?.statusCode }
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
extension WKScriptMessage: ScriptMessageLike {
|
|
196
|
+
var messageName: String { name }
|
|
197
|
+
var isMainFrame: Bool { frameInfo.isMainFrame }
|
|
198
|
+
var sourceOrigin: String? {
|
|
199
|
+
let origin = frameInfo.securityOrigin
|
|
200
|
+
return normalizedOrigin(
|
|
201
|
+
scheme: origin.`protocol`,
|
|
202
|
+
host: origin.host,
|
|
203
|
+
// WKSecurityOrigin reports 0 for the scheme-default port.
|
|
204
|
+
port: origin.port == 0 ? nil : origin.port
|
|
205
|
+
)
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/// Navigation-action policy: sub-frame navigations are always allowed (the WaaS
|
|
210
|
+
/// page has none); a main-frame navigation is allowed only to https on the
|
|
211
|
+
/// pinned origin so the bridge can never be re-injected into another origin.
|
|
212
|
+
func navigationActionPolicy(
|
|
213
|
+
for action: NavigationActionLike,
|
|
214
|
+
expectedOrigin: String
|
|
215
|
+
) -> WKNavigationActionPolicy {
|
|
216
|
+
if action.isTargetMainFrame == false { return .allow }
|
|
217
|
+
return isNavigationAllowed(to: action.requestURL, expectedOrigin: expectedOrigin)
|
|
218
|
+
? .allow
|
|
219
|
+
: .cancel
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/// HTTP-error gate: returns the status code to fail the load with when a
|
|
223
|
+
/// main-frame response is >= 400, or `nil` when the response should be allowed.
|
|
224
|
+
/// Sub-frame responses never fail the load.
|
|
225
|
+
func navigationResponseHttpError(for response: NavigationResponseLike) -> Int? {
|
|
226
|
+
guard response.isForMainFrame,
|
|
227
|
+
let statusCode = response.httpStatusCode,
|
|
228
|
+
statusCode >= 400 else {
|
|
229
|
+
return nil
|
|
230
|
+
}
|
|
231
|
+
return statusCode
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/// Whether a script message should be delivered to JS: it must target our
|
|
235
|
+
/// bridge handler and pass the main-frame + pinned-origin trust check.
|
|
236
|
+
func shouldDeliverScriptMessage(
|
|
237
|
+
_ message: ScriptMessageLike,
|
|
238
|
+
expectedOrigin: String
|
|
239
|
+
) -> Bool {
|
|
240
|
+
guard message.messageName == bridgeHandlerName else { return false }
|
|
241
|
+
return isMessageTrusted(
|
|
242
|
+
isMainFrame: message.isMainFrame,
|
|
243
|
+
messageOrigin: message.sourceOrigin,
|
|
244
|
+
expectedOrigin: expectedOrigin
|
|
245
|
+
)
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// MARK: - Pending-load registry
|
|
249
|
+
|
|
250
|
+
/// Tracks the in-flight `open` completion per instance and guarantees each load
|
|
251
|
+
/// settles exactly once. Extracted from `DynamicWebViewImpl` so the
|
|
252
|
+
/// settle-once state machine (didCommit success; didFail / http_error / cancel /
|
|
253
|
+
/// content-process crash failures; and double-settle no-ops) is unit-testable
|
|
254
|
+
/// without a live WKWebView.
|
|
255
|
+
final class PendingLoadRegistry {
|
|
256
|
+
private var completions: [String: (Error?) -> Void] = [:]
|
|
257
|
+
|
|
258
|
+
/// Registers the completion to invoke when `instanceId`'s load settles.
|
|
259
|
+
func register(instanceId: String, completion: @escaping (Error?) -> Void) {
|
|
260
|
+
completions[instanceId] = completion
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/// Whether a completion is still pending for `instanceId`.
|
|
264
|
+
func isPending(instanceId: String) -> Bool {
|
|
265
|
+
completions[instanceId] != nil
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/// Settles the pending completion (removing it first) with `error` — `nil`
|
|
269
|
+
/// means success. Returns `true` if a completion was pending; `false` when it
|
|
270
|
+
/// was already settled, in which case this is a no-op.
|
|
271
|
+
@discardableResult
|
|
272
|
+
func settle(instanceId: String, error: Error?) -> Bool {
|
|
273
|
+
guard let completion = completions.removeValue(forKey: instanceId) else {
|
|
274
|
+
return false
|
|
275
|
+
}
|
|
276
|
+
completion(error)
|
|
277
|
+
return true
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// MARK: - Per-instance handler shims
|
|
282
|
+
|
|
283
|
+
/// Per-instance message-handler shim. Captures its own `instanceId` and pinned
|
|
284
|
+
/// `expectedOrigin` so message routing and origin trust are enforced
|
|
285
|
+
/// structurally, not by reverse-looking-up the sending WKWebView. Kept as a
|
|
286
|
+
/// separate `NSObject` subclass so `DynamicWebViewImpl` does NOT declare
|
|
287
|
+
/// `WKScriptMessageHandler` conformance (which would leak into the generated
|
|
288
|
+
/// `-Swift.h` and break sibling `.mm` files that don't import WebKit).
|
|
289
|
+
private class MessageBridge: NSObject, WKScriptMessageHandler {
|
|
290
|
+
private let instanceId: String
|
|
291
|
+
private let expectedOrigin: String
|
|
292
|
+
weak var owner: DynamicWebViewImpl?
|
|
293
|
+
|
|
294
|
+
init(instanceId: String, expectedOrigin: String, owner: DynamicWebViewImpl) {
|
|
295
|
+
self.instanceId = instanceId
|
|
296
|
+
self.expectedOrigin = expectedOrigin
|
|
297
|
+
self.owner = owner
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
func userContentController(
|
|
301
|
+
_ userContentController: WKUserContentController,
|
|
302
|
+
didReceive message: WKScriptMessage
|
|
303
|
+
) {
|
|
304
|
+
// Pin the bridge to the opened origin's main frame: a redirect or a
|
|
305
|
+
// compromised/embedded frame cannot forward attacker messages as trusted.
|
|
306
|
+
// The decision lives in `shouldDeliverScriptMessage` so it is unit-testable
|
|
307
|
+
// without a live WKWebView (WKScriptMessage has no public initializer).
|
|
308
|
+
guard shouldDeliverScriptMessage(message, expectedOrigin: expectedOrigin) else {
|
|
309
|
+
return
|
|
310
|
+
}
|
|
311
|
+
owner?.deliverMessage(instanceId: instanceId, body: message.body)
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/// Per-instance navigation-delegate shim. Same NSObject-containment pattern as
|
|
316
|
+
/// `MessageBridge`. Captures `instanceId` + pinned `expectedOrigin` to gate
|
|
317
|
+
/// navigation and route lifecycle events without a WebView reverse-lookup.
|
|
318
|
+
private class NavigationBridge: NSObject, WKNavigationDelegate {
|
|
319
|
+
private let instanceId: String
|
|
320
|
+
private let expectedOrigin: String
|
|
321
|
+
weak var owner: DynamicWebViewImpl?
|
|
322
|
+
|
|
323
|
+
init(instanceId: String, expectedOrigin: String, owner: DynamicWebViewImpl) {
|
|
324
|
+
self.instanceId = instanceId
|
|
325
|
+
self.expectedOrigin = expectedOrigin
|
|
326
|
+
self.owner = owner
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Fail closed against off-origin / non-https navigation. The initial load is
|
|
330
|
+
// to the pinned origin so it is allowed; any later main-frame navigation to a
|
|
331
|
+
// different origin (open redirect, injected link, scripted location change) is
|
|
332
|
+
// cancelled so the bridge can never be re-injected into an attacker origin.
|
|
333
|
+
// Subframe navigations are left to WebKit (the WaaS page has no subframes).
|
|
334
|
+
func webView(
|
|
335
|
+
_ webView: WKWebView,
|
|
336
|
+
decidePolicyFor navigationAction: WKNavigationAction,
|
|
337
|
+
decisionHandler: @escaping (WKNavigationActionPolicy) -> Void
|
|
338
|
+
) {
|
|
339
|
+
decisionHandler(navigationActionPolicy(
|
|
340
|
+
for: navigationAction,
|
|
341
|
+
expectedOrigin: expectedOrigin
|
|
342
|
+
))
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// Intercept the HTTP response before commit: for a main-frame non-2xx/3xx,
|
|
346
|
+
// cancel and record the status so the owner rejects the pending `open` with a
|
|
347
|
+
// typed `http_error` instead of rendering the error body as a success.
|
|
348
|
+
// Gated on `isForMainFrame` so a sub-resource 4xx/5xx cannot fail the load.
|
|
349
|
+
func webView(
|
|
350
|
+
_ webView: WKWebView,
|
|
351
|
+
decidePolicyFor navigationResponse: WKNavigationResponse,
|
|
352
|
+
decisionHandler: @escaping (WKNavigationResponsePolicy) -> Void
|
|
353
|
+
) {
|
|
354
|
+
if let statusCode = navigationResponseHttpError(for: navigationResponse) {
|
|
355
|
+
owner?.handleHttpError(instanceId: instanceId, statusCode: statusCode)
|
|
356
|
+
decisionHandler(.cancel)
|
|
357
|
+
return
|
|
358
|
+
}
|
|
359
|
+
decisionHandler(.allow)
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
func webView(_ webView: WKWebView, didCommit navigation: WKNavigation!) {
|
|
363
|
+
owner?.handleDidCommit(instanceId: instanceId)
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
func webView(
|
|
367
|
+
_ webView: WKWebView,
|
|
368
|
+
didFailProvisionalNavigation navigation: WKNavigation!,
|
|
369
|
+
withError error: Error
|
|
370
|
+
) {
|
|
371
|
+
owner?.handleDidFail(instanceId: instanceId, error: error)
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
func webView(
|
|
375
|
+
_ webView: WKWebView,
|
|
376
|
+
didFail navigation: WKNavigation!,
|
|
377
|
+
withError error: Error
|
|
378
|
+
) {
|
|
379
|
+
owner?.handleDidFail(instanceId: instanceId, error: error)
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
func webViewWebContentProcessDidTerminate(_ webView: WKWebView) {
|
|
383
|
+
owner?.handleContentProcessTerminated(instanceId: instanceId)
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/// Swift implementation of the `DynamicWebView` TurboModule.
|
|
388
|
+
///
|
|
389
|
+
/// Owns one `WKWebView` per JS `SandboxHost` instance. Each WebView
|
|
390
|
+
/// is attached (hidden, zero-frame, non-interactive) to the foreground-
|
|
391
|
+
/// active window so WKWebView's WebContent process is not suspended by
|
|
392
|
+
/// iOS during long-running MPC ceremonies.
|
|
393
|
+
@objc public class DynamicWebViewImpl: NSObject {
|
|
394
|
+
|
|
395
|
+
@objc public weak var delegate: DynamicWebViewImplDelegate?
|
|
396
|
+
|
|
397
|
+
private var webViews: [String: WKWebView] = [:]
|
|
398
|
+
// Pending completion for `open` — resolved on first terminal navigation
|
|
399
|
+
// event (didCommit / didFail* / HTTP >= 400 / content process crash) or
|
|
400
|
+
// explicit cancel (close called before settle). `internal` (not `private`)
|
|
401
|
+
// so the settle-once state machine can be driven directly in unit tests.
|
|
402
|
+
let pending = PendingLoadRegistry()
|
|
403
|
+
// The origin each instance is pinned to (navigation + message trust).
|
|
404
|
+
private var expectedOrigins: [String: String] = [:]
|
|
405
|
+
// Per-instance handlers. The navigation delegate is held weakly by WKWebView,
|
|
406
|
+
// so it must be retained here; the message handler is retained by the
|
|
407
|
+
// userContentController but is stored for symmetric teardown.
|
|
408
|
+
private var messageBridges: [String: MessageBridge] = [:]
|
|
409
|
+
private var navigationBridges: [String: NavigationBridge] = [:]
|
|
410
|
+
|
|
411
|
+
@objc public override init() {
|
|
412
|
+
super.init()
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
fileprivate func deliverMessage(instanceId: String, body: Any) {
|
|
416
|
+
delegate?.didReceiveMessage(forInstanceId: instanceId, data: body)
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// `internal` (not `fileprivate`) so the reason-mapping wiring can be driven
|
|
420
|
+
// directly in unit tests after registering a pending completion.
|
|
421
|
+
func handleDidCommit(instanceId: String) {
|
|
422
|
+
pending.settle(instanceId: instanceId, error: nil)
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
func handleDidFail(instanceId: String, error: Error) {
|
|
426
|
+
// `settle` is a no-op when an HTTP-error policy cancel already consumed the
|
|
427
|
+
// completion, so this follow-up failure event cannot double-resolve.
|
|
428
|
+
let nsError = error as NSError
|
|
429
|
+
pending.settle(instanceId: instanceId, error: makeError(
|
|
430
|
+
reason: Reason.navigationFailed,
|
|
431
|
+
message: nsError.localizedDescription,
|
|
432
|
+
underlyingDescription: nsError.localizedDescription
|
|
433
|
+
))
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
func handleHttpError(instanceId: String, statusCode: Int) {
|
|
437
|
+
pending.settle(instanceId: instanceId, error: makeError(
|
|
438
|
+
reason: Reason.httpError,
|
|
439
|
+
message: "HTTP \(statusCode)",
|
|
440
|
+
statusCode: statusCode
|
|
441
|
+
))
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
func handleContentProcessTerminated(instanceId: String) {
|
|
445
|
+
pending.settle(instanceId: instanceId, error: makeError(
|
|
446
|
+
reason: Reason.contentProcessTerminated,
|
|
447
|
+
message: "WKWebView WebContent process terminated."
|
|
448
|
+
))
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
@objc public func open(
|
|
452
|
+
instanceId: String,
|
|
453
|
+
url: String,
|
|
454
|
+
completion: @escaping (Error?) -> Void
|
|
455
|
+
) {
|
|
456
|
+
DispatchQueue.main.async { [weak self] in
|
|
457
|
+
guard let self = self else { return }
|
|
458
|
+
|
|
459
|
+
if self.webViews[instanceId] != nil {
|
|
460
|
+
completion(makeError(
|
|
461
|
+
reason: Reason.instanceBusy,
|
|
462
|
+
message: "open called on an instanceId that already has an active WebView."
|
|
463
|
+
))
|
|
464
|
+
return
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// Reject non-https schemes (file://, about:, javascript:, http://, etc.).
|
|
468
|
+
// The WebView hosts WaaS/MPC content and must never load attacker-controlled
|
|
469
|
+
// local files or pseudo-URL handlers. A missing host is also rejected so a
|
|
470
|
+
// bare scheme like "https://" cannot slip through.
|
|
471
|
+
guard let parsedUrl = URL(string: url),
|
|
472
|
+
parsedUrl.scheme?.lowercased() == "https",
|
|
473
|
+
let host = parsedUrl.host, !host.isEmpty,
|
|
474
|
+
let expectedOrigin = originString(of: parsedUrl) else {
|
|
475
|
+
completion(makeError(
|
|
476
|
+
reason: Reason.invalidUrl,
|
|
477
|
+
message: "Invalid URL: \(url)"
|
|
478
|
+
))
|
|
479
|
+
return
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
guard let hostView = self.findHostView() else {
|
|
483
|
+
completion(makeError(
|
|
484
|
+
reason: Reason.noHostView,
|
|
485
|
+
message: "No foreground window available to attach WebView."
|
|
486
|
+
))
|
|
487
|
+
return
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
let config = WKWebViewConfiguration()
|
|
491
|
+
// Ephemeral, in-memory store: cookies/localStorage/IndexedDB written by
|
|
492
|
+
// the hosted page are scoped to this WKWebView and discarded on close.
|
|
493
|
+
// Prevents cross-WebView leakage of WaaS/MPC session state and survives
|
|
494
|
+
// neither app relaunch nor reinstall.
|
|
495
|
+
config.websiteDataStore = .nonPersistent()
|
|
496
|
+
// Dedicated process pool per instance isolates the WebContent process
|
|
497
|
+
// from any other WKWebView in the host app, blocking shared-process
|
|
498
|
+
// side channels (e.g. shared JIT heap, IPC ordering).
|
|
499
|
+
config.processPool = WKProcessPool()
|
|
500
|
+
let userScript = WKUserScript(
|
|
501
|
+
source: bridgeUserScript,
|
|
502
|
+
injectionTime: .atDocumentStart,
|
|
503
|
+
forMainFrameOnly: true
|
|
504
|
+
)
|
|
505
|
+
config.userContentController.addUserScript(userScript)
|
|
506
|
+
|
|
507
|
+
let messageBridge = MessageBridge(
|
|
508
|
+
instanceId: instanceId,
|
|
509
|
+
expectedOrigin: expectedOrigin,
|
|
510
|
+
owner: self
|
|
511
|
+
)
|
|
512
|
+
let navigationBridge = NavigationBridge(
|
|
513
|
+
instanceId: instanceId,
|
|
514
|
+
expectedOrigin: expectedOrigin,
|
|
515
|
+
owner: self
|
|
516
|
+
)
|
|
517
|
+
config.userContentController.add(messageBridge, name: bridgeHandlerName)
|
|
518
|
+
|
|
519
|
+
let webView = WKWebView(frame: .zero, configuration: config)
|
|
520
|
+
webView.navigationDelegate = navigationBridge
|
|
521
|
+
webView.isHidden = true
|
|
522
|
+
webView.isUserInteractionEnabled = false
|
|
523
|
+
// Enables Safari Web Inspector for debug builds (iOS 16.4+).
|
|
524
|
+
// Connect via Safari > Develop > [Device] > [WebView].
|
|
525
|
+
#if DEBUG
|
|
526
|
+
if #available(iOS 16.4, *) {
|
|
527
|
+
webView.isInspectable = true
|
|
528
|
+
}
|
|
529
|
+
#endif
|
|
530
|
+
|
|
531
|
+
hostView.addSubview(webView)
|
|
532
|
+
self.webViews[instanceId] = webView
|
|
533
|
+
self.expectedOrigins[instanceId] = expectedOrigin
|
|
534
|
+
self.messageBridges[instanceId] = messageBridge
|
|
535
|
+
self.navigationBridges[instanceId] = navigationBridge
|
|
536
|
+
self.pending.register(instanceId: instanceId, completion: completion)
|
|
537
|
+
|
|
538
|
+
webView.load(URLRequest(url: parsedUrl))
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
@objc public func sendMessage(instanceId: String, data: NSDictionary) {
|
|
543
|
+
DispatchQueue.main.async { [weak self] in
|
|
544
|
+
guard let self = self else { return }
|
|
545
|
+
guard let webView = self.webViews[instanceId] else { return }
|
|
546
|
+
|
|
547
|
+
// callAsyncJavaScript binds `payload` via WebKit's structured-clone
|
|
548
|
+
// argument map instead of interpolating the value into JS source.
|
|
549
|
+
// Eliminates the entire class of injection bugs that hand-rolled
|
|
550
|
+
// escaping (U+2028 / U+2029 line terminators, quote breakouts, etc.)
|
|
551
|
+
// is exposed to. The iframe contract is fire-and-forget, so the
|
|
552
|
+
// completion is intentionally dropped.
|
|
553
|
+
webView.callAsyncJavaScript(
|
|
554
|
+
"if (window.__dynamicReceive) { window.__dynamicReceive(payload); }",
|
|
555
|
+
arguments: ["payload": data],
|
|
556
|
+
in: nil,
|
|
557
|
+
in: .page,
|
|
558
|
+
completionHandler: nil
|
|
559
|
+
)
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
@objc public func close(instanceId: String) {
|
|
564
|
+
DispatchQueue.main.async { [weak self] in
|
|
565
|
+
guard let self = self else { return }
|
|
566
|
+
|
|
567
|
+
// A close while `open` is pending means the caller intentionally aborted
|
|
568
|
+
// the load. Reject the pending completion so the awaiter doesn't hang.
|
|
569
|
+
// `settle` is a no-op if the load already settled.
|
|
570
|
+
self.pending.settle(instanceId: instanceId, error: makeError(
|
|
571
|
+
reason: Reason.cancelled,
|
|
572
|
+
message: "WebView was closed before the load settled."
|
|
573
|
+
))
|
|
574
|
+
|
|
575
|
+
self.expectedOrigins.removeValue(forKey: instanceId)
|
|
576
|
+
self.messageBridges.removeValue(forKey: instanceId)
|
|
577
|
+
self.navigationBridges.removeValue(forKey: instanceId)
|
|
578
|
+
|
|
579
|
+
guard let webView = self.webViews.removeValue(forKey: instanceId) else { return }
|
|
580
|
+
|
|
581
|
+
webView.stopLoading()
|
|
582
|
+
webView.navigationDelegate = nil
|
|
583
|
+
webView.configuration.userContentController.removeScriptMessageHandler(forName: bridgeHandlerName)
|
|
584
|
+
webView.configuration.userContentController.removeAllUserScripts()
|
|
585
|
+
webView.removeFromSuperview()
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
// MARK: - Helpers
|
|
590
|
+
|
|
591
|
+
private func findHostView() -> UIView? {
|
|
592
|
+
let scenes = UIApplication.shared.connectedScenes
|
|
593
|
+
.compactMap { $0 as? UIWindowScene }
|
|
594
|
+
|
|
595
|
+
let activeScene = scenes.first(where: { $0.activationState == .foregroundActive })
|
|
596
|
+
?? scenes.first
|
|
597
|
+
|
|
598
|
+
let window = activeScene?.windows.first(where: { $0.isKeyWindow })
|
|
599
|
+
?? activeScene?.windows.first
|
|
600
|
+
|
|
601
|
+
return window?.rootViewController?.view ?? window
|
|
602
|
+
}
|
|
603
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import Security
|
|
3
|
+
import CryptoKit
|
|
4
|
+
|
|
5
|
+
/// Production implementation of `SecKeyAPI` — each method is a thin
|
|
6
|
+
/// passthrough to the real Security/CryptoKit C call.
|
|
7
|
+
///
|
|
8
|
+
/// This file is excluded from coverage measurement
|
|
9
|
+
/// (`sonar.coverage.exclusions`) because every method is a one-line
|
|
10
|
+
/// passthrough that cannot be exercised on the iOS Simulator. The
|
|
11
|
+
/// behaviour these calls perform is verified manually on a real
|
|
12
|
+
/// device before each release.
|
|
13
|
+
final class LiveSecKeyAPI: SecKeyAPI {
|
|
14
|
+
func isSecureEnclaveAvailable() -> Bool {
|
|
15
|
+
if #available(iOS 13.0, *) {
|
|
16
|
+
return SecureEnclave.isAvailable
|
|
17
|
+
}
|
|
18
|
+
return false
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
func itemCopyMatching(query: CFDictionary) -> (status: OSStatus, item: CFTypeRef?) {
|
|
22
|
+
var item: CFTypeRef?
|
|
23
|
+
let status = SecItemCopyMatching(query, &item)
|
|
24
|
+
return (status, item)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
func itemDelete(query: CFDictionary) -> OSStatus {
|
|
28
|
+
return SecItemDelete(query)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
func createAccessControl(
|
|
32
|
+
protection: CFTypeRef,
|
|
33
|
+
flags: SecAccessControlCreateFlags
|
|
34
|
+
) -> SecAccessControl? {
|
|
35
|
+
return SecAccessControlCreateWithFlags(kCFAllocatorDefault, protection, flags, nil)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
func createRandomKey(attributes: CFDictionary) throws -> SecKey {
|
|
39
|
+
var error: Unmanaged<CFError>?
|
|
40
|
+
guard let key = SecKeyCreateRandomKey(attributes, &error) else {
|
|
41
|
+
throw LiveSecKeyAPI.error(from: error)
|
|
42
|
+
}
|
|
43
|
+
return key
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
func copyPublicKey(_ key: SecKey) -> SecKey? {
|
|
47
|
+
return SecKeyCopyPublicKey(key)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
func createSignature(
|
|
51
|
+
key: SecKey,
|
|
52
|
+
algorithm: SecKeyAlgorithm,
|
|
53
|
+
data: CFData
|
|
54
|
+
) throws -> Data {
|
|
55
|
+
var error: Unmanaged<CFError>?
|
|
56
|
+
guard let signature = SecKeyCreateSignature(key, algorithm, data, &error) else {
|
|
57
|
+
throw LiveSecKeyAPI.error(from: error)
|
|
58
|
+
}
|
|
59
|
+
return signature as Data
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
func copyExternalRepresentation(_ key: SecKey) throws -> Data {
|
|
63
|
+
var error: Unmanaged<CFError>?
|
|
64
|
+
guard let data = SecKeyCopyExternalRepresentation(key, &error) else {
|
|
65
|
+
throw LiveSecKeyAPI.error(from: error)
|
|
66
|
+
}
|
|
67
|
+
return data as Data
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Mirrors the string-interpolation form the original code used so
|
|
71
|
+
// that the NSError's localizedDescription remains byte-identical to
|
|
72
|
+
// pre-refactor output.
|
|
73
|
+
private static func error(from cfError: Unmanaged<CFError>?) -> NSError {
|
|
74
|
+
let description: String
|
|
75
|
+
if let cfError = cfError {
|
|
76
|
+
description = "\(cfError.takeRetainedValue())"
|
|
77
|
+
} else {
|
|
78
|
+
description = "Unknown"
|
|
79
|
+
}
|
|
80
|
+
return NSError(
|
|
81
|
+
domain: "SecKeyAPI",
|
|
82
|
+
code: 0,
|
|
83
|
+
userInfo: [NSLocalizedDescriptionKey: description]
|
|
84
|
+
)
|
|
85
|
+
}
|
|
86
|
+
}
|