@hot-updater/react-native 0.22.2 → 0.23.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.
- package/android/src/main/java/com/hotupdater/BundleFileStorageService.kt +17 -12
- package/android/src/main/java/com/hotupdater/HotUpdater.kt +1 -1
- package/android/src/main/java/com/hotupdater/HotUpdaterFactory.kt +1 -0
- package/android/src/main/java/com/hotupdater/HotUpdaterImpl.kt +1 -1
- package/android/src/main/java/com/hotupdater/SignatureVerifier.kt +346 -0
- package/ios/HotUpdater/Internal/BundleFileStorageService.swift +26 -18
- package/ios/HotUpdater/Internal/SignatureVerifier.swift +339 -0
- package/lib/commonjs/checkForUpdate.js +1 -1
- package/lib/commonjs/checkForUpdate.js.map +1 -1
- package/lib/commonjs/index.js +16 -1
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/specs/NativeHotUpdater.js.map +1 -1
- package/lib/commonjs/types.js +45 -0
- package/lib/commonjs/types.js.map +1 -0
- package/lib/module/checkForUpdate.js +1 -1
- package/lib/module/checkForUpdate.js.map +1 -1
- package/lib/module/index.js +1 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/specs/NativeHotUpdater.js.map +1 -1
- package/lib/module/types.js +40 -0
- package/lib/module/types.js.map +1 -0
- package/lib/typescript/commonjs/index.d.ts +1 -0
- package/lib/typescript/commonjs/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/specs/NativeHotUpdater.d.ts +7 -2
- package/lib/typescript/commonjs/specs/NativeHotUpdater.d.ts.map +1 -1
- package/lib/typescript/commonjs/types.d.ts +34 -0
- package/lib/typescript/commonjs/types.d.ts.map +1 -0
- package/lib/typescript/module/index.d.ts +1 -0
- package/lib/typescript/module/index.d.ts.map +1 -1
- package/lib/typescript/module/specs/NativeHotUpdater.d.ts +7 -2
- package/lib/typescript/module/specs/NativeHotUpdater.d.ts.map +1 -1
- package/lib/typescript/module/types.d.ts +34 -0
- package/lib/typescript/module/types.d.ts.map +1 -0
- package/package.json +6 -6
- package/src/checkForUpdate.ts +1 -1
- package/src/index.ts +5 -0
- package/src/specs/NativeHotUpdater.ts +7 -2
- package/src/types.ts +63 -0
|
@@ -40,7 +40,7 @@ interface BundleStorageService {
|
|
|
40
40
|
* Updates the bundle from the specified URL
|
|
41
41
|
* @param bundleId ID of the bundle to update
|
|
42
42
|
* @param fileUrl URL of the bundle file to download (or null to reset)
|
|
43
|
-
* @param fileHash
|
|
43
|
+
* @param fileHash Combined hash string for verification (sig:<signature> or <hex_hash>)
|
|
44
44
|
* @param progressCallback Callback for download progress updates
|
|
45
45
|
* @return true if the update was successful
|
|
46
46
|
*/
|
|
@@ -56,6 +56,7 @@ interface BundleStorageService {
|
|
|
56
56
|
* Implementation of BundleStorageService
|
|
57
57
|
*/
|
|
58
58
|
class BundleFileStorageService(
|
|
59
|
+
private val context: android.content.Context,
|
|
59
60
|
private val fileSystem: FileSystemService,
|
|
60
61
|
private val downloadService: DownloadService,
|
|
61
62
|
private val decompressService: DecompressService,
|
|
@@ -90,7 +91,10 @@ class BundleFileStorageService(
|
|
|
90
91
|
fileHash: String?,
|
|
91
92
|
progressCallback: (Double) -> Unit,
|
|
92
93
|
): Boolean {
|
|
93
|
-
Log.d(
|
|
94
|
+
Log.d(
|
|
95
|
+
"BundleStorage",
|
|
96
|
+
"updateBundle bundleId $bundleId fileUrl $fileUrl fileHash $fileHash",
|
|
97
|
+
)
|
|
94
98
|
|
|
95
99
|
// If no URL is provided, reset to fallback
|
|
96
100
|
if (fileUrl.isNullOrEmpty()) {
|
|
@@ -182,18 +186,19 @@ class BundleFileStorageService(
|
|
|
182
186
|
tempDir.deleteRecursively()
|
|
183
187
|
return@withContext false
|
|
184
188
|
}
|
|
189
|
+
|
|
185
190
|
is DownloadResult.Success -> {
|
|
186
191
|
Log.d("BundleStorage", "Download successful")
|
|
187
|
-
// 1) Verify
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
192
|
+
// 1) Verify bundle integrity (hash or signature based on fileHash format)
|
|
193
|
+
Log.d("BundleStorage", "Verifying bundle integrity...")
|
|
194
|
+
try {
|
|
195
|
+
SignatureVerifier.verifyBundle(context, tempBundleFile, fileHash)
|
|
196
|
+
Log.d("BundleStorage", "Bundle verification completed successfully")
|
|
197
|
+
} catch (e: SignatureVerificationException) {
|
|
198
|
+
Log.e("BundleStorage", "Bundle verification failed", e)
|
|
199
|
+
tempDir.deleteRecursively()
|
|
200
|
+
tempBundleFile.delete()
|
|
201
|
+
return@withContext false
|
|
197
202
|
}
|
|
198
203
|
|
|
199
204
|
// 2) Create a .tmp directory under bundle-store (to avoid colliding with an existing bundleId folder)
|
|
@@ -49,7 +49,7 @@ class HotUpdater {
|
|
|
49
49
|
* @param context Application context
|
|
50
50
|
* @param bundleId ID of the bundle to update
|
|
51
51
|
* @param fileUrl URL of the bundle file to download (or null to reset)
|
|
52
|
-
* @param fileHash
|
|
52
|
+
* @param fileHash Combined hash string for verification (sig:<signature> or <hex_hash>)
|
|
53
53
|
* @param progressCallback Callback for download progress updates
|
|
54
54
|
* @return true if the update was successful
|
|
55
55
|
*/
|
|
@@ -185,7 +185,7 @@ class HotUpdaterImpl(
|
|
|
185
185
|
* Updates the bundle from the specified URL
|
|
186
186
|
* @param bundleId ID of the bundle to update
|
|
187
187
|
* @param fileUrl URL of the bundle file to download (or null to reset)
|
|
188
|
-
* @param fileHash
|
|
188
|
+
* @param fileHash Combined hash string for verification (sig:<signature> or <hex_hash>)
|
|
189
189
|
* @param progressCallback Callback for download progress updates
|
|
190
190
|
* @return true if the update was successful
|
|
191
191
|
*/
|
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
package com.hotupdater
|
|
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
|
+
sealed class SignatureVerificationException(
|
|
25
|
+
message: String,
|
|
26
|
+
) : Exception(message) {
|
|
27
|
+
class PublicKeyNotConfigured :
|
|
28
|
+
SignatureVerificationException(
|
|
29
|
+
"Public key not configured for signature verification. " +
|
|
30
|
+
"Add 'hot_updater_public_key' to res/values/strings.xml",
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
class InvalidPublicKeyFormat :
|
|
34
|
+
SignatureVerificationException(
|
|
35
|
+
"Public key format is invalid. Ensure the public key is in PEM format (BEGIN PUBLIC KEY)",
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
class InvalidSignatureFormat :
|
|
39
|
+
SignatureVerificationException(
|
|
40
|
+
"Signature format is invalid. The signature must be base64-encoded",
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
class VerificationFailed :
|
|
44
|
+
SignatureVerificationException(
|
|
45
|
+
"Bundle signature verification failed. The bundle may be corrupted or tampered with",
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
class HashMismatch :
|
|
49
|
+
SignatureVerificationException(
|
|
50
|
+
"Bundle hash verification failed. The bundle file hash does not match. File may be corrupted",
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
class HashCalculationFailed :
|
|
54
|
+
SignatureVerificationException(
|
|
55
|
+
"Failed to calculate file hash. Could not read file for hash verification",
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
class UnsignedNotAllowed :
|
|
59
|
+
SignatureVerificationException(
|
|
60
|
+
"Unsigned bundle not allowed when signing is enabled. " +
|
|
61
|
+
"Public key is configured but bundle is not signed. Rejecting update",
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
class SecurityFrameworkError(
|
|
65
|
+
cause: Throwable,
|
|
66
|
+
) : SignatureVerificationException(
|
|
67
|
+
"Security framework error during verification: ${cause.message}",
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Service for verifying bundle integrity through hash or RSA-SHA256 signature verification.
|
|
73
|
+
* Uses Java Signature API for cryptographic operations.
|
|
74
|
+
*
|
|
75
|
+
* fileHash format:
|
|
76
|
+
* - Signed: `sig:<base64_signature>` - Verify signature (implicitly verifies hash)
|
|
77
|
+
* - Unsigned: `<hex_hash>` - Verify SHA256 hash only
|
|
78
|
+
*
|
|
79
|
+
* Security rules:
|
|
80
|
+
* - null/empty fileHash → REJECT
|
|
81
|
+
* - sig:... + public key configured → verify signature → Install/REJECT
|
|
82
|
+
* - sig:... + public key NOT configured → REJECT (can't verify)
|
|
83
|
+
* - <hash> + public key configured → REJECT (unsigned not allowed)
|
|
84
|
+
* - <hash> + public key NOT configured → verify hash → Install/REJECT
|
|
85
|
+
*/
|
|
86
|
+
object SignatureVerifier {
|
|
87
|
+
private const val TAG = "SignatureVerifier"
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Reads public key from Android string resources.
|
|
91
|
+
* @param context Application context
|
|
92
|
+
* @return Public key PEM string or null if not configured
|
|
93
|
+
*/
|
|
94
|
+
private fun getPublicKeyFromConfig(context: Context): String? {
|
|
95
|
+
val resourceId =
|
|
96
|
+
context.resources.getIdentifier(
|
|
97
|
+
"hot_updater_public_key",
|
|
98
|
+
"string",
|
|
99
|
+
context.packageName,
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
if (resourceId == 0) {
|
|
103
|
+
Log.d(TAG, "hot_updater_public_key not found in strings.xml")
|
|
104
|
+
return null
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
val publicKeyPEM = context.getString(resourceId)
|
|
108
|
+
if (publicKeyPEM.isEmpty()) {
|
|
109
|
+
Log.d(TAG, "hot_updater_public_key is empty")
|
|
110
|
+
return null
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return publicKeyPEM
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Checks if signing is enabled (public key is configured).
|
|
118
|
+
* @param context Application context
|
|
119
|
+
* @return true if public key is configured
|
|
120
|
+
*/
|
|
121
|
+
fun isSigningEnabled(context: Context): Boolean = getPublicKeyFromConfig(context) != null
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Checks if fileHash is in signed format (starts with "sig:").
|
|
125
|
+
* @param fileHash The file hash string to check
|
|
126
|
+
* @return true if signed format
|
|
127
|
+
*/
|
|
128
|
+
fun isSignedFormat(fileHash: String?): Boolean = fileHash?.startsWith(SIGNED_HASH_PREFIX) == true
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Extracts signature from signed format fileHash.
|
|
132
|
+
* @param fileHash The signed file hash (sig:<signature>)
|
|
133
|
+
* @return Base64-encoded signature or null if not signed format
|
|
134
|
+
*/
|
|
135
|
+
fun extractSignature(fileHash: String?): String? {
|
|
136
|
+
if (!isSignedFormat(fileHash)) return null
|
|
137
|
+
return fileHash?.removePrefix(SIGNED_HASH_PREFIX)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Verifies bundle integrity based on fileHash format.
|
|
142
|
+
* Determines verification mode by checking for "sig:" prefix.
|
|
143
|
+
*
|
|
144
|
+
* @param context Application context
|
|
145
|
+
* @param bundleFile The bundle file to verify
|
|
146
|
+
* @param fileHash Combined hash string (sig:<signature> or <hex_hash>)
|
|
147
|
+
* @throws SignatureVerificationException if verification fails
|
|
148
|
+
*/
|
|
149
|
+
fun verifyBundle(
|
|
150
|
+
context: Context,
|
|
151
|
+
bundleFile: File,
|
|
152
|
+
fileHash: String?,
|
|
153
|
+
) {
|
|
154
|
+
val signingEnabled = isSigningEnabled(context)
|
|
155
|
+
|
|
156
|
+
// Rule: null/empty fileHash → REJECT
|
|
157
|
+
if (fileHash.isNullOrEmpty()) {
|
|
158
|
+
Log.e(TAG, "fileHash is null or empty. Rejecting update.")
|
|
159
|
+
throw SignatureVerificationException.VerificationFailed()
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (isSignedFormat(fileHash)) {
|
|
163
|
+
// Signed format: sig:<signature>
|
|
164
|
+
val signature = extractSignature(fileHash)
|
|
165
|
+
if (signature.isNullOrEmpty()) {
|
|
166
|
+
Log.e(TAG, "Failed to extract signature from fileHash")
|
|
167
|
+
throw SignatureVerificationException.InvalidSignatureFormat()
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Rule: sig:... + public key NOT configured → REJECT
|
|
171
|
+
if (!signingEnabled) {
|
|
172
|
+
Log.e(TAG, "Signed bundle but public key not configured. Cannot verify.")
|
|
173
|
+
throw SignatureVerificationException.PublicKeyNotConfigured()
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Rule: sig:... + public key configured → verify signature
|
|
177
|
+
verifySignature(context, bundleFile, signature)
|
|
178
|
+
} else {
|
|
179
|
+
// Unsigned format: <hex_hash>
|
|
180
|
+
|
|
181
|
+
// Rule: <hash> + public key configured → REJECT
|
|
182
|
+
if (signingEnabled) {
|
|
183
|
+
Log.e(TAG, "Unsigned bundle not allowed when signing is enabled. Rejecting.")
|
|
184
|
+
throw SignatureVerificationException.UnsignedNotAllowed()
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Rule: <hash> + public key NOT configured → verify hash
|
|
188
|
+
verifyHash(bundleFile, fileHash)
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Verifies SHA256 hash of a file.
|
|
194
|
+
* @param bundleFile The file to verify
|
|
195
|
+
* @param expectedHash Expected SHA256 hash (hex string)
|
|
196
|
+
* @throws SignatureVerificationException.HashMismatch if verification fails
|
|
197
|
+
*/
|
|
198
|
+
fun verifyHash(
|
|
199
|
+
bundleFile: File,
|
|
200
|
+
expectedHash: String,
|
|
201
|
+
) {
|
|
202
|
+
Log.d(TAG, "Verifying hash for file: ${bundleFile.name}")
|
|
203
|
+
|
|
204
|
+
if (!HashUtils.verifyHash(bundleFile, expectedHash)) {
|
|
205
|
+
Log.e(TAG, "Hash mismatch!")
|
|
206
|
+
throw SignatureVerificationException.HashMismatch()
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
Log.i(TAG, "✅ Hash verified successfully")
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Verifies RSA-SHA256 signature of a file.
|
|
214
|
+
* Calculates the file hash internally and verifies the signature.
|
|
215
|
+
*
|
|
216
|
+
* @param context Application context
|
|
217
|
+
* @param bundleFile The file to verify
|
|
218
|
+
* @param signatureBase64 Base64-encoded RSA-SHA256 signature
|
|
219
|
+
* @throws SignatureVerificationException if verification fails
|
|
220
|
+
*/
|
|
221
|
+
fun verifySignature(
|
|
222
|
+
context: Context,
|
|
223
|
+
bundleFile: File,
|
|
224
|
+
signatureBase64: String,
|
|
225
|
+
) {
|
|
226
|
+
Log.d(TAG, "Verifying signature for file: ${bundleFile.name}")
|
|
227
|
+
|
|
228
|
+
// Get public key from config
|
|
229
|
+
val publicKeyPEM =
|
|
230
|
+
getPublicKeyFromConfig(context)
|
|
231
|
+
?: run {
|
|
232
|
+
Log.e(TAG, "Cannot verify signature: public key not configured in strings.xml")
|
|
233
|
+
throw SignatureVerificationException.PublicKeyNotConfigured()
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
try {
|
|
237
|
+
// Convert PEM to PublicKey
|
|
238
|
+
val publicKey = createPublicKey(publicKeyPEM)
|
|
239
|
+
|
|
240
|
+
// Calculate file hash
|
|
241
|
+
val fileHashHex =
|
|
242
|
+
HashUtils.calculateSHA256(bundleFile)
|
|
243
|
+
?: run {
|
|
244
|
+
Log.e(TAG, "Failed to calculate file hash")
|
|
245
|
+
throw SignatureVerificationException.HashCalculationFailed()
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
Log.d(TAG, "Calculated file hash: $fileHashHex")
|
|
249
|
+
|
|
250
|
+
// Decode signature from base64
|
|
251
|
+
val signatureBytes =
|
|
252
|
+
try {
|
|
253
|
+
Base64.decode(signatureBase64, Base64.DEFAULT)
|
|
254
|
+
} catch (e: Exception) {
|
|
255
|
+
Log.e(TAG, "Failed to decode signature from base64", e)
|
|
256
|
+
throw SignatureVerificationException.InvalidSignatureFormat()
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Convert hex fileHash to bytes
|
|
260
|
+
val fileHashBytes = hexToByteArray(fileHashHex)
|
|
261
|
+
|
|
262
|
+
// Verify signature using RSA-SHA256
|
|
263
|
+
val verifier = Signature.getInstance("SHA256withRSA")
|
|
264
|
+
verifier.initVerify(publicKey)
|
|
265
|
+
verifier.update(fileHashBytes)
|
|
266
|
+
val isValid = verifier.verify(signatureBytes)
|
|
267
|
+
|
|
268
|
+
if (isValid) {
|
|
269
|
+
Log.i(TAG, "✅ Signature verified successfully")
|
|
270
|
+
} else {
|
|
271
|
+
Log.e(TAG, "❌ Signature verification failed")
|
|
272
|
+
throw SignatureVerificationException.VerificationFailed()
|
|
273
|
+
}
|
|
274
|
+
} catch (e: SignatureVerificationException) {
|
|
275
|
+
throw e
|
|
276
|
+
} catch (e: Exception) {
|
|
277
|
+
Log.e(TAG, "Signature verification error", e)
|
|
278
|
+
throw SignatureVerificationException.SecurityFrameworkError(e)
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Converts PEM-formatted public key to PublicKey.
|
|
284
|
+
* @param publicKeyPEM Public key in PEM format
|
|
285
|
+
* @return PublicKey instance
|
|
286
|
+
* @throws SignatureVerificationException.InvalidPublicKeyFormat if conversion fails
|
|
287
|
+
*/
|
|
288
|
+
private fun createPublicKey(publicKeyPEM: String): PublicKey {
|
|
289
|
+
try {
|
|
290
|
+
// Remove PEM headers/footers and whitespace
|
|
291
|
+
val publicKeyBase64 =
|
|
292
|
+
publicKeyPEM
|
|
293
|
+
.replace("-----BEGIN PUBLIC KEY-----", "")
|
|
294
|
+
.replace("-----END PUBLIC KEY-----", "")
|
|
295
|
+
.replace("\\n", "")
|
|
296
|
+
.replace("\n", "")
|
|
297
|
+
.replace("\r", "")
|
|
298
|
+
.replace(" ", "")
|
|
299
|
+
.trim()
|
|
300
|
+
|
|
301
|
+
// Decode base64
|
|
302
|
+
val keyBytes = Base64.decode(publicKeyBase64, Base64.DEFAULT)
|
|
303
|
+
|
|
304
|
+
// Create PublicKey from X.509 format (SubjectPublicKeyInfo)
|
|
305
|
+
val spec = X509EncodedKeySpec(keyBytes)
|
|
306
|
+
val keyFactory = KeyFactory.getInstance("RSA")
|
|
307
|
+
val publicKey = keyFactory.generatePublic(spec)
|
|
308
|
+
|
|
309
|
+
Log.d(TAG, "Public key loaded successfully")
|
|
310
|
+
return publicKey
|
|
311
|
+
} catch (e: Exception) {
|
|
312
|
+
Log.e(TAG, "Failed to create public key", e)
|
|
313
|
+
throw SignatureVerificationException.InvalidPublicKeyFormat()
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Converts hex string to ByteArray.
|
|
319
|
+
* @param hexString Hex-encoded string
|
|
320
|
+
* @return ByteArray
|
|
321
|
+
* @throws SignatureVerificationException.InvalidSignatureFormat if conversion fails
|
|
322
|
+
*/
|
|
323
|
+
private fun hexToByteArray(hexString: String): ByteArray {
|
|
324
|
+
try {
|
|
325
|
+
val len = hexString.length
|
|
326
|
+
if (len % 2 != 0) {
|
|
327
|
+
throw SignatureVerificationException.InvalidSignatureFormat()
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
val data = ByteArray(len / 2)
|
|
331
|
+
var i = 0
|
|
332
|
+
while (i < len) {
|
|
333
|
+
data[i / 2] =
|
|
334
|
+
(
|
|
335
|
+
(Character.digit(hexString[i], 16) shl 4) +
|
|
336
|
+
Character.digit(hexString[i + 1], 16)
|
|
337
|
+
).toByte()
|
|
338
|
+
i += 2
|
|
339
|
+
}
|
|
340
|
+
return data
|
|
341
|
+
} catch (e: Exception) {
|
|
342
|
+
Log.e(TAG, "Failed to convert hex to byte array", e)
|
|
343
|
+
throw SignatureVerificationException.InvalidSignatureFormat()
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
@@ -9,6 +9,7 @@ public enum BundleStorageError: Error, CustomNSError {
|
|
|
9
9
|
case invalidZipFile
|
|
10
10
|
case insufficientDiskSpace
|
|
11
11
|
case hashMismatch
|
|
12
|
+
case signatureVerificationFailed(SignatureVerificationError)
|
|
12
13
|
case moveOperationFailed(Error)
|
|
13
14
|
case copyOperationFailed(Error)
|
|
14
15
|
case fileSystemError(Error)
|
|
@@ -29,9 +30,10 @@ public enum BundleStorageError: Error, CustomNSError {
|
|
|
29
30
|
case .invalidZipFile: return 1006
|
|
30
31
|
case .insufficientDiskSpace: return 1007
|
|
31
32
|
case .hashMismatch: return 1008
|
|
32
|
-
case .
|
|
33
|
-
case .
|
|
34
|
-
case .
|
|
33
|
+
case .signatureVerificationFailed: return 1009
|
|
34
|
+
case .moveOperationFailed: return 1010
|
|
35
|
+
case .copyOperationFailed: return 1011
|
|
36
|
+
case .fileSystemError: return 1012
|
|
35
37
|
case .unknown: return 1099
|
|
36
38
|
}
|
|
37
39
|
}
|
|
@@ -74,6 +76,11 @@ public enum BundleStorageError: Error, CustomNSError {
|
|
|
74
76
|
userInfo[NSLocalizedDescriptionKey] = "Downloaded bundle hash does not match expected hash"
|
|
75
77
|
userInfo[NSLocalizedRecoverySuggestionErrorKey] = "The file may have been corrupted or tampered with. Try downloading again"
|
|
76
78
|
|
|
79
|
+
case .signatureVerificationFailed(let underlyingError):
|
|
80
|
+
userInfo[NSLocalizedDescriptionKey] = "Bundle signature verification failed"
|
|
81
|
+
userInfo[NSUnderlyingErrorKey] = underlyingError
|
|
82
|
+
userInfo[NSLocalizedRecoverySuggestionErrorKey] = "The bundle signature is invalid. Update rejected for security"
|
|
83
|
+
|
|
77
84
|
case .moveOperationFailed(let underlyingError):
|
|
78
85
|
userInfo[NSLocalizedDescriptionKey] = "Failed to move bundle to final location"
|
|
79
86
|
userInfo[NSUnderlyingErrorKey] = underlyingError
|
|
@@ -362,7 +369,7 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
362
369
|
* Updates the bundle from the specified URL. This operation is asynchronous.
|
|
363
370
|
* @param bundleId ID of the bundle to update
|
|
364
371
|
* @param fileUrl URL of the bundle file to download (or nil to reset)
|
|
365
|
-
* @param fileHash
|
|
372
|
+
* @param fileHash Combined hash string for verification (sig:<signature> or <hex_hash>)
|
|
366
373
|
* @param progressHandler Callback for download and extraction progress (0.0 to 1.0)
|
|
367
374
|
* @param completion Callback with result of the operation
|
|
368
375
|
*/
|
|
@@ -455,7 +462,7 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
455
462
|
* This method is part of the asynchronous `updateBundle` flow.
|
|
456
463
|
* @param bundleId ID of the bundle to update
|
|
457
464
|
* @param fileUrl URL of the bundle file to download
|
|
458
|
-
* @param fileHash
|
|
465
|
+
* @param fileHash Combined hash string for verification (sig:<signature> or <hex_hash>)
|
|
459
466
|
* @param storeDir Path to the bundle-store directory
|
|
460
467
|
* @param progressHandler Callback for download and extraction progress
|
|
461
468
|
* @param completion Callback with result of the operation
|
|
@@ -597,7 +604,7 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
597
604
|
* This method is part of the asynchronous `updateBundle` flow and is expected to run on a background thread.
|
|
598
605
|
* @param location URL of the downloaded file
|
|
599
606
|
* @param tempBundleFile Path to store the downloaded bundle file
|
|
600
|
-
* @param fileHash
|
|
607
|
+
* @param fileHash Combined hash string for verification (sig:<signature> or <hex_hash>)
|
|
601
608
|
* @param storeDir Path to the bundle-store directory
|
|
602
609
|
* @param bundleId ID of the bundle being processed
|
|
603
610
|
* @param tempDirectory Temporary directory for processing
|
|
@@ -645,18 +652,19 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
645
652
|
NSLog("[BundleStorage] Created tmpDir: \(tmpDir)")
|
|
646
653
|
logFileSystemDiagnostics(path: tmpDir, context: "TmpDir Created")
|
|
647
654
|
|
|
648
|
-
// 5) Verify
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
655
|
+
// 5) Verify bundle integrity (hash or signature based on fileHash format)
|
|
656
|
+
NSLog("[BundleStorage] Verifying bundle integrity...")
|
|
657
|
+
let tempBundleURL = URL(fileURLWithPath: tempBundleFile)
|
|
658
|
+
let verificationResult = SignatureVerifier.verifyBundle(fileURL: tempBundleURL, fileHash: fileHash)
|
|
659
|
+
switch verificationResult {
|
|
660
|
+
case .success:
|
|
661
|
+
NSLog("[BundleStorage] Bundle verification completed successfully")
|
|
662
|
+
case .failure(let error):
|
|
663
|
+
NSLog("[BundleStorage] Bundle verification failed: \(error)")
|
|
664
|
+
try? self.fileSystem.removeItem(atPath: tmpDir)
|
|
665
|
+
self.cleanupTemporaryFiles([tempDirectory])
|
|
666
|
+
completion(.failure(BundleStorageError.signatureVerificationFailed(error)))
|
|
667
|
+
return
|
|
660
668
|
}
|
|
661
669
|
|
|
662
670
|
// 6) Unzip directly into tmpDir with progress tracking (0.8 - 1.0)
|