@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.
- package/LICENSE +21 -0
- package/README.md +36 -0
- package/ReactNativeBundleUpdate.podspec +34 -0
- package/android/CMakeLists.txt +24 -0
- package/android/build.gradle +139 -0
- package/android/gradle.properties +4 -0
- package/android/src/main/AndroidManifest.xml +1 -0
- package/android/src/main/cpp/cpp-adapter.cpp +6 -0
- package/android/src/main/java/com/margelo/nitro/reactnativebundleupdate/ReactNativeBundleUpdate.kt +1409 -0
- package/android/src/main/java/com/margelo/nitro/reactnativebundleupdate/ReactNativeBundleUpdatePackage.kt +24 -0
- package/ios/Frameworks/Gopenpgp.xcframework/Info.plist +52 -0
- 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 +96 -0
- package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64/Gopenpgp.framework/Headers/Constants.objc.h +197 -0
- package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64/Gopenpgp.framework/Headers/Crypto.objc.h +1963 -0
- package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64/Gopenpgp.framework/Headers/Gopenpgp.h +23 -0
- package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64/Gopenpgp.framework/Headers/Mime.objc.h +59 -0
- package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64/Gopenpgp.framework/Headers/Mobile.objc.h +252 -0
- package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64/Gopenpgp.framework/Headers/Profile.objc.h +107 -0
- package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64/Gopenpgp.framework/Headers/Universe.objc.h +29 -0
- package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64/Gopenpgp.framework/Headers/ref.h +35 -0
- package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64/Gopenpgp.framework/Info.plist +20 -0
- package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64/Gopenpgp.framework/Modules/module.modulemap +13 -0
- 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 +96 -0
- package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64_x86_64-simulator/Gopenpgp.framework/Headers/Constants.objc.h +197 -0
- package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64_x86_64-simulator/Gopenpgp.framework/Headers/Crypto.objc.h +1963 -0
- package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64_x86_64-simulator/Gopenpgp.framework/Headers/Gopenpgp.h +23 -0
- package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64_x86_64-simulator/Gopenpgp.framework/Headers/Mime.objc.h +59 -0
- package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64_x86_64-simulator/Gopenpgp.framework/Headers/Mobile.objc.h +252 -0
- package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64_x86_64-simulator/Gopenpgp.framework/Headers/Profile.objc.h +107 -0
- package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64_x86_64-simulator/Gopenpgp.framework/Headers/Universe.objc.h +29 -0
- package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64_x86_64-simulator/Gopenpgp.framework/Headers/ref.h +35 -0
- package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64_x86_64-simulator/Gopenpgp.framework/Info.plist +20 -0
- package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64_x86_64-simulator/Gopenpgp.framework/Modules/module.modulemap +13 -0
- package/ios/ReactNativeBundleUpdate.swift +1338 -0
- package/lib/module/ReactNativeBundleUpdate.nitro.js +4 -0
- package/lib/module/ReactNativeBundleUpdate.nitro.js.map +1 -0
- package/lib/module/index.js +6 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/ReactNativeBundleUpdate.nitro.d.ts +101 -0
- package/lib/typescript/src/ReactNativeBundleUpdate.nitro.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +4 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/nitro.json +17 -0
- package/nitrogen/generated/android/c++/JAscFileInfo.hpp +65 -0
- package/nitrogen/generated/android/c++/JBundleDownloadASCParams.hpp +77 -0
- package/nitrogen/generated/android/c++/JBundleDownloadEvent.hpp +65 -0
- package/nitrogen/generated/android/c++/JBundleDownloadParams.hpp +73 -0
- package/nitrogen/generated/android/c++/JBundleDownloadResult.hpp +73 -0
- package/nitrogen/generated/android/c++/JBundleInstallParams.hpp +69 -0
- package/nitrogen/generated/android/c++/JBundleSwitchParams.hpp +65 -0
- package/nitrogen/generated/android/c++/JBundleVerifyASCParams.hpp +73 -0
- package/nitrogen/generated/android/c++/JBundleVerifyParams.hpp +69 -0
- package/nitrogen/generated/android/c++/JFallbackBundleInfo.hpp +65 -0
- package/nitrogen/generated/android/c++/JFunc_void_BundleDownloadEvent.hpp +78 -0
- package/nitrogen/generated/android/c++/JHybridReactNativeBundleUpdateSpec.cpp +486 -0
- package/nitrogen/generated/android/c++/JHybridReactNativeBundleUpdateSpec.hpp +89 -0
- package/nitrogen/generated/android/c++/JLocalBundleInfo.hpp +61 -0
- package/nitrogen/generated/android/c++/JTestResult.hpp +61 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativebundleupdate/AscFileInfo.kt +44 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativebundleupdate/BundleDownloadASCParams.kt +53 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativebundleupdate/BundleDownloadEvent.kt +44 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativebundleupdate/BundleDownloadParams.kt +50 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativebundleupdate/BundleDownloadResult.kt +50 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativebundleupdate/BundleInstallParams.kt +47 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativebundleupdate/BundleSwitchParams.kt +44 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativebundleupdate/BundleVerifyASCParams.kt +50 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativebundleupdate/BundleVerifyParams.kt +47 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativebundleupdate/FallbackBundleInfo.kt +44 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativebundleupdate/Func_void_BundleDownloadEvent.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativebundleupdate/HybridReactNativeBundleUpdateSpec.kt +159 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativebundleupdate/LocalBundleInfo.kt +41 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativebundleupdate/TestResult.kt +41 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativebundleupdate/reactnativebundleupdateOnLoad.kt +35 -0
- package/nitrogen/generated/android/reactnativebundleupdate+autolinking.cmake +81 -0
- package/nitrogen/generated/android/reactnativebundleupdate+autolinking.gradle +27 -0
- package/nitrogen/generated/android/reactnativebundleupdateOnLoad.cpp +46 -0
- package/nitrogen/generated/android/reactnativebundleupdateOnLoad.hpp +25 -0
- package/nitrogen/generated/ios/ReactNativeBundleUpdate+autolinking.rb +60 -0
- package/nitrogen/generated/ios/ReactNativeBundleUpdate-Swift-Cxx-Bridge.cpp +113 -0
- package/nitrogen/generated/ios/ReactNativeBundleUpdate-Swift-Cxx-Bridge.hpp +513 -0
- package/nitrogen/generated/ios/ReactNativeBundleUpdate-Swift-Cxx-Umbrella.hpp +83 -0
- package/nitrogen/generated/ios/ReactNativeBundleUpdateAutolinking.mm +33 -0
- package/nitrogen/generated/ios/ReactNativeBundleUpdateAutolinking.swift +25 -0
- package/nitrogen/generated/ios/c++/HybridReactNativeBundleUpdateSpecSwift.cpp +11 -0
- package/nitrogen/generated/ios/c++/HybridReactNativeBundleUpdateSpecSwift.hpp +304 -0
- package/nitrogen/generated/ios/swift/AscFileInfo.swift +58 -0
- package/nitrogen/generated/ios/swift/BundleDownloadASCParams.swift +91 -0
- package/nitrogen/generated/ios/swift/BundleDownloadEvent.swift +58 -0
- package/nitrogen/generated/ios/swift/BundleDownloadParams.swift +80 -0
- package/nitrogen/generated/ios/swift/BundleDownloadResult.swift +80 -0
- package/nitrogen/generated/ios/swift/BundleInstallParams.swift +69 -0
- package/nitrogen/generated/ios/swift/BundleSwitchParams.swift +58 -0
- package/nitrogen/generated/ios/swift/BundleVerifyASCParams.swift +80 -0
- package/nitrogen/generated/ios/swift/BundleVerifyParams.swift +69 -0
- package/nitrogen/generated/ios/swift/FallbackBundleInfo.swift +58 -0
- package/nitrogen/generated/ios/swift/Func_void.swift +47 -0
- package/nitrogen/generated/ios/swift/Func_void_BundleDownloadEvent.swift +47 -0
- package/nitrogen/generated/ios/swift/Func_void_BundleDownloadResult.swift +47 -0
- package/nitrogen/generated/ios/swift/Func_void_TestResult.swift +47 -0
- package/nitrogen/generated/ios/swift/Func_void_bool.swift +47 -0
- package/nitrogen/generated/ios/swift/Func_void_std__exception_ptr.swift +47 -0
- package/nitrogen/generated/ios/swift/Func_void_std__string.swift +47 -0
- package/nitrogen/generated/ios/swift/Func_void_std__vector_AscFileInfo_.swift +47 -0
- package/nitrogen/generated/ios/swift/Func_void_std__vector_FallbackBundleInfo_.swift +47 -0
- package/nitrogen/generated/ios/swift/Func_void_std__vector_LocalBundleInfo_.swift +47 -0
- package/nitrogen/generated/ios/swift/HybridReactNativeBundleUpdateSpec.swift +80 -0
- package/nitrogen/generated/ios/swift/HybridReactNativeBundleUpdateSpec_cxx.swift +595 -0
- package/nitrogen/generated/ios/swift/LocalBundleInfo.swift +47 -0
- package/nitrogen/generated/ios/swift/TestResult.swift +47 -0
- package/nitrogen/generated/shared/c++/AscFileInfo.hpp +83 -0
- package/nitrogen/generated/shared/c++/BundleDownloadASCParams.hpp +95 -0
- package/nitrogen/generated/shared/c++/BundleDownloadEvent.hpp +83 -0
- package/nitrogen/generated/shared/c++/BundleDownloadParams.hpp +91 -0
- package/nitrogen/generated/shared/c++/BundleDownloadResult.hpp +91 -0
- package/nitrogen/generated/shared/c++/BundleInstallParams.hpp +87 -0
- package/nitrogen/generated/shared/c++/BundleSwitchParams.hpp +83 -0
- package/nitrogen/generated/shared/c++/BundleVerifyASCParams.hpp +91 -0
- package/nitrogen/generated/shared/c++/BundleVerifyParams.hpp +87 -0
- package/nitrogen/generated/shared/c++/FallbackBundleInfo.hpp +83 -0
- package/nitrogen/generated/shared/c++/HybridReactNativeBundleUpdateSpec.cpp +45 -0
- package/nitrogen/generated/shared/c++/HybridReactNativeBundleUpdateSpec.hpp +124 -0
- package/nitrogen/generated/shared/c++/LocalBundleInfo.hpp +79 -0
- package/nitrogen/generated/shared/c++/TestResult.hpp +79 -0
- package/package.json +169 -0
- package/src/ReactNativeBundleUpdate.nitro.ts +143 -0
- package/src/index.tsx +8 -0
package/android/src/main/java/com/margelo/nitro/reactnativebundleupdate/ReactNativeBundleUpdate.kt
ADDED
|
@@ -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
|
+
}
|