react-native-instantpay-code-push 1.1.0

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 (87) hide show
  1. package/InstantpayCodePush.podspec +20 -0
  2. package/LICENSE +20 -0
  3. package/README.md +158 -0
  4. package/android/build.gradle +91 -0
  5. package/android/gradle.properties +5 -0
  6. package/android/src/main/AndroidManifest.xml +2 -0
  7. package/android/src/main/java/com/instantpaycodepush/BundleFileStorageService.kt +835 -0
  8. package/android/src/main/java/com/instantpaycodepush/BundleMetadata.kt +249 -0
  9. package/android/src/main/java/com/instantpaycodepush/CommonHelper.kt +39 -0
  10. package/android/src/main/java/com/instantpaycodepush/DecompressService.kt +85 -0
  11. package/android/src/main/java/com/instantpaycodepush/DecompressionStrategy.kt +24 -0
  12. package/android/src/main/java/com/instantpaycodepush/FileManagerService.kt +105 -0
  13. package/android/src/main/java/com/instantpaycodepush/HashUtils.kt +50 -0
  14. package/android/src/main/java/com/instantpaycodepush/InstantpayCodePushModule.kt +182 -0
  15. package/android/src/main/java/com/instantpaycodepush/InstantpayCodePushPackage.kt +33 -0
  16. package/android/src/main/java/com/instantpaycodepush/IpayCodePush.kt +101 -0
  17. package/android/src/main/java/com/instantpaycodepush/IpayCodePushException.kt +135 -0
  18. package/android/src/main/java/com/instantpaycodepush/IpayCodePushImpl.kt +329 -0
  19. package/android/src/main/java/com/instantpaycodepush/OkHttpDownloadService.kt +283 -0
  20. package/android/src/main/java/com/instantpaycodepush/ReactIntegrationManager.kt +141 -0
  21. package/android/src/main/java/com/instantpaycodepush/ReactIntegrationManagerBase.kt +35 -0
  22. package/android/src/main/java/com/instantpaycodepush/SignatureVerifier.kt +354 -0
  23. package/android/src/main/java/com/instantpaycodepush/VersionedPreferencesService.kt +70 -0
  24. package/android/src/main/java/com/instantpaycodepush/ZipDecompressionStrategy.kt +198 -0
  25. package/ios/InstantpayCodePush.h +5 -0
  26. package/ios/InstantpayCodePush.mm +21 -0
  27. package/lib/module/DefaultResolver.js +34 -0
  28. package/lib/module/DefaultResolver.js.map +1 -0
  29. package/lib/module/NativeInstantpayCodePush.js +5 -0
  30. package/lib/module/NativeInstantpayCodePush.js.map +1 -0
  31. package/lib/module/checkForUpdate.js +68 -0
  32. package/lib/module/checkForUpdate.js.map +1 -0
  33. package/lib/module/error.js +137 -0
  34. package/lib/module/error.js.map +1 -0
  35. package/lib/module/fetchUpdateInfo.js +36 -0
  36. package/lib/module/fetchUpdateInfo.js.map +1 -0
  37. package/lib/module/global.d.js +8 -0
  38. package/lib/module/global.d.js.map +1 -0
  39. package/lib/module/hooks/useEventCallback.js +13 -0
  40. package/lib/module/hooks/useEventCallback.js.map +1 -0
  41. package/lib/module/index.js +291 -0
  42. package/lib/module/index.js.map +1 -0
  43. package/lib/module/native.js +233 -0
  44. package/lib/module/native.js.map +1 -0
  45. package/lib/module/package.json +1 -0
  46. package/lib/module/store.js +53 -0
  47. package/lib/module/store.js.map +1 -0
  48. package/lib/module/types.js +62 -0
  49. package/lib/module/types.js.map +1 -0
  50. package/lib/module/wrap.js +171 -0
  51. package/lib/module/wrap.js.map +1 -0
  52. package/lib/typescript/package.json +1 -0
  53. package/lib/typescript/src/DefaultResolver.d.ts +10 -0
  54. package/lib/typescript/src/DefaultResolver.d.ts.map +1 -0
  55. package/lib/typescript/src/NativeInstantpayCodePush.d.ts +100 -0
  56. package/lib/typescript/src/NativeInstantpayCodePush.d.ts.map +1 -0
  57. package/lib/typescript/src/checkForUpdate.d.ts +29 -0
  58. package/lib/typescript/src/checkForUpdate.d.ts.map +1 -0
  59. package/lib/typescript/src/error.d.ts +124 -0
  60. package/lib/typescript/src/error.d.ts.map +1 -0
  61. package/lib/typescript/src/fetchUpdateInfo.d.ts +8 -0
  62. package/lib/typescript/src/fetchUpdateInfo.d.ts.map +1 -0
  63. package/lib/typescript/src/hooks/useEventCallback.d.ts +5 -0
  64. package/lib/typescript/src/hooks/useEventCallback.d.ts.map +1 -0
  65. package/lib/typescript/src/index.d.ts +203 -0
  66. package/lib/typescript/src/index.d.ts.map +1 -0
  67. package/lib/typescript/src/native.d.ts +128 -0
  68. package/lib/typescript/src/native.d.ts.map +1 -0
  69. package/lib/typescript/src/store.d.ts +11 -0
  70. package/lib/typescript/src/store.d.ts.map +1 -0
  71. package/lib/typescript/src/types.d.ts +174 -0
  72. package/lib/typescript/src/types.d.ts.map +1 -0
  73. package/lib/typescript/src/wrap.d.ts +179 -0
  74. package/lib/typescript/src/wrap.d.ts.map +1 -0
  75. package/package.json +174 -0
  76. package/src/DefaultResolver.ts +36 -0
  77. package/src/NativeInstantpayCodePush.ts +111 -0
  78. package/src/checkForUpdate.ts +122 -0
  79. package/src/error.ts +159 -0
  80. package/src/fetchUpdateInfo.ts +47 -0
  81. package/src/global.d.ts +23 -0
  82. package/src/hooks/useEventCallback.ts +30 -0
  83. package/src/index.tsx +379 -0
  84. package/src/native.ts +280 -0
  85. package/src/store.ts +69 -0
  86. package/src/types.ts +227 -0
  87. package/src/wrap.tsx +384 -0
