@umutcansu/react-native-pinvault 0.0.0-stage → 2.3.2
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/CHANGELOG.md +55 -0
- package/LICENSE +21 -0
- package/README.md +386 -2
- package/RNPinVault.podspec +43 -0
- package/android/build.gradle +68 -0
- package/android/consumer-rules.pro +25 -0
- package/android/src/main/AndroidManifest.xml +16 -0
- package/android/src/main/java/io/github/umutcansu/pinvault/reactnative/ConfigParser.kt +300 -0
- package/android/src/main/java/io/github/umutcansu/pinvault/reactnative/JsEnvironmentGuard.kt +53 -0
- package/android/src/main/java/io/github/umutcansu/pinvault/reactnative/NativeSecurity.kt +239 -0
- package/android/src/main/java/io/github/umutcansu/pinvault/reactnative/PinVaultModule.kt +410 -0
- package/android/src/main/java/io/github/umutcansu/pinvault/reactnative/PinVaultNetworking.kt +296 -0
- package/android/src/main/java/io/github/umutcansu/pinvault/reactnative/PinVaultPackage.kt +25 -0
- package/android/src/main/java/io/github/umutcansu/pinvault/reactnative/PinnedFetch.kt +131 -0
- package/android/src/main/java/io/github/umutcansu/pinvault/reactnative/ResultMapper.kt +188 -0
- package/android/src/main/java/io/github/umutcansu/pinvault/reactnative/StrictJson.kt +225 -0
- package/android/src/main/java/io/github/umutcansu/pinvault/reactnative/VaultTokenStore.kt +63 -0
- package/ios/Core/ConfigParser.swift +276 -0
- package/ios/Core/JSEnvironmentGuard.swift +129 -0
- package/ios/Core/NativeSecurity.swift +197 -0
- package/ios/Core/PinnedFetch.swift +119 -0
- package/ios/Core/ReactNetworking.swift +172 -0
- package/ios/Core/ResultMapper.swift +163 -0
- package/ios/Core/StrictJSON.swift +214 -0
- package/ios/Core/VaultTokenStore.swift +66 -0
- package/ios/PinVaultBridge.swift +412 -0
- package/ios/PinVaultReactNetworking.swift +47 -0
- package/ios/RNPinVault.h +7 -0
- package/ios/RNPinVault.mm +293 -0
- package/ios/RNPinVaultURLRequestHandler.mm +92 -0
- package/lib/module/NativePinVault.js +13 -0
- package/lib/module/NativePinVault.js.map +1 -0
- package/lib/module/index.js +312 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/types.js +2 -0
- package/lib/module/types.js.map +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/NativePinVault.d.ts +50 -0
- package/lib/typescript/src/NativePinVault.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +150 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/lib/typescript/src/types.d.ts +346 -0
- package/lib/typescript/src/types.d.ts.map +1 -0
- package/package.json +128 -4
- package/src/NativePinVault.ts +72 -0
- package/src/index.ts +385 -0
- package/src/types.ts +351 -0
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
// The Swift side of the TurboModule (RNPinVault.mm forwards every method here).
|
|
2
|
+
// It calls PinVault.shared and nothing else: no pinning, key handling or vault
|
|
3
|
+
// decryption happens in this file. Inputs are parsed by Core/ConfigParser and
|
|
4
|
+
// Core/PinnedFetch, results mapped by Core/ResultMapper.
|
|
5
|
+
import Foundation
|
|
6
|
+
import PinVault
|
|
7
|
+
#if canImport(UIKit)
|
|
8
|
+
import UIKit
|
|
9
|
+
#endif
|
|
10
|
+
|
|
11
|
+
@objc(RNPinVaultBridge)
|
|
12
|
+
public final class PinVaultBridge: NSObject {
|
|
13
|
+
public typealias Resolve = (Any?) -> Void
|
|
14
|
+
public typealias Reject = (String, String, [String: Any]?) -> Void
|
|
15
|
+
|
|
16
|
+
// Process-wide, like the library: a JS reload makes a new module, not a new PinVault.
|
|
17
|
+
private static let tokens = VaultTokenStore()
|
|
18
|
+
private static let lock = NSLock()
|
|
19
|
+
private static var startedInProcess = false
|
|
20
|
+
private static var jsGuard: JSEnvironmentGuard?
|
|
21
|
+
private static weak var current: PinVaultBridge?
|
|
22
|
+
private static let startQueue = StartQueue()
|
|
23
|
+
private static var rnMaxResponseBytes = ReactNetworking.defaultMaxResponseBytes
|
|
24
|
+
|
|
25
|
+
private let mapper = ResultMapper(tokens: PinVaultBridge.tokens)
|
|
26
|
+
|
|
27
|
+
@objc public var emitConnectionEvent: (([String: Any]) -> Void)?
|
|
28
|
+
@objc public var emitGuardRequest: (([String: Any]) -> Void)?
|
|
29
|
+
/// RNPinVault.mm: whether RCTNetworking hands an https request to
|
|
30
|
+
/// RNPinVaultURLRequestHandler (true), to another handler (false), or nil = unknown.
|
|
31
|
+
@objc public var reactNetworkingIsPinned: (() -> NSNumber?)?
|
|
32
|
+
|
|
33
|
+
/// `PinVault.shared.registerBackgroundTask()` for the app's AppDelegate (before launch ends):
|
|
34
|
+
/// the app target does not link the PinVault package itself.
|
|
35
|
+
@objc public static func registerBackgroundTask() -> Bool {
|
|
36
|
+
PinVault.shared.registerBackgroundTask()
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
@objc public override init() {
|
|
40
|
+
super.init()
|
|
41
|
+
Self.lock.lock()
|
|
42
|
+
Self.current = self
|
|
43
|
+
Self.lock.unlock()
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
private static func locked<T>(_ body: () -> T) -> T {
|
|
47
|
+
lock.lock(); defer { lock.unlock() }
|
|
48
|
+
return body()
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
private static var started: Bool {
|
|
52
|
+
lock.lock(); defer { lock.unlock() }
|
|
53
|
+
return startedInProcess
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/// For the React Native request handler: `start` has run (a failed start
|
|
57
|
+
/// leaves the library's sessions refusing every request).
|
|
58
|
+
static var isStarted: Bool { started }
|
|
59
|
+
|
|
60
|
+
/// `ios.reactNativeMaxResponseBytes` of the last start.
|
|
61
|
+
static var reactNativeMaxResponseBytes: Int64 { locked { rnMaxResponseBytes } }
|
|
62
|
+
|
|
63
|
+
/// An error → (exception name, message) for JS, tokens redacted.
|
|
64
|
+
static let describe: @Sendable (any Error) -> (String, String) = { error in
|
|
65
|
+
let mapped = ResultMapper(tokens: tokens).exception(error) as? [String: Any] ?? [:]
|
|
66
|
+
return ((mapped["name"] as? String) ?? "Error", (mapped["message"] as? String) ?? "")
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/// Runs a library call with the JS guard's verdicts on `operations` asked
|
|
70
|
+
/// beforehand, so the library's synchronous guard check holds no thread.
|
|
71
|
+
private static func guarded<T>(_ operations: [GuardedOperation], _ body: () async throws -> T) async rethrows -> T {
|
|
72
|
+
guard let g = locked({ jsGuard }) else { return try await body() }
|
|
73
|
+
return try await g.withVerdicts(operations, body)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// MARK: helpers
|
|
77
|
+
|
|
78
|
+
private func run(_ resolve: @escaping Resolve, _ reject: @escaping Reject, _ body: @escaping () async throws -> Any?) {
|
|
79
|
+
Task.detached {
|
|
80
|
+
do {
|
|
81
|
+
resolve(try await body() ?? NSNull())
|
|
82
|
+
} catch let e as BridgeInputError {
|
|
83
|
+
reject("E_INVALID_ARGUMENT", Self.tokens.redact(e.message) ?? "", nil)
|
|
84
|
+
} catch {
|
|
85
|
+
reject("E_NATIVE", Self.tokens.redact("\(error)") ?? "", self.exceptionInfo(error))
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
private func exceptionInfo(_ error: any Error) -> [String: Any] {
|
|
91
|
+
let mapped = mapper.exception(error) as? [String: Any] ?? [:]
|
|
92
|
+
return ["exceptionName": mapped["name"] ?? "Error", "exceptionMessage": mapped["message"] ?? NSNull()]
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
private static func checkKey(_ key: String) throws {
|
|
96
|
+
if key.isEmpty || key.count > 128 { throw BridgeInputError("key: must be 1 to 128 characters") }
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
private static func checkToken(_ token: String?) throws {
|
|
100
|
+
if let token, token.count > 4096 { throw BridgeInputError("token: longer than 4096 characters") }
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// MARK: start / config
|
|
104
|
+
|
|
105
|
+
@objc public func start(_ json: String, resolve: @escaping Resolve, reject: @escaping Reject) {
|
|
106
|
+
Task.detached {
|
|
107
|
+
await Self.startQueue.run {
|
|
108
|
+
let parsed: ParsedConfig
|
|
109
|
+
var newGuard: JSEnvironmentGuard?
|
|
110
|
+
do {
|
|
111
|
+
#if DEBUG
|
|
112
|
+
let release = false
|
|
113
|
+
#else
|
|
114
|
+
let release = true
|
|
115
|
+
#endif
|
|
116
|
+
parsed = try ConfigParser.parse(
|
|
117
|
+
json, tokens: Self.tokens,
|
|
118
|
+
guardFactory: { timeout in
|
|
119
|
+
let g = JSEnvironmentGuard(timeoutMs: timeout) { requestId, operation in
|
|
120
|
+
let bridge = Self.locked { Self.current }
|
|
121
|
+
bridge?.emitGuardRequest?(["requestId": requestId, "operation": operation])
|
|
122
|
+
}
|
|
123
|
+
newGuard = g
|
|
124
|
+
return g
|
|
125
|
+
},
|
|
126
|
+
listener: { event in
|
|
127
|
+
let bridge = Self.locked { Self.current }
|
|
128
|
+
guard let bridge else { return }
|
|
129
|
+
bridge.emitConnectionEvent?(bridge.mapper.event(event))
|
|
130
|
+
},
|
|
131
|
+
native: try NativeSecurity.load(),
|
|
132
|
+
release: release
|
|
133
|
+
)
|
|
134
|
+
} catch let e as BridgeInputError {
|
|
135
|
+
// The guard of the running config stays: a refused config changes nothing.
|
|
136
|
+
reject("E_INVALID_CONFIG", Self.tokens.redact(e.message) ?? "", nil)
|
|
137
|
+
return
|
|
138
|
+
} catch {
|
|
139
|
+
let message = (error as? PinVaultError)?.message ?? "\(error)"
|
|
140
|
+
reject("E_INVALID_CONFIG", Self.tokens.redact(message) ?? "", nil)
|
|
141
|
+
return
|
|
142
|
+
}
|
|
143
|
+
let pinned = self.reactNetworkingIsPinned?()?.boolValue
|
|
144
|
+
if pinned != true {
|
|
145
|
+
let why = !PinVaultReactNetworking.enabled
|
|
146
|
+
? "Info.plist PinVaultPinReactNativeNetworking is NO"
|
|
147
|
+
: pinned == nil ? "RCTNetworking's handler could not be checked"
|
|
148
|
+
: "RCTNetworking hands https to another RCTURLRequestHandler"
|
|
149
|
+
NSLog("PinVault: React Native's own fetch / XMLHttpRequest / images may NOT be pinned (\(why)); README, \"Networking\"")
|
|
150
|
+
if parsed.requirePinnedReactNativeNetworking {
|
|
151
|
+
reject("E_NETWORKING_NOT_PINNED", "requirePinnedReactNativeNetworking: \(why)", nil)
|
|
152
|
+
return
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
Self.locked {
|
|
156
|
+
Self.jsGuard = newGuard
|
|
157
|
+
Self.rnMaxResponseBytes = parsed.reactNativeMaxResponseBytes
|
|
158
|
+
}
|
|
159
|
+
// A second start applies the new config: the library keeps the
|
|
160
|
+
// first one otherwise (the samples restart the same way).
|
|
161
|
+
if Self.started { PinVault.shared.reset() }
|
|
162
|
+
// INIT, and ENROLL for a pending enrollment the start picks up.
|
|
163
|
+
let result = await Self.guarded([.start, .enroll]) { await PinVault.shared.start(config: parsed.config) }
|
|
164
|
+
Self.locked { Self.startedInProcess = true }
|
|
165
|
+
resolve(self.mapper.initResult(result))
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
@objc public func updateNow(resolve: @escaping Resolve, reject: @escaping Reject) {
|
|
171
|
+
run(resolve, reject) { self.mapper.update(await PinVault.shared.updateNow()) }
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
@objc public func currentVersion(resolve: @escaping Resolve, reject: @escaping Reject) {
|
|
175
|
+
run(resolve, reject) { PinVault.shared.currentVersion() }
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
@objc public func hostPinVersions(resolve: @escaping Resolve, reject: @escaping Reject) {
|
|
179
|
+
run(resolve, reject) { PinVault.shared.hostPinVersions() }
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
@objc public func pinsForHost(_ hostname: String, resolve: @escaping Resolve, reject: @escaping Reject) {
|
|
183
|
+
run(resolve, reject) { ["pins": PinVault.shared.pinsForHost(hostname) ?? NSNull()] as [String: Any] }
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
@objc public func signingStatus(_ configApiId: String?, resolve: @escaping Resolve, reject: @escaping Reject) {
|
|
187
|
+
run(resolve, reject) { PinVault.shared.signingStatus(configApiId: configApiId).map(self.mapper.signing) }
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
@objc public func isForceUpdate(resolve: @escaping Resolve, reject: @escaping Reject) {
|
|
191
|
+
run(resolve, reject) { PinVault.shared.isForceUpdate() }
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
@objc public func reset(resolve: @escaping Resolve, reject: @escaping Reject) {
|
|
195
|
+
run(resolve, reject) { PinVault.shared.reset(); return nil }
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
@objc public func schedulePeriodicUpdates(_ intervalHours: NSNumber?, resolve: @escaping Resolve, reject: @escaping Reject) {
|
|
199
|
+
run(resolve, reject) {
|
|
200
|
+
guard Self.started else { return false }
|
|
201
|
+
if let h = intervalHours?.int64Value, h < 1 || h > 24 * 30 {
|
|
202
|
+
throw BridgeInputError("intervalHours: must be between 1 and 720")
|
|
203
|
+
}
|
|
204
|
+
return PinVault.shared.schedulePeriodicUpdates(intervalHours: intervalHours?.int64Value)
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
@objc public func cancelPeriodicUpdates(resolve: @escaping Resolve, reject: @escaping Reject) {
|
|
209
|
+
run(resolve, reject) { PinVault.shared.cancelPeriodicUpdates(); return nil }
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
@objc public func enableDebugLogging(resolve: @escaping Resolve, reject: @escaping Reject) {
|
|
213
|
+
run(resolve, reject) {
|
|
214
|
+
#if DEBUG
|
|
215
|
+
PinVault.enableDebugLogging()
|
|
216
|
+
return true
|
|
217
|
+
#else
|
|
218
|
+
return false
|
|
219
|
+
#endif
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// MARK: pinned HTTP
|
|
224
|
+
|
|
225
|
+
@objc public func fetch(_ json: String, resolve: @escaping Resolve, reject: @escaping Reject) {
|
|
226
|
+
Task.detached {
|
|
227
|
+
let request: FetchRequest
|
|
228
|
+
do {
|
|
229
|
+
request = try PinnedFetch.parse(json)
|
|
230
|
+
} catch let e as BridgeInputError {
|
|
231
|
+
reject("E_INVALID_ARGUMENT", Self.tokens.redact(e.message) ?? "", nil)
|
|
232
|
+
return
|
|
233
|
+
} catch {
|
|
234
|
+
reject("E_INVALID_ARGUMENT", Self.tokens.redact("\(error)") ?? "", nil)
|
|
235
|
+
return
|
|
236
|
+
}
|
|
237
|
+
guard Self.started else {
|
|
238
|
+
reject("E_NOT_STARTED", "PinVault has not started: call start() first", nil)
|
|
239
|
+
return
|
|
240
|
+
}
|
|
241
|
+
let session = request.settings.map { PinVault.shared.session(settings: $0) } ?? PinVault.shared.session()
|
|
242
|
+
do {
|
|
243
|
+
resolve(try await PinnedFetch.execute(session, request))
|
|
244
|
+
} catch {
|
|
245
|
+
let info = self.exceptionInfo(error)
|
|
246
|
+
let name = info["exceptionName"] as? String ?? "Error"
|
|
247
|
+
let message = info["exceptionMessage"] as? String ?? ""
|
|
248
|
+
reject("E_FETCH", "\(name): \(message)", info)
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// MARK: enrollment
|
|
254
|
+
|
|
255
|
+
@objc public func deviceId(resolve: @escaping Resolve, reject: @escaping Reject) {
|
|
256
|
+
run(resolve, reject) {
|
|
257
|
+
#if canImport(UIKit)
|
|
258
|
+
return await MainActor.run { UIDevice.current.identifierForVendor?.uuidString.lowercased() }
|
|
259
|
+
#else
|
|
260
|
+
return nil
|
|
261
|
+
#endif
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
@objc public func enrollForResult(_ token: String, label: String?, resolve: @escaping Resolve, reject: @escaping Reject) {
|
|
266
|
+
run(resolve, reject) {
|
|
267
|
+
if token.isEmpty || token.count > 4096 { throw BridgeInputError("token: must be 1 to 4096 characters") }
|
|
268
|
+
return await Self.tokens.withTransientSecret(token) {
|
|
269
|
+
self.mapper.enrollment(await Self.guarded([.enroll]) { await PinVault.shared.enrollForResult(token: token, label: label) })
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
@objc public func autoEnrollForResult(resolve: @escaping Resolve, reject: @escaping Reject) {
|
|
275
|
+
run(resolve, reject) { self.mapper.enrollment(await Self.guarded([.enroll]) { await PinVault.shared.autoEnrollForResult() }) }
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
@objc public func checkPendingEnrollment(resolve: @escaping Resolve, reject: @escaping Reject) {
|
|
279
|
+
run(resolve, reject) { self.mapper.enrollment(await Self.guarded([.enroll]) { await PinVault.shared.checkPendingEnrollment() }) }
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
@objc public func isEnrolled(_ label: String?, resolve: @escaping Resolve, reject: @escaping Reject) {
|
|
283
|
+
run(resolve, reject) { PinVault.shared.isEnrolled(label: label) }
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
@objc public func isEnrollmentPending(_ label: String?, resolve: @escaping Resolve, reject: @escaping Reject) {
|
|
287
|
+
run(resolve, reject) { PinVault.shared.isEnrollmentPending(label: label) }
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
@objc public func enrollmentVerificationCode(_ label: String?, resolve: @escaping Resolve, reject: @escaping Reject) {
|
|
291
|
+
run(resolve, reject) { PinVault.shared.enrollmentVerificationCode(label: label) }
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
@objc public func enrolledClientCN(_ label: String?, resolve: @escaping Resolve, reject: @escaping Reject) {
|
|
295
|
+
run(resolve, reject) { PinVault.shared.enrolledClientCN(label: label) }
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
@objc public func enrolledClientNotAfter(_ label: String?, resolve: @escaping Resolve, reject: @escaping Reject) {
|
|
299
|
+
run(resolve, reject) { PinVault.shared.enrolledClientNotAfter(label: label) }
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
@objc public func unenroll(_ label: String?, wipeVaultFiles: Bool, resolve: @escaping Resolve, reject: @escaping Reject) {
|
|
303
|
+
run(resolve, reject) { PinVault.shared.unenroll(label: label, wipeVaultFiles: wipeVaultFiles); return nil }
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
@objc public func identityKeySecurityLevel(_ label: String?, resolve: @escaping Resolve, reject: @escaping Reject) {
|
|
307
|
+
run(resolve, reject) { PinVault.shared.identityKeySecurityLevel(label: label)?.rawValue }
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// MARK: vault files
|
|
311
|
+
|
|
312
|
+
@objc public func setVaultToken(_ key: String, token: String?, resolve: @escaping Resolve, reject: @escaping Reject) {
|
|
313
|
+
run(resolve, reject) {
|
|
314
|
+
try Self.checkKey(key)
|
|
315
|
+
try Self.checkToken(token)
|
|
316
|
+
Self.tokens.put(key, token)
|
|
317
|
+
return nil
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
@objc public func clearVaultTokens(resolve: @escaping Resolve, reject: @escaping Reject) {
|
|
322
|
+
run(resolve, reject) { Self.tokens.clear() }
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
@objc public func fetchFile(_ key: String, token: String?, resolve: @escaping Resolve, reject: @escaping Reject) {
|
|
326
|
+
run(resolve, reject) {
|
|
327
|
+
try Self.checkKey(key)
|
|
328
|
+
try Self.checkToken(token)
|
|
329
|
+
if let token { Self.tokens.put(key, token) }
|
|
330
|
+
return self.mapper.vaultFile(await Self.guarded([.fetchFile]) { await PinVault.shared.fetchFile(key) })
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
@objc public func loadFile(_ key: String, encoding: String, resolve: @escaping Resolve, reject: @escaping Reject) {
|
|
335
|
+
run(resolve, reject) {
|
|
336
|
+
try Self.checkKey(key)
|
|
337
|
+
let enc = try ResultMapper.checkEncoding(encoding)
|
|
338
|
+
return PinVault.shared.loadFile(key).map { ResultMapper.encode($0, enc) }
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
@objc public func fileStatus(_ key: String, resolve: @escaping Resolve, reject: @escaping Reject) {
|
|
343
|
+
run(resolve, reject) { try Self.checkKey(key); return PinVault.shared.fileStatus(key).rawValue }
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
@objc public func unlockFile(_ key: String, prompt json: String, resolve: @escaping Resolve, reject: @escaping Reject) {
|
|
347
|
+
run(resolve, reject) {
|
|
348
|
+
try Self.checkKey(key)
|
|
349
|
+
let (prompt, encoding) = try ConfigParser.unlockPrompt(json)
|
|
350
|
+
// LAContext's Face ID / passcode prompt is the library's.
|
|
351
|
+
return self.mapper.unlock(
|
|
352
|
+
await Self.guarded([.unlockFile]) { await PinVault.shared.unlockFile(key: key, prompt: prompt) }, encoding: encoding
|
|
353
|
+
)
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
@objc public func isFileLocked(_ key: String, resolve: @escaping Resolve, reject: @escaping Reject) {
|
|
358
|
+
run(resolve, reject) { try Self.checkKey(key); return PinVault.shared.isFileLocked(key) }
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
@objc public func hasFile(_ key: String, resolve: @escaping Resolve, reject: @escaping Reject) {
|
|
362
|
+
run(resolve, reject) { try Self.checkKey(key); return PinVault.shared.hasFile(key) }
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
@objc public func fileVersion(_ key: String, resolve: @escaping Resolve, reject: @escaping Reject) {
|
|
366
|
+
run(resolve, reject) { try Self.checkKey(key); return PinVault.shared.fileVersion(key) }
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
@objc public func clearFile(_ key: String, resolve: @escaping Resolve, reject: @escaping Reject) {
|
|
370
|
+
run(resolve, reject) { try Self.checkKey(key); PinVault.shared.clearFile(key); return nil }
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
@objc public func syncAllFiles(resolve: @escaping Resolve, reject: @escaping Reject) {
|
|
374
|
+
run(resolve, reject) { (await Self.guarded([.fetchFile]) { await PinVault.shared.syncAllFiles() }).mapValues(self.mapper.vaultFile) }
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// MARK: attestation
|
|
378
|
+
|
|
379
|
+
@objc public func attestNow(_ configApiId: String?, resolve: @escaping Resolve, reject: @escaping Reject) {
|
|
380
|
+
run(resolve, reject) { self.mapper.attestation(await PinVault.shared.attestNow(configApiId: configApiId)) }
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
@objc public func fetchAttestationToken(_ host: String?, resolve: @escaping Resolve, reject: @escaping Reject) {
|
|
384
|
+
run(resolve, reject) { self.mapper.attestationToken(await PinVault.shared.fetchAttestationToken(host: host)) }
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
@objc public func attestationStatus(_ configApiId: String?, resolve: @escaping Resolve, reject: @escaping Reject) {
|
|
388
|
+
run(resolve, reject) { self.mapper.attestation(PinVault.shared.attestationStatus(configApiId: configApiId)) }
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// MARK: environmentGuard
|
|
392
|
+
|
|
393
|
+
@objc public func answerGuard(_ requestId: String, allowed: Bool) {
|
|
394
|
+
let g = Self.locked { Self.jsGuard }
|
|
395
|
+
g?.answer(requestId, allowed: allowed)
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/// Serialises `start` calls (a second start resets and starts again).
|
|
400
|
+
private actor StartQueue {
|
|
401
|
+
private var tail: Task<Void, Never>?
|
|
402
|
+
|
|
403
|
+
func run(_ body: @escaping @Sendable () async -> Void) async {
|
|
404
|
+
let previous = tail
|
|
405
|
+
let task = Task {
|
|
406
|
+
await previous?.value
|
|
407
|
+
await body()
|
|
408
|
+
}
|
|
409
|
+
tail = task
|
|
410
|
+
await task.value
|
|
411
|
+
}
|
|
412
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// The Swift side of RNPinVaultURLRequestHandler.mm: React Native's https
|
|
2
|
+
// requests (fetch, XMLHttpRequest, <Image>) through PinVault's pinned session.
|
|
3
|
+
// The rules live in Core/ReactNetworking.swift.
|
|
4
|
+
import Foundation
|
|
5
|
+
import PinVault
|
|
6
|
+
|
|
7
|
+
@objc(RNPinVaultReactNetworking)
|
|
8
|
+
public final class PinVaultReactNetworking: NSObject {
|
|
9
|
+
|
|
10
|
+
/// Info.plist `PinVaultPinReactNativeNetworking` = NO hands https back to
|
|
11
|
+
/// React Native's own (unpinned) handler — a native opt-out, which a JS
|
|
12
|
+
/// bundle cannot flip. Default: on.
|
|
13
|
+
@objc public static let enabled: Bool =
|
|
14
|
+
(Bundle.main.object(forInfoDictionaryKey: "PinVaultPinReactNativeNetworking") as? Bool) ?? true
|
|
15
|
+
|
|
16
|
+
@objc public static func canHandle(_ request: URLRequest) -> Bool {
|
|
17
|
+
enabled && ReactNetworking.handles(request)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/// The request token for RN; nothing reaches the blocks before `resume()`.
|
|
21
|
+
@objc public static func prepare(
|
|
22
|
+
_ request: URLRequest,
|
|
23
|
+
didSendData: @escaping (Int64) -> Void,
|
|
24
|
+
didReceiveResponse: @escaping (URLResponse) -> Void,
|
|
25
|
+
didReceiveData: @escaping (Data) -> Void,
|
|
26
|
+
didComplete: @escaping (NSError?) -> Void
|
|
27
|
+
) -> ReactNetworkingTask {
|
|
28
|
+
var send: ReactNetworking.Send?
|
|
29
|
+
if PinVaultBridge.isStarted {
|
|
30
|
+
send = { (request: URLRequest, limit: Int64) async throws -> (Data, URLResponse) in
|
|
31
|
+
try await PinVault.shared.session().data(for: request, maxResponseBytes: limit)
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return ReactNetworking.prepare(
|
|
35
|
+
request,
|
|
36
|
+
send: send,
|
|
37
|
+
maxResponseBytes: PinVaultBridge.reactNativeMaxResponseBytes,
|
|
38
|
+
describe: PinVaultBridge.describe,
|
|
39
|
+
callbacks: ReactNetworking.Callbacks(
|
|
40
|
+
didSendData: didSendData,
|
|
41
|
+
didReceiveResponse: didReceiveResponse,
|
|
42
|
+
didReceiveData: didReceiveData,
|
|
43
|
+
didComplete: { didComplete($0.map { $0 as NSError }) }
|
|
44
|
+
)
|
|
45
|
+
)
|
|
46
|
+
}
|
|
47
|
+
}
|
package/ios/RNPinVault.h
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// RNPinVault — the ObjC++ shell of the TurboModule. It conforms to the
|
|
2
|
+
// codegen protocol and forwards every call to the Swift bridge
|
|
3
|
+
// (PinVaultBridge.swift), which calls PinVault.shared.
|
|
4
|
+
#import <RNPinVaultSpec/RNPinVaultSpec.h>
|
|
5
|
+
|
|
6
|
+
@interface RNPinVault : NativePinVaultSpecBase <NativePinVaultSpec>
|
|
7
|
+
@end
|