@necessarysdk/react-native 0.0.0-stage → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/LICENSE +201 -0
  3. package/NOTICE +2 -0
  4. package/NecessaryReactNative.podspec +23 -0
  5. package/README.md +429 -2
  6. package/android/build.gradle +83 -0
  7. package/android/src/main/AndroidManifest.xml +41 -0
  8. package/android/src/main/cpp/CMakeLists.txt +7 -0
  9. package/android/src/main/cpp/code_integrity.cpp +484 -0
  10. package/android/src/main/java/com/necessarysdk/reactnative/Causes.kt +49 -0
  11. package/android/src/main/java/com/necessarysdk/reactnative/InstallationKeyManager.kt +388 -0
  12. package/android/src/main/java/com/necessarysdk/reactnative/IntegrityProbeManager.kt +1157 -0
  13. package/android/src/main/java/com/necessarysdk/reactnative/KeyPresence.kt +62 -0
  14. package/android/src/main/java/com/necessarysdk/reactnative/NecessaryModules.kt +94 -0
  15. package/android/src/main/java/com/necessarysdk/reactnative/NecessaryNative.kt +338 -0
  16. package/android/src/main/java/com/necessarysdk/reactnative/NecessaryPackage.kt +38 -0
  17. package/android/src/main/java/com/necessarysdk/reactnative/ProbeReads.kt +333 -0
  18. package/android/src/main/java/com/necessarysdk/reactnative/ScreenGuard.kt +56 -0
  19. package/android/src/main/java/com/necessarysdk/reactnative/SecureStore.kt +127 -0
  20. package/android/src/main/java/com/necessarysdk/reactnative/StepUpKeyEntry.kt +57 -0
  21. package/android/src/main/java/com/necessarysdk/reactnative/StepUpKeyManager.kt +767 -0
  22. package/contract/report-1/android-8-ios-5/RULES.md +149 -0
  23. package/contract/report-1/android-8-ios-5/report.schema.json +2753 -0
  24. package/contract/report-1/android-8-ios-5/samples/android-8-complete.json +205 -0
  25. package/contract/report-1/android-8-ios-5/samples/android-8-incomplete.json +205 -0
  26. package/contract/report-1/android-8-ios-5/samples/android-8-invalid.json +205 -0
  27. package/contract/report-1/android-8-ios-5/samples/android-8-location-one-fix.json +228 -0
  28. package/contract/report-1/android-8-ios-5/samples/android-8-location-two-fixes.json +228 -0
  29. package/contract/report-1/android-8-ios-5/samples/android-8-refused.json +208 -0
  30. package/contract/report-1/android-8-ios-5/samples/ios-5-complete.json +106 -0
  31. package/contract/report-1/android-8-ios-5/samples/ios-5-incomplete.json +105 -0
  32. package/contract/report-1/android-8-ios-5/samples/ios-5-invalid.json +107 -0
  33. package/contract/report-1/android-8-ios-5/samples/ios-5-partial.json +110 -0
  34. package/contract/report-1/android-8-ios-5/samples/ios-5-refused.json +109 -0
  35. package/doc/integration_guide.md +771 -0
  36. package/doc/measurements.md +774 -0
  37. package/ios/InstallationKeyManager.swift +694 -0
  38. package/ios/IntegrityProbeManager.swift +959 -0
  39. package/ios/NecessaryModules.h +34 -0
  40. package/ios/NecessaryModules.mm +208 -0
  41. package/ios/NecessaryNative.swift +327 -0
  42. package/ios/PrivacyInfo.xcprivacy +14 -0
  43. package/ios/ProbeReads.swift +370 -0
  44. package/lib/client.d.ts +105 -0
  45. package/lib/client.js +176 -0
  46. package/lib/collector.d.ts +18 -0
  47. package/lib/collector.js +49 -0
  48. package/lib/deviceTrust.d.ts +253 -0
  49. package/lib/deviceTrust.js +699 -0
  50. package/lib/errors.d.ts +43 -0
  51. package/lib/errors.js +56 -0
  52. package/lib/index.d.ts +18 -0
  53. package/lib/index.js +34 -0
  54. package/lib/installationKey.d.ts +62 -0
  55. package/lib/installationKey.js +185 -0
  56. package/lib/installationStore.d.ts +49 -0
  57. package/lib/installationStore.js +127 -0
  58. package/lib/native/NativeNecessaryInstallationKey.d.ts +10 -0
  59. package/lib/native/NativeNecessaryInstallationKey.js +4 -0
  60. package/lib/native/NativeNecessaryIntegrity.d.ts +8 -0
  61. package/lib/native/NativeNecessaryIntegrity.js +4 -0
  62. package/lib/native/NativeNecessarySecureScreen.d.ts +7 -0
  63. package/lib/native/NativeNecessarySecureScreen.js +4 -0
  64. package/lib/native/NativeNecessaryStepUpKey.d.ts +9 -0
  65. package/lib/native/NativeNecessaryStepUpKey.js +4 -0
  66. package/lib/native/NativeNecessaryStorage.d.ts +14 -0
  67. package/lib/native/NativeNecessaryStorage.js +4 -0
  68. package/lib/protocol.d.ts +24 -0
  69. package/lib/protocol.js +50 -0
  70. package/lib/reinstallHint.d.ts +23 -0
  71. package/lib/reinstallHint.js +65 -0
  72. package/lib/report.d.ts +62 -0
  73. package/lib/report.js +134 -0
  74. package/lib/requestProof.d.ts +116 -0
  75. package/lib/requestProof.js +220 -0
  76. package/lib/secureScreen.d.ts +15 -0
  77. package/lib/secureScreen.js +32 -0
  78. package/lib/stepUpKey.d.ts +105 -0
  79. package/lib/stepUpKey.js +187 -0
  80. package/lib/transport.d.ts +39 -0
  81. package/lib/transport.js +117 -0
  82. package/lib/util/answers.d.ts +6 -0
  83. package/lib/util/answers.js +16 -0
  84. package/lib/util/bytes.d.ts +18 -0
  85. package/lib/util/bytes.js +207 -0
  86. package/lib/util/native.d.ts +20 -0
  87. package/lib/util/native.js +49 -0
  88. package/lib/util/release.d.ts +2 -0
  89. package/lib/util/release.js +7 -0
  90. package/lib/util/target.d.ts +18 -0
  91. package/lib/util/target.js +128 -0
  92. package/package.json +81 -4
  93. package/src/client.ts +221 -0
  94. package/src/collector.ts +64 -0
  95. package/src/deviceTrust.ts +851 -0
  96. package/src/errors.ts +71 -0
  97. package/src/index.ts +19 -0
  98. package/src/installationKey.ts +223 -0
  99. package/src/installationStore.ts +143 -0
  100. package/src/native/NativeNecessaryInstallationKey.ts +12 -0
  101. package/src/native/NativeNecessaryIntegrity.ts +15 -0
  102. package/src/native/NativeNecessarySecureScreen.ts +9 -0
  103. package/src/native/NativeNecessaryStepUpKey.ts +11 -0
  104. package/src/native/NativeNecessaryStorage.ts +16 -0
  105. package/src/protocol.ts +52 -0
  106. package/src/reinstallHint.ts +69 -0
  107. package/src/report.ts +168 -0
  108. package/src/requestProof.ts +295 -0
  109. package/src/secureScreen.ts +29 -0
  110. package/src/stepUpKey.ts +238 -0
  111. package/src/transport.ts +145 -0
  112. package/src/util/answers.ts +15 -0
  113. package/src/util/bytes.ts +194 -0
  114. package/src/util/native.ts +56 -0
  115. package/src/util/release.ts +6 -0
  116. package/src/util/target.ts +132 -0
  117. package/vectors/client_server.json +264 -0
