@onekeyfe/react-native-app-update 1.1.22 → 1.1.24

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.
@@ -5,7 +5,7 @@
5
5
  <application>
6
6
  <provider
7
7
  android:name="androidx.core.content.FileProvider"
8
- android:authorities="${applicationId}.fileprovider"
8
+ android:authorities="${applicationId}.appupdate.fileprovider"
9
9
  android:exported="false"
10
10
  android:grantUriPermissions="true">
11
11
  <meta-data
@@ -202,23 +202,6 @@ class ReactNativeAppUpdate : HybridReactNativeAppUpdateSpec() {
202
202
  return bytesToHex(digest.digest())
203
203
  }
204
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
205
  /**
223
206
  * Download ASC file for the given URL and save to the given path.
224
207
  * Returns the saved file, or null on failure.
@@ -251,7 +234,63 @@ class ReactNativeAppUpdate : HybridReactNativeAppUpdateSpec() {
251
234
  }
252
235
 
253
236
  /**
254
- * Check if an existing APK is valid by verifying its SHA256 against the ASC file.
237
+ * Verify GPG signature of an ASC file and extract the SHA256 hash.
238
+ * Returns the SHA256 hash if signature is valid, null otherwise.
239
+ */
240
+ private fun verifyAscAndExtractSha256(ascFile: File): String? {
241
+ val ascContent = ascFile.readText()
242
+ if (!ascContent.contains("-----BEGIN PGP SIGNED MESSAGE-----")) return null
243
+
244
+ val lines = ascContent.lines()
245
+ val hashHeaderIdx = lines.indexOfFirst { it.startsWith("Hash:") }
246
+ val sigStartIdx = lines.indexOfFirst { it == "-----BEGIN PGP SIGNATURE-----" }
247
+ val sigEndIdx = lines.indexOfFirst { it == "-----END PGP SIGNATURE-----" }
248
+ if (hashHeaderIdx < 0 || sigStartIdx < 0 || sigEndIdx < 0) return null
249
+
250
+ val bodyStartIdx = hashHeaderIdx + 2
251
+ val bodyLines = lines.subList(bodyStartIdx, sigStartIdx)
252
+ val cleartextBody = bodyLines.joinToString("\r\n").trimEnd()
253
+ val sigBlock = lines.subList(sigStartIdx, sigEndIdx + 1).joinToString("\n")
254
+
255
+ // Verify GPG signature
256
+ val sigInputStream = PGPUtil.getDecoderStream(sigBlock.byteInputStream())
257
+ val sigFactory = JcaPGPObjectFactory(sigInputStream)
258
+ val signatureList = sigFactory.nextObject()
259
+ if (signatureList !is PGPSignatureList || signatureList.isEmpty) return null
260
+
261
+ val pgpSignature = signatureList[0]
262
+ val pubKeyStream = PGPUtil.getDecoderStream(GPG_PUBLIC_KEY.byteInputStream())
263
+ val pgpPubKeyRingCollection = PGPPublicKeyRingCollection(pubKeyStream, JcaKeyFingerprintCalculator())
264
+ val publicKey = pgpPubKeyRingCollection.getPublicKey(pgpSignature.keyID) ?: return null
265
+
266
+ pgpSignature.init(JcaPGPContentVerifierBuilderProvider().setProvider(bcProvider), publicKey)
267
+ val unescapedLines = cleartextBody.lines().map { line ->
268
+ if (line.startsWith("- ")) line.substring(2) else line
269
+ }
270
+ val dataToVerify = unescapedLines.joinToString("\r\n").toByteArray(Charsets.UTF_8)
271
+ pgpSignature.update(dataToVerify)
272
+ if (!pgpSignature.verify()) return null
273
+
274
+ // Extract SHA256 from verified cleartext
275
+ val sha256 = cleartextBody.trim().split("\\s+".toRegex())[0].lowercase()
276
+ if (sha256.length != 64 || !sha256.all { it in '0'..'9' || it in 'a'..'f' }) return null
277
+ return sha256
278
+ }
279
+
280
+ /** Constant-time comparison to prevent timing attacks on hash values */
281
+ private fun secureCompare(a: String, b: String): Boolean {
282
+ val aBytes = a.toByteArray(Charsets.UTF_8)
283
+ val bBytes = b.toByteArray(Charsets.UTF_8)
284
+ if (aBytes.size != bBytes.size) return false
285
+ var result = 0
286
+ for (i in aBytes.indices) {
287
+ result = result or (aBytes[i].toInt() xor bBytes[i].toInt())
288
+ }
289
+ return result == 0
290
+ }
291
+
292
+ /**
293
+ * Check if an existing APK is valid by verifying GPG signature and SHA256 against the ASC file.
255
294
  * Downloads ASC if not present. Returns true if APK is valid.
256
295
  */
