@onekeyfe/react-native-bundle-update 1.1.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (130) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +36 -0
  3. package/ReactNativeBundleUpdate.podspec +34 -0
  4. package/android/CMakeLists.txt +24 -0
  5. package/android/build.gradle +139 -0
  6. package/android/gradle.properties +4 -0
  7. package/android/src/main/AndroidManifest.xml +1 -0
  8. package/android/src/main/cpp/cpp-adapter.cpp +6 -0
  9. package/android/src/main/java/com/margelo/nitro/reactnativebundleupdate/ReactNativeBundleUpdate.kt +1409 -0
  10. package/android/src/main/java/com/margelo/nitro/reactnativebundleupdate/ReactNativeBundleUpdatePackage.kt +24 -0
  11. package/ios/Frameworks/Gopenpgp.xcframework/Info.plist +52 -0
  12. package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64/Gopenpgp.framework/Gopenpgp +0 -0
  13. package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64/Gopenpgp.framework/Headers/Armor.objc.h +96 -0
  14. package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64/Gopenpgp.framework/Headers/Constants.objc.h +197 -0
  15. package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64/Gopenpgp.framework/Headers/Crypto.objc.h +1963 -0
  16. package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64/Gopenpgp.framework/Headers/Gopenpgp.h +23 -0
  17. package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64/Gopenpgp.framework/Headers/Mime.objc.h +59 -0
  18. package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64/Gopenpgp.framework/Headers/Mobile.objc.h +252 -0
  19. package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64/Gopenpgp.framework/Headers/Profile.objc.h +107 -0
  20. package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64/Gopenpgp.framework/Headers/Universe.objc.h +29 -0
  21. package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64/Gopenpgp.framework/Headers/ref.h +35 -0
  22. package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64/Gopenpgp.framework/Info.plist +20 -0
  23. package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64/Gopenpgp.framework/Modules/module.modulemap +13 -0
  24. package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64_x86_64-simulator/Gopenpgp.framework/Gopenpgp +0 -0
  25. package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64_x86_64-simulator/Gopenpgp.framework/Headers/Armor.objc.h +96 -0
  26. package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64_x86_64-simulator/Gopenpgp.framework/Headers/Constants.objc.h +197 -0
  27. package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64_x86_64-simulator/Gopenpgp.framework/Headers/Crypto.objc.h +1963 -0
  28. package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64_x86_64-simulator/Gopenpgp.framework/Headers/Gopenpgp.h +23 -0
  29. package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64_x86_64-simulator/Gopenpgp.framework/Headers/Mime.objc.h +59 -0
  30. package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64_x86_64-simulator/Gopenpgp.framework/Headers/Mobile.objc.h +252 -0
  31. package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64_x86_64-simulator/Gopenpgp.framework/Headers/Profile.objc.h +107 -0
  32. package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64_x86_64-simulator/Gopenpgp.framework/Headers/Universe.objc.h +29 -0
  33. package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64_x86_64-simulator/Gopenpgp.framework/Headers/ref.h +35 -0
  34. package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64_x86_64-simulator/Gopenpgp.framework/Info.plist +20 -0
  35. package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64_x86_64-simulator/Gopenpgp.framework/Modules/module.modulemap +13 -0
  36. package/ios/ReactNativeBundleUpdate.swift +1338 -0
  37. package/lib/module/ReactNativeBundleUpdate.nitro.js +4 -0
  38. package/lib/module/ReactNativeBundleUpdate.nitro.js.map +1 -0
  39. package/lib/module/index.js +6 -0
  40. package/lib/module/index.js.map +1 -0
  41. package/lib/module/package.json +1 -0
  42. package/lib/typescript/package.json +1 -0
  43. package/lib/typescript/src/ReactNativeBundleUpdate.nitro.d.ts +101 -0
  44. package/lib/typescript/src/ReactNativeBundleUpdate.nitro.d.ts.map +1 -0
  45. package/lib/typescript/src/index.d.ts +4 -0
  46. package/lib/typescript/src/index.d.ts.map +1 -0
  47. package/nitro.json +17 -0
  48. package/nitrogen/generated/android/c++/JAscFileInfo.hpp +65 -0
  49. package/nitrogen/generated/android/c++/JBundleDownloadASCParams.hpp +77 -0
  50. package/nitrogen/generated/android/c++/JBundleDownloadEvent.hpp +65 -0
  51. package/nitrogen/generated/android/c++/JBundleDownloadParams.hpp +73 -0
  52. package/nitrogen/generated/android/c++/JBundleDownloadResult.hpp +73 -0
  53. package/nitrogen/generated/android/c++/JBundleInstallParams.hpp +69 -0
  54. package/nitrogen/generated/android/c++/JBundleSwitchParams.hpp +65 -0
  55. package/nitrogen/generated/android/c++/JBundleVerifyASCParams.hpp +73 -0
  56. package/nitrogen/generated/android/c++/JBundleVerifyParams.hpp +69 -0
  57. package/nitrogen/generated/android/c++/JFallbackBundleInfo.hpp +65 -0
  58. package/nitrogen/generated/android/c++/JFunc_void_BundleDownloadEvent.hpp +78 -0
  59. package/nitrogen/generated/android/c++/JHybridReactNativeBundleUpdateSpec.cpp +486 -0
  60. package/nitrogen/generated/android/c++/JHybridReactNativeBundleUpdateSpec.hpp +89 -0
  61. package/nitrogen/generated/android/c++/JLocalBundleInfo.hpp +61 -0
  62. package/nitrogen/generated/android/c++/JTestResult.hpp +61 -0
  63. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativebundleupdate/AscFileInfo.kt +44 -0
  64. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativebundleupdate/BundleDownloadASCParams.kt +53 -0
  65. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativebundleupdate/BundleDownloadEvent.kt +44 -0
  66. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativebundleupdate/BundleDownloadParams.kt +50 -0
  67. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativebundleupdate/BundleDownloadResult.kt +50 -0
  68. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativebundleupdate/BundleInstallParams.kt +47 -0
  69. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativebundleupdate/BundleSwitchParams.kt +44 -0
  70. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativebundleupdate/BundleVerifyASCParams.kt +50 -0
  71. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativebundleupdate/BundleVerifyParams.kt +47 -0
  72. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativebundleupdate/FallbackBundleInfo.kt +44 -0
  73. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativebundleupdate/Func_void_BundleDownloadEvent.kt +80 -0
  74. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativebundleupdate/HybridReactNativeBundleUpdateSpec.kt +159 -0
  75. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativebundleupdate/LocalBundleInfo.kt +41 -0
  76. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativebundleupdate/TestResult.kt +41 -0
  77. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativebundleupdate/reactnativebundleupdateOnLoad.kt +35 -0
  78. package/nitrogen/generated/android/reactnativebundleupdate+autolinking.cmake +81 -0
  79. package/nitrogen/generated/android/reactnativebundleupdate+autolinking.gradle +27 -0
  80. package/nitrogen/generated/android/reactnativebundleupdateOnLoad.cpp +46 -0
  81. package/nitrogen/generated/android/reactnativebundleupdateOnLoad.hpp +25 -0
  82. package/nitrogen/generated/ios/ReactNativeBundleUpdate+autolinking.rb +60 -0
  83. package/nitrogen/generated/ios/ReactNativeBundleUpdate-Swift-Cxx-Bridge.cpp +113 -0
  84. package/nitrogen/generated/ios/ReactNativeBundleUpdate-Swift-Cxx-Bridge.hpp +513 -0
  85. package/nitrogen/generated/ios/ReactNativeBundleUpdate-Swift-Cxx-Umbrella.hpp +83 -0
  86. package/nitrogen/generated/ios/ReactNativeBundleUpdateAutolinking.mm +33 -0
  87. package/nitrogen/generated/ios/ReactNativeBundleUpdateAutolinking.swift +25 -0
  88. package/nitrogen/generated/ios/c++/HybridReactNativeBundleUpdateSpecSwift.cpp +11 -0
  89. package/nitrogen/generated/ios/c++/HybridReactNativeBundleUpdateSpecSwift.hpp +304 -0
  90. package/nitrogen/generated/ios/swift/AscFileInfo.swift +58 -0
  91. package/nitrogen/generated/ios/swift/BundleDownloadASCParams.swift +91 -0
  92. package/nitrogen/generated/ios/swift/BundleDownloadEvent.swift +58 -0
  93. package/nitrogen/generated/ios/swift/BundleDownloadParams.swift +80 -0
  94. package/nitrogen/generated/ios/swift/BundleDownloadResult.swift +80 -0
  95. package/nitrogen/generated/ios/swift/BundleInstallParams.swift +69 -0
  96. package/nitrogen/generated/ios/swift/BundleSwitchParams.swift +58 -0
  97. package/nitrogen/generated/ios/swift/BundleVerifyASCParams.swift +80 -0
  98. package/nitrogen/generated/ios/swift/BundleVerifyParams.swift +69 -0
  99. package/nitrogen/generated/ios/swift/FallbackBundleInfo.swift +58 -0
  100. package/nitrogen/generated/ios/swift/Func_void.swift +47 -0
  101. package/nitrogen/generated/ios/swift/Func_void_BundleDownloadEvent.swift +47 -0
  102. package/nitrogen/generated/ios/swift/Func_void_BundleDownloadResult.swift +47 -0
  103. package/nitrogen/generated/ios/swift/Func_void_TestResult.swift +47 -0
  104. package/nitrogen/generated/ios/swift/Func_void_bool.swift +47 -0
  105. package/nitrogen/generated/ios/swift/Func_void_std__exception_ptr.swift +47 -0
  106. package/nitrogen/generated/ios/swift/Func_void_std__string.swift +47 -0
  107. package/nitrogen/generated/ios/swift/Func_void_std__vector_AscFileInfo_.swift +47 -0
  108. package/nitrogen/generated/ios/swift/Func_void_std__vector_FallbackBundleInfo_.swift +47 -0
  109. package/nitrogen/generated/ios/swift/Func_void_std__vector_LocalBundleInfo_.swift +47 -0
  110. package/nitrogen/generated/ios/swift/HybridReactNativeBundleUpdateSpec.swift +80 -0
  111. package/nitrogen/generated/ios/swift/HybridReactNativeBundleUpdateSpec_cxx.swift +595 -0
  112. package/nitrogen/generated/ios/swift/LocalBundleInfo.swift +47 -0
  113. package/nitrogen/generated/ios/swift/TestResult.swift +47 -0
  114. package/nitrogen/generated/shared/c++/AscFileInfo.hpp +83 -0
  115. package/nitrogen/generated/shared/c++/BundleDownloadASCParams.hpp +95 -0
  116. package/nitrogen/generated/shared/c++/BundleDownloadEvent.hpp +83 -0
  117. package/nitrogen/generated/shared/c++/BundleDownloadParams.hpp +91 -0
  118. package/nitrogen/generated/shared/c++/BundleDownloadResult.hpp +91 -0
  119. package/nitrogen/generated/shared/c++/BundleInstallParams.hpp +87 -0
  120. package/nitrogen/generated/shared/c++/BundleSwitchParams.hpp +83 -0
  121. package/nitrogen/generated/shared/c++/BundleVerifyASCParams.hpp +91 -0
  122. package/nitrogen/generated/shared/c++/BundleVerifyParams.hpp +87 -0
  123. package/nitrogen/generated/shared/c++/FallbackBundleInfo.hpp +83 -0
  124. package/nitrogen/generated/shared/c++/HybridReactNativeBundleUpdateSpec.cpp +45 -0
  125. package/nitrogen/generated/shared/c++/HybridReactNativeBundleUpdateSpec.hpp +124 -0
  126. package/nitrogen/generated/shared/c++/LocalBundleInfo.hpp +79 -0
  127. package/nitrogen/generated/shared/c++/TestResult.hpp +79 -0
  128. package/package.json +169 -0
  129. package/src/ReactNativeBundleUpdate.nitro.ts +143 -0
  130. package/src/index.tsx +8 -0
