@onekeyfe/react-native-bundle-update 6.7.0-1 → 6.7.0-alpha.311
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/android/src/main/java/com/margelo/nitro/reactnativebundleupdate/ReactNativeBundleUpdate.kt +604 -230
- package/ios/ReactNativeBundleUpdate.swift +782 -198
- package/lib/typescript/src/ReactNativeBundleUpdate.nitro.d.ts +0 -5
- package/nitrogen/generated/android/c++/JBundleDownloadASCParams.hpp +1 -5
- package/nitrogen/generated/android/c++/JBundleDownloadResult.hpp +1 -5
- package/nitrogen/generated/android/c++/JBundleInstallParams.hpp +1 -5
- package/nitrogen/generated/android/c++/JBundleVerifyASCParams.hpp +1 -5
- package/nitrogen/generated/android/c++/JBundleVerifyParams.hpp +1 -5
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativebundleupdate/BundleDownloadASCParams.kt +2 -7
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativebundleupdate/BundleDownloadResult.kt +3 -8
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativebundleupdate/BundleInstallParams.kt +3 -8
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativebundleupdate/BundleVerifyASCParams.kt +3 -8
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativebundleupdate/BundleVerifyParams.kt +3 -8
- package/nitrogen/generated/ios/swift/BundleDownloadASCParams.swift +2 -7
- package/nitrogen/generated/ios/swift/BundleDownloadResult.swift +2 -7
- package/nitrogen/generated/ios/swift/BundleInstallParams.swift +2 -7
- package/nitrogen/generated/ios/swift/BundleVerifyASCParams.swift +2 -7
- package/nitrogen/generated/ios/swift/BundleVerifyParams.swift +2 -7
- package/nitrogen/generated/shared/c++/BundleDownloadASCParams.hpp +1 -5
- package/nitrogen/generated/shared/c++/BundleDownloadResult.hpp +1 -5
- package/nitrogen/generated/shared/c++/BundleInstallParams.hpp +1 -5
- package/nitrogen/generated/shared/c++/BundleVerifyASCParams.hpp +1 -5
- package/nitrogen/generated/shared/c++/BundleVerifyParams.hpp +1 -5
- package/package.json +1 -1
- package/src/ReactNativeBundleUpdate.nitro.ts +0 -5
package/android/src/main/java/com/margelo/nitro/reactnativebundleupdate/ReactNativeBundleUpdate.kt
CHANGED
|
@@ -15,11 +15,13 @@ import okhttp3.Request
|
|
|
15
15
|
import java.io.File
|
|
16
16
|
import java.io.FileInputStream
|
|
17
17
|
import java.io.FileOutputStream
|
|
18
|
+
import java.io.IOException
|
|
18
19
|
import java.nio.file.Files
|
|
19
20
|
import java.nio.file.Path
|
|
20
21
|
import java.nio.file.Paths
|
|
21
22
|
import java.util.concurrent.ConcurrentHashMap
|
|
22
23
|
import java.util.concurrent.CopyOnWriteArrayList
|
|
24
|
+
import java.util.concurrent.CountDownLatch
|
|
23
25
|
import java.util.concurrent.TimeUnit
|
|
24
26
|
import java.util.concurrent.atomic.AtomicInteger
|
|
25
27
|
import java.util.concurrent.atomic.AtomicLong
|
|
@@ -33,6 +35,49 @@ private data class BundleListener(
|
|
|
33
35
|
val callback: (BundleDownloadEvent) -> Unit
|
|
34
36
|
)
|
|
35
37
|
|
|
38
|
+
private class BundleUpdateError(
|
|
39
|
+
val code: String,
|
|
40
|
+
val step: String,
|
|
41
|
+
val httpStatus: Int? = null,
|
|
42
|
+
) : Exception(buildString {
|
|
43
|
+
append("ONEKEY_UPDATE_ERROR:")
|
|
44
|
+
append(JSONObject().put("code", code).put("step", step).apply {
|
|
45
|
+
if (code == "HTTP_ERROR" && httpStatus != null && httpStatus in 100..599) {
|
|
46
|
+
put("httpStatus", httpStatus)
|
|
47
|
+
}
|
|
48
|
+
}.toString())
|
|
49
|
+
}) {
|
|
50
|
+
override fun printStackTrace(writer: java.io.PrintWriter) { writer.print(message.orEmpty()) }
|
|
51
|
+
override fun printStackTrace(stream: java.io.PrintStream) { stream.print(message.orEmpty()) }
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
private fun bundleUpdateError(error: Exception, step: String): BundleUpdateError {
|
|
55
|
+
if (error is BundleUpdateError) {
|
|
56
|
+
return if (error.step == step) error else BundleUpdateError(error.code, step, error.httpStatus)
|
|
57
|
+
}
|
|
58
|
+
val code = when (error) {
|
|
59
|
+
is ConcurrentRangeDownloader.PermanentHttpException,
|
|
60
|
+
is ConcurrentRangeDownloader.TransientHttpException -> "HTTP_ERROR"
|
|
61
|
+
is java.net.SocketTimeoutException -> "TIMEOUT"
|
|
62
|
+
is java.net.UnknownHostException,
|
|
63
|
+
is java.net.ConnectException,
|
|
64
|
+
is java.net.ProtocolException,
|
|
65
|
+
is java.net.SocketException -> "NETWORK_ERROR"
|
|
66
|
+
is javax.net.ssl.SSLException -> "SECURITY_REJECTED"
|
|
67
|
+
is java.io.FileNotFoundException,
|
|
68
|
+
is java.nio.file.AccessDeniedException -> "STORAGE_ERROR"
|
|
69
|
+
is IllegalArgumentException -> "INVALID_ARGUMENT"
|
|
70
|
+
is SecurityException -> "SECURITY_REJECTED"
|
|
71
|
+
else -> "UNKNOWN"
|
|
72
|
+
}
|
|
73
|
+
val status = when (error) {
|
|
74
|
+
is ConcurrentRangeDownloader.PermanentHttpException -> error.code
|
|
75
|
+
is ConcurrentRangeDownloader.TransientHttpException -> error.code
|
|
76
|
+
else -> null
|
|
77
|
+
}
|
|
78
|
+
return BundleUpdateError(code, step, status)
|
|
79
|
+
}
|
|
80
|
+
|
|
36
81
|
// Public static store for CustomReactNativeHost access (called before JS starts)
|
|
37
82
|
object BundleUpdateStoreAndroid {
|
|
38
83
|
private const val PREFS_NAME = "BundleUpdatePrefs"
|
|
@@ -162,6 +207,7 @@ object BundleUpdateStoreAndroid {
|
|
|
162
207
|
}
|
|
163
208
|
|
|
164
209
|
fun clearUpdateBundleData(context: Context) {
|
|
210
|
+
didProcessPreLaunchTask = true
|
|
165
211
|
invalidateValidatedBundleInfoCache()
|
|
166
212
|
val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
|
167
213
|
prefs.edit().clear().commit()
|
|
@@ -249,6 +295,16 @@ object BundleUpdateStoreAndroid {
|
|
|
249
295
|
* Only handles the happy path (status=pending, bundle exists, env matches).
|
|
250
296
|
* All complex logic (retry, download, error handling) remains in JS layer.
|
|
251
297
|
*/
|
|
298
|
+
private fun failPendingTask(mmkv: MMKV, taskObj: JSONObject, reason: String) {
|
|
299
|
+
try {
|
|
300
|
+
taskObj.put("status", "failed")
|
|
301
|
+
taskObj.put("lastError", reason)
|
|
302
|
+
mmkv.encode("onekey_pending_install_task", taskObj.toString())
|
|
303
|
+
} catch (e: Exception) {
|
|
304
|
+
OneKeyLog.warn("BundleUpdate", "processPreLaunchPendingTask: failed to update MMKV task failure: ${e.message}")
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
252
308
|
fun processPreLaunchPendingTask(context: Context) {
|
|
253
309
|
if (didProcessPreLaunchTask) return
|
|
254
310
|
didProcessPreLaunchTask = true
|
|
@@ -282,11 +338,8 @@ object BundleUpdateStoreAndroid {
|
|
|
282
338
|
|
|
283
339
|
val currentBV = getCurrentBundleVersion(context)
|
|
284
340
|
val currentBundleVersionStr = if (currentBV != null) {
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
} else {
|
|
288
|
-
getBuiltinBundleVersion(context)
|
|
289
|
-
}
|
|
341
|
+
bundleVersionInFolder(currentBV, currentAppVersion) ?: return
|
|
342
|
+
} else getBuiltinBundleVersion(context)
|
|
290
343
|
if (taskObj.optString("scheduledEnvBundleVersion") != currentBundleVersionStr) return
|
|
291
344
|
|
|
292
345
|
// 4. Extract payload
|
|
@@ -295,20 +348,67 @@ object BundleUpdateStoreAndroid {
|
|
|
295
348
|
val bundleVersion = payload.optString("bundleVersion")
|
|
296
349
|
val signature = payload.optString("signature")
|
|
297
350
|
if (appVersion.isEmpty() || bundleVersion.isEmpty() || signature.isEmpty()) return
|
|
298
|
-
if (!isSafeVersionString(appVersion) || !isSafeVersionString(bundleVersion))
|
|
351
|
+
if (!isSafeVersionString(appVersion) || !isSafeVersionString(bundleVersion)) {
|
|
352
|
+
failPendingTask(mmkv, taskObj, "ERR_VERIFY_FAILED:Invalid version string")
|
|
353
|
+
return
|
|
354
|
+
}
|
|
355
|
+
if (appVersion != currentAppVersion) {
|
|
356
|
+
OneKeyLog.warn("BundleUpdate", "processPreLaunchPendingTask: target appVersion ($appVersion) != currentAppVersion ($currentAppVersion)")
|
|
357
|
+
failPendingTask(mmkv, taskObj, "ERR_ENV_CHANGED:Target appVersion mismatch")
|
|
358
|
+
return
|
|
359
|
+
}
|
|
299
360
|
|
|
300
|
-
// 5. Verify bundle directory and
|
|
361
|
+
// 5. Verify bundle directory, canonical path confinement, and required files
|
|
301
362
|
val folderName = "$appVersion-$bundleVersion"
|
|
363
|
+
val baseBundleDir = File(getBundleDir(context)).canonicalPath
|
|
302
364
|
val bundleDirPath = File(getBundleDir(context), folderName)
|
|
365
|
+
if (Files.isSymbolicLink(bundleDirPath.toPath()) ||
|
|
366
|
+
!bundleDirPath.canonicalPath.startsWith(baseBundleDir + File.separator)
|
|
367
|
+
) {
|
|
368
|
+
OneKeyLog.error("BundleUpdate", "processPreLaunchPendingTask: bundle path outside bundle directory: ${bundleDirPath.canonicalPath}")
|
|
369
|
+
failPendingTask(mmkv, taskObj, "ERR_VERIFY_FAILED:Bundle path outside bundle directory")
|
|
370
|
+
return
|
|
371
|
+
}
|
|
372
|
+
val sigFile = File(getSignatureFilePath(context, folderName))
|
|
373
|
+
val baseAscDir = File(getAscDir(context)).canonicalPath
|
|
374
|
+
if (Files.isSymbolicLink(sigFile.toPath()) ||
|
|
375
|
+
!sigFile.canonicalPath.startsWith(baseAscDir + File.separator)
|
|
376
|
+
) {
|
|
377
|
+
OneKeyLog.error("BundleUpdate", "processPreLaunchPendingTask: signature path outside asc directory: ${sigFile.canonicalPath}")
|
|
378
|
+
failPendingTask(mmkv, taskObj, "ERR_VERIFY_FAILED:Signature path outside asc directory")
|
|
379
|
+
return
|
|
380
|
+
}
|
|
303
381
|
if (!bundleDirPath.exists()) return
|
|
304
382
|
val entryFile = File(bundleDirPath, "main.jsbundle.hbc")
|
|
305
|
-
|
|
306
|
-
|
|
383
|
+
val metadataFile = File(bundleDirPath, "metadata.json")
|
|
384
|
+
if (!entryFile.exists() || !metadataFile.exists()) {
|
|
385
|
+
OneKeyLog.warn("BundleUpdate", "processPreLaunchPendingTask: bundle dir exists but required files missing: ${bundleDirPath.absolutePath}")
|
|
386
|
+
failPendingTask(mmkv, taskObj, "ERR_VERIFY_FAILED:Required bundle files missing")
|
|
307
387
|
return
|
|
308
388
|
}
|
|
309
389
|
|
|
310
|
-
|
|
311
|
-
|
|
390
|
+
val skipGPG = BuildConfig.ALLOW_SKIP_GPG_VERIFICATION && isDevSettingsEnabled(context) && isSkipGPGEnabled(context)
|
|
391
|
+
if (!skipGPG && !validateMetadataFileSha256(context, folderName, signature, appVersion, bundleVersion)) {
|
|
392
|
+
OneKeyLog.error("BundleUpdate", "processPreLaunchPendingTask: GPG signature verification failed for $folderName")
|
|
393
|
+
failPendingTask(mmkv, taskObj, "ERR_VERIFY_FAILED:Bundle signature verification failed")
|
|
394
|
+
return
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
val metadata = parseMetadataJson(readFileContent(metadataFile))
|
|
398
|
+
if (!validateEntryBundlesSha256(bundleDirPath.absolutePath, metadata)) {
|
|
399
|
+
OneKeyLog.error("BundleUpdate", "processPreLaunchPendingTask: entry bundle SHA256 verification failed for $folderName")
|
|
400
|
+
failPendingTask(mmkv, taskObj, "ERR_VERIFY_FAILED:Entry bundle SHA256 verification failed")
|
|
401
|
+
return
|
|
402
|
+
}
|
|
403
|
+
if (!validateBundlePairCompatibility(context, bundleDirPath.absolutePath, metadata)) {
|
|
404
|
+
OneKeyLog.error("BundleUpdate", "processPreLaunchPendingTask: bundle pair compatibility check failed for $folderName")
|
|
405
|
+
failPendingTask(mmkv, taskObj, "ERR_VERIFY_FAILED:Bundle pair compatibility check failed")
|
|
406
|
+
return
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// 6. Apply (same as installBundle) — write signature BEFORE flipping
|
|
410
|
+
// currentBundleVersion, and use commit() for synchronous writes.
|
|
411
|
+
writeSignatureFile(context, folderName, signature)
|
|
312
412
|
val bundlePrefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
|
313
413
|
val versionPrefs = context.getSharedPreferences(NATIVE_VERSION_PREFS_NAME, Context.MODE_PRIVATE)
|
|
314
414
|
val currentVersion = bundlePrefs.getString(CURRENT_BUNDLE_VERSION_KEY, "")
|
|
@@ -320,9 +420,6 @@ object BundleUpdateStoreAndroid {
|
|
|
320
420
|
}
|
|
321
421
|
}
|
|
322
422
|
.commit()
|
|
323
|
-
if (!signature.isNullOrEmpty()) {
|
|
324
|
-
writeSignatureFile(context, folderName, signature)
|
|
325
|
-
}
|
|
326
423
|
versionPrefs.edit()
|
|
327
424
|
.putString("nativeVersion", currentAppVersion)
|
|
328
425
|
.putString("nativeBuildNumber", getBuildNumber(context))
|
|
@@ -470,11 +567,30 @@ object BundleUpdateStoreAndroid {
|
|
|
470
567
|
return compatible
|
|
471
568
|
}
|
|
472
569
|
|
|
473
|
-
fun
|
|
570
|
+
fun bundleVersionInFolder(folder: String, appVersion: String): String? {
|
|
571
|
+
if (!isSafeVersionString(appVersion)) return null
|
|
572
|
+
val prefix = "$appVersion-"
|
|
573
|
+
if (!folder.startsWith(prefix)) return null
|
|
574
|
+
val bundleVersion = folder.removePrefix(prefix)
|
|
575
|
+
return bundleVersion.takeIf { isSafeVersionString(it) }
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
@JvmOverloads
|
|
579
|
+
fun readMetadataFileSha256(
|
|
580
|
+
signature: String?,
|
|
581
|
+
expectedAppVersion: String? = null,
|
|
582
|
+
expectedBundleVersion: String? = null,
|
|
583
|
+
expectedAppType: String? = "android",
|
|
584
|
+
): String? {
|
|
474
585
|
if (signature.isNullOrEmpty()) return null
|
|
475
586
|
|
|
476
587
|
// GPG cleartext signature verification is required
|
|
477
|
-
val gpgResult = verifyGPGAndExtractSha256(
|
|
588
|
+
val gpgResult = verifyGPGAndExtractSha256(
|
|
589
|
+
signature,
|
|
590
|
+
expectedAppVersion = expectedAppVersion,
|
|
591
|
+
expectedBundleVersion = expectedBundleVersion,
|
|
592
|
+
expectedAppType = expectedAppType,
|
|
593
|
+
)
|
|
478
594
|
if (gpgResult != null) return gpgResult
|
|
479
595
|
|
|
480
596
|
OneKeyLog.error("BundleUpdate", "readMetadataFileSha256: GPG verification failed, rejecting unsigned content")
|
|
@@ -505,8 +621,19 @@ object BundleUpdateStoreAndroid {
|
|
|
505
621
|
* (NOT_PGP_SIGNED_MESSAGE / INVALID_FORMAT / NO_SIGNATURE / PUBKEY_NOT_FOUND
|
|
506
622
|
* / SIGNATURE_INVALID / NO_SHA256_FIELD / ERROR_*) maps to null.
|
|
507
623
|
*/
|
|
508
|
-
|
|
509
|
-
|
|
624
|
+
@JvmOverloads
|
|
625
|
+
fun verifyGPGAndExtractSha256(
|
|
626
|
+
signature: String,
|
|
627
|
+
expectedAppVersion: String? = null,
|
|
628
|
+
expectedBundleVersion: String? = null,
|
|
629
|
+
expectedAppType: String? = "android",
|
|
630
|
+
): String? {
|
|
631
|
+
val result = BundleCryptoCore.verifyGpgCleartext(
|
|
632
|
+
signature,
|
|
633
|
+
expectedAppVersion = expectedAppVersion,
|
|
634
|
+
expectedBundleVersion = expectedBundleVersion,
|
|
635
|
+
expectedAppType = expectedAppType,
|
|
636
|
+
)
|
|
510
637
|
if (!result.valid) {
|
|
511
638
|
OneKeyLog.error("BundleUpdate", "GPG verification failed: ${result.reason}")
|
|
512
639
|
return null
|
|
@@ -514,12 +641,19 @@ object BundleUpdateStoreAndroid {
|
|
|
514
641
|
return result.sha256
|
|
515
642
|
}
|
|
516
643
|
|
|
517
|
-
fun validateMetadataFileSha256(context: Context, currentBundleVersion: String, signature: String
|
|
644
|
+
fun validateMetadataFileSha256(context: Context, currentBundleVersion: String, signature: String?, expectedAppVersion: String, expectedBundleVersion: String): Boolean {
|
|
518
645
|
val metadataFilePath = getMetadataFilePath(context, currentBundleVersion) ?: run {
|
|
519
646
|
OneKeyLog.debug("BundleUpdate", "metadataFilePath is null")
|
|
520
647
|
return false
|
|
521
648
|
}
|
|
522
|
-
|
|
649
|
+
if (!isSafeVersionString(expectedAppVersion) || !isSafeVersionString(expectedBundleVersion) ||
|
|
650
|
+
currentBundleVersion != "$expectedAppVersion-$expectedBundleVersion") return false
|
|
651
|
+
val extractedSha256 = readMetadataFileSha256(
|
|
652
|
+
signature,
|
|
653
|
+
expectedAppVersion = expectedAppVersion,
|
|
654
|
+
expectedBundleVersion = expectedBundleVersion,
|
|
655
|
+
expectedAppType = "android",
|
|
656
|
+
)
|
|
523
657
|
if (extractedSha256.isNullOrEmpty()) return false
|
|
524
658
|
val calculated = calculateSHA256(metadataFilePath) ?: return false
|
|
525
659
|
return secureCompare(calculated, extractedSha256)
|
|
@@ -717,7 +851,7 @@ object BundleUpdateStoreAndroid {
|
|
|
717
851
|
private fun getValidatedCurrentBundleInfo(context: Context): ValidatedBundleInfo? {
|
|
718
852
|
processPreLaunchPendingTask(context)
|
|
719
853
|
return try {
|
|
720
|
-
val currentAppVersion = getAppVersion(context)
|
|
854
|
+
val currentAppVersion = getAppVersion(context) ?: return null
|
|
721
855
|
val currentBundleVersion = getCurrentBundleVersion(context) ?: run {
|
|
722
856
|
OneKeyLog.warn("BundleUpdate", "getJsBundlePath: no currentBundleVersion stored")
|
|
723
857
|
invalidateValidatedBundleInfoCache()
|
|
@@ -775,11 +909,12 @@ object BundleUpdateStoreAndroid {
|
|
|
775
909
|
val signature = readSignatureFile(context, currentBundleVersion)
|
|
776
910
|
OneKeyLog.debug("BundleUpdate", "getJsBundlePath: signatureLength=${signature.length}")
|
|
777
911
|
|
|
778
|
-
val
|
|
779
|
-
if (
|
|
780
|
-
OneKeyLog.warn("BundleUpdate", "Startup SHA256 validation skipped (DevSettings enabled)")
|
|
912
|
+
val skipGPG = BuildConfig.ALLOW_SKIP_GPG_VERIFICATION && isDevSettingsEnabled(context) && isSkipGPGEnabled(context)
|
|
913
|
+
if (skipGPG) {
|
|
914
|
+
OneKeyLog.warn("BundleUpdate", "Startup SHA256 validation skipped (DevSettings + skip-GPG enabled)")
|
|
781
915
|
}
|
|
782
|
-
|
|
916
|
+
val currentBundleRelease = bundleVersionInFolder(currentBundleVersion, currentAppVersion) ?: return null
|
|
917
|
+
if (!skipGPG && !validateMetadataFileSha256(context, currentBundleVersion, signature, currentAppVersion, currentBundleRelease)) {
|
|
783
918
|
OneKeyLog.warn("BundleUpdate", "getJsBundlePath: validateMetadataFileSha256 failed, signatureLength=${signature.length}")
|
|
784
919
|
return null
|
|
785
920
|
}
|
|
@@ -1066,6 +1201,7 @@ object BundleUpdateStoreAndroid {
|
|
|
1066
1201
|
fun deleteDir(dir: File): Boolean = deleteDirectory(dir)
|
|
1067
1202
|
|
|
1068
1203
|
private const val MAX_UNZIPPED_SIZE = 512L * 1024 * 1024 // 512 MB limit
|
|
1204
|
+
private const val MAX_ZIP_ENTRIES = 4096
|
|
1069
1205
|
|
|
1070
1206
|
fun unzipFile(zipFilePath: String, destDirectory: String) {
|
|
1071
1207
|
val destDir = File(destDirectory)
|
|
@@ -1073,9 +1209,14 @@ object BundleUpdateStoreAndroid {
|
|
|
1073
1209
|
|
|
1074
1210
|
val destDirPath: Path = Paths.get(destDir.canonicalPath)
|
|
1075
1211
|
var totalBytesWritten = 0L
|
|
1212
|
+
var entryCount = 0
|
|
1076
1213
|
ZipInputStream(FileInputStream(zipFilePath)).use { zipIn ->
|
|
1077
1214
|
var entry: ZipEntry? = zipIn.nextEntry
|
|
1078
1215
|
while (entry != null) {
|
|
1216
|
+
entryCount++
|
|
1217
|
+
if (entryCount > MAX_ZIP_ENTRIES) {
|
|
1218
|
+
throw SecurityException("Zip entry count exceeds maximum allowed ($MAX_ZIP_ENTRIES)")
|
|
1219
|
+
}
|
|
1079
1220
|
val outFile = File(destDir, entry.name)
|
|
1080
1221
|
val outPath: Path = Paths.get(outFile.canonicalPath)
|
|
1081
1222
|
if (!outPath.startsWith(destDirPath)) {
|
|
@@ -1135,7 +1276,9 @@ class ReactNativeBundleUpdate : HybridReactNativeBundleUpdateSpec() {
|
|
|
1135
1276
|
// in-memory map, a crashed run leaves no stale lock — the entry is gone on
|
|
1136
1277
|
// relaunch, giving inherent stale-lock recovery (OCDS §5.8).
|
|
1137
1278
|
private val activeDownloads =
|
|
1138
|
-
ConcurrentHashMap<String, ConcurrentRangeDownloader.CancelHandle
|
|
1279
|
+
ConcurrentHashMap<String, Pair<ConcurrentRangeDownloader.CancelHandle, CountDownLatch>>()
|
|
1280
|
+
private val clearDownloadsLock = Any()
|
|
1281
|
+
private var clearingDownloads = false
|
|
1139
1282
|
private val httpClient = OkHttpClient.Builder()
|
|
1140
1283
|
// OCDS §5.4 timeouts. `readTimeout` is the primary inter-byte stall
|
|
1141
1284
|
// window: OkHttp raises SocketTimeoutException when no bytes arrive within
|
|
@@ -1148,7 +1291,7 @@ class ReactNativeBundleUpdate : HybridReactNativeBundleUpdateSpec() {
|
|
|
1148
1291
|
.addNetworkInterceptor { chain ->
|
|
1149
1292
|
val req = chain.request()
|
|
1150
1293
|
if (!req.url.isHttps) {
|
|
1151
|
-
throw
|
|
1294
|
+
throw SecurityException("Redirect to non-HTTPS URL is not allowed: ${req.url}")
|
|
1152
1295
|
}
|
|
1153
1296
|
chain.proceed(req)
|
|
1154
1297
|
}
|
|
@@ -1165,33 +1308,16 @@ class ReactNativeBundleUpdate : HybridReactNativeBundleUpdateSpec() {
|
|
|
1165
1308
|
}
|
|
1166
1309
|
}
|
|
1167
1310
|
|
|
1168
|
-
/**
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
* - "Already downloading" / "Invalid version string format" /
|
|
1179
|
-
* "Bundle download URL must use HTTPS" → preserved verbatim; the
|
|
1180
|
-
* hooks unrecoverable-list matches them by exact substring.
|
|
1181
|
-
* - Anything else → "IO_<exceptionClassName>" so the JS extractor
|
|
1182
|
-
* splits the bucket on exception class without leaking the message.
|
|
1183
|
-
*/
|
|
1184
|
-
private fun sanitizeErrorMessageForEvent(e: Exception): String {
|
|
1185
|
-
val msg = e.message ?: return "IO_${e.javaClass.simpleName}"
|
|
1186
|
-
if (msg.startsWith("Bundle SHA256 verification failed:")) return msg
|
|
1187
|
-
if (msg.startsWith("HTTP ")) return msg
|
|
1188
|
-
if (msg == "Already downloading" ||
|
|
1189
|
-
msg == "Invalid version string format" ||
|
|
1190
|
-
msg == "Bundle download URL must use HTTPS" ||
|
|
1191
|
-
msg == "Empty response body" ||
|
|
1192
|
-
msg == "Failed to finalize download"
|
|
1193
|
-
) return msg
|
|
1194
|
-
return "IO_${e.javaClass.simpleName}"
|
|
1311
|
+
/** Keep raw exceptions in local logs and send only the fixed update wire to JS. */
|
|
1312
|
+
private fun <T> updatePromise(step: String, work: suspend () -> T): Promise<T> {
|
|
1313
|
+
return Promise.async {
|
|
1314
|
+
try {
|
|
1315
|
+
work()
|
|
1316
|
+
} catch (e: Exception) {
|
|
1317
|
+
OneKeyLog.error("BundleUpdate", "$step failed: ${e.javaClass.simpleName}: ${e.message}")
|
|
1318
|
+
throw bundleUpdateError(e, step)
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1195
1321
|
}
|
|
1196
1322
|
|
|
1197
1323
|
override fun addDownloadListener(callback: (BundleDownloadEvent) -> Unit): Double {
|
|
@@ -1239,7 +1365,7 @@ class ReactNativeBundleUpdate : HybridReactNativeBundleUpdateSpec() {
|
|
|
1239
1365
|
}
|
|
1240
1366
|
|
|
1241
1367
|
override fun downloadBundle(params: BundleDownloadParams): Promise<BundleDownloadResult> {
|
|
1242
|
-
return
|
|
1368
|
+
return updatePromise("download") {
|
|
1243
1369
|
// Per-dest single-flight is acquired below once `filePath` is known.
|
|
1244
1370
|
// Input validation (version/HTTPS) runs FIRST and is intentionally
|
|
1245
1371
|
// NOT gated by the lock — a malformed request must fail the same way
|
|
@@ -1247,6 +1373,8 @@ class ReactNativeBundleUpdate : HybridReactNativeBundleUpdateSpec() {
|
|
|
1247
1373
|
// lock entry it then has to unwind.
|
|
1248
1374
|
var acquiredKey: String? = null
|
|
1249
1375
|
var cancelHandle: ConcurrentRangeDownloader.CancelHandle? = null
|
|
1376
|
+
var completionLatch: CountDownLatch? = null
|
|
1377
|
+
var duplicateDownload = false
|
|
1250
1378
|
|
|
1251
1379
|
try {
|
|
1252
1380
|
val context = getContext()
|
|
@@ -1260,12 +1388,12 @@ class ReactNativeBundleUpdate : HybridReactNativeBundleUpdateSpec() {
|
|
|
1260
1388
|
if (!BundleUpdateStoreAndroid.isSafeVersionString(appVersion) ||
|
|
1261
1389
|
!BundleUpdateStoreAndroid.isSafeVersionString(bundleVersion)) {
|
|
1262
1390
|
OneKeyLog.error("BundleUpdate", "downloadBundle: invalid version string format: appVersion=$appVersion, bundleVersion=$bundleVersion")
|
|
1263
|
-
throw
|
|
1391
|
+
throw BundleUpdateError("INVALID_ARGUMENT", "download")
|
|
1264
1392
|
}
|
|
1265
1393
|
|
|
1266
1394
|
if (!downloadUrl.startsWith("https://")) {
|
|
1267
1395
|
OneKeyLog.error("BundleUpdate", "downloadBundle: URL is not HTTPS: $downloadUrl")
|
|
1268
|
-
throw
|
|
1396
|
+
throw BundleUpdateError("SECURITY_REJECTED", "download")
|
|
1269
1397
|
}
|
|
1270
1398
|
|
|
1271
1399
|
val fileName = "$appVersion-$bundleVersion.zip"
|
|
@@ -1282,15 +1410,24 @@ class ReactNativeBundleUpdate : HybridReactNativeBundleUpdateSpec() {
|
|
|
1282
1410
|
// concurrently. The handle is passed to the concurrent downloader so
|
|
1283
1411
|
// clearDownload/clearBundle can stop its workers before deleting files.
|
|
1284
1412
|
val handle = ConcurrentRangeDownloader.CancelHandle()
|
|
1285
|
-
|
|
1413
|
+
val finished = CountDownLatch(1)
|
|
1414
|
+
val registration = handle to finished
|
|
1415
|
+
val wasActive = synchronized(activeDownloads) {
|
|
1416
|
+
clearingDownloads || activeDownloads.putIfAbsent(filePath, registration) != null
|
|
1417
|
+
}
|
|
1418
|
+
if (wasActive) {
|
|
1286
1419
|
OneKeyLog.warn("BundleUpdate", "downloadBundle: rejected, already downloading $filePath")
|
|
1287
|
-
|
|
1420
|
+
duplicateDownload = true
|
|
1421
|
+
throw BundleUpdateError("INVALID_ARGUMENT", "download")
|
|
1288
1422
|
}
|
|
1289
1423
|
acquiredKey = filePath
|
|
1290
1424
|
cancelHandle = handle
|
|
1425
|
+
completionLatch = finished
|
|
1426
|
+
fun ensureNotCancelled() {
|
|
1427
|
+
if (handle.aborted.get()) throw BundleUpdateError("CANCELLED", "download")
|
|
1428
|
+
}
|
|
1291
1429
|
|
|
1292
1430
|
val result = BundleDownloadResult(
|
|
1293
|
-
downloadedFile = filePath,
|
|
1294
1431
|
downloadUrl = downloadUrl,
|
|
1295
1432
|
latestVersion = appVersion,
|
|
1296
1433
|
bundleVersion = bundleVersion,
|
|
@@ -1304,6 +1441,7 @@ class ReactNativeBundleUpdate : HybridReactNativeBundleUpdateSpec() {
|
|
|
1304
1441
|
if (downloadedFile.exists()) {
|
|
1305
1442
|
OneKeyLog.info("BundleUpdate", "downloadBundle: file already exists, verifying SHA256...")
|
|
1306
1443
|
if (verifyBundleSHA256(filePath, sha256)) {
|
|
1444
|
+
ensureNotCancelled()
|
|
1307
1445
|
OneKeyLog.info("BundleUpdate", "downloadBundle: existing file SHA256 valid, skipping download")
|
|
1308
1446
|
// Final file is authoritative — drop any stale concurrent
|
|
1309
1447
|
// partial + segment files left by an earlier interrupted
|
|
@@ -1318,8 +1456,9 @@ class ReactNativeBundleUpdate : HybridReactNativeBundleUpdateSpec() {
|
|
|
1318
1456
|
// where a second downloadBundle for this dest could otherwise
|
|
1319
1457
|
// pass the single-flight guard and run concurrently.
|
|
1320
1458
|
Thread.sleep(1000)
|
|
1459
|
+
ensureNotCancelled()
|
|
1321
1460
|
sendEvent("update/complete")
|
|
1322
|
-
return@
|
|
1461
|
+
return@updatePromise result
|
|
1323
1462
|
} else {
|
|
1324
1463
|
OneKeyLog.warn("BundleUpdate", "downloadBundle: existing file SHA256 mismatch, re-downloading")
|
|
1325
1464
|
downloadedFile.delete()
|
|
@@ -1337,6 +1476,7 @@ class ReactNativeBundleUpdate : HybridReactNativeBundleUpdateSpec() {
|
|
|
1337
1476
|
// FALLBACK we fall through to the original single-stream path below.
|
|
1338
1477
|
// Transient errors bubble to the outer catch (the concurrent
|
|
1339
1478
|
// downloader keeps its partial + manifest for resume).
|
|
1479
|
+
ensureNotCancelled()
|
|
1340
1480
|
sendEvent("update/start")
|
|
1341
1481
|
run {
|
|
1342
1482
|
// onProgress is invoked concurrently by all 8 worker threads, so
|
|
@@ -1350,6 +1490,7 @@ class ReactNativeBundleUpdate : HybridReactNativeBundleUpdateSpec() {
|
|
|
1350
1490
|
httpClient = httpClient,
|
|
1351
1491
|
log = { msg -> OneKeyLog.info("BundleUpdate", msg) },
|
|
1352
1492
|
).download(downloadUrl, partialFilePath, cancelHandle) { transferred, total ->
|
|
1493
|
+
if (handle.aborted.get()) return@download
|
|
1353
1494
|
if (total > 0) {
|
|
1354
1495
|
val p = ((transferred * 100) / total).toInt().coerceIn(0, 100)
|
|
1355
1496
|
val prev = concurrentProgress.get()
|
|
@@ -1358,24 +1499,30 @@ class ReactNativeBundleUpdate : HybridReactNativeBundleUpdateSpec() {
|
|
|
1358
1499
|
}
|
|
1359
1500
|
}
|
|
1360
1501
|
}
|
|
1502
|
+
ensureNotCancelled()
|
|
1361
1503
|
if (concurrentOutcome == ConcurrentRangeDownloader.Outcome.COMPLETED) {
|
|
1362
1504
|
// Promote .partial -> final, then SHA256 verify — identical
|
|
1363
1505
|
// finalize to the single-stream path below.
|
|
1364
1506
|
if (downloadedFile.exists()) downloadedFile.delete()
|
|
1365
1507
|
if (!File(partialFilePath).renameTo(downloadedFile)) {
|
|
1366
1508
|
OneKeyLog.error("BundleUpdate", "downloadBundle: concurrent rename .partial -> final failed")
|
|
1367
|
-
throw
|
|
1509
|
+
throw BundleUpdateError("STORAGE_ERROR", "download")
|
|
1368
1510
|
}
|
|
1369
1511
|
OneKeyLog.info("BundleUpdate", "downloadBundle: concurrent download finished, verifying SHA256...")
|
|
1370
|
-
|
|
1512
|
+
ensureNotCancelled()
|
|
1513
|
+
val calculatedHash = BundleUpdateStoreAndroid.calculateSHA256(filePath)
|
|
1514
|
+
?: throw BundleUpdateError("STORAGE_ERROR", "download")
|
|
1515
|
+
if (!BundleUpdateStoreAndroid.secureCompare(calculatedHash, sha256)) {
|
|
1516
|
+
ensureNotCancelled()
|
|
1371
1517
|
val reason = BundleUpdateStoreAndroid.lastSHA256FailureReason() ?: "MISMATCH"
|
|
1372
1518
|
File(filePath).delete()
|
|
1373
1519
|
OneKeyLog.error("BundleUpdate", "downloadBundle: concurrent SHA256 verification failed, reason=$reason")
|
|
1374
|
-
throw
|
|
1520
|
+
throw BundleUpdateError("INTEGRITY_FAILED", "download")
|
|
1375
1521
|
}
|
|
1522
|
+
ensureNotCancelled()
|
|
1376
1523
|
sendEvent("update/complete")
|
|
1377
1524
|
OneKeyLog.info("BundleUpdate", "downloadBundle: concurrent completed successfully, appVersion=$appVersion, bundleVersion=$bundleVersion")
|
|
1378
|
-
return@
|
|
1525
|
+
return@updatePromise result
|
|
1379
1526
|
}
|
|
1380
1527
|
OneKeyLog.info("BundleUpdate", "downloadBundle: concurrent not used, falling back to single-stream")
|
|
1381
1528
|
}
|
|
@@ -1402,13 +1549,15 @@ class ReactNativeBundleUpdate : HybridReactNativeBundleUpdateSpec() {
|
|
|
1402
1549
|
OneKeyLog.info("BundleUpdate", "downloadBundle: partial matches expected size ($partialSize), trying promote+verify")
|
|
1403
1550
|
if (downloadedFile.exists()) downloadedFile.delete()
|
|
1404
1551
|
if (partialFile.renameTo(downloadedFile) && verifyBundleSHA256(filePath, sha256)) {
|
|
1552
|
+
ensureNotCancelled()
|
|
1405
1553
|
OneKeyLog.info("BundleUpdate", "downloadBundle: recovered crashed-before-rename bundle, skipping download")
|
|
1406
1554
|
// The `finally` removes the per-dest lock after this
|
|
1407
1555
|
// return; the lock therefore spans the skip delay,
|
|
1408
1556
|
// same as the existing-file cache-hit path above.
|
|
1409
1557
|
Thread.sleep(1000)
|
|
1558
|
+
ensureNotCancelled()
|
|
1410
1559
|
sendEvent("update/complete")
|
|
1411
|
-
return@
|
|
1560
|
+
return@updatePromise result
|
|
1412
1561
|
} else {
|
|
1413
1562
|
OneKeyLog.warn("BundleUpdate", "downloadBundle: promote+verify failed, discarding both files")
|
|
1414
1563
|
if (downloadedFile.exists()) downloadedFile.delete()
|
|
@@ -1436,6 +1585,7 @@ class ReactNativeBundleUpdate : HybridReactNativeBundleUpdateSpec() {
|
|
|
1436
1585
|
requestBuilder.addHeader("Range", "bytes=$partialBytes-")
|
|
1437
1586
|
}
|
|
1438
1587
|
val response = httpClient.newCall(requestBuilder.build()).execute()
|
|
1588
|
+
ensureNotCancelled()
|
|
1439
1589
|
|
|
1440
1590
|
// 416 Range Not Satisfiable: server says our offset is past the
|
|
1441
1591
|
// file length. Two sub-cases distinguishable from
|
|
@@ -1454,9 +1604,10 @@ class ReactNativeBundleUpdate : HybridReactNativeBundleUpdateSpec() {
|
|
|
1454
1604
|
OneKeyLog.info("BundleUpdate", "downloadBundle: HTTP 416 with total==$totalFromHeader matches partial, attempting promote+verify")
|
|
1455
1605
|
if (downloadedFile.exists()) downloadedFile.delete()
|
|
1456
1606
|
if (partialFile.renameTo(downloadedFile) && verifyBundleSHA256(filePath, sha256)) {
|
|
1607
|
+
ensureNotCancelled()
|
|
1457
1608
|
OneKeyLog.info("BundleUpdate", "downloadBundle: 416 recovery succeeded, skipping download")
|
|
1458
1609
|
sendEvent("update/complete")
|
|
1459
|
-
return@
|
|
1610
|
+
return@updatePromise result
|
|
1460
1611
|
}
|
|
1461
1612
|
OneKeyLog.warn("BundleUpdate", "downloadBundle: 416 recovery failed verify, discarding")
|
|
1462
1613
|
if (downloadedFile.exists()) downloadedFile.delete()
|
|
@@ -1464,10 +1615,8 @@ class ReactNativeBundleUpdate : HybridReactNativeBundleUpdateSpec() {
|
|
|
1464
1615
|
OneKeyLog.warn("BundleUpdate", "downloadBundle: HTTP 416 (range not satisfiable), discarding partial and failing this attempt")
|
|
1465
1616
|
if (partialFile.exists()) partialFile.delete()
|
|
1466
1617
|
for (i in 0 until CONCURRENT_SEGMENT_COUNT) File("$partialFilePath.seg$i").delete()
|
|
1467
|
-
//
|
|
1468
|
-
|
|
1469
|
-
// recognizes "HTTP " prefix and forwards this string verbatim.
|
|
1470
|
-
throw Exception("HTTP 416 (range not satisfiable)")
|
|
1618
|
+
// The outer catch emits one error event for this failure.
|
|
1619
|
+
throw BundleUpdateError("HTTP_ERROR", "download", 416)
|
|
1471
1620
|
}
|
|
1472
1621
|
|
|
1473
1622
|
val expectsResume = partialBytes > 0
|
|
@@ -1477,7 +1626,7 @@ class ReactNativeBundleUpdate : HybridReactNativeBundleUpdateSpec() {
|
|
|
1477
1626
|
OneKeyLog.error("BundleUpdate", "downloadBundle: HTTP error, statusCode=${response.code}")
|
|
1478
1627
|
response.close()
|
|
1479
1628
|
// outer catch is the single source of update/error events.
|
|
1480
|
-
throw
|
|
1629
|
+
throw BundleUpdateError("HTTP_ERROR", "download", response.code)
|
|
1481
1630
|
}
|
|
1482
1631
|
|
|
1483
1632
|
// Server can ignore `Range` and return the full body with 200; in
|
|
@@ -1521,7 +1670,7 @@ class ReactNativeBundleUpdate : HybridReactNativeBundleUpdateSpec() {
|
|
|
1521
1670
|
// consumption would close it for us.
|
|
1522
1671
|
val body = response.body ?: run {
|
|
1523
1672
|
response.close()
|
|
1524
|
-
throw
|
|
1673
|
+
throw BundleUpdateError("NETWORK_ERROR", "download")
|
|
1525
1674
|
}
|
|
1526
1675
|
val contentLength = body.contentLength()
|
|
1527
1676
|
// Total size of the whole resource (not the slice). On 206 prefer
|
|
@@ -1547,6 +1696,7 @@ class ReactNativeBundleUpdate : HybridReactNativeBundleUpdateSpec() {
|
|
|
1547
1696
|
}
|
|
1548
1697
|
|
|
1549
1698
|
// Append iff server granted us a 206 partial; otherwise overwrite.
|
|
1699
|
+
val maxDownloadBytes = 512L * 1024 * 1024
|
|
1550
1700
|
val appendMode = isPartialResponse
|
|
1551
1701
|
var totalBytesRead = if (isPartialResponse) partialBytes else 0L
|
|
1552
1702
|
body.byteStream().use { inputStream ->
|
|
@@ -1556,8 +1706,12 @@ class ReactNativeBundleUpdate : HybridReactNativeBundleUpdateSpec() {
|
|
|
1556
1706
|
|
|
1557
1707
|
var prevProgress = -1
|
|
1558
1708
|
while (inputStream.read(buffer).also { bytesRead = it } != -1) {
|
|
1559
|
-
|
|
1709
|
+
ensureNotCancelled()
|
|
1560
1710
|
totalBytesRead += bytesRead
|
|
1711
|
+
if (totalBytesRead > maxDownloadBytes) {
|
|
1712
|
+
throw BundleUpdateError("SECURITY_REJECTED", "download")
|
|
1713
|
+
}
|
|
1714
|
+
outputStream.write(buffer, 0, bytesRead)
|
|
1561
1715
|
if (totalSize > 0) {
|
|
1562
1716
|
val progress = ((totalBytesRead * 100) / totalSize).toInt().coerceIn(0, 100)
|
|
1563
1717
|
if (progress != prevProgress) {
|
|
@@ -1571,6 +1725,7 @@ class ReactNativeBundleUpdate : HybridReactNativeBundleUpdateSpec() {
|
|
|
1571
1725
|
}
|
|
1572
1726
|
|
|
1573
1727
|
val partialAfter = File(partialFilePath)
|
|
1728
|
+
ensureNotCancelled()
|
|
1574
1729
|
OneKeyLog.info(
|
|
1575
1730
|
"BundleUpdate",
|
|
1576
1731
|
"downloadBundle: download finished, totalBytesRead=$totalBytesRead, partialExists=${partialAfter.exists()}, partialSize=${if (partialAfter.exists()) partialAfter.length() else -1}, finalizing..."
|
|
@@ -1583,63 +1738,39 @@ class ReactNativeBundleUpdate : HybridReactNativeBundleUpdateSpec() {
|
|
|
1583
1738
|
if (!partialAfter.renameTo(downloadedFile)) {
|
|
1584
1739
|
OneKeyLog.error("BundleUpdate", "downloadBundle: rename .partial -> final failed")
|
|
1585
1740
|
// outer catch is the single source of update/error events;
|
|
1586
|
-
//
|
|
1587
|
-
throw
|
|
1741
|
+
// The outer catch emits one error event for this failure.
|
|
1742
|
+
throw BundleUpdateError("STORAGE_ERROR", "download")
|
|
1588
1743
|
}
|
|
1589
1744
|
|
|
1590
1745
|
OneKeyLog.info("BundleUpdate", "downloadBundle: verifying SHA256...")
|
|
1591
|
-
|
|
1746
|
+
ensureNotCancelled()
|
|
1747
|
+
val calculatedHash = BundleUpdateStoreAndroid.calculateSHA256(filePath)
|
|
1748
|
+
?: throw BundleUpdateError("STORAGE_ERROR", "download")
|
|
1749
|
+
if (!BundleUpdateStoreAndroid.secureCompare(calculatedHash, sha256)) {
|
|
1750
|
+
ensureNotCancelled()
|
|
1592
1751
|
val reason = BundleUpdateStoreAndroid.lastSHA256FailureReason() ?: "MISMATCH"
|
|
1593
1752
|
File(filePath).delete()
|
|
1594
1753
|
OneKeyLog.error("BundleUpdate", "downloadBundle: SHA256 verification failed after download, reason=$reason")
|
|
1595
|
-
// outer catch emits the
|
|
1596
|
-
|
|
1597
|
-
throw Exception("Bundle SHA256 verification failed: $reason")
|
|
1754
|
+
// The outer catch emits the fixed integrity failure wire.
|
|
1755
|
+
throw BundleUpdateError("INTEGRITY_FAILED", "download")
|
|
1598
1756
|
}
|
|
1599
1757
|
|
|
1758
|
+
ensureNotCancelled()
|
|
1600
1759
|
sendEvent("update/complete")
|
|
1601
1760
|
OneKeyLog.info("BundleUpdate", "downloadBundle: completed successfully, appVersion=$appVersion, bundleVersion=$bundleVersion")
|
|
1602
1761
|
result
|
|
1603
1762
|
} catch (e: Exception) {
|
|
1604
|
-
// Keep the rich detail in OneKeyLog (local-only). The JS
|
|
1605
|
-
// event channel must NOT carry e.message verbatim — Android
|
|
1606
|
-
// FileNotFoundException etc. embed the full /data/user/.../
|
|
1607
|
-
// path including the package identifier, and downstream
|
|
1608
|
-
// listeners would forward that into analytics. Emit only the
|
|
1609
|
-
// sanitized tag; mirrors the iOS sendEvent payload at
|
|
1610
|
-
// ReactNativeBundleUpdate.swift's "update/error" sites and
|
|
1611
|
-
// matches the verbatim guarantees documented on
|
|
1612
|
-
// sanitizeErrorMessageForEvent.
|
|
1613
1763
|
OneKeyLog.error("BundleUpdate", "downloadBundle: failed: ${e.javaClass.simpleName}: ${e.message}")
|
|
1614
|
-
val sanitized = sanitizeErrorMessageForEvent(e)
|
|
1615
|
-
// An INTENTIONAL cancel (clearDownload/clearBundle flipped THIS
|
|
1616
|
-
// run's CancelHandle, which aborts the workers → they throw
|
|
1617
|
-
// IOException("aborted")) is NOT a download failure — the caller
|
|
1618
|
-
// deliberately tore the run down. Before the cancel-then-delete
|
|
1619
|
-
// path existed, clearing never aborted an in-flight run, so no
|
|
1620
|
-
// `update/error` was emitted; preserve that. Detect it off the
|
|
1621
|
-
// handle's `aborted` flag rather than the message, because
|
|
1622
|
-
// sanitizeErrorMessageForEvent collapses "aborted" to the generic
|
|
1623
|
-
// "IO_IOException" tag and would be indistinguishable from a real
|
|
1624
|
-
// I/O failure.
|
|
1625
1764
|
val intentionallyCancelled = cancelHandle?.aborted?.get() == true
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
sendEvent("update/error", message = sanitized)
|
|
1765
|
+
val updateError = if (intentionallyCancelled) {
|
|
1766
|
+
BundleUpdateError("CANCELLED", "download")
|
|
1767
|
+
} else {
|
|
1768
|
+
bundleUpdateError(e, "download")
|
|
1769
|
+
}
|
|
1770
|
+
if (!duplicateDownload && !intentionallyCancelled) {
|
|
1771
|
+
sendEvent("update/error", message = updateError.message.orEmpty())
|
|
1634
1772
|
}
|
|
1635
|
-
|
|
1636
|
-
// rejection surfacing to JS carries no /data/user/<u>/<pkg>/
|
|
1637
|
-
// paths. Without this rewrap, FileNotFoundException etc.
|
|
1638
|
-
// would re-leak via Promise.reject's message channel even
|
|
1639
|
-
// though the event payload was already sanitized. Keep
|
|
1640
|
-
// the original exception as `cause` so OneKeyLog (and any
|
|
1641
|
-
// native crash reporter) still sees the full chain.
|
|
1642
|
-
throw Exception(sanitized, e)
|
|
1773
|
+
throw updateError
|
|
1643
1774
|
} finally {
|
|
1644
1775
|
// Release THIS run's per-dest single-flight lock. Keyed remove
|
|
1645
1776
|
// (only if we actually acquired it — input-validation failures
|
|
@@ -1648,8 +1779,10 @@ class ReactNativeBundleUpdate : HybridReactNativeBundleUpdateSpec() {
|
|
|
1648
1779
|
// the same dest may have re-registered after a concurrent cancel.
|
|
1649
1780
|
val key = acquiredKey
|
|
1650
1781
|
val handle = cancelHandle
|
|
1651
|
-
|
|
1652
|
-
|
|
1782
|
+
val finished = completionLatch
|
|
1783
|
+
finished?.countDown()
|
|
1784
|
+
if (key != null && handle != null && finished != null) {
|
|
1785
|
+
activeDownloads.remove(key, handle to finished)
|
|
1653
1786
|
}
|
|
1654
1787
|
}
|
|
1655
1788
|
}
|
|
@@ -1674,31 +1807,73 @@ class ReactNativeBundleUpdate : HybridReactNativeBundleUpdateSpec() {
|
|
|
1674
1807
|
return isValid
|
|
1675
1808
|
}
|
|
1676
1809
|
|
|
1810
|
+
private fun isSafeVersionString(version: String): Boolean {
|
|
1811
|
+
return BundleUpdateStoreAndroid.isSafeVersionString(version)
|
|
1812
|
+
}
|
|
1813
|
+
|
|
1677
1814
|
override fun downloadBundleASC(params: BundleDownloadASCParams): Promise<Unit> {
|
|
1678
|
-
return
|
|
1679
|
-
val context = getContext()
|
|
1815
|
+
return updatePromise("downloadASC") {
|
|
1680
1816
|
val appVersion = params.latestVersion
|
|
1681
1817
|
val bundleVersion = params.bundleVersion
|
|
1682
1818
|
val signature = params.signature
|
|
1819
|
+
if (!isSafeVersionString(appVersion) || !isSafeVersionString(bundleVersion)) {
|
|
1820
|
+
throw BundleUpdateError("INVALID_ARGUMENT", "downloadASC")
|
|
1821
|
+
}
|
|
1822
|
+
if (signature.toByteArray(Charsets.UTF_8).size > 64 * 1024) {
|
|
1823
|
+
throw BundleUpdateError("INVALID_ARGUMENT", "downloadASC")
|
|
1824
|
+
}
|
|
1683
1825
|
|
|
1826
|
+
val context = getContext()
|
|
1684
1827
|
OneKeyLog.info("BundleUpdate", "downloadBundleASC: appVersion=$appVersion, bundleVersion=$bundleVersion, signatureLength=${signature.length}")
|
|
1685
1828
|
|
|
1686
1829
|
val storageKey = "$appVersion-$bundleVersion"
|
|
1687
|
-
BundleUpdateStoreAndroid.
|
|
1688
|
-
|
|
1689
|
-
|
|
1830
|
+
val ascBaseCanonical = File(BundleUpdateStoreAndroid.getAscDir(context)).canonicalPath
|
|
1831
|
+
val signatureFile = File(BundleUpdateStoreAndroid.getSignatureFilePath(context, storageKey))
|
|
1832
|
+
if (Files.isSymbolicLink(signatureFile.toPath()) ||
|
|
1833
|
+
!signatureFile.canonicalPath.startsWith(ascBaseCanonical + File.separator)
|
|
1834
|
+
) {
|
|
1835
|
+
throw BundleUpdateError("SECURITY_REJECTED", "downloadASC")
|
|
1836
|
+
}
|
|
1690
1837
|
}
|
|
1691
1838
|
}
|
|
1692
1839
|
|
|
1693
1840
|
override fun verifyBundleASC(params: BundleVerifyASCParams): Promise<Unit> {
|
|
1694
|
-
return
|
|
1695
|
-
val context = getContext()
|
|
1696
|
-
val filePath = params.downloadedFile
|
|
1841
|
+
return updatePromise("verifyASC") {
|
|
1697
1842
|
val sha256 = params.sha256
|
|
1698
1843
|
val appVersion = params.latestVersion
|
|
1699
1844
|
val bundleVersion = params.bundleVersion
|
|
1700
1845
|
val signature = params.signature
|
|
1701
1846
|
|
|
1847
|
+
if (!isSafeVersionString(appVersion) || !isSafeVersionString(bundleVersion)) {
|
|
1848
|
+
throw BundleUpdateError("INVALID_ARGUMENT", "verifyASC")
|
|
1849
|
+
}
|
|
1850
|
+
|
|
1851
|
+
val context = getContext()
|
|
1852
|
+
val filePath = File(BundleUpdateStoreAndroid.getDownloadBundleDir(context),
|
|
1853
|
+
"$appVersion-$bundleVersion.zip").absolutePath
|
|
1854
|
+
val downloadBaseCanonical = File(BundleUpdateStoreAndroid.getDownloadBundleDir(context)).canonicalPath
|
|
1855
|
+
val resolvedFilePath = File(filePath).canonicalPath
|
|
1856
|
+
if (!resolvedFilePath.startsWith(downloadBaseCanonical + File.separator)) {
|
|
1857
|
+
OneKeyLog.error("BundleUpdate", "verifyBundleASC: ZIP outside download directory: $resolvedFilePath")
|
|
1858
|
+
throw BundleUpdateError("SECURITY_REJECTED", "verifyASC")
|
|
1859
|
+
}
|
|
1860
|
+
if (!File(resolvedFilePath).isFile) {
|
|
1861
|
+
throw BundleUpdateError("PACKAGE_MISSING", "verifyASC")
|
|
1862
|
+
}
|
|
1863
|
+
|
|
1864
|
+
val folderName = "$appVersion-$bundleVersion"
|
|
1865
|
+
val isInstalled = BundleUpdateStoreAndroid.getCurrentBundleVersion(context) == folderName ||
|
|
1866
|
+
BundleUpdateStoreAndroid.readFallbackUpdateBundleDataFile(context).any {
|
|
1867
|
+
it["appVersion"] == appVersion && it["bundleVersion"] == bundleVersion
|
|
1868
|
+
}
|
|
1869
|
+
val bundleBaseCanonical = File(BundleUpdateStoreAndroid.getBundleDir(context)).canonicalPath
|
|
1870
|
+
val destinationDir = File(BundleUpdateStoreAndroid.getBundleDir(context), folderName)
|
|
1871
|
+
val destination = destinationDir.canonicalPath
|
|
1872
|
+
if (!destination.startsWith(bundleBaseCanonical + File.separator)) {
|
|
1873
|
+
OneKeyLog.error("BundleUpdate", "verifyBundleASC: destination outside bundle directory: $destination")
|
|
1874
|
+
throw BundleUpdateError("SECURITY_REJECTED", "verifyASC")
|
|
1875
|
+
}
|
|
1876
|
+
|
|
1702
1877
|
OneKeyLog.info("BundleUpdate", "verifyBundleASC: appVersion=$appVersion, bundleVersion=$bundleVersion, file=$filePath, signatureLength=${signature.length}")
|
|
1703
1878
|
|
|
1704
1879
|
// GPG verification skipped only when both DevSettings and skip-GPG toggle are enabled
|
|
@@ -1706,44 +1881,69 @@ class ReactNativeBundleUpdate : HybridReactNativeBundleUpdateSpec() {
|
|
|
1706
1881
|
OneKeyLog.info("BundleUpdate", "verifyBundleASC: GPG check: skipGPG=$skipGPG")
|
|
1707
1882
|
|
|
1708
1883
|
if (!skipGPG) {
|
|
1884
|
+
OneKeyLog.info("BundleUpdate", "verifyBundleASC: pre-verifying GPG signature before extraction...")
|
|
1885
|
+
if (BundleUpdateStoreAndroid.readMetadataFileSha256(
|
|
1886
|
+
signature,
|
|
1887
|
+
expectedAppVersion = appVersion,
|
|
1888
|
+
expectedBundleVersion = bundleVersion,
|
|
1889
|
+
expectedAppType = "android",
|
|
1890
|
+
).isNullOrEmpty()
|
|
1891
|
+
) {
|
|
1892
|
+
OneKeyLog.error("BundleUpdate", "verifyBundleASC: pre-extraction GPG signature verification failed")
|
|
1893
|
+
throw BundleUpdateError("INTEGRITY_FAILED", "verifyASC")
|
|
1894
|
+
}
|
|
1895
|
+
|
|
1709
1896
|
OneKeyLog.info("BundleUpdate", "verifyBundleASC: verifying SHA256 of downloaded file...")
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
//
|
|
1714
|
-
// this bucket the same way the download stage does.
|
|
1897
|
+
val calculatedHash = BundleUpdateStoreAndroid.calculateSHA256(resolvedFilePath)
|
|
1898
|
+
?: throw BundleUpdateError("STORAGE_ERROR", "verifyASC")
|
|
1899
|
+
if (!BundleUpdateStoreAndroid.secureCompare(calculatedHash, sha256)) {
|
|
1900
|
+
// Keep the detailed hash reason in the local log only.
|
|
1715
1901
|
val reason = BundleUpdateStoreAndroid.lastSHA256FailureReason() ?: "MISMATCH"
|
|
1716
1902
|
OneKeyLog.error("BundleUpdate", "verifyBundleASC: SHA256 verification failed for file=$filePath, reason=$reason")
|
|
1717
|
-
throw
|
|
1903
|
+
throw BundleUpdateError("INTEGRITY_FAILED", "verifyASC")
|
|
1718
1904
|
}
|
|
1719
1905
|
OneKeyLog.info("BundleUpdate", "verifyBundleASC: SHA256 verified OK")
|
|
1720
1906
|
} else {
|
|
1721
1907
|
OneKeyLog.warn("BundleUpdate", "verifyBundleASC: SHA256 + GPG verification skipped (DevSettings enabled)")
|
|
1722
1908
|
}
|
|
1723
1909
|
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1910
|
+
if (isInstalled) {
|
|
1911
|
+
if (!skipGPG && !BundleUpdateStoreAndroid.validateMetadataFileSha256(context, folderName, signature, appVersion, bundleVersion)) {
|
|
1912
|
+
throw BundleUpdateError("INTEGRITY_FAILED", "verifyASC")
|
|
1913
|
+
}
|
|
1914
|
+
val metadata = BundleUpdateStoreAndroid.parseMetadataJson(
|
|
1915
|
+
BundleUpdateStoreAndroid.readFileContent(File(destination, "metadata.json"))
|
|
1916
|
+
)
|
|
1917
|
+
if (!BundleUpdateStoreAndroid.validateAllFilesInDir(context, destination, metadata, appVersion, bundleVersion) ||
|
|
1918
|
+
!BundleUpdateStoreAndroid.validateBundlePairCompatibility(context, destination, metadata)
|
|
1919
|
+
) {
|
|
1920
|
+
throw BundleUpdateError("INTEGRITY_FAILED", "verifyASC")
|
|
1921
|
+
}
|
|
1922
|
+
return@updatePromise
|
|
1923
|
+
}
|
|
1727
1924
|
|
|
1728
1925
|
try {
|
|
1926
|
+
if (destinationDir.exists()) {
|
|
1927
|
+
BundleUpdateStoreAndroid.deleteDir(destinationDir)
|
|
1928
|
+
}
|
|
1729
1929
|
OneKeyLog.info("BundleUpdate", "verifyBundleASC: extracting zip to $destination...")
|
|
1730
|
-
BundleUpdateStoreAndroid.unzipFile(
|
|
1930
|
+
BundleUpdateStoreAndroid.unzipFile(resolvedFilePath, destination)
|
|
1731
1931
|
OneKeyLog.info("BundleUpdate", "verifyBundleASC: extraction completed")
|
|
1732
1932
|
|
|
1733
1933
|
val metadataFile = File(destination, "metadata.json")
|
|
1734
1934
|
if (!metadataFile.exists()) {
|
|
1735
1935
|
OneKeyLog.error("BundleUpdate", "verifyBundleASC: metadata.json not found after extraction")
|
|
1736
1936
|
BundleUpdateStoreAndroid.deleteDir(destinationDir)
|
|
1737
|
-
throw
|
|
1937
|
+
throw BundleUpdateError("PACKAGE_MISSING", "verifyASC")
|
|
1738
1938
|
}
|
|
1739
1939
|
|
|
1740
1940
|
val currentBundleVersion = "$appVersion-$bundleVersion"
|
|
1741
1941
|
if (!skipGPG) {
|
|
1742
1942
|
OneKeyLog.info("BundleUpdate", "verifyBundleASC: validating GPG signature for metadata...")
|
|
1743
|
-
if (!BundleUpdateStoreAndroid.validateMetadataFileSha256(context, currentBundleVersion, signature)) {
|
|
1943
|
+
if (!BundleUpdateStoreAndroid.validateMetadataFileSha256(context, currentBundleVersion, signature, appVersion, bundleVersion)) {
|
|
1744
1944
|
OneKeyLog.error("BundleUpdate", "verifyBundleASC: GPG signature verification failed")
|
|
1745
1945
|
BundleUpdateStoreAndroid.deleteDir(destinationDir)
|
|
1746
|
-
throw
|
|
1946
|
+
throw BundleUpdateError("INTEGRITY_FAILED", "verifyASC")
|
|
1747
1947
|
}
|
|
1748
1948
|
OneKeyLog.info("BundleUpdate", "verifyBundleASC: GPG signature verified OK")
|
|
1749
1949
|
}
|
|
@@ -1754,12 +1954,12 @@ class ReactNativeBundleUpdate : HybridReactNativeBundleUpdateSpec() {
|
|
|
1754
1954
|
if (!BundleUpdateStoreAndroid.validateAllFilesInDir(context, destination, metadata, appVersion, bundleVersion)) {
|
|
1755
1955
|
OneKeyLog.error("BundleUpdate", "verifyBundleASC: file integrity check failed")
|
|
1756
1956
|
BundleUpdateStoreAndroid.deleteDir(destinationDir)
|
|
1757
|
-
throw
|
|
1957
|
+
throw BundleUpdateError("INTEGRITY_FAILED", "verifyASC")
|
|
1758
1958
|
}
|
|
1759
1959
|
if (!BundleUpdateStoreAndroid.validateBundlePairCompatibility(context, destination, metadata)) {
|
|
1760
1960
|
OneKeyLog.error("BundleUpdate", "verifyBundleASC: bundle pair compatibility check failed")
|
|
1761
1961
|
BundleUpdateStoreAndroid.deleteDir(destinationDir)
|
|
1762
|
-
throw
|
|
1962
|
+
throw BundleUpdateError("INCOMPATIBLE", "verifyASC")
|
|
1763
1963
|
}
|
|
1764
1964
|
|
|
1765
1965
|
OneKeyLog.info("BundleUpdate", "verifyBundleASC: all verifications passed, appVersion=$appVersion, bundleVersion=$bundleVersion")
|
|
@@ -1773,23 +1973,38 @@ class ReactNativeBundleUpdate : HybridReactNativeBundleUpdateSpec() {
|
|
|
1773
1973
|
}
|
|
1774
1974
|
|
|
1775
1975
|
override fun verifyBundle(params: BundleVerifyParams): Promise<Unit> {
|
|
1776
|
-
return
|
|
1777
|
-
val filePath = params.downloadedFile
|
|
1976
|
+
return updatePromise("verifyPackage") {
|
|
1778
1977
|
val sha256 = params.sha256
|
|
1779
1978
|
val appVersion = params.latestVersion
|
|
1780
1979
|
val bundleVersion = params.bundleVersion
|
|
1781
1980
|
|
|
1981
|
+
if (!isSafeVersionString(appVersion) || !isSafeVersionString(bundleVersion)) {
|
|
1982
|
+
throw BundleUpdateError("INVALID_ARGUMENT", "verifyPackage")
|
|
1983
|
+
}
|
|
1984
|
+
|
|
1985
|
+
val context = getContext()
|
|
1986
|
+
val filePath = File(BundleUpdateStoreAndroid.getDownloadBundleDir(context),
|
|
1987
|
+
"$appVersion-$bundleVersion.zip").absolutePath
|
|
1988
|
+
val downloadBaseCanonical = File(BundleUpdateStoreAndroid.getDownloadBundleDir(context)).canonicalPath
|
|
1989
|
+
val resolvedFilePath = File(filePath).canonicalPath
|
|
1990
|
+
if (!resolvedFilePath.startsWith(downloadBaseCanonical + File.separator)) {
|
|
1991
|
+
throw BundleUpdateError("SECURITY_REJECTED", "verifyPackage")
|
|
1992
|
+
}
|
|
1993
|
+
if (!File(resolvedFilePath).isFile) {
|
|
1994
|
+
throw BundleUpdateError("PACKAGE_MISSING", "verifyPackage")
|
|
1995
|
+
}
|
|
1996
|
+
|
|
1782
1997
|
OneKeyLog.info("BundleUpdate", "verifyBundle: appVersion=$appVersion, bundleVersion=$bundleVersion, file=$filePath")
|
|
1783
1998
|
|
|
1784
1999
|
// Verify SHA256 of the downloaded file
|
|
1785
|
-
val calculated = BundleUpdateStoreAndroid.calculateSHA256(
|
|
2000
|
+
val calculated = BundleUpdateStoreAndroid.calculateSHA256(resolvedFilePath)
|
|
1786
2001
|
if (calculated == null) {
|
|
1787
2002
|
OneKeyLog.error("BundleUpdate", "verifyBundle: failed to calculate SHA256 for file=$filePath")
|
|
1788
|
-
throw
|
|
2003
|
+
throw BundleUpdateError("STORAGE_ERROR", "verifyPackage")
|
|
1789
2004
|
}
|
|
1790
2005
|
if (!BundleUpdateStoreAndroid.secureCompare(calculated, sha256)) {
|
|
1791
2006
|
OneKeyLog.error("BundleUpdate", "verifyBundle: SHA256 mismatch, expected=${sha256.take(16)}..., got=${calculated.take(16)}...")
|
|
1792
|
-
throw
|
|
2007
|
+
throw BundleUpdateError("INTEGRITY_FAILED", "verifyPackage")
|
|
1793
2008
|
}
|
|
1794
2009
|
|
|
1795
2010
|
OneKeyLog.info("BundleUpdate", "verifyBundle: SHA256 verified OK for appVersion=$appVersion, bundleVersion=$bundleVersion")
|
|
@@ -1802,12 +2017,16 @@ class ReactNativeBundleUpdate : HybridReactNativeBundleUpdateSpec() {
|
|
|
1802
2017
|
// commits so a same-version reinstall (or any reader that re-cached
|
|
1803
2018
|
// pre-commit data inside the race window) can't leave stale entries.
|
|
1804
2019
|
BundleUpdateStoreAndroid.invalidateValidatedBundleInfoCache()
|
|
1805
|
-
return
|
|
1806
|
-
val context = getContext()
|
|
2020
|
+
return updatePromise("install") {
|
|
1807
2021
|
val appVersion = params.latestVersion
|
|
1808
2022
|
val bundleVersion = params.bundleVersion
|
|
1809
2023
|
val signature = params.signature
|
|
1810
2024
|
|
|
2025
|
+
if (!isSafeVersionString(appVersion) || !isSafeVersionString(bundleVersion)) {
|
|
2026
|
+
throw BundleUpdateError("INVALID_ARGUMENT", "install")
|
|
2027
|
+
}
|
|
2028
|
+
|
|
2029
|
+
val context = getContext()
|
|
1811
2030
|
OneKeyLog.info("BundleUpdate", "installBundle: appVersion=$appVersion, bundleVersion=$bundleVersion, signatureLength=${signature.length}")
|
|
1812
2031
|
|
|
1813
2032
|
// GPG verification skipped only when both DevSettings and skip-GPG toggle are enabled
|
|
@@ -1816,13 +2035,53 @@ class ReactNativeBundleUpdate : HybridReactNativeBundleUpdateSpec() {
|
|
|
1816
2035
|
|
|
1817
2036
|
val folderName = "$appVersion-$bundleVersion"
|
|
1818
2037
|
val currentFolderName = BundleUpdateStoreAndroid.getCurrentBundleVersion(context)
|
|
2038
|
+
val previousActiveBundleVersion = currentFolderName?.let {
|
|
2039
|
+
BundleUpdateStoreAndroid.bundleVersionInFolder(it, appVersion)
|
|
2040
|
+
}
|
|
1819
2041
|
OneKeyLog.info("BundleUpdate", "installBundle: target=$folderName, current=$currentFolderName")
|
|
1820
2042
|
|
|
1821
|
-
// Verify bundle directory exists
|
|
2043
|
+
// Verify bundle directory exists and is canonically confined inside getBundleDir(context)
|
|
2044
|
+
val bundleBaseCanonical = File(BundleUpdateStoreAndroid.getBundleDir(context)).canonicalPath
|
|
1822
2045
|
val bundleDirPath = File(BundleUpdateStoreAndroid.getBundleDir(context), folderName)
|
|
2046
|
+
if (Files.isSymbolicLink(bundleDirPath.toPath()) ||
|
|
2047
|
+
!bundleDirPath.canonicalPath.startsWith(bundleBaseCanonical + File.separator)
|
|
2048
|
+
) {
|
|
2049
|
+
throw BundleUpdateError("SECURITY_REJECTED", "install")
|
|
2050
|
+
}
|
|
2051
|
+
val ascBaseCanonical = File(BundleUpdateStoreAndroid.getAscDir(context)).canonicalPath
|
|
2052
|
+
val sigFile = File(BundleUpdateStoreAndroid.getSignatureFilePath(context, folderName))
|
|
2053
|
+
if (Files.isSymbolicLink(sigFile.toPath()) ||
|
|
2054
|
+
!sigFile.canonicalPath.startsWith(ascBaseCanonical + File.separator)
|
|
2055
|
+
) {
|
|
2056
|
+
throw BundleUpdateError("SECURITY_REJECTED", "install")
|
|
2057
|
+
}
|
|
1823
2058
|
if (!bundleDirPath.exists()) {
|
|
1824
2059
|
OneKeyLog.error("BundleUpdate", "installBundle: bundle directory not found: ${bundleDirPath.absolutePath}")
|
|
1825
|
-
throw
|
|
2060
|
+
throw BundleUpdateError("PACKAGE_MISSING", "install")
|
|
2061
|
+
}
|
|
2062
|
+
|
|
2063
|
+
val mainBundleFile = File(bundleDirPath, "main.jsbundle.hbc")
|
|
2064
|
+
val metadataFile = File(bundleDirPath, "metadata.json")
|
|
2065
|
+
if (!mainBundleFile.exists() || !metadataFile.exists()) {
|
|
2066
|
+
OneKeyLog.error("BundleUpdate", "installBundle: required bundle files missing in: ${bundleDirPath.absolutePath}")
|
|
2067
|
+
throw BundleUpdateError("PACKAGE_MISSING", "install")
|
|
2068
|
+
}
|
|
2069
|
+
|
|
2070
|
+
if (!skipGPG) {
|
|
2071
|
+
if (signature.isEmpty() || !BundleUpdateStoreAndroid.validateMetadataFileSha256(context, folderName, signature, appVersion, bundleVersion)) {
|
|
2072
|
+
OneKeyLog.error("BundleUpdate", "installBundle: GPG signature verification failed for $folderName")
|
|
2073
|
+
throw BundleUpdateError("INTEGRITY_FAILED", "install")
|
|
2074
|
+
}
|
|
2075
|
+
}
|
|
2076
|
+
|
|
2077
|
+
val metadata = BundleUpdateStoreAndroid.parseMetadataJson(BundleUpdateStoreAndroid.readFileContent(metadataFile))
|
|
2078
|
+
if (!BundleUpdateStoreAndroid.validateEntryBundlesSha256(bundleDirPath.absolutePath, metadata)) {
|
|
2079
|
+
OneKeyLog.error("BundleUpdate", "installBundle: entry bundle SHA256 verification failed for $folderName")
|
|
2080
|
+
throw BundleUpdateError("INTEGRITY_FAILED", "install")
|
|
2081
|
+
}
|
|
2082
|
+
if (!BundleUpdateStoreAndroid.validateBundlePairCompatibility(context, bundleDirPath.absolutePath, metadata)) {
|
|
2083
|
+
OneKeyLog.error("BundleUpdate", "installBundle: bundle pair compatibility check failed for $folderName")
|
|
2084
|
+
throw BundleUpdateError("INCOMPATIBLE", "install")
|
|
1826
2085
|
}
|
|
1827
2086
|
|
|
1828
2087
|
val currentSignature = if (currentFolderName != null) BundleUpdateStoreAndroid.readSignatureFile(context, currentFolderName) else ""
|
|
@@ -1838,13 +2097,11 @@ class ReactNativeBundleUpdate : HybridReactNativeBundleUpdateSpec() {
|
|
|
1838
2097
|
val fallbackData = BundleUpdateStoreAndroid.readFallbackUpdateBundleDataFile(context)
|
|
1839
2098
|
|
|
1840
2099
|
if (!currentFolderName.isNullOrEmpty()) {
|
|
1841
|
-
val
|
|
1842
|
-
if (
|
|
1843
|
-
val curAppVersion = currentFolderName.substring(0, lastDashIndex)
|
|
1844
|
-
val curBundleVersion = currentFolderName.substring(lastDashIndex + 1)
|
|
2100
|
+
val curBundleVersion = BundleUpdateStoreAndroid.bundleVersionInFolder(currentFolderName, appVersion)
|
|
2101
|
+
if (curBundleVersion != null) {
|
|
1845
2102
|
if (currentSignature.isNotEmpty()) {
|
|
1846
2103
|
fallbackData.add(mutableMapOf(
|
|
1847
|
-
"appVersion" to
|
|
2104
|
+
"appVersion" to appVersion,
|
|
1848
2105
|
"bundleVersion" to curBundleVersion,
|
|
1849
2106
|
"signature" to currentSignature
|
|
1850
2107
|
))
|
|
@@ -1853,16 +2110,20 @@ class ReactNativeBundleUpdate : HybridReactNativeBundleUpdateSpec() {
|
|
|
1853
2110
|
}
|
|
1854
2111
|
|
|
1855
2112
|
if (fallbackData.size > 3) {
|
|
1856
|
-
val
|
|
1857
|
-
val shiftApp =
|
|
1858
|
-
val shiftBundle =
|
|
2113
|
+
val removedBundle = fallbackData.removeAt(0)
|
|
2114
|
+
val shiftApp = removedBundle["appVersion"]
|
|
2115
|
+
val shiftBundle = removedBundle["bundleVersion"]
|
|
1859
2116
|
if (shiftApp != null && shiftBundle != null) {
|
|
1860
2117
|
val shiftFolderName = "$shiftApp-$shiftBundle"
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
2118
|
+
if (shiftBundle != previousActiveBundleVersion && shiftFolderName != currentFolderName) {
|
|
2119
|
+
val oldDir = File(BundleUpdateStoreAndroid.getBundleDir(context), shiftFolderName)
|
|
2120
|
+
if (oldDir.exists()) {
|
|
2121
|
+
BundleUpdateStoreAndroid.deleteDir(oldDir)
|
|
2122
|
+
}
|
|
2123
|
+
BundleUpdateStoreAndroid.deleteSignatureFile(context, shiftFolderName)
|
|
2124
|
+
} else {
|
|
2125
|
+
OneKeyLog.info("BundleUpdate", "installBundle: skipping disk deletion of active bundle $shiftFolderName during eviction")
|
|
1864
2126
|
}
|
|
1865
|
-
BundleUpdateStoreAndroid.deleteSignatureFile(context, shiftFolderName)
|
|
1866
2127
|
}
|
|
1867
2128
|
}
|
|
1868
2129
|
|
|
@@ -1879,58 +2140,72 @@ class ReactNativeBundleUpdate : HybridReactNativeBundleUpdateSpec() {
|
|
|
1879
2140
|
}
|
|
1880
2141
|
}
|
|
1881
2142
|
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
2143
|
+
private fun clearDownloadFiles(context: Context, includeInstalled: Boolean) {
|
|
2144
|
+
synchronized(clearDownloadsLock) {
|
|
2145
|
+
try {
|
|
2146
|
+
val active = synchronized(activeDownloads) {
|
|
2147
|
+
clearingDownloads = true
|
|
2148
|
+
activeDownloads.entries.map { it.key to it.value }
|
|
2149
|
+
}
|
|
2150
|
+
active.forEach { (_, run) -> run.first.aborted.set(true) }
|
|
2151
|
+
active.forEach { (key, run) ->
|
|
2152
|
+
OneKeyLog.info("BundleUpdate", "cancelling in-flight download: $key")
|
|
2153
|
+
run.first.cancel()
|
|
2154
|
+
}
|
|
2155
|
+
val deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(65)
|
|
2156
|
+
for ((_, run) in active) {
|
|
2157
|
+
val remaining = deadline - System.nanoTime()
|
|
2158
|
+
val completed = try {
|
|
2159
|
+
remaining > 0 && run.second.await(remaining, TimeUnit.NANOSECONDS)
|
|
2160
|
+
} catch (e: InterruptedException) {
|
|
2161
|
+
Thread.currentThread().interrupt()
|
|
2162
|
+
false
|
|
2163
|
+
}
|
|
2164
|
+
if (!completed) throw BundleUpdateError("UNKNOWN", "cleanup")
|
|
2165
|
+
}
|
|
2166
|
+
synchronized(activeDownloads) {
|
|
2167
|
+
if (active.any { (key, run) ->
|
|
2168
|
+
val current = activeDownloads[key]
|
|
2169
|
+
current != null && current !== run
|
|
2170
|
+
}) {
|
|
2171
|
+
throw BundleUpdateError("UNKNOWN", "cleanup")
|
|
2172
|
+
}
|
|
2173
|
+
val downloadDir = File(BundleUpdateStoreAndroid.getDownloadBundleDir(context))
|
|
2174
|
+
if (downloadDir.exists()) {
|
|
2175
|
+
if (!BundleUpdateStoreAndroid.deleteDir(downloadDir)) throw BundleUpdateError("STORAGE_ERROR", "cleanup")
|
|
2176
|
+
OneKeyLog.info("BundleUpdate", "download directory deleted")
|
|
2177
|
+
}
|
|
2178
|
+
if (includeInstalled) {
|
|
2179
|
+
val bundleDir = File(BundleUpdateStoreAndroid.getBundleDir(context))
|
|
2180
|
+
if (bundleDir.exists()) {
|
|
2181
|
+
if (!BundleUpdateStoreAndroid.deleteDir(bundleDir)) throw BundleUpdateError("STORAGE_ERROR", "cleanup")
|
|
2182
|
+
OneKeyLog.info("BundleUpdate", "bundle directory deleted")
|
|
2183
|
+
}
|
|
2184
|
+
}
|
|
2185
|
+
}
|
|
2186
|
+
} finally {
|
|
2187
|
+
synchronized(activeDownloads) {
|
|
2188
|
+
clearingDownloads = false
|
|
2189
|
+
}
|
|
1894
2190
|
}
|
|
1895
2191
|
}
|
|
1896
2192
|
}
|
|
1897
2193
|
|
|
1898
2194
|
override fun clearDownload(): Promise<Unit> {
|
|
1899
|
-
return
|
|
2195
|
+
return updatePromise("cleanup") {
|
|
1900
2196
|
OneKeyLog.info("BundleUpdate", "clearDownload: clearing download directory...")
|
|
1901
2197
|
val context = getContext()
|
|
1902
|
-
|
|
1903
|
-
cancelAllActiveDownloads()
|
|
1904
|
-
val downloadDir = File(BundleUpdateStoreAndroid.getDownloadBundleDir(context))
|
|
1905
|
-
if (downloadDir.exists()) {
|
|
1906
|
-
BundleUpdateStoreAndroid.deleteDir(downloadDir)
|
|
1907
|
-
OneKeyLog.info("BundleUpdate", "clearDownload: download directory deleted")
|
|
1908
|
-
} else {
|
|
1909
|
-
OneKeyLog.info("BundleUpdate", "clearDownload: download directory does not exist, skipping")
|
|
1910
|
-
}
|
|
2198
|
+
clearDownloadFiles(context, includeInstalled = false)
|
|
1911
2199
|
OneKeyLog.info("BundleUpdate", "clearDownload: completed")
|
|
1912
2200
|
}
|
|
1913
2201
|
}
|
|
1914
2202
|
|
|
1915
2203
|
override fun clearBundle(): Promise<Unit> {
|
|
1916
2204
|
BundleUpdateStoreAndroid.invalidateValidatedBundleInfoCache()
|
|
1917
|
-
return
|
|
2205
|
+
return updatePromise("cleanup") {
|
|
1918
2206
|
OneKeyLog.info("BundleUpdate", "clearBundle: clearing download and bundle directories...")
|
|
1919
2207
|
val context = getContext()
|
|
1920
|
-
|
|
1921
|
-
cancelAllActiveDownloads()
|
|
1922
|
-
// Clear download directory
|
|
1923
|
-
val downloadDir = File(BundleUpdateStoreAndroid.getDownloadBundleDir(context))
|
|
1924
|
-
if (downloadDir.exists()) {
|
|
1925
|
-
BundleUpdateStoreAndroid.deleteDir(downloadDir)
|
|
1926
|
-
OneKeyLog.info("BundleUpdate", "clearBundle: download directory deleted")
|
|
1927
|
-
}
|
|
1928
|
-
// Clear installed bundle directory
|
|
1929
|
-
val bundleDir = File(BundleUpdateStoreAndroid.getBundleDir(context))
|
|
1930
|
-
if (bundleDir.exists()) {
|
|
1931
|
-
BundleUpdateStoreAndroid.deleteDir(bundleDir)
|
|
1932
|
-
OneKeyLog.info("BundleUpdate", "clearBundle: bundle directory deleted")
|
|
1933
|
-
}
|
|
2208
|
+
clearDownloadFiles(context, includeInstalled = true)
|
|
1934
2209
|
OneKeyLog.info("BundleUpdate", "clearBundle: completed")
|
|
1935
2210
|
}
|
|
1936
2211
|
}
|
|
@@ -1943,9 +2218,8 @@ class ReactNativeBundleUpdate : HybridReactNativeBundleUpdateSpec() {
|
|
|
1943
2218
|
* appVersion's artifacts and the active currentBundleVersion. Tolerates
|
|
1944
2219
|
* already-missing files. Returns the count of deleted version directories.
|
|
1945
2220
|
*
|
|
1946
|
-
*
|
|
1947
|
-
*
|
|
1948
|
-
* logic, so behavior matches the rest of the module.
|
|
2221
|
+
* The running app version is matched as an exact prefix before best-effort
|
|
2222
|
+
* classification of non-current stems.
|
|
1949
2223
|
*/
|
|
1950
2224
|
override fun pruneStaleAppVersionBundles(): Promise<Double> {
|
|
1951
2225
|
BundleUpdateStoreAndroid.invalidateValidatedBundleInfoCache()
|
|
@@ -1978,6 +2252,7 @@ class ReactNativeBundleUpdate : HybridReactNativeBundleUpdateSpec() {
|
|
|
1978
2252
|
// unparseable (leave foreign names alone).
|
|
1979
2253
|
fun shouldKeep(stem: String): Boolean {
|
|
1980
2254
|
if (currentBundleVersion != null && stem == currentBundleVersion) return true
|
|
2255
|
+
if (BundleUpdateStoreAndroid.bundleVersionInFolder(stem, currentAppV) != null) return true
|
|
1981
2256
|
val appV = appVersionFromStem(stem) ?: return true
|
|
1982
2257
|
return appV == currentAppV
|
|
1983
2258
|
}
|
|
@@ -2083,7 +2358,7 @@ class ReactNativeBundleUpdate : HybridReactNativeBundleUpdateSpec() {
|
|
|
2083
2358
|
|
|
2084
2359
|
override fun resetToBuiltInBundle(): Promise<Unit> {
|
|
2085
2360
|
BundleUpdateStoreAndroid.invalidateValidatedBundleInfoCache()
|
|
2086
|
-
return
|
|
2361
|
+
return updatePromise("cleanup") {
|
|
2087
2362
|
OneKeyLog.info("BundleUpdate", "resetToBuiltInBundle: clearing currentBundleVersion preference...")
|
|
2088
2363
|
val context = getContext()
|
|
2089
2364
|
val prefs = context.getSharedPreferences("BundleUpdatePrefs", Context.MODE_PRIVATE)
|
|
@@ -2100,7 +2375,7 @@ class ReactNativeBundleUpdate : HybridReactNativeBundleUpdateSpec() {
|
|
|
2100
2375
|
|
|
2101
2376
|
override fun clearAllJSBundleData(): Promise<TestResult> {
|
|
2102
2377
|
BundleUpdateStoreAndroid.invalidateValidatedBundleInfoCache()
|
|
2103
|
-
return
|
|
2378
|
+
return updatePromise("cleanup") {
|
|
2104
2379
|
OneKeyLog.info("BundleUpdate", "clearAllJSBundleData: starting...")
|
|
2105
2380
|
val context = getContext()
|
|
2106
2381
|
val downloadDir = File(BundleUpdateStoreAndroid.getDownloadBundleDir(context))
|
|
@@ -2138,16 +2413,37 @@ class ReactNativeBundleUpdate : HybridReactNativeBundleUpdateSpec() {
|
|
|
2138
2413
|
|
|
2139
2414
|
override fun setCurrentUpdateBundleData(params: BundleSwitchParams): Promise<Unit> {
|
|
2140
2415
|
BundleUpdateStoreAndroid.invalidateValidatedBundleInfoCache()
|
|
2141
|
-
return
|
|
2416
|
+
return updatePromise("install") {
|
|
2417
|
+
if (!isSafeVersionString(params.appVersion) || !isSafeVersionString(params.bundleVersion)) {
|
|
2418
|
+
OneKeyLog.error("BundleUpdate", "setCurrentUpdateBundleData: invalid version string format: appVersion=${params.appVersion}, bundleVersion=${params.bundleVersion}")
|
|
2419
|
+
throw BundleUpdateError("INVALID_ARGUMENT", "install")
|
|
2420
|
+
}
|
|
2421
|
+
|
|
2142
2422
|
val context = getContext()
|
|
2143
2423
|
val bundleVersion = "${params.appVersion}-${params.bundleVersion}"
|
|
2144
2424
|
OneKeyLog.info("BundleUpdate", "setCurrentUpdateBundleData: switching to $bundleVersion")
|
|
2145
2425
|
|
|
2146
|
-
|
|
2426
|
+
val baseBundleDir = File(BundleUpdateStoreAndroid.getBundleDir(context)).canonicalPath
|
|
2147
2427
|
val bundleDirPath = File(BundleUpdateStoreAndroid.getBundleDir(context), bundleVersion)
|
|
2428
|
+
if (Files.isSymbolicLink(bundleDirPath.toPath()) ||
|
|
2429
|
+
!bundleDirPath.canonicalPath.startsWith(baseBundleDir + File.separator)
|
|
2430
|
+
) {
|
|
2431
|
+
OneKeyLog.error("BundleUpdate", "setCurrentUpdateBundleData: bundle path outside bundle directory: ${bundleDirPath.canonicalPath}")
|
|
2432
|
+
throw BundleUpdateError("SECURITY_REJECTED", "install")
|
|
2433
|
+
}
|
|
2434
|
+
val sigFile = File(BundleUpdateStoreAndroid.getSignatureFilePath(context, bundleVersion))
|
|
2435
|
+
val baseAscDir = File(BundleUpdateStoreAndroid.getAscDir(context)).canonicalPath
|
|
2436
|
+
if (Files.isSymbolicLink(sigFile.toPath()) ||
|
|
2437
|
+
!sigFile.canonicalPath.startsWith(baseAscDir + File.separator)
|
|
2438
|
+
) {
|
|
2439
|
+
OneKeyLog.error("BundleUpdate", "setCurrentUpdateBundleData: signature path outside asc directory: ${sigFile.canonicalPath}")
|
|
2440
|
+
throw BundleUpdateError("SECURITY_REJECTED", "install")
|
|
2441
|
+
}
|
|
2442
|
+
|
|
2443
|
+
// Verify the bundle directory actually exists
|
|
2148
2444
|
if (!bundleDirPath.exists()) {
|
|
2149
2445
|
OneKeyLog.error("BundleUpdate", "setCurrentUpdateBundleData: bundle directory not found: ${bundleDirPath.absolutePath}")
|
|
2150
|
-
throw
|
|
2446
|
+
throw BundleUpdateError("PACKAGE_MISSING", "install")
|
|
2151
2447
|
}
|
|
2152
2448
|
|
|
2153
2449
|
// Verify GPG signature is valid (skipped when both DevSettings and skip-GPG toggle are enabled)
|
|
@@ -2155,22 +2451,36 @@ class ReactNativeBundleUpdate : HybridReactNativeBundleUpdateSpec() {
|
|
|
2155
2451
|
OneKeyLog.info("BundleUpdate", "setCurrentUpdateBundleData: GPG check: skipGPG=$skipGPGSwitch")
|
|
2156
2452
|
if (!skipGPGSwitch) {
|
|
2157
2453
|
if (params.signature.isEmpty() ||
|
|
2158
|
-
!BundleUpdateStoreAndroid.validateMetadataFileSha256(context, bundleVersion, params.signature)) {
|
|
2454
|
+
!BundleUpdateStoreAndroid.validateMetadataFileSha256(context, bundleVersion, params.signature, params.appVersion, params.bundleVersion)) {
|
|
2159
2455
|
OneKeyLog.error("BundleUpdate", "setCurrentUpdateBundleData: GPG signature verification failed")
|
|
2160
|
-
throw
|
|
2456
|
+
throw BundleUpdateError("INTEGRITY_FAILED", "install")
|
|
2161
2457
|
}
|
|
2162
2458
|
OneKeyLog.info("BundleUpdate", "setCurrentUpdateBundleData: GPG signature verified OK")
|
|
2163
2459
|
} else {
|
|
2164
2460
|
OneKeyLog.warn("BundleUpdate", "setCurrentUpdateBundleData: GPG signature verification skipped (DevSettings + skip-GPG enabled)")
|
|
2165
2461
|
}
|
|
2166
2462
|
|
|
2167
|
-
val
|
|
2168
|
-
|
|
2169
|
-
.
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2463
|
+
val metadataFile = File(bundleDirPath, "metadata.json")
|
|
2464
|
+
if (!metadataFile.exists()) {
|
|
2465
|
+
OneKeyLog.error("BundleUpdate", "setCurrentUpdateBundleData: metadata.json not found in ${bundleDirPath.absolutePath}")
|
|
2466
|
+
throw BundleUpdateError("PACKAGE_MISSING", "install")
|
|
2467
|
+
}
|
|
2468
|
+
val metadata = BundleUpdateStoreAndroid.parseMetadataJson(BundleUpdateStoreAndroid.readFileContent(metadataFile))
|
|
2469
|
+
if (!BundleUpdateStoreAndroid.validateEntryBundlesSha256(bundleDirPath.absolutePath, metadata)) {
|
|
2470
|
+
OneKeyLog.error("BundleUpdate", "setCurrentUpdateBundleData: entry bundle SHA256 verification failed")
|
|
2471
|
+
throw BundleUpdateError("INTEGRITY_FAILED", "install")
|
|
2472
|
+
}
|
|
2473
|
+
if (!BundleUpdateStoreAndroid.validateBundlePairCompatibility(context, bundleDirPath.absolutePath, metadata)) {
|
|
2474
|
+
OneKeyLog.error("BundleUpdate", "setCurrentUpdateBundleData: bundle pair compatibility check failed")
|
|
2475
|
+
throw BundleUpdateError("INCOMPATIBLE", "install")
|
|
2173
2476
|
}
|
|
2477
|
+
|
|
2478
|
+
BundleUpdateStoreAndroid.setCurrentBundleVersionAndSignature(context, bundleVersion, params.signature)
|
|
2479
|
+
val currentNativeVersion = BundleUpdateStoreAndroid.getAppVersion(context) ?: ""
|
|
2480
|
+
BundleUpdateStoreAndroid.setNativeVersion(context, currentNativeVersion)
|
|
2481
|
+
val currentBuildNumber = BundleUpdateStoreAndroid.getBuildNumber(context)
|
|
2482
|
+
BundleUpdateStoreAndroid.setNativeBuildNumber(context, currentBuildNumber)
|
|
2483
|
+
BundleUpdateStoreAndroid.invalidateValidatedBundleInfoCache()
|
|
2174
2484
|
OneKeyLog.info("BundleUpdate", "setCurrentUpdateBundleData: switched to $bundleVersion")
|
|
2175
2485
|
}
|
|
2176
2486
|
}
|
|
@@ -2278,7 +2588,7 @@ n2DMz6gqk326W6SFynYtvuiXo7wG4Cmn3SuIU8xfv9rJqunpZGYchMd7nZektmEJ
|
|
|
2278
2588
|
91Js0rQ=
|
|
2279
2589
|
=A/Ii
|
|
2280
2590
|
-----END PGP SIGNATURE-----"""
|
|
2281
|
-
val result = BundleUpdateStoreAndroid.verifyGPGAndExtractSha256(testSignature)
|
|
2591
|
+
val result = BundleUpdateStoreAndroid.verifyGPGAndExtractSha256(testSignature, expectedAppType = null)
|
|
2282
2592
|
val isValid = result == "2ada9c871104fc40649fa3de67a7d8e33faadc18e9abd587e8bb85be0a003eba"
|
|
2283
2593
|
OneKeyLog.info("BundleUpdate", "testVerification: GPG verification result: $isValid")
|
|
2284
2594
|
isValid
|
|
@@ -2305,9 +2615,17 @@ n2DMz6gqk326W6SFynYtvuiXo7wG4Cmn3SuIU8xfv9rJqunpZGYchMd7nZektmEJ
|
|
|
2305
2615
|
|
|
2306
2616
|
override fun isBundleExists(appVersion: String, bundleVersion: String): Promise<Boolean> {
|
|
2307
2617
|
return Promise.async {
|
|
2618
|
+
if (!isSafeVersionString(appVersion) || !isSafeVersionString(bundleVersion)) {
|
|
2619
|
+
OneKeyLog.error("BundleUpdate", "isBundleExists: invalid version string format: appVersion=$appVersion, bundleVersion=$bundleVersion")
|
|
2620
|
+
throw SecurityException("Invalid version string")
|
|
2621
|
+
}
|
|
2308
2622
|
val context = getContext()
|
|
2309
2623
|
val folderName = "$appVersion-$bundleVersion"
|
|
2624
|
+
val baseBundleDir = File(BundleUpdateStoreAndroid.getBundleDir(context)).canonicalPath
|
|
2310
2625
|
val bundlePath = File(BundleUpdateStoreAndroid.getBundleDir(context), folderName)
|
|
2626
|
+
if (!bundlePath.canonicalPath.startsWith(baseBundleDir + File.separator)) {
|
|
2627
|
+
throw SecurityException("Bundle path outside bundle directory")
|
|
2628
|
+
}
|
|
2311
2629
|
val exists = bundlePath.exists()
|
|
2312
2630
|
OneKeyLog.info("BundleUpdate", "isBundleExists: appVersion=$appVersion, bundleVersion=$bundleVersion, exists=$exists")
|
|
2313
2631
|
exists
|
|
@@ -2315,19 +2633,27 @@ n2DMz6gqk326W6SFynYtvuiXo7wG4Cmn3SuIU8xfv9rJqunpZGYchMd7nZektmEJ
|
|
|
2315
2633
|
}
|
|
2316
2634
|
|
|
2317
2635
|
override fun verifyExtractedBundle(appVersion: String, bundleVersion: String): Promise<Unit> {
|
|
2318
|
-
return
|
|
2636
|
+
return updatePromise("verifyPackage") {
|
|
2319
2637
|
OneKeyLog.info("BundleUpdate", "verifyExtractedBundle: appVersion=$appVersion, bundleVersion=$bundleVersion")
|
|
2638
|
+
if (!isSafeVersionString(appVersion) || !isSafeVersionString(bundleVersion)) {
|
|
2639
|
+
OneKeyLog.error("BundleUpdate", "verifyExtractedBundle: invalid version string format: appVersion=$appVersion, bundleVersion=$bundleVersion")
|
|
2640
|
+
throw BundleUpdateError("INVALID_ARGUMENT", "verifyPackage")
|
|
2641
|
+
}
|
|
2320
2642
|
val context = getContext()
|
|
2321
2643
|
val folderName = "$appVersion-$bundleVersion"
|
|
2644
|
+
val baseBundleDir = File(BundleUpdateStoreAndroid.getBundleDir(context)).canonicalPath
|
|
2322
2645
|
val bundlePath = File(BundleUpdateStoreAndroid.getBundleDir(context), folderName)
|
|
2646
|
+
if (!bundlePath.canonicalPath.startsWith(baseBundleDir + File.separator)) {
|
|
2647
|
+
throw BundleUpdateError("SECURITY_REJECTED", "verifyPackage")
|
|
2648
|
+
}
|
|
2323
2649
|
if (!bundlePath.exists()) {
|
|
2324
2650
|
OneKeyLog.error("BundleUpdate", "verifyExtractedBundle: bundle directory not found: ${bundlePath.absolutePath}")
|
|
2325
|
-
throw
|
|
2651
|
+
throw BundleUpdateError("PACKAGE_MISSING", "verifyPackage")
|
|
2326
2652
|
}
|
|
2327
2653
|
val metadataFile = File(bundlePath, "metadata.json")
|
|
2328
2654
|
if (!metadataFile.exists()) {
|
|
2329
2655
|
OneKeyLog.error("BundleUpdate", "verifyExtractedBundle: metadata.json not found in ${bundlePath.absolutePath}")
|
|
2330
|
-
throw
|
|
2656
|
+
throw BundleUpdateError("PACKAGE_MISSING", "verifyPackage")
|
|
2331
2657
|
}
|
|
2332
2658
|
|
|
2333
2659
|
OneKeyLog.info("BundleUpdate", "verifyExtractedBundle: parsing metadata and validating files...")
|
|
@@ -2335,11 +2661,11 @@ n2DMz6gqk326W6SFynYtvuiXo7wG4Cmn3SuIU8xfv9rJqunpZGYchMd7nZektmEJ
|
|
|
2335
2661
|
val metadata = BundleUpdateStoreAndroid.parseMetadataJson(metadataContent)
|
|
2336
2662
|
if (!BundleUpdateStoreAndroid.validateAllFilesInDir(context, bundlePath.absolutePath, metadata, appVersion, bundleVersion)) {
|
|
2337
2663
|
OneKeyLog.error("BundleUpdate", "verifyExtractedBundle: file integrity check failed")
|
|
2338
|
-
throw
|
|
2664
|
+
throw BundleUpdateError("INTEGRITY_FAILED", "verifyPackage")
|
|
2339
2665
|
}
|
|
2340
2666
|
if (!BundleUpdateStoreAndroid.validateBundlePairCompatibility(context, bundlePath.absolutePath, metadata)) {
|
|
2341
2667
|
OneKeyLog.error("BundleUpdate", "verifyExtractedBundle: bundle pair compatibility check failed")
|
|
2342
|
-
throw
|
|
2668
|
+
throw BundleUpdateError("INCOMPATIBLE", "verifyPackage")
|
|
2343
2669
|
}
|
|
2344
2670
|
OneKeyLog.info("BundleUpdate", "verifyExtractedBundle: all files verified OK, fileCount=${metadata.size}")
|
|
2345
2671
|
}
|
|
@@ -2348,19 +2674,15 @@ n2DMz6gqk326W6SFynYtvuiXo7wG4Cmn3SuIU8xfv9rJqunpZGYchMd7nZektmEJ
|
|
|
2348
2674
|
override fun listLocalBundles(): Promise<Array<LocalBundleInfo>> {
|
|
2349
2675
|
return Promise.async {
|
|
2350
2676
|
val context = getContext()
|
|
2677
|
+
val appVersion = BundleUpdateStoreAndroid.getAppVersion(context) ?: return@async emptyArray<LocalBundleInfo>()
|
|
2351
2678
|
val bundleDir = File(BundleUpdateStoreAndroid.getBundleDir(context))
|
|
2352
2679
|
val results = mutableListOf<LocalBundleInfo>()
|
|
2353
2680
|
if (bundleDir.exists() && bundleDir.isDirectory) {
|
|
2354
2681
|
bundleDir.listFiles()?.forEach { child ->
|
|
2355
2682
|
if (!child.isDirectory) return@forEach
|
|
2356
2683
|
val name = child.name
|
|
2357
|
-
val
|
|
2358
|
-
|
|
2359
|
-
val appVer = name.substring(0, lastDash)
|
|
2360
|
-
val bundleVer = name.substring(lastDash + 1)
|
|
2361
|
-
if (appVer.isNotEmpty() && bundleVer.isNotEmpty()) {
|
|
2362
|
-
results.add(LocalBundleInfo(appVersion = appVer, bundleVersion = bundleVer))
|
|
2363
|
-
}
|
|
2684
|
+
val bundleVersion = BundleUpdateStoreAndroid.bundleVersionInFolder(name, appVersion) ?: return@forEach
|
|
2685
|
+
results.add(LocalBundleInfo(appVersion = appVersion, bundleVersion = bundleVersion))
|
|
2364
2686
|
}
|
|
2365
2687
|
}
|
|
2366
2688
|
OneKeyLog.info("BundleUpdate", "listLocalBundles: found ${results.size} bundles")
|
|
@@ -2415,10 +2737,24 @@ n2DMz6gqk326W6SFynYtvuiXo7wG4Cmn3SuIU8xfv9rJqunpZGYchMd7nZektmEJ
|
|
|
2415
2737
|
|
|
2416
2738
|
override fun testDeleteJsBundle(appVersion: String, bundleVersion: String): Promise<TestResult> {
|
|
2417
2739
|
return Promise.async {
|
|
2740
|
+
if (!BuildConfig.DEBUG) {
|
|
2741
|
+
throw SecurityException("Test fault-injection methods are disabled in release builds")
|
|
2742
|
+
}
|
|
2743
|
+
if (!isSafeVersionString(appVersion) || !isSafeVersionString(bundleVersion)) {
|
|
2744
|
+
throw SecurityException("Invalid version string")
|
|
2745
|
+
}
|
|
2418
2746
|
OneKeyLog.info("BundleUpdate", "testDeleteJsBundle: appVersion=$appVersion, bundleVersion=$bundleVersion")
|
|
2419
2747
|
val context = getContext()
|
|
2420
2748
|
val folderName = "$appVersion-$bundleVersion"
|
|
2421
|
-
val
|
|
2749
|
+
val baseBundleDir = File(BundleUpdateStoreAndroid.getBundleDir(context)).canonicalPath
|
|
2750
|
+
val bundleDirPath = File(BundleUpdateStoreAndroid.getBundleDir(context), folderName)
|
|
2751
|
+
if (!bundleDirPath.canonicalPath.startsWith(baseBundleDir + File.separator)) {
|
|
2752
|
+
throw SecurityException("Bundle path outside bundle directory")
|
|
2753
|
+
}
|
|
2754
|
+
val jsBundlePath = File(bundleDirPath, "main.jsbundle.hbc")
|
|
2755
|
+
if (!jsBundlePath.canonicalPath.startsWith(baseBundleDir + File.separator)) {
|
|
2756
|
+
throw SecurityException("Bundle path outside bundle directory")
|
|
2757
|
+
}
|
|
2422
2758
|
|
|
2423
2759
|
if (jsBundlePath.exists()) {
|
|
2424
2760
|
val success = jsBundlePath.delete()
|
|
@@ -2438,10 +2774,20 @@ n2DMz6gqk326W6SFynYtvuiXo7wG4Cmn3SuIU8xfv9rJqunpZGYchMd7nZektmEJ
|
|
|
2438
2774
|
|
|
2439
2775
|
override fun testDeleteJsRuntimeDir(appVersion: String, bundleVersion: String): Promise<TestResult> {
|
|
2440
2776
|
return Promise.async {
|
|
2777
|
+
if (!BuildConfig.DEBUG) {
|
|
2778
|
+
throw SecurityException("Test fault-injection methods are disabled in release builds")
|
|
2779
|
+
}
|
|
2780
|
+
if (!isSafeVersionString(appVersion) || !isSafeVersionString(bundleVersion)) {
|
|
2781
|
+
throw SecurityException("Invalid version string")
|
|
2782
|
+
}
|
|
2441
2783
|
OneKeyLog.info("BundleUpdate", "testDeleteJsRuntimeDir: appVersion=$appVersion, bundleVersion=$bundleVersion")
|
|
2442
2784
|
val context = getContext()
|
|
2443
2785
|
val folderName = "$appVersion-$bundleVersion"
|
|
2786
|
+
val baseBundleDir = File(BundleUpdateStoreAndroid.getBundleDir(context)).canonicalPath
|
|
2444
2787
|
val dir = File(BundleUpdateStoreAndroid.getBundleDir(context), folderName)
|
|
2788
|
+
if (!dir.canonicalPath.startsWith(baseBundleDir + File.separator)) {
|
|
2789
|
+
throw SecurityException("Bundle path outside bundle directory")
|
|
2790
|
+
}
|
|
2445
2791
|
|
|
2446
2792
|
if (dir.exists()) {
|
|
2447
2793
|
BundleUpdateStoreAndroid.deleteDir(dir)
|
|
@@ -2461,10 +2807,24 @@ n2DMz6gqk326W6SFynYtvuiXo7wG4Cmn3SuIU8xfv9rJqunpZGYchMd7nZektmEJ
|
|
|
2461
2807
|
|
|
2462
2808
|
override fun testDeleteMetadataJson(appVersion: String, bundleVersion: String): Promise<TestResult> {
|
|
2463
2809
|
return Promise.async {
|
|
2810
|
+
if (!BuildConfig.DEBUG) {
|
|
2811
|
+
throw SecurityException("Test fault-injection methods are disabled in release builds")
|
|
2812
|
+
}
|
|
2813
|
+
if (!isSafeVersionString(appVersion) || !isSafeVersionString(bundleVersion)) {
|
|
2814
|
+
throw SecurityException("Invalid version string")
|
|
2815
|
+
}
|
|
2464
2816
|
OneKeyLog.info("BundleUpdate", "testDeleteMetadataJson: appVersion=$appVersion, bundleVersion=$bundleVersion")
|
|
2465
2817
|
val context = getContext()
|
|
2466
2818
|
val folderName = "$appVersion-$bundleVersion"
|
|
2467
|
-
val
|
|
2819
|
+
val baseBundleDir = File(BundleUpdateStoreAndroid.getBundleDir(context)).canonicalPath
|
|
2820
|
+
val bundleDirPath = File(BundleUpdateStoreAndroid.getBundleDir(context), folderName)
|
|
2821
|
+
if (!bundleDirPath.canonicalPath.startsWith(baseBundleDir + File.separator)) {
|
|
2822
|
+
throw SecurityException("Bundle path outside bundle directory")
|
|
2823
|
+
}
|
|
2824
|
+
val metadataFile = File(bundleDirPath, "metadata.json")
|
|
2825
|
+
if (!metadataFile.canonicalPath.startsWith(baseBundleDir + File.separator)) {
|
|
2826
|
+
throw SecurityException("Bundle path outside bundle directory")
|
|
2827
|
+
}
|
|
2468
2828
|
|
|
2469
2829
|
if (metadataFile.exists()) {
|
|
2470
2830
|
val success = metadataFile.delete()
|
|
@@ -2484,11 +2844,24 @@ n2DMz6gqk326W6SFynYtvuiXo7wG4Cmn3SuIU8xfv9rJqunpZGYchMd7nZektmEJ
|
|
|
2484
2844
|
|
|
2485
2845
|
override fun testWriteEmptyMetadataJson(appVersion: String, bundleVersion: String): Promise<TestResult> {
|
|
2486
2846
|
return Promise.async {
|
|
2847
|
+
if (!BuildConfig.DEBUG) {
|
|
2848
|
+
throw SecurityException("Test fault-injection methods are disabled in release builds")
|
|
2849
|
+
}
|
|
2850
|
+
if (!isSafeVersionString(appVersion) || !isSafeVersionString(bundleVersion)) {
|
|
2851
|
+
throw SecurityException("Invalid version string")
|
|
2852
|
+
}
|
|
2487
2853
|
OneKeyLog.info("BundleUpdate", "testWriteEmptyMetadataJson: appVersion=$appVersion, bundleVersion=$bundleVersion")
|
|
2488
2854
|
val context = getContext()
|
|
2489
2855
|
val folderName = "$appVersion-$bundleVersion"
|
|
2856
|
+
val baseBundleDir = File(BundleUpdateStoreAndroid.getBundleDir(context)).canonicalPath
|
|
2490
2857
|
val jsRuntimeDir = File(BundleUpdateStoreAndroid.getBundleDir(context), folderName)
|
|
2858
|
+
if (!jsRuntimeDir.canonicalPath.startsWith(baseBundleDir + File.separator)) {
|
|
2859
|
+
throw SecurityException("Bundle path outside bundle directory")
|
|
2860
|
+
}
|
|
2491
2861
|
val metadataPath = File(jsRuntimeDir, "metadata.json")
|
|
2862
|
+
if (!metadataPath.canonicalPath.startsWith(baseBundleDir + File.separator)) {
|
|
2863
|
+
throw SecurityException("Bundle path outside bundle directory")
|
|
2864
|
+
}
|
|
2492
2865
|
|
|
2493
2866
|
if (!jsRuntimeDir.exists()) {
|
|
2494
2867
|
if (!jsRuntimeDir.mkdirs()) {
|
|
@@ -2508,3 +2881,4 @@ n2DMz6gqk326W6SFynYtvuiXo7wG4Cmn3SuIU8xfv9rJqunpZGYchMd7nZektmEJ
|
|
|
2508
2881
|
}
|
|
2509
2882
|
}
|
|
2510
2883
|
}
|
|
2884
|
+
|