257
296
  private fun tryVerifyExistingApk(url: String, filePath: String, apkFile: File): Boolean {
@@ -269,16 +308,16 @@ class ReactNativeAppUpdate : HybridReactNativeAppUpdateSpec() {
269
308
  OneKeyLog.info("AppUpdate", "tryVerifyExistingApk: ASC downloaded to ${ascFile.absolutePath}")
270
309
  }
271
310
 
272
- val expectedSha256 = extractSha256FromAsc(ascFile)
311
+ val expectedSha256 = verifyAscAndExtractSha256(ascFile)
273
312
  if (expectedSha256 == null) {
274
- OneKeyLog.warn("AppUpdate", "tryVerifyExistingApk: failed to extract SHA256 from ASC")
313
+ OneKeyLog.warn("AppUpdate", "tryVerifyExistingApk: GPG verification or SHA256 extraction failed")
275
314
  return false
276
315
  }
277
316
 
278
317
  OneKeyLog.info("AppUpdate", "tryVerifyExistingApk: computing SHA256 of existing APK (size=${apkFile.length()})...")
279
318
  val actualSha256 = computeSha256(apkFile)
280
319
 
281
- if (actualSha256 == expectedSha256) {
320
+ if (secureCompare(actualSha256, expectedSha256)) {
282
321
  OneKeyLog.info("AppUpdate", "tryVerifyExistingApk: SHA256 matches, APK is valid")
283
322
  true
284
323
  } else {
@@ -301,13 +340,25 @@ class ReactNativeAppUpdate : HybridReactNativeAppUpdateSpec() {
301
340
  return try {
302
341
  val context = NitroModules.applicationContext ?: return false
303
342
  MMKV.initialize(context)
304
- val mmkv = MMKV.mmkvWithID("onekey-app-setting") ?: return false
343
+ val mmkv = MMKV.mmkvWithID("onekey-app-dev-setting") ?: return false
305
344
  mmkv.decodeBool("onekey_developer_mode_enabled", false)
306
345
  } catch (e: Exception) {
307
346
  false
308
347
  }
309
348
  }
310
349
 
350
+ /** Returns true if the skip-GPG-verification toggle is enabled via MMKV storage. */
351
+ private fun isSkipGPGEnabled(): Boolean {
352
+ return try {
353
+ val context = NitroModules.applicationContext ?: return false
354
+ MMKV.initialize(context)
355
+ val mmkv = MMKV.mmkvWithID("onekey-app-dev-setting") ?: return false
356
+ mmkv.decodeBool("onekey_bundle_skip_gpg_verification", false)
357
+ } catch (e: Exception) {
358
+ false
359
+ }
360
+ }
361
+
311
362
  override fun downloadAPK(params: AppUpdateDownloadParams): Promise<Unit> {
312
363
  return Promise.async {
313
364
  if (isDownloading.getAndSet(true)) {
@@ -508,9 +559,12 @@ class ReactNativeAppUpdate : HybridReactNativeAppUpdateSpec() {
508
559
  val filePath = filePathFromUrl(params.downloadUrl)
509
560
  OneKeyLog.info("AppUpdate", "verifyASC: filePath=$filePath")
510
561
 
511
- // Skip GPG verification when DevSettings (developer mode) is enabled
512
- if (isDevSettingsEnabled()) {
513
- OneKeyLog.warn("AppUpdate", "verifyASC: GPG verification skipped (DevSettings enabled)")
562
+ // Skip GPG verification only when both DevSettings and skip-GPG toggle are enabled
563
+ val devSettings = isDevSettingsEnabled()
564
+ val skipGPGToggle = isSkipGPGEnabled()
565
+ OneKeyLog.info("AppUpdate", "verifyASC: GPG check: devSettings=$devSettings, skipGPGToggle=$skipGPGToggle")
566
+ if (devSettings && skipGPGToggle) {
567
+ OneKeyLog.warn("AppUpdate", "verifyASC: GPG verification skipped (DevSettings + skip-GPG enabled)")
514
568
  return@async
515
569
  }
516
570
 
@@ -611,7 +665,7 @@ class ReactNativeAppUpdate : HybridReactNativeAppUpdateSpec() {
611
665
  val fileSha256 = computeSha256(apkFile)
612
666
  OneKeyLog.info("AppUpdate", "verifyASC: APK SHA256=${fileSha256.take(16)}...")
613
667
 
614
- if (fileSha256 != sha256) {
668
+ if (!secureCompare(fileSha256, sha256)) {
615
669
  OneKeyLog.error("AppUpdate", "verifyASC: SHA256 MISMATCH — expected=${sha256.take(16)}..., got=${fileSha256.take(16)}...")
616
670
  throw Exception("SHA256 mismatch for APK file")
617
671
  }
@@ -767,7 +821,7 @@ class ReactNativeAppUpdate : HybridReactNativeAppUpdateSpec() {
767
821
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
768
822
  val apkUri = FileProvider.getUriForFile(
769
823
  context,
770
- "${context.packageName}.fileprovider",
824
+ "${context.packageName}.appupdate.fileprovider",
771
825
  file
772
826
  )
773
827
  intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
@@ -3,6 +3,8 @@ import ReactNativeNativeLogger
3
3
 
4
4
  class ReactNativeAppUpdate: HybridReactNativeAppUpdateSpec {
5
5
 
6
+ private var nextListenerId: Int64 = 1
7
+
6
8
  func downloadAPK(params: AppUpdateDownloadParams) throws -> Promise<Void> {
7
9
  OneKeyLog.debug("AppUpdate", "downloadAPK not available on iOS")
8
10
  return Promise.resolved(withResult: ())
@@ -33,7 +35,9 @@ class ReactNativeAppUpdate: HybridReactNativeAppUpdateSpec {
33
35
  }
34
36
 
35
37
  func addDownloadListener(callback: @escaping (DownloadEvent) -> Void) throws -> Double {
36
- return 0
38
+ let id = nextListenerId
39
+ nextListenerId += 1
40
+ return Double(id)
37
41
  }
38
42
 
39
43
  func removeDownloadListener(id: Double) throws {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onekeyfe/react-native-app-update",
3
- "version": "1.1.22",
3
+ "version": "1.1.24",
4
4
  "description": "react-native-app-update",
5
5
  "main": "./lib/module/index.js",
6
6
  "types": "./lib/typescript/src/index.d.ts",
package/README.md DELETED
@@ -1,36 +0,0 @@
1
- # react-native-app-update
2
-
3
- react-native-app-update
4
-
5
- ## Installation
6
-
7
- ```sh
8
- npm install react-native-app-update react-native-nitro-modules
9
-
10
- > `react-native-nitro-modules` is required as this library relies on [Nitro Modules](https://nitro.margelo.com/).
11
- ```
12
-
13
- ## Usage
14
-
15
- ```js
16
- import { ReactNativeAppUpdate } from 'react-native-app-update';
17
-
18
- // ...
19
-
20
- const result = await ReactNativeAppUpdate.hello({ message: 'World' });
21
- console.log(result); // { success: true, data: 'Hello, World!' }
22
- ```
23
-
24
- ## Contributing
25
-
26
- - [Development workflow](CONTRIBUTING.md#development-workflow)
27
- - [Sending a pull request](CONTRIBUTING.md#sending-a-pull-request)
28
- - [Code of conduct](CODE_OF_CONDUCT.md)
29
-
30
- ## License
31
-
32
- MIT
33
-
34
- ---
35
-
36
- Made with [create-react-native-library](https://github.com/callstack/react-native-builder-bob)