@onekeyfe/react-native-app-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/ReactNativeAppUpdate.podspec +30 -0
- package/android/CMakeLists.txt +24 -0
- package/android/build.gradle +141 -0
- package/android/gradle.properties +4 -0
- package/android/src/main/AndroidManifest.xml +17 -0
- package/android/src/main/cpp/cpp-adapter.cpp +6 -0
- package/android/src/main/java/com/margelo/nitro/reactnativeappupdate/ReactNativeAppUpdate.kt +819 -0
- package/android/src/main/java/com/margelo/nitro/reactnativeappupdate/ReactNativeAppUpdatePackage.kt +24 -0
- package/android/src/main/res/xml/app_update_file_paths.xml +4 -0
- package/ios/ReactNativeAppUpdate.swift +41 -0
- package/lib/module/ReactNativeAppUpdate.nitro.js +4 -0
- package/lib/module/ReactNativeAppUpdate.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/ReactNativeAppUpdate.nitro.d.ts +28 -0
- package/lib/typescript/src/ReactNativeAppUpdate.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++/JAppUpdateDownloadParams.hpp +65 -0
- package/nitrogen/generated/android/c++/JAppUpdateFileParams.hpp +57 -0
- package/nitrogen/generated/android/c++/JDownloadEvent.hpp +65 -0
- package/nitrogen/generated/android/c++/JFunc_void_DownloadEvent.hpp +78 -0
- package/nitrogen/generated/android/c++/JHybridReactNativeAppUpdateSpec.cpp +162 -0
- package/nitrogen/generated/android/c++/JHybridReactNativeAppUpdateSpec.hpp +72 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativeappupdate/AppUpdateDownloadParams.kt +44 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativeappupdate/AppUpdateFileParams.kt +38 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativeappupdate/DownloadEvent.kt +44 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativeappupdate/Func_void_DownloadEvent.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativeappupdate/HybridReactNativeAppUpdateSpec.kt +91 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativeappupdate/reactnativeappupdateOnLoad.kt +35 -0
- package/nitrogen/generated/android/reactnativeappupdate+autolinking.cmake +81 -0
- package/nitrogen/generated/android/reactnativeappupdate+autolinking.gradle +27 -0
- package/nitrogen/generated/android/reactnativeappupdateOnLoad.cpp +46 -0
- package/nitrogen/generated/android/reactnativeappupdateOnLoad.hpp +25 -0
- package/nitrogen/generated/ios/ReactNativeAppUpdate+autolinking.rb +60 -0
- package/nitrogen/generated/ios/ReactNativeAppUpdate-Swift-Cxx-Bridge.cpp +57 -0
- package/nitrogen/generated/ios/ReactNativeAppUpdate-Swift-Cxx-Bridge.hpp +154 -0
- package/nitrogen/generated/ios/ReactNativeAppUpdate-Swift-Cxx-Umbrella.hpp +55 -0
- package/nitrogen/generated/ios/ReactNativeAppUpdateAutolinking.mm +33 -0
- package/nitrogen/generated/ios/ReactNativeAppUpdateAutolinking.swift +25 -0
- package/nitrogen/generated/ios/c++/HybridReactNativeAppUpdateSpecSwift.cpp +11 -0
- package/nitrogen/generated/ios/c++/HybridReactNativeAppUpdateSpecSwift.hpp +140 -0
- package/nitrogen/generated/ios/swift/AppUpdateDownloadParams.swift +58 -0
- package/nitrogen/generated/ios/swift/AppUpdateFileParams.swift +36 -0
- package/nitrogen/generated/ios/swift/DownloadEvent.swift +58 -0
- package/nitrogen/generated/ios/swift/Func_void.swift +47 -0
- package/nitrogen/generated/ios/swift/Func_void_DownloadEvent.swift +47 -0
- package/nitrogen/generated/ios/swift/Func_void_std__exception_ptr.swift +47 -0
- package/nitrogen/generated/ios/swift/HybridReactNativeAppUpdateSpec.swift +63 -0
- package/nitrogen/generated/ios/swift/HybridReactNativeAppUpdateSpec_cxx.swift +261 -0
- package/nitrogen/generated/shared/c++/AppUpdateDownloadParams.hpp +83 -0
- package/nitrogen/generated/shared/c++/AppUpdateFileParams.hpp +75 -0
- package/nitrogen/generated/shared/c++/DownloadEvent.hpp +83 -0
- package/nitrogen/generated/shared/c++/HybridReactNativeAppUpdateSpec.cpp +28 -0
- package/nitrogen/generated/shared/c++/HybridReactNativeAppUpdateSpec.hpp +78 -0
- package/package.json +169 -0
- package/src/ReactNativeAppUpdate.nitro.ts +30 -0
- package/src/index.tsx +8 -0
|
@@ -0,0 +1,819 @@
|
|
|
1
|
+
package com.margelo.nitro.reactnativeappupdate
|
|
2
|
+
|
|
3
|
+
import android.app.NotificationChannel
|
|
4
|
+
import android.app.NotificationManager
|
|
5
|
+
import android.content.Intent
|
|
6
|
+
import android.content.pm.PackageManager
|
|
7
|
+
import android.net.Uri
|
|
8
|
+
import android.os.Build
|
|
9
|
+
import androidx.core.app.ActivityCompat
|
|
10
|
+
import androidx.core.app.NotificationCompat
|
|
11
|
+
import androidx.core.app.NotificationManagerCompat
|
|
12
|
+
import androidx.core.content.FileProvider
|
|
13
|
+
import com.facebook.proguard.annotations.DoNotStrip
|
|
14
|
+
import com.margelo.nitro.core.Promise
|
|
15
|
+
import com.margelo.nitro.NitroModules
|
|
16
|
+
import com.margelo.nitro.nativelogger.OneKeyLog
|
|
17
|
+
import com.tencent.mmkv.MMKV
|
|
18
|
+
import okhttp3.OkHttpClient
|
|
19
|
+
import okhttp3.Request
|
|
20
|
+
import okio.buffer
|
|
21
|
+
import okio.sink
|
|
22
|
+
import java.io.BufferedInputStream
|
|
23
|
+
import java.io.BufferedReader
|
|
24
|
+
import java.io.File
|
|
25
|
+
import java.io.FileInputStream
|
|
26
|
+
import java.io.FileOutputStream
|
|
27
|
+
import java.io.InputStreamReader
|
|
28
|
+
import java.security.MessageDigest
|
|
29
|
+
import java.util.concurrent.CopyOnWriteArrayList
|
|
30
|
+
import java.util.concurrent.TimeUnit
|
|
31
|
+
import java.util.concurrent.atomic.AtomicBoolean
|
|
32
|
+
import java.util.concurrent.atomic.AtomicLong
|
|
33
|
+
import org.bouncycastle.openpgp.PGPPublicKeyRingCollection
|
|
34
|
+
import org.bouncycastle.openpgp.PGPSignatureList
|
|
35
|
+
import org.bouncycastle.openpgp.PGPUtil
|
|
36
|
+
import org.bouncycastle.openpgp.jcajce.JcaPGPObjectFactory
|
|
37
|
+
import org.bouncycastle.openpgp.operator.jcajce.JcaKeyFingerprintCalculator
|
|
38
|
+
import org.bouncycastle.openpgp.operator.jcajce.JcaPGPContentVerifierBuilderProvider
|
|
39
|
+
import org.bouncycastle.jce.provider.BouncyCastleProvider
|
|
40
|
+
|
|
41
|
+
// OneKey GPG public key for signature verification
|
|
42
|
+
private const val GPG_PUBLIC_KEY = """-----BEGIN PGP PUBLIC KEY BLOCK-----
|
|
43
|
+
|
|
44
|
+
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
|
|
91
|
+
P2Q5dClenjjjVA==
|
|
92
|
+
=F0np
|
|
93
|
+
-----END PGP PUBLIC KEY BLOCK-----"""
|
|
94
|
+
|
|
95
|
+
private data class Listener(
|
|
96
|
+
val id: Double,
|
|
97
|
+
val callback: (DownloadEvent) -> Unit
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
@DoNotStrip
|
|
101
|
+
class ReactNativeAppUpdate : HybridReactNativeAppUpdateSpec() {
|
|
102
|
+
|
|
103
|
+
companion object {
|
|
104
|
+
private const val CHANNEL_ID = "updateApp"
|
|
105
|
+
private const val NOTIFICATION_ID = 1
|
|
106
|
+
// Use our own BouncyCastle provider instance to avoid Android's stripped-down built-in "BC"
|
|
107
|
+
private val bcProvider = BouncyCastleProvider()
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
private val listeners = CopyOnWriteArrayList<Listener>()
|
|
111
|
+
private val nextListenerId = AtomicLong(1)
|
|
112
|
+
private val isDownloading = AtomicBoolean(false)
|
|
113
|
+
// downloadThread removed: downloads use coroutine-based Promise.async, not raw threads
|
|
114
|
+
|
|
115
|
+
private fun sendEvent(type: String, progress: Int = 0, message: String = "") {
|
|
116
|
+
val event = DownloadEvent(type = type, progress = progress.toDouble(), message = message)
|
|
117
|
+
for (listener in listeners) {
|
|
118
|
+
try {
|
|
119
|
+
listener.callback(event)
|
|
120
|
+
} catch (e: Exception) {
|
|
121
|
+
OneKeyLog.error("AppUpdate", "Error sending event: ${e.message}")
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
override fun addDownloadListener(callback: (DownloadEvent) -> Unit): Double {
|
|
127
|
+
val id = nextListenerId.getAndIncrement().toDouble()
|
|
128
|
+
listeners.add(Listener(id, callback))
|
|
129
|
+
return id
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
override fun removeDownloadListener(id: Double) {
|
|
133
|
+
listeners.removeAll { it.id == id }
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
private fun getApkCacheDir(): File {
|
|
137
|
+
val context = NitroModules.applicationContext
|
|
138
|
+
?: throw SecurityException("Application context unavailable")
|
|
139
|
+
val apkDir = File(context.cacheDir, "apks")
|
|
140
|
+
if (!apkDir.exists()) apkDir.mkdirs()
|
|
141
|
+
return apkDir
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Derive a local file name from a download URL by extracting the last path segment.
|
|
146
|
+
* e.g. "https://example.com/path/to/app-1.0.apk" -> "app-1.0.apk"
|
|
147
|
+
*/
|
|
148
|
+
private fun filePathFromUrl(url: String): String {
|
|
149
|
+
val path = Uri.parse(url).lastPathSegment
|
|
150
|
+
if (path.isNullOrBlank()) {
|
|
151
|
+
throw Exception("Cannot derive file name from URL: $url")
|
|
152
|
+
}
|
|
153
|
+
return path
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
private fun buildFile(path: String): File {
|
|
157
|
+
val stripped = path.removePrefix("file:///")
|
|
158
|
+
val context = NitroModules.applicationContext
|
|
159
|
+
?: throw SecurityException("Application context unavailable, cannot validate file path")
|
|
160
|
+
|
|
161
|
+
// Resolve relative paths against cacheDir/apk/
|
|
162
|
+
val file = if (stripped.startsWith("/")) {
|
|
163
|
+
File(stripped)
|
|
164
|
+
} else {
|
|
165
|
+
File(getApkCacheDir(), stripped)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Validate the resolved path is within the app's cache or files directory
|
|
169
|
+
val canonicalPath = file.canonicalPath
|
|
170
|
+
val cacheDir = context.cacheDir.canonicalPath
|
|
171
|
+
val filesDir = context.filesDir.canonicalPath
|
|
172
|
+
val externalCacheDir = context.externalCacheDir?.canonicalPath
|
|
173
|
+
val externalFilesDir = context.getExternalFilesDir(null)?.canonicalPath
|
|
174
|
+
val allowed = canonicalPath.startsWith(cacheDir + File.separator) || canonicalPath == cacheDir ||
|
|
175
|
+
canonicalPath.startsWith(filesDir + File.separator) || canonicalPath == filesDir ||
|
|
176
|
+
(externalCacheDir != null && (canonicalPath.startsWith(externalCacheDir + File.separator) || canonicalPath == externalCacheDir)) ||
|
|
177
|
+
(externalFilesDir != null && (canonicalPath.startsWith(externalFilesDir + File.separator) || canonicalPath == externalFilesDir))
|
|
178
|
+
if (!allowed) {
|
|
179
|
+
OneKeyLog.error("AppUpdate", "buildFile: path outside allowed directories, " +
|
|
180
|
+
"input=$path, resolved=$canonicalPath, " +
|
|
181
|
+
"cacheDir=$cacheDir, filesDir=$filesDir, " +
|
|
182
|
+
"externalCacheDir=$externalCacheDir, externalFilesDir=$externalFilesDir")
|
|
183
|
+
throw SecurityException("Path outside allowed directories: $canonicalPath")
|
|
184
|
+
}
|
|
185
|
+
OneKeyLog.debug("AppUpdate", "buildFile: input=$path, resolved=$canonicalPath")
|
|
186
|
+
return file
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
private fun bytesToHex(bytes: ByteArray): String {
|
|
190
|
+
return bytes.joinToString("") { "%02x".format(it) }
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
private fun computeSha256(file: File): String {
|
|
194
|
+
val digest = MessageDigest.getInstance("SHA-256")
|
|
195
|
+
BufferedInputStream(FileInputStream(file)).use { bis ->
|
|
196
|
+
val buffer = ByteArray(8192)
|
|
197
|
+
var count: Int
|
|
198
|
+
while (bis.read(buffer).also { count = it } > 0) {
|
|
199
|
+
digest.update(buffer, 0, count)
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return bytesToHex(digest.digest())
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Extract SHA256 hash from a PGP cleartext signed ASC file.
|
|
207
|
+
* Returns null if the file is invalid or cannot be parsed.
|
|
208
|
+
*/
|
|
209
|
+
private fun extractSha256FromAsc(ascFile: File): String? {
|
|
210
|
+
val ascContent = ascFile.readText()
|
|
211
|
+
if (!ascContent.contains("-----BEGIN PGP SIGNED MESSAGE-----")) return null
|
|
212
|
+
val lines = ascContent.lines()
|
|
213
|
+
val hashHeaderIdx = lines.indexOfFirst { it.startsWith("Hash:") }
|
|
214
|
+
val sigStartIdx = lines.indexOfFirst { it == "-----BEGIN PGP SIGNATURE-----" }
|
|
215
|
+
if (hashHeaderIdx < 0 || sigStartIdx < 0) return null
|
|
216
|
+
val bodyLines = lines.subList(hashHeaderIdx + 2, sigStartIdx)
|
|
217
|
+
val sha256 = bodyLines.joinToString("\n").trim().split("\\s+".toRegex())[0].lowercase()
|
|
218
|
+
if (sha256.length != 64 || !sha256.all { it in '0'..'9' || it in 'a'..'f' }) return null
|
|
219
|
+
return sha256
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Download ASC file for the given URL and save to the given path.
|
|
224
|
+
* Returns the saved file, or null on failure.
|
|
225
|
+
*/
|
|
226
|
+
private fun downloadAscFile(ascUrl: String, ascFile: File): File? {
|
|
227
|
+
val client = OkHttpClient.Builder()
|
|
228
|
+
.connectTimeout(10, TimeUnit.SECONDS)
|
|
229
|
+
.readTimeout(30, TimeUnit.SECONDS)
|
|
230
|
+
.followRedirects(false)
|
|
231
|
+
.followSslRedirects(false)
|
|
232
|
+
.build()
|
|
233
|
+
val request = Request.Builder().url(ascUrl).build()
|
|
234
|
+
val response = client.newCall(request).execute()
|
|
235
|
+
if (!response.isSuccessful) return null
|
|
236
|
+
val content = StringBuilder()
|
|
237
|
+
val maxAscSize = 10 * 1024
|
|
238
|
+
val body = response.body ?: return null
|
|
239
|
+
BufferedReader(InputStreamReader(body.byteStream())).use { reader ->
|
|
240
|
+
var line: String?
|
|
241
|
+
while (reader.readLine().also { line = it } != null) {
|
|
242
|
+
content.append(line).append("\n")
|
|
243
|
+
if (content.length > maxAscSize) return null
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
val ascContent = content.toString()
|
|
247
|
+
if (ascContent.isEmpty()) return null
|
|
248
|
+
ascFile.parentFile?.mkdirs()
|
|
249
|
+
FileOutputStream(ascFile).use { fos -> fos.write(ascContent.toByteArray()) }
|
|
250
|
+
return ascFile
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Check if an existing APK is valid by verifying its SHA256 against the ASC file.
|
|
255
|
+
* Downloads ASC if not present. Returns true if APK is valid.
|
|
256
|
+
*/
|
|
257
|
+
private fun tryVerifyExistingApk(url: String, filePath: String, apkFile: File): Boolean {
|
|
258
|
+
return try {
|
|
259
|
+
val ascFilePath = "$filePath.SHA256SUMS.asc"
|
|
260
|
+
val ascFile = buildFile(ascFilePath)
|
|
261
|
+
|
|
262
|
+
if (!ascFile.exists()) {
|
|
263
|
+
val ascUrl = "$url.SHA256SUMS.asc"
|
|
264
|
+
OneKeyLog.info("AppUpdate", "tryVerifyExistingApk: ASC not found, downloading from $ascUrl")
|
|
265
|
+
if (downloadAscFile(ascUrl, ascFile) == null) {
|
|
266
|
+
OneKeyLog.warn("AppUpdate", "tryVerifyExistingApk: ASC download failed")
|
|
267
|
+
return false
|
|
268
|
+
}
|
|
269
|
+
OneKeyLog.info("AppUpdate", "tryVerifyExistingApk: ASC downloaded to ${ascFile.absolutePath}")
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
val expectedSha256 = extractSha256FromAsc(ascFile)
|
|
273
|
+
if (expectedSha256 == null) {
|
|
274
|
+
OneKeyLog.warn("AppUpdate", "tryVerifyExistingApk: failed to extract SHA256 from ASC")
|
|
275
|
+
return false
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
OneKeyLog.info("AppUpdate", "tryVerifyExistingApk: computing SHA256 of existing APK (size=${apkFile.length()})...")
|
|
279
|
+
val actualSha256 = computeSha256(apkFile)
|
|
280
|
+
|
|
281
|
+
if (actualSha256 == expectedSha256) {
|
|
282
|
+
OneKeyLog.info("AppUpdate", "tryVerifyExistingApk: SHA256 matches, APK is valid")
|
|
283
|
+
true
|
|
284
|
+
} else {
|
|
285
|
+
OneKeyLog.warn("AppUpdate", "tryVerifyExistingApk: SHA256 mismatch, expected=${expectedSha256.take(16)}..., got=${actualSha256.take(16)}...")
|
|
286
|
+
false
|
|
287
|
+
}
|
|
288
|
+
} catch (e: Exception) {
|
|
289
|
+
OneKeyLog.warn("AppUpdate", "tryVerifyExistingApk: failed: ${e.javaClass.simpleName}: ${e.message}")
|
|
290
|
+
false
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
private fun isDebuggable(): Boolean {
|
|
295
|
+
val context = NitroModules.applicationContext ?: return false
|
|
296
|
+
return (context.applicationInfo.flags and android.content.pm.ApplicationInfo.FLAG_DEBUGGABLE) != 0
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/** Returns true if OneKey developer mode (DevSettings) is enabled via MMKV storage. */
|
|
300
|
+
private fun isDevSettingsEnabled(): Boolean {
|
|
301
|
+
return try {
|
|
302
|
+
val context = NitroModules.applicationContext ?: return false
|
|
303
|
+
MMKV.initialize(context)
|
|
304
|
+
val mmkv = MMKV.mmkvWithID("onekey-app-setting") ?: return false
|
|
305
|
+
mmkv.decodeBool("onekey_developer_mode_enabled", false)
|
|
306
|
+
} catch (e: Exception) {
|
|
307
|
+
false
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
override fun downloadAPK(params: AppUpdateDownloadParams): Promise<Unit> {
|
|
312
|
+
return Promise.async {
|
|
313
|
+
if (isDownloading.getAndSet(true)) {
|
|
314
|
+
OneKeyLog.warn("AppUpdate", "downloadAPK: rejected, already downloading")
|
|
315
|
+
throw Exception("Download already in progress")
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
try {
|
|
319
|
+
val url = params.downloadUrl
|
|
320
|
+
val filePath = filePathFromUrl(url)
|
|
321
|
+
val notificationTitle = params.notificationTitle
|
|
322
|
+
val fileSize = params.fileSize.toLong()
|
|
323
|
+
|
|
324
|
+
OneKeyLog.info("AppUpdate", "downloadAPK: url=$url, filePath=$filePath, fileSize=$fileSize")
|
|
325
|
+
|
|
326
|
+
val context = NitroModules.applicationContext
|
|
327
|
+
?: throw Exception("Application context unavailable")
|
|
328
|
+
|
|
329
|
+
val notifyManager = NotificationManagerCompat.from(context)
|
|
330
|
+
val builder = NotificationCompat.Builder(context, CHANNEL_ID)
|
|
331
|
+
.setContentTitle(notificationTitle)
|
|
332
|
+
.setContentText("")
|
|
333
|
+
.setOngoing(true)
|
|
334
|
+
.setPriority(NotificationCompat.PRIORITY_LOW)
|
|
335
|
+
.setSmallIcon(android.R.drawable.stat_sys_download)
|
|
336
|
+
|
|
337
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
338
|
+
val channel = NotificationChannel(
|
|
339
|
+
CHANNEL_ID, "updateApp", NotificationManager.IMPORTANCE_DEFAULT
|
|
340
|
+
)
|
|
341
|
+
notifyManager.createNotificationChannel(channel)
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
if (!url.startsWith("https://")) {
|
|
345
|
+
OneKeyLog.error("AppUpdate", "downloadAPK: URL is not HTTPS: $url")
|
|
346
|
+
throw Exception("Download URL must use HTTPS")
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
val downloadedFile = buildFile(filePath)
|
|
350
|
+
if (downloadedFile.exists()) {
|
|
351
|
+
OneKeyLog.info("AppUpdate", "downloadAPK: existing APK found (size=${downloadedFile.length()}), verifying...")
|
|
352
|
+
if (tryVerifyExistingApk(url, filePath, downloadedFile)) {
|
|
353
|
+
OneKeyLog.info("AppUpdate", "downloadAPK: existing APK is valid, skipping download")
|
|
354
|
+
sendEvent("update/downloaded")
|
|
355
|
+
return@async
|
|
356
|
+
}
|
|
357
|
+
OneKeyLog.info("AppUpdate", "downloadAPK: existing APK invalid, deleting and re-downloading...")
|
|
358
|
+
downloadedFile.delete()
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
val client = OkHttpClient.Builder()
|
|
362
|
+
.connectTimeout(10, TimeUnit.SECONDS)
|
|
363
|
+
.readTimeout(60, TimeUnit.SECONDS)
|
|
364
|
+
.followRedirects(false)
|
|
365
|
+
.followSslRedirects(false)
|
|
366
|
+
.build()
|
|
367
|
+
val request = Request.Builder().url(url).build()
|
|
368
|
+
val response = client.newCall(request).execute()
|
|
369
|
+
|
|
370
|
+
if (!response.isSuccessful) {
|
|
371
|
+
OneKeyLog.error("AppUpdate", "downloadAPK: HTTP error, statusCode=${response.code}")
|
|
372
|
+
sendEvent("update/error", message = response.code.toString())
|
|
373
|
+
throw Exception(response.code.toString())
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
val body = response.body ?: throw Exception("Empty response body")
|
|
377
|
+
val contentLength = if (fileSize > 0) fileSize else body.contentLength()
|
|
378
|
+
OneKeyLog.info("AppUpdate", "downloadAPK: HTTP 200, contentLength=$contentLength, starting download...")
|
|
379
|
+
val source = body.source()
|
|
380
|
+
val sink = downloadedFile.sink().buffer()
|
|
381
|
+
val sinkBuffer = sink.buffer
|
|
382
|
+
|
|
383
|
+
var totalBytesRead = 0L
|
|
384
|
+
val bufferSize = 8 * 1024L
|
|
385
|
+
sendEvent("update/start")
|
|
386
|
+
var prevProgress = 0
|
|
387
|
+
|
|
388
|
+
try {
|
|
389
|
+
while (true) {
|
|
390
|
+
val bytesRead = source.read(sinkBuffer, bufferSize)
|
|
391
|
+
if (bytesRead == -1L) break
|
|
392
|
+
sink.emit()
|
|
393
|
+
totalBytesRead += bytesRead
|
|
394
|
+
if (contentLength > 0) {
|
|
395
|
+
val progress = ((totalBytesRead * 100) / contentLength).toInt()
|
|
396
|
+
if (prevProgress != progress) {
|
|
397
|
+
sendEvent("update/downloading", progress = progress)
|
|
398
|
+
OneKeyLog.info("AppUpdate", "download progress: $progress%")
|
|
399
|
+
builder.setProgress(100, progress, false)
|
|
400
|
+
if (ActivityCompat.checkSelfPermission(
|
|
401
|
+
context, android.Manifest.permission.POST_NOTIFICATIONS
|
|
402
|
+
) == PackageManager.PERMISSION_GRANTED
|
|
403
|
+
) {
|
|
404
|
+
notifyManager.notify(NOTIFICATION_ID, builder.build())
|
|
405
|
+
}
|
|
406
|
+
prevProgress = progress
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
} finally {
|
|
411
|
+
sink.flush()
|
|
412
|
+
sink.close()
|
|
413
|
+
source.close()
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
OneKeyLog.info("AppUpdate", "Download completed")
|
|
417
|
+
sendEvent("update/downloaded")
|
|
418
|
+
|
|
419
|
+
notifyManager.cancel(NOTIFICATION_ID)
|
|
420
|
+
builder.setContentText("")
|
|
421
|
+
.setProgress(0, 0, false)
|
|
422
|
+
.setOngoing(false)
|
|
423
|
+
.setAutoCancel(true)
|
|
424
|
+
if (ActivityCompat.checkSelfPermission(
|
|
425
|
+
context, android.Manifest.permission.POST_NOTIFICATIONS
|
|
426
|
+
) == PackageManager.PERMISSION_GRANTED
|
|
427
|
+
) {
|
|
428
|
+
notifyManager.notify(NOTIFICATION_ID, builder.build())
|
|
429
|
+
}
|
|
430
|
+
} catch (e: Exception) {
|
|
431
|
+
OneKeyLog.error("AppUpdate", "downloadAPK: failed: ${e.javaClass.simpleName}: ${e.message}")
|
|
432
|
+
sendEvent("update/error", message = "${e.javaClass.simpleName}: ${e.message}")
|
|
433
|
+
throw e
|
|
434
|
+
} finally {
|
|
435
|
+
isDownloading.set(false)
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
override fun downloadASC(params: AppUpdateFileParams): Promise<Unit> {
|
|
441
|
+
return Promise.async {
|
|
442
|
+
val url = params.downloadUrl
|
|
443
|
+
val filePath = filePathFromUrl(url)
|
|
444
|
+
|
|
445
|
+
OneKeyLog.info("AppUpdate", "downloadASC: url=$url, filePath=$filePath")
|
|
446
|
+
|
|
447
|
+
if (!url.startsWith("https://")) {
|
|
448
|
+
OneKeyLog.error("AppUpdate", "downloadASC: URL is not HTTPS: $url")
|
|
449
|
+
throw Exception("Download URL must use HTTPS")
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
val ascFileUrl = "$url.SHA256SUMS.asc"
|
|
453
|
+
val ascFilePath = "$filePath.SHA256SUMS.asc"
|
|
454
|
+
OneKeyLog.info("AppUpdate", "downloadASC: ascFileUrl=$ascFileUrl")
|
|
455
|
+
|
|
456
|
+
val client = OkHttpClient.Builder()
|
|
457
|
+
.connectTimeout(10, TimeUnit.SECONDS)
|
|
458
|
+
.readTimeout(30, TimeUnit.SECONDS)
|
|
459
|
+
.followRedirects(false)
|
|
460
|
+
.followSslRedirects(false)
|
|
461
|
+
.build()
|
|
462
|
+
val request = Request.Builder().url(ascFileUrl).build()
|
|
463
|
+
val response = client.newCall(request).execute()
|
|
464
|
+
|
|
465
|
+
if (!response.isSuccessful) {
|
|
466
|
+
OneKeyLog.error("AppUpdate", "downloadASC: HTTP error, statusCode=${response.code}")
|
|
467
|
+
throw Exception(response.code.toString())
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
OneKeyLog.info("AppUpdate", "downloadASC: HTTP 200, reading ASC content...")
|
|
471
|
+
|
|
472
|
+
val content = StringBuilder()
|
|
473
|
+
val maxAscSize = 10 * 1024 // 10 KB max for ASC files
|
|
474
|
+
val body = response.body ?: throw Exception("Empty ASC response body")
|
|
475
|
+
BufferedReader(InputStreamReader(body.byteStream())).use { reader ->
|
|
476
|
+
var line: String?
|
|
477
|
+
while (reader.readLine().also { line = it } != null) {
|
|
478
|
+
content.append(line).append("\n")
|
|
479
|
+
if (content.length > maxAscSize) {
|
|
480
|
+
OneKeyLog.error("AppUpdate", "downloadASC: ASC file exceeds max size ($maxAscSize bytes)")
|
|
481
|
+
throw Exception("ASC file exceeds maximum allowed size")
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
val ascContent = content.toString()
|
|
487
|
+
if (ascContent.isEmpty()) {
|
|
488
|
+
OneKeyLog.error("AppUpdate", "downloadASC: ASC content is empty")
|
|
489
|
+
throw Exception("Empty ASC file")
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
OneKeyLog.info("AppUpdate", "downloadASC: ASC content size=${ascContent.length} bytes")
|
|
493
|
+
|
|
494
|
+
val ascFile = buildFile(ascFilePath)
|
|
495
|
+
if (ascFile.exists()) {
|
|
496
|
+
OneKeyLog.info("AppUpdate", "downloadASC: existing ASC file found, deleting...")
|
|
497
|
+
ascFile.delete()
|
|
498
|
+
}
|
|
499
|
+
FileOutputStream(ascFile).use { fos ->
|
|
500
|
+
fos.write(ascContent.toByteArray())
|
|
501
|
+
}
|
|
502
|
+
OneKeyLog.info("AppUpdate", "downloadASC: saved ASC file to $ascFilePath")
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
override fun verifyASC(params: AppUpdateFileParams): Promise<Unit> {
|
|
507
|
+
return Promise.async {
|
|
508
|
+
val filePath = filePathFromUrl(params.downloadUrl)
|
|
509
|
+
OneKeyLog.info("AppUpdate", "verifyASC: filePath=$filePath")
|
|
510
|
+
|
|
511
|
+
// Skip GPG verification when DevSettings (developer mode) is enabled
|
|
512
|
+
if (isDevSettingsEnabled()) {
|
|
513
|
+
OneKeyLog.warn("AppUpdate", "verifyASC: GPG verification skipped (DevSettings enabled)")
|
|
514
|
+
return@async
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
try {
|
|
518
|
+
|
|
519
|
+
val ascFilePath = "$filePath.SHA256SUMS.asc"
|
|
520
|
+
val ascFile = buildFile(ascFilePath)
|
|
521
|
+
if (!ascFile.exists()) {
|
|
522
|
+
OneKeyLog.error("AppUpdate", "verifyASC: ASC file not found at $ascFilePath")
|
|
523
|
+
throw Exception("ASC file not found")
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
val ascContent = ascFile.readText()
|
|
527
|
+
OneKeyLog.info("AppUpdate", "verifyASC: ASC file loaded, size=${ascContent.length} bytes")
|
|
528
|
+
|
|
529
|
+
if (!ascContent.contains("-----BEGIN PGP SIGNED MESSAGE-----")) {
|
|
530
|
+
OneKeyLog.error("AppUpdate", "verifyASC: ASC file missing PGP signed message header")
|
|
531
|
+
throw Exception("ASC file does not contain a PGP signed message")
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// Parse the cleartext signed message
|
|
535
|
+
val lines = ascContent.lines()
|
|
536
|
+
val hashHeaderIdx = lines.indexOfFirst { it.startsWith("Hash:") }
|
|
537
|
+
val sigStartIdx = lines.indexOfFirst { it == "-----BEGIN PGP SIGNATURE-----" }
|
|
538
|
+
val sigEndIdx = lines.indexOfFirst { it == "-----END PGP SIGNATURE-----" }
|
|
539
|
+
|
|
540
|
+
if (hashHeaderIdx < 0 || sigStartIdx < 0 || sigEndIdx < 0) {
|
|
541
|
+
OneKeyLog.error("AppUpdate", "verifyASC: invalid cleartext format (hashHeader=$hashHeaderIdx, sigStart=$sigStartIdx, sigEnd=$sigEndIdx)")
|
|
542
|
+
throw Exception("Invalid PGP cleartext signed message format")
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
OneKeyLog.info("AppUpdate", "verifyASC: parsed cleartext message structure OK")
|
|
546
|
+
|
|
547
|
+
val bodyStartIdx = hashHeaderIdx + 2
|
|
548
|
+
val bodyLines = lines.subList(bodyStartIdx, sigStartIdx)
|
|
549
|
+
val cleartextBody = bodyLines.joinToString("\r\n").trimEnd()
|
|
550
|
+
|
|
551
|
+
val sigBlock = lines.subList(sigStartIdx, sigEndIdx + 1).joinToString("\n")
|
|
552
|
+
|
|
553
|
+
// Parse signature
|
|
554
|
+
OneKeyLog.info("AppUpdate", "verifyASC: parsing PGP signature...")
|
|
555
|
+
val sigInputStream = PGPUtil.getDecoderStream(sigBlock.byteInputStream())
|
|
556
|
+
val sigFactory = JcaPGPObjectFactory(sigInputStream)
|
|
557
|
+
val signatureList = sigFactory.nextObject()
|
|
558
|
+
|
|
559
|
+
if (signatureList !is PGPSignatureList || signatureList.isEmpty) {
|
|
560
|
+
OneKeyLog.error("AppUpdate", "verifyASC: no PGP signature found in ASC file")
|
|
561
|
+
throw Exception("No PGP signature found in ASC file")
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
val pgpSignature = signatureList[0]
|
|
565
|
+
val keyId = pgpSignature.keyID
|
|
566
|
+
OneKeyLog.info("AppUpdate", "verifyASC: signature keyID=${java.lang.Long.toHexString(keyId).uppercase()}")
|
|
567
|
+
|
|
568
|
+
// Parse public key
|
|
569
|
+
OneKeyLog.info("AppUpdate", "verifyASC: loading GPG public key...")
|
|
570
|
+
val pubKeyStream = PGPUtil.getDecoderStream(GPG_PUBLIC_KEY.byteInputStream())
|
|
571
|
+
val pgpPubKeyRingCollection = PGPPublicKeyRingCollection(pubKeyStream, JcaKeyFingerprintCalculator())
|
|
572
|
+
val publicKey = pgpPubKeyRingCollection.getPublicKey(keyId)
|
|
573
|
+
if (publicKey == null) {
|
|
574
|
+
OneKeyLog.error("AppUpdate", "verifyASC: GPG public key not found for keyID=${java.lang.Long.toHexString(keyId).uppercase()}")
|
|
575
|
+
throw Exception("GPG public key not found for signature verification")
|
|
576
|
+
}
|
|
577
|
+
OneKeyLog.info("AppUpdate", "verifyASC: public key matched, verifying signature...")
|
|
578
|
+
|
|
579
|
+
// Verify signature
|
|
580
|
+
pgpSignature.init(JcaPGPContentVerifierBuilderProvider().setProvider(bcProvider), publicKey)
|
|
581
|
+
|
|
582
|
+
val unescapedLines = cleartextBody.lines().map { line ->
|
|
583
|
+
if (line.startsWith("- ")) line.substring(2) else line
|
|
584
|
+
}
|
|
585
|
+
val dataToVerify = unescapedLines.joinToString("\r\n").toByteArray(Charsets.UTF_8)
|
|
586
|
+
pgpSignature.update(dataToVerify)
|
|
587
|
+
|
|
588
|
+
if (!pgpSignature.verify()) {
|
|
589
|
+
OneKeyLog.error("AppUpdate", "verifyASC: GPG signature verification FAILED")
|
|
590
|
+
throw Exception("GPG signature verification failed for ASC file")
|
|
591
|
+
}
|
|
592
|
+
OneKeyLog.info("AppUpdate", "verifyASC: GPG signature verified OK")
|
|
593
|
+
|
|
594
|
+
// Extract SHA256 from cleartext (format: "<sha256hash> <filename>\n" or just "<sha256hash>")
|
|
595
|
+
val sha256 = cleartextBody.trim().split("\\s+".toRegex())[0].lowercase()
|
|
596
|
+
OneKeyLog.info("AppUpdate", "verifyASC: extracted SHA256=${sha256.take(16)}...")
|
|
597
|
+
|
|
598
|
+
if (sha256.length != 64 || !sha256.all { it in '0'..'9' || it in 'a'..'f' }) {
|
|
599
|
+
OneKeyLog.error("AppUpdate", "verifyASC: invalid SHA256 hash format (length=${sha256.length})")
|
|
600
|
+
throw Exception("Invalid SHA256 hash format in ASC file")
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
// Verify APK file SHA256
|
|
604
|
+
val apkFile = buildFile(filePath)
|
|
605
|
+
if (!apkFile.exists()) {
|
|
606
|
+
OneKeyLog.error("AppUpdate", "verifyASC: APK file not found at $filePath")
|
|
607
|
+
throw Exception("APK file not found: $filePath")
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
OneKeyLog.info("AppUpdate", "verifyASC: computing SHA256 of APK file (size=${apkFile.length()} bytes)...")
|
|
611
|
+
val fileSha256 = computeSha256(apkFile)
|
|
612
|
+
OneKeyLog.info("AppUpdate", "verifyASC: APK SHA256=${fileSha256.take(16)}...")
|
|
613
|
+
|
|
614
|
+
if (fileSha256 != sha256) {
|
|
615
|
+
OneKeyLog.error("AppUpdate", "verifyASC: SHA256 MISMATCH — expected=${sha256.take(16)}..., got=${fileSha256.take(16)}...")
|
|
616
|
+
throw Exception("SHA256 mismatch for APK file")
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
OneKeyLog.info("AppUpdate", "verifyASC: GPG signature + SHA256 verification passed")
|
|
620
|
+
|
|
621
|
+
} catch (e: Exception) {
|
|
622
|
+
OneKeyLog.error("AppUpdate", "verifyASC: failed: ${e.javaClass.simpleName}: ${e.message}")
|
|
623
|
+
throw e
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
// Track verified files with their SHA-256 hash to prevent TOCTOU attacks
|
|
629
|
+
private data class VerifiedFile(val canonicalPath: String, val sha256: String)
|
|
630
|
+
private val verifiedFiles = java.util.Collections.synchronizedMap(mutableMapOf<String, String>())
|
|
631
|
+
|
|
632
|
+
override fun verifyAPK(params: AppUpdateFileParams): Promise<Unit> {
|
|
633
|
+
return Promise.async {
|
|
634
|
+
val filePath = filePathFromUrl(params.downloadUrl)
|
|
635
|
+
OneKeyLog.info("AppUpdate", "verifyAPK: filePath=$filePath")
|
|
636
|
+
|
|
637
|
+
val file = buildFile(filePath)
|
|
638
|
+
if (!file.exists()) {
|
|
639
|
+
OneKeyLog.error("AppUpdate", "verifyAPK: APK file not found at $filePath")
|
|
640
|
+
throw Exception("NOT_FOUND_PACKAGE")
|
|
641
|
+
}
|
|
642
|
+
OneKeyLog.info("AppUpdate", "verifyAPK: APK file found, size=${file.length()} bytes")
|
|
643
|
+
|
|
644
|
+
val context = NitroModules.applicationContext
|
|
645
|
+
?: throw Exception("Application context unavailable")
|
|
646
|
+
val pm = context.packageManager
|
|
647
|
+
|
|
648
|
+
// Check package name
|
|
649
|
+
OneKeyLog.info("AppUpdate", "verifyAPK: parsing APK package info...")
|
|
650
|
+
val info = pm.getPackageArchiveInfo(file.absolutePath, 0)
|
|
651
|
+
if (info?.packageName == null) {
|
|
652
|
+
OneKeyLog.error("AppUpdate", "verifyAPK: failed to parse APK package info (INVALID_PACKAGE)")
|
|
653
|
+
throw Exception("INVALID_PACKAGE")
|
|
654
|
+
}
|
|
655
|
+
OneKeyLog.info("AppUpdate", "verifyAPK: APK packageName=${info.packageName}, versionName=${info.versionName}, versionCode=${if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) info.longVersionCode else @Suppress("DEPRECATION") info.versionCode}")
|
|
656
|
+
|
|
657
|
+
val debugBuild = isDebuggable()
|
|
658
|
+
|
|
659
|
+
// Check package name
|
|
660
|
+
if (info.packageName != context.packageName) {
|
|
661
|
+
OneKeyLog.error("AppUpdate", "verifyAPK: package name mismatch — APK=${info.packageName}, installed=${context.packageName}")
|
|
662
|
+
if (!debugBuild) throw Exception("PACKAGE_NAME_MISMATCH")
|
|
663
|
+
OneKeyLog.warn("AppUpdate", "verifyAPK: DEBUG build — ignoring package name mismatch")
|
|
664
|
+
} else {
|
|
665
|
+
OneKeyLog.info("AppUpdate", "verifyAPK: package name matches installed app")
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
// Verify APK signing certificate matches the installed app
|
|
669
|
+
OneKeyLog.info("AppUpdate", "verifyAPK: verifying APK signing certificate (API level=${Build.VERSION.SDK_INT})...")
|
|
670
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
|
671
|
+
val apkInfo = pm.getPackageArchiveInfo(file.absolutePath, PackageManager.GET_SIGNING_CERTIFICATES)
|
|
672
|
+
val installedInfo = pm.getPackageInfo(context.packageName, PackageManager.GET_SIGNING_CERTIFICATES)
|
|
673
|
+
val apkSigners = apkInfo?.signingInfo?.apkContentsSigners
|
|
674
|
+
val installedSigners = installedInfo?.signingInfo?.apkContentsSigners
|
|
675
|
+
if (apkSigners == null || installedSigners == null) {
|
|
676
|
+
OneKeyLog.error("AppUpdate", "verifyAPK: signing info unavailable (apkSigners=${apkSigners != null}, installedSigners=${installedSigners != null})")
|
|
677
|
+
if (!debugBuild) throw Exception("SIGNATURE_UNAVAILABLE")
|
|
678
|
+
OneKeyLog.warn("AppUpdate", "verifyAPK: DEBUG build — ignoring unavailable signatures")
|
|
679
|
+
} else {
|
|
680
|
+
OneKeyLog.info("AppUpdate", "verifyAPK: APK signers count=${apkSigners.size}, installed signers count=${installedSigners.size}")
|
|
681
|
+
if (apkSigners.toSet() != installedSigners.toSet()) {
|
|
682
|
+
OneKeyLog.error("AppUpdate", "verifyAPK: signing certificate MISMATCH")
|
|
683
|
+
if (!debugBuild) throw Exception("SIGNATURE_MISMATCH")
|
|
684
|
+
OneKeyLog.warn("AppUpdate", "verifyAPK: DEBUG build — ignoring certificate mismatch")
|
|
685
|
+
} else {
|
|
686
|
+
OneKeyLog.info("AppUpdate", "verifyAPK: signing certificate verified OK")
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
} else {
|
|
690
|
+
@Suppress("DEPRECATION")
|
|
691
|
+
val apkInfo = pm.getPackageArchiveInfo(file.absolutePath, PackageManager.GET_SIGNATURES)
|
|
692
|
+
@Suppress("DEPRECATION")
|
|
693
|
+
val installedInfo = pm.getPackageInfo(context.packageName, PackageManager.GET_SIGNATURES)
|
|
694
|
+
val apkSignatures = apkInfo?.signatures
|
|
695
|
+
val installedSignatures = installedInfo?.signatures
|
|
696
|
+
if (apkSignatures == null || installedSignatures == null) {
|
|
697
|
+
OneKeyLog.error("AppUpdate", "verifyAPK: legacy signatures unavailable")
|
|
698
|
+
if (!debugBuild) throw Exception("SIGNATURE_UNAVAILABLE")
|
|
699
|
+
OneKeyLog.warn("AppUpdate", "verifyAPK: DEBUG build — ignoring unavailable signatures")
|
|
700
|
+
} else {
|
|
701
|
+
OneKeyLog.info("AppUpdate", "verifyAPK: APK signatures count=${apkSignatures.size}, installed signatures count=${installedSignatures.size}")
|
|
702
|
+
if (apkSignatures.toSet() != installedSignatures.toSet()) {
|
|
703
|
+
OneKeyLog.error("AppUpdate", "verifyAPK: legacy signing certificate MISMATCH")
|
|
704
|
+
if (!debugBuild) throw Exception("SIGNATURE_MISMATCH")
|
|
705
|
+
OneKeyLog.warn("AppUpdate", "verifyAPK: DEBUG build — ignoring certificate mismatch")
|
|
706
|
+
} else {
|
|
707
|
+
OneKeyLog.info("AppUpdate", "verifyAPK: signing certificate verified OK")
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
// Compute SHA-256 hash of the verified file for TOCTOU protection
|
|
713
|
+
OneKeyLog.info("AppUpdate", "verifyAPK: computing SHA-256 hash for TOCTOU protection...")
|
|
714
|
+
val fileHash = computeSha256(file)
|
|
715
|
+
verifiedFiles[file.canonicalPath] = fileHash
|
|
716
|
+
OneKeyLog.info("AppUpdate", "verifyAPK: APK verified successfully, hash=${fileHash.take(16)}..., stored for install verification")
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
override fun installAPK(params: AppUpdateFileParams): Promise<Unit> {
|
|
721
|
+
return Promise.async {
|
|
722
|
+
val filePath = filePathFromUrl(params.downloadUrl)
|
|
723
|
+
OneKeyLog.info("AppUpdate", "installAPK: filePath=$filePath")
|
|
724
|
+
|
|
725
|
+
val context = NitroModules.applicationContext
|
|
726
|
+
?: throw Exception("Application context unavailable")
|
|
727
|
+
val file = buildFile(filePath)
|
|
728
|
+
if (!file.exists()) {
|
|
729
|
+
OneKeyLog.error("AppUpdate", "installAPK: APK file not found at $filePath")
|
|
730
|
+
throw Exception("NOT_FOUND_PACKAGE")
|
|
731
|
+
}
|
|
732
|
+
OneKeyLog.info("AppUpdate", "installAPK: APK file found, size=${file.length()} bytes")
|
|
733
|
+
|
|
734
|
+
// Ensure verifyAPK was called before installation
|
|
735
|
+
val debugBuild = isDebuggable()
|
|
736
|
+
val expectedHash = verifiedFiles.remove(file.canonicalPath)
|
|
737
|
+
if (expectedHash == null) {
|
|
738
|
+
OneKeyLog.error("AppUpdate", "installAPK: APK was not verified before installation (no hash in verifiedFiles)")
|
|
739
|
+
if (!debugBuild) throw Exception("APK must be verified before installation")
|
|
740
|
+
OneKeyLog.warn("AppUpdate", "installAPK: DEBUG build — ignoring missing verification")
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
// Re-verify file hash to prevent TOCTOU attacks (file swapped after verification)
|
|
744
|
+
if (expectedHash != null) {
|
|
745
|
+
OneKeyLog.info("AppUpdate", "installAPK: expected hash=${expectedHash.take(16)}..., re-verifying TOCTOU...")
|
|
746
|
+
val currentHash = computeSha256(file)
|
|
747
|
+
if (currentHash != expectedHash) {
|
|
748
|
+
OneKeyLog.error("AppUpdate", "installAPK: TOCTOU check FAILED — file was modified after verification (current=${currentHash.take(16)}..., expected=${expectedHash.take(16)}...)")
|
|
749
|
+
if (!debugBuild) throw Exception("APK file was modified after verification")
|
|
750
|
+
OneKeyLog.warn("AppUpdate", "installAPK: DEBUG build — ignoring TOCTOU mismatch")
|
|
751
|
+
} else {
|
|
752
|
+
OneKeyLog.info("AppUpdate", "installAPK: TOCTOU check passed, hash matches")
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
// Parse APK info for logging
|
|
757
|
+
val pm = context.packageManager
|
|
758
|
+
val apkInfo = pm.getPackageArchiveInfo(file.absolutePath, 0)
|
|
759
|
+
if (apkInfo != null) {
|
|
760
|
+
val versionCode = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) apkInfo.longVersionCode else @Suppress("DEPRECATION") apkInfo.versionCode.toLong()
|
|
761
|
+
OneKeyLog.info("AppUpdate", "installAPK: APK package=${apkInfo.packageName}, versionName=${apkInfo.versionName}, versionCode=$versionCode")
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
OneKeyLog.info("AppUpdate", "installAPK: creating install intent (API level=${Build.VERSION.SDK_INT})...")
|
|
765
|
+
val intent = Intent(Intent.ACTION_VIEW)
|
|
766
|
+
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
|
767
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
|
768
|
+
val apkUri = FileProvider.getUriForFile(
|
|
769
|
+
context,
|
|
770
|
+
"${context.packageName}.fileprovider",
|
|
771
|
+
file
|
|
772
|
+
)
|
|
773
|
+
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
|
774
|
+
intent.setDataAndType(apkUri, "application/vnd.android.package-archive")
|
|
775
|
+
OneKeyLog.info("AppUpdate", "installAPK: using FileProvider URI=$apkUri")
|
|
776
|
+
} else {
|
|
777
|
+
intent.setDataAndType(Uri.fromFile(file), "application/vnd.android.package-archive")
|
|
778
|
+
OneKeyLog.info("AppUpdate", "installAPK: using file URI")
|
|
779
|
+
}
|
|
780
|
+
context.startActivity(intent)
|
|
781
|
+
OneKeyLog.info("AppUpdate", "installAPK: install intent launched, awaiting user confirmation")
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
override fun clearCache(): Promise<Unit> {
|
|
786
|
+
return Promise.async {
|
|
787
|
+
OneKeyLog.info("AppUpdate", "clearCache: starting cleanup...")
|
|
788
|
+
isDownloading.set(false)
|
|
789
|
+
val verifiedCount = verifiedFiles.size
|
|
790
|
+
verifiedFiles.clear()
|
|
791
|
+
OneKeyLog.info("AppUpdate", "clearCache: reset download state, cleared $verifiedCount verified file entries")
|
|
792
|
+
|
|
793
|
+
// Clean up downloaded APK and ASC files from cacheDir/apks/ directory
|
|
794
|
+
val context = NitroModules.applicationContext
|
|
795
|
+
if (context != null) {
|
|
796
|
+
val apkDir = File(context.cacheDir, "apks")
|
|
797
|
+
if (apkDir.exists()) {
|
|
798
|
+
val filesToDelete = apkDir.listFiles() ?: emptyArray()
|
|
799
|
+
OneKeyLog.info("AppUpdate", "clearCache: found ${filesToDelete.size} cached file(s) to delete in ${apkDir.absolutePath}")
|
|
800
|
+
var deletedCount = 0
|
|
801
|
+
filesToDelete.forEach { file ->
|
|
802
|
+
val size = file.length()
|
|
803
|
+
if (file.delete()) {
|
|
804
|
+
OneKeyLog.debug("AppUpdate", "clearCache: deleted ${file.name} (${size} bytes)")
|
|
805
|
+
deletedCount++
|
|
806
|
+
} else {
|
|
807
|
+
OneKeyLog.warn("AppUpdate", "clearCache: failed to delete ${file.name}")
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
OneKeyLog.info("AppUpdate", "clearCache: completed, deleted $deletedCount/${filesToDelete.size} files")
|
|
811
|
+
} else {
|
|
812
|
+
OneKeyLog.info("AppUpdate", "clearCache: apks cache directory does not exist, nothing to clean")
|
|
813
|
+
}
|
|
814
|
+
} else {
|
|
815
|
+
OneKeyLog.warn("AppUpdate", "clearCache: application context unavailable, skipping file cleanup")
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
}
|