@necessarysdk/react-native 0.0.0-stage → 0.1.5
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 +22 -0
- package/LICENSE +201 -0
- package/NOTICE +2 -0
- package/NecessaryReactNative.podspec +23 -0
- package/README.md +429 -2
- package/android/build.gradle +83 -0
- package/android/src/main/AndroidManifest.xml +41 -0
- package/android/src/main/cpp/CMakeLists.txt +7 -0
- package/android/src/main/cpp/code_integrity.cpp +484 -0
- package/android/src/main/java/com/necessarysdk/reactnative/Causes.kt +49 -0
- package/android/src/main/java/com/necessarysdk/reactnative/InstallationKeyManager.kt +388 -0
- package/android/src/main/java/com/necessarysdk/reactnative/IntegrityProbeManager.kt +1152 -0
- package/android/src/main/java/com/necessarysdk/reactnative/KeyPresence.kt +62 -0
- package/android/src/main/java/com/necessarysdk/reactnative/NecessaryModules.kt +94 -0
- package/android/src/main/java/com/necessarysdk/reactnative/NecessaryNative.kt +323 -0
- package/android/src/main/java/com/necessarysdk/reactnative/NecessaryPackage.kt +38 -0
- package/android/src/main/java/com/necessarysdk/reactnative/ProbeReads.kt +314 -0
- package/android/src/main/java/com/necessarysdk/reactnative/ScreenGuard.kt +56 -0
- package/android/src/main/java/com/necessarysdk/reactnative/SecureStore.kt +127 -0
- package/android/src/main/java/com/necessarysdk/reactnative/StepUpKeyEntry.kt +57 -0
- package/android/src/main/java/com/necessarysdk/reactnative/StepUpKeyManager.kt +762 -0
- package/contract/report-1/android-8-ios-5/RULES.md +149 -0
- package/contract/report-1/android-8-ios-5/report.schema.json +2753 -0
- package/contract/report-1/android-8-ios-5/samples/android-8-complete.json +205 -0
- package/contract/report-1/android-8-ios-5/samples/android-8-incomplete.json +205 -0
- package/contract/report-1/android-8-ios-5/samples/android-8-invalid.json +205 -0
- package/contract/report-1/android-8-ios-5/samples/android-8-location-one-fix.json +228 -0
- package/contract/report-1/android-8-ios-5/samples/android-8-location-two-fixes.json +228 -0
- package/contract/report-1/android-8-ios-5/samples/android-8-refused.json +208 -0
- package/contract/report-1/android-8-ios-5/samples/ios-5-complete.json +106 -0
- package/contract/report-1/android-8-ios-5/samples/ios-5-incomplete.json +105 -0
- package/contract/report-1/android-8-ios-5/samples/ios-5-invalid.json +107 -0
- package/contract/report-1/android-8-ios-5/samples/ios-5-partial.json +110 -0
- package/contract/report-1/android-8-ios-5/samples/ios-5-refused.json +109 -0
- package/doc/integration_guide.md +771 -0
- package/doc/measurements.md +774 -0
- package/ios/InstallationKeyManager.swift +694 -0
- package/ios/IntegrityProbeManager.swift +960 -0
- package/ios/NecessaryModules.h +34 -0
- package/ios/NecessaryModules.mm +208 -0
- package/ios/NecessaryNative.swift +327 -0
- package/ios/PrivacyInfo.xcprivacy +14 -0
- package/ios/ProbeReads.swift +353 -0
- package/lib/client.d.ts +105 -0
- package/lib/client.js +176 -0
- package/lib/collector.d.ts +18 -0
- package/lib/collector.js +49 -0
- package/lib/deviceTrust.d.ts +253 -0
- package/lib/deviceTrust.js +690 -0
- package/lib/errors.d.ts +43 -0
- package/lib/errors.js +56 -0
- package/lib/index.d.ts +18 -0
- package/lib/index.js +34 -0
- package/lib/installationKey.d.ts +62 -0
- package/lib/installationKey.js +185 -0
- package/lib/installationStore.d.ts +49 -0
- package/lib/installationStore.js +127 -0
- package/lib/native/NativeNecessaryInstallationKey.d.ts +10 -0
- package/lib/native/NativeNecessaryInstallationKey.js +4 -0
- package/lib/native/NativeNecessaryIntegrity.d.ts +8 -0
- package/lib/native/NativeNecessaryIntegrity.js +4 -0
- package/lib/native/NativeNecessarySecureScreen.d.ts +7 -0
- package/lib/native/NativeNecessarySecureScreen.js +4 -0
- package/lib/native/NativeNecessaryStepUpKey.d.ts +9 -0
- package/lib/native/NativeNecessaryStepUpKey.js +4 -0
- package/lib/native/NativeNecessaryStorage.d.ts +14 -0
- package/lib/native/NativeNecessaryStorage.js +4 -0
- package/lib/protocol.d.ts +24 -0
- package/lib/protocol.js +50 -0
- package/lib/reinstallHint.d.ts +23 -0
- package/lib/reinstallHint.js +65 -0
- package/lib/report.d.ts +62 -0
- package/lib/report.js +140 -0
- package/lib/requestProof.d.ts +116 -0
- package/lib/requestProof.js +220 -0
- package/lib/secureScreen.d.ts +15 -0
- package/lib/secureScreen.js +32 -0
- package/lib/stepUpKey.d.ts +105 -0
- package/lib/stepUpKey.js +187 -0
- package/lib/transport.d.ts +39 -0
- package/lib/transport.js +117 -0
- package/lib/util/bytes.d.ts +18 -0
- package/lib/util/bytes.js +207 -0
- package/lib/util/native.d.ts +20 -0
- package/lib/util/native.js +49 -0
- package/lib/util/release.d.ts +2 -0
- package/lib/util/release.js +7 -0
- package/lib/util/target.d.ts +18 -0
- package/lib/util/target.js +128 -0
- package/package.json +81 -4
- package/src/client.ts +221 -0
- package/src/collector.ts +64 -0
- package/src/deviceTrust.ts +842 -0
- package/src/errors.ts +71 -0
- package/src/index.ts +19 -0
- package/src/installationKey.ts +223 -0
- package/src/installationStore.ts +143 -0
- package/src/native/NativeNecessaryInstallationKey.ts +12 -0
- package/src/native/NativeNecessaryIntegrity.ts +15 -0
- package/src/native/NativeNecessarySecureScreen.ts +9 -0
- package/src/native/NativeNecessaryStepUpKey.ts +11 -0
- package/src/native/NativeNecessaryStorage.ts +16 -0
- package/src/protocol.ts +52 -0
- package/src/reinstallHint.ts +69 -0
- package/src/report.ts +174 -0
- package/src/requestProof.ts +295 -0
- package/src/secureScreen.ts +29 -0
- package/src/stepUpKey.ts +238 -0
- package/src/transport.ts +145 -0
- package/src/util/bytes.ts +194 -0
- package/src/util/native.ts +56 -0
- package/src/util/release.ts +6 -0
- package/src/util/target.ts +132 -0
- 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>
|