@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.
- package/LICENSE +21 -0
- package/README.md +36 -0
- package/ReactNativeBundleCrypto.podspec +35 -0
- package/android/CMakeLists.txt +24 -0
- package/android/build.gradle +136 -0
- package/android/gradle.properties +4 -0
- package/android/src/main/AndroidManifest.xml +1 -0
- package/android/src/main/cpp/cpp-adapter.cpp +6 -0
- package/android/src/main/java/com/margelo/nitro/reactnativebundlecrypto/BundleCryptoCore.kt +526 -0
- package/android/src/main/java/com/margelo/nitro/reactnativebundlecrypto/ReactNativeBundleCrypto.kt +110 -0
- package/android/src/main/java/com/margelo/nitro/reactnativebundlecrypto/ReactNativeBundleCryptoPackage.kt +24 -0
- package/ios/BundleCryptoCore.swift +529 -0
- package/ios/Frameworks/Gopenpgp.xcframework/Info.plist +52 -0
- package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64/Gopenpgp.framework/Gopenpgp +0 -0
- package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64/Gopenpgp.framework/Headers/Armor.objc.h +96 -0
- package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64/Gopenpgp.framework/Headers/Constants.objc.h +197 -0
- package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64/Gopenpgp.framework/Headers/Crypto.objc.h +1963 -0
- package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64/Gopenpgp.framework/Headers/Gopenpgp.h +23 -0
- package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64/Gopenpgp.framework/Headers/Mime.objc.h +59 -0
- package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64/Gopenpgp.framework/Headers/Mobile.objc.h +252 -0
- package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64/Gopenpgp.framework/Headers/Profile.objc.h +107 -0
- package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64/Gopenpgp.framework/Headers/Universe.objc.h +29 -0
- package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64/Gopenpgp.framework/Headers/ref.h +35 -0
- package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64/Gopenpgp.framework/Info.plist +20 -0
- package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64/Gopenpgp.framework/Modules/module.modulemap +13 -0
- package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64_x86_64-simulator/Gopenpgp.framework/Gopenpgp +0 -0
- package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64_x86_64-simulator/Gopenpgp.framework/Headers/Armor.objc.h +96 -0
- package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64_x86_64-simulator/Gopenpgp.framework/Headers/Constants.objc.h +197 -0
- package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64_x86_64-simulator/Gopenpgp.framework/Headers/Crypto.objc.h +1963 -0
- package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64_x86_64-simulator/Gopenpgp.framework/Headers/Gopenpgp.h +23 -0
- package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64_x86_64-simulator/Gopenpgp.framework/Headers/Mime.objc.h +59 -0
- package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64_x86_64-simulator/Gopenpgp.framework/Headers/Mobile.objc.h +252 -0
- package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64_x86_64-simulator/Gopenpgp.framework/Headers/Profile.objc.h +107 -0
- package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64_x86_64-simulator/Gopenpgp.framework/Headers/Universe.objc.h +29 -0
- package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64_x86_64-simulator/Gopenpgp.framework/Headers/ref.h +35 -0
- package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64_x86_64-simulator/Gopenpgp.framework/Info.plist +20 -0
- package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64_x86_64-simulator/Gopenpgp.framework/Modules/module.modulemap +13 -0
- package/ios/ReactNativeBundleCrypto.swift +104 -0
- package/lib/module/ReactNativeBundleCrypto.nitro.js +4 -0
- package/lib/module/ReactNativeBundleCrypto.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/ReactNativeBundleCrypto.nitro.d.ts +35 -0
- package/lib/typescript/src/ReactNativeBundleCrypto.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++/JDirHashEntry.hpp +61 -0
- package/nitrogen/generated/android/c++/JHybridReactNativeBundleCryptoSpec.cpp +240 -0
- package/nitrogen/generated/android/c++/JHybridReactNativeBundleCryptoSpec.hpp +74 -0
- package/nitrogen/generated/android/c++/JSha256Result.hpp +62 -0
- package/nitrogen/generated/android/c++/JVerifyDetachedAscResult.hpp +66 -0
- package/nitrogen/generated/android/c++/JVerifyGpgCleartextResult.hpp +66 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativebundlecrypto/DirHashEntry.kt +41 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativebundlecrypto/HybridReactNativeBundleCryptoSpec.kt +94 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativebundlecrypto/Sha256Result.kt +41 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativebundlecrypto/VerifyDetachedAscResult.kt +44 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativebundlecrypto/VerifyGpgCleartextResult.kt +44 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativebundlecrypto/reactnativebundlecryptoOnLoad.kt +35 -0
- package/nitrogen/generated/android/reactnativebundlecrypto+autolinking.cmake +81 -0
- package/nitrogen/generated/android/reactnativebundlecrypto+autolinking.gradle +27 -0
- package/nitrogen/generated/android/reactnativebundlecryptoOnLoad.cpp +44 -0
- package/nitrogen/generated/android/reactnativebundlecryptoOnLoad.hpp +25 -0
- package/nitrogen/generated/ios/ReactNativeBundleCrypto+autolinking.rb +60 -0
- package/nitrogen/generated/ios/ReactNativeBundleCrypto-Swift-Cxx-Bridge.cpp +97 -0
- package/nitrogen/generated/ios/ReactNativeBundleCrypto-Swift-Cxx-Bridge.hpp +429 -0
- package/nitrogen/generated/ios/ReactNativeBundleCrypto-Swift-Cxx-Umbrella.hpp +59 -0
- package/nitrogen/generated/ios/ReactNativeBundleCryptoAutolinking.mm +33 -0
- package/nitrogen/generated/ios/ReactNativeBundleCryptoAutolinking.swift +25 -0
- package/nitrogen/generated/ios/c++/HybridReactNativeBundleCryptoSpecSwift.cpp +11 -0
- package/nitrogen/generated/ios/c++/HybridReactNativeBundleCryptoSpecSwift.hpp +162 -0
- package/nitrogen/generated/ios/swift/DirHashEntry.swift +47 -0
- package/nitrogen/generated/ios/swift/Func_void.swift +47 -0
- package/nitrogen/generated/ios/swift/Func_void_Sha256Result.swift +47 -0
- package/nitrogen/generated/ios/swift/Func_void_VerifyDetachedAscResult.swift +47 -0
- package/nitrogen/generated/ios/swift/Func_void_VerifyGpgCleartextResult.swift +47 -0
- package/nitrogen/generated/ios/swift/Func_void_bool.swift +47 -0
- package/nitrogen/generated/ios/swift/Func_void_std__exception_ptr.swift +47 -0
- package/nitrogen/generated/ios/swift/Func_void_std__vector_DirHashEntry_.swift +47 -0
- package/nitrogen/generated/ios/swift/Func_void_std__vector_std__string_.swift +47 -0
- package/nitrogen/generated/ios/swift/HybridReactNativeBundleCryptoSpec.swift +65 -0
- package/nitrogen/generated/ios/swift/HybridReactNativeBundleCryptoSpec_cxx.swift +314 -0
- package/nitrogen/generated/ios/swift/Sha256Result.swift +85 -0
- package/nitrogen/generated/ios/swift/VerifyDetachedAscResult.swift +96 -0
- package/nitrogen/generated/ios/swift/VerifyGpgCleartextResult.swift +96 -0
- package/nitrogen/generated/shared/c++/DirHashEntry.hpp +79 -0
- package/nitrogen/generated/shared/c++/HybridReactNativeBundleCryptoSpec.cpp +30 -0
- package/nitrogen/generated/shared/c++/HybridReactNativeBundleCryptoSpec.hpp +84 -0
- package/nitrogen/generated/shared/c++/Sha256Result.hpp +80 -0
- package/nitrogen/generated/shared/c++/VerifyDetachedAscResult.hpp +84 -0
- package/nitrogen/generated/shared/c++/VerifyGpgCleartextResult.hpp +84 -0
- package/package.json +169 -0
- package/src/ReactNativeBundleCrypto.nitro.ts +56 -0
- 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
|
+
}
|
package/android/src/main/java/com/margelo/nitro/reactnativebundlecrypto/ReactNativeBundleCrypto.kt
ADDED
|
@@ -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
|
+
|