@onekeyfe/react-native-bundle-update 3.0.38 → 3.0.39

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. package/ReactNativeBundleUpdate.podspec +13 -3
  2. package/android/build.gradle +11 -4
  3. package/android/src/main/java/com/margelo/nitro/reactnativebundleupdate/ReactNativeBundleUpdate.kt +82 -211
  4. package/ios/ReactNativeBundleUpdate.swift +109 -302
  5. package/package.json +1 -1
  6. package/ios/Frameworks/Gopenpgp.xcframework/Info.plist +0 -52
  7. package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64/Gopenpgp.framework/Gopenpgp +0 -0
  8. package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64/Gopenpgp.framework/Headers/Armor.objc.h +0 -96
  9. package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64/Gopenpgp.framework/Headers/Constants.objc.h +0 -197
  10. package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64/Gopenpgp.framework/Headers/Crypto.objc.h +0 -1963
  11. package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64/Gopenpgp.framework/Headers/Gopenpgp.h +0 -23
  12. package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64/Gopenpgp.framework/Headers/Mime.objc.h +0 -59
  13. package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64/Gopenpgp.framework/Headers/Mobile.objc.h +0 -252
  14. package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64/Gopenpgp.framework/Headers/Profile.objc.h +0 -107
  15. package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64/Gopenpgp.framework/Headers/Universe.objc.h +0 -29
  16. package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64/Gopenpgp.framework/Headers/ref.h +0 -35
  17. package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64/Gopenpgp.framework/Info.plist +0 -20
  18. package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64/Gopenpgp.framework/Modules/module.modulemap +0 -13
  19. package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64_x86_64-simulator/Gopenpgp.framework/Gopenpgp +0 -0
  20. package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64_x86_64-simulator/Gopenpgp.framework/Headers/Armor.objc.h +0 -96
  21. package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64_x86_64-simulator/Gopenpgp.framework/Headers/Constants.objc.h +0 -197
  22. package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64_x86_64-simulator/Gopenpgp.framework/Headers/Crypto.objc.h +0 -1963
  23. package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64_x86_64-simulator/Gopenpgp.framework/Headers/Gopenpgp.h +0 -23
  24. package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64_x86_64-simulator/Gopenpgp.framework/Headers/Mime.objc.h +0 -59
  25. package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64_x86_64-simulator/Gopenpgp.framework/Headers/Mobile.objc.h +0 -252
  26. package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64_x86_64-simulator/Gopenpgp.framework/Headers/Profile.objc.h +0 -107
  27. package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64_x86_64-simulator/Gopenpgp.framework/Headers/Universe.objc.h +0 -29
  28. package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64_x86_64-simulator/Gopenpgp.framework/Headers/ref.h +0 -35
  29. package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64_x86_64-simulator/Gopenpgp.framework/Info.plist +0 -20
  30. package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64_x86_64-simulator/Gopenpgp.framework/Modules/module.modulemap +0 -13
@@ -19,8 +19,6 @@ Pod::Spec.new do |s|
19
19
  "cpp/**/*.{hpp,cpp}",
20
20
  ]
21
21
 
22
- s.vendored_frameworks = 'ios/Frameworks/Gopenpgp.xcframework'
23
-
24
22
  # When ONEKEY_ALLOW_SKIP_GPG_VERIFICATION env var is set to a non-empty, non-'false' value,
25
23
  # enable the ALLOW_SKIP_GPG_VERIFICATION Swift compilation condition.
26
24
  # Without this flag, all skip-GPG code paths are compiled out (dead code elimination).
@@ -33,7 +31,19 @@ Pod::Spec.new do |s|
33
31
  s.dependency 'React-jsi'
34
32
  s.dependency 'React-callinvoker'
35
33
  s.dependency 'ReactNativeNativeLogger'
36
- s.dependency 'SSZipArchive', '~> 2.4'
34
+ # Shared GPG/sha256/path-safety security core. iOS verification + hashing now
35
+ # delegates to react-native-bundle-crypto's BundleCryptoCore, which owns the
36
+ # single in-tree Gopenpgp.xcframework — removing the duplicate framework that
37
+ # previously caused a same-named pod conflict on iOS.
38
+ s.dependency 'ReactNativeBundleCrypto'
39
+ # Shared concurrent + background multi-range downloader. The iOS bundle
40
+ # download now delegates its 8-range background download to
41
+ # react-native-range-downloader's RangeDownloader.shared (the concurrent
42
+ # downloader that originated here), removing the in-tree duplicate.
43
+ s.dependency 'ReactNativeRangeDownloader'
44
+ # >= 2.5.4 pulls the fix for the zip-slip / symlink path-traversal CVEs
45
+ # (CVE-2023-39139 etc.) — important since OTA unzips downloaded archives.
46
+ s.dependency 'SSZipArchive', '>= 2.5.4'
37
47
  s.dependency 'MMKV', '~> 2.2'
