@onekeyfe/react-native-app-update 1.1.21 → 1.1.23
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/android/src/main/java/com/margelo/nitro/reactnativeappupdate/ReactNativeAppUpdate.kt
CHANGED
|
@@ -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
|
-
*
|
|
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 =
|
|
311
|
+
val expectedSha256 = verifyAscAndExtractSha256(ascFile)
|
|
273
312
|
if (expectedSha256 == null) {
|
|
274
|
-
OneKeyLog.warn("AppUpdate", "tryVerifyExistingApk:
|
|
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
|
|
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
|
|
512
|
-
|
|
513
|
-
|
|
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
|
|
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
|
}
|
|
@@ -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
|
-
|
|
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
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)
|