@necessarysdk/react-native 0.0.0-stage → 0.1.5
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/CHANGELOG.md +22 -0
- package/LICENSE +201 -0
- package/NOTICE +2 -0
- package/NecessaryReactNative.podspec +23 -0
- package/README.md +429 -2
- package/android/build.gradle +83 -0
- package/android/src/main/AndroidManifest.xml +41 -0
- package/android/src/main/cpp/CMakeLists.txt +7 -0
- package/android/src/main/cpp/code_integrity.cpp +484 -0
- package/android/src/main/java/com/necessarysdk/reactnative/Causes.kt +49 -0
- package/android/src/main/java/com/necessarysdk/reactnative/InstallationKeyManager.kt +388 -0
- package/android/src/main/java/com/necessarysdk/reactnative/IntegrityProbeManager.kt +1152 -0
- package/android/src/main/java/com/necessarysdk/reactnative/KeyPresence.kt +62 -0
- package/android/src/main/java/com/necessarysdk/reactnative/NecessaryModules.kt +94 -0
- package/android/src/main/java/com/necessarysdk/reactnative/NecessaryNative.kt +323 -0
- package/android/src/main/java/com/necessarysdk/reactnative/NecessaryPackage.kt +38 -0
- package/android/src/main/java/com/necessarysdk/reactnative/ProbeReads.kt +314 -0
- package/android/src/main/java/com/necessarysdk/reactnative/ScreenGuard.kt +56 -0
- package/android/src/main/java/com/necessarysdk/reactnative/SecureStore.kt +127 -0
- package/android/src/main/java/com/necessarysdk/reactnative/StepUpKeyEntry.kt +57 -0
- package/android/src/main/java/com/necessarysdk/reactnative/StepUpKeyManager.kt +762 -0
- package/contract/report-1/android-8-ios-5/RULES.md +149 -0
- package/contract/report-1/android-8-ios-5/report.schema.json +2753 -0
- package/contract/report-1/android-8-ios-5/samples/android-8-complete.json +205 -0
- package/contract/report-1/android-8-ios-5/samples/android-8-incomplete.json +205 -0
- package/contract/report-1/android-8-ios-5/samples/android-8-invalid.json +205 -0
- package/contract/report-1/android-8-ios-5/samples/android-8-location-one-fix.json +228 -0
- package/contract/report-1/android-8-ios-5/samples/android-8-location-two-fixes.json +228 -0
- package/contract/report-1/android-8-ios-5/samples/android-8-refused.json +208 -0
- package/contract/report-1/android-8-ios-5/samples/ios-5-complete.json +106 -0
- package/contract/report-1/android-8-ios-5/samples/ios-5-incomplete.json +105 -0
- package/contract/report-1/android-8-ios-5/samples/ios-5-invalid.json +107 -0
- package/contract/report-1/android-8-ios-5/samples/ios-5-partial.json +110 -0
- package/contract/report-1/android-8-ios-5/samples/ios-5-refused.json +109 -0
- package/doc/integration_guide.md +771 -0
- package/doc/measurements.md +774 -0
- package/ios/InstallationKeyManager.swift +694 -0
- package/ios/IntegrityProbeManager.swift +960 -0
- package/ios/NecessaryModules.h +34 -0
- package/ios/NecessaryModules.mm +208 -0
- package/ios/NecessaryNative.swift +327 -0
- package/ios/PrivacyInfo.xcprivacy +14 -0
- package/ios/ProbeReads.swift +353 -0
- package/lib/client.d.ts +105 -0
- package/lib/client.js +176 -0
- package/lib/collector.d.ts +18 -0
- package/lib/collector.js +49 -0
- package/lib/deviceTrust.d.ts +253 -0
- package/lib/deviceTrust.js +690 -0
- package/lib/errors.d.ts +43 -0
- package/lib/errors.js +56 -0
- package/lib/index.d.ts +18 -0
- package/lib/index.js +34 -0
- package/lib/installationKey.d.ts +62 -0
- package/lib/installationKey.js +185 -0
- package/lib/installationStore.d.ts +49 -0
- package/lib/installationStore.js +127 -0
- package/lib/native/NativeNecessaryInstallationKey.d.ts +10 -0
- package/lib/native/NativeNecessaryInstallationKey.js +4 -0
- package/lib/native/NativeNecessaryIntegrity.d.ts +8 -0
- package/lib/native/NativeNecessaryIntegrity.js +4 -0
- package/lib/native/NativeNecessarySecureScreen.d.ts +7 -0
- package/lib/native/NativeNecessarySecureScreen.js +4 -0
- package/lib/native/NativeNecessaryStepUpKey.d.ts +9 -0
- package/lib/native/NativeNecessaryStepUpKey.js +4 -0
- package/lib/native/NativeNecessaryStorage.d.ts +14 -0
- package/lib/native/NativeNecessaryStorage.js +4 -0
- package/lib/protocol.d.ts +24 -0
- package/lib/protocol.js +50 -0
- package/lib/reinstallHint.d.ts +23 -0
- package/lib/reinstallHint.js +65 -0
- package/lib/report.d.ts +62 -0
- package/lib/report.js +140 -0
- package/lib/requestProof.d.ts +116 -0
- package/lib/requestProof.js +220 -0
- package/lib/secureScreen.d.ts +15 -0
- package/lib/secureScreen.js +32 -0
- package/lib/stepUpKey.d.ts +105 -0
- package/lib/stepUpKey.js +187 -0
- package/lib/transport.d.ts +39 -0
- package/lib/transport.js +117 -0
- package/lib/util/bytes.d.ts +18 -0
- package/lib/util/bytes.js +207 -0
- package/lib/util/native.d.ts +20 -0
- package/lib/util/native.js +49 -0
- package/lib/util/release.d.ts +2 -0
- package/lib/util/release.js +7 -0
- package/lib/util/target.d.ts +18 -0
- package/lib/util/target.js +128 -0
- package/package.json +81 -4
- package/src/client.ts +221 -0
- package/src/collector.ts +64 -0
- package/src/deviceTrust.ts +842 -0
- package/src/errors.ts +71 -0
- package/src/index.ts +19 -0
- package/src/installationKey.ts +223 -0
- package/src/installationStore.ts +143 -0
- package/src/native/NativeNecessaryInstallationKey.ts +12 -0
- package/src/native/NativeNecessaryIntegrity.ts +15 -0
- package/src/native/NativeNecessarySecureScreen.ts +9 -0
- package/src/native/NativeNecessaryStepUpKey.ts +11 -0
- package/src/native/NativeNecessaryStorage.ts +16 -0
- package/src/protocol.ts +52 -0
- package/src/reinstallHint.ts +69 -0
- package/src/report.ts +174 -0
- package/src/requestProof.ts +295 -0
- package/src/secureScreen.ts +29 -0
- package/src/stepUpKey.ts +238 -0
- package/src/transport.ts +145 -0
- package/src/util/bytes.ts +194 -0
- package/src/util/native.ts +56 -0
- package/src/util/release.ts +6 -0
- package/src/util/target.ts +132 -0
- package/vectors/client_server.json +264 -0
|
@@ -0,0 +1,484 @@
|
|
|
1
|
+
// Native code-integrity probe.
|
|
2
|
+
//
|
|
3
|
+
// Runs IN the app's own process and reads the app's own already-mapped,
|
|
4
|
+
// readable r-x pages by direct pointer. That is the whole reason it is native:
|
|
5
|
+
// an earlier attempt in managed code opened /proc/self/mem, which SELinux denies to
|
|
6
|
+
// untrusted_app on Android 9/10; reading one's own mapped pages needs no such
|
|
7
|
+
// access and no ptrace.
|
|
8
|
+
//
|
|
9
|
+
// For each target library it compares EVERY executable mapping against the same
|
|
10
|
+
// bytes on disk. Iterating every VMA (not just the first) is essential: an
|
|
11
|
+
// inline hooker flips individual code pages writable to patch them, which
|
|
12
|
+
// splits a library's single r-x mapping into several, and the patched page is
|
|
13
|
+
// usually not the first. The dynamic linker does not modify .text at runtime
|
|
14
|
+
// (relocations land in the GOT/data segments), so on a clean device memory and
|
|
15
|
+
// disk are byte-identical; a hook overwrites a prologue and shows up here as a
|
|
16
|
+
// difference, whatever the hooking framework is called.
|
|
17
|
+
//
|
|
18
|
+
// Targets are grouped into three buckets so the server can weight them
|
|
19
|
+
// separately:
|
|
20
|
+
// core - libc, libart. Validated at zero on clean hardware; scored today.
|
|
21
|
+
// ext - other high-value system hook targets, TLS included (cert-pinning
|
|
22
|
+
// bypass patches libssl/libcrypto). Scored once baselined on device.
|
|
23
|
+
// app - the app's own native code (the React Native runtime, its JavaScript
|
|
24
|
+
// engine and the app's own libraries). Catches in-memory patching
|
|
25
|
+
// of the app itself. Scored once baselined.
|
|
26
|
+
//
|
|
27
|
+
// Two rules from the release review of 2026-10-31:
|
|
28
|
+
// - One inventory. /proc/self/maps is read ONCE, whole, and every bucket is
|
|
29
|
+
// taken from that snapshot. It used to be reopened per target name, and a
|
|
30
|
+
// failed reopen counted as "not loaded": a bucket could read complete
|
|
31
|
+
// while a target went unmeasured. A snapshot that cannot be read answers
|
|
32
|
+
// checked false with a reason -- no bucket complete, no byte count made up.
|
|
33
|
+
// Complete means: every target mapping in that snapshot compared.
|
|
34
|
+
// - One comparison at a time, process-wide. A comparison makes execute-only
|
|
35
|
+
// pages readable, reads them and restores them; two at once could read a
|
|
36
|
+
// page the other had just made unreadable again. Every caller -- any
|
|
37
|
+
// module instance, any runtime, any thread -- takes the same lock for the
|
|
38
|
+
// whole snapshot/unlock/read/restore. (It does not make a library that
|
|
39
|
+
// the app itself unloads meanwhile safe to read.)
|
|
40
|
+
//
|
|
41
|
+
// And from the pass over the review's other items, the same day:
|
|
42
|
+
// - The app region is the app's own: the APKs it was installed from (base
|
|
43
|
+
// and splits, by exact path, as Android names them) and the libraries the
|
|
44
|
+
// SDK names. It used to be every mapping ending in ".apk" -- another
|
|
45
|
+
// package's APK-mapped code (WebView, a Play services module) counted as
|
|
46
|
+
// the app's. Whatever executable file of the app's own is mapped but not
|
|
47
|
+
// in the region -- a library another library extracted, the app's own
|
|
48
|
+
// code the platform compiled -- is counted and named (app_outside_count,
|
|
49
|
+
// app_outside_files), so the report says what the region leaves out.
|
|
50
|
+
|
|
51
|
+
#include <jni.h>
|
|
52
|
+
#include <fcntl.h>
|
|
53
|
+
#include <unistd.h>
|
|
54
|
+
#include <string.h>
|
|
55
|
+
#include <stdio.h>
|
|
56
|
+
#include <stdlib.h>
|
|
57
|
+
#include <sys/mman.h>
|
|
58
|
+
#include <algorithm>
|
|
59
|
+
#include <atomic>
|
|
60
|
+
#include <mutex>
|
|
61
|
+
#include <set>
|
|
62
|
+
#include <string>
|
|
63
|
+
#include <vector>
|
|
64
|
+
|
|
65
|
+
namespace {
|
|
66
|
+
|
|
67
|
+
// Bytes compared per bucket at most. Deliberately far above any real target
|
|
68
|
+
// set, so every target mapping is measured IN FULL: the old 4 MiB cap per
|
|
69
|
+
// suffix left the rest of large mappings (libart, and every library mapped
|
|
70
|
+
// straight from the APK, share the ".apk" suffix) unmeasured while reporting clean
|
|
71
|
+
// before. Anything the budget still cuts off is reported
|
|
72
|
+
// as skipped_bytes, never silently dropped.
|
|
73
|
+
constexpr size_t kBucketBudget = 256u << 20;
|
|
74
|
+
constexpr size_t kChunk = 256u << 10; // compared in 256 KiB chunks
|
|
75
|
+
|
|
76
|
+
const char *kCore[] = {"/libc.so", "/libart.so", nullptr};
|
|
77
|
+
const char *kExt[] = {
|
|
78
|
+
"/libc++.so", "/libssl.so", "/libcrypto.so",
|
|
79
|
+
"/libandroid_runtime.so", "/libbinder.so", nullptr,
|
|
80
|
+
};
|
|
81
|
+
// The app's own native code. A normal React Native release APK does NOT extract
|
|
82
|
+
// its native libraries: they are mapped straight out of the (uncompressed) zip,
|
|
83
|
+
// so /proc/self/maps shows them backed by ".../base.apk" rather than
|
|
84
|
+
// ".../lib/arm64/libreactnative.so". The app's own APKs (base and splits) are
|
|
85
|
+
// therefore targets by their exact paths, which the app passes in; these
|
|
86
|
+
// names -- the React Native runtime, its JavaScript engine (Hermes, under
|
|
87
|
+
// either of its names, with its tooling), JSI, fbjni and the app's own native
|
|
88
|
+
// modules -- still cover builds that do extract.
|
|
89
|
+
const char *kAppNamed[] = {
|
|
90
|
+
"/libreactnative.so", "/libhermesvm.so", "/libhermes.so",
|
|
91
|
+
"/libhermestooling.so", "/libjsi.so", "/libfbjni.so", "/libappmodules.so",
|
|
92
|
+
nullptr,
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
// What the app owns: the APKs it was installed from, by exact path, and the
|
|
96
|
+
// directories its own files live in (its install directory, its data
|
|
97
|
+
// directories), each ending in '/'.
|
|
98
|
+
struct AppOwn {
|
|
99
|
+
std::vector<std::string> apks;
|
|
100
|
+
std::vector<std::string> dirs;
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
// One target: a path suffix ("/libc.so") or an exact path (one of the app's
|
|
104
|
+
// APKs), and the name a difference is reported under.
|
|
105
|
+
struct Target {
|
|
106
|
+
std::string match;
|
|
107
|
+
bool exact;
|
|
108
|
+
std::string name;
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
std::vector<Target> named_targets(const char **suffixes) {
|
|
112
|
+
std::vector<Target> targets;
|
|
113
|
+
for (int i = 0; suffixes[i] != nullptr; i++) {
|
|
114
|
+
targets.push_back({suffixes[i], false, suffixes[i] + 1}); // name drops the leading '/'
|
|
115
|
+
}
|
|
116
|
+
return targets;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
std::string base_name(const std::string &path) {
|
|
120
|
+
size_t slash = path.rfind('/');
|
|
121
|
+
return slash == std::string::npos ? path : path.substr(slash + 1);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
std::vector<Target> app_targets(const AppOwn &own) {
|
|
125
|
+
std::vector<Target> targets = named_targets(kAppNamed);
|
|
126
|
+
for (const std::string &apk : own.apks) {
|
|
127
|
+
if (!apk.empty()) targets.push_back({apk, true, base_name(apk)});
|
|
128
|
+
}
|
|
129
|
+
return targets;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// What one bucket measured. expected = compared + skipped + unreadable, so a
|
|
133
|
+
// bucket that measured less than its targets says so instead of reading clean.
|
|
134
|
+
struct Coverage {
|
|
135
|
+
long expected = 0; // bytes in the executable target mappings found
|
|
136
|
+
long compared = 0; // bytes actually compared against disk
|
|
137
|
+
long skipped = 0; // not compared: the bucket budget ran out
|
|
138
|
+
long unreadable = 0; // not compared: unlockable, unopenable or short read
|
|
139
|
+
long diff = 0; // compared bytes that differ from disk
|
|
140
|
+
int found = 0; // target libraries mapped at all
|
|
141
|
+
int withDiff = 0; // target libraries with at least one differing byte
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
// One executable mapping of the snapshot.
|
|
145
|
+
struct Mapping {
|
|
146
|
+
unsigned long start;
|
|
147
|
+
unsigned long end;
|
|
148
|
+
unsigned long offset;
|
|
149
|
+
char perms[5];
|
|
150
|
+
std::string path;
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
// Every comparison, from any thread of the process, one at a time.
|
|
154
|
+
std::mutex g_inspection;
|
|
155
|
+
|
|
156
|
+
// The memory map (/proc/self/maps; another path only in the host test), read
|
|
157
|
+
// whole into `text`. False when it cannot be read, or a read fails part-way.
|
|
158
|
+
bool read_maps(std::string *text, const char *path = "/proc/self/maps") {
|
|
159
|
+
int fd = open(path, O_RDONLY | O_CLOEXEC);
|
|
160
|
+
if (fd < 0) return false;
|
|
161
|
+
char chunk[16384];
|
|
162
|
+
bool ok = true;
|
|
163
|
+
for (;;) {
|
|
164
|
+
ssize_t got = read(fd, chunk, sizeof(chunk));
|
|
165
|
+
if (got < 0) { ok = false; break; }
|
|
166
|
+
if (got == 0) break;
|
|
167
|
+
text->append(chunk, (size_t) got);
|
|
168
|
+
}
|
|
169
|
+
close(fd);
|
|
170
|
+
return ok;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// The executable, file-backed mappings of one snapshot.
|
|
174
|
+
std::vector<Mapping> executable_mappings(const std::string &text) {
|
|
175
|
+
std::vector<Mapping> found;
|
|
176
|
+
size_t at = 0;
|
|
177
|
+
while (at < text.size()) {
|
|
178
|
+
size_t next = text.find('\n', at);
|
|
179
|
+
if (next == std::string::npos) next = text.size();
|
|
180
|
+
std::string line = text.substr(at, next - at);
|
|
181
|
+
at = next + 1;
|
|
182
|
+
Mapping m{};
|
|
183
|
+
int consumed = 0;
|
|
184
|
+
if (sscanf(line.c_str(), "%lx-%lx %4s %lx %*x:%*x %*d %n",
|
|
185
|
+
&m.start, &m.end, m.perms, &m.offset, &consumed) < 4) continue;
|
|
186
|
+
if (consumed <= 0 || m.perms[2] != 'x' || m.end <= m.start) continue;
|
|
187
|
+
m.path = line.substr((size_t) consumed);
|
|
188
|
+
if (m.path.empty()) continue;
|
|
189
|
+
found.push_back(m);
|
|
190
|
+
}
|
|
191
|
+
return found;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
struct Counters {
|
|
195
|
+
int xomUnlocked = 0; // execute-only mappings made readable for the copy
|
|
196
|
+
int xomUnreadable = 0; // execute-only mappings that could not be
|
|
197
|
+
int restoreFailures = 0; // protections that could not be put back
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
int protection_of(const char *perms) {
|
|
201
|
+
int prot = PROT_NONE;
|
|
202
|
+
if (perms[0] == 'r') prot |= PROT_READ;
|
|
203
|
+
if (perms[1] == 'w') prot |= PROT_WRITE;
|
|
204
|
+
if (perms[2] == 'x') prot |= PROT_EXEC;
|
|
205
|
+
return prot;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Puts a mapping's EXACT original protection back on every exit path. The old
|
|
209
|
+
// loop restored plain PROT_EXEC by hand at each early exit, and changed
|
|
210
|
+
// mappings it then skipped as non-targets.
|
|
211
|
+
class ProtectionGuard {
|
|
212
|
+
public:
|
|
213
|
+
explicit ProtectionGuard(int *failures) : failures_(failures) {}
|
|
214
|
+
ProtectionGuard(const ProtectionGuard &) = delete;
|
|
215
|
+
ProtectionGuard &operator=(const ProtectionGuard &) = delete;
|
|
216
|
+
void arm(void *start, size_t len, int original) {
|
|
217
|
+
start_ = start; len_ = len; original_ = original; armed_ = true;
|
|
218
|
+
}
|
|
219
|
+
~ProtectionGuard() {
|
|
220
|
+
if (armed_ && mprotect(start_, len_, original_) != 0) (*failures_)++;
|
|
221
|
+
}
|
|
222
|
+
private:
|
|
223
|
+
int *failures_;
|
|
224
|
+
void *start_ = nullptr;
|
|
225
|
+
size_t len_ = 0;
|
|
226
|
+
int original_ = PROT_NONE;
|
|
227
|
+
bool armed_ = false;
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
bool ends_with(const char *text, const char *suffix) {
|
|
231
|
+
size_t len = strlen(text), suffix_len = strlen(suffix);
|
|
232
|
+
return len >= suffix_len && strcmp(text + len - suffix_len, suffix) == 0;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Compare `limit` bytes of one mapping with the file bytes at `offset`.
|
|
236
|
+
// Returns the bytes compared; adds differing bytes to *diff.
|
|
237
|
+
size_t compare_mapping(const unsigned char *mem, int fd, unsigned long offset,
|
|
238
|
+
size_t limit, unsigned char *buf, long *diff) {
|
|
239
|
+
size_t done = 0;
|
|
240
|
+
while (done < limit) {
|
|
241
|
+
size_t want = limit - done < kChunk ? limit - done : kChunk;
|
|
242
|
+
ssize_t got = pread(fd, buf, want, (off_t) (offset + done));
|
|
243
|
+
if (got <= 0) break;
|
|
244
|
+
for (ssize_t i = 0; i < got; i++) if (mem[done + i] != buf[i]) (*diff)++;
|
|
245
|
+
done += (size_t) got;
|
|
246
|
+
if ((size_t) got < want) break; // short read: the rest is unreadable
|
|
247
|
+
}
|
|
248
|
+
return done;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
bool selects(const Target &t, const std::string &path) {
|
|
252
|
+
return t.exact ? path == t.match : ends_with(path.c_str(), t.match.c_str());
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Compare every executable mapping of the snapshot the target selects
|
|
256
|
+
// against disk, adding what was measured to `cov` and marking each mapping
|
|
257
|
+
// selected. Returns whether any mapping matched.
|
|
258
|
+
bool compare_one(const Target &target, const std::vector<Mapping> &mappings,
|
|
259
|
+
Coverage *cov, Counters *counters, unsigned char *buf,
|
|
260
|
+
std::vector<bool> *selected) {
|
|
261
|
+
bool any = false;
|
|
262
|
+
long diffBefore = cov->diff;
|
|
263
|
+
for (size_t index = 0; index < mappings.size(); index++) {
|
|
264
|
+
const Mapping &m = mappings[index];
|
|
265
|
+
// Select the target BEFORE touching any protection: a mapping that is
|
|
266
|
+
// not measured is never modified.
|
|
267
|
+
if (!selects(target, m.path)) continue;
|
|
268
|
+
if ((*selected)[index]) continue; // one mapping, one bucket
|
|
269
|
+
(*selected)[index] = true;
|
|
270
|
+
any = true;
|
|
271
|
+
size_t span = m.end - m.start;
|
|
272
|
+
cov->expected += (long) span;
|
|
273
|
+
size_t used = (size_t) (cov->compared + cov->unreadable);
|
|
274
|
+
size_t room = used < kBucketBudget ? kBucketBudget - used : 0;
|
|
275
|
+
size_t limit = span < room ? span : room;
|
|
276
|
+
cov->skipped += (long) (span - limit);
|
|
277
|
+
if (limit == 0) continue;
|
|
278
|
+
|
|
279
|
+
// Android 10+ maps system libraries EXECUTE-ONLY (--xp); dereferencing
|
|
280
|
+
// such a page segfaults. Add PROT_READ for the copy only; the guard
|
|
281
|
+
// restores the original protection whatever happens next. If adding it
|
|
282
|
+
// is refused, the bytes are unreadable -- counted, never clean.
|
|
283
|
+
ProtectionGuard guard(&counters->restoreFailures);
|
|
284
|
+
if (m.perms[0] != 'r') {
|
|
285
|
+
int original = protection_of(m.perms);
|
|
286
|
+
if (mprotect((void *) m.start, span, original | PROT_READ) != 0) {
|
|
287
|
+
counters->xomUnreadable++;
|
|
288
|
+
cov->unreadable += (long) limit;
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
291
|
+
guard.arm((void *) m.start, span, original);
|
|
292
|
+
counters->xomUnlocked++;
|
|
293
|
+
}
|
|
294
|
+
int fd = open(m.path.c_str(), O_RDONLY | O_CLOEXEC);
|
|
295
|
+
if (fd < 0) {
|
|
296
|
+
cov->unreadable += (long) limit;
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
size_t done = compare_mapping((const unsigned char *) m.start, fd, m.offset,
|
|
300
|
+
limit, buf, &cov->diff);
|
|
301
|
+
close(fd);
|
|
302
|
+
cov->compared += (long) done;
|
|
303
|
+
cov->unreadable += (long) (limit - done);
|
|
304
|
+
}
|
|
305
|
+
if (any) {
|
|
306
|
+
cov->found++;
|
|
307
|
+
if (cov->diff > diffBefore) cov->withDiff++;
|
|
308
|
+
}
|
|
309
|
+
return any;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Sum a bucket; append the name of any target that differs to `names`.
|
|
313
|
+
void compare_bucket(const std::vector<Target> &targets, const std::vector<Mapping> &mappings,
|
|
314
|
+
Coverage *cov, Counters *counters, unsigned char *buf,
|
|
315
|
+
std::vector<bool> *selected, std::string *names) {
|
|
316
|
+
for (const Target &target : targets) {
|
|
317
|
+
long before = cov->diff;
|
|
318
|
+
if (!compare_one(target, mappings, cov, counters, buf, selected)) continue;
|
|
319
|
+
if (cov->diff > before && names->size() + target.name.size() + 1 < 480) {
|
|
320
|
+
if (!names->empty()) names->push_back(',');
|
|
321
|
+
names->append(target.name);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// The app's own executable files mapped but in no bucket: counted, and named
|
|
327
|
+
// (sorted, at most kOutsideNames), so the app region never reads as all of the
|
|
328
|
+
// app's code.
|
|
329
|
+
constexpr size_t kOutsideNames = 32;
|
|
330
|
+
|
|
331
|
+
std::set<std::string> outside_files(const std::vector<Mapping> &mappings,
|
|
332
|
+
const std::vector<bool> &selected, const AppOwn &own) {
|
|
333
|
+
std::set<std::string> files;
|
|
334
|
+
for (size_t index = 0; index < mappings.size(); index++) {
|
|
335
|
+
if (selected[index]) continue;
|
|
336
|
+
const std::string &path = mappings[index].path;
|
|
337
|
+
for (const std::string &dir : own.dirs) {
|
|
338
|
+
if (!dir.empty() && path.compare(0, dir.size(), dir) == 0) {
|
|
339
|
+
files.insert(path);
|
|
340
|
+
break;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
return files;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
void append_json_string(std::string *out, const std::string &value) {
|
|
348
|
+
out->push_back('"');
|
|
349
|
+
for (unsigned char c : value) {
|
|
350
|
+
if (c == '"' || c == '\\') {
|
|
351
|
+
out->push_back('\\');
|
|
352
|
+
out->push_back((char) c);
|
|
353
|
+
} else if (c < 0x20) {
|
|
354
|
+
char escaped[8];
|
|
355
|
+
snprintf(escaped, sizeof(escaped), "\\u%04x", c);
|
|
356
|
+
out->append(escaped);
|
|
357
|
+
} else {
|
|
358
|
+
out->push_back((char) c);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
out->push_back('"');
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
bool complete(const Coverage &cov) {
|
|
365
|
+
return cov.expected > 0 && cov.compared == cov.expected;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
} // namespace
|
|
369
|
+
|
|
370
|
+
#ifdef NECESSARY_HOST_TEST
|
|
371
|
+
// The host test's witness that comparisons never overlap.
|
|
372
|
+
std::atomic<int> g_in_flight{0};
|
|
373
|
+
std::atomic<int> g_max_in_flight{0};
|
|
374
|
+
#endif
|
|
375
|
+
|
|
376
|
+
namespace {
|
|
377
|
+
|
|
378
|
+
// The whole comparison, one at a time in the process: the snapshot, every
|
|
379
|
+
// unlock, read and restore. `maps_path` is /proc/self/maps but in the host test.
|
|
380
|
+
std::string inspect(const char *maps_path, const AppOwn &own) {
|
|
381
|
+
std::lock_guard<std::mutex> one_at_a_time(g_inspection);
|
|
382
|
+
#ifdef NECESSARY_HOST_TEST
|
|
383
|
+
struct InFlight {
|
|
384
|
+
InFlight() {
|
|
385
|
+
int now = ++g_in_flight;
|
|
386
|
+
int seen = g_max_in_flight.load();
|
|
387
|
+
while (now > seen && !g_max_in_flight.compare_exchange_weak(seen, now)) {}
|
|
388
|
+
}
|
|
389
|
+
~InFlight() { --g_in_flight; }
|
|
390
|
+
} in_flight;
|
|
391
|
+
#endif
|
|
392
|
+
Coverage core, ext, app;
|
|
393
|
+
Counters counters;
|
|
394
|
+
std::string names;
|
|
395
|
+
const char *reason = "";
|
|
396
|
+
std::string snapshot;
|
|
397
|
+
std::vector<Mapping> mappings;
|
|
398
|
+
std::set<std::string> outside;
|
|
399
|
+
if (!read_maps(&snapshot, maps_path)) {
|
|
400
|
+
reason = "maps_unreadable";
|
|
401
|
+
} else if (snapshot.empty()) {
|
|
402
|
+
reason = "maps_empty";
|
|
403
|
+
} else {
|
|
404
|
+
mappings = executable_mappings(snapshot);
|
|
405
|
+
std::vector<bool> selected(mappings.size(), false);
|
|
406
|
+
unsigned char *buf = (unsigned char *) malloc(kChunk);
|
|
407
|
+
if (!buf) return std::string();
|
|
408
|
+
compare_bucket(named_targets(kCore), mappings, &core, &counters, buf, &selected, &names);
|
|
409
|
+
compare_bucket(named_targets(kExt), mappings, &ext, &counters, buf, &selected, &names);
|
|
410
|
+
compare_bucket(app_targets(own), mappings, &app, &counters, buf, &selected, &names);
|
|
411
|
+
free(buf);
|
|
412
|
+
outside = outside_files(mappings, selected, own);
|
|
413
|
+
if (core.found + ext.found + app.found == 0) reason = "no_target_mapped";
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
bool checked = (core.found + ext.found + app.found) > 0;
|
|
417
|
+
char json[2048];
|
|
418
|
+
snprintf(json, sizeof(json),
|
|
419
|
+
"{\"checked\":%s,\"reason\":\"%s\","
|
|
420
|
+
"\"diff_bytes\":%ld," // core diff
|
|
421
|
+
"\"core_compared_bytes\":%ld,\"core_diff_bytes\":%ld,"
|
|
422
|
+
"\"core_expected_bytes\":%ld,\"core_skipped_bytes\":%ld,"
|
|
423
|
+
"\"core_unreadable_bytes\":%ld,\"core_complete\":%s,"
|
|
424
|
+
"\"ext_compared_bytes\":%ld,\"ext_diff_bytes\":%ld,\"ext_libs_diff\":%d,"
|
|
425
|
+
"\"ext_expected_bytes\":%ld,\"ext_skipped_bytes\":%ld,"
|
|
426
|
+
"\"ext_unreadable_bytes\":%ld,\"ext_complete\":%s,"
|
|
427
|
+
"\"app_compared_bytes\":%ld,\"app_diff_bytes\":%ld,\"app_libs_diff\":%d,"
|
|
428
|
+
"\"app_expected_bytes\":%ld,\"app_skipped_bytes\":%ld,"
|
|
429
|
+
"\"app_unreadable_bytes\":%ld,\"app_complete\":%s,"
|
|
430
|
+
"\"xom_regions_unlocked\":%d,\"xom_regions_unreadable\":%d,"
|
|
431
|
+
"\"protect_restore_failures\":%d,\"app_outside_count\":%d,",
|
|
432
|
+
checked ? "true" : "false", checked ? "" : reason,
|
|
433
|
+
core.diff,
|
|
434
|
+
core.compared, core.diff,
|
|
435
|
+
core.expected, core.skipped, core.unreadable, complete(core) ? "true" : "false",
|
|
436
|
+
ext.compared, ext.diff, ext.withDiff,
|
|
437
|
+
ext.expected, ext.skipped, ext.unreadable, complete(ext) ? "true" : "false",
|
|
438
|
+
app.compared, app.diff, app.withDiff,
|
|
439
|
+
app.expected, app.skipped, app.unreadable, complete(app) ? "true" : "false",
|
|
440
|
+
counters.xomUnlocked, counters.xomUnreadable,
|
|
441
|
+
counters.restoreFailures, (int) outside.size());
|
|
442
|
+
std::string out(json);
|
|
443
|
+
out.append("\"app_outside_files\":[");
|
|
444
|
+
std::vector<std::string> listed;
|
|
445
|
+
for (const std::string &path : outside) listed.push_back(base_name(path));
|
|
446
|
+
std::sort(listed.begin(), listed.end());
|
|
447
|
+
listed.erase(std::unique(listed.begin(), listed.end()), listed.end());
|
|
448
|
+
for (size_t i = 0; i < listed.size() && i < kOutsideNames; i++) {
|
|
449
|
+
if (i) out.push_back(',');
|
|
450
|
+
append_json_string(&out, listed[i].substr(0, 128));
|
|
451
|
+
}
|
|
452
|
+
out.append("],\"diffed_libs\":");
|
|
453
|
+
append_json_string(&out, names);
|
|
454
|
+
out.push_back('}');
|
|
455
|
+
return out;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
std::vector<std::string> strings_of(JNIEnv *env, jobjectArray array) {
|
|
459
|
+
std::vector<std::string> values;
|
|
460
|
+
if (array == nullptr) return values;
|
|
461
|
+
jsize length = env->GetArrayLength(array);
|
|
462
|
+
for (jsize i = 0; i < length; i++) {
|
|
463
|
+
jstring item = (jstring) env->GetObjectArrayElement(array, i);
|
|
464
|
+
if (item == nullptr) continue;
|
|
465
|
+
const char *chars = env->GetStringUTFChars(item, nullptr);
|
|
466
|
+
if (chars != nullptr) {
|
|
467
|
+
values.emplace_back(chars);
|
|
468
|
+
env->ReleaseStringUTFChars(item, chars);
|
|
469
|
+
}
|
|
470
|
+
env->DeleteLocalRef(item);
|
|
471
|
+
}
|
|
472
|
+
return values;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
} // namespace
|
|
476
|
+
|
|
477
|
+
extern "C" JNIEXPORT jstring JNICALL
|
|
478
|
+
Java_com_necessarysdk_reactnative_IntegrityProbeManager_nativeCodeIntegrity(
|
|
479
|
+
JNIEnv *env, jobject /* this */, jobjectArray app_apks, jobjectArray app_dirs) {
|
|
480
|
+
AppOwn own{strings_of(env, app_apks), strings_of(env, app_dirs)};
|
|
481
|
+
std::string json = inspect("/proc/self/maps", own);
|
|
482
|
+
if (json.empty()) return nullptr;
|
|
483
|
+
return env->NewStringUTF(json.c_str());
|
|
484
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
package com.necessarysdk.reactnative
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* What failed, and everything beneath it: a failure and each of its causes,
|
|
5
|
+
* outermost first, as "Class: message < Class: message". Every failure the
|
|
6
|
+
* SDK reports carries it, so a key that reads as unavailable or dead says
|
|
7
|
+
* what the keystore itself answered -- on Android 9 an
|
|
8
|
+
* UnrecoverableKeyException's message is always "Failed to obtain information
|
|
9
|
+
* about key", and only the KeyStoreException beneath it says which failure it
|
|
10
|
+
* was (the release review of 2026-10-31, item 4: "preserve the original cause
|
|
11
|
+
* chain and available error codes").
|
|
12
|
+
*
|
|
13
|
+
* Class names and messages only: never a stack trace, a key or a value of the
|
|
14
|
+
* app's. A pure function, so a JVM test holds it.
|
|
15
|
+
*/
|
|
16
|
+
internal object Causes {
|
|
17
|
+
/** How many failures a chain names: deeper ones are the same failure rewrapped. */
|
|
18
|
+
const val MAX_LINKS = 8
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* [error] and its causes. [detail] adds what a failure's message leaves
|
|
22
|
+
* out -- AndroidKeyStore's numeric error code -- or null. Empty for no error.
|
|
23
|
+
*/
|
|
24
|
+
fun chain(error: Throwable?, detail: (Throwable) -> String? = { null }): String {
|
|
25
|
+
val links = mutableListOf<String>()
|
|
26
|
+
val seen = mutableSetOf<Throwable>()
|
|
27
|
+
var current = error
|
|
28
|
+
while (current != null && links.size < MAX_LINKS && seen.add(current)) {
|
|
29
|
+
val message = current.message?.trim().orEmpty()
|
|
30
|
+
val extra = detail(current)
|
|
31
|
+
links.add(
|
|
32
|
+
current.javaClass.simpleName +
|
|
33
|
+
(if (message.isEmpty()) "" else ": $message") +
|
|
34
|
+
(if (extra.isNullOrEmpty()) "" else " ($extra)"),
|
|
35
|
+
)
|
|
36
|
+
current = current.cause
|
|
37
|
+
}
|
|
38
|
+
if (current != null && current !in seen) {
|
|
39
|
+
links.add("…")
|
|
40
|
+
}
|
|
41
|
+
return links.joinToString(" < ")
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** [message], then what failed beneath it when anything did. */
|
|
45
|
+
fun withCauses(message: String, cause: Throwable?, detail: (Throwable) -> String? = { null }): String {
|
|
46
|
+
val beneath = chain(cause, detail)
|
|
47
|
+
return if (beneath.isEmpty()) message else "$message [cause: $beneath]"
|
|
48
|
+
}
|
|
49
|
+
}
|