38
48
 
39
49
  load 'nitrogen/generated/ios/ReactNativeBundleUpdate+autolinking.rb'
@@ -136,11 +136,18 @@ dependencies {
136
136
 
137
137
  implementation project(":onekeyfe_react-native-native-logger")
138
138
 
139
- implementation "com.squareup.okhttp3:okhttp:4.12.0"
139
+ // Shared security core (GPG verify / sha256 / dir-hash / constant-time
140
+ // compare) lives in react-native-bundle-crypto. This module calls
141
+ // BundleCryptoCore directly instead of carrying its own BouncyCastle-based
142
+ // implementation, so the GPG public key + verify logic has a single source
143
+ // of truth across both native modules.
144
+ implementation project(":onekeyfe_react-native-bundle-crypto")
145
+
146
+ // Shared concurrent range downloader lives in react-native-range-downloader.
147
+ // This module reuses it instead of carrying its own byte-identical copy.
148
+ implementation project(":onekeyfe_react-native-range-downloader")
140
149
 
141
- // BouncyCastle for GPG/PGP signature verification
142
- implementation "org.bouncycastle:bcpg-jdk15to18:1.78.1"
143
- implementation "org.bouncycastle:bcprov-jdk15to18:1.78.1"
150
+ implementation "com.squareup.okhttp3:okhttp:4.12.0"
144
151
 
145
152
  // MMKV for reading DevSettings (compileOnly: provided by the host app via react-native-mmkv)
146
153
  compileOnly "io.github.zhongwuzw:mmkv:2.2.4"
@@ -7,28 +7,22 @@ import com.facebook.proguard.annotations.DoNotStrip
7
7
  import com.margelo.nitro.core.Promise
8
8
  import com.margelo.nitro.NitroModules
9
9
  import com.margelo.nitro.nativelogger.OneKeyLog
10
+ import com.margelo.nitro.reactnativebundlecrypto.BundleCryptoCore
11
+ import com.margelo.nitro.reactnativerangedownloader.ConcurrentRangeDownloader
10
12
  import com.tencent.mmkv.MMKV
11
13
  import okhttp3.OkHttpClient
12
14
  import okhttp3.Request
13
- import java.io.BufferedInputStream
14
15
  import java.io.File
15
16
  import java.io.FileInputStream
16
17
  import java.io.FileOutputStream
17
18
  import java.nio.file.Files
18
19
  import java.nio.file.Path
19
20
  import java.nio.file.Paths
20
- import java.security.MessageDigest
21
21
  import java.util.concurrent.CopyOnWriteArrayList
22
22
  import java.util.concurrent.atomic.AtomicBoolean
23
23
  import java.util.concurrent.atomic.AtomicLong
24
24
  import java.util.zip.ZipEntry
25
25
  import java.util.zip.ZipInputStream
26
- import org.bouncycastle.openpgp.PGPPublicKeyRingCollection
27
- import org.bouncycastle.openpgp.PGPUtil
28
- import org.bouncycastle.openpgp.jcajce.JcaPGPObjectFactory
29
- import org.bouncycastle.openpgp.operator.jcajce.JcaKeyFingerprintCalculator
30
- import org.bouncycastle.openpgp.operator.jcajce.JcaPGPContentVerifierBuilderProvider
31
- import org.bouncycastle.jce.provider.BouncyCastleProvider
32
26
  import org.json.JSONArray
33
27
  import org.json.JSONObject
34
28
 
@@ -37,75 +31,8 @@ private data class BundleListener(
37
31
  val callback: (BundleDownloadEvent) -> Unit
38
32
  )
39
33
 
40
- // OneKey GPG public key for signature verification
41
- private const val GPG_PUBLIC_KEY = """-----BEGIN PGP PUBLIC KEY BLOCK-----
42
-
43
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- zHFIK9HmDehx3LoDAYc66nhZXyasiu6qGPzuxXu8/4qTY8MnhXJRBkbWz5P84fx1
51
- /Db5WETLE72on11XLreFWmlJnEWN4UOARrNn1Zxbwl+uxlSJyM+2GTl4yoccG+WR
52
- uOUCmRXTgduHxejPGI1PfsNmFpVefAWBDO7SdnwZb1oUP3AFmhH5CD1GnmLnET+l
53
- /c+7XfFLwgSUVSADBdO3GVS4Cr9ux4nIrHGJCrrroFfM2yvG8AtUVr16PQARAQAB
54
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- xy/rjN2SMJTFzkNXPkwXYrMYoahGez1oZfLzV6SQ0+blNc3aATt9aQW6uaCZtMw1
65
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 5ousZOOqsCkl1J7rCeaIFowIEA3TzH0FWIQGahGiHN/+zwc7iSIL9gNEq4/AYJWK
74
- 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
- nGylez9tZBfv+Bymv5RTRAHey49noF/6ZcF5fidtXAS2tfhuRIlOUfEY+QyB3lXj
76
- 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
- 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
- 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
- 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
- 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
- CPgkWQr9luk/3KjSntMjh9ySx5VJbAi2IX2X/w6Ze9hky3DeEdxRRlV0meTTGupP
82
- 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
- 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
- 9TDkaCoUlch/mf5IJW1RnyUZ50RbB7jBeyg8XHE5zYarDmvhOskV2ADcym1h5teZ
85
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- dQXaCdBX3HtQ6vVD964omzE+XkIJm0w30YVbXRwPEWjtw7kKH78GSSR95u4j/hZr
100
- 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
- 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
- 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
103
- =HlTQ
104
- -----END PGP PUBLIC KEY BLOCK-----"""
105
-
106
34
  // Public static store for CustomReactNativeHost access (called before JS starts)
107
35
  object BundleUpdateStoreAndroid {
108
- private val bcProvider = BouncyCastleProvider()
109
36
  private const val PREFS_NAME = "BundleUpdatePrefs"
110
37
  internal const val NATIVE_VERSION_PREFS_NAME = "NativeVersionPrefs"
111
38
  private const val CURRENT_BUNDLE_VERSION_KEY = "currentBundleVersion"
@@ -426,53 +353,21 @@ object BundleUpdateStoreAndroid {
426
353
  */
427
354
  fun lastSHA256FailureReason(): String? = lastSHA256Failure.get()
428
355
 
356
+ // Thin delegate over the shared BundleCryptoCore.sha256OfFile. The actual
357
+ // streaming MessageDigest implementation and its failureReason taxonomy
358
+ // (FILE_NOT_FOUND / FILE_DISAPPEARED / FILE_TRUNCATED / PERMISSION_DENIED /
359
+ // OOM / IO_<class> / UNEXPECTED_<class>) now live in react-native-bundle-crypto.
360
+ // We re-publish that reason into the existing per-thread lastSHA256Failure
361
+ // ThreadLocal so lastSHA256FailureReason() keeps working unchanged for all
362
+ // existing analytics call sites.
429
363
  fun calculateSHA256(filePath: String): String? {
430
364
  lastSHA256Failure.set(null)
431
- val file = File(filePath)
432
- if (!file.exists()) {
433
- lastSHA256Failure.set("FILE_NOT_FOUND")
434
- OneKeyLog.error("BundleUpdate", "calculateSHA256: file not found: $filePath")
365
+ val result = BundleCryptoCore.sha256OfFile(filePath)
366
+ if (result.sha256 == null) {
367
+ lastSHA256Failure.set(result.failureReason)
435
368
  return null
436
369
  }
437
- return try {
438
- val digest = MessageDigest.getInstance("SHA-256")
439
- BufferedInputStream(FileInputStream(filePath)).use { bis ->
440
- val buffer = ByteArray(8192)
441
- var count: Int
442
- while (bis.read(buffer).also { count = it } > 0) {
443
- digest.update(buffer, 0, count)
444
- }
445
- }
446
- bytesToHex(digest.digest())
447
- } catch (e: java.io.FileNotFoundException) {
448
- lastSHA256Failure.set("FILE_DISAPPEARED")
449
- OneKeyLog.error("BundleUpdate", "calculateSHA256: file disappeared during read: ${e.message}")
450
- null
451
- } catch (e: java.io.EOFException) {
452
- lastSHA256Failure.set("FILE_TRUNCATED")
453
- OneKeyLog.error("BundleUpdate", "calculateSHA256: truncated file: ${e.message}")
454
- null
455
- } catch (e: SecurityException) {
456
- lastSHA256Failure.set("PERMISSION_DENIED")
457
- OneKeyLog.error("BundleUpdate", "calculateSHA256: permission denied: ${e.message}")
458
- null
459
- } catch (e: OutOfMemoryError) {
460
- lastSHA256Failure.set("OOM")
461
- OneKeyLog.error("BundleUpdate", "calculateSHA256: OutOfMemoryError on ${file.length()} bytes")
462
- null
463
- } catch (e: java.io.IOException) {
464
- lastSHA256Failure.set("IO_${e.javaClass.simpleName}")
465
- OneKeyLog.error("BundleUpdate", "calculateSHA256: ${e.javaClass.simpleName}: ${e.message}")
466
- null
467
- } catch (e: Exception) {
468
- lastSHA256Failure.set("UNEXPECTED_${e.javaClass.simpleName}")
469
- OneKeyLog.error("BundleUpdate", "calculateSHA256: ${e.javaClass.simpleName}: ${e.message}")
470
- null
471
- }
472
- }
473
-
474
- private fun bytesToHex(bytes: ByteArray): String {
475
- return bytes.joinToString("") { "%02x".format(it) }
370
+ return result.sha256
476
371
  }
477
372
 
478
373
  fun getMetadataFilePath(context: Context, currentBundleVersion: String?): String? {
@@ -564,16 +459,13 @@ object BundleUpdateStoreAndroid {
564
459
  return null
565
460
  }
566
461
 
567
- /** Constant-time comparison to prevent timing attacks on hash values */
462
+ /**
463
+ * Constant-time comparison to prevent timing attacks on hash values.
464
+ * Delegates to the shared BundleCryptoCore.secureEqualHex (same byte-wise
465
+ * constant-time algorithm).
466
+ */
568
467
  fun secureCompare(a: String, b: String): Boolean {
569
- val aBytes = a.toByteArray(Charsets.UTF_8)
570
- val bBytes = b.toByteArray(Charsets.UTF_8)
571
- if (aBytes.size != bBytes.size) return false
572
- var result = 0
573
- for (i in aBytes.indices) {
574
- result = result or (aBytes[i].toInt() xor bBytes[i].toInt())
575
- }
576
- return result == 0
468
+ return BundleCryptoCore.secureEqualHex(a, b)
577
469
  }
578
470
 
579
471
  fun isSafeVersionString(version: String): Boolean {
@@ -581,91 +473,23 @@ object BundleUpdateStoreAndroid {
581
473
  }
582
474
 
583
475
  /**
584
- * Verify a PGP cleartext-signed message using BouncyCastle and extract the sha256.
585
- * Returns null if verification fails or the signature is not a PGP cleartext message.
476
+ * Verify a PGP cleartext-signed message and extract the sha256.
477
+ * Returns null if verification fails or the signature is not a PGP cleartext
478
+ * message.
479
+ *
480
+ * Delegates to the shared BundleCryptoCore.verifyGpgCleartext (the
481
+ * BouncyCastle verify logic + the OneKey GPG public key now live there as
482
+ * the single source of truth). Behavior is preserved: any non-valid result
483
+ * (NOT_PGP_SIGNED_MESSAGE / INVALID_FORMAT / NO_SIGNATURE / PUBKEY_NOT_FOUND
484
+ * / SIGNATURE_INVALID / NO_SHA256_FIELD / ERROR_*) maps to null.
586
485
  */
587
486
  fun verifyGPGAndExtractSha256(signature: String): String? {
588
- if (!signature.contains("-----BEGIN PGP SIGNED MESSAGE-----")) return null
589
-
590
- return try {
591
- // Parse the cleartext signed message
592
- val inputStream = signature.byteInputStream()
593
- val pgpFactory = JcaPGPObjectFactory(PGPUtil.getDecoderStream(inputStream))
594
-
595
- // Parse the public key
596
- val pubKeyStream = PGPUtil.getDecoderStream(GPG_PUBLIC_KEY.byteInputStream())
597
- val pgpPubKeyRingCollection = PGPPublicKeyRingCollection(pubKeyStream, JcaKeyFingerprintCalculator())
598
-
599
- // Extract cleartext and signature from the PGP signed message manually
600
- // BouncyCastle's cleartext handling requires manual parsing
601
- val lines = signature.lines()
602
- val hashHeaderIdx = lines.indexOfFirst { it.startsWith("Hash:") }
603
- val sigStartIdx = lines.indexOfFirst { it == "-----BEGIN PGP SIGNATURE-----" }
604
- val sigEndIdx = lines.indexOfFirst { it == "-----END PGP SIGNATURE-----" }
605
-
606
- if (hashHeaderIdx < 0 || sigStartIdx < 0 || sigEndIdx < 0) {
607
- OneKeyLog.error("BundleUpdate", "Invalid PGP cleartext signed message format")
608
- return null
609
- }
610
-
611
- // The cleartext body is between the Hash header blank line and the PGP SIGNATURE block
612
- val bodyStartIdx = hashHeaderIdx + 2 // skip Hash: line and the blank line after it
613
- val bodyLines = lines.subList(bodyStartIdx, sigStartIdx)
614
- // Remove trailing empty line that PGP adds
615
- val cleartextBody = bodyLines.joinToString("\r\n").trimEnd()
616
-
617
- // The signature block
618
- val sigBlock = lines.subList(sigStartIdx, sigEndIdx + 1).joinToString("\n")
619
-
620
- // Decode the signature
621
- val sigInputStream = PGPUtil.getDecoderStream(sigBlock.byteInputStream())
622
- val sigFactory = JcaPGPObjectFactory(sigInputStream)
623
- val signatureList = sigFactory.nextObject()
624
-
625
- if (signatureList !is org.bouncycastle.openpgp.PGPSignatureList || signatureList.isEmpty) {
626
- OneKeyLog.error("BundleUpdate", "No PGP signature found in message")
627
- return null
628
- }
629
-
630
- val pgpSignature = signatureList[0]
631
- val keyId = pgpSignature.keyID
632
-
633
- // Find the public key
634
- val publicKey = pgpPubKeyRingCollection.getPublicKey(keyId)
635
- if (publicKey == null) {
636
- OneKeyLog.error("BundleUpdate", "Public key not found for keyId: ${java.lang.Long.toHexString(keyId)}")
637
- return null
638
- }
639
-
640
- // Verify the signature
641
- pgpSignature.init(JcaPGPContentVerifierBuilderProvider().setProvider(bcProvider), publicKey)
642
-
643
- // Dash-unescape the cleartext per RFC 4880 Section 7.1
644
- val unescapedLines = cleartextBody.lines().map { line ->
645
- if (line.startsWith("- ")) line.substring(2) else line
646
- }
647
- val dataToVerify = unescapedLines.joinToString("\r\n").toByteArray(Charsets.UTF_8)
648
- pgpSignature.update(dataToVerify)
649
-
650
- if (!pgpSignature.verify()) {
651
- OneKeyLog.error("BundleUpdate", "GPG signature verification failed")
652
- return null
653
- }
654
-
655
- // Signature verified, parse the cleartext JSON
656
- val json = JSONObject(cleartextBody.trim())
657
- val sha256 = json.optString("sha256", "")
658
- if (sha256.isEmpty()) {
659
- OneKeyLog.error("BundleUpdate", "No sha256 field in signed cleartext JSON")
660
- return null
661
- }
662
-
663
- OneKeyLog.info("BundleUpdate", "GPG verification succeeded, sha256: $sha256")
664
- sha256
665
- } catch (e: Exception) {
666
- OneKeyLog.error("BundleUpdate", "GPG verification error: ${e.message}")
667
- null
487
+ val result = BundleCryptoCore.verifyGpgCleartext(signature)
488
+ if (!result.valid) {
489
+ OneKeyLog.error("BundleUpdate", "GPG verification failed: ${result.reason}")
490
+ return null
668
491
  }
492
+ return result.sha256
669
493
  }
670
494
 
671
495
  fun validateMetadataFileSha256(context: Context, currentBundleVersion: String, signature: String?): Boolean {
@@ -1380,6 +1204,10 @@ class ReactNativeBundleUpdate : HybridReactNativeBundleUpdateSpec() {
1380
1204
  OneKeyLog.info("BundleUpdate", "downloadBundle: file already exists, verifying SHA256...")
1381
1205
  if (verifyBundleSHA256(filePath, sha256)) {
1382
1206
  OneKeyLog.info("BundleUpdate", "downloadBundle: existing file SHA256 valid, skipping download")
1207
+ // Final file is authoritative — drop any stale concurrent
1208
+ // partial/manifest left by an earlier interrupted attempt.
1209
+ if (partialFile.exists()) partialFile.delete()
1210
+ File("$partialFilePath.progress").delete()
1383
1211
  isDownloading.set(false)
1384
1212
  Thread.sleep(1000)
1385
1213
  sendEvent("update/complete")
@@ -1392,6 +1220,49 @@ class ReactNativeBundleUpdate : HybridReactNativeBundleUpdateSpec() {
1392
1220
  }
1393
1221
  }
1394
1222
 
1223
+ // === Concurrent multi-range download (P2) ===
1224
+ // Try the 8-range concurrent path first. On COMPLETED the .partial
1225
+ // is fully on disk, so we promote + verify here and finish. On
1226
+ // FALLBACK we fall through to the original single-stream path below.
1227
+ // Transient errors bubble to the outer catch (the concurrent
1228
+ // downloader keeps its partial + manifest for resume).
1229
+ sendEvent("update/start")
1230
+ run {
1231
+ var concurrentProgress = -1
1232
+ val concurrentOutcome = ConcurrentRangeDownloader(
1233
+ httpClient = httpClient,
1234
+ log = { msg -> OneKeyLog.info("BundleUpdate", msg) },
1235
+ ).download(downloadUrl, partialFilePath) { transferred, total ->
1236
+ if (total > 0) {
1237
+ val p = ((transferred * 100) / total).toInt().coerceIn(0, 100)
1238
+ if (p != concurrentProgress) {
1239
+ sendEvent("update/downloading", progress = p)
1240
+ concurrentProgress = p
1241
+ }
1242
+ }
1243
+ }
1244
+ if (concurrentOutcome == ConcurrentRangeDownloader.Outcome.COMPLETED) {
1245
+ // Promote .partial -> final, then SHA256 verify — identical
1246
+ // finalize to the single-stream path below.
1247
+ if (downloadedFile.exists()) downloadedFile.delete()
1248
+ if (!File(partialFilePath).renameTo(downloadedFile)) {
1249
+ OneKeyLog.error("BundleUpdate", "downloadBundle: concurrent rename .partial -> final failed")
1250
+ throw Exception("Failed to finalize download")
1251
+ }
1252
+ OneKeyLog.info("BundleUpdate", "downloadBundle: concurrent download finished, verifying SHA256...")
1253
+ if (!verifyBundleSHA256(filePath, sha256)) {
1254
+ val reason = BundleUpdateStoreAndroid.lastSHA256FailureReason() ?: "MISMATCH"
1255
+ File(filePath).delete()
1256
+ OneKeyLog.error("BundleUpdate", "downloadBundle: concurrent SHA256 verification failed, reason=$reason")
1257
+ throw Exception("Bundle SHA256 verification failed: $reason")
1258
+ }
1259
+ sendEvent("update/complete")
1260
+ OneKeyLog.info("BundleUpdate", "downloadBundle: concurrent completed successfully, appVersion=$appVersion, bundleVersion=$bundleVersion")
1261
+ return@async result
1262
+ }
1263
+ OneKeyLog.info("BundleUpdate", "downloadBundle: concurrent not used, falling back to single-stream")
1264
+ }
1265
+
1395
1266
  // Resume: if a partial from a previous run exists and is smaller
1396
1267
  // than the expected size, send `Range: bytes=<offset>-` so the
1397
1268
  // server fills in the rest. If the partial is exactly the
@@ -1430,8 +1301,8 @@ class ReactNativeBundleUpdate : HybridReactNativeBundleUpdateSpec() {
1430
1301
  }
1431
1302
  }
1432
1303
 
1433
- sendEvent("update/start")
1434
- OneKeyLog.info("BundleUpdate", "downloadBundle: starting download (resume=${partialBytes > 0})...")
1304
+ // update/start already emitted before the concurrent attempt above.
1305
+ OneKeyLog.info("BundleUpdate", "downloadBundle: starting single-stream download (resume=${partialBytes > 0})...")
1435
1306
 
1436
1307
  val requestBuilder = Request.Builder().url(downloadUrl)
1437
1308
  if (partialBytes > 0) {