@onekeyfe/react-native-split-bundle-loader 3.0.20 → 3.0.21
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.
|
@@ -7,8 +7,10 @@ import com.facebook.react.bridge.Promise
|
|
|
7
7
|
import com.facebook.react.bridge.ReactApplicationContext
|
|
8
8
|
import com.facebook.react.module.annotations.ReactModule
|
|
9
9
|
import java.io.File
|
|
10
|
+
import java.io.FileInputStream
|
|
10
11
|
import java.io.FileOutputStream
|
|
11
12
|
import java.io.IOException
|
|
13
|
+
import java.security.MessageDigest
|
|
12
14
|
import java.util.concurrent.Semaphore
|
|
13
15
|
|
|
14
16
|
/**
|
|
@@ -171,6 +173,13 @@ class SplitBundleLoaderModule(reactContext: ReactApplicationContext) :
|
|
|
171
173
|
try {
|
|
172
174
|
ensureExtractDirFreshForCurrentInstall(reactApplicationContext)
|
|
173
175
|
val absolutePath = resolveSegmentPath(relativePath, sha256)
|
|
176
|
+
if (absolutePath != null && !verifySha256(absolutePath, sha256)) {
|
|
177
|
+
promise.reject(
|
|
178
|
+
"SPLIT_BUNDLE_SHA256_MISMATCH",
|
|
179
|
+
"Segment SHA-256 mismatch: $relativePath"
|
|
180
|
+
)
|
|
181
|
+
return
|
|
182
|
+
}
|
|
174
183
|
if (absolutePath != null) {
|
|
175
184
|
promise.resolve(absolutePath)
|
|
176
185
|
} else {
|
|
@@ -195,10 +204,6 @@ class SplitBundleLoaderModule(reactContext: ReactApplicationContext) :
|
|
|
195
204
|
sha256: String,
|
|
196
205
|
promise: Promise
|
|
197
206
|
) {
|
|
198
|
-
// NOTE (#44): sha256 param is not verified at load time by design.
|
|
199
|
-
// Per §6.4.1, runtime trusts that OTA install has already verified
|
|
200
|
-
// segment integrity. Builtin segments are signed as part of the APK/IPA.
|
|
201
|
-
// If runtime SHA-256 verification is needed, add it here.
|
|
202
207
|
try {
|
|
203
208
|
ensureExtractDirFreshForCurrentInstall(reactApplicationContext)
|
|
204
209
|
val segId = segmentId.toInt()
|
|
@@ -212,6 +217,18 @@ class SplitBundleLoaderModule(reactContext: ReactApplicationContext) :
|
|
|
212
217
|
return
|
|
213
218
|
}
|
|
214
219
|
|
|
220
|
+
// Per-segment integrity check at load time. Replaces the legacy
|
|
221
|
+
// assumption that install-time validation is sufficient: now
|
|
222
|
+
// BundleUpdate skips full-tree sha256 on startup hot path, so
|
|
223
|
+
// segment integrity is enforced lazily here.
|
|
224
|
+
if (!verifySha256(absolutePath, sha256)) {
|
|
225
|
+
promise.reject(
|
|
226
|
+
"SPLIT_BUNDLE_SHA256_MISMATCH",
|
|
227
|
+
"Segment SHA-256 mismatch: $relativePath (key=$segmentKey)"
|
|
228
|
+
)
|
|
229
|
+
return
|
|
230
|
+
}
|
|
231
|
+
|
|
215
232
|
// Use ReactContext.registerSegment which works in both bridge
|
|
216
233
|
// and bridgeless modes. In bridge mode it delegates to
|
|
217
234
|
// CatalystInstance; in bridgeless mode it delegates to ReactHost.
|
|
@@ -409,4 +426,70 @@ class SplitBundleLoaderModule(reactContext: ReactApplicationContext) :
|
|
|
409
426
|
null
|
|
410
427
|
}
|
|
411
428
|
}
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* Reflectively asks bundle-update whether the current bundle's manifest
|
|
432
|
+
* is required to ship per-segment SHA-256s (three-bundle / split-thread
|
|
433
|
+
* format). When this returns true, an empty [expected] in [verifySha256]
|
|
434
|
+
* is treated as a hard fail instead of the back-compat skip.
|
|
435
|
+
*
|
|
436
|
+
* Returns false (back-compat allowed) if the symbol is absent — older
|
|
437
|
+
* bundle-update versions don't expose this and we don't want to wedge
|
|
438
|
+
* the loader on an upgrade race.
|
|
439
|
+
*/
|
|
440
|
+
private fun currentBundleRequiresPerSegmentHash(): Boolean {
|
|
441
|
+
return try {
|
|
442
|
+
val bundleUpdateStore = Class.forName(
|
|
443
|
+
"com.margelo.nitro.reactnativebundleupdate.BundleUpdateStoreAndroid"
|
|
444
|
+
)
|
|
445
|
+
val method = bundleUpdateStore.getMethod(
|
|
446
|
+
"currentBundleRequiresPerSegmentHash",
|
|
447
|
+
Context::class.java
|
|
448
|
+
)
|
|
449
|
+
// Kotlin `object` instance methods need INSTANCE; @JvmStatic
|
|
450
|
+
// exposes a static method with the same name. Try the static
|
|
451
|
+
// call first, then fall back to INSTANCE so we work either way
|
|
452
|
+
// if the bundle-update side toggles @JvmStatic in the future.
|
|
453
|
+
val result = try {
|
|
454
|
+
method.invoke(null, reactApplicationContext)
|
|
455
|
+
} catch (_: NullPointerException) {
|
|
456
|
+
val instance = bundleUpdateStore.getField("INSTANCE").get(null)
|
|
457
|
+
method.invoke(instance, reactApplicationContext)
|
|
458
|
+
} catch (_: IllegalArgumentException) {
|
|
459
|
+
val instance = bundleUpdateStore.getField("INSTANCE").get(null)
|
|
460
|
+
method.invoke(instance, reactApplicationContext)
|
|
461
|
+
}
|
|
462
|
+
(result as? Boolean) ?: false
|
|
463
|
+
} catch (_: Exception) {
|
|
464
|
+
false
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* Streams [path] and compares its SHA-256 against [expected] (hex,
|
|
470
|
+
* case-insensitive). When [expected] is empty, defers to bundle-update:
|
|
471
|
+
* three-bundle format manifests must ship per-segment hashes (fail
|
|
472
|
+
* closed); older formats are allowed to omit them (back-compat).
|
|
473
|
+
*/
|
|
474
|
+
private fun verifySha256(path: String, expected: String): Boolean {
|
|
475
|
+
if (expected.isEmpty()) {
|
|
476
|
+
return !currentBundleRequiresPerSegmentHash()
|
|
477
|
+
}
|
|
478
|
+
return try {
|
|
479
|
+
val md = MessageDigest.getInstance("SHA-256")
|
|
480
|
+
FileInputStream(path).use { input ->
|
|
481
|
+
val buf = ByteArray(64 * 1024)
|
|
482
|
+
while (true) {
|
|
483
|
+
val n = input.read(buf)
|
|
484
|
+
if (n <= 0) break
|
|
485
|
+
md.update(buf, 0, n)
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
val actual = md.digest().joinToString("") { "%02x".format(it) }
|
|
489
|
+
actual.equals(expected, ignoreCase = true)
|
|
490
|
+
} catch (e: Exception) {
|
|
491
|
+
SBLLogger.warn("verifySha256 failed for $path: ${e.message}")
|
|
492
|
+
false
|
|
493
|
+
}
|
|
494
|
+
}
|
|
412
495
|
}
|
package/ios/SplitBundleLoader.mm
CHANGED
|
@@ -4,10 +4,76 @@
|
|
|
4
4
|
#import <ReactCommon/RCTHost+Internal.h>
|
|
5
5
|
#import <ReactCommon/RCTInstance.h>
|
|
6
6
|
#import <objc/runtime.h>
|
|
7
|
+
#import <CommonCrypto/CommonDigest.h>
|
|
7
8
|
#include <jsi/jsi.h>
|
|
8
9
|
|
|
9
10
|
@implementation SplitBundleLoader
|
|
10
11
|
|
|
12
|
+
// Reflectively asks bundle-update whether the current bundle's manifest is
|
|
13
|
+
// required to ship per-segment SHA-256s (three-bundle / split-thread format).
|
|
14
|
+
// When this returns YES, an empty `expected` in verifySha256OfFile:expected:
|
|
15
|
+
// is treated as a hard fail instead of the back-compat skip. Returns NO when
|
|
16
|
+
// the symbol is absent so older bundle-update versions don't wedge the loader.
|
|
17
|
+
+ (BOOL)currentBundleRequiresPerSegmentHash
|
|
18
|
+
{
|
|
19
|
+
Class cls = NSClassFromString(@"ReactNativeBundleUpdate.BundleUpdateStore");
|
|
20
|
+
if (!cls) return NO;
|
|
21
|
+
SEL sel = NSSelectorFromString(@"currentBundleRequiresPerSegmentHash");
|
|
22
|
+
if (![cls respondsToSelector:sel]) return NO;
|
|
23
|
+
NSMethodSignature *sig = [cls methodSignatureForSelector:sel];
|
|
24
|
+
if (!sig) return NO;
|
|
25
|
+
// BOOL on Apple platforms is signed char (or bool on arm64); accept both.
|
|
26
|
+
const char *retType = sig.methodReturnType;
|
|
27
|
+
if (strcmp(retType, @encode(BOOL)) != 0 &&
|
|
28
|
+
strcmp(retType, @encode(char)) != 0 &&
|
|
29
|
+
strcmp(retType, @encode(bool)) != 0) {
|
|
30
|
+
return NO;
|
|
31
|
+
}
|
|
32
|
+
NSInvocation *inv = [NSInvocation invocationWithMethodSignature:sig];
|
|
33
|
+
inv.target = cls;
|
|
34
|
+
inv.selector = sel;
|
|
35
|
+
[inv invoke];
|
|
36
|
+
BOOL result = NO;
|
|
37
|
+
[inv getReturnValue:&result];
|
|
38
|
+
return result;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Segment-time SHA-256 verification helper. Returns YES on match, NO on
|
|
42
|
+
// mismatch or read failure. Streams the file with a fixed-size buffer so a
|
|
43
|
+
// large segment doesn't pull the whole bundle into memory just to hash it.
|
|
44
|
+
+ (BOOL)verifySha256OfFile:(NSString *)path expected:(NSString *)expected
|
|
45
|
+
{
|
|
46
|
+
if (expected.length == 0) {
|
|
47
|
+
// Defer to bundle-update: three-bundle format manifests must ship
|
|
48
|
+
// per-segment hashes (fail closed); older formats are allowed to omit
|
|
49
|
+
// them (back-compat).
|
|
50
|
+
return ![SplitBundleLoader currentBundleRequiresPerSegmentHash];
|
|
51
|
+
}
|
|
52
|
+
NSFileHandle *handle = [NSFileHandle fileHandleForReadingAtPath:path];
|
|
53
|
+
if (!handle) {
|
|
54
|
+
return NO;
|
|
55
|
+
}
|
|
56
|
+
CC_SHA256_CTX ctx;
|
|
57
|
+
CC_SHA256_Init(&ctx);
|
|
58
|
+
@try {
|
|
59
|
+
const NSUInteger chunk = 64 * 1024;
|
|
60
|
+
while (1) {
|
|
61
|
+
NSData *data = [handle readDataOfLength:chunk];
|
|
62
|
+
if (data.length == 0) break;
|
|
63
|
+
CC_SHA256_Update(&ctx, data.bytes, (CC_LONG)data.length);
|
|
64
|
+
}
|
|
65
|
+
} @finally {
|
|
66
|
+
[handle closeFile];
|
|
67
|
+
}
|
|
68
|
+
unsigned char digest[CC_SHA256_DIGEST_LENGTH];
|
|
69
|
+
CC_SHA256_Final(digest, &ctx);
|
|
70
|
+
NSMutableString *hex = [NSMutableString stringWithCapacity:CC_SHA256_DIGEST_LENGTH * 2];
|
|
71
|
+
for (int i = 0; i < CC_SHA256_DIGEST_LENGTH; i++) {
|
|
72
|
+
[hex appendFormat:@"%02x", digest[i]];
|
|
73
|
+
}
|
|
74
|
+
return [hex caseInsensitiveCompare:expected] == NSOrderedSame;
|
|
75
|
+
}
|
|
76
|
+
|
|
11
77
|
- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
|
|
12
78
|
(const facebook::react::ObjCTurboModule::InitParams &)params
|
|
13
79
|
{
|
|
@@ -175,13 +241,19 @@
|
|
|
175
241
|
}
|
|
176
242
|
|
|
177
243
|
NSString *absolutePath = [SplitBundleLoader resolveAbsolutePath:relativePath];
|
|
178
|
-
if (absolutePath) {
|
|
179
|
-
resolve(absolutePath);
|
|
180
|
-
} else {
|
|
244
|
+
if (!absolutePath) {
|
|
181
245
|
reject(@"SPLIT_BUNDLE_NOT_FOUND",
|
|
182
246
|
[NSString stringWithFormat:@"Segment file not found: %@", relativePath],
|
|
183
247
|
nil);
|
|
248
|
+
return;
|
|
184
249
|
}
|
|
250
|
+
if (![SplitBundleLoader verifySha256OfFile:absolutePath expected:sha256]) {
|
|
251
|
+
reject(@"SPLIT_BUNDLE_SHA256_MISMATCH",
|
|
252
|
+
[NSString stringWithFormat:@"Segment SHA-256 mismatch: %@", relativePath],
|
|
253
|
+
nil);
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
resolve(absolutePath);
|
|
185
257
|
} @catch (NSException *exception) {
|
|
186
258
|
reject(@"SPLIT_BUNDLE_RESOLVE_ERROR", exception.reason, nil);
|
|
187
259
|
}
|
|
@@ -263,6 +335,13 @@
|
|
|
263
335
|
return;
|
|
264
336
|
}
|
|
265
337
|
|
|
338
|
+
if (![SplitBundleLoader verifySha256OfFile:absolutePath expected:sha256]) {
|
|
339
|
+
reject(@"SPLIT_BUNDLE_SHA256_MISMATCH",
|
|
340
|
+
[NSString stringWithFormat:@"Segment SHA-256 mismatch: %@ (key=%@)", relativePath, segmentKey],
|
|
341
|
+
nil);
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
|
|
266
345
|
// Register segment (#13: supports both bridge and bridgeless)
|
|
267
346
|
NSError *regError = nil;
|
|
268
347
|
if ([SplitBundleLoader registerSegment:segId path:absolutePath error:®Error]) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@onekeyfe/react-native-split-bundle-loader",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.21",
|
|
4
4
|
"description": "react-native-split-bundle-loader",
|
|
5
5
|
"main": "./lib/module/index.js",
|
|
6
6
|
"types": "./lib/typescript/src/index.d.ts",
|
|
@@ -82,6 +82,7 @@
|
|
|
82
82
|
"typescript": "^5.9.2"
|
|
83
83
|
},
|
|
84
84
|
"peerDependencies": {
|
|
85
|
+
"@onekeyfe/react-native-bundle-update": ">=3.0.21",
|
|
85
86
|
"react": "*",
|
|
86
87
|
"react-native": "*"
|
|
87
88
|
},
|