react-native-device-integrity 0.1.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/DeviceIntegrity.podspec +21 -0
- package/LICENSE +20 -0
- package/README.md +286 -0
- package/android/build.gradle +55 -0
- package/android/src/main/AndroidManifest.xml +20 -0
- package/android/src/main/java/com/deviceintegrity/DeviceIntegrityModule.kt +48 -0
- package/android/src/main/java/com/deviceintegrity/DeviceIntegrityPackage.kt +31 -0
- package/android/src/main/java/com/deviceintegrity/IntegrityChecks.kt +454 -0
- package/app.plugin.js +1 -0
- package/ios/DIIntegrityChecks.h +15 -0
- package/ios/DIIntegrityChecks.m +395 -0
- package/ios/DeviceIntegrity.h +5 -0
- package/ios/DeviceIntegrity.mm +29 -0
- package/ios/PrivacyInfo.xcprivacy +14 -0
- package/lib/module/NativeDeviceIntegrity.js +5 -0
- package/lib/module/NativeDeviceIntegrity.js.map +1 -0
- package/lib/module/checkIntegrity.js +15 -0
- package/lib/module/checkIntegrity.js.map +1 -0
- package/lib/module/checkIntegrity.native.js +67 -0
- package/lib/module/checkIntegrity.native.js.map +1 -0
- package/lib/module/index.js +5 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/resolveStatus.js +76 -0
- package/lib/module/resolveStatus.js.map +1 -0
- package/lib/module/types.js +4 -0
- package/lib/module/types.js.map +1 -0
- package/lib/module/useDeviceIntegrity.js +62 -0
- package/lib/module/useDeviceIntegrity.js.map +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/NativeDeviceIntegrity.d.ts +17 -0
- package/lib/typescript/src/NativeDeviceIntegrity.d.ts.map +1 -0
- package/lib/typescript/src/checkIntegrity.d.ts +6 -0
- package/lib/typescript/src/checkIntegrity.d.ts.map +1 -0
- package/lib/typescript/src/checkIntegrity.native.d.ts +3 -0
- package/lib/typescript/src/checkIntegrity.native.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +4 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/lib/typescript/src/resolveStatus.d.ts +8 -0
- package/lib/typescript/src/resolveStatus.d.ts.map +1 -0
- package/lib/typescript/src/types.d.ts +23 -0
- package/lib/typescript/src/types.d.ts.map +1 -0
- package/lib/typescript/src/useDeviceIntegrity.d.ts +10 -0
- package/lib/typescript/src/useDeviceIntegrity.d.ts.map +1 -0
- package/package.json +210 -0
- package/plugin/build/index.d.ts +19 -0
- package/plugin/build/index.js +49 -0
- package/src/NativeDeviceIntegrity.ts +19 -0
- package/src/checkIntegrity.native.ts +88 -0
- package/src/checkIntegrity.ts +16 -0
- package/src/index.tsx +10 -0
- package/src/resolveStatus.ts +125 -0
- package/src/types.ts +41 -0
- package/src/useDeviceIntegrity.ts +92 -0
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import type { Platform } from 'react-native';
|
|
2
|
+
import type {
|
|
3
|
+
CheckIntegrityOptions,
|
|
4
|
+
IntegrityResult,
|
|
5
|
+
Signal,
|
|
6
|
+
SignalCategory,
|
|
7
|
+
UnknownReason,
|
|
8
|
+
} from './types';
|
|
9
|
+
|
|
10
|
+
const SIGNAL_CATEGORIES: ReadonlySet<string> = new Set([
|
|
11
|
+
'jailbreak',
|
|
12
|
+
'root',
|
|
13
|
+
'hooking',
|
|
14
|
+
'debugger',
|
|
15
|
+
'emulator',
|
|
16
|
+
'tamper',
|
|
17
|
+
'environment',
|
|
18
|
+
]);
|
|
19
|
+
|
|
20
|
+
function coerceCategory(category: unknown): SignalCategory {
|
|
21
|
+
if (typeof category === 'string' && SIGNAL_CATEGORIES.has(category)) {
|
|
22
|
+
return category as SignalCategory;
|
|
23
|
+
}
|
|
24
|
+
return 'environment';
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function normalizeSignal(raw: unknown): Signal {
|
|
28
|
+
if (raw === null || typeof raw !== 'object') {
|
|
29
|
+
return {
|
|
30
|
+
id: 'unknown_signal',
|
|
31
|
+
category: 'environment',
|
|
32
|
+
description: '',
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const signal = raw as Record<string, unknown>;
|
|
37
|
+
const id =
|
|
38
|
+
typeof signal.id === 'string' && signal.id.length > 0
|
|
39
|
+
? signal.id
|
|
40
|
+
: 'unknown_signal';
|
|
41
|
+
const description =
|
|
42
|
+
typeof signal.description === 'string' ? signal.description : '';
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
id,
|
|
46
|
+
category: coerceCategory(signal.category),
|
|
47
|
+
description,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function malformed(
|
|
52
|
+
platform: typeof Platform.OS,
|
|
53
|
+
error: string
|
|
54
|
+
): IntegrityResult {
|
|
55
|
+
return {
|
|
56
|
+
status: 'unknown',
|
|
57
|
+
signals: [],
|
|
58
|
+
platform,
|
|
59
|
+
reason: 'native_error',
|
|
60
|
+
error,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Maps a native integrity report + options to the public IntegrityResult.
|
|
66
|
+
* Compromise evidence wins over incomplete runs; incomplete/malformed never become 'clean'.
|
|
67
|
+
*/
|
|
68
|
+
export function resolveStatus(
|
|
69
|
+
report: unknown,
|
|
70
|
+
options: CheckIntegrityOptions = {},
|
|
71
|
+
platform: typeof Platform.OS
|
|
72
|
+
): IntegrityResult {
|
|
73
|
+
if (report === null || typeof report !== 'object' || Array.isArray(report)) {
|
|
74
|
+
return malformed(platform, 'Malformed native integrity report');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const nativeReport = report as Record<string, unknown>;
|
|
78
|
+
|
|
79
|
+
if (!Array.isArray(nativeReport.signals)) {
|
|
80
|
+
return malformed(platform, 'Malformed native integrity report');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const signals = nativeReport.signals.map(normalizeSignal);
|
|
84
|
+
const completed = nativeReport.completed === true;
|
|
85
|
+
const treatEmulatorAsCompromised =
|
|
86
|
+
options.treatEmulatorAsCompromised === true;
|
|
87
|
+
|
|
88
|
+
const hasNonEmulatorSignal = signals.some(
|
|
89
|
+
(signal) => signal.category !== 'emulator'
|
|
90
|
+
);
|
|
91
|
+
const hasEmulatorSignal = signals.some(
|
|
92
|
+
(signal) => signal.category === 'emulator'
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
if (
|
|
96
|
+
hasNonEmulatorSignal ||
|
|
97
|
+
(hasEmulatorSignal && treatEmulatorAsCompromised)
|
|
98
|
+
) {
|
|
99
|
+
return {
|
|
100
|
+
status: 'compromised',
|
|
101
|
+
signals,
|
|
102
|
+
platform,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (!completed) {
|
|
107
|
+
const reason: UnknownReason =
|
|
108
|
+
nativeReport.reason === 'not_implemented'
|
|
109
|
+
? 'not_implemented'
|
|
110
|
+
: 'incomplete';
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
status: 'unknown',
|
|
114
|
+
signals,
|
|
115
|
+
platform,
|
|
116
|
+
reason,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
status: 'clean',
|
|
122
|
+
signals,
|
|
123
|
+
platform,
|
|
124
|
+
};
|
|
125
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { Platform } from 'react-native';
|
|
2
|
+
|
|
3
|
+
export type IntegrityStatus = 'clean' | 'compromised' | 'unknown';
|
|
4
|
+
|
|
5
|
+
export type SignalCategory =
|
|
6
|
+
| 'jailbreak'
|
|
7
|
+
| 'root'
|
|
8
|
+
| 'hooking'
|
|
9
|
+
| 'debugger'
|
|
10
|
+
| 'emulator'
|
|
11
|
+
| 'tamper'
|
|
12
|
+
| 'environment';
|
|
13
|
+
|
|
14
|
+
export interface Signal {
|
|
15
|
+
id: string;
|
|
16
|
+
category: SignalCategory;
|
|
17
|
+
description: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export type UnknownReason =
|
|
21
|
+
| 'unsupported_platform'
|
|
22
|
+
| 'native_module_unavailable'
|
|
23
|
+
| 'native_error'
|
|
24
|
+
| 'timeout'
|
|
25
|
+
| 'incomplete'
|
|
26
|
+
| 'not_implemented';
|
|
27
|
+
|
|
28
|
+
export interface IntegrityResult {
|
|
29
|
+
status: IntegrityStatus;
|
|
30
|
+
signals: Signal[];
|
|
31
|
+
platform: typeof Platform.OS;
|
|
32
|
+
reason?: UnknownReason;
|
|
33
|
+
error?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface CheckIntegrityOptions {
|
|
37
|
+
/** Treat emulator/simulator signals as compromising. Default false (reported, but status stays 'clean'). */
|
|
38
|
+
treatEmulatorAsCompromised?: boolean;
|
|
39
|
+
/** Resolve as 'unknown' with reason 'timeout' if native does not answer in time. Default 10000 ms. */
|
|
40
|
+
timeoutMs?: number;
|
|
41
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
2
|
+
import { AppState, type AppStateStatus } from 'react-native';
|
|
3
|
+
import { checkIntegrity } from './checkIntegrity';
|
|
4
|
+
import type {
|
|
5
|
+
CheckIntegrityOptions,
|
|
6
|
+
IntegrityResult,
|
|
7
|
+
IntegrityStatus,
|
|
8
|
+
Signal,
|
|
9
|
+
} from './types';
|
|
10
|
+
|
|
11
|
+
export type UseDeviceIntegrityResult = {
|
|
12
|
+
status: IntegrityStatus;
|
|
13
|
+
signals: Signal[];
|
|
14
|
+
result: IntegrityResult | null;
|
|
15
|
+
loading: boolean;
|
|
16
|
+
refresh: () => Promise<IntegrityResult>;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export function useDeviceIntegrity(
|
|
20
|
+
options: CheckIntegrityOptions = {}
|
|
21
|
+
): UseDeviceIntegrityResult {
|
|
22
|
+
const treatEmulatorAsCompromised = options.treatEmulatorAsCompromised;
|
|
23
|
+
const timeoutMs = options.timeoutMs;
|
|
24
|
+
|
|
25
|
+
const [result, setResult] = useState<IntegrityResult | null>(null);
|
|
26
|
+
const [loading, setLoading] = useState(true);
|
|
27
|
+
|
|
28
|
+
const requestIdRef = useRef(0);
|
|
29
|
+
const mountedRef = useRef(true);
|
|
30
|
+
const appStateRef = useRef(AppState.currentState);
|
|
31
|
+
|
|
32
|
+
const run = useCallback(async (): Promise<IntegrityResult> => {
|
|
33
|
+
const requestId = ++requestIdRef.current;
|
|
34
|
+
setLoading(true);
|
|
35
|
+
|
|
36
|
+
const next = await checkIntegrity({
|
|
37
|
+
treatEmulatorAsCompromised,
|
|
38
|
+
timeoutMs,
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
if (mountedRef.current && requestId === requestIdRef.current) {
|
|
42
|
+
setResult(next);
|
|
43
|
+
setLoading(false);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return next;
|
|
47
|
+
}, [treatEmulatorAsCompromised, timeoutMs]);
|
|
48
|
+
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
mountedRef.current = true;
|
|
51
|
+
run().catch(() => {
|
|
52
|
+
// checkIntegrity never rejects; guard against unexpected throws
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
return () => {
|
|
56
|
+
mountedRef.current = false;
|
|
57
|
+
};
|
|
58
|
+
}, [run]);
|
|
59
|
+
|
|
60
|
+
useEffect(() => {
|
|
61
|
+
const onChange = (nextState: AppStateStatus) => {
|
|
62
|
+
const previous = appStateRef.current;
|
|
63
|
+
appStateRef.current = nextState;
|
|
64
|
+
|
|
65
|
+
if (
|
|
66
|
+
(previous === 'background' || previous === 'inactive') &&
|
|
67
|
+
nextState === 'active'
|
|
68
|
+
) {
|
|
69
|
+
run().catch(() => {
|
|
70
|
+
// checkIntegrity never rejects; guard against unexpected throws
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const subscription = AppState.addEventListener('change', onChange);
|
|
76
|
+
return () => {
|
|
77
|
+
subscription.remove();
|
|
78
|
+
};
|
|
79
|
+
}, [run]);
|
|
80
|
+
|
|
81
|
+
const refresh = useCallback((): Promise<IntegrityResult> => {
|
|
82
|
+
return run();
|
|
83
|
+
}, [run]);
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
status: result?.status ?? 'unknown',
|
|
87
|
+
signals: result?.signals ?? [],
|
|
88
|
+
result,
|
|
89
|
+
loading,
|
|
90
|
+
refresh,
|
|
91
|
+
};
|
|
92
|
+
}
|