@onekeyfe/react-native-bundle-crypto 3.0.39

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +36 -0
  3. package/ReactNativeBundleCrypto.podspec +35 -0
  4. package/android/CMakeLists.txt +24 -0
  5. package/android/build.gradle +136 -0
  6. package/android/gradle.properties +4 -0
  7. package/android/src/main/AndroidManifest.xml +1 -0
  8. package/android/src/main/cpp/cpp-adapter.cpp +6 -0
  9. package/android/src/main/java/com/margelo/nitro/reactnativebundlecrypto/BundleCryptoCore.kt +526 -0
  10. package/android/src/main/java/com/margelo/nitro/reactnativebundlecrypto/ReactNativeBundleCrypto.kt +110 -0
  11. package/android/src/main/java/com/margelo/nitro/reactnativebundlecrypto/ReactNativeBundleCryptoPackage.kt +24 -0
  12. package/ios/BundleCryptoCore.swift +529 -0
  13. package/ios/Frameworks/Gopenpgp.xcframework/Info.plist +52 -0
  14. package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64/Gopenpgp.framework/Gopenpgp +0 -0
  15. package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64/Gopenpgp.framework/Headers/Armor.objc.h +96 -0
  16. package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64/Gopenpgp.framework/Headers/Constants.objc.h +197 -0
  17. package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64/Gopenpgp.framework/Headers/Crypto.objc.h +1963 -0
  18. package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64/Gopenpgp.framework/Headers/Gopenpgp.h +23 -0
  19. package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64/Gopenpgp.framework/Headers/Mime.objc.h +59 -0
  20. package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64/Gopenpgp.framework/Headers/Mobile.objc.h +252 -0
  21. package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64/Gopenpgp.framework/Headers/Profile.objc.h +107 -0
  22. package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64/Gopenpgp.framework/Headers/Universe.objc.h +29 -0
  23. package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64/Gopenpgp.framework/Headers/ref.h +35 -0
  24. package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64/Gopenpgp.framework/Info.plist +20 -0
  25. package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64/Gopenpgp.framework/Modules/module.modulemap +13 -0
  26. package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64_x86_64-simulator/Gopenpgp.framework/Gopenpgp +0 -0
  27. package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64_x86_64-simulator/Gopenpgp.framework/Headers/Armor.objc.h +96 -0
  28. package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64_x86_64-simulator/Gopenpgp.framework/Headers/Constants.objc.h +197 -0
  29. package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64_x86_64-simulator/Gopenpgp.framework/Headers/Crypto.objc.h +1963 -0
  30. package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64_x86_64-simulator/Gopenpgp.framework/Headers/Gopenpgp.h +23 -0
  31. package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64_x86_64-simulator/Gopenpgp.framework/Headers/Mime.objc.h +59 -0
  32. package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64_x86_64-simulator/Gopenpgp.framework/Headers/Mobile.objc.h +252 -0
  33. package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64_x86_64-simulator/Gopenpgp.framework/Headers/Profile.objc.h +107 -0
  34. package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64_x86_64-simulator/Gopenpgp.framework/Headers/Universe.objc.h +29 -0
  35. package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64_x86_64-simulator/Gopenpgp.framework/Headers/ref.h +35 -0
  36. package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64_x86_64-simulator/Gopenpgp.framework/Info.plist +20 -0
  37. package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64_x86_64-simulator/Gopenpgp.framework/Modules/module.modulemap +13 -0
  38. package/ios/ReactNativeBundleCrypto.swift +104 -0
  39. package/lib/module/ReactNativeBundleCrypto.nitro.js +4 -0
  40. package/lib/module/ReactNativeBundleCrypto.nitro.js.map +1 -0
  41. package/lib/module/index.js +6 -0
  42. package/lib/module/index.js.map +1 -0
  43. package/lib/module/package.json +1 -0
  44. package/lib/typescript/package.json +1 -0
  45. package/lib/typescript/src/ReactNativeBundleCrypto.nitro.d.ts +35 -0
  46. package/lib/typescript/src/ReactNativeBundleCrypto.nitro.d.ts.map +1 -0
  47. package/lib/typescript/src/index.d.ts +4 -0
  48. package/lib/typescript/src/index.d.ts.map +1 -0
  49. package/nitro.json +17 -0
  50. package/nitrogen/generated/android/c++/JDirHashEntry.hpp +61 -0
  51. package/nitrogen/generated/android/c++/JHybridReactNativeBundleCryptoSpec.cpp +240 -0
  52. package/nitrogen/generated/android/c++/JHybridReactNativeBundleCryptoSpec.hpp +74 -0
  53. package/nitrogen/generated/android/c++/JSha256Result.hpp +62 -0
  54. package/nitrogen/generated/android/c++/JVerifyDetachedAscResult.hpp +66 -0
  55. package/nitrogen/generated/android/c++/JVerifyGpgCleartextResult.hpp +66 -0
  56. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativebundlecrypto/DirHashEntry.kt +41 -0
  57. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativebundlecrypto/HybridReactNativeBundleCryptoSpec.kt +94 -0
  58. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativebundlecrypto/Sha256Result.kt +41 -0
  59. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativebundlecrypto/VerifyDetachedAscResult.kt +44 -0
  60. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativebundlecrypto/VerifyGpgCleartextResult.kt +44 -0
  61. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativebundlecrypto/reactnativebundlecryptoOnLoad.kt +35 -0
  62. package/nitrogen/generated/android/reactnativebundlecrypto+autolinking.cmake +81 -0
  63. package/nitrogen/generated/android/reactnativebundlecrypto+autolinking.gradle +27 -0
  64. package/nitrogen/generated/android/reactnativebundlecryptoOnLoad.cpp +44 -0
  65. package/nitrogen/generated/android/reactnativebundlecryptoOnLoad.hpp +25 -0
  66. package/nitrogen/generated/ios/ReactNativeBundleCrypto+autolinking.rb +60 -0
  67. package/nitrogen/generated/ios/ReactNativeBundleCrypto-Swift-Cxx-Bridge.cpp +97 -0
  68. package/nitrogen/generated/ios/ReactNativeBundleCrypto-Swift-Cxx-Bridge.hpp +429 -0
  69. package/nitrogen/generated/ios/ReactNativeBundleCrypto-Swift-Cxx-Umbrella.hpp +59 -0
  70. package/nitrogen/generated/ios/ReactNativeBundleCryptoAutolinking.mm +33 -0
  71. package/nitrogen/generated/ios/ReactNativeBundleCryptoAutolinking.swift +25 -0
  72. package/nitrogen/generated/ios/c++/HybridReactNativeBundleCryptoSpecSwift.cpp +11 -0
  73. package/nitrogen/generated/ios/c++/HybridReactNativeBundleCryptoSpecSwift.hpp +162 -0
  74. package/nitrogen/generated/ios/swift/DirHashEntry.swift +47 -0
  75. package/nitrogen/generated/ios/swift/Func_void.swift +47 -0
  76. package/nitrogen/generated/ios/swift/Func_void_Sha256Result.swift +47 -0
  77. package/nitrogen/generated/ios/swift/Func_void_VerifyDetachedAscResult.swift +47 -0
  78. package/nitrogen/generated/ios/swift/Func_void_VerifyGpgCleartextResult.swift +47 -0
  79. package/nitrogen/generated/ios/swift/Func_void_bool.swift +47 -0
  80. package/nitrogen/generated/ios/swift/Func_void_std__exception_ptr.swift +47 -0
  81. package/nitrogen/generated/ios/swift/Func_void_std__vector_DirHashEntry_.swift +47 -0
  82. package/nitrogen/generated/ios/swift/Func_void_std__vector_std__string_.swift +47 -0
  83. package/nitrogen/generated/ios/swift/HybridReactNativeBundleCryptoSpec.swift +65 -0
  84. package/nitrogen/generated/ios/swift/HybridReactNativeBundleCryptoSpec_cxx.swift +314 -0
  85. package/nitrogen/generated/ios/swift/Sha256Result.swift +85 -0
  86. package/nitrogen/generated/ios/swift/VerifyDetachedAscResult.swift +96 -0
  87. package/nitrogen/generated/ios/swift/VerifyGpgCleartextResult.swift +96 -0
  88. package/nitrogen/generated/shared/c++/DirHashEntry.hpp +79 -0
  89. package/nitrogen/generated/shared/c++/HybridReactNativeBundleCryptoSpec.cpp +30 -0
  90. package/nitrogen/generated/shared/c++/HybridReactNativeBundleCryptoSpec.hpp +84 -0
  91. package/nitrogen/generated/shared/c++/Sha256Result.hpp +80 -0
  92. package/nitrogen/generated/shared/c++/VerifyDetachedAscResult.hpp +84 -0
  93. package/nitrogen/generated/shared/c++/VerifyGpgCleartextResult.hpp +84 -0
  94. package/package.json +169 -0
  95. package/src/ReactNativeBundleCrypto.nitro.ts +56 -0
  96. package/src/index.tsx +8 -0