@@ -0,0 +1,34 @@
1
+ // The SDK's five native modules on iOS, each a thin door to the one native side the app shares
2
+ // (NecessaryNative, in NecessaryNative.swift). The specs they conform to are generated from
3
+ // src/native/*.ts.
4
+
5
+ #import <React/RCTBridgeModule.h>
6
+
7
+ #ifdef RCT_NEW_ARCH_ENABLED
8
+ #import <NecessarySpec/NecessarySpec.h>
9
+
10
+ @interface NecessaryInstallationKey : NSObject <NativeNecessaryInstallationKeySpec>
11
+ @end
12
+ @interface NecessaryIntegrity : NSObject <NativeNecessaryIntegritySpec>
13
+ @end
14
+ @interface NecessaryStepUpKey : NSObject <NativeNecessaryStepUpKeySpec>
15
+ @end
16
+ @interface NecessarySecureScreen : NSObject <NativeNecessarySecureScreenSpec>
17
+ @end
18
+ @interface NecessaryStorage : NSObject <NativeNecessaryStorageSpec>
19
+ @end
20
+
21
+ #else
22
+
23
+ @interface NecessaryInstallationKey : NSObject <RCTBridgeModule>
24
+ @end
25
+ @interface NecessaryIntegrity : NSObject <RCTBridgeModule>
26
+ @end
27
+ @interface NecessaryStepUpKey : NSObject <RCTBridgeModule>
28
+ @end
29
+ @interface NecessarySecureScreen : NSObject <RCTBridgeModule>
30
+ @end
31
+ @interface NecessaryStorage : NSObject <RCTBridgeModule>
32
+ @end
33
+
34
+ #endif
@@ -0,0 +1,208 @@
1
+ #import "NecessaryModules.h"
2
+
3
+ // What the generated interface of NecessaryNative names: its location delegate, and UIKit.
4
+ #import <CoreLocation/CoreLocation.h>
5
+ #import <UIKit/UIKit.h>
6
+
7
+ #if __has_include(<NecessaryReactNative/NecessaryReactNative-Swift.h>)
8
+ #import <NecessaryReactNative/NecessaryReactNative-Swift.h>
9
+ #else
10
+ #import "NecessaryReactNative-Swift.h"
11
+ #endif
12
+
13
+ // A refusal from the native side, as React Native's promise takes it: its code and its words.
14
+ static void (^Rejecting(RCTPromiseRejectBlock reject))(NSString *, NSString *) {
15
+ return ^(NSString *code, NSString *message) {
16
+ reject(code, message, nil);
17
+ };
18
+ }
19
+
20
+ static NSArray<NSString *> *Strings(NSArray *items) {
21
+ NSMutableArray<NSString *> *strings = [NSMutableArray arrayWithCapacity:items.count];
22
+ for (id item in items) {
23
+ if ([item isKindOfClass:[NSString class]]) {
24
+ [strings addObject:item];
25
+ }
26
+ }
27
+ return strings;
28
+ }
29
+
30
+ @implementation NecessaryInstallationKey
31
+
32
+ RCT_EXPORT_MODULE()
33
+
34
+ + (BOOL)requiresMainQueueSetup
35
+ {
36
+ return NO;
37
+ }
38
+
39
+ RCT_EXPORT_METHOD(getOrCreateKey : (RCTPromiseResolveBlock)resolve reject : (RCTPromiseRejectBlock)reject)
40
+ {
41
+ [NecessaryNative.shared getOrCreateKeyWithResolve:resolve reject:Rejecting(reject)];
42
+ }
43
+
44
+ RCT_EXPORT_METHOD(sign : (NSString *)payload resolve : (RCTPromiseResolveBlock)resolve reject : (RCTPromiseRejectBlock)reject)
45
+ {
46
+ [NecessaryNative.shared signWithPayload:payload resolve:resolve reject:Rejecting(reject)];
47
+ }
48
+
49
+ RCT_EXPORT_METHOD(deleteKey : (RCTPromiseResolveBlock)resolve reject : (RCTPromiseRejectBlock)reject)
50
+ {
51
+ [NecessaryNative.shared deleteKeyWithResolve:resolve reject:Rejecting(reject)];
52
+ }
53
+
54
+ RCT_EXPORT_METHOD(reinstallHintIdentifier : (RCTPromiseResolveBlock)resolve reject : (RCTPromiseRejectBlock)reject)
55
+ {
56
+ [NecessaryNative.shared reinstallHintIdentifierWithResolve:resolve reject:Rejecting(reject)];
57
+ }
58
+
59
+ #ifdef RCT_NEW_ARCH_ENABLED
60
+ - (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
61
+ (const facebook::react::ObjCTurboModule::InitParams &)params
62
+ {
63
+ return std::make_shared<facebook::react::NativeNecessaryInstallationKeySpecJSI>(params);
64
+ }
65
+ #endif
66
+
67
+ @end
68
+
69
+ @implementation NecessaryIntegrity
70
+
71
+ RCT_EXPORT_MODULE()
72
+
73
+ + (BOOL)requiresMainQueueSetup
74
+ {
75
+ return NO;
76
+ }
77
+
78
+ RCT_EXPORT_METHOD(collect : (NSArray *)requiredProbes challengeNonce : (NSString *)challengeNonce optionalProbes : (
79
+ NSArray *)optionalProbes enabledMeasurements : (NSArray *)enabledMeasurements resolve : (RCTPromiseResolveBlock)
80
+ resolve reject : (RCTPromiseRejectBlock)reject)
81
+ {
82
+ [NecessaryNative.shared collectWithRequiredProbes:Strings(requiredProbes)
83
+ challengeNonce:challengeNonce
84
+ optionalProbes:Strings(optionalProbes)
85
+ enabledMeasurements:Strings(enabledMeasurements)
86
+ resolve:resolve
87
+ reject:Rejecting(reject)];
88
+ }
89
+
90
+ RCT_EXPORT_METHOD(requestLocationPermission : (RCTPromiseResolveBlock)resolve reject : (RCTPromiseRejectBlock)reject)
91
+ {
92
+ [NecessaryNative.shared requestLocationPermissionWithResolve:resolve reject:Rejecting(reject)];
93
+ }
94
+
95
+ #ifdef RCT_NEW_ARCH_ENABLED
96
+ - (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
97
+ (const facebook::react::ObjCTurboModule::InitParams &)params
98
+ {
99
+ return std::make_shared<facebook::react::NativeNecessaryIntegritySpecJSI>(params);
100
+ }
101
+ #endif
102
+
103
+ @end
104
+
105
+ @implementation NecessaryStepUpKey
106
+
107
+ RCT_EXPORT_MODULE()
108
+
109
+ + (BOOL)requiresMainQueueSetup
110
+ {
111
+ return NO;
112
+ }
113
+
114
+ // iOS makes per-use keys only: the mode and the window are Android's.
115
+ RCT_EXPORT_METHOD(getOrCreateKey : (NSString *)factor mode : (NSString *)mode windowSeconds : (double)
116
+ windowSeconds resolve : (RCTPromiseResolveBlock)resolve reject : (RCTPromiseRejectBlock)reject)
117
+ {
118
+ [NecessaryNative.shared getOrCreateStepUpKeyWithFactor:factor resolve:resolve reject:Rejecting(reject)];
119
+ }
120
+
121
+ RCT_EXPORT_METHOD(sign : (NSString *)payload reason : (NSString *)reason mode : (NSString *)mode resolve : (
122
+ RCTPromiseResolveBlock)resolve reject : (RCTPromiseRejectBlock)reject)
123
+ {
124
+ [NecessaryNative.shared signStepUpWithPayload:payload reason:reason resolve:resolve reject:Rejecting(reject)];
125
+ }
126
+
127
+ RCT_EXPORT_METHOD(deleteKey : (RCTPromiseResolveBlock)resolve reject : (RCTPromiseRejectBlock)reject)
128
+ {
129
+ [NecessaryNative.shared deleteStepUpKeyWithResolve:resolve reject:Rejecting(reject)];
130
+ }
131
+
132
+ #ifdef RCT_NEW_ARCH_ENABLED
133
+ - (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
134
+ (const facebook::react::ObjCTurboModule::InitParams &)params
135
+ {
136
+ return std::make_shared<facebook::react::NativeNecessaryStepUpKeySpecJSI>(params);
137
+ }
138
+ #endif
139
+
140
+ @end
141
+
142
+ @implementation NecessarySecureScreen
143
+
144
+ RCT_EXPORT_MODULE()
145
+
146
+ + (BOOL)requiresMainQueueSetup
147
+ {
148
+ return NO;
149
+ }
150
+
151
+ RCT_EXPORT_METHOD(protect : (BOOL)on resolve : (RCTPromiseResolveBlock)resolve reject : (RCTPromiseRejectBlock)reject)
152
+ {
153
+ [NecessaryNative.shared protectWithOn:on resolve:resolve reject:Rejecting(reject)];
154
+ }
155
+
156
+ #ifdef RCT_NEW_ARCH_ENABLED
157
+ - (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
158
+ (const facebook::react::ObjCTurboModule::InitParams &)params
159
+ {
160
+ return std::make_shared<facebook::react::NativeNecessarySecureScreenSpecJSI>(params);
161
+ }
162
+ #endif
163
+
164
+ @end
165
+
166
+ @implementation NecessaryStorage
167
+
168
+ RCT_EXPORT_MODULE()
169
+
170
+ + (BOOL)requiresMainQueueSetup
171
+ {
172
+ return NO;
173
+ }
174
+
175
+ RCT_EXPORT_METHOD(read : (NSString *)key resolve : (RCTPromiseResolveBlock)resolve reject : (RCTPromiseRejectBlock)reject)
176
+ {
177
+ [NecessaryNative.shared readWithKey:key
178
+ resolve:^(id value) {
179
+ resolve(value == [NSNull null] ? nil : value);
180
+ }
181
+ reject:Rejecting(reject)];
182
+ }
183
+
184
+ RCT_EXPORT_METHOD(write : (NSString *)key value : (NSString *)value resolve : (RCTPromiseResolveBlock)
185
+ resolve reject : (RCTPromiseRejectBlock)reject)
186
+ {
187
+ [NecessaryNative.shared writeWithKey:key value:value resolve:resolve reject:Rejecting(reject)];
188
+ }
189
+
190
+ RCT_EXPORT_METHOD(remove : (NSString *)key resolve : (RCTPromiseResolveBlock)resolve reject : (RCTPromiseRejectBlock)reject)
191
+ {
192
+ [NecessaryNative.shared removeWithKey:key resolve:resolve reject:Rejecting(reject)];
193
+ }
194
+
195
+ RCT_EXPORT_METHOD(randomBytes : (double)count resolve : (RCTPromiseResolveBlock)resolve reject : (RCTPromiseRejectBlock)reject)
196
+ {
197
+ [NecessaryNative.shared randomBytesWithCount:(NSInteger)count resolve:resolve reject:Rejecting(reject)];
198
+ }
199
+
200
+ #ifdef RCT_NEW_ARCH_ENABLED
201
+ - (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
202
+ (const facebook::react::ObjCTurboModule::InitParams &)params
203
+ {
204
+ return std::make_shared<facebook::react::NativeNecessaryStorageSpecJSI>(params);
205
+ }
206
+ #endif
207
+
208
+ @end
@@ -0,0 +1,327 @@
1
+ import CoreLocation
2
+ import Foundation
3
+ import Security
4
+ import UIKit
5
+
6
+ /// The SDK's native side on iOS, shared by its five modules: the installation key, the step-up
7
+ /// key, the collector, screen protection and the secure storage. Keychain, Secure Enclave and
8
+ /// collection work runs on one serial queue off the main thread; the screen covers and the
9
+ /// location prompt are UIKit's, on the main thread. The modules (the `.mm` files beside this one)
10
+ /// are React Native's side; nothing here depends on React Native.
11
+ @objc(NecessaryNative)
12
+ public final class NecessaryNative: NSObject, CLLocationManagerDelegate {
13
+ public typealias Resolve = (Any?) -> Void
14
+ public typealias Reject = (String, String) -> Void
15
+
16
+ /// One per process: the keys, their locks and the covers belong to the app, whichever React
17
+ /// Native runtime asks.
18
+ @objc public static let shared = NecessaryNative(
19
+ bundleIdentifier: Bundle.main.bundleIdentifier ?? "necessary")
20
+
21
+ private let installationKeys: InstallationKeyManager
22
+ private let stepUpKeys: StepUpKeyManager
23
+ private let integrityProbes = IntegrityProbeManager()
24
+ private let store: NecessaryStore
25
+ private let worker = DispatchQueue(label: "necessary.native", qos: .userInitiated)
26
+ private let storageWorker = DispatchQueue(label: "necessary.storage", qos: .userInitiated)
27
+ /// The app's own request for the location permission, answered when the person chooses; a
28
+ /// scan never asks.
29
+ private var locationManager: CLLocationManager?
30
+ private var pendingLocation: Resolve?
31
+ /// While a secret is typed: iOS lets no app block screenshots, so the windows the person sees
32
+ /// are covered whenever the screen is recorded, mirrored or sent to AirPlay.
33
+ private var screenProtected = false
34
+ /// Callers holding the protection: it lasts until the last one is done.
35
+ private var screenHolders = 0
36
+ /// The covers in place now, one per window covered.
37
+ private var captureCovers: [UIView] = []
38
+ /// What moves the covers: the capture starting or stopping, and a window or a scene coming or
39
+ /// going -- so that a cover follows the window the person sees when that window is replaced,
40
+ /// or appears only later.
41
+ private static let coverEvents: [Notification.Name] = [
42
+ UIScreen.capturedDidChangeNotification,
43
+ UIWindow.didBecomeKeyNotification,
44
+ UIWindow.didBecomeVisibleNotification,
45
+ UIWindow.didBecomeHiddenNotification,
46
+ UIScene.willEnterForegroundNotification,
47
+ UIScene.didActivateNotification,
48
+ UIScene.didDisconnectNotification,
49
+ ]
50
+
51
+ init(bundleIdentifier: String) {
52
+ // The keys' aliases are built from the host app's bundle identifier.
53
+ installationKeys = InstallationKeyManager(bundleIdentifier: bundleIdentifier)
54
+ stepUpKeys = StepUpKeyManager(bundleIdentifier: bundleIdentifier)
55
+ store = NecessaryStore(service: "\(bundleIdentifier).necessary.storage")
56
+ super.init()
57
+ }
58
+
59
+ // MARK: - The installation key
60
+
61
+ @objc(getOrCreateKeyWithResolve:reject:) public func getOrCreateKey(resolve: @escaping Resolve, reject: @escaping Reject) {
62
+ let manager = installationKeys
63
+ run(resolve, reject) { try manager.getOrCreateKey() }
64
+ }
65
+
66
+ @objc(signWithPayload:resolve:reject:) public func sign(payload: String, resolve: @escaping Resolve, reject: @escaping Reject) {
67
+ let manager = installationKeys
68
+ run(resolve, reject) { try manager.signPayload(payload) }
69
+ }
70
+
71
+ @objc(deleteKeyWithResolve:reject:) public func deleteKey(resolve: @escaping Resolve, reject: @escaping Reject) {
72
+ let manager = installationKeys
73
+ run(resolve, reject) { try manager.deleteKey() }
74
+ }
75
+
76
+ /// The reinstall hint's source, the identifier for vendor: the same for every app of this
77
+ /// vendor on the phone. The JavaScript side hashes it before it leaves the phone.
78
+ @objc(reinstallHintIdentifierWithResolve:reject:) public func reinstallHintIdentifier(resolve: @escaping Resolve, reject: @escaping Reject) {
79
+ DispatchQueue.main.async {
80
+ resolve(UIDevice.current.identifierForVendor?.uuidString ?? "")
81
+ }
82
+ }
83
+
84
+ // MARK: - The collector
85
+
86
+ @objc(collectWithRequiredProbes:challengeNonce:optionalProbes:enabledMeasurements:resolve:reject:)
87
+ public func collect(
88
+ requiredProbes: [String],
89
+ challengeNonce: String,
90
+ optionalProbes: [String],
91
+ enabledMeasurements: [String],
92
+ resolve: @escaping Resolve,
93
+ reject: @escaping Reject
94
+ ) {
95
+ let collector = integrityProbes
96
+ let enabled = Set(enabledMeasurements)
97
+ run(resolve, reject) {
98
+ collector.collect(requiredProbes: requiredProbes, challengeNonce: challengeNonce,
99
+ optionalProbes: optionalProbes, enabledMeasurements: enabled)
100
+ }
101
+ }
102
+
103
+ /// For the location and mock_location measurements: asks the person once, from a button on
104
+ /// the app's own screen.
105
+ @objc(requestLocationPermissionWithResolve:reject:) public func requestLocationPermission(resolve: @escaping Resolve, reject: @escaping Reject) {
106
+ DispatchQueue.main.async {
107
+ let manager = self.locationManager ?? CLLocationManager()
108
+ self.locationManager = manager
109
+ let state = self.locationState(manager)
110
+ guard state == "undetermined", self.pendingLocation == nil else {
111
+ resolve(state == "undetermined" ? "none" : state)
112
+ return
113
+ }
114
+ self.pendingLocation = resolve
115
+ manager.delegate = self
116
+ manager.requestWhenInUseAuthorization()
117
+ }
118
+ }
119
+
120
+ private func locationState(_ manager: CLLocationManager) -> String {
121
+ switch manager.authorizationStatus {
122
+ case .authorizedWhenInUse, .authorizedAlways:
123
+ return manager.accuracyAuthorization == .reducedAccuracy ? "approximate" : "precise"
124
+ case .notDetermined:
125
+ return "undetermined"
126
+ default:
127
+ return "none"
128
+ }
129
+ }
130
+
131
+ public func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
132
+ let state = locationState(manager)
133
+ guard state != "undetermined", let pending = pendingLocation else { return }
134
+ pendingLocation = nil
135
+ pending(state)
136
+ }
137
+
138
+ // MARK: - The step-up key
139
+
140
+ @objc(getOrCreateStepUpKeyWithFactor:resolve:reject:) public func getOrCreateStepUpKey(factor: String, resolve: @escaping Resolve, reject: @escaping Reject) {
141
+ let manager = stepUpKeys
142
+ run(resolve, reject) { try manager.getOrCreateKey(factor: factor) }
143
+ }
144
+
145
+ /// Off the main thread: the call blocks while the system prompt is up.
146
+ @objc(signStepUpWithPayload:reason:resolve:reject:) public func signStepUp(payload: String, reason: String, resolve: @escaping Resolve,
147
+ reject: @escaping Reject) {
148
+ let manager = stepUpKeys
149
+ run(resolve, reject) { try manager.signPayload(payload, reason: reason) }
150
+ }
151
+
152
+ @objc(deleteStepUpKeyWithResolve:reject:) public func deleteStepUpKey(resolve: @escaping Resolve, reject: @escaping Reject) {
153
+ let manager = stepUpKeys
154
+ run(resolve, reject) { try manager.deleteKey() }
155
+ }
156
+
157
+ // MARK: - Screen protection
158
+
159
+ @objc(protectWithOn:resolve:reject:) public func protect(on: Bool, resolve: @escaping Resolve, reject: @escaping Reject) {
160
+ DispatchQueue.main.async {
161
+ if on {
162
+ self.screenHolders += 1
163
+ } else if self.screenHolders > 0 {
164
+ self.screenHolders -= 1
165
+ }
166
+ self.setScreenProtection(self.screenHolders > 0)
167
+ resolve(["held": self.screenHolders > 0, "secure": false,
168
+ "covered_while_captured": self.screenHolders > 0])
169
+ }
170
+ }
171
+
172
+ private func setScreenProtection(_ on: Bool) {
173
+ screenProtected = on
174
+ for name in NecessaryNative.coverEvents {
175
+ NotificationCenter.default.removeObserver(self, name: name, object: nil)
176
+ if on {
177
+ NotificationCenter.default.addObserver(
178
+ self, selector: #selector(coverEvent(_:)), name: name, object: nil)
179
+ }
180
+ }
181
+ updateCaptureCovers()
182
+ }
183
+
184
+ @objc private func coverEvent(_ notification: Notification) {
185
+ updateCaptureCovers()
186
+ }
187
+
188
+ /// Covers, in each of the app's scenes whose screen is captured, the window the person sees --
189
+ /// its key window, or else its first visible window at the normal level -- and nothing else.
190
+ /// The covers are made again at every change, so that none stays on a window replaced and none
191
+ /// is missing from a window that appeared after the protection began.
192
+ private func updateCaptureCovers() {
193
+ for cover in captureCovers {
194
+ cover.removeFromSuperview()
195
+ }
196
+ captureCovers = []
197
+ guard screenProtected else { return }
198
+ let scenes = UIApplication.shared.connectedScenes.compactMap { $0 as? UIWindowScene }
199
+ for scene in scenes where scene.screen.isCaptured {
200
+ guard let window = scene.windows.first(where: { $0.isKeyWindow })
201
+ ?? scene.windows.first(where: { !$0.isHidden && $0.windowLevel == .normal })
202
+ else { continue }
203
+ let view = UIView(frame: window.bounds)
204
+ view.backgroundColor = .black
205
+ view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
206
+ window.addSubview(view)
207
+ captureCovers.append(view)
208
+ }
209
+ }
210
+
211
+ // MARK: - The secure storage and the random source
212
+
213
+ @objc(readWithKey:resolve:reject:) public func read(key: String, resolve: @escaping Resolve, reject: @escaping Reject) {
214
+ let store = self.store
215
+ run(resolve, reject, on: storageWorker) { try store.read(key) ?? NSNull() }
216
+ }
217
+
218
+ @objc(writeWithKey:value:resolve:reject:) public func write(key: String, value: String, resolve: @escaping Resolve, reject: @escaping Reject) {
219
+ let store = self.store
220
+ run(resolve, reject, on: storageWorker) { try store.write(key, value: value) }
221
+ }
222
+
223
+ @objc(removeWithKey:resolve:reject:) public func remove(key: String, resolve: @escaping Resolve, reject: @escaping Reject) {
224
+ let store = self.store
225
+ run(resolve, reject, on: storageWorker) { try store.remove(key) }
226
+ }
227
+
228
+ @objc(randomBytesWithCount:resolve:reject:) public func randomBytes(count: Int, resolve: @escaping Resolve, reject: @escaping Reject) {
229
+ guard count >= 0, count <= 4096 else {
230
+ reject("INVALID_ARGUMENT", "count must be from 0 to 4096.")
231
+ return
232
+ }
233
+ var bytes = [UInt8](repeating: 0, count: count)
234
+ let status = count == 0 ? errSecSuccess : SecRandomCopyBytes(kSecRandomDefault, count, &bytes)
235
+ guard status == errSecSuccess else {
236
+ reject("RANDOM_FAILED", "SecRandomCopyBytes failed (OSStatus \(status)).")
237
+ return
238
+ }
239
+ resolve(Data(bytes).base64EncodedString()
240
+ .replacingOccurrences(of: "+", with: "-")
241
+ .replacingOccurrences(of: "/", with: "_")
242
+ .replacingOccurrences(of: "=", with: ""))
243
+ }
244
+
245
+ // MARK: - Answers
246
+
247
+ private func run(_ resolve: @escaping Resolve, _ reject: @escaping Reject,
248
+ on queue: DispatchQueue? = nil, _ operation: @escaping () throws -> Any) {
249
+ (queue ?? worker).async {
250
+ do {
251
+ resolve(try operation())
252
+ } catch let failure as InstallationKeyFailure {
253
+ reject(failure.code, failure.message)
254
+ } catch {
255
+ reject("NATIVE_KEY_ERROR", error.localizedDescription)
256
+ }
257
+ }
258
+ }
259
+ }
260
+
261
+ /// The installation ID and the pending step-up re-enrolment, as generic-password Keychain items:
262
+ /// readable after the first unlock, on this device only, never synchronized.
263
+ final class NecessaryStore {
264
+ private let service: String
265
+
266
+ init(service: String) {
267
+ self.service = service
268
+ }
269
+
270
+ private func query(_ key: String) -> [String: Any] {
271
+ [
272
+ kSecClass as String: kSecClassGenericPassword,
273
+ kSecAttrService as String: service,
274
+ kSecAttrAccount as String: key,
275
+ kSecAttrSynchronizable as String: kCFBooleanFalse as Any,
276
+ ]
277
+ }
278
+
279
+ func read(_ key: String) throws -> String? {
280
+ var request = query(key)
281
+ request[kSecReturnData as String] = true
282
+ request[kSecMatchLimit as String] = kSecMatchLimitOne
283
+ var item: CFTypeRef?
284
+ let status = SecItemCopyMatching(request as CFDictionary, &item)
285
+ if status == errSecItemNotFound {
286
+ return nil
287
+ }
288
+ guard status == errSecSuccess else {
289
+ throw InstallationKeyFailure(code: "STORAGE_UNREADABLE",
290
+ message: "The Keychain could not read '\(key)' (OSStatus \(status)).")
291
+ }
292
+ guard let data = item as? Data, let text = String(data: data, encoding: .utf8) else {
293
+ throw InstallationKeyFailure(code: "STORAGE_UNREADABLE",
294
+ message: "The Keychain item '\(key)' is not UTF-8 text.")
295
+ }
296
+ return text
297
+ }
298
+
299
+ func write(_ key: String, value: String) throws -> Bool {
300
+ let data = Data(value.utf8)
301
+ let update: [String: Any] = [
302
+ kSecValueData as String: data,
303
+ kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly,
304
+ ]
305
+ var status = SecItemUpdate(query(key) as CFDictionary, update as CFDictionary)
306
+ if status == errSecItemNotFound {
307
+ var add = query(key)
308
+ add[kSecValueData as String] = data
309
+ add[kSecAttrAccessible as String] = kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly
310
+ status = SecItemAdd(add as CFDictionary, nil)
311
+ }
312
+ guard status == errSecSuccess else {
313
+ throw InstallationKeyFailure(code: "STORAGE_WRITE_FAILED",
314
+ message: "The Keychain could not write '\(key)' (OSStatus \(status)).")
315
+ }
316
+ return true
317
+ }
318
+
319
+ func remove(_ key: String) throws -> Bool {
320
+ let status = SecItemDelete(query(key) as CFDictionary)
321
+ guard status == errSecSuccess || status == errSecItemNotFound else {
322
+ throw InstallationKeyFailure(code: "STORAGE_WRITE_FAILED",
323
+ message: "The Keychain could not delete '\(key)' (OSStatus \(status)).")
324
+ }
325
+ return true
326
+ }
327
+ }
@@ -0,0 +1,14 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3
+ <plist version="1.0">
4
+ <dict>
5
+ <key>NSPrivacyTrackingDomains</key>
6
+ <array/>
7
+ <key>NSPrivacyAccessedAPITypes</key>
8
+ <array/>
9
+ <key>NSPrivacyCollectedDataTypes</key>
10
+ <array/>
11
+ <key>NSPrivacyTracking</key>
12
+ <false/>
13
+ </dict>
14
+ </plist>