@@ -0,0 +1,141 @@
1
+ package com.instantpaycodepush
2
+
3
+ import android.content.Context
4
+ import android.util.Log
5
+ import com.facebook.react.ReactApplication
6
+ import com.facebook.react.ReactHost
7
+ import com.facebook.react.ReactInstanceEventListener
8
+ import com.facebook.react.bridge.JSBundleLoader
9
+ import com.facebook.react.bridge.ReactContext
10
+ import com.facebook.react.common.LifecycleState
11
+ import kotlinx.coroutines.suspendCancellableCoroutine
12
+ import java.lang.reflect.Field
13
+ import kotlin.coroutines.resume
14
+
15
+ class ReactIntegrationManager(
16
+ context: Context,
17
+ ) : ReactIntegrationManagerBase(context) {
18
+
19
+ companion object {
20
+ private const val CLASS_TAG = "*ReactIntegrationManager"
21
+ }
22
+
23
+ public fun setJSBundle(
24
+ application: ReactApplication,
25
+ bundleURL: String,
26
+ ) {
27
+ try {
28
+ val reactHost = application.reactHost
29
+ check(reactHost != null)
30
+
31
+ // Try both Java and Kotlin field names for compatibility
32
+ val reactHostDelegateField =
33
+ try {
34
+ reactHost::class.java.getDeclaredField("mReactHostDelegate")
35
+ } catch (e: NoSuchFieldException) {
36
+ try {
37
+ reactHost::class.java.getDeclaredField("reactHostDelegate")
38
+ } catch (e2: NoSuchFieldException) {
39
+ throw RuntimeException("Neither mReactHostDelegate nor reactHostDelegate field found", e2)
40
+ }
41
+ }
42
+
43
+ reactHostDelegateField.isAccessible = true
44
+ val reactHostDelegate =
45
+ reactHostDelegateField.get(
46
+ reactHost,
47
+ )
48
+ val jsBundleLoaderField = reactHostDelegate::class.java.getDeclaredField("jsBundleLoader")
49
+ jsBundleLoaderField.isAccessible = true
50
+ jsBundleLoaderField.set(reactHostDelegate, getJSBundlerLoader(bundleURL))
51
+ } catch (e: Exception) {
52
+ try {
53
+ // Fallback to old architecture if ReactHost is not available
54
+ @Suppress("DEPRECATION")
55
+ val instanceManager = application.reactNativeHost.reactInstanceManager
56
+ val bundleLoader: JSBundleLoader? = this.getJSBundlerLoader(bundleURL)
57
+ val bundleLoaderField: Field =
58
+ instanceManager::class.java.getDeclaredField("mBundleLoader")
59
+ bundleLoaderField.isAccessible = true
60
+
61
+ if (bundleLoader != null) {
62
+ bundleLoaderField.set(instanceManager, bundleLoader)
63
+ } else {
64
+ bundleLoaderField.set(instanceManager, null)
65
+ }
66
+ } catch (e: Exception) {
67
+ CommonHelper.logPrint(CLASS_TAG, "Failed to setJSBundle (fallback): ${e.message}")
68
+ }
69
+ CommonHelper.logPrint(CLASS_TAG, "Failed to setJSBundle: ${e.message}")
70
+ }
71
+ }
72
+
73
+ /**
74
+ * Reload the React Native application, ensuring ReactContext is initialized first.
75
+ * Caller should run this on main thread.
76
+ */
77
+ public suspend fun reload(application: ReactApplication) {
78
+ try {
79
+ val reactHost = application.reactHost
80
+ if (reactHost != null) {
81
+ // Ensure initialized; if not, start and wait
82
+ waitForReactContextInitialized(reactHost)
83
+
84
+ val activity = reactHost.currentReactContext?.currentActivity
85
+ if (reactHost.lifecycleState != LifecycleState.RESUMED && activity != null) {
86
+ reactHost.onHostResume(activity)
87
+ }
88
+ reactHost.reload("Requested by IpayCodePush")
89
+ } else {
90
+ // Fallback to old architecture if ReactHost is not available
91
+ @Suppress("DEPRECATION")
92
+ val reactNativeHost = application.reactNativeHost
93
+ try {
94
+ reactNativeHost.reactInstanceManager.recreateReactContextInBackground()
95
+ } catch (e: Exception) {
96
+ val currentActivity = reactNativeHost.reactInstanceManager.currentReactContext?.currentActivity
97
+ if (currentActivity == null) {
98
+ return
99
+ }
100
+
101
+ currentActivity.runOnUiThread {
102
+ currentActivity.recreate()
103
+ }
104
+ } catch (e: Exception) {
105
+ CommonHelper.logPrint(CLASS_TAG, "Failed to reload: ${e.message}")
106
+ }
107
+ }
108
+ } catch (e: Exception) {
109
+ CommonHelper.logPrint(CLASS_TAG, "Failed to reloads: ${e.message}")
110
+ }
111
+ }
112
+
113
+ /**
114
+ * Waits until ReactContext is initialized.
115
+ * @return true if ReactContext was already initialized; false if we waited for it.
116
+ */
117
+ suspend fun waitForReactContextInitialized(reactHost: ReactHost): Boolean {
118
+ return try {
119
+ // If already initialized, return immediately
120
+ if (reactHost.currentReactContext != null) return true
121
+
122
+ // Wait for initialization; MainApplication handles starting the host
123
+ suspendCancellableCoroutine { continuation ->
124
+ val listener =
125
+ object : ReactInstanceEventListener {
126
+ override fun onReactContextInitialized(context: ReactContext) {
127
+ reactHost.removeReactInstanceEventListener(this)
128
+ if (continuation.isActive) continuation.resume(Unit)
129
+ }
130
+ }
131
+
132
+ reactHost.addReactInstanceEventListener(listener)
133
+ continuation.invokeOnCancellation { reactHost.removeReactInstanceEventListener(listener) }
134
+ }
135
+ false
136
+ } catch (e: Exception) {
137
+ CommonHelper.logPrint(CLASS_TAG, "waitForReactContextInitialized failed: ${e.message}")
138
+ true
139
+ }
140
+ }
141
+ }
@@ -0,0 +1,35 @@
1
+ package com.instantpaycodepush
2
+
3
+ import android.app.Application
4
+ import android.content.Context
5
+ import com.facebook.react.ReactApplication
6
+ import com.facebook.react.bridge.JSBundleLoader
7
+
8
+ open class ReactIntegrationManagerBase(
9
+ private val context: Context,
10
+ ) {
11
+
12
+ fun getJSBundlerLoader(bundleFileUrl: String): JSBundleLoader? {
13
+ val bundleLoader: JSBundleLoader?
14
+
15
+ if (bundleFileUrl.lowercase().startsWith("assets://")) {
16
+ bundleLoader =
17
+ JSBundleLoader.createAssetLoader(
18
+ context,
19
+ bundleFileUrl,
20
+ false,
21
+ )
22
+ } else {
23
+ bundleLoader = JSBundleLoader.createFileLoader(bundleFileUrl)
24
+ }
25
+ return bundleLoader
26
+ }
27
+
28
+ public fun getReactApplication(application: Application?): ReactApplication {
29
+ if (application is ReactApplication) {
30
+ return application
31
+ } else {
32
+ throw IllegalArgumentException("Application does not implement ReactApplication")
33
+ }
34
+ }
35
+ }
@@ -0,0 +1,354 @@
1
+ package com.instantpaycodepush
2
+
3
+ import android.content.Context
4
+ import android.util.Base64
5
+ import android.util.Log
6
+ import java.io.File
7
+ import java.security.KeyFactory
8
+ import java.security.PublicKey
9
+ import java.security.Signature
10
+ import java.security.spec.X509EncodedKeySpec
11
+
12
+ /**
13
+ * Prefix for signed file hash format.
14
+ */
15
+ private const val SIGNED_HASH_PREFIX = "sig:"
16
+
17
+ /**
18
+ * Custom exceptions for signature verification errors.
19
+ *
20
+ * **IMPORTANT**: The error messages in these exceptions are used by the JavaScript layer
21
+ * (`packages/react-native/src/types.ts`) to detect signature verification failures.
22
+ * If you change these messages, update `isSignatureVerificationError()` in types.ts accordingly.
23
+ */
24
+
25
+ sealed class SignatureVerificationException(
26
+ message: String,
27
+ ) : Exception(message) {
28
+ class PublicKeyNotConfigured :
29
+ SignatureVerificationException(
30
+ "Public key not configured for signature verification. " +
31
+ "Add 'ipay_code_push_public_key' to res/values/strings.xml",
32
+ )
33
+
34
+ class InvalidPublicKeyFormat :
35
+ SignatureVerificationException(
36
+ "Public key format is invalid. Ensure the public key is in PEM format (BEGIN PUBLIC KEY)",
37
+ )
38
+
39
+ class MissingFileHash :
40
+ SignatureVerificationException(
41
+ "File hash is missing or empty. Ensure the bundle update includes a valid file hash",
42
+ )
43
+
44
+ class InvalidSignatureFormat :
45
+ SignatureVerificationException(
46
+ "Signature format is invalid or corrupted. The signature data is malformed or cannot be decoded",
47
+ )
48
+
49
+ class SignatureVerificationFailed :
50
+ SignatureVerificationException(
51
+ "Bundle signature verification failed. The bundle may be corrupted or tampered with",
52
+ )
53
+
54
+ class FileHashMismatch :
55
+ SignatureVerificationException(
56
+ "File hash verification failed. The bundle file hash does not match the expected value. File may be corrupted",
57
+ )
58
+
59
+ class FileReadFailed :
60
+ SignatureVerificationException(
61
+ "Failed to read file for verification. Could not read file for hash verification",
62
+ )
63
+
64
+ class UnsignedNotAllowed :
65
+ SignatureVerificationException(
66
+ "Unsigned bundle not allowed when signing is enabled. " +
67
+ "Public key is configured but bundle is not signed. Rejecting update",
68
+ )
69
+
70
+ class SecurityFrameworkError(
71
+ cause: Throwable,
72
+ ) : SignatureVerificationException(
73
+ "Security framework error during verification: ${cause.message}",
74
+ )
75
+ }
76
+
77
+
78
+ /**
79
+ * Service for verifying bundle integrity through hash or RSA-SHA256 signature verification.
80
+ * Uses Java Signature API for cryptographic operations.
81
+ *
82
+ * fileHash format:
83
+ * - Signed: `sig:<base64_signature>` - Verify signature (implicitly verifies hash)
84
+ * - Unsigned: `<hex_hash>` - Verify SHA256 hash only
85
+ *
86
+ * Security rules:
87
+ * - null/empty fileHash → REJECT
88
+ * - sig:... + public key configured → verify signature → Install/REJECT
89
+ * - sig:... + public key NOT configured → REJECT (can't verify)
90
+ * - <hash> + public key configured → REJECT (unsigned not allowed)
91
+ * - <hash> + public key NOT configured → verify hash → Install/REJECT
92
+ */
93
+ object SignatureVerifier {
94
+ private const val CLASS_TAG = "*SignatureVerifier"
95
+
96
+ /**
97
+ * Reads public key from Android string resources.
98
+ * @param context Application context
99
+ * @return Public key PEM string or null if not configured
100
+ */
101
+ private fun getPublicKeyFromConfig(context: Context): String? {
102
+ val resourceId =
103
+ context.resources.getIdentifier(
104
+ "ipay_code_push_public_key",
105
+ "string",
106
+ context.packageName,
107
+ )
108
+
109
+ if (resourceId == 0) {
110
+ CommonHelper.logPrint(CLASS_TAG, "ipay_code_push_public_key not found in strings.xml")
111
+ return null
112
+ }
113
+
114
+ val publicKeyPEM = context.getString(resourceId)
115
+ if (publicKeyPEM.isEmpty()) {
116
+ CommonHelper.logPrint(CLASS_TAG, "ipay_code_push_public_key is empty")
117
+ return null
118
+ }
119
+
120
+ return publicKeyPEM
121
+ }
122
+
123
+ /**
124
+ * Checks if signing is enabled (public key is configured).
125
+ * @param context Application context
126
+ * @return true if public key is configured
127
+ */
128
+ fun isSigningEnabled(context: Context): Boolean = getPublicKeyFromConfig(context) != null
129
+
130
+ /**
131
+ * Checks if fileHash is in signed format (starts with "sig:").
132
+ * @param fileHash The file hash string to check
133
+ * @return true if signed format
134
+ */
135
+ fun isSignedFormat(fileHash: String?): Boolean = fileHash?.startsWith(SIGNED_HASH_PREFIX) == true
136
+
137
+ /**
138
+ * Extracts signature from signed format fileHash.
139
+ * @param fileHash The signed file hash (sig:<signature>)
140
+ * @return Base64-encoded signature or null if not signed format
141
+ */
142
+ fun extractSignature(fileHash: String?): String? {
143
+ if (!isSignedFormat(fileHash)) return null
144
+ return fileHash?.removePrefix(SIGNED_HASH_PREFIX)
145
+ }
146
+
147
+ /**
148
+ * Verifies bundle integrity based on fileHash format.
149
+ * Determines verification mode by checking for "sig:" prefix.
150
+ *
151
+ * @param context Application context
152
+ * @param bundleFile The bundle file to verify
153
+ * @param fileHash Combined hash string (sig:<signature> or <hex_hash>)
154
+ * @throws SignatureVerificationException if verification fails
155
+ */
156
+ fun verifyBundle(
157
+ context: Context,
158
+ bundleFile: File,
159
+ fileHash: String?,
160
+ ) {
161
+ val signingEnabled = isSigningEnabled(context)
162
+
163
+ // Rule: null/empty fileHash → REJECT
164
+ if (fileHash.isNullOrEmpty()) {
165
+ CommonHelper.logPrint(CommonHelper.ERROR_LOG,CLASS_TAG, "fileHash is null or empty. Rejecting update.")
166
+ throw SignatureVerificationException.MissingFileHash()
167
+ }
168
+
169
+ if (isSignedFormat(fileHash)) {
170
+ // Signed format: sig:<signature>
171
+ val signature = extractSignature(fileHash)
172
+ if (signature.isNullOrEmpty()) {
173
+ CommonHelper.logPrint(CommonHelper.ERROR_LOG,CLASS_TAG, "Failed to extract signature from fileHash")
174
+ throw SignatureVerificationException.InvalidSignatureFormat()
175
+ }
176
+
177
+ // Rule: sig:... + public key NOT configured → REJECT
178
+ if (!signingEnabled) {
179
+ CommonHelper.logPrint(CommonHelper.ERROR_LOG,CLASS_TAG, "Signed bundle but public key not configured. Cannot verify.")
180
+ throw SignatureVerificationException.PublicKeyNotConfigured()
181
+ }
182
+
183
+ // Rule: sig:... + public key configured → verify signature
184
+ verifySignature(context, bundleFile, signature)
185
+ } else {
186
+ // Unsigned format: <hex_hash>
187
+
188
+ // Rule: <hash> + public key configured → REJECT
189
+ if (signingEnabled) {
190
+ CommonHelper.logPrint(CommonHelper.ERROR_LOG,CLASS_TAG, "Unsigned bundle not allowed when signing is enabled. Rejecting.")
191
+ throw SignatureVerificationException.UnsignedNotAllowed()
192
+ }
193
+
194
+ // Rule: <hash> + public key NOT configured → verify hash
195
+ verifyHash(bundleFile, fileHash)
196
+ }
197
+ }
198
+
199
+ /**
200
+ * Verifies SHA256 hash of a file.
201
+ * @param bundleFile The file to verify
202
+ * @param expectedHash Expected SHA256 hash (hex string)
203
+ * @throws SignatureVerificationException.FileHashMismatch if verification fails
204
+ */
205
+ fun verifyHash(
206
+ bundleFile: File,
207
+ expectedHash: String,
208
+ ) {
209
+ CommonHelper.logPrint(CLASS_TAG, "Verifying hash for file: ${bundleFile.name}")
210
+
211
+ if (!HashUtils.verifyHash(bundleFile, expectedHash)) {
212
+ CommonHelper.logPrint(CommonHelper.ERROR_LOG,CLASS_TAG, "Hash mismatch!")
213
+ throw SignatureVerificationException.FileHashMismatch()
214
+ }
215
+
216
+ CommonHelper.logPrint(CLASS_TAG, "✅ Hash verified successfully")
217
+ }
218
+
219
+ /**
220
+ * Verifies RSA-SHA256 signature of a file.
221
+ * Calculates the file hash internally and verifies the signature.
222
+ *
223
+ * @param context Application context
224
+ * @param bundleFile The file to verify
225
+ * @param signatureBase64 Base64-encoded RSA-SHA256 signature
226
+ * @throws SignatureVerificationException if verification fails
227
+ */
228
+ fun verifySignature(
229
+ context: Context,
230
+ bundleFile: File,
231
+ signatureBase64: String,
232
+ ) {
233
+ CommonHelper.logPrint(CLASS_TAG, "Verifying signature for file: ${bundleFile.name}")
234
+
235
+ // Get public key from config
236
+ val publicKeyPEM =
237
+ getPublicKeyFromConfig(context)
238
+ ?: run {
239
+ CommonHelper.logPrint(CommonHelper.ERROR_LOG,CLASS_TAG, "Cannot verify signature: public key not configured in strings.xml")
240
+ throw SignatureVerificationException.PublicKeyNotConfigured()
241
+ }
242
+
243
+ try {
244
+ // Convert PEM to PublicKey
245
+ val publicKey = createPublicKey(publicKeyPEM)
246
+
247
+ // Calculate file hash
248
+ val fileHashHex =
249
+ HashUtils.calculateSHA256(bundleFile)
250
+ ?: run {
251
+ CommonHelper.logPrint(CommonHelper.ERROR_LOG,CLASS_TAG, "Failed to calculate file hash")
252
+ throw SignatureVerificationException.FileReadFailed()
253
+ }
254
+
255
+ CommonHelper.logPrint(CLASS_TAG, "Calculated file hash: $fileHashHex")
256
+
257
+ // Decode signature from base64
258
+ val signatureBytes =
259
+ try {
260
+ Base64.decode(signatureBase64, Base64.DEFAULT)
261
+ } catch (e: Exception) {
262
+ CommonHelper.logPrint(CommonHelper.ERROR_LOG,CLASS_TAG, "Failed to decode signature from base64 $e")
263
+ throw SignatureVerificationException.InvalidSignatureFormat()
264
+ }
265
+
266
+ // Convert hex fileHash to bytes
267
+ val fileHashBytes = hexToByteArray(fileHashHex)
268
+
269
+ // Verify signature using RSA-SHA256
270
+ val verifier = Signature.getInstance("SHA256withRSA")
271
+ verifier.initVerify(publicKey)
272
+ verifier.update(fileHashBytes)
273
+ val isValid = verifier.verify(signatureBytes)
274
+
275
+ if (isValid) {
276
+ CommonHelper.logPrint(CLASS_TAG, "✅ Signature verified successfully")
277
+ } else {
278
+ CommonHelper.logPrint(CommonHelper.ERROR_LOG,CLASS_TAG, "❌ Signature verification failed")
279
+ throw SignatureVerificationException.SignatureVerificationFailed()
280
+ }
281
+ } catch (e: SignatureVerificationException) {
282
+ CommonHelper.logPrint(CommonHelper.ERROR_LOG,CLASS_TAG, "SignatureVerificationException : Signature verification error $e")
283
+ throw e
284
+ } catch (e: Exception) {
285
+ CommonHelper.logPrint(CommonHelper.ERROR_LOG,CLASS_TAG, "Signature verification error $e")
286
+ throw SignatureVerificationException.SecurityFrameworkError(e)
287
+ }
288
+ }
289
+
290
+ /**
291
+ * Converts PEM-formatted public key to PublicKey.
292
+ * @param publicKeyPEM Public key in PEM format
293
+ * @return PublicKey instance
294
+ * @throws SignatureVerificationException.InvalidPublicKeyFormat if conversion fails
295
+ */
296
+ private fun createPublicKey(publicKeyPEM: String): PublicKey {
297
+ try {
298
+ // Remove PEM headers/footers and whitespace
299
+ val publicKeyBase64 =
300
+ publicKeyPEM
301
+ .replace("-----BEGIN PUBLIC KEY-----", "")
302
+ .replace("-----END PUBLIC KEY-----", "")
303
+ .replace("\\n", "")
304
+ .replace("\n", "")
305
+ .replace("\r", "")
306
+ .replace(" ", "")
307
+ .trim()
308
+
309
+ // Decode base64
310
+ val keyBytes = Base64.decode(publicKeyBase64, Base64.DEFAULT)
311
+
312
+ // Create PublicKey from X.509 format (SubjectPublicKeyInfo)
313
+ val spec = X509EncodedKeySpec(keyBytes)
314
+ val keyFactory = KeyFactory.getInstance("RSA")
315
+ val publicKey = keyFactory.generatePublic(spec)
316
+
317
+ CommonHelper.logPrint(CLASS_TAG, "Public key loaded successfully")
318
+ return publicKey
319
+ } catch (e: Exception) {
320
+ CommonHelper.logPrint(CommonHelper.ERROR_LOG, CLASS_TAG, "Failed to create public key $e")
321
+ throw SignatureVerificationException.InvalidPublicKeyFormat()
322
+ }
323
+ }
324
+
325
+ /**
326
+ * Converts hex string to ByteArray.
327
+ * @param hexString Hex-encoded string
328
+ * @return ByteArray
329
+ * @throws SignatureVerificationException.SignatureVerificationFailed if conversion fails
330
+ */
331
+ private fun hexToByteArray(hexString: String): ByteArray {
332
+ try {
333
+ val len = hexString.length
334
+ if (len % 2 != 0) {
335
+ throw SignatureVerificationException.InvalidSignatureFormat()
336
+ }
337
+
338
+ val data = ByteArray(len / 2)
339
+ var i = 0
340
+ while (i < len) {
341
+ data[i / 2] =
342
+ (
343
+ (Character.digit(hexString[i], 16) shl 4) +
344
+ Character.digit(hexString[i + 1], 16)
345
+ ).toByte()
346
+ i += 2
347
+ }
348
+ return data
349
+ } catch (e: Exception) {
350
+ CommonHelper.logPrint(CommonHelper.ERROR_LOG, CLASS_TAG, "Failed to convert hex to byte array $e")
351
+ throw SignatureVerificationException.InvalidSignatureFormat()
352
+ }
353
+ }
354
+ }
@@ -0,0 +1,70 @@
1
+ package com.instantpaycodepush
2
+
3
+ import android.content.Context
4
+ import android.content.SharedPreferences
5
+ import java.io.File
6
+
7
+ /**
8
+ * Interface for preference storage operations
9
+ */
10
+ interface PreferencesService {
11
+ /**
12
+ * Gets a stored preference value
13
+ * @param key The key to retrieve
14
+ * @return The stored value or null if not found
15
+ */
16
+ fun getItem(key: String): String?
17
+
18
+ /**
19
+ * Sets a preference value
20
+ * @param key The key to store under
21
+ * @param value The value to store (or null to remove)
22
+ */
23
+ fun setItem(
24
+ key: String,
25
+ value: String?,
26
+ )
27
+ }
28
+
29
+ /**
30
+ * Implementation of PreferencesService using SharedPreferences
31
+ * Modified from original HotUpdaterPrefs to follow the service pattern
32
+ */
33
+
34
+ class VersionedPreferencesService(
35
+ private val context: Context,
36
+ private val isolationKey: String,
37
+ ) : PreferencesService {
38
+
39
+ private val prefs: SharedPreferences
40
+
41
+ init {
42
+
43
+ val sharedPrefsDir = File(context.applicationInfo.dataDir, "shared_prefs")
44
+ if (sharedPrefsDir.exists() && sharedPrefsDir.isDirectory) {
45
+ sharedPrefsDir.listFiles()?.forEach { file ->
46
+ if (file.name.startsWith("IpayCodePushPrefs_") && file.name != "$isolationKey.xml") {
47
+ file.delete()
48
+ }
49
+ }
50
+ }
51
+
52
+ prefs = context.getSharedPreferences(isolationKey, Context.MODE_PRIVATE)
53
+ }
54
+
55
+ override fun getItem(key: String): String? = prefs.getString(key, null)
56
+
57
+ override fun setItem(
58
+ key: String,
59
+ value: String?,
60
+ ) {
61
+ prefs.edit().apply {
62
+ if (value == null) {
63
+ remove(key)
64
+ } else {
65
+ putString(key, value)
66
+ }
67
+ apply()
68
+ }
69
+ }
70
+ }