@@ -0,0 +1,526 @@
1
+ package com.margelo.nitro.reactnativebundlecrypto
2
+
3
+ import com.margelo.nitro.nativelogger.OneKeyLog
4
+ import java.io.BufferedInputStream
5
+ import java.io.File
6
+ import java.io.FileInputStream
7
+ import java.nio.file.Files
8
+ import java.security.MessageDigest
9
+ import org.bouncycastle.jce.provider.BouncyCastleProvider
10
+ import org.bouncycastle.openpgp.PGPPublicKeyRingCollection
11
+ import org.bouncycastle.openpgp.PGPSignatureList
12
+ import org.bouncycastle.openpgp.PGPUtil
13
+ import org.bouncycastle.openpgp.jcajce.JcaPGPObjectFactory
14
+ import org.bouncycastle.openpgp.operator.jcajce.JcaKeyFingerprintCalculator
15
+ import org.bouncycastle.openpgp.operator.jcajce.JcaPGPContentVerifierBuilderProvider
16
+ import org.json.JSONObject
17
+
18
+ // ============================================================================
19
+ // BundleCryptoCore (Android)
20
+ //
21
+ // P3a refactor: the real BouncyCastle GPG cleartext verify, detached ASC verify,
22
+ // java MessageDigest sha256 (with failureReason taxonomy), per-entry dir hashing,
23
+ // constant-time compare, symlink/traversal guard, and safe file ops now live
24
+ // here as plain functions returning plain Kotlin types (data class / Boolean /
25
+ // List). This is the single source of truth for the security logic — the Nitro
26
+ // HybridObject (ReactNativeBundleCrypto) is a thin shell that calls these and
27
+ // assembles the generated Nitro Result structs.
28
+ //
29
+ // This split lets other native code (e.g. the bundle-update downloader) call the
30
+ // security core directly and synchronously, without going through a Nitro Promise.
31
+ //
32
+ // Behavior is IDENTICAL to the previous in-method implementation:
33
+ // - Verification failures always return invalid + reason — never a false pass.
34
+ // - sha256 failureReason taxonomy preserved verbatim (FILE_NOT_FOUND /
35
+ // FILE_DISAPPEARED / FILE_TRUNCATED / PERMISSION_DENIED / OOM / IO_<class> /
36
+ // UNEXPECTED_<class>).
37
+ //
38
+ // Single source of truth for the OneKey GPG public key + BouncyCastle provider
39
+ // lives here. This module does NOT expose the public key to JS.
40
+ //
41
+ // Cleartext form (verifyGpgCleartext) <- ReactNativeBundleUpdate.kt
42
+ // verifyGPGAndExtractSha256
43
+ // (body JSON -> sha256)
44
+ // Detached form (verifyDetachedAsc) <- ReactNativeAppUpdate.kt
45
+ // verifyAscContentAndExtractSha256
46
+ // (cleartext-signed SHA256SUMS, first
47
+ // hex token)
48
+ // Both forms are PGP CLEARTEXT SIGNED MESSAGEs; they share the same manual
49
+ // BouncyCastle cleartext parsing and differ only in body extraction.
50
+ // ============================================================================
51
+
52
+ // OneKey GPG public key for signature verification.
53
+ // SINGLE AUTHORITATIVE COPY for the bundle-crypto module.
54
+ private const val GPG_PUBLIC_KEY = """-----BEGIN PGP PUBLIC KEY BLOCK-----
55
+
56
+ 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
116
+ =HlTQ
117
+ -----END PGP PUBLIC KEY BLOCK-----"""
118
+
119
+ /**
120
+ * Plain security core for the bundle-crypto module. The Nitro HybridObject
121
+ * delegates to these functions. Functions return plain Kotlin types and throw
122
+ * on hard failures; verification helpers always fail safe (invalid + reason).
123
+ */
124
+ object BundleCryptoCore {
125
+
126
+ // Single BouncyCastle provider instance for this module (mirrors source).
127
+ private val bcProvider = BouncyCastleProvider()
128
+
129
+ // Plain value type mirroring the (relativePath, sha256) pair the Nitro layer
130
+ // exposes as `DirHashEntry`. Kept separate so the core has zero Nitro deps.
131
+ data class DirHash(val relativePath: String, val sha256: String)
132
+
133
+ // Plain result of a GPG/ASC cleartext verification.
134
+ data class VerifyResult(val valid: Boolean, val sha256: String?, val reason: String?)
135
+
136
+ // Plain result of a sha256 computation (sha256 xor failureReason).
137
+ data class Sha256ResultData(val sha256: String?, val failureReason: String?)
138
+
139
+ // MARK: - GPG cleartext (bundle form): body JSON -> sha256
140
+ // Ported verbatim from ReactNativeBundleUpdate.verifyGPGAndExtractSha256.
141
+
142
+ fun verifyGpgCleartext(signedMessage: String): VerifyResult {
143
+ if (!signedMessage.contains("-----BEGIN PGP SIGNED MESSAGE-----")) {
144
+ return VerifyResult(false, null, "NOT_PGP_SIGNED_MESSAGE")
145
+ }
146
+
147
+ return try {
148
+ // Parse the public key
149
+ val pubKeyStream = PGPUtil.getDecoderStream(GPG_PUBLIC_KEY.byteInputStream())
150
+ val pgpPubKeyRingCollection = PGPPublicKeyRingCollection(pubKeyStream, JcaKeyFingerprintCalculator())
151
+
152
+ // Extract cleartext and signature from the PGP signed message manually
153
+ // (BouncyCastle's cleartext handling requires manual parsing).
154
+ val lines = signedMessage.lines()
155
+ val hashHeaderIdx = lines.indexOfFirst { it.startsWith("Hash:") }
156
+ val sigStartIdx = lines.indexOfFirst { it == "-----BEGIN PGP SIGNATURE-----" }
157
+ val sigEndIdx = lines.indexOfFirst { it == "-----END PGP SIGNATURE-----" }
158
+
159
+ if (hashHeaderIdx < 0 || sigStartIdx < 0 || sigEndIdx < 0) {
160
+ OneKeyLog.error("BundleCrypto", "Invalid PGP cleartext signed message format")
161
+ return VerifyResult(false, null, "INVALID_FORMAT")
162
+ }
163
+
164
+ // The cleartext body is between the Hash header blank line and the PGP SIGNATURE block.
165
+ val bodyStartIdx = hashHeaderIdx + 2 // skip Hash: line and the blank line after it
166
+ val bodyLines = lines.subList(bodyStartIdx, sigStartIdx)
167
+ // Remove trailing empty line that PGP adds.
168
+ val cleartextBody = bodyLines.joinToString("\r\n").trimEnd()
169
+
170
+ // The signature block.
171
+ val sigBlock = lines.subList(sigStartIdx, sigEndIdx + 1).joinToString("\n")
172
+
173
+ // Decode the signature.
174
+ val sigInputStream = PGPUtil.getDecoderStream(sigBlock.byteInputStream())
175
+ val sigFactory = JcaPGPObjectFactory(sigInputStream)
176
+ val signatureList = sigFactory.nextObject()
177
+
178
+ if (signatureList !is PGPSignatureList || signatureList.isEmpty) {
179
+ OneKeyLog.error("BundleCrypto", "No PGP signature found in message")
180
+ return VerifyResult(false, null, "NO_SIGNATURE")
181
+ }
182
+
183
+ val pgpSignature = signatureList[0]
184
+ val keyId = pgpSignature.keyID
185
+
186
+ // Find the public key.
187
+ val publicKey = pgpPubKeyRingCollection.getPublicKey(keyId)
188
+ if (publicKey == null) {
189
+ OneKeyLog.error("BundleCrypto", "Public key not found for keyId: ${java.lang.Long.toHexString(keyId)}")
190
+ return VerifyResult(false, null, "PUBKEY_NOT_FOUND")
191
+ }
192
+
193
+ // Verify the signature.
194
+ pgpSignature.init(JcaPGPContentVerifierBuilderProvider().setProvider(bcProvider), publicKey)
195
+
196
+ // Dash-unescape the cleartext per RFC 4880 Section 7.1.
197
+ val unescapedLines = cleartextBody.lines().map { line ->
198
+ if (line.startsWith("- ")) line.substring(2) else line
199
+ }
200
+ val dataToVerify = unescapedLines.joinToString("\r\n").toByteArray(Charsets.UTF_8)
201
+ pgpSignature.update(dataToVerify)
202
+
203
+ if (!pgpSignature.verify()) {
204
+ OneKeyLog.error("BundleCrypto", "GPG signature verification failed")
205
+ return VerifyResult(false, null, "SIGNATURE_INVALID")
206
+ }
207
+
208
+ // Signature verified, parse the cleartext JSON.
209
+ val json = JSONObject(cleartextBody.trim())
210
+ val sha256 = json.optString("sha256", "")
211
+ if (sha256.isEmpty()) {
212
+ OneKeyLog.error("BundleCrypto", "No sha256 field in signed cleartext JSON")
213
+ return VerifyResult(false, null, "NO_SHA256_FIELD")
214
+ }
215
+
216
+ OneKeyLog.info("BundleCrypto", "GPG cleartext verification succeeded")
217
+ VerifyResult(true, sha256, null)
218
+ } catch (e: Exception) {
219
+ OneKeyLog.error("BundleCrypto", "GPG verification error: ${e.javaClass.simpleName}: ${e.message}")
220
+ VerifyResult(false, null, "ERROR_${e.javaClass.simpleName}")
221
+ }
222
+ }
223
+
224
+ // MARK: - Detached ASC (APK form): cleartext-signed SHA256SUMS -> first hex token
225
+ // Ported verbatim from ReactNativeAppUpdate.verifyAscContentAndExtractSha256.
226
+ // The on-disk SHA256SUMS.asc is itself a PGP CLEARTEXT SIGNED MESSAGE; the
227
+ // only difference from verifyGpgCleartext is the body parsing (no JSON — first
228
+ // whitespace token must be a 64-char lowercase hex sha256).
229
+
230
+ fun verifyDetachedAsc(ascContent: String): VerifyResult {
231
+ if (!ascContent.contains("-----BEGIN PGP SIGNED MESSAGE-----")) {
232
+ return VerifyResult(false, null, "NOT_PGP_SIGNED_MESSAGE")
233
+ }
234
+
235
+ return try {
236
+ val lines = ascContent.lines()
237
+ val hashHeaderIdx = lines.indexOfFirst { it.startsWith("Hash:") }
238
+ val sigStartIdx = lines.indexOfFirst { it == "-----BEGIN PGP SIGNATURE-----" }
239
+ val sigEndIdx = lines.indexOfFirst { it == "-----END PGP SIGNATURE-----" }
240
+ if (hashHeaderIdx < 0 || sigStartIdx < 0 || sigEndIdx < 0) {
241
+ return VerifyResult(false, null, "INVALID_FORMAT")
242
+ }
243
+
244
+ val bodyStartIdx = hashHeaderIdx + 2
245
+ val bodyLines = lines.subList(bodyStartIdx, sigStartIdx)
246
+ val cleartextBody = bodyLines.joinToString("\r\n").trimEnd()
247
+ val sigBlock = lines.subList(sigStartIdx, sigEndIdx + 1).joinToString("\n")
248
+
249
+ // Verify GPG signature.
250
+ val sigInputStream = PGPUtil.getDecoderStream(sigBlock.byteInputStream())
251
+ val sigFactory = JcaPGPObjectFactory(sigInputStream)
252
+ val signatureList = sigFactory.nextObject()
253
+ if (signatureList !is PGPSignatureList || signatureList.isEmpty) {
254
+ return VerifyResult(false, null, "NO_SIGNATURE")
255
+ }
256
+
257
+ val pgpSignature = signatureList[0]
258
+ val pubKeyStream = PGPUtil.getDecoderStream(GPG_PUBLIC_KEY.byteInputStream())
259
+ val pgpPubKeyRingCollection = PGPPublicKeyRingCollection(pubKeyStream, JcaKeyFingerprintCalculator())
260
+ val publicKey = pgpPubKeyRingCollection.getPublicKey(pgpSignature.keyID)
261
+ ?: return VerifyResult(false, null, "PUBKEY_NOT_FOUND")
262
+
263
+ pgpSignature.init(JcaPGPContentVerifierBuilderProvider().setProvider(bcProvider), publicKey)
264
+ val unescapedLines = cleartextBody.lines().map { line ->
265
+ if (line.startsWith("- ")) line.substring(2) else line
266
+ }
267
+ val dataToVerify = unescapedLines.joinToString("\r\n").toByteArray(Charsets.UTF_8)
268
+ pgpSignature.update(dataToVerify)
269
+ if (!pgpSignature.verify()) {
270
+ return VerifyResult(false, null, "SIGNATURE_INVALID")
271
+ }
272
+
273
+ // Extract SHA256 from verified cleartext.
274
+ val sha256 = cleartextBody.trim().split("\\s+".toRegex())[0].lowercase()
275
+ if (sha256.length != 64 || !sha256.all { it in '0'..'9' || it in 'a'..'f' }) {
276
+ return VerifyResult(false, null, "SHA256_TOKEN_INVALID")
277
+ }
278
+
279
+ OneKeyLog.info("BundleCrypto", "ASC detached verification succeeded")
280
+ VerifyResult(true, sha256, null)
281
+ } catch (e: Exception) {
282
+ OneKeyLog.error("BundleCrypto", "ASC verification error: ${e.javaClass.simpleName}: ${e.message}")
283
+ VerifyResult(false, null, "ERROR_${e.javaClass.simpleName}")
284
+ }
285
+ }
286
+
287
+ // MARK: - sha256 (java MessageDigest streaming) — ported from calculateSHA256.
288
+ // failureReason taxonomy preserved verbatim (FILE_NOT_FOUND / FILE_DISAPPEARED
289
+ // / FILE_TRUNCATED / PERMISSION_DENIED / OOM / IO_<class> / UNEXPECTED_<class>).
290
+
291
+ fun sha256OfFile(filePath: String): Sha256ResultData {
292
+ return calculateSHA256(filePath)
293
+ }
294
+
295
+ private fun calculateSHA256(filePath: String): Sha256ResultData {
296
+ val file = File(filePath)
297
+ if (!file.exists()) {
298
+ OneKeyLog.error("BundleCrypto", "calculateSHA256: file not found: $filePath")
299
+ return Sha256ResultData(null, "FILE_NOT_FOUND")
300
+ }
301
+ return try {
302
+ val digest = MessageDigest.getInstance("SHA-256")
303
+ BufferedInputStream(FileInputStream(filePath)).use { bis ->
304
+ val buffer = ByteArray(8192)
305
+ var count: Int
306
+ while (bis.read(buffer).also { count = it } > 0) {
307
+ digest.update(buffer, 0, count)
308
+ }
309
+ }
310
+ Sha256ResultData(bytesToHex(digest.digest()), null)
311
+ } catch (e: java.io.FileNotFoundException) {
312
+ OneKeyLog.error("BundleCrypto", "calculateSHA256: file disappeared during read: ${e.message}")
313
+ Sha256ResultData(null, "FILE_DISAPPEARED")
314
+ } catch (e: java.io.EOFException) {
315
+ OneKeyLog.error("BundleCrypto", "calculateSHA256: truncated file: ${e.message}")
316
+ Sha256ResultData(null, "FILE_TRUNCATED")
317
+ } catch (e: SecurityException) {
318
+ OneKeyLog.error("BundleCrypto", "calculateSHA256: permission denied: ${e.message}")
319
+ Sha256ResultData(null, "PERMISSION_DENIED")
320
+ } catch (e: OutOfMemoryError) {
321
+ OneKeyLog.error("BundleCrypto", "calculateSHA256: OutOfMemoryError on ${file.length()} bytes")
322
+ Sha256ResultData(null, "OOM")
323
+ } catch (e: java.io.IOException) {
324
+ OneKeyLog.error("BundleCrypto", "calculateSHA256: ${e.javaClass.simpleName}: ${e.message}")
325
+ Sha256ResultData(null, "IO_${e.javaClass.simpleName}")
326
+ } catch (e: Exception) {
327
+ OneKeyLog.error("BundleCrypto", "calculateSHA256: ${e.javaClass.simpleName}: ${e.message}")
328
+ Sha256ResultData(null, "UNEXPECTED_${e.javaClass.simpleName}")
329
+ }
330
+ }
331
+
332
+ private fun bytesToHex(bytes: ByteArray): String {
333
+ return bytes.joinToString("") { "%02x".format(it) }
334
+ }
335
+
336
+ // MARK: - Constant-time hex compare — ported from secureCompare.
337
+
338
+ fun secureEqualHex(a: String, b: String): Boolean {
339
+ return secureCompare(a, b)
340
+ }
341
+
342
+ private fun secureCompare(a: String, b: String): Boolean {
343
+ val aBytes = a.toByteArray(Charsets.UTF_8)
344
+ val bBytes = b.toByteArray(Charsets.UTF_8)
345
+ if (aBytes.size != bBytes.size) return false
346
+ var result = 0
347
+ for (i in aBytes.indices) {
348
+ result = result or (aBytes[i].toInt() xor bBytes[i].toInt())
349
+ }
350
+ return result == 0
351
+ }
352
+
353
+ // MARK: - Per-entry directory hash verification — ported from
354
+ // validateAllFilesInDir / validateFilesRecursive. Caller passes the expected
355
+ // relativePath -> sha256 entries (JS derives these from verified metadata).
356
+ // Verifies every on-disk file matches AND every expected entry exists.
357
+
358
+ fun verifyDirAgainstHashes(dirPath: String, entries: List<DirHash>): Boolean {
359
+ val dir = File(dirPath)
360
+ if (!dir.exists() || !dir.isDirectory) {
361
+ OneKeyLog.error("BundleCrypto", "[bundle-verify] dir not found: $dirPath")
362
+ return false
363
+ }
364
+ val jsBundleDir = dir.absolutePath + "/"
365
+ val expected = HashMap<String, String>()
366
+ for (entry in entries) expected[entry.relativePath] = entry.sha256
367
+
368
+ if (!validateFilesRecursive(dir, expected, jsBundleDir)) return false
369
+
370
+ // Verify completeness.
371
+ for ((relativePath, _) in expected) {
372
+ val expectedFile = File(jsBundleDir + relativePath)
373
+ if (!expectedFile.exists()) {
374
+ OneKeyLog.error("BundleCrypto", "[bundle-verify] File listed in metadata but missing on disk: $relativePath")
375
+ return false
376
+ }
377
+ }
378
+ return true
379
+ }
380
+
381
+ private fun validateFilesRecursive(dir: File, expected: Map<String, String>, jsBundleDir: String): Boolean {
382
+ val files = dir.listFiles() ?: return true
383
+ for (file in files) {
384
+ if (file.isDirectory) {
385
+ if (!validateFilesRecursive(file, expected, jsBundleDir)) return false
386
+ } else {
387
+ if (file.name.contains("metadata.json") || file.name.contains(".DS_Store")) continue
388
+ val relativePath = file.absolutePath.replace(jsBundleDir, "")
389
+ val expectedSHA256 = expected[relativePath]
390
+ if (expectedSHA256 == null) {
391
+ OneKeyLog.error("BundleCrypto", "[bundle-verify] File on disk not found in metadata: $relativePath")
392
+ return false
393
+ }
394
+ val actualSHA256 = calculateSHA256(file.absolutePath).sha256
395
+ if (actualSHA256 == null) {
396
+ OneKeyLog.error("BundleCrypto", "[bundle-verify] Failed to calculate SHA256 for file: $relativePath")
397
+ return false
398
+ }
399
+ if (!secureCompare(expectedSHA256, actualSHA256)) {
400
+ OneKeyLog.error("BundleCrypto", "[bundle-verify] SHA256 mismatch for $relativePath")
401
+ return false
402
+ }
403
+ }
404
+ }
405
+ return true
406
+ }
407
+
408
+ // MARK: - Hash a whole dir into relativePath -> sha256 entries (new helper,
409
+ // same recursion/skip rules as verifyDirAgainstHashes). Throws if any file
410
+ // cannot be hashed.
411
+
412
+ fun hashDir(dirPath: String): List<DirHash> {
413
+ val dir = File(dirPath)
414
+ if (!dir.exists() || !dir.isDirectory) {
415
+ return emptyList()
416
+ }
417
+ val jsBundleDir = dir.absolutePath + "/"
418
+ val results = ArrayList<DirHash>()
419
+ hashFilesRecursive(dir, jsBundleDir, results)
420
+ return results
421
+ }
422
+
423
+ private fun hashFilesRecursive(dir: File, jsBundleDir: String, out: ArrayList<DirHash>) {
424
+ val files = dir.listFiles() ?: return
425
+ for (file in files) {
426
+ if (file.isDirectory) {
427
+ hashFilesRecursive(file, jsBundleDir, out)
428
+ } else {
429
+ if (file.name.contains("metadata.json") || file.name.contains(".DS_Store")) continue
430
+ val relativePath = file.absolutePath.replace(jsBundleDir, "")
431
+ val sha256 = calculateSHA256(file.absolutePath).sha256
432
+ ?: throw Exception("HASH_FAILED:$relativePath")
433
+ out.add(DirHash(relativePath, sha256))
434
+ }
435
+ }
436
+ }
437
+
438
+ // MARK: - Symlink / path-traversal guard — ported from the unzip-time guard
439
+ // in ReactNativeBundleUpdate.unzipFile (canonicalPath prefix check + symlink
440
+ // detection via Files.isSymbolicLink), consolidated into a standalone verb.
441
+
442
+ fun validateExtractedPathSafety(destination: String): Boolean {
443
+ val destDir = File(destination)
444
+ if (!destDir.exists()) {
445
+ // Nothing extracted yet — nothing unsafe to find. Mirrors iOS (empty
446
+ // enumerator => true).
447
+ return true
448
+ }
449
+ val resolvedDestination = destDir.canonicalPath
450
+ return validatePathSafetyRecursive(destDir, resolvedDestination)
451
+ }
452
+
453
+ private fun validatePathSafetyRecursive(dir: File, resolvedDestination: String): Boolean {
454
+ val files = dir.listFiles() ?: return true
455
+ for (file in files) {
456
+ if (Files.isSymbolicLink(file.toPath())) {
457
+ OneKeyLog.error("BundleCrypto", "Symlink detected in extracted bundle: ${file.name}")
458
+ return false
459
+ }
460
+ val resolvedPath = file.canonicalPath
461
+ if (resolvedPath != resolvedDestination && !resolvedPath.startsWith("$resolvedDestination/")) {
462
+ OneKeyLog.error("BundleCrypto", "Path traversal detected in extracted bundle: ${file.name}")
463
+ return false
464
+ }
465
+ if (file.isDirectory) {
466
+ if (!validatePathSafetyRecursive(file, resolvedDestination)) return false
467
+ }
468
+ }
469
+ return true
470
+ }
471
+
472
+ // MARK: - Safe file verbs — atomic write / safe rename / version-dir listing.
473
+ // Consolidates the write/move/enumerate patterns from ReactNativeBundleUpdate
474
+ // (atomic temp-file write, remove-then-move, directory enumeration) into
475
+ // explicit APIs.
476
+
477
+ fun atomicWriteFile(filePath: String, contentUtf8: String) {
478
+ val target = File(filePath)
479
+ target.parentFile?.mkdirs()
480
+ // Write to a temp file in the same directory, then atomically rename.
481
+ val tmp = File.createTempFile("bundle-crypto-", ".tmp", target.parentFile)
482
+ try {
483
+ tmp.writeText(contentUtf8, Charsets.UTF_8)
484
+ // Files.move with ATOMIC_MOVE / REPLACE_EXISTING gives the same-volume
485
+ // atomic replace semantics as the source's atomic write.
486
+ Files.move(
487
+ tmp.toPath(),
488
+ target.toPath(),
489
+ java.nio.file.StandardCopyOption.REPLACE_EXISTING,
490
+ java.nio.file.StandardCopyOption.ATOMIC_MOVE
491
+ )
492
+ } catch (e: java.nio.file.AtomicMoveNotSupportedException) {
493
+ // Fallback for filesystems without atomic move support.
494
+ Files.move(tmp.toPath(), target.toPath(), java.nio.file.StandardCopyOption.REPLACE_EXISTING)
495
+ } finally {
496
+ if (tmp.exists()) tmp.delete()
497
+ }
498
+ }
499
+
500
+ fun safeRename(fromPath: String, toPath: String) {
501
+ val from = File(fromPath)
502
+ val to = File(toPath)
503
+ to.parentFile?.mkdirs()
504
+ // Same pattern as downloadBundle: remove existing target, then move.
505
+ if (to.exists()) {
506
+ if (!to.deleteRecursively()) {
507
+ throw Exception("safeRename: failed to remove existing target: $toPath")
508
+ }
509
+ }
510
+ try {
511
+ Files.move(
512
+ from.toPath(),
513
+ to.toPath(),
514
+ java.nio.file.StandardCopyOption.ATOMIC_MOVE
515
+ )
516
+ } catch (e: java.nio.file.AtomicMoveNotSupportedException) {
517
+ Files.move(from.toPath(), to.toPath())
518
+ }
519
+ }
520
+
521
+ fun listVersionDirs(parentDir: String): List<String> {
522
+ val dir = File(parentDir)
523
+ val children = dir.listFiles() ?: return emptyList()
524
+ return children.filter { it.isDirectory }.map { it.name }
525
+ }
526
+ }
@@ -0,0 +1,110 @@
1
+ package com.margelo.nitro.reactnativebundlecrypto
2
+
3
+ import com.facebook.proguard.annotations.DoNotStrip
4
+ import com.margelo.nitro.core.Promise
5
+
6
+ // ============================================================================
7
+ // react-native-bundle-crypto (Android) — Nitro HybridObject thin shell
8
+ //
9
+ // P3a refactor: the real BouncyCastle GPG/ASC verify, MessageDigest sha256,
10
+ // per-entry dir hashing, constant-time compare, symlink/traversal guard, and
11
+ // safe file ops now live in BundleCryptoCore (plain functions, plain Kotlin
12
+ // types). This class is a thin shell: each Nitro method calls the matching
13
+ // BundleCryptoCore.xxx and assembles the generated Nitro Result struct
14
+ // (VerifyGpgCleartextResult etc.). secureEqualHex is synchronous and returns
15
+ // the core result directly.
16
+ //
17
+ // The public Nitro interface, Result field names, and fail-safe behavior are
18
+ // UNCHANGED from the previous in-method implementation. There is a single copy
19
+ // of the logic (in BundleCryptoCore) — this shell never duplicates it.
20
+ //
21
+ // The GPG public key + BouncyCastle provider live in BundleCryptoCore (single
22
+ // source of truth). This module does NOT expose the public key to JS.
23
+ // ============================================================================
24
+
25
+ @DoNotStrip
26
+ class ReactNativeBundleCrypto : HybridReactNativeBundleCryptoSpec() {
27
+
28
+ // MARK: - GPG cleartext (bundle form): body JSON -> sha256
29
+
30
+ override fun verifyGpgCleartext(signedMessage: String): Promise<VerifyGpgCleartextResult> {
31
+ return Promise.async {
32
+ val r = BundleCryptoCore.verifyGpgCleartext(signedMessage)
33
+ VerifyGpgCleartextResult(r.valid, r.sha256, r.reason)
34
+ }
35
+ }
36
+
37
+ // MARK: - Detached ASC (APK form): cleartext-signed SHA256SUMS -> first hex token
38
+
39
+ override fun verifyDetachedAsc(ascContent: String): Promise<VerifyDetachedAscResult> {
40
+ return Promise.async {
41
+ val r = BundleCryptoCore.verifyDetachedAsc(ascContent)
42
+ VerifyDetachedAscResult(r.valid, r.sha256, r.reason)
43
+ }
44
+ }
45
+
46
+ // MARK: - sha256 (java MessageDigest streaming)
47
+
48
+ override fun sha256OfFile(filePath: String): Promise<Sha256Result> {
49
+ return Promise.async {
50
+ val r = BundleCryptoCore.sha256OfFile(filePath)
51
+ Sha256Result(r.sha256, r.failureReason)
52
+ }
53
+ }
54
+
55
+ // MARK: - Constant-time hex compare (synchronous)
56
+
57
+ override fun secureEqualHex(a: String, b: String): Boolean {
58
+ return BundleCryptoCore.secureEqualHex(a, b)
59
+ }
60
+
61
+ // MARK: - Per-entry directory hash verification
62
+
63
+ override fun verifyDirAgainstHashes(
64
+ dirPath: String,
65
+ entries: Array<DirHashEntry>
66
+ ): Promise<Boolean> {
67
+ return Promise.async {
68
+ val coreEntries = entries.map { BundleCryptoCore.DirHash(it.relativePath, it.sha256) }
69
+ BundleCryptoCore.verifyDirAgainstHashes(dirPath, coreEntries)
70
+ }
71
+ }
72
+
73
+ // MARK: - Hash a whole dir into relativePath -> sha256 entries
74
+
75
+ override fun hashDir(dirPath: String): Promise<Array<DirHashEntry>> {
76
+ return Promise.async {
77
+ BundleCryptoCore.hashDir(dirPath)
78
+ .map { DirHashEntry(it.relativePath, it.sha256) }
79
+ .toTypedArray()
80
+ }
81
+ }
82
+
83
+ // MARK: - Symlink / path-traversal guard
84
+
85
+ override fun validateExtractedPathSafety(destination: String): Promise<Boolean> {
86
+ return Promise.async {
87
+ BundleCryptoCore.validateExtractedPathSafety(destination)
88
+ }
89
+ }
90
+
91
+ // MARK: - Safe file verbs — atomic write / safe rename / version-dir listing.
92
+
93
+ override fun atomicWriteFile(filePath: String, contentUtf8: String): Promise<Unit> {
94
+ return Promise.async {
95
+ BundleCryptoCore.atomicWriteFile(filePath, contentUtf8)
96
+ }
97
+ }
98
+
99
+ override fun safeRename(fromPath: String, toPath: String): Promise<Unit> {
100
+ return Promise.async {
101
+ BundleCryptoCore.safeRename(fromPath, toPath)
102
+ }
103
+ }
104
+
105
+ override fun listVersionDirs(parentDir: String): Promise<Array<String>> {
106
+ return Promise.async {
107
+ BundleCryptoCore.listVersionDirs(parentDir).toTypedArray()
108
+ }
109
+ }
110
+ }
@@ -0,0 +1,24 @@
1
+ package com.margelo.nitro.reactnativebundlecrypto
2
+
3
+ import com.facebook.react.BaseReactPackage
4
+ import com.facebook.react.bridge.NativeModule
5
+ import com.facebook.react.bridge.ReactApplicationContext
6
+ import com.facebook.react.module.model.ReactModuleInfoProvider
7
+
8
+ class ReactNativeBundleCryptoPackage : BaseReactPackage() {
9
+ override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? {
10
+ return null
11
+ }
12
+
13
+ override fun getReactModuleInfoProvider(): ReactModuleInfoProvider {
14
+ return ReactModuleInfoProvider { HashMap() }
15
+ }
16
+
17
+ companion object {
18
+ init {
19
+ System.loadLibrary("reactnativebundlecrypto")
20
+ }
21
+ }
22
+ }
23
+
24
+