@@ -0,0 +1,1409 @@
1
+ package com.margelo.nitro.reactnativebundleupdate
2
+
3
+ import android.content.Context
4
+ import android.content.SharedPreferences
5
+ import android.os.Build
6
+ import com.facebook.proguard.annotations.DoNotStrip
7
+ import com.margelo.nitro.core.Promise
8
+ import com.margelo.nitro.NitroModules
9
+ import com.margelo.nitro.nativelogger.OneKeyLog
10
+ import com.tencent.mmkv.MMKV
11
+ import okhttp3.OkHttpClient
12
+ import okhttp3.Request
13
+ import java.io.BufferedInputStream
14
+ import java.io.File
15
+ import java.io.FileInputStream
16
+ import java.io.FileOutputStream
17
+ import java.nio.file.Files
18
+ import java.nio.file.Path
19
+ import java.nio.file.Paths
20
+ import java.security.MessageDigest
21
+ import java.util.concurrent.CopyOnWriteArrayList
22
+ import java.util.concurrent.atomic.AtomicBoolean
23
+ import java.util.concurrent.atomic.AtomicLong
24
+ import java.util.zip.ZipEntry
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
+ import org.json.JSONArray
33
+ import org.json.JSONObject
34
+
35
+ private data class BundleListener(
36
+ val id: Double,
37
+ val callback: (BundleDownloadEvent) -> Unit
38
+ )
39
+
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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
90
+ P2Q5dClenjjjVA==
91
+ =F0np
92
+ -----END PGP PUBLIC KEY BLOCK-----"""
93
+
94
+ // Public static store for CustomReactNativeHost access (called before JS starts)
95
+ object BundleUpdateStoreAndroid {
96
+ private val bcProvider = BouncyCastleProvider()
97
+ private const val PREFS_NAME = "BundleUpdatePrefs"
98
+ private const val NATIVE_VERSION_PREFS_NAME = "NativeVersionPrefs"
99
+ private const val CURRENT_BUNDLE_VERSION_KEY = "currentBundleVersion"
100
+
101
+ fun getDownloadBundleDir(context: Context): String {
102
+ val dir = File(context.filesDir, "onekey-bundle-download")
103
+ if (!dir.exists()) dir.mkdirs()
104
+ return dir.absolutePath
105
+ }
106
+
107
+ fun getBundleDir(context: Context): String {
108
+ val dir = File(context.filesDir, "onekey-bundle")
109
+ if (!dir.exists()) dir.mkdirs()
110
+ return dir.absolutePath
111
+ }
112
+
113
+ fun getAscDir(context: Context): String {
114
+ val dir = File(getBundleDir(context), "asc")
115
+ if (!dir.exists()) dir.mkdirs()
116
+ return dir.absolutePath
117
+ }
118
+
119
+ fun getSignatureFilePath(context: Context, version: String): String {
120
+ return File(getAscDir(context), "$version-signature.asc").absolutePath
121
+ }
122
+
123
+ fun writeSignatureFile(context: Context, version: String, signature: String) {
124
+ val file = File(getSignatureFilePath(context, version))
125
+ val existed = file.exists()
126
+ file.parentFile?.mkdirs()
127
+ file.writeText(signature, Charsets.UTF_8)
128
+ OneKeyLog.info("BundleUpdate", "writeSignatureFile: version=$version, existed=$existed, size=${file.length()}, path=${file.absolutePath}")
129
+ }
130
+
131
+ fun readSignatureFile(context: Context, version: String): String {
132
+ val file = File(getSignatureFilePath(context, version))
133
+ if (!file.exists()) {
134
+ OneKeyLog.debug("BundleUpdate", "readSignatureFile: not found for version=$version")
135
+ return ""
136
+ }
137
+ return try {
138
+ val content = file.readText(Charsets.UTF_8)
139
+ OneKeyLog.debug("BundleUpdate", "readSignatureFile: version=$version, size=${content.length}")
140
+ content
141
+ } catch (e: Exception) {
142
+ OneKeyLog.error("BundleUpdate", "readSignatureFile: failed to read $version: ${e.message}")
143
+ ""
144
+ }
145
+ }
146
+
147
+ fun deleteSignatureFile(context: Context, version: String) {
148
+ val file = File(getSignatureFilePath(context, version))
149
+ if (file.exists()) file.delete()
150
+ }
151
+
152
+ fun getCurrentBundleVersion(context: Context): String? {
153
+ val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
154
+ return prefs.getString(CURRENT_BUNDLE_VERSION_KEY, null)
155
+ }
156
+
157
+ fun setCurrentBundleVersionAndSignature(context: Context, version: String, signature: String?) {
158
+ val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
159
+ val currentVersion = prefs.getString(CURRENT_BUNDLE_VERSION_KEY, "")
160
+ val editor = prefs.edit()
161
+ editor.putString(CURRENT_BUNDLE_VERSION_KEY, version)
162
+ // Remove old signature key from prefs (legacy cleanup)
163
+ if (!currentVersion.isNullOrEmpty()) {
164
+ editor.remove(currentVersion)
165
+ }
166
+ editor.apply()
167
+
168
+ // Store signature to file
169
+ if (!signature.isNullOrEmpty()) {
170
+ writeSignatureFile(context, version, signature)
171
+ }
172
+ }
173
+
174
+ fun clearUpdateBundleData(context: Context) {
175
+ val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
176
+ prefs.edit().clear().apply()
177
+ // Clear all signature files
178
+ val ascDir = File(getAscDir(context))
179
+ if (ascDir.exists()) {
180
+ ascDir.listFiles()?.forEach { it.delete() }
181
+ }
182
+ }
183
+
184
+ fun getCurrentBundleDir(context: Context, currentBundleVersion: String?): String? {
185
+ if (currentBundleVersion == null) return null
186
+ return File(getBundleDir(context), currentBundleVersion).absolutePath
187
+ }
188
+
189
+ fun getAppVersion(context: Context): String? {
190
+ return try {
191
+ val pm = context.packageManager
192
+ val pi = pm.getPackageInfo(context.packageName, 0)
193
+ pi.versionName
194
+ } catch (e: Exception) {
195
+ OneKeyLog.error("BundleUpdate", "Error getting package info: ${e.message}")
196
+ null
197
+ }
198
+ }
199
+
200
+ fun getNativeVersion(context: Context): String {
201
+ val prefs = context.getSharedPreferences(NATIVE_VERSION_PREFS_NAME, Context.MODE_PRIVATE)
202
+ return prefs.getString("nativeVersion", "") ?: ""
203
+ }
204
+
205
+ fun setNativeVersion(context: Context, nativeVersion: String) {
206
+ val prefs = context.getSharedPreferences(NATIVE_VERSION_PREFS_NAME, Context.MODE_PRIVATE)
207
+ prefs.edit().putString("nativeVersion", nativeVersion).apply()
208
+ }
209
+
210
+ fun calculateSHA256(filePath: String): String? {
211
+ return try {
212
+ val digest = MessageDigest.getInstance("SHA-256")
213
+ BufferedInputStream(FileInputStream(filePath)).use { bis ->
214
+ val buffer = ByteArray(8192)
215
+ var count: Int
216
+ while (bis.read(buffer).also { count = it } > 0) {
217
+ digest.update(buffer, 0, count)
218
+ }
219
+ }
220
+ bytesToHex(digest.digest())
221
+ } catch (e: Exception) {
222
+ OneKeyLog.error("BundleUpdate", "Error calculating SHA256: ${e.message}")
223
+ null
224
+ }
225
+ }
226
+
227
+ private fun bytesToHex(bytes: ByteArray): String {
228
+ return bytes.joinToString("") { "%02x".format(it) }
229
+ }
230
+
231
+ fun getMetadataFilePath(context: Context, currentBundleVersion: String?): String? {
232
+ if (currentBundleVersion == null) return null
233
+ val file = File(File(getBundleDir(context), currentBundleVersion), "metadata.json")
234
+ return if (file.exists()) file.absolutePath else null
235
+ }
236
+
237
+ fun getMetadataFileContent(context: Context, currentBundleVersion: String?): String? {
238
+ val path = getMetadataFilePath(context, currentBundleVersion) ?: return null
239
+ return readFileContent(File(path))
240
+ }
241
+
242
+ fun parseMetadataJson(jsonContent: String): Map<String, String> {
243
+ val metadata = mutableMapOf<String, String>()
244
+ try {
245
+ val obj = JSONObject(jsonContent)
246
+ val keys = obj.keys()
247
+ while (keys.hasNext()) {
248
+ val key = keys.next()
249
+ metadata[key] = obj.getString(key)
250
+ }
251
+ } catch (e: Exception) {
252
+ OneKeyLog.error("BundleUpdate", "Error parsing metadata JSON: ${e.message}")
253
+ throw Exception("Failed to parse metadata.json: ${e.message}")
254
+ }
255
+ if (metadata.isEmpty()) {
256
+ throw Exception("metadata.json is empty or contains no file entries")
257
+ }
258
+ return metadata
259
+ }
260
+
261
+ fun readMetadataFileSha256(signature: String?): String? {
262
+ if (signature.isNullOrEmpty()) return null
263
+
264
+ // GPG cleartext signature verification is required
265
+ val gpgResult = verifyGPGAndExtractSha256(signature)
266
+ if (gpgResult != null) return gpgResult
267
+
268
+ OneKeyLog.error("BundleUpdate", "readMetadataFileSha256: GPG verification failed, rejecting unsigned content")
269
+ return null
270
+ }
271
+
272
+ /** Constant-time comparison to prevent timing attacks on hash values */
273
+ fun secureCompare(a: String, b: String): Boolean {
274
+ val aBytes = a.toByteArray(Charsets.UTF_8)
275
+ val bBytes = b.toByteArray(Charsets.UTF_8)
276
+ if (aBytes.size != bBytes.size) return false
277
+ var result = 0
278
+ for (i in aBytes.indices) {
279
+ result = result or (aBytes[i].toInt() xor bBytes[i].toInt())
280
+ }
281
+ return result == 0
282
+ }
283
+
284
+ fun isSafeVersionString(version: String): Boolean {
285
+ return version.isNotEmpty() && version.all { it.isLetterOrDigit() || it == '.' || it == '-' || it == '_' }
286
+ }
287
+
288
+ /**
289
+ * Verify a PGP cleartext-signed message using BouncyCastle and extract the sha256.
290
+ * Returns null if verification fails or the signature is not a PGP cleartext message.
291
+ */
292
+ fun verifyGPGAndExtractSha256(signature: String): String? {
293
+ if (!signature.contains("-----BEGIN PGP SIGNED MESSAGE-----")) return null
294
+
295
+ return try {
296
+ // Parse the cleartext signed message
297
+ val inputStream = signature.byteInputStream()
298
+ val pgpFactory = JcaPGPObjectFactory(PGPUtil.getDecoderStream(inputStream))
299
+
300
+ // Parse the public key
301
+ val pubKeyStream = PGPUtil.getDecoderStream(GPG_PUBLIC_KEY.byteInputStream())
302
+ val pgpPubKeyRingCollection = PGPPublicKeyRingCollection(pubKeyStream, JcaKeyFingerprintCalculator())
303
+
304
+ // Extract cleartext and signature from the PGP signed message manually
305
+ // BouncyCastle's cleartext handling requires manual parsing
306
+ val lines = signature.lines()
307
+ val hashHeaderIdx = lines.indexOfFirst { it.startsWith("Hash:") }
308
+ val sigStartIdx = lines.indexOfFirst { it == "-----BEGIN PGP SIGNATURE-----" }
309
+ val sigEndIdx = lines.indexOfFirst { it == "-----END PGP SIGNATURE-----" }
310
+
311
+ if (hashHeaderIdx < 0 || sigStartIdx < 0 || sigEndIdx < 0) {
312
+ OneKeyLog.error("BundleUpdate", "Invalid PGP cleartext signed message format")
313
+ return null
314
+ }
315
+
316
+ // The cleartext body is between the Hash header blank line and the PGP SIGNATURE block
317
+ val bodyStartIdx = hashHeaderIdx + 2 // skip Hash: line and the blank line after it
318
+ val bodyLines = lines.subList(bodyStartIdx, sigStartIdx)
319
+ // Remove trailing empty line that PGP adds
320
+ val cleartextBody = bodyLines.joinToString("\r\n").trimEnd()
321
+
322
+ // The signature block
323
+ val sigBlock = lines.subList(sigStartIdx, sigEndIdx + 1).joinToString("\n")
324
+
325
+ // Decode the signature
326
+ val sigInputStream = PGPUtil.getDecoderStream(sigBlock.byteInputStream())
327
+ val sigFactory = JcaPGPObjectFactory(sigInputStream)
328
+ val signatureList = sigFactory.nextObject()
329
+
330
+ if (signatureList !is org.bouncycastle.openpgp.PGPSignatureList || signatureList.isEmpty) {
331
+ OneKeyLog.error("BundleUpdate", "No PGP signature found in message")
332
+ return null
333
+ }
334
+
335
+ val pgpSignature = signatureList[0]
336
+ val keyId = pgpSignature.keyID
337
+
338
+ // Find the public key
339
+ val publicKey = pgpPubKeyRingCollection.getPublicKey(keyId)
340
+ if (publicKey == null) {
341
+ OneKeyLog.error("BundleUpdate", "Public key not found for keyId: ${java.lang.Long.toHexString(keyId)}")
342
+ return null
343
+ }
344
+
345
+ // Verify the signature
346
+ pgpSignature.init(JcaPGPContentVerifierBuilderProvider().setProvider(bcProvider), publicKey)
347
+
348
+ // Dash-unescape the cleartext per RFC 4880 Section 7.1
349
+ val unescapedLines = cleartextBody.lines().map { line ->
350
+ if (line.startsWith("- ")) line.substring(2) else line
351
+ }
352
+ val dataToVerify = unescapedLines.joinToString("\r\n").toByteArray(Charsets.UTF_8)
353
+ pgpSignature.update(dataToVerify)
354
+
355
+ if (!pgpSignature.verify()) {
356
+ OneKeyLog.error("BundleUpdate", "GPG signature verification failed")
357
+ return null
358
+ }
359
+
360
+ // Signature verified, parse the cleartext JSON
361
+ val json = JSONObject(cleartextBody.trim())
362
+ val sha256 = json.optString("sha256", "")
363
+ if (sha256.isEmpty()) {
364
+ OneKeyLog.error("BundleUpdate", "No sha256 field in signed cleartext JSON")
365
+ return null
366
+ }
367
+
368
+ OneKeyLog.info("BundleUpdate", "GPG verification succeeded, sha256: $sha256")
369
+ sha256
370
+ } catch (e: Exception) {
371
+ OneKeyLog.error("BundleUpdate", "GPG verification error: ${e.message}")
372
+ null
373
+ }
374
+ }
375
+
376
+ fun validateMetadataFileSha256(context: Context, currentBundleVersion: String, signature: String?): Boolean {
377
+ val metadataFilePath = getMetadataFilePath(context, currentBundleVersion) ?: run {
378
+ OneKeyLog.debug("BundleUpdate", "metadataFilePath is null")
379
+ return false
380
+ }
381
+ val extractedSha256 = readMetadataFileSha256(signature)
382
+ if (extractedSha256.isNullOrEmpty()) return false
383
+ val calculated = calculateSHA256(metadataFilePath) ?: return false
384
+ return secureCompare(calculated, extractedSha256)
385
+ }
386
+
387
+ fun validateAllFilesInDir(context: Context, dirPath: String, metadata: Map<String, String>, appVersion: String, bundleVersion: String): Boolean {
388
+ val dir = File(dirPath)
389
+ if (!dir.exists() || !dir.isDirectory) return false
390
+
391
+ val parentBundleDir = getBundleDir(context)
392
+ val folderName = "$appVersion-$bundleVersion"
393
+ val jsBundleDir = File(parentBundleDir, folderName).absolutePath + "/"
394
+
395
+ if (!validateFilesRecursive(dir, metadata, jsBundleDir)) return false
396
+
397
+ // Verify completeness
398
+ for (entry in metadata.entries) {
399
+ val expectedFile = File(jsBundleDir + entry.key)
400
+ if (!expectedFile.exists()) {
401
+ OneKeyLog.error("BundleUpdate", "[bundle-verify] File listed in metadata but missing on disk: ${entry.key}")
402
+ return false
403
+ }
404
+ }
405
+ return true
406
+ }
407
+
408
+ private fun validateFilesRecursive(dir: File, metadata: Map<String, String>, jsBundleDir: String): Boolean {
409
+ val files = dir.listFiles() ?: return true
410
+ for (file in files) {
411
+ if (file.isDirectory) {
412
+ if (!validateFilesRecursive(file, metadata, jsBundleDir)) return false
413
+ } else {
414
+ if (file.name.contains("metadata.json") || file.name.contains(".DS_Store")) continue
415
+ val relativePath = file.absolutePath.replace(jsBundleDir, "")
416
+ val expectedSHA256 = metadata[relativePath]
417
+ if (expectedSHA256 == null) {
418
+ OneKeyLog.error("BundleUpdate", "[bundle-verify] File on disk not found in metadata: $relativePath")
419
+ return false
420
+ }
421
+ val actualSHA256 = calculateSHA256(file.absolutePath)
422
+ if (actualSHA256 == null) {
423
+ OneKeyLog.error("BundleUpdate", "[bundle-verify] Failed to calculate SHA256 for file: $relativePath")
424
+ return false
425
+ }
426
+ if (!secureCompare(expectedSHA256, actualSHA256)) {
427
+ OneKeyLog.error("BundleUpdate", "[bundle-verify] SHA256 mismatch for $relativePath")
428
+ return false
429
+ }
430
+ }
431
+ }
432
+ return true
433
+ }
434
+
435
+ fun readFileContent(file: File): String {
436
+ return file.readText(Charsets.UTF_8)
437
+ }
438
+
439
+ fun getFallbackUpdateBundleDataFile(context: Context): File {
440
+ val path = File(getBundleDir(context), "fallbackUpdateBundleData.json")
441
+ if (!path.exists()) {
442
+ try { path.createNewFile() } catch (e: Exception) {
443
+ OneKeyLog.error("BundleUpdate", "getFallbackUpdateBundleDataFile: ${e.message}")
444
+ }
445
+ }
446
+ return path
447
+ }
448
+
449
+ fun readFallbackUpdateBundleDataFile(context: Context): MutableList<MutableMap<String, String>> {
450
+ val file = getFallbackUpdateBundleDataFile(context)
451
+ val content = try { readFileContent(file) } catch (e: Exception) { "" }
452
+ if (content.isEmpty()) return mutableListOf()
453
+ return try {
454
+ val arr = JSONArray(content)
455
+ val result = mutableListOf<MutableMap<String, String>>()
456
+ for (i in 0 until arr.length()) {
457
+ val obj = arr.getJSONObject(i)
458
+ val map = mutableMapOf<String, String>()
459
+ val keys = obj.keys()
460
+ while (keys.hasNext()) {
461
+ val key = keys.next()
462
+ map[key] = obj.getString(key)
463
+ }
464
+ result.add(map)
465
+ }
466
+ result
467
+ } catch (e: Exception) {
468
+ OneKeyLog.error("BundleUpdate", "readFallbackUpdateBundleDataFile: ${e.message}")
469
+ mutableListOf()
470
+ }
471
+ }
472
+
473
+ fun writeFallbackUpdateBundleDataFile(data: List<Map<String, String>>, context: Context) {
474
+ val file = getFallbackUpdateBundleDataFile(context)
475
+ val arr = JSONArray()
476
+ for (map in data) {
477
+ val obj = JSONObject()
478
+ for ((k, v) in map) obj.put(k, v)
479
+ arr.put(obj)
480
+ }
481
+ try {
482
+ // Atomic write: write to temp file then rename to avoid partial writes
483
+ val tmpFile = File(file.parent, "${file.name}.tmp")
484
+ FileOutputStream(tmpFile).use { fos ->
485
+ fos.write(arr.toString().toByteArray(Charsets.UTF_8))
486
+ fos.fd.sync()
487
+ }
488
+ if (!tmpFile.renameTo(file)) {
489
+ tmpFile.delete()
490
+ throw Exception("Failed to rename temp file to ${file.name}")
491
+ }
492
+ } catch (e: Exception) {
493
+ OneKeyLog.error("BundleUpdate", "writeFallbackUpdateBundleDataFile: ${e.message}")
494
+ }
495
+ }
496
+
497
+ fun getCurrentBundleMainJSBundle(context: Context): String? {
498
+ return try {
499
+ val currentAppVersion = getAppVersion(context)
500
+ val currentBundleVersion = getCurrentBundleVersion(context) ?: run {
501
+ OneKeyLog.warn("BundleUpdate", "getJsBundlePath: no currentBundleVersion stored")
502
+ return null
503
+ }
504
+
505
+ OneKeyLog.info("BundleUpdate", "currentAppVersion: $currentAppVersion, currentBundleVersion: $currentBundleVersion")
506
+
507
+ val prevNativeVersion = getNativeVersion(context)
508
+ if (prevNativeVersion.isEmpty()) {
509
+ OneKeyLog.warn("BundleUpdate", "getJsBundlePath: prevNativeVersion is empty")
510
+ return ""
511
+ }
512
+
513
+ if (currentAppVersion != prevNativeVersion) {
514
+ OneKeyLog.info("BundleUpdate", "currentAppVersion is not equal to prevNativeVersion $currentAppVersion $prevNativeVersion")
515
+ clearUpdateBundleData(context)
516
+ return null
517
+ }
518
+
519
+ val bundleDir = getCurrentBundleDir(context, currentBundleVersion) ?: run {
520
+ OneKeyLog.warn("BundleUpdate", "getJsBundlePath: getCurrentBundleDir returned null")
521
+ return null
522
+ }
523
+ if (!File(bundleDir).exists()) {
524
+ OneKeyLog.info("BundleUpdate", "currentBundleDir does not exist: $bundleDir")
525
+ return null
526
+ }
527
+
528
+ val signature = readSignatureFile(context, currentBundleVersion)
529
+ OneKeyLog.debug("BundleUpdate", "getJsBundlePath: signatureLength=${signature.length}")
530
+
531
+ val devSettingsEnabled = isDevSettingsEnabled(context)
532
+ if (devSettingsEnabled) {
533
+ OneKeyLog.warn("BundleUpdate", "Startup SHA256 validation skipped (DevSettings enabled)")
534
+ }
535
+ if (!devSettingsEnabled && !validateMetadataFileSha256(context, currentBundleVersion, signature)) {
536
+ OneKeyLog.warn("BundleUpdate", "getJsBundlePath: validateMetadataFileSha256 failed, signatureLength=${signature.length}")
537
+ return null
538
+ }
539
+
540
+ val metadataContent = getMetadataFileContent(context, currentBundleVersion) ?: run {
541
+ OneKeyLog.warn("BundleUpdate", "getJsBundlePath: getMetadataFileContent returned null")
542
+ return null
543
+ }
544
+ val metadata = parseMetadataJson(metadataContent)
545
+
546
+ val lastDashIndex = currentBundleVersion.lastIndexOf("-")
547
+ if (lastDashIndex > 0) {
548
+ val appVer = currentBundleVersion.substring(0, lastDashIndex)
549
+ val bundleVer = currentBundleVersion.substring(lastDashIndex + 1)
550
+ if (!validateAllFilesInDir(context, bundleDir, metadata, appVer, bundleVer)) {
551
+ OneKeyLog.info("BundleUpdate", "validateAllFilesInDir failed on startup")
552
+ return null
553
+ }
554
+ }
555
+
556
+ val mainJSBundleFile = File(bundleDir, "main.jsbundle.hbc")
557
+ val mainJSBundlePath = mainJSBundleFile.absolutePath
558
+ OneKeyLog.info("BundleUpdate", "mainJSBundlePath: $mainJSBundlePath")
559
+ if (!mainJSBundleFile.exists()) {
560
+ OneKeyLog.info("BundleUpdate", "mainJSBundleFile does not exist")
561
+ return null
562
+ }
563
+ mainJSBundlePath
564
+ } catch (e: Exception) {
565
+ OneKeyLog.error("BundleUpdate", "Error getting bundle: ${e.message}")
566
+ null
567
+ }
568
+ }
569
+
570
+ fun getWebEmbedPath(context: Context): String {
571
+ val currentBundleDir = getCurrentBundleDir(context, getCurrentBundleVersion(context)) ?: return ""
572
+ return File(currentBundleDir, "web-embed").absolutePath
573
+ }
574
+
575
+ /**
576
+ * Returns true if the OneKey developer mode (DevSettings) is enabled.
577
+ * Reads the persisted value from MMKV storage (key: onekey_developer_mode_enabled,
578
+ * instance: onekey-app-setting) written by the JS ServiceDevSetting layer.
579
+ */
580
+ fun isDevSettingsEnabled(context: Context): Boolean {
581
+ return try {
582
+ MMKV.initialize(context)
583
+ val mmkv = MMKV.mmkvWithID("onekey-app-setting") ?: return false
584
+ mmkv.decodeBool("onekey_developer_mode_enabled", false)
585
+ } catch (e: Exception) {
586
+ false
587
+ }
588
+ }
589
+
590
+ /**
591
+ * Returns true if the skip-GPG-verification toggle is enabled in developer settings.
592
+ * Reads the persisted value from MMKV storage (key: onekey_bundle_skip_gpg_verification,
593
+ * instance: onekey-app-setting).
594
+ */
595
+ fun isSkipGPGEnabled(context: Context): Boolean {
596
+ return try {
597
+ MMKV.initialize(context)
598
+ val mmkv = MMKV.mmkvWithID("onekey-app-setting") ?: return false
599
+ mmkv.decodeBool("onekey_bundle_skip_gpg_verification", false)
600
+ } catch (e: Exception) {
601
+ false
602
+ }
603
+ }
604
+
605
+ private fun deleteDirectory(directory: File) {
606
+ if (directory.exists()) {
607
+ directory.listFiles()?.forEach { file ->
608
+ if (file.isDirectory) deleteDirectory(file) else file.delete()
609
+ }
610
+ directory.delete()
611
+ }
612
+ }
613
+
614
+ fun deleteDir(dir: File) = deleteDirectory(dir)
615
+
616
+ private const val MAX_UNZIPPED_SIZE = 512L * 1024 * 1024 // 512 MB limit
617
+
618
+ fun unzipFile(zipFilePath: String, destDirectory: String) {
619
+ val destDir = File(destDirectory)
620
+ if (!destDir.exists()) destDir.mkdirs()
621
+
622
+ val destDirPath: Path = Paths.get(destDir.canonicalPath)
623
+ var totalBytesWritten = 0L
624
+ ZipInputStream(FileInputStream(zipFilePath)).use { zipIn ->
625
+ var entry: ZipEntry? = zipIn.nextEntry
626
+ while (entry != null) {
627
+ val outFile = File(destDir, entry.name)
628
+ val outPath: Path = Paths.get(outFile.canonicalPath)
629
+ if (!outPath.startsWith(destDirPath)) {
630
+ throw java.io.IOException("Entry is outside of the target dir: ${entry.name}")
631
+ }
632
+ if (!entry.isDirectory) {
633
+ outFile.parentFile?.mkdirs()
634
+ FileOutputStream(outFile).use { fos ->
635
+ val buffer = ByteArray(8192)
636
+ var length: Int
637
+ while (zipIn.read(buffer).also { length = it } > 0) {
638
+ totalBytesWritten += length
639
+ if (totalBytesWritten > MAX_UNZIPPED_SIZE) {
640
+ throw java.io.IOException("Decompression bomb detected: extracted size exceeds ${MAX_UNZIPPED_SIZE / 1024 / 1024} MB")
641
+ }
642
+ fos.write(buffer, 0, length)
643
+ }
644
+ }
645
+ // Validate no symlink was created (zip slip via symlink attack)
646
+ if (Files.isSymbolicLink(outFile.toPath())) {
647
+ outFile.delete()
648
+ throw java.io.IOException("Symlink detected in zip entry: ${entry.name}")
649
+ }
650
+ } else {
651
+ outFile.mkdirs()
652
+ }
653
+ zipIn.closeEntry()
654
+ entry = zipIn.nextEntry
655
+ }
656
+ }
657
+ }
658
+ }
659
+
660
+ @DoNotStrip
661
+ class ReactNativeBundleUpdate : HybridReactNativeBundleUpdateSpec() {
662
+
663
+ companion object {
664
+ private const val PREFS_NAME = "BundleUpdatePrefs"
665
+ }
666
+
667
+ private val listeners = CopyOnWriteArrayList<BundleListener>()
668
+ private val nextListenerId = AtomicLong(1)
669
+ private val isDownloading = AtomicBoolean(false)
670
+ private val httpClient = OkHttpClient.Builder()
671
+ .addNetworkInterceptor { chain ->
672
+ val req = chain.request()
673
+ if (!req.url.isHttps) {
674
+ throw java.io.IOException("Redirect to non-HTTPS URL is not allowed: ${req.url}")
675
+ }
676
+ chain.proceed(req)
677
+ }
678
+ .build()
679
+
680
+ private fun sendEvent(type: String, progress: Int = 0, message: String = "") {
681
+ val event = BundleDownloadEvent(type = type, progress = progress.toDouble(), message = message)
682
+ for (listener in listeners) {
683
+ try {
684
+ listener.callback(event)
685
+ } catch (e: Exception) {
686
+ OneKeyLog.error("BundleUpdate", "Error sending event: ${e.message}")
687
+ }
688
+ }
689
+ }
690
+
691
+ override fun addDownloadListener(callback: (BundleDownloadEvent) -> Unit): Double {
692
+ val id = nextListenerId.getAndIncrement().toDouble()
693
+ listeners.add(BundleListener(id, callback))
694
+ OneKeyLog.debug("BundleUpdate", "addDownloadListener: id=$id, totalListeners=${listeners.size}")
695
+ return id
696
+ }
697
+
698
+ override fun removeDownloadListener(id: Double) {
699
+ listeners.removeAll { it.id == id }
700
+ OneKeyLog.debug("BundleUpdate", "removeDownloadListener: id=$id, totalListeners=${listeners.size}")
701
+ }
702
+
703
+ private fun getContext(): Context {
704
+ return NitroModules.applicationContext
705
+ ?: throw Exception("Application context unavailable")
706
+ }
707
+
708
+ private fun isDebuggable(): Boolean {
709
+ val context = NitroModules.applicationContext ?: return false
710
+ return (context.applicationInfo.flags and android.content.pm.ApplicationInfo.FLAG_DEBUGGABLE) != 0
711
+ }
712
+
713
+ /** Returns true if OneKey developer mode (DevSettings) is enabled via MMKV storage. */
714
+ private fun isDevSettingsEnabled(): Boolean {
715
+ return try {
716
+ val context = NitroModules.applicationContext ?: return false
717
+ BundleUpdateStoreAndroid.isDevSettingsEnabled(context)
718
+ } catch (e: Exception) {
719
+ false
720
+ }
721
+ }
722
+
723
+ /** Returns true if the skip-GPG-verification toggle is enabled via MMKV storage. */
724
+ private fun isSkipGPGEnabled(): Boolean {
725
+ return try {
726
+ val context = NitroModules.applicationContext ?: return false
727
+ BundleUpdateStoreAndroid.isSkipGPGEnabled(context)
728
+ } catch (e: Exception) {
729
+ false
730
+ }
731
+ }
732
+
733
+ override fun downloadBundle(params: BundleDownloadParams): Promise<BundleDownloadResult> {
734
+ return Promise.async {
735
+ if (isDownloading.getAndSet(true)) {
736
+ OneKeyLog.warn("BundleUpdate", "downloadBundle: rejected, already downloading")
737
+ throw Exception("Already downloading")
738
+ }
739
+
740
+ try {
741
+ val context = getContext()
742
+ val appVersion = params.latestVersion
743
+ val bundleVersion = params.bundleVersion
744
+ val downloadUrl = params.downloadUrl
745
+ val sha256 = params.sha256
746
+
747
+ OneKeyLog.info("BundleUpdate", "downloadBundle: appVersion=$appVersion, bundleVersion=$bundleVersion, fileSize=${params.fileSize}, url=$downloadUrl")
748
+
749
+ if (!BundleUpdateStoreAndroid.isSafeVersionString(appVersion) ||
750
+ !BundleUpdateStoreAndroid.isSafeVersionString(bundleVersion)) {
751
+ OneKeyLog.error("BundleUpdate", "downloadBundle: invalid version string format: appVersion=$appVersion, bundleVersion=$bundleVersion")
752
+ throw Exception("Invalid version string format")
753
+ }
754
+
755
+ if (!downloadUrl.startsWith("https://")) {
756
+ OneKeyLog.error("BundleUpdate", "downloadBundle: URL is not HTTPS: $downloadUrl")
757
+ throw Exception("Bundle download URL must use HTTPS")
758
+ }
759
+
760
+ val fileName = "$appVersion-$bundleVersion.zip"
761
+ val filePath = File(BundleUpdateStoreAndroid.getDownloadBundleDir(context), fileName).absolutePath
762
+
763
+ val result = BundleDownloadResult(
764
+ downloadedFile = filePath,
765
+ downloadUrl = downloadUrl,
766
+ latestVersion = appVersion,
767
+ bundleVersion = bundleVersion,
768
+ sha256 = sha256
769
+ )
770
+
771
+ OneKeyLog.info("BundleUpdate", "downloadBundle: filePath=$filePath")
772
+
773
+ val downloadedFile = File(filePath)
774
+ if (downloadedFile.exists()) {
775
+ OneKeyLog.info("BundleUpdate", "downloadBundle: file already exists, verifying SHA256...")
776
+ if (verifyBundleSHA256(filePath, sha256)) {
777
+ OneKeyLog.info("BundleUpdate", "downloadBundle: existing file SHA256 valid, skipping download")
778
+ isDownloading.set(false)
779
+ Thread.sleep(1000)
780
+ sendEvent("update/complete")
781
+ return@async result
782
+ } else {
783
+ OneKeyLog.warn("BundleUpdate", "downloadBundle: existing file SHA256 mismatch, re-downloading")
784
+ downloadedFile.delete()
785
+ }
786
+ }
787
+
788
+ sendEvent("update/start")
789
+ OneKeyLog.info("BundleUpdate", "downloadBundle: starting download...")
790
+
791
+ val request = Request.Builder().url(downloadUrl).build()
792
+ val response = httpClient.newCall(request).execute()
793
+
794
+ if (!response.isSuccessful) {
795
+ OneKeyLog.error("BundleUpdate", "downloadBundle: HTTP error, statusCode=${response.code}")
796
+ sendEvent("update/error", message = "HTTP ${response.code}")
797
+ throw Exception("HTTP ${response.code}")
798
+ }
799
+
800
+ val body = response.body ?: throw Exception("Empty response body")
801
+ val fileSize = if (params.fileSize > 0) params.fileSize.toLong() else body.contentLength()
802
+ OneKeyLog.info("BundleUpdate", "downloadBundle: HTTP 200, contentLength=$fileSize, downloading...")
803
+
804
+ // Ensure parent directory exists before writing
805
+ val parentDir = File(filePath).parentFile
806
+ if (parentDir != null && !parentDir.exists()) {
807
+ parentDir.mkdirs()
808
+ OneKeyLog.info("BundleUpdate", "downloadBundle: created parent directory: ${parentDir.absolutePath}")
809
+ }
810
+
811
+ var totalBytesRead = 0L
812
+ body.byteStream().use { inputStream ->
813
+ FileOutputStream(filePath).use { outputStream ->
814
+ val buffer = ByteArray(8192)
815
+ var bytesRead: Int
816
+
817
+ var prevProgress = 0
818
+ while (inputStream.read(buffer).also { bytesRead = it } != -1) {
819
+ outputStream.write(buffer, 0, bytesRead)
820
+ totalBytesRead += bytesRead
821
+ if (fileSize > 0) {
822
+ val progress = ((totalBytesRead * 100) / fileSize).toInt()
823
+ if (progress != prevProgress) {
824
+ sendEvent("update/downloading", progress = progress)
825
+ OneKeyLog.info("BundleUpdate", "download progress: $progress% ($totalBytesRead/$fileSize)")
826
+ prevProgress = progress
827
+ }
828
+ }
829
+ }
830
+ }
831
+ }
832
+
833
+ val downloadedFileAfter = File(filePath)
834
+ OneKeyLog.info("BundleUpdate", "downloadBundle: download finished, totalBytesRead=$totalBytesRead, fileExists=${downloadedFileAfter.exists()}, fileSize=${if (downloadedFileAfter.exists()) downloadedFileAfter.length() else -1}, verifying SHA256...")
835
+ if (!verifyBundleSHA256(filePath, sha256)) {
836
+ File(filePath).delete()
837
+ OneKeyLog.error("BundleUpdate", "downloadBundle: SHA256 verification failed after download")
838
+ sendEvent("update/error", message = "Bundle signature verification failed")
839
+ throw Exception("Bundle signature verification failed")
840
+ }
841
+
842
+ sendEvent("update/complete")
843
+ OneKeyLog.info("BundleUpdate", "downloadBundle: completed successfully, appVersion=$appVersion, bundleVersion=$bundleVersion")
844
+ result
845
+ } catch (e: Exception) {
846
+ OneKeyLog.error("BundleUpdate", "downloadBundle: failed: ${e.javaClass.simpleName}: ${e.message}")
847
+ sendEvent("update/error", message = "${e.javaClass.simpleName}: ${e.message}")
848
+ throw e
849
+ } finally {
850
+ isDownloading.set(false)
851
+ }
852
+ }
853
+ }
854
+
855
+ private fun verifyBundleSHA256(bundlePath: String, sha256: String): Boolean {
856
+ val calculated = BundleUpdateStoreAndroid.calculateSHA256(bundlePath)
857
+ if (calculated == null) {
858
+ OneKeyLog.error("BundleUpdate", "verifyBundleSHA256: failed to calculate SHA256 for: $bundlePath")
859
+ return false
860
+ }
861
+ val isValid = BundleUpdateStoreAndroid.secureCompare(calculated, sha256)
862
+ OneKeyLog.debug("BundleUpdate", "verifyBundleSHA256: path=$bundlePath, expected=${sha256.take(16)}..., calculated=${calculated.take(16)}..., valid=$isValid")
863
+ return isValid
864
+ }
865
+
866
+ override fun downloadBundleASC(params: BundleDownloadASCParams): Promise<Unit> {
867
+ return Promise.async {
868
+ val context = getContext()
869
+ val appVersion = params.latestVersion
870
+ val bundleVersion = params.bundleVersion
871
+ val signature = params.signature
872
+
873
+ OneKeyLog.info("BundleUpdate", "downloadBundleASC: appVersion=$appVersion, bundleVersion=$bundleVersion, signatureLength=${signature.length}")
874
+
875
+ val storageKey = "$appVersion-$bundleVersion"
876
+ BundleUpdateStoreAndroid.writeSignatureFile(context, storageKey, signature)
877
+
878
+ OneKeyLog.info("BundleUpdate", "downloadBundleASC: stored signature for key=$storageKey")
879
+ }
880
+ }
881
+
882
+ override fun verifyBundleASC(params: BundleVerifyASCParams): Promise<Unit> {
883
+ return Promise.async {
884
+ val context = getContext()
885
+ val filePath = params.downloadedFile
886
+ val sha256 = params.sha256
887
+ val appVersion = params.latestVersion
888
+ val bundleVersion = params.bundleVersion
889
+ val signature = params.signature
890
+
891
+ OneKeyLog.info("BundleUpdate", "verifyBundleASC: appVersion=$appVersion, bundleVersion=$bundleVersion, file=$filePath, signatureLength=${signature.length}")
892
+
893
+ // GPG verification skipped only when both DevSettings and skip-GPG toggle are enabled
894
+ val devSettings = isDevSettingsEnabled()
895
+ val skipGPGToggle = isSkipGPGEnabled()
896
+ val skipGPG = devSettings && skipGPGToggle
897
+ OneKeyLog.info("BundleUpdate", "verifyBundleASC: GPG check: devSettings=$devSettings, skipGPGToggle=$skipGPGToggle, skipGPG=$skipGPG")
898
+
899
+ if (!skipGPG) {
900
+ OneKeyLog.info("BundleUpdate", "verifyBundleASC: verifying SHA256 of downloaded file...")
901
+ if (!verifyBundleSHA256(filePath, sha256)) {
902
+ OneKeyLog.error("BundleUpdate", "verifyBundleASC: SHA256 verification failed for file=$filePath")
903
+ throw Exception("Bundle signature verification failed")
904
+ }
905
+ OneKeyLog.info("BundleUpdate", "verifyBundleASC: SHA256 verified OK")
906
+ } else {
907
+ OneKeyLog.warn("BundleUpdate", "verifyBundleASC: SHA256 + GPG verification skipped (DevSettings enabled)")
908
+ }
909
+
910
+ val folderName = "$appVersion-$bundleVersion"
911
+ val destination = File(BundleUpdateStoreAndroid.getBundleDir(context), folderName).absolutePath
912
+ val destinationDir = File(destination)
913
+
914
+ try {
915
+ OneKeyLog.info("BundleUpdate", "verifyBundleASC: extracting zip to $destination...")
916
+ BundleUpdateStoreAndroid.unzipFile(filePath, destination)
917
+ OneKeyLog.info("BundleUpdate", "verifyBundleASC: extraction completed")
918
+
919
+ val metadataFile = File(destination, "metadata.json")
920
+ if (!metadataFile.exists()) {
921
+ OneKeyLog.error("BundleUpdate", "verifyBundleASC: metadata.json not found after extraction")
922
+ BundleUpdateStoreAndroid.deleteDir(destinationDir)
923
+ throw Exception("Failed to read metadata.json")
924
+ }
925
+
926
+ val currentBundleVersion = "$appVersion-$bundleVersion"
927
+ if (!skipGPG) {
928
+ OneKeyLog.info("BundleUpdate", "verifyBundleASC: validating GPG signature for metadata...")
929
+ if (!BundleUpdateStoreAndroid.validateMetadataFileSha256(context, currentBundleVersion, signature)) {
930
+ OneKeyLog.error("BundleUpdate", "verifyBundleASC: GPG signature verification failed")
931
+ BundleUpdateStoreAndroid.deleteDir(destinationDir)
932
+ throw Exception("Bundle signature verification failed")
933
+ }
934
+ OneKeyLog.info("BundleUpdate", "verifyBundleASC: GPG signature verified OK")
935
+ }
936
+
937
+ OneKeyLog.info("BundleUpdate", "verifyBundleASC: validating all extracted files against metadata...")
938
+ val metadataContent = BundleUpdateStoreAndroid.readFileContent(metadataFile)
939
+ val metadata = BundleUpdateStoreAndroid.parseMetadataJson(metadataContent)
940
+ if (!BundleUpdateStoreAndroid.validateAllFilesInDir(context, destination, metadata, appVersion, bundleVersion)) {
941
+ OneKeyLog.error("BundleUpdate", "verifyBundleASC: file integrity check failed")
942
+ BundleUpdateStoreAndroid.deleteDir(destinationDir)
943
+ throw Exception("Extracted files verification against metadata failed")
944
+ }
945
+
946
+ OneKeyLog.info("BundleUpdate", "verifyBundleASC: all verifications passed, appVersion=$appVersion, bundleVersion=$bundleVersion")
947
+ } catch (e: Exception) {
948
+ if (destinationDir.exists()) {
949
+ BundleUpdateStoreAndroid.deleteDir(destinationDir)
950
+ }
951
+ throw e
952
+ }
953
+ }
954
+ }
955
+
956
+ override fun verifyBundle(params: BundleVerifyParams): Promise<Unit> {
957
+ return Promise.async {
958
+ val filePath = params.downloadedFile
959
+ val sha256 = params.sha256
960
+ val appVersion = params.latestVersion
961
+ val bundleVersion = params.bundleVersion
962
+
963
+ OneKeyLog.info("BundleUpdate", "verifyBundle: appVersion=$appVersion, bundleVersion=$bundleVersion, file=$filePath")
964
+
965
+ // Verify SHA256 of the downloaded file
966
+ val calculated = BundleUpdateStoreAndroid.calculateSHA256(filePath)
967
+ if (calculated == null) {
968
+ OneKeyLog.error("BundleUpdate", "verifyBundle: failed to calculate SHA256 for file=$filePath")
969
+ throw Exception("Failed to calculate SHA256")
970
+ }
971
+ if (!BundleUpdateStoreAndroid.secureCompare(calculated, sha256)) {
972
+ OneKeyLog.error("BundleUpdate", "verifyBundle: SHA256 mismatch, expected=${sha256.take(16)}..., got=${calculated.take(16)}...")
973
+ throw Exception("SHA256 verification failed")
974
+ }
975
+
976
+ OneKeyLog.info("BundleUpdate", "verifyBundle: SHA256 verified OK for appVersion=$appVersion, bundleVersion=$bundleVersion")
977
+ }
978
+ }
979
+
980
+ override fun installBundle(params: BundleInstallParams): Promise<Unit> {
981
+ return Promise.async {
982
+ val context = getContext()
983
+ val appVersion = params.latestVersion
984
+ val bundleVersion = params.bundleVersion
985
+ val signature = params.signature
986
+
987
+ OneKeyLog.info("BundleUpdate", "installBundle: appVersion=$appVersion, bundleVersion=$bundleVersion, signatureLength=${signature.length}")
988
+
989
+ // GPG verification skipped only when both DevSettings and skip-GPG toggle are enabled
990
+ val devSettings = isDevSettingsEnabled()
991
+ val skipGPGToggle = isSkipGPGEnabled()
992
+ val skipGPG = devSettings && skipGPGToggle
993
+ OneKeyLog.info("BundleUpdate", "installBundle: GPG check: devSettings=$devSettings, skipGPGToggle=$skipGPGToggle, skipGPG=$skipGPG")
994
+
995
+ val folderName = "$appVersion-$bundleVersion"
996
+ val currentFolderName = BundleUpdateStoreAndroid.getCurrentBundleVersion(context)
997
+ OneKeyLog.info("BundleUpdate", "installBundle: target=$folderName, current=$currentFolderName")
998
+
999
+ // Verify bundle directory exists
1000
+ val bundleDirPath = File(BundleUpdateStoreAndroid.getBundleDir(context), folderName)
1001
+ if (!bundleDirPath.exists()) {
1002
+ OneKeyLog.error("BundleUpdate", "installBundle: bundle directory not found: ${bundleDirPath.absolutePath}")
1003
+ throw Exception("Bundle directory not found: $folderName")
1004
+ }
1005
+
1006
+ val currentSignature = if (currentFolderName != null) BundleUpdateStoreAndroid.readSignatureFile(context, currentFolderName) else ""
1007
+
1008
+ BundleUpdateStoreAndroid.setCurrentBundleVersionAndSignature(context, folderName, signature)
1009
+ val nativeVersion = BundleUpdateStoreAndroid.getAppVersion(context) ?: ""
1010
+ BundleUpdateStoreAndroid.setNativeVersion(context, nativeVersion)
1011
+
1012
+ // Manage fallback data
1013
+ try {
1014
+ val fallbackData = BundleUpdateStoreAndroid.readFallbackUpdateBundleDataFile(context)
1015
+
1016
+ if (!currentFolderName.isNullOrEmpty()) {
1017
+ val lastDashIndex = currentFolderName.lastIndexOf("-")
1018
+ if (lastDashIndex > 0) {
1019
+ val curAppVersion = currentFolderName.substring(0, lastDashIndex)
1020
+ val curBundleVersion = currentFolderName.substring(lastDashIndex + 1)
1021
+ if (currentSignature.isNotEmpty()) {
1022
+ fallbackData.add(mutableMapOf(
1023
+ "appVersion" to curAppVersion,
1024
+ "bundleVersion" to curBundleVersion,
1025
+ "signature" to currentSignature
1026
+ ))
1027
+ }
1028
+ }
1029
+ }
1030
+
1031
+ if (fallbackData.size > 3) {
1032
+ val shifted = fallbackData.removeAt(0)
1033
+ val shiftApp = shifted["appVersion"]
1034
+ val shiftBundle = shifted["bundleVersion"]
1035
+ if (shiftApp != null && shiftBundle != null) {
1036
+ val shiftFolderName = "$shiftApp-$shiftBundle"
1037
+ val oldDir = File(BundleUpdateStoreAndroid.getBundleDir(context), shiftFolderName)
1038
+ if (oldDir.exists()) {
1039
+ BundleUpdateStoreAndroid.deleteDir(oldDir)
1040
+ }
1041
+ BundleUpdateStoreAndroid.deleteSignatureFile(context, shiftFolderName)
1042
+ }
1043
+ }
1044
+
1045
+ BundleUpdateStoreAndroid.writeFallbackUpdateBundleDataFile(fallbackData, context)
1046
+ OneKeyLog.info("BundleUpdate", "installBundle: completed successfully, installed version=$folderName, fallbackCount=${fallbackData.size}")
1047
+ } catch (e: Exception) {
1048
+ OneKeyLog.error("BundleUpdate", "installBundle: fallbackUpdateBundleData error: ${e.message}")
1049
+ }
1050
+ }
1051
+ }
1052
+
1053
+ override fun clearBundle(): Promise<Unit> {
1054
+ return Promise.async {
1055
+ OneKeyLog.info("BundleUpdate", "clearBundle: clearing download directory...")
1056
+ val context = getContext()
1057
+ val downloadDir = File(BundleUpdateStoreAndroid.getDownloadBundleDir(context))
1058
+ if (downloadDir.exists()) {
1059
+ BundleUpdateStoreAndroid.deleteDir(downloadDir)
1060
+ OneKeyLog.info("BundleUpdate", "clearBundle: download directory deleted")
1061
+ } else {
1062
+ OneKeyLog.info("BundleUpdate", "clearBundle: download directory does not exist, skipping")
1063
+ }
1064
+ isDownloading.set(false)
1065
+ OneKeyLog.info("BundleUpdate", "clearBundle: completed")
1066
+ }
1067
+ }
1068
+
1069
+ override fun clearAllJSBundleData(): Promise<TestResult> {
1070
+ return Promise.async {
1071
+ OneKeyLog.info("BundleUpdate", "clearAllJSBundleData: starting...")
1072
+ val context = getContext()
1073
+ val downloadDir = File(BundleUpdateStoreAndroid.getDownloadBundleDir(context))
1074
+ if (downloadDir.exists()) {
1075
+ BundleUpdateStoreAndroid.deleteDir(downloadDir)
1076
+ OneKeyLog.info("BundleUpdate", "clearAllJSBundleData: deleted download dir")
1077
+ }
1078
+ val bundleDir = File(BundleUpdateStoreAndroid.getBundleDir(context))
1079
+ if (bundleDir.exists()) {
1080
+ BundleUpdateStoreAndroid.deleteDir(bundleDir)
1081
+ OneKeyLog.info("BundleUpdate", "clearAllJSBundleData: deleted bundle dir")
1082
+ }
1083
+ BundleUpdateStoreAndroid.clearUpdateBundleData(context)
1084
+
1085
+ OneKeyLog.info("BundleUpdate", "clearAllJSBundleData: completed successfully")
1086
+ TestResult(success = true, message = "Successfully cleared all JS bundle data")
1087
+ }
1088
+ }
1089
+
1090
+ override fun getFallbackUpdateBundleData(): Promise<Array<FallbackBundleInfo>> {
1091
+ return Promise.async {
1092
+ val context = getContext()
1093
+ val data = BundleUpdateStoreAndroid.readFallbackUpdateBundleDataFile(context)
1094
+ val result = data.mapNotNull { map ->
1095
+ val appVersion = map["appVersion"] ?: return@mapNotNull null
1096
+ val bundleVersion = map["bundleVersion"] ?: return@mapNotNull null
1097
+ val signature = map["signature"] ?: return@mapNotNull null
1098
+ FallbackBundleInfo(appVersion = appVersion, bundleVersion = bundleVersion, signature = signature)
1099
+ }.toTypedArray()
1100
+ OneKeyLog.info("BundleUpdate", "getFallbackUpdateBundleData: found ${result.size} fallback entries")
1101
+ result
1102
+ }
1103
+ }
1104
+
1105
+ override fun setCurrentUpdateBundleData(params: BundleSwitchParams): Promise<Unit> {
1106
+ return Promise.async {
1107
+ val context = getContext()
1108
+ val bundleVersion = "${params.appVersion}-${params.bundleVersion}"
1109
+ OneKeyLog.info("BundleUpdate", "setCurrentUpdateBundleData: switching to $bundleVersion")
1110
+
1111
+ // Verify the bundle directory actually exists
1112
+ val bundleDirPath = File(BundleUpdateStoreAndroid.getBundleDir(context), bundleVersion)
1113
+ if (!bundleDirPath.exists()) {
1114
+ OneKeyLog.error("BundleUpdate", "setCurrentUpdateBundleData: bundle directory not found: ${bundleDirPath.absolutePath}")
1115
+ throw Exception("Bundle directory not found")
1116
+ }
1117
+
1118
+ // Verify GPG signature is valid (skipped when both DevSettings and skip-GPG toggle are enabled)
1119
+ val devSettings = isDevSettingsEnabled()
1120
+ val skipGPGToggle = isSkipGPGEnabled()
1121
+ OneKeyLog.info("BundleUpdate", "setCurrentUpdateBundleData: GPG check: devSettings=$devSettings, skipGPGToggle=$skipGPGToggle")
1122
+ if (!(devSettings && skipGPGToggle)) {
1123
+ if (params.signature.isEmpty() ||
1124
+ !BundleUpdateStoreAndroid.validateMetadataFileSha256(context, bundleVersion, params.signature)) {
1125
+ OneKeyLog.error("BundleUpdate", "setCurrentUpdateBundleData: GPG signature verification failed")
1126
+ throw Exception("Bundle signature verification failed")
1127
+ }
1128
+ OneKeyLog.info("BundleUpdate", "setCurrentUpdateBundleData: GPG signature verified OK")
1129
+ } else {
1130
+ OneKeyLog.warn("BundleUpdate", "setCurrentUpdateBundleData: GPG signature verification skipped (DevSettings + skip-GPG enabled)")
1131
+ }
1132
+
1133
+ val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
1134
+ prefs.edit()
1135
+ .putString("currentBundleVersion", bundleVersion)
1136
+ .apply()
1137
+ if (params.signature.isNotEmpty()) {
1138
+ BundleUpdateStoreAndroid.writeSignatureFile(context, bundleVersion, params.signature)
1139
+ }
1140
+ OneKeyLog.info("BundleUpdate", "setCurrentUpdateBundleData: switched to $bundleVersion")
1141
+ }
1142
+ }
1143
+
1144
+ override fun getWebEmbedPath(): String {
1145
+ val context = NitroModules.applicationContext ?: return ""
1146
+ val path = BundleUpdateStoreAndroid.getWebEmbedPath(context)
1147
+ OneKeyLog.debug("BundleUpdate", "getWebEmbedPath: $path")
1148
+ return path
1149
+ }
1150
+
1151
+ override fun getWebEmbedPathAsync(): Promise<String> {
1152
+ return Promise.async {
1153
+ val context = getContext()
1154
+ val path = BundleUpdateStoreAndroid.getWebEmbedPath(context)
1155
+ OneKeyLog.debug("BundleUpdate", "getWebEmbedPathAsync: $path")
1156
+ path
1157
+ }
1158
+ }
1159
+
1160
+ override fun getJsBundlePath(): Promise<String> {
1161
+ return Promise.async {
1162
+ val context = getContext()
1163
+ val path = BundleUpdateStoreAndroid.getCurrentBundleMainJSBundle(context) ?: ""
1164
+ OneKeyLog.info("BundleUpdate", "getJsBundlePath: ${if (path.isEmpty()) "(empty/no bundle)" else path}")
1165
+ path
1166
+ }
1167
+ }
1168
+
1169
+ override fun getNativeAppVersion(): Promise<String> {
1170
+ return Promise.async {
1171
+ val context = getContext()
1172
+ val version = BundleUpdateStoreAndroid.getAppVersion(context) ?: ""
1173
+ OneKeyLog.info("BundleUpdate", "getNativeAppVersion: $version")
1174
+ version
1175
+ }
1176
+ }
1177
+
1178
+ override fun testVerification(): Promise<Boolean> {
1179
+ return Promise.async {
1180
+ val testSignature = """-----BEGIN PGP SIGNED MESSAGE-----
1181
+ Hash: SHA256
1182
+
1183
+ {
1184
+ "fileName": "metadata.json",
1185
+ "sha256": "2ada9c871104fc40649fa3de67a7d8e33faadc18e9abd587e8bb85be0a003eba",
1186
+ "size": 158590,
1187
+ "generatedAt": "2025-09-19T07:49:13.000Z"
1188
+ }
1189
+ -----BEGIN PGP SIGNATURE-----
1190
+
1191
+ iQJCBAEBCAAsFiEE62iuVE8f3YzSZGJPs2mmepC/OHsFAmjNJ1IOHGRldkBvbmVr
1192
+ ZXkuc28ACgkQs2mmepC/OHs6Rw/9FKHl5aNsE7V0IsFf/l+h16BYKFwVsL69alMk
1193
+ CFLna8oUn0+tyECF6wKBKw5pHo5YR27o2pJfYbAER6dygDF6WTZ1lZdf5QcBMjGA
1194
+ LCeXC0hzUBzSSOH4bKBTa3fHp//HdSV1F2OnkymbXqYN7WXvuQPLZ0nV6aU88hCk
1195
+ HgFifcvkXAnWKoosUtj0Bban/YBRyvmQ5C2akxUPEkr4Yck1QXwzJeNRd7wMXHjH
1196
+ JFK6lJcuABiB8wpJDXJkFzKs29pvHIK2B2vdOjU2rQzKOUwaKHofDi5C4+JitT2b
1197
+ 2pSeYP3PAxXYw6XDOmKTOiC7fPnfLjtcPjNYNFCezVKZT6LKvZW9obnW8Q9LNJ4W
1198
+ okMPgHObkabv3OqUaTA9QNVfI/X9nvggzlPnaKDUrDWTf7n3vlrdexugkLtV/tJA
1199
+ uguPlI5hY7Ue5OW7ckWP46hfmq1+UaIdeUY7dEO+rPZDz6KcArpaRwBiLPBhneIr
1200
+ /X3KuMzS272YbPbavgCZGN9xJR5kZsEQE5HhPCbr6Nf0qDnh+X8mg0tAB/U6F+ZE
1201
+ o90sJL1ssIaYvST+VWVaGRr4V5nMDcgHzWSF9Q/wm22zxe4alDaBdvOlUseW0iaM
1202
+ n2DMz6gqk326W6SFynYtvuiXo7wG4Cmn3SuIU8xfv9rJqunpZGYchMd7nZektmEJ
1203
+ 91Js0rQ=
1204
+ =A/Ii
1205
+ -----END PGP SIGNATURE-----"""
1206
+ val result = BundleUpdateStoreAndroid.verifyGPGAndExtractSha256(testSignature)
1207
+ val isValid = result == "2ada9c871104fc40649fa3de67a7d8e33faadc18e9abd587e8bb85be0a003eba"
1208
+ OneKeyLog.info("BundleUpdate", "testVerification: GPG verification result: $isValid")
1209
+ isValid
1210
+ }
1211
+ }
1212
+
1213
+ override fun isBundleExists(appVersion: String, bundleVersion: String): Promise<Boolean> {
1214
+ return Promise.async {
1215
+ val context = getContext()
1216
+ val folderName = "$appVersion-$bundleVersion"
1217
+ val bundlePath = File(BundleUpdateStoreAndroid.getBundleDir(context), folderName)
1218
+ val exists = bundlePath.exists()
1219
+ OneKeyLog.info("BundleUpdate", "isBundleExists: appVersion=$appVersion, bundleVersion=$bundleVersion, exists=$exists")
1220
+ exists
1221
+ }
1222
+ }
1223
+
1224
+ override fun verifyExtractedBundle(appVersion: String, bundleVersion: String): Promise<Unit> {
1225
+ return Promise.async {
1226
+ OneKeyLog.info("BundleUpdate", "verifyExtractedBundle: appVersion=$appVersion, bundleVersion=$bundleVersion")
1227
+ val context = getContext()
1228
+ val folderName = "$appVersion-$bundleVersion"
1229
+ val bundlePath = File(BundleUpdateStoreAndroid.getBundleDir(context), folderName)
1230
+ if (!bundlePath.exists()) {
1231
+ OneKeyLog.error("BundleUpdate", "verifyExtractedBundle: bundle directory not found: ${bundlePath.absolutePath}")
1232
+ throw Exception("Bundle directory not found")
1233
+ }
1234
+ val metadataFile = File(bundlePath, "metadata.json")
1235
+ if (!metadataFile.exists()) {
1236
+ OneKeyLog.error("BundleUpdate", "verifyExtractedBundle: metadata.json not found in ${bundlePath.absolutePath}")
1237
+ throw Exception("metadata.json not found")
1238
+ }
1239
+
1240
+ OneKeyLog.info("BundleUpdate", "verifyExtractedBundle: parsing metadata and validating files...")
1241
+ val metadataContent = BundleUpdateStoreAndroid.readFileContent(metadataFile)
1242
+ val metadata = BundleUpdateStoreAndroid.parseMetadataJson(metadataContent)
1243
+ if (!BundleUpdateStoreAndroid.validateAllFilesInDir(context, bundlePath.absolutePath, metadata, appVersion, bundleVersion)) {
1244
+ OneKeyLog.error("BundleUpdate", "verifyExtractedBundle: file integrity check failed")
1245
+ throw Exception("File integrity check failed")
1246
+ }
1247
+ OneKeyLog.info("BundleUpdate", "verifyExtractedBundle: all files verified OK, fileCount=${metadata.size}")
1248
+ }
1249
+ }
1250
+
1251
+ override fun listLocalBundles(): Promise<Array<LocalBundleInfo>> {
1252
+ return Promise.async {
1253
+ val context = getContext()
1254
+ val bundleDir = File(BundleUpdateStoreAndroid.getBundleDir(context))
1255
+ val results = mutableListOf<LocalBundleInfo>()
1256
+ if (bundleDir.exists() && bundleDir.isDirectory) {
1257
+ bundleDir.listFiles()?.forEach { child ->
1258
+ if (!child.isDirectory) return@forEach
1259
+ val name = child.name
1260
+ val lastDash = name.lastIndexOf('-')
1261
+ if (lastDash <= 0) return@forEach
1262
+ val appVer = name.substring(0, lastDash)
1263
+ val bundleVer = name.substring(lastDash + 1)
1264
+ if (appVer.isNotEmpty() && bundleVer.isNotEmpty()) {
1265
+ results.add(LocalBundleInfo(appVersion = appVer, bundleVersion = bundleVer))
1266
+ }
1267
+ }
1268
+ }
1269
+ OneKeyLog.info("BundleUpdate", "listLocalBundles: found ${results.size} bundles")
1270
+ results.toTypedArray()
1271
+ }
1272
+ }
1273
+
1274
+ override fun listAscFiles(): Promise<Array<AscFileInfo>> {
1275
+ return Promise.async {
1276
+ val context = getContext()
1277
+ val ascDir = File(BundleUpdateStoreAndroid.getAscDir(context))
1278
+ val results = mutableListOf<AscFileInfo>()
1279
+ if (ascDir.exists() && ascDir.isDirectory) {
1280
+ ascDir.listFiles()?.forEach { file ->
1281
+ if (file.isFile) {
1282
+ results.add(AscFileInfo(fileName = file.name, filePath = file.absolutePath, fileSize = file.length().toDouble()))
1283
+ }
1284
+ }
1285
+ }
1286
+ OneKeyLog.info("BundleUpdate", "listAscFiles: found ${results.size} files")
1287
+ results.toTypedArray()
1288
+ }
1289
+ }
1290
+
1291
+ override fun getSha256FromFilePath(filePath: String): Promise<String> {
1292
+ return Promise.async {
1293
+ OneKeyLog.info("BundleUpdate", "getSha256FromFilePath: filePath=$filePath")
1294
+ if (filePath.isEmpty()) {
1295
+ OneKeyLog.warn("BundleUpdate", "getSha256FromFilePath: empty filePath")
1296
+ return@async ""
1297
+ }
1298
+
1299
+ // Restrict to bundle-related directories only
1300
+ val context = getContext()
1301
+ val resolvedPath = File(filePath).canonicalPath
1302
+ val bundleDir = File(BundleUpdateStoreAndroid.getBundleDir(context)).canonicalPath
1303
+ val downloadDir = File(BundleUpdateStoreAndroid.getDownloadBundleDir(context)).canonicalPath
1304
+ if (!resolvedPath.startsWith(bundleDir) && !resolvedPath.startsWith(downloadDir)) {
1305
+ OneKeyLog.error("BundleUpdate", "getSha256FromFilePath: path outside allowed directories: $resolvedPath")
1306
+ throw Exception("File path outside allowed bundle directories")
1307
+ }
1308
+
1309
+ val sha256 = BundleUpdateStoreAndroid.calculateSHA256(filePath) ?: ""
1310
+ OneKeyLog.info("BundleUpdate", "getSha256FromFilePath: sha256=${if (sha256.isEmpty()) "(empty)" else sha256.take(16) + "..."}")
1311
+ sha256
1312
+ }
1313
+ }
1314
+
1315
+ override fun testDeleteJsBundle(appVersion: String, bundleVersion: String): Promise<TestResult> {
1316
+ return Promise.async {
1317
+ OneKeyLog.info("BundleUpdate", "testDeleteJsBundle: appVersion=$appVersion, bundleVersion=$bundleVersion")
1318
+ val context = getContext()
1319
+ val folderName = "$appVersion-$bundleVersion"
1320
+ val jsBundlePath = File(File(BundleUpdateStoreAndroid.getBundleDir(context), folderName), "main.jsbundle.hbc")
1321
+
1322
+ if (jsBundlePath.exists()) {
1323
+ val success = jsBundlePath.delete()
1324
+ if (success) {
1325
+ OneKeyLog.info("BundleUpdate", "testDeleteJsBundle: deleted ${jsBundlePath.absolutePath}")
1326
+ TestResult(success = true, message = "Deleted jsBundle: ${jsBundlePath.absolutePath}")
1327
+ } else {
1328
+ OneKeyLog.error("BundleUpdate", "testDeleteJsBundle: failed to delete ${jsBundlePath.absolutePath}")
1329
+ throw Exception("Failed to delete jsBundle: ${jsBundlePath.absolutePath}")
1330
+ }
1331
+ } else {
1332
+ OneKeyLog.warn("BundleUpdate", "testDeleteJsBundle: file not found: ${jsBundlePath.absolutePath}")
1333
+ TestResult(success = false, message = "jsBundle not found: ${jsBundlePath.absolutePath}")
1334
+ }
1335
+ }
1336
+ }
1337
+
1338
+ override fun testDeleteJsRuntimeDir(appVersion: String, bundleVersion: String): Promise<TestResult> {
1339
+ return Promise.async {
1340
+ OneKeyLog.info("BundleUpdate", "testDeleteJsRuntimeDir: appVersion=$appVersion, bundleVersion=$bundleVersion")
1341
+ val context = getContext()
1342
+ val folderName = "$appVersion-$bundleVersion"
1343
+ val dir = File(BundleUpdateStoreAndroid.getBundleDir(context), folderName)
1344
+
1345
+ if (dir.exists()) {
1346
+ BundleUpdateStoreAndroid.deleteDir(dir)
1347
+ if (!dir.exists()) {
1348
+ OneKeyLog.info("BundleUpdate", "testDeleteJsRuntimeDir: deleted ${dir.absolutePath}")
1349
+ TestResult(success = true, message = "Deleted js runtime directory: ${dir.absolutePath}")
1350
+ } else {
1351
+ OneKeyLog.error("BundleUpdate", "testDeleteJsRuntimeDir: failed to delete ${dir.absolutePath}")
1352
+ throw Exception("Failed to delete js runtime directory: ${dir.absolutePath}")
1353
+ }
1354
+ } else {
1355
+ OneKeyLog.warn("BundleUpdate", "testDeleteJsRuntimeDir: directory not found: ${dir.absolutePath}")
1356
+ TestResult(success = false, message = "js runtime directory not found: ${dir.absolutePath}")
1357
+ }
1358
+ }
1359
+ }
1360
+
1361
+ override fun testDeleteMetadataJson(appVersion: String, bundleVersion: String): Promise<TestResult> {
1362
+ return Promise.async {
1363
+ OneKeyLog.info("BundleUpdate", "testDeleteMetadataJson: appVersion=$appVersion, bundleVersion=$bundleVersion")
1364
+ val context = getContext()
1365
+ val folderName = "$appVersion-$bundleVersion"
1366
+ val metadataFile = File(File(BundleUpdateStoreAndroid.getBundleDir(context), folderName), "metadata.json")
1367
+
1368
+ if (metadataFile.exists()) {
1369
+ val success = metadataFile.delete()
1370
+ if (success) {
1371
+ OneKeyLog.info("BundleUpdate", "testDeleteMetadataJson: deleted ${metadataFile.absolutePath}")
1372
+ TestResult(success = true, message = "Deleted metadata.json: ${metadataFile.absolutePath}")
1373
+ } else {
1374
+ OneKeyLog.error("BundleUpdate", "testDeleteMetadataJson: failed to delete ${metadataFile.absolutePath}")
1375
+ throw Exception("Failed to delete metadata.json: ${metadataFile.absolutePath}")
1376
+ }
1377
+ } else {
1378
+ OneKeyLog.warn("BundleUpdate", "testDeleteMetadataJson: file not found: ${metadataFile.absolutePath}")
1379
+ TestResult(success = false, message = "metadata.json not found: ${metadataFile.absolutePath}")
1380
+ }
1381
+ }
1382
+ }
1383
+
1384
+ override fun testWriteEmptyMetadataJson(appVersion: String, bundleVersion: String): Promise<TestResult> {
1385
+ return Promise.async {
1386
+ OneKeyLog.info("BundleUpdate", "testWriteEmptyMetadataJson: appVersion=$appVersion, bundleVersion=$bundleVersion")
1387
+ val context = getContext()
1388
+ val folderName = "$appVersion-$bundleVersion"
1389
+ val jsRuntimeDir = File(BundleUpdateStoreAndroid.getBundleDir(context), folderName)
1390
+ val metadataPath = File(jsRuntimeDir, "metadata.json")
1391
+
1392
+ if (!jsRuntimeDir.exists()) {
1393
+ if (!jsRuntimeDir.mkdirs()) {
1394
+ OneKeyLog.error("BundleUpdate", "testWriteEmptyMetadataJson: failed to create dir: ${jsRuntimeDir.absolutePath}")
1395
+ throw Exception("Failed to create directory: ${jsRuntimeDir.absolutePath}")
1396
+ }
1397
+ }
1398
+
1399
+ val emptyJson = JSONObject()
1400
+ FileOutputStream(metadataPath).use { fos ->
1401
+ fos.write(emptyJson.toString(2).toByteArray(Charsets.UTF_8))
1402
+ fos.flush()
1403
+ }
1404
+
1405
+ OneKeyLog.info("BundleUpdate", "testWriteEmptyMetadataJson: created ${metadataPath.absolutePath}")
1406
+ TestResult(success = true, message = "Created empty metadata.json: ${metadataPath.absolutePath}")
1407
+ }
1408
+ }
1409
+ }