@onekeyfe/react-native-bundle-update 3.0.37 → 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.
- package/ReactNativeBundleUpdate.podspec +13 -3
- package/android/build.gradle +11 -4
- package/android/src/main/java/com/margelo/nitro/reactnativebundleupdate/ReactNativeBundleUpdate.kt +82 -211
- package/ios/ReactNativeBundleUpdate.swift +109 -302
- package/package.json +1 -1
- package/ios/Frameworks/Gopenpgp.xcframework/Info.plist +0 -52
- package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64/Gopenpgp.framework/Gopenpgp +0 -0
- package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64/Gopenpgp.framework/Headers/Armor.objc.h +0 -96
- package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64/Gopenpgp.framework/Headers/Constants.objc.h +0 -197
- package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64/Gopenpgp.framework/Headers/Crypto.objc.h +0 -1963
- package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64/Gopenpgp.framework/Headers/Gopenpgp.h +0 -23
- package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64/Gopenpgp.framework/Headers/Mime.objc.h +0 -59
- package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64/Gopenpgp.framework/Headers/Mobile.objc.h +0 -252
- package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64/Gopenpgp.framework/Headers/Profile.objc.h +0 -107
- package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64/Gopenpgp.framework/Headers/Universe.objc.h +0 -29
- package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64/Gopenpgp.framework/Headers/ref.h +0 -35
- package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64/Gopenpgp.framework/Info.plist +0 -20
- package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64/Gopenpgp.framework/Modules/module.modulemap +0 -13
- package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64_x86_64-simulator/Gopenpgp.framework/Gopenpgp +0 -0
- package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64_x86_64-simulator/Gopenpgp.framework/Headers/Armor.objc.h +0 -96
- package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64_x86_64-simulator/Gopenpgp.framework/Headers/Constants.objc.h +0 -197
- package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64_x86_64-simulator/Gopenpgp.framework/Headers/Crypto.objc.h +0 -1963
- package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64_x86_64-simulator/Gopenpgp.framework/Headers/Gopenpgp.h +0 -23
- package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64_x86_64-simulator/Gopenpgp.framework/Headers/Mime.objc.h +0 -59
- package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64_x86_64-simulator/Gopenpgp.framework/Headers/Mobile.objc.h +0 -252
- package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64_x86_64-simulator/Gopenpgp.framework/Headers/Profile.objc.h +0 -107
- package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64_x86_64-simulator/Gopenpgp.framework/Headers/Universe.objc.h +0 -29
- package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64_x86_64-simulator/Gopenpgp.framework/Headers/ref.h +0 -35
- package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64_x86_64-simulator/Gopenpgp.framework/Info.plist +0 -20
- 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
|
-
|
|
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'
|
package/android/build.gradle
CHANGED
|
@@ -136,11 +136,18 @@ dependencies {
|
|
|
136
136
|
|
|
137
137
|
implementation project(":onekeyfe_react-native-native-logger")
|
|
138
138
|
|
|
139
|
-
|
|
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
|
-
|
|
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"
|
package/android/src/main/java/com/margelo/nitro/reactnativebundleupdate/ReactNativeBundleUpdate.kt
CHANGED
|
@@ -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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
-
VoXq/QsXCOsGkMdFLwlUjarRaxFX6RTV6S51LOlJFRsyGwXiMz08GSNagSafQ0YL
|
|
45
|
-
Gi+aoemPh6Ta5jWgYGIUWXavkjJciJYw43ACMdVmIWos94bA41Xm93dq9C3VRpl+
|
|
46
|
-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
-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
-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
-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
-
zHFIK9HmDehx3LoDAYc66nhZXyasiu6qGPzuxXu8/4qTY8MnhXJRBkbWz5P84fx1
|
|
51
|
-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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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
-
ibcfWW9neHVpRtTlMYCoa2reGaBGCv0Nd8pMcyFUQkVaes5cQHkh3r5Dba+YrVvp
|
|
66
|
-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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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
-
F3DD6NK40bkfw8I6Qjwf/IXd921ZbKe88UMjVBTpm2IH3WXR51My9LN/2gzV9zL+
|
|
79
|
-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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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
-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
-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
-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
-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
-
Xit9ckUtqpC2KFG8EiIwpNtRvZXSl3etUvPYKTeAmo988QSYJZLQ3HqswTybSw6Q
|
|
98
|
-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
-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
-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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
|
|
432
|
-
if (
|
|
433
|
-
lastSHA256Failure.set(
|
|
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
|
|
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
|
-
/**
|
|
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
|
-
|
|
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
|
|
585
|
-
* Returns null if verification fails or the signature is not a PGP cleartext
|
|
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
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
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
|
-
|
|
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) {
|