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.
Files changed (44) hide show
  1. package/README.md +138 -11
  2. package/android/src/main/java/com/deviceintegrity/DeviceIntegrityModule.kt +68 -2
  3. package/android/src/main/java/com/deviceintegrity/IntegrityChecks.kt +5 -1
  4. package/android/src/main/java/com/deviceintegrity/TamperChecks.kt +158 -0
  5. package/ios/DIIntegrityChecks.h +7 -1
  6. package/ios/DIIntegrityChecks.m +9 -1
  7. package/ios/DITamperChecks.h +27 -0
  8. package/ios/DITamperChecks.m +304 -0
  9. package/ios/DeviceIntegrity.mm +56 -4
  10. package/jest/index.d.ts +39 -0
  11. package/jest/index.js +100 -0
  12. package/lib/module/NativeDeviceIntegrity.js.map +1 -1
  13. package/lib/module/checkIntegrity.js +2 -0
  14. package/lib/module/checkIntegrity.js.map +1 -1
  15. package/lib/module/checkIntegrity.native.js +83 -6
  16. package/lib/module/checkIntegrity.native.js.map +1 -1
  17. package/lib/module/index.js +1 -0
  18. package/lib/module/index.js.map +1 -1
  19. package/lib/module/resolveStatus.js +50 -5
  20. package/lib/module/resolveStatus.js.map +1 -1
  21. package/lib/module/types.js +4 -1
  22. package/lib/module/types.js.map +1 -1
  23. package/lib/module/useDeviceIntegrity.js +21 -7
  24. package/lib/module/useDeviceIntegrity.js.map +1 -1
  25. package/lib/typescript/src/NativeDeviceIntegrity.d.ts +2 -2
  26. package/lib/typescript/src/NativeDeviceIntegrity.d.ts.map +1 -1
  27. package/lib/typescript/src/checkIntegrity.d.ts.map +1 -1
  28. package/lib/typescript/src/checkIntegrity.native.d.ts +6 -0
  29. package/lib/typescript/src/checkIntegrity.native.d.ts.map +1 -1
  30. package/lib/typescript/src/index.d.ts +2 -1
  31. package/lib/typescript/src/index.d.ts.map +1 -1
  32. package/lib/typescript/src/resolveStatus.d.ts +1 -0
  33. package/lib/typescript/src/resolveStatus.d.ts.map +1 -1
  34. package/lib/typescript/src/types.d.ts +49 -2
  35. package/lib/typescript/src/types.d.ts.map +1 -1
  36. package/lib/typescript/src/useDeviceIntegrity.d.ts.map +1 -1
  37. package/package.json +8 -2
  38. package/src/NativeDeviceIntegrity.ts +5 -2
  39. package/src/checkIntegrity.native.ts +112 -15
  40. package/src/checkIntegrity.ts +2 -0
  41. package/src/index.tsx +5 -0
  42. package/src/resolveStatus.ts +69 -12
  43. package/src/types.ts +76 -2
  44. package/src/useDeviceIntegrity.ts +22 -7
@@ -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 signals = nativeReport.signals.map(normalizeSignal);
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 treatEmulatorAsCompromised =
86
- options.treatEmulatorAsCompromised === true;
143
+ const compromising = resolveCompromisingCategories(options);
87
144
 
88
- const hasNonEmulatorSignal = signals.some(
89
- (signal) => signal.category !== 'emulator'
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
- id: string;
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
- /** Treat emulator/simulator signals as compromising. Default false (reported, but status stays 'clean'). */
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 treatEmulatorAsCompromised = options.treatEmulatorAsCompromised;
23
- const timeoutMs = options.timeoutMs;
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
- }, [treatEmulatorAsCompromised, timeoutMs]);
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;