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
package/src/resolveStatus.ts
CHANGED
|
@@ -4,6 +4,7 @@ import type {
|
|
|
4
4
|
IntegrityResult,
|
|
5
5
|
Signal,
|
|
6
6
|
SignalCategory,
|
|
7
|
+
SignalId,
|
|
7
8
|
UnknownReason,
|
|
8
9
|
} from './types';
|
|
9
10
|
|
|
@@ -17,6 +18,16 @@ const SIGNAL_CATEGORIES: ReadonlySet<string> = new Set([
|
|
|
17
18
|
'environment',
|
|
18
19
|
]);
|
|
19
20
|
|
|
21
|
+
/** Default compromising set: every category except emulator. */
|
|
22
|
+
const DEFAULT_COMPROMISING_CATEGORIES: readonly SignalCategory[] = [
|
|
23
|
+
'jailbreak',
|
|
24
|
+
'root',
|
|
25
|
+
'hooking',
|
|
26
|
+
'debugger',
|
|
27
|
+
'tamper',
|
|
28
|
+
'environment',
|
|
29
|
+
];
|
|
30
|
+
|
|
20
31
|
function coerceCategory(category: unknown): SignalCategory {
|
|
21
32
|
if (typeof category === 'string' && SIGNAL_CATEGORIES.has(category)) {
|
|
22
33
|
return category as SignalCategory;
|
|
@@ -48,6 +59,46 @@ function normalizeSignal(raw: unknown): Signal {
|
|
|
48
59
|
};
|
|
49
60
|
}
|
|
50
61
|
|
|
62
|
+
function resolveCompromisingCategories(
|
|
63
|
+
options: CheckIntegrityOptions
|
|
64
|
+
): ReadonlySet<SignalCategory> {
|
|
65
|
+
const base =
|
|
66
|
+
options.policy?.compromisingCategories != null
|
|
67
|
+
? options.policy.compromisingCategories
|
|
68
|
+
: DEFAULT_COMPROMISING_CATEGORIES;
|
|
69
|
+
|
|
70
|
+
const set = new Set<SignalCategory>(base);
|
|
71
|
+
|
|
72
|
+
if (options.treatEmulatorAsCompromised === true) {
|
|
73
|
+
set.add('emulator');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return set;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function partitionIgnored(
|
|
80
|
+
signals: Signal[],
|
|
81
|
+
ignore: SignalId[] | undefined
|
|
82
|
+
): { active: Signal[]; ignored: Signal[] } {
|
|
83
|
+
if (ignore == null || ignore.length === 0) {
|
|
84
|
+
return { active: signals, ignored: [] };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const ignoreSet = new Set<string>(ignore);
|
|
88
|
+
const active: Signal[] = [];
|
|
89
|
+
const ignored: Signal[] = [];
|
|
90
|
+
|
|
91
|
+
for (const signal of signals) {
|
|
92
|
+
if (ignoreSet.has(signal.id)) {
|
|
93
|
+
ignored.push(signal);
|
|
94
|
+
} else {
|
|
95
|
+
active.push(signal);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return { active, ignored };
|
|
100
|
+
}
|
|
101
|
+
|
|
51
102
|
function malformed(
|
|
52
103
|
platform: typeof Platform.OS,
|
|
53
104
|
error: string
|
|
@@ -55,6 +106,8 @@ function malformed(
|
|
|
55
106
|
return {
|
|
56
107
|
status: 'unknown',
|
|
57
108
|
signals: [],
|
|
109
|
+
ignored: [],
|
|
110
|
+
durationMs: 0,
|
|
58
111
|
platform,
|
|
59
112
|
reason: 'native_error',
|
|
60
113
|
error,
|
|
@@ -64,6 +117,7 @@ function malformed(
|
|
|
64
117
|
/**
|
|
65
118
|
* Maps a native integrity report + options to the public IntegrityResult.
|
|
66
119
|
* Compromise evidence wins over incomplete runs; incomplete/malformed never become 'clean'.
|
|
120
|
+
* `durationMs` is set to 0 here — `checkIntegrity` overwrites with measured wall time.
|
|
67
121
|
*/
|
|
68
122
|
export function resolveStatus(
|
|
69
123
|
report: unknown,
|
|
@@ -80,25 +134,24 @@ export function resolveStatus(
|
|
|
80
134
|
return malformed(platform, 'Malformed native integrity report');
|
|
81
135
|
}
|
|
82
136
|
|
|
83
|
-
const
|
|
137
|
+
const allSignals = nativeReport.signals.map(normalizeSignal);
|
|
138
|
+
const { active: signals, ignored } = partitionIgnored(
|
|
139
|
+
allSignals,
|
|
140
|
+
options.ignore
|
|
141
|
+
);
|
|
84
142
|
const completed = nativeReport.completed === true;
|
|
85
|
-
const
|
|
86
|
-
options.treatEmulatorAsCompromised === true;
|
|
143
|
+
const compromising = resolveCompromisingCategories(options);
|
|
87
144
|
|
|
88
|
-
const
|
|
89
|
-
(signal
|
|
90
|
-
);
|
|
91
|
-
const hasEmulatorSignal = signals.some(
|
|
92
|
-
(signal) => signal.category === 'emulator'
|
|
145
|
+
const hasCompromisingSignal = signals.some((signal) =>
|
|
146
|
+
compromising.has(signal.category)
|
|
93
147
|
);
|
|
94
148
|
|
|
95
|
-
if (
|
|
96
|
-
hasNonEmulatorSignal ||
|
|
97
|
-
(hasEmulatorSignal && treatEmulatorAsCompromised)
|
|
98
|
-
) {
|
|
149
|
+
if (hasCompromisingSignal) {
|
|
99
150
|
return {
|
|
100
151
|
status: 'compromised',
|
|
101
152
|
signals,
|
|
153
|
+
ignored,
|
|
154
|
+
durationMs: 0,
|
|
102
155
|
platform,
|
|
103
156
|
};
|
|
104
157
|
}
|
|
@@ -112,6 +165,8 @@ export function resolveStatus(
|
|
|
112
165
|
return {
|
|
113
166
|
status: 'unknown',
|
|
114
167
|
signals,
|
|
168
|
+
ignored,
|
|
169
|
+
durationMs: 0,
|
|
115
170
|
platform,
|
|
116
171
|
reason,
|
|
117
172
|
};
|
|
@@ -120,6 +175,8 @@ export function resolveStatus(
|
|
|
120
175
|
return {
|
|
121
176
|
status: 'clean',
|
|
122
177
|
signals,
|
|
178
|
+
ignored,
|
|
179
|
+
durationMs: 0,
|
|
123
180
|
platform,
|
|
124
181
|
};
|
|
125
182
|
}
|
package/src/types.ts
CHANGED
|
@@ -11,8 +11,38 @@ export type SignalCategory =
|
|
|
11
11
|
| 'tamper'
|
|
12
12
|
| 'environment';
|
|
13
13
|
|
|
14
|
+
/**
|
|
15
|
+
* Stable public signal ids. Unknown future ids remain accepted on `Signal.id`.
|
|
16
|
+
*/
|
|
17
|
+
export const SIGNAL_IDS = [
|
|
18
|
+
'simulator',
|
|
19
|
+
'jailbreak_files',
|
|
20
|
+
'jailbreak_url_schemes',
|
|
21
|
+
'jailbreak_symlinks',
|
|
22
|
+
'jailbreak_writable_system',
|
|
23
|
+
'hooking_libraries',
|
|
24
|
+
'hooking_dyld_insert',
|
|
25
|
+
'debugger_attached',
|
|
26
|
+
'emulator',
|
|
27
|
+
'root_su_binary',
|
|
28
|
+
'root_management_apps',
|
|
29
|
+
'root_magisk_files',
|
|
30
|
+
'root_test_keys',
|
|
31
|
+
'root_dangerous_props',
|
|
32
|
+
'root_rw_system',
|
|
33
|
+
'hooking_frida',
|
|
34
|
+
'hooking_xposed',
|
|
35
|
+
'tamper_signature_mismatch',
|
|
36
|
+
'tamper_untrusted_installer',
|
|
37
|
+
'tamper_binary_decrypted',
|
|
38
|
+
'tamper_team_id_mismatch',
|
|
39
|
+
] as const;
|
|
40
|
+
|
|
41
|
+
export type SignalId = (typeof SIGNAL_IDS)[number];
|
|
42
|
+
|
|
14
43
|
export interface Signal {
|
|
15
|
-
|
|
44
|
+
/** Known ids autocomplete; unknown future ids are still accepted. */
|
|
45
|
+
id: SignalId | (string & {});
|
|
16
46
|
category: SignalCategory;
|
|
17
47
|
description: string;
|
|
18
48
|
}
|
|
@@ -28,14 +58,58 @@ export type UnknownReason =
|
|
|
28
58
|
export interface IntegrityResult {
|
|
29
59
|
status: IntegrityStatus;
|
|
30
60
|
signals: Signal[];
|
|
61
|
+
/** Signals removed by `ignore` — never affect status. Always present. */
|
|
62
|
+
ignored: Signal[];
|
|
63
|
+
/** Wall time of the check in JS (monotonic when available). 0 on unsupported platforms. */
|
|
64
|
+
durationMs: number;
|
|
31
65
|
platform: typeof Platform.OS;
|
|
32
66
|
reason?: UnknownReason;
|
|
33
67
|
error?: string;
|
|
34
68
|
}
|
|
35
69
|
|
|
70
|
+
export interface AndroidIntegrityOptions {
|
|
71
|
+
/** SHA-256 digests of expected signing certificates (hex; colons/whitespace optional). */
|
|
72
|
+
expectedSigningCertificates?: string[];
|
|
73
|
+
/** Package names of allowed installers (e.g. `com.android.vending`). */
|
|
74
|
+
allowedInstallers?: string[];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export interface IosIntegrityOptions {
|
|
78
|
+
/** Expected Apple Team IDs (keychain access-group prefix). */
|
|
79
|
+
expectedTeamIds?: string[];
|
|
80
|
+
/** When true, require the main executable to be encrypted (App Store / TestFlight). */
|
|
81
|
+
requireEncryptedBinary?: boolean;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export interface IntegrityPolicy {
|
|
85
|
+
/**
|
|
86
|
+
* Categories that make status `'compromised'`.
|
|
87
|
+
* When omitted, defaults to every category except `'emulator'`.
|
|
88
|
+
*/
|
|
89
|
+
compromisingCategories?: SignalCategory[];
|
|
90
|
+
}
|
|
91
|
+
|
|
36
92
|
export interface CheckIntegrityOptions {
|
|
37
|
-
/**
|
|
93
|
+
/**
|
|
94
|
+
* Treat emulator/simulator signals as compromising. Default false (reported,
|
|
95
|
+
* but status stays 'clean' unless the category is otherwise in the policy set).
|
|
96
|
+
* Shorthand that unions `'emulator'` into the compromising category set.
|
|
97
|
+
*/
|
|
38
98
|
treatEmulatorAsCompromised?: boolean;
|
|
39
99
|
/** Resolve as 'unknown' with reason 'timeout' if native does not answer in time. Default 10000 ms. */
|
|
40
100
|
timeoutMs?: number;
|
|
101
|
+
/**
|
|
102
|
+
* Signal ids to exclude from status resolution. Matching signals move to
|
|
103
|
+
* `result.ignored` and never affect status.
|
|
104
|
+
*/
|
|
105
|
+
ignore?: SignalId[];
|
|
106
|
+
/**
|
|
107
|
+
* Status policy. `compromisingCategories` replaces the default set when
|
|
108
|
+
* provided; `treatEmulatorAsCompromised` is unioned in when both are set.
|
|
109
|
+
*/
|
|
110
|
+
policy?: IntegrityPolicy;
|
|
111
|
+
/** Android-only tamper options forwarded to native when provided. */
|
|
112
|
+
android?: AndroidIntegrityOptions;
|
|
113
|
+
/** iOS-only tamper options forwarded to native when provided. */
|
|
114
|
+
ios?: IosIntegrityOptions;
|
|
41
115
|
}
|
|
@@ -16,11 +16,27 @@ export type UseDeviceIntegrityResult = {
|
|
|
16
16
|
refresh: () => Promise<IntegrityResult>;
|
|
17
17
|
};
|
|
18
18
|
|
|
19
|
+
/**
|
|
20
|
+
* Stable serialization of options that affect the check. Inline object literals
|
|
21
|
+
* with the same values must not retrigger the effect.
|
|
22
|
+
*/
|
|
23
|
+
function optionsKey(options: CheckIntegrityOptions): string {
|
|
24
|
+
return JSON.stringify({
|
|
25
|
+
treatEmulatorAsCompromised: options.treatEmulatorAsCompromised === true,
|
|
26
|
+
timeoutMs: options.timeoutMs,
|
|
27
|
+
ignore: options.ignore,
|
|
28
|
+
policy: options.policy,
|
|
29
|
+
android: options.android,
|
|
30
|
+
ios: options.ios,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
19
34
|
export function useDeviceIntegrity(
|
|
20
35
|
options: CheckIntegrityOptions = {}
|
|
21
36
|
): UseDeviceIntegrityResult {
|
|
22
|
-
const
|
|
23
|
-
const
|
|
37
|
+
const key = optionsKey(options);
|
|
38
|
+
const optionsRef = useRef(options);
|
|
39
|
+
optionsRef.current = options;
|
|
24
40
|
|
|
25
41
|
const [result, setResult] = useState<IntegrityResult | null>(null);
|
|
26
42
|
const [loading, setLoading] = useState(true);
|
|
@@ -33,10 +49,7 @@ export function useDeviceIntegrity(
|
|
|
33
49
|
const requestId = ++requestIdRef.current;
|
|
34
50
|
setLoading(true);
|
|
35
51
|
|
|
36
|
-
const next = await checkIntegrity(
|
|
37
|
-
treatEmulatorAsCompromised,
|
|
38
|
-
timeoutMs,
|
|
39
|
-
});
|
|
52
|
+
const next = await checkIntegrity(optionsRef.current);
|
|
40
53
|
|
|
41
54
|
if (mountedRef.current && requestId === requestIdRef.current) {
|
|
42
55
|
setResult(next);
|
|
@@ -44,7 +57,9 @@ export function useDeviceIntegrity(
|
|
|
44
57
|
}
|
|
45
58
|
|
|
46
59
|
return next;
|
|
47
|
-
|
|
60
|
+
// `key` is the by-value options fingerprint (not used in the body; optionsRef is).
|
|
61
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps -- equal-by-value options
|
|
62
|
+
}, [key]);
|
|
48
63
|
|
|
49
64
|
useEffect(() => {
|
|
50
65
|
mountedRef.current = true;
|