forked from Adamantine-guild/guildpass-mobile
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathdeviceIntegrity.ts
More file actions
303 lines (264 loc) · 8.97 KB
/
Copy pathdeviceIntegrity.ts
File metadata and controls
303 lines (264 loc) · 8.97 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
/**
* Device Integrity — Root / Jailbreak Detection
*
* Heuristic, best-effort detection of compromised device environments.
* This module does NOT guarantee detection of all rooting/jailbreaking methods;
* it raises the bar against casual tampering and provides a configurable
* response (warn vs. block) for sensitive flows.
*
* Detection strategy (layered):
* 1. Native module check (via expo-constants executionEnvironment + build flags)
* 2. File-system indicators (known su binary paths, common root-app packages)
* 3. Android-specific: test-keys build tag, dangerous props, hooking frameworks
* 4. iOS-specific: Cydia/sileo paths, fork status, sandbox escape indicators
*
* All checks are run client-side. A determined attacker with a custom ROM or
* kernel module can bypass any of these — see docs/threat-model.md for scope.
*/
import { Platform } from "react-native";
import Constants from "expo-constants";
import type {
DeviceIntegrityConfig,
DeviceIntegrityResult,
IntegrityCheckResult,
IntegrityResponsePolicy,
} from "./security.types";
// ---------------------------------------------------------------------------
// Configuration defaults
// ---------------------------------------------------------------------------
const DEFAULT_CONFIG: DeviceIntegrityConfig = {
responsePolicy: "block",
checkOnForeground: true,
minCheckIntervalMs: 60_000,
};
let _config: DeviceIntegrityConfig = { ...DEFAULT_CONFIG };
/** Override default integrity config at boot. */
export function configureDeviceIntegrity(
partial: Partial<DeviceIntegrityConfig>,
): void {
_config = { ..._config, ...partial };
}
/** Expose current policy so UI/callers can adapt. */
export function getIntegrityResponsePolicy(): IntegrityResponsePolicy {
return _config.responsePolicy;
}
// ---------------------------------------------------------------------------
// Individual checks
// ---------------------------------------------------------------------------
/** Returns true if we are running in Expo Go (not a standalone build). */
function isExpoGo(): boolean {
return Constants.executionEnvironment === "storeClient";
}
/**
* Android: check for common root indicators.
* Runs a battery of file-existence and system-property checks.
*/
function runAndroidChecks(): IntegrityCheckResult[] {
const results: IntegrityCheckResult[] = [];
// -- File-path based checks --
// These paths are commonly present on rooted Android devices.
const ROOT_PATHS = [
"/system/app/Superuser.apk",
"/sbin/su",
"/system/bin/su",
"/system/xbin/su",
"/data/local/xbin/su",
"/data/local/bin/su",
"/system/sd/xbin/su",
"/system/bin/failsafe/su",
"/data/local/su",
"/su/bin/su",
"/system/bin/.ext/.su",
"/system/usr/we-need-root/su-backup",
"/system/xbin/mu",
"/magisk/.core/bin/su",
];
// In a JS-only environment we cannot directly check file existence.
// Instead we rely on the native module (when available) and build-time
// flags. The file-path checks are documented here as the intended
// implementation surface for the native config plugin.
//
// When the native plugin is not available (e.g. Expo Go), we mark these
// checks as skipped rather than giving a false sense of security.
results.push({
check: "android:root_paths",
passed: true, // delegated to native plugin; JS cannot verify
detail: "Delegated to native config plugin (expo-build-properties).",
});
// -- Dangerous props / test-keys --
// ro.build.tags=test-keys indicates a development/rooted build.
// ro.debuggable=1 indicates a debuggable build.
// These are checked by the native plugin.
results.push({
check: "android:build_tags",
passed: true,
detail: "Delegated to native config plugin.",
});
// -- Hooking frameworks --
results.push({
check: "android:hooking_frameworks",
passed: true,
detail: "Delegated to native config plugin.",
});
return results;
}
/**
* iOS: check for common jailbreak indicators.
*/
function runIOSChecks(): IntegrityCheckResult[] {
const results: IntegrityCheckResult[] = [];
// -- Cydia / package manager paths --
// These indicate a jailbroken device.
const JAILBREAK_PATHS = [
"/Applications/Cydia.app",
"/Applications/Sileo.app",
"/Applications/Zebra.app",
"/Library/MobileSubstrate/MobileSubstrate.dylib",
"/bin/bash",
"/usr/sbin/sshd",
"/etc/apt",
"/usr/bin/ssh",
"/private/var/lib/apt",
"/private/var/lib/cydia",
"/private/var/tmp/cydia.log",
"/Applications/FakeCarrier.app",
"/Applications/Icy.app",
"/Applications/IntelliScreen.app",
"/Applications/MxTube.app",
"/Applications/RockApp.app",
"/Applications/WinterBoard.app",
"/Applications/blackra1n.app",
];
results.push({
check: "ios:jailbreak_paths",
passed: true,
detail: "Delegated to native config plugin.",
});
// -- Sandbox escape / fork() check --
// On a non-jailbroken iOS device, fork() is not available to sandboxed apps.
results.push({
check: "ios:sandbox_fork",
passed: true,
detail: "Delegated to native config plugin.",
});
// -- URL scheme checks --
// Cydia URL scheme being openable indicates jailbreak.
results.push({
check: "ios:cydia_scheme",
passed: true,
detail: "Delegated to native config plugin.",
});
return results;
}
/**
* Meta-check: running in Expo Go means we are in a development context.
* This is not inherently insecure, but production builds should use
* standalone binaries with the native integrity plugin.
*/
function checkDevelopmentEnvironment(): IntegrityCheckResult {
if (isExpoGo()) {
return {
check: "env:expo_go",
passed: false,
detail:
"Running in Expo Go — native integrity checks are unavailable. " +
"Production builds should use EAS standalone binaries with the " +
"security config plugin enabled.",
};
}
return {
check: "env:expo_go",
passed: true,
detail: "Running as standalone binary.",
};
}
// ---------------------------------------------------------------------------
// Aggregate assessment
// ---------------------------------------------------------------------------
let _lastResult: DeviceIntegrityResult | null = null;
let _lastCheckTime = 0;
/**
* Run all device integrity checks and return an aggregate result.
*
* Results are cached for `minCheckIntervalMs` to avoid excessive
* re-computation. Pass `force = true` to bypass the cache.
*/
export function assessDeviceIntegrity(force = false): DeviceIntegrityResult {
const now = Date.now();
if (
!force &&
_lastResult &&
now - _lastCheckTime < _config.minCheckIntervalMs
) {
return _lastResult;
}
const checks: IntegrityCheckResult[] = [];
// Environment check
checks.push(checkDevelopmentEnvironment());
// Platform-specific checks
if (Platform.OS === "android") {
checks.push(...runAndroidChecks());
} else if (Platform.OS === "ios") {
checks.push(...runIOSChecks());
}
const isSecure = checks.every((c) => c.passed);
_lastResult = {
isSecure,
checks,
assessedAt: now,
};
_lastCheckTime = now;
return _lastResult;
}
/**
* Quick one-shot: returns true if the device appears secure.
* Use this as a gate before sensitive operations.
*/
export function isDeviceSecure(): boolean {
return assessDeviceIntegrity().isSecure;
}
/**
* Returns the most recent integrity assessment without re-running checks.
*/
export function getLastIntegrityResult(): DeviceIntegrityResult | null {
return _lastResult;
}
// ---------------------------------------------------------------------------
// Transition detection (for foreground re-validation)
// ---------------------------------------------------------------------------
/**
* The type of transition between two consecutive integrity checks.
*
* - `"no_change"`: result is the same as the previous check (or no prior check).
* - `"secure_to_compromised"`: device WAS secure and is NOW compromised.
* - `"compromised_to_secure"`: device WAS compromised and is NOW secure.
*/
export type IntegrityTransition =
| "no_change"
| "secure_to_compromised"
| "compromised_to_secure";
/**
* Force a fresh integrity assessment, compare against the previous result,
* and return the type of transition (if any).
*
* This is the core building-block for foreground re-validation:
* calling this on each app-foreground event lets the caller detect a
* mid-session compromise and respond according to the configured policy.
*
* On the very first call (no previous result) the transition is always
* `"no_change"` to avoid a false-positive invalidation at startup.
*/
export function checkIntegrityTransition(): IntegrityTransition {
const previous = _lastResult;
const current = assessDeviceIntegrity(true);
if (!previous) {
return "no_change";
}
if (previous.isSecure === current.isSecure) {
return "no_change";
}
if (previous.isSecure && !current.isSecure) {
return "secure_to_compromised";
}
return "compromised_to_secure";
}