react-native-device-integrity 0.1.0 → 0.2.0
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/README.md +138 -11
- package/android/src/main/java/com/deviceintegrity/DeviceIntegrityModule.kt +68 -2
- package/android/src/main/java/com/deviceintegrity/IntegrityChecks.kt +5 -1
- package/android/src/main/java/com/deviceintegrity/TamperChecks.kt +158 -0
- package/ios/DIIntegrityChecks.h +7 -1
- package/ios/DIIntegrityChecks.m +9 -1
- package/ios/DITamperChecks.h +27 -0
- package/ios/DITamperChecks.m +304 -0
- package/ios/DeviceIntegrity.mm +56 -4
- package/jest/index.d.ts +39 -0
- package/jest/index.js +100 -0
- package/lib/module/NativeDeviceIntegrity.js.map +1 -1
- package/lib/module/checkIntegrity.js +2 -0
- package/lib/module/checkIntegrity.js.map +1 -1
- package/lib/module/checkIntegrity.native.js +83 -6
- package/lib/module/checkIntegrity.native.js.map +1 -1
- package/lib/module/index.js +1 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/resolveStatus.js +50 -5
- package/lib/module/resolveStatus.js.map +1 -1
- package/lib/module/types.js +4 -1
- package/lib/module/types.js.map +1 -1
- package/lib/module/useDeviceIntegrity.js +21 -7
- package/lib/module/useDeviceIntegrity.js.map +1 -1
- package/lib/typescript/src/NativeDeviceIntegrity.d.ts +2 -2
- package/lib/typescript/src/NativeDeviceIntegrity.d.ts.map +1 -1
- package/lib/typescript/src/checkIntegrity.d.ts.map +1 -1
- package/lib/typescript/src/checkIntegrity.native.d.ts +6 -0
- package/lib/typescript/src/checkIntegrity.native.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +2 -1
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/resolveStatus.d.ts +1 -0
- package/lib/typescript/src/resolveStatus.d.ts.map +1 -1
- package/lib/typescript/src/types.d.ts +49 -2
- package/lib/typescript/src/types.d.ts.map +1 -1
- package/lib/typescript/src/useDeviceIntegrity.d.ts.map +1 -1
- package/package.json +8 -2
- package/src/NativeDeviceIntegrity.ts +5 -2
- package/src/checkIntegrity.native.ts +112 -15
- package/src/checkIntegrity.ts +2 -0
- package/src/index.tsx +5 -0
- package/src/resolveStatus.ts +69 -12
- package/src/types.ts +76 -2
- package/src/useDeviceIntegrity.ts +22 -7
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
// Independent implementation informed by OWASP MASTG (MASVS-RESILIENCE-2
|
|
2
|
+
// app integrity); no code copied.
|
|
3
|
+
|
|
4
|
+
#import "DITamperChecks.h"
|
|
5
|
+
|
|
6
|
+
#import <Security/Security.h>
|
|
7
|
+
#import <mach-o/dyld.h>
|
|
8
|
+
#import <mach-o/loader.h>
|
|
9
|
+
#import <string.h>
|
|
10
|
+
|
|
11
|
+
@implementation DITamperChecks
|
|
12
|
+
|
|
13
|
+
+ (void)addSignalId:(NSString *)signalId
|
|
14
|
+
category:(NSString *)category
|
|
15
|
+
description:(NSString *)description
|
|
16
|
+
signals:(NSMutableArray<NSDictionary *> *)signals
|
|
17
|
+
seenIds:(NSMutableSet<NSString *> *)seenIds
|
|
18
|
+
{
|
|
19
|
+
if ([seenIds containsObject:signalId]) {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
[seenIds addObject:signalId];
|
|
23
|
+
[signals addObject:@{
|
|
24
|
+
@"id" : signalId,
|
|
25
|
+
@"category" : category,
|
|
26
|
+
@"description" : description,
|
|
27
|
+
}];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
+ (void)runWithConfig:(NSDictionary *)config
|
|
31
|
+
signals:(NSMutableArray<NSDictionary *> *)signals
|
|
32
|
+
seenIds:(NSMutableSet<NSString *> *)seenIds
|
|
33
|
+
completed:(BOOL *)completed
|
|
34
|
+
{
|
|
35
|
+
if (config == nil || config.count == 0) {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
#if TARGET_OS_SIMULATOR
|
|
40
|
+
// Simulator: App Store encryption and Team ID from keychain access groups
|
|
41
|
+
// are not meaningful on the host Mac — skip by design (not incomplete).
|
|
42
|
+
(void)signals;
|
|
43
|
+
(void)seenIds;
|
|
44
|
+
(void)completed;
|
|
45
|
+
return;
|
|
46
|
+
#else
|
|
47
|
+
|
|
48
|
+
NSArray *expectedTeamIds = config[@"expectedTeamIds"];
|
|
49
|
+
if ([expectedTeamIds isKindOfClass:[NSArray class]] && expectedTeamIds.count == 0) {
|
|
50
|
+
// Configured without a usable Team ID: the check cannot run.
|
|
51
|
+
if (completed != NULL) {
|
|
52
|
+
*completed = NO;
|
|
53
|
+
}
|
|
54
|
+
} else if ([expectedTeamIds isKindOfClass:[NSArray class]]) {
|
|
55
|
+
@try {
|
|
56
|
+
BOOL couldNotRun = NO;
|
|
57
|
+
NSString *teamId = [self currentTeamIdCouldNotRun:&couldNotRun];
|
|
58
|
+
if (couldNotRun) {
|
|
59
|
+
if (completed != NULL) {
|
|
60
|
+
*completed = NO;
|
|
61
|
+
}
|
|
62
|
+
} else if (teamId == nil || ![self teamId:teamId matchesExpected:expectedTeamIds]) {
|
|
63
|
+
[self addSignalId:@"tamper_team_id_mismatch"
|
|
64
|
+
category:@"tamper"
|
|
65
|
+
description:@"The app Team ID does not match an expected value"
|
|
66
|
+
signals:signals
|
|
67
|
+
seenIds:seenIds];
|
|
68
|
+
}
|
|
69
|
+
} @catch (__unused NSException *exception) {
|
|
70
|
+
if (completed != NULL) {
|
|
71
|
+
*completed = NO;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
NSNumber *requireEncrypted = config[@"requireEncryptedBinary"];
|
|
77
|
+
if ([requireEncrypted isKindOfClass:[NSNumber class]] && [requireEncrypted boolValue]) {
|
|
78
|
+
@try {
|
|
79
|
+
if ([self isMainExecutableDecryptedOrMissingEncryption]) {
|
|
80
|
+
[self addSignalId:@"tamper_binary_decrypted"
|
|
81
|
+
category:@"tamper"
|
|
82
|
+
description:@"The main executable does not appear to be encrypted"
|
|
83
|
+
signals:signals
|
|
84
|
+
seenIds:seenIds];
|
|
85
|
+
}
|
|
86
|
+
} @catch (__unused NSException *exception) {
|
|
87
|
+
if (completed != NULL) {
|
|
88
|
+
*completed = NO;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
#endif
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
#pragma mark - Team ID (keychain access group)
|
|
96
|
+
|
|
97
|
+
/*
|
|
98
|
+
* Public-API Team ID lookup: a Keychain generic-password item's
|
|
99
|
+
* kSecAttrAccessGroup is of the form "TEAMID.<app-id>". Adding (if needed)
|
|
100
|
+
* and querying such an item, then reading that attribute, yields the Team
|
|
101
|
+
* ID prefix without private APIs. Keychain APIs are not on Apple's
|
|
102
|
+
* required-reason API list. The temporary item is deleted after the query.
|
|
103
|
+
*/
|
|
104
|
+
+ (nullable NSString *)currentTeamIdCouldNotRun:(BOOL *)couldNotRun
|
|
105
|
+
{
|
|
106
|
+
if (couldNotRun != NULL) {
|
|
107
|
+
*couldNotRun = NO;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
NSString *bundleId = [[NSBundle mainBundle] bundleIdentifier];
|
|
111
|
+
if (bundleId.length == 0) {
|
|
112
|
+
bundleId = @"unknown";
|
|
113
|
+
}
|
|
114
|
+
NSString *service =
|
|
115
|
+
[NSString stringWithFormat:@"%@.deviceintegrity.teamid", bundleId];
|
|
116
|
+
|
|
117
|
+
NSDictionary *query = @{
|
|
118
|
+
(__bridge id)kSecClass : (__bridge id)kSecClassGenericPassword,
|
|
119
|
+
(__bridge id)kSecAttrService : service,
|
|
120
|
+
(__bridge id)kSecReturnAttributes : @YES,
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
CFTypeRef result = NULL;
|
|
124
|
+
OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)query, &result);
|
|
125
|
+
|
|
126
|
+
if (status == errSecItemNotFound) {
|
|
127
|
+
NSDictionary *add = @{
|
|
128
|
+
(__bridge id)kSecClass : (__bridge id)kSecClassGenericPassword,
|
|
129
|
+
(__bridge id)kSecAttrService : service,
|
|
130
|
+
(__bridge id)kSecAttrAccessible :
|
|
131
|
+
(__bridge id)kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly,
|
|
132
|
+
(__bridge id)kSecValueData : [@"di" dataUsingEncoding:NSUTF8StringEncoding],
|
|
133
|
+
};
|
|
134
|
+
status = SecItemAdd((__bridge CFDictionaryRef)add, NULL);
|
|
135
|
+
if (status != errSecSuccess) {
|
|
136
|
+
if (couldNotRun != NULL) {
|
|
137
|
+
*couldNotRun = YES;
|
|
138
|
+
}
|
|
139
|
+
return nil;
|
|
140
|
+
}
|
|
141
|
+
result = NULL;
|
|
142
|
+
status = SecItemCopyMatching((__bridge CFDictionaryRef)query, &result);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (status != errSecSuccess || result == NULL) {
|
|
146
|
+
if (couldNotRun != NULL) {
|
|
147
|
+
*couldNotRun = YES;
|
|
148
|
+
}
|
|
149
|
+
[self deleteTeamIdItemWithService:service];
|
|
150
|
+
return nil;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
NSDictionary *attrs = (__bridge_transfer NSDictionary *)result;
|
|
154
|
+
NSString *accessGroup = attrs[(__bridge id)kSecAttrAccessGroup];
|
|
155
|
+
|
|
156
|
+
[self deleteTeamIdItemWithService:service];
|
|
157
|
+
|
|
158
|
+
if (![accessGroup isKindOfClass:[NSString class]] || accessGroup.length == 0) {
|
|
159
|
+
if (couldNotRun != NULL) {
|
|
160
|
+
*couldNotRun = YES;
|
|
161
|
+
}
|
|
162
|
+
return nil;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
NSRange dot = [accessGroup rangeOfString:@"."];
|
|
166
|
+
if (dot.location == NSNotFound || dot.location == 0) {
|
|
167
|
+
if (couldNotRun != NULL) {
|
|
168
|
+
*couldNotRun = YES;
|
|
169
|
+
}
|
|
170
|
+
return nil;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
NSString *prefix = [[accessGroup substringToIndex:dot.location]
|
|
174
|
+
stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
|
|
175
|
+
if (prefix.length == 0) {
|
|
176
|
+
if (couldNotRun != NULL) {
|
|
177
|
+
*couldNotRun = YES;
|
|
178
|
+
}
|
|
179
|
+
return nil;
|
|
180
|
+
}
|
|
181
|
+
return prefix;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
+ (void)deleteTeamIdItemWithService:(NSString *)service
|
|
185
|
+
{
|
|
186
|
+
NSDictionary *del = @{
|
|
187
|
+
(__bridge id)kSecClass : (__bridge id)kSecClassGenericPassword,
|
|
188
|
+
(__bridge id)kSecAttrService : service,
|
|
189
|
+
};
|
|
190
|
+
SecItemDelete((__bridge CFDictionaryRef)del);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
+ (BOOL)teamId:(NSString *)teamId matchesExpected:(NSArray *)expectedTeamIds
|
|
194
|
+
{
|
|
195
|
+
for (id raw in expectedTeamIds) {
|
|
196
|
+
if (![raw isKindOfClass:[NSString class]]) {
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
NSString *expected = [(NSString *)raw
|
|
200
|
+
stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
|
|
201
|
+
if (expected.length == 0) {
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
// Team IDs are uppercase alphanumerics; compare case-sensitively.
|
|
205
|
+
if ([teamId isEqualToString:expected]) {
|
|
206
|
+
return YES;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return NO;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
#pragma mark - Binary encryption (LC_ENCRYPTION_INFO)
|
|
213
|
+
|
|
214
|
+
+ (const struct mach_header *)mainExecutableHeader
|
|
215
|
+
{
|
|
216
|
+
NSString *execPath = [[NSBundle mainBundle] executablePath];
|
|
217
|
+
const char *execPathC = execPath.fileSystemRepresentation;
|
|
218
|
+
|
|
219
|
+
// Prefer image 0 when its path matches the main bundle executable.
|
|
220
|
+
const char *image0Name = _dyld_get_image_name(0);
|
|
221
|
+
if (image0Name != NULL && execPathC != NULL && strcmp(image0Name, execPathC) == 0) {
|
|
222
|
+
return _dyld_get_image_header(0);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Otherwise find the MH_EXECUTE image (or path match).
|
|
226
|
+
uint32_t count = _dyld_image_count();
|
|
227
|
+
for (uint32_t i = 0; i < count; i++) {
|
|
228
|
+
const struct mach_header *header = _dyld_get_image_header(i);
|
|
229
|
+
if (header == NULL) {
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
if (header->filetype == MH_EXECUTE) {
|
|
233
|
+
return header;
|
|
234
|
+
}
|
|
235
|
+
const char *name = _dyld_get_image_name(i);
|
|
236
|
+
if (name != NULL && execPathC != NULL && strcmp(name, execPathC) == 0) {
|
|
237
|
+
return header;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Last resort: image 0 is conventionally the main executable.
|
|
242
|
+
return _dyld_get_image_header(0);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
+ (BOOL)isMainExecutableDecryptedOrMissingEncryption
|
|
246
|
+
{
|
|
247
|
+
const struct mach_header *header = [self mainExecutableHeader];
|
|
248
|
+
if (header == NULL) {
|
|
249
|
+
// Cannot inspect — treat as incomplete via exception path; return NO here
|
|
250
|
+
// and let the caller mark incomplete only on throw. Missing header means
|
|
251
|
+
// we could not run the check.
|
|
252
|
+
@throw [NSException exceptionWithName:@"DITamperChecksException"
|
|
253
|
+
reason:@"main executable header unavailable"
|
|
254
|
+
userInfo:nil];
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
BOOL is64 = (header->magic == MH_MAGIC_64 || header->magic == MH_CIGAM_64);
|
|
258
|
+
BOOL is32 = (header->magic == MH_MAGIC || header->magic == MH_CIGAM);
|
|
259
|
+
if (!is64 && !is32) {
|
|
260
|
+
@throw [NSException exceptionWithName:@"DITamperChecksException"
|
|
261
|
+
reason:@"unrecognized mach-o magic"
|
|
262
|
+
userInfo:nil];
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
uint32_t ncmds = 0;
|
|
266
|
+
const struct load_command *cmd = NULL;
|
|
267
|
+
|
|
268
|
+
if (is64) {
|
|
269
|
+
const struct mach_header_64 *h64 = (const struct mach_header_64 *)header;
|
|
270
|
+
ncmds = h64->ncmds;
|
|
271
|
+
cmd = (const struct load_command *)(h64 + 1);
|
|
272
|
+
} else {
|
|
273
|
+
ncmds = header->ncmds;
|
|
274
|
+
cmd = (const struct load_command *)(header + 1);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
BOOL found = NO;
|
|
278
|
+
for (uint32_t i = 0; i < ncmds; i++) {
|
|
279
|
+
if (cmd->cmdsize == 0) {
|
|
280
|
+
break;
|
|
281
|
+
}
|
|
282
|
+
if (cmd->cmd == LC_ENCRYPTION_INFO_64) {
|
|
283
|
+
const struct encryption_info_command_64 *enc =
|
|
284
|
+
(const struct encryption_info_command_64 *)cmd;
|
|
285
|
+
found = YES;
|
|
286
|
+
if (enc->cryptid == 0) {
|
|
287
|
+
return YES;
|
|
288
|
+
}
|
|
289
|
+
} else if (cmd->cmd == LC_ENCRYPTION_INFO) {
|
|
290
|
+
const struct encryption_info_command *enc =
|
|
291
|
+
(const struct encryption_info_command *)cmd;
|
|
292
|
+
found = YES;
|
|
293
|
+
if (enc->cryptid == 0) {
|
|
294
|
+
return YES;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
cmd = (const struct load_command *)((const uint8_t *)cmd + cmd->cmdsize);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Missing encryption load command ⇒ treat as decrypted / not encrypted.
|
|
301
|
+
return !found;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
@end
|
package/ios/DeviceIntegrity.mm
CHANGED
|
@@ -14,13 +14,65 @@
|
|
|
14
14
|
return @"DeviceIntegrity";
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
/**
|
|
18
|
+
* Defensively parse options[@"ios"]. Wrong types / empty arrays ⇒ not
|
|
19
|
+
* configured (check does not run, report is not incomplete).
|
|
20
|
+
*/
|
|
21
|
+
static NSDictionary *DIParseIOSConfig(NSDictionary *options)
|
|
22
|
+
{
|
|
23
|
+
if (![options isKindOfClass:[NSDictionary class]]) {
|
|
24
|
+
return nil;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
id iosRaw = options[@"ios"];
|
|
28
|
+
if (![iosRaw isKindOfClass:[NSDictionary class]]) {
|
|
29
|
+
return nil;
|
|
30
|
+
}
|
|
31
|
+
NSDictionary *ios = (NSDictionary *)iosRaw;
|
|
32
|
+
NSMutableDictionary *config = [NSMutableDictionary dictionary];
|
|
33
|
+
|
|
34
|
+
id teamIdsRaw = ios[@"expectedTeamIds"];
|
|
35
|
+
if ([teamIdsRaw isKindOfClass:[NSArray class]]) {
|
|
36
|
+
NSMutableArray<NSString *> *valid = [NSMutableArray array];
|
|
37
|
+
for (id item in (NSArray *)teamIdsRaw) {
|
|
38
|
+
if (![item isKindOfClass:[NSString class]]) {
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
NSString *trimmed = [(NSString *)item
|
|
42
|
+
stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
|
|
43
|
+
if (trimmed.length > 0) {
|
|
44
|
+
[valid addObject:trimmed];
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
// Keep an empty list when values were given but none are usable, so the
|
|
48
|
+
// check reports incomplete instead of silently not running.
|
|
49
|
+
if (valid.count > 0 || ((NSArray *)teamIdsRaw).count > 0) {
|
|
50
|
+
config[@"expectedTeamIds"] = [valid copy];
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
id requireEncRaw = ios[@"requireEncryptedBinary"];
|
|
55
|
+
if ([requireEncRaw isKindOfClass:[NSNumber class]] &&
|
|
56
|
+
[(NSNumber *)requireEncRaw boolValue]) {
|
|
57
|
+
config[@"requireEncryptedBinary"] = @YES;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (config.count == 0) {
|
|
61
|
+
return nil;
|
|
62
|
+
}
|
|
63
|
+
return [config copy];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
- (void)checkIntegrity:(NSDictionary *)options
|
|
67
|
+
resolve:(RCTPromiseResolveBlock)resolve
|
|
18
68
|
reject:(RCTPromiseRejectBlock)reject
|
|
19
69
|
{
|
|
20
70
|
@try {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
71
|
+
NSDictionary *iosConfig = DIParseIOSConfig(options);
|
|
72
|
+
[DIIntegrityChecks runWithIOSConfig:iosConfig
|
|
73
|
+
completion:^(NSDictionary *report) {
|
|
74
|
+
resolve(report);
|
|
75
|
+
}];
|
|
24
76
|
} @catch (NSException *exception) {
|
|
25
77
|
reject(@"E_INTEGRITY", exception.reason ?: @"Unexpected integrity check failure", nil);
|
|
26
78
|
}
|
package/jest/index.d.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
CheckIntegrityOptions,
|
|
3
|
+
IntegrityResult,
|
|
4
|
+
IntegrityStatus,
|
|
5
|
+
Signal,
|
|
6
|
+
SignalId,
|
|
7
|
+
} from '../src/types';
|
|
8
|
+
|
|
9
|
+
export type {
|
|
10
|
+
CheckIntegrityOptions,
|
|
11
|
+
IntegrityResult,
|
|
12
|
+
IntegrityStatus,
|
|
13
|
+
Signal,
|
|
14
|
+
SignalId,
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export declare const SIGNAL_IDS: readonly SignalId[];
|
|
18
|
+
|
|
19
|
+
export type UseDeviceIntegrityResult = {
|
|
20
|
+
status: IntegrityStatus;
|
|
21
|
+
signals: Signal[];
|
|
22
|
+
result: IntegrityResult;
|
|
23
|
+
loading: false;
|
|
24
|
+
refresh: () => Promise<IntegrityResult>;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export declare function checkIntegrity(
|
|
28
|
+
options?: CheckIntegrityOptions
|
|
29
|
+
): Promise<IntegrityResult>;
|
|
30
|
+
|
|
31
|
+
export declare function useDeviceIntegrity(
|
|
32
|
+
options?: CheckIntegrityOptions
|
|
33
|
+
): UseDeviceIntegrityResult;
|
|
34
|
+
|
|
35
|
+
export declare function setMockIntegrityResult(
|
|
36
|
+
partial: Partial<IntegrityResult>
|
|
37
|
+
): void;
|
|
38
|
+
|
|
39
|
+
export declare function resetMockIntegrity(): void;
|
package/jest/index.js
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Jest mock for `react-native-device-integrity`.
|
|
5
|
+
*
|
|
6
|
+
* Usage in a test file:
|
|
7
|
+
*
|
|
8
|
+
* jest.mock(
|
|
9
|
+
* 'react-native-device-integrity',
|
|
10
|
+
* () => require('react-native-device-integrity/jest')
|
|
11
|
+
* );
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const SIGNAL_IDS = Object.freeze([
|
|
15
|
+
'simulator',
|
|
16
|
+
'jailbreak_files',
|
|
17
|
+
'jailbreak_url_schemes',
|
|
18
|
+
'jailbreak_symlinks',
|
|
19
|
+
'jailbreak_writable_system',
|
|
20
|
+
'hooking_libraries',
|
|
21
|
+
'hooking_dyld_insert',
|
|
22
|
+
'debugger_attached',
|
|
23
|
+
'emulator',
|
|
24
|
+
'root_su_binary',
|
|
25
|
+
'root_management_apps',
|
|
26
|
+
'root_magisk_files',
|
|
27
|
+
'root_test_keys',
|
|
28
|
+
'root_dangerous_props',
|
|
29
|
+
'root_rw_system',
|
|
30
|
+
'hooking_frida',
|
|
31
|
+
'hooking_xposed',
|
|
32
|
+
'tamper_signature_mismatch',
|
|
33
|
+
'tamper_untrusted_installer',
|
|
34
|
+
'tamper_binary_decrypted',
|
|
35
|
+
'tamper_team_id_mismatch',
|
|
36
|
+
]);
|
|
37
|
+
|
|
38
|
+
function defaultResult() {
|
|
39
|
+
return {
|
|
40
|
+
status: 'clean',
|
|
41
|
+
signals: [],
|
|
42
|
+
ignored: [],
|
|
43
|
+
platform: 'ios',
|
|
44
|
+
durationMs: 0,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
let mockResult = defaultResult();
|
|
49
|
+
|
|
50
|
+
function cloneResult(result) {
|
|
51
|
+
return {
|
|
52
|
+
...result,
|
|
53
|
+
signals: Array.isArray(result.signals) ? result.signals.slice() : [],
|
|
54
|
+
ignored: Array.isArray(result.ignored) ? result.ignored.slice() : [],
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function setMockIntegrityResult(partial) {
|
|
59
|
+
mockResult = {
|
|
60
|
+
...mockResult,
|
|
61
|
+
...partial,
|
|
62
|
+
signals:
|
|
63
|
+
partial && Object.prototype.hasOwnProperty.call(partial, 'signals')
|
|
64
|
+
? partial.signals
|
|
65
|
+
: mockResult.signals,
|
|
66
|
+
ignored:
|
|
67
|
+
partial && Object.prototype.hasOwnProperty.call(partial, 'ignored')
|
|
68
|
+
? partial.ignored
|
|
69
|
+
: mockResult.ignored,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function resetMockIntegrity() {
|
|
74
|
+
mockResult = defaultResult();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function checkIntegrity() {
|
|
78
|
+
return Promise.resolve(cloneResult(mockResult));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function useDeviceIntegrity() {
|
|
82
|
+
const result = cloneResult(mockResult);
|
|
83
|
+
return {
|
|
84
|
+
status: result.status,
|
|
85
|
+
signals: result.signals,
|
|
86
|
+
result,
|
|
87
|
+
loading: false,
|
|
88
|
+
refresh: function refresh() {
|
|
89
|
+
return checkIntegrity();
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
module.exports = {
|
|
95
|
+
SIGNAL_IDS,
|
|
96
|
+
checkIntegrity,
|
|
97
|
+
useDeviceIntegrity,
|
|
98
|
+
setMockIntegrityResult,
|
|
99
|
+
resetMockIntegrity,
|
|
100
|
+
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"names":["TurboModuleRegistry","get"],"sourceRoot":"../../src","sources":["NativeDeviceIntegrity.ts"],"mappings":";;
|
|
1
|
+
{"version":3,"names":["TurboModuleRegistry","get"],"sourceRoot":"../../src","sources":["NativeDeviceIntegrity.ts"],"mappings":";;AACA,SAASA,mBAAmB,QAAQ,cAAc;AAoBlD,eAAeA,mBAAmB,CAACC,GAAG,CAAO,iBAAiB,CAAC","ignoreList":[]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"names":["Platform","checkIntegrity","_options","status","signals","platform","OS","reason"],"sourceRoot":"../../src","sources":["checkIntegrity.ts"],"mappings":";;AAAA,SAASA,QAAQ,QAAQ,cAAc;AAGvC;AACA;AACA;AACA,OAAO,eAAeC,cAAcA,CAClCC,QAAgC,EACN;EAC1B,OAAO;IACLC,MAAM,EAAE,SAAS;IACjBC,OAAO,EAAE,EAAE;IACXC,QAAQ,
|
|
1
|
+
{"version":3,"names":["Platform","checkIntegrity","_options","status","signals","ignored","durationMs","platform","OS","reason"],"sourceRoot":"../../src","sources":["checkIntegrity.ts"],"mappings":";;AAAA,SAASA,QAAQ,QAAQ,cAAc;AAGvC;AACA;AACA;AACA,OAAO,eAAeC,cAAcA,CAClCC,QAAgC,EACN;EAC1B,OAAO;IACLC,MAAM,EAAE,SAAS;IACjBC,OAAO,EAAE,EAAE;IACXC,OAAO,EAAE,EAAE;IACXC,UAAU,EAAE,CAAC;IACbC,QAAQ,EAAEP,QAAQ,CAACQ,EAAE;IACrBC,MAAM,EAAE;EACV,CAAC;AACH","ignoreList":[]}
|
|
@@ -10,6 +10,13 @@ class IntegrityTimeoutError extends Error {
|
|
|
10
10
|
this.name = 'IntegrityTimeoutError';
|
|
11
11
|
}
|
|
12
12
|
}
|
|
13
|
+
function nowMs() {
|
|
14
|
+
const perf = globalThis.performance;
|
|
15
|
+
if (perf != null && typeof perf.now === 'function') {
|
|
16
|
+
return perf.now();
|
|
17
|
+
}
|
|
18
|
+
return Date.now();
|
|
19
|
+
}
|
|
13
20
|
function withTimeout(promise, timeoutMs) {
|
|
14
21
|
return new Promise((resolve, reject) => {
|
|
15
22
|
const timer = setTimeout(() => {
|
|
@@ -24,11 +31,73 @@ function withTimeout(promise, timeoutMs) {
|
|
|
24
31
|
});
|
|
25
32
|
});
|
|
26
33
|
}
|
|
34
|
+
|
|
35
|
+
// A blank entry makes native treat the check as configured but unusable,
|
|
36
|
+
// so it reports the run as incomplete instead of skipping the check.
|
|
37
|
+
const UNUSABLE_LIST = [''];
|
|
38
|
+
function stringArray(value) {
|
|
39
|
+
if (value == null) {
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
if (!Array.isArray(value)) {
|
|
43
|
+
return UNUSABLE_LIST;
|
|
44
|
+
}
|
|
45
|
+
const strings = value.filter(item => typeof item === 'string');
|
|
46
|
+
if (value.length > 0 && strings.length === 0) {
|
|
47
|
+
return UNUSABLE_LIST;
|
|
48
|
+
}
|
|
49
|
+
return strings;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Build the native options bag: only `android` / `ios`, with sanitized fields.
|
|
54
|
+
* Missing fields are omitted. A list that is provided but unusable (wrong type,
|
|
55
|
+
* no strings) is sent as a blank entry so the check fails closed as incomplete.
|
|
56
|
+
*/
|
|
57
|
+
export function sanitizeNativeOptions(options) {
|
|
58
|
+
const native = {};
|
|
59
|
+
if (options.android != null && typeof options.android === 'object') {
|
|
60
|
+
const android = {};
|
|
61
|
+
const certs = stringArray(options.android.expectedSigningCertificates);
|
|
62
|
+
if (certs != null) {
|
|
63
|
+
android.expectedSigningCertificates = certs;
|
|
64
|
+
}
|
|
65
|
+
const installers = stringArray(options.android.allowedInstallers);
|
|
66
|
+
if (installers != null) {
|
|
67
|
+
android.allowedInstallers = installers;
|
|
68
|
+
}
|
|
69
|
+
if (Object.keys(android).length > 0) {
|
|
70
|
+
native.android = android;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
if (options.ios != null && typeof options.ios === 'object') {
|
|
74
|
+
const ios = {};
|
|
75
|
+
const teamIds = stringArray(options.ios.expectedTeamIds);
|
|
76
|
+
if (teamIds != null) {
|
|
77
|
+
ios.expectedTeamIds = teamIds;
|
|
78
|
+
}
|
|
79
|
+
if (typeof options.ios.requireEncryptedBinary === 'boolean') {
|
|
80
|
+
ios.requireEncryptedBinary = options.ios.requireEncryptedBinary;
|
|
81
|
+
}
|
|
82
|
+
if (Object.keys(ios).length > 0) {
|
|
83
|
+
native.ios = ios;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return native;
|
|
87
|
+
}
|
|
88
|
+
function withDuration(result, startedAt) {
|
|
89
|
+
return {
|
|
90
|
+
...result,
|
|
91
|
+
durationMs: Math.max(0, nowMs() - startedAt)
|
|
92
|
+
};
|
|
93
|
+
}
|
|
27
94
|
export async function checkIntegrity(options = {}) {
|
|
28
95
|
if (Platform.OS !== 'ios' && Platform.OS !== 'android') {
|
|
29
96
|
return {
|
|
30
97
|
status: 'unknown',
|
|
31
98
|
signals: [],
|
|
99
|
+
ignored: [],
|
|
100
|
+
durationMs: 0,
|
|
32
101
|
platform: Platform.OS,
|
|
33
102
|
reason: 'unsupported_platform'
|
|
34
103
|
};
|
|
@@ -37,31 +106,39 @@ export async function checkIntegrity(options = {}) {
|
|
|
37
106
|
return {
|
|
38
107
|
status: 'unknown',
|
|
39
108
|
signals: [],
|
|
109
|
+
ignored: [],
|
|
110
|
+
durationMs: 0,
|
|
40
111
|
platform: Platform.OS,
|
|
41
112
|
reason: 'native_module_unavailable'
|
|
42
113
|
};
|
|
43
114
|
}
|
|
44
115
|
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
116
|
+
const startedAt = nowMs();
|
|
117
|
+
const nativeOptions = sanitizeNativeOptions(options);
|
|
45
118
|
try {
|
|
46
|
-
const report = await withTimeout(NativeDeviceIntegrity.checkIntegrity(), timeoutMs);
|
|
47
|
-
return resolveStatus(report, options, Platform.OS);
|
|
119
|
+
const report = await withTimeout(NativeDeviceIntegrity.checkIntegrity(nativeOptions), timeoutMs);
|
|
120
|
+
return withDuration(resolveStatus(report, options, Platform.OS), startedAt);
|
|
48
121
|
} catch (error) {
|
|
49
122
|
if (error instanceof IntegrityTimeoutError) {
|
|
50
|
-
return {
|
|
123
|
+
return withDuration({
|
|
51
124
|
status: 'unknown',
|
|
52
125
|
signals: [],
|
|
126
|
+
ignored: [],
|
|
127
|
+
durationMs: 0,
|
|
53
128
|
platform: Platform.OS,
|
|
54
129
|
reason: 'timeout'
|
|
55
|
-
};
|
|
130
|
+
}, startedAt);
|
|
56
131
|
}
|
|
57
132
|
const message = error instanceof Error ? error.message : typeof error === 'string' ? error : 'Native integrity check failed';
|
|
58
|
-
return {
|
|
133
|
+
return withDuration({
|
|
59
134
|
status: 'unknown',
|
|
60
135
|
signals: [],
|
|
136
|
+
ignored: [],
|
|
137
|
+
durationMs: 0,
|
|
61
138
|
platform: Platform.OS,
|
|
62
139
|
reason: 'native_error',
|
|
63
140
|
error: message
|
|
64
|
-
};
|
|
141
|
+
}, startedAt);
|
|
65
142
|
}
|
|
66
143
|
}
|
|
67
144
|
//# sourceMappingURL=checkIntegrity.native.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"names":["Platform","NativeDeviceIntegrity","resolveStatus","DEFAULT_TIMEOUT_MS","IntegrityTimeoutError","Error","constructor","name","withTimeout","promise","timeoutMs","Promise","resolve","reject","timer","setTimeout","then","value","clearTimeout","error","
|
|
1
|
+
{"version":3,"names":["Platform","NativeDeviceIntegrity","resolveStatus","DEFAULT_TIMEOUT_MS","IntegrityTimeoutError","Error","constructor","name","nowMs","perf","globalThis","performance","now","Date","withTimeout","promise","timeoutMs","Promise","resolve","reject","timer","setTimeout","then","value","clearTimeout","error","UNUSABLE_LIST","stringArray","undefined","Array","isArray","strings","filter","item","length","sanitizeNativeOptions","options","native","android","certs","expectedSigningCertificates","installers","allowedInstallers","Object","keys","ios","teamIds","expectedTeamIds","requireEncryptedBinary","withDuration","result","startedAt","durationMs","Math","max","checkIntegrity","OS","status","signals","ignored","platform","reason","nativeOptions","report","message"],"sourceRoot":"../../src","sources":["checkIntegrity.native.ts"],"mappings":"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","ignoreList":[]}
|