@hot-updater/react-native 0.22.2 → 0.23.1

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 (39) hide show
  1. package/android/src/main/java/com/hotupdater/BundleFileStorageService.kt +17 -12
  2. package/android/src/main/java/com/hotupdater/HotUpdater.kt +1 -1
  3. package/android/src/main/java/com/hotupdater/HotUpdaterFactory.kt +1 -0
  4. package/android/src/main/java/com/hotupdater/HotUpdaterImpl.kt +1 -1
  5. package/android/src/main/java/com/hotupdater/SignatureVerifier.kt +346 -0
  6. package/ios/HotUpdater/Internal/BundleFileStorageService.swift +26 -18
  7. package/ios/HotUpdater/Internal/SignatureVerifier.swift +339 -0
  8. package/lib/commonjs/checkForUpdate.js +1 -1
  9. package/lib/commonjs/checkForUpdate.js.map +1 -1
  10. package/lib/commonjs/index.js +16 -1
  11. package/lib/commonjs/index.js.map +1 -1
  12. package/lib/commonjs/specs/NativeHotUpdater.js.map +1 -1
  13. package/lib/commonjs/types.js +45 -0
  14. package/lib/commonjs/types.js.map +1 -0
  15. package/lib/module/checkForUpdate.js +1 -1
  16. package/lib/module/checkForUpdate.js.map +1 -1
  17. package/lib/module/index.js +1 -0
  18. package/lib/module/index.js.map +1 -1
  19. package/lib/module/specs/NativeHotUpdater.js.map +1 -1
  20. package/lib/module/types.js +40 -0
  21. package/lib/module/types.js.map +1 -0
  22. package/lib/typescript/commonjs/index.d.ts +1 -0
  23. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  24. package/lib/typescript/commonjs/specs/NativeHotUpdater.d.ts +7 -2
  25. package/lib/typescript/commonjs/specs/NativeHotUpdater.d.ts.map +1 -1
  26. package/lib/typescript/commonjs/types.d.ts +34 -0
  27. package/lib/typescript/commonjs/types.d.ts.map +1 -0
  28. package/lib/typescript/module/index.d.ts +1 -0
  29. package/lib/typescript/module/index.d.ts.map +1 -1
  30. package/lib/typescript/module/specs/NativeHotUpdater.d.ts +7 -2
  31. package/lib/typescript/module/specs/NativeHotUpdater.d.ts.map +1 -1
  32. package/lib/typescript/module/types.d.ts +34 -0
  33. package/lib/typescript/module/types.d.ts.map +1 -0
  34. package/package.json +7 -6
  35. package/plugin/build/withHotUpdater.js +55 -4
  36. package/src/checkForUpdate.ts +1 -1
  37. package/src/index.ts +5 -0
  38. package/src/specs/NativeHotUpdater.ts +7 -2
  39. 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 SHA256 hash of the bundle file for verification (nullable)
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("BundleStorage", "updateBundle bundleId $bundleId fileUrl $fileUrl fileHash $fileHash")
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 file hash if provided
188
- if (!fileHash.isNullOrEmpty()) {
189
- Log.d("BundleStorage", "Verifying file hash...")
190
- if (!HashUtils.verifyHash(tempBundleFile, fileHash)) {
191
- Log.d("BundleStorage", "Hash mismatch! Deleting and aborting.")
192
- tempDir.deleteRecursively()
193
- tempBundleFile.delete()
194
- return@withContext false
195
- }
196
- Log.d("BundleStorage", "Hash verification passed")
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 SHA256 hash of the bundle file for verification (nullable)
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
  */
@@ -39,6 +39,7 @@ object HotUpdaterFactory {
39
39
  // Create bundle storage with dependencies
40
40
  val bundleStorage =
41
41
  BundleFileStorageService(
42
+ appContext,
42
43
  fileSystem,
43
44
  downloadService,
44
45
  decompressService,
@@ -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 SHA256 hash of the bundle file for verification (nullable)
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 .moveOperationFailed: return 1009
33
- case .copyOperationFailed: return 1010
34
- case .fileSystemError: return 1011
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 SHA256 hash of the bundle file for verification (nullable)
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 SHA256 hash of the bundle file for verification (nullable)
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 SHA256 hash of the bundle file for verification (nullable)
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 file hash if provided
649
- if let expectedHash = fileHash {
650
- NSLog("[BundleStorage] Verifying file hash...")
651
- let tempBundleURL = URL(fileURLWithPath: tempBundleFile)
652
- guard HashUtils.verifyHash(fileURL: tempBundleURL, expectedHash: expectedHash) else {
653
- NSLog("[BundleStorage] Hash mismatch!")
654
- try? self.fileSystem.removeItem(atPath: tmpDir)
655
- self.cleanupTemporaryFiles([tempDirectory])
656
- completion(.failure(BundleStorageError.hashMismatch))
657
- return
658
- }
659
- NSLog("[BundleStorage] Hash verification passed")
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)