@capgo/capacitor-updater 6.30.0 → 6.35.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.
@@ -400,16 +400,16 @@ public class CapgoUpdater {
400
400
 
401
401
  if (!this.hasOldPrivateKeyPropertyInConfig && !sessionKey.isEmpty()) {
402
402
  // V2 Encryption (publicKey)
403
- CryptoCipherV2.decryptFile(downloaded, publicKey, sessionKey);
404
- checksumDecrypted = CryptoCipherV2.decryptChecksum(checksumRes, publicKey);
405
- checksum = CryptoCipherV2.calcChecksum(downloaded);
403
+ CryptoCipher.decryptFile(downloaded, publicKey, sessionKey);
404
+ checksumDecrypted = CryptoCipher.decryptChecksum(checksumRes, publicKey);
405
+ checksum = CryptoCipher.calcChecksum(downloaded);
406
406
  } else if (this.hasOldPrivateKeyPropertyInConfig) {
407
- // V1 Encryption (privateKey) - deprecated but supported
408
- CryptoCipherV1.decryptFile(downloaded, privateKey, sessionKey, version);
409
- checksum = CryptoCipherV1.calcChecksum(downloaded);
410
- } else {
411
- checksum = CryptoCipherV2.calcChecksum(downloaded);
407
+ // V1 Encryption (privateKey) - deprecated not supported
408
+ this.sendStats("checksum_fail");
409
+ throw new IOException("V1 decryption is no longer supported for security reasons.");
412
410
  }
411
+ CryptoCipher.logChecksumInfo("Calculated checksum", checksum);
412
+ CryptoCipher.logChecksumInfo("Expected checksum", checksumDecrypted);
413
413
  if ((!checksumDecrypted.isEmpty() || !this.publicKey.isEmpty()) && !checksumDecrypted.equals(checksum)) {
414
414
  logger.error("Error checksum '" + checksumDecrypted + "' '" + checksum + "' '");
415
415
  this.sendStats("checksum_fail");
@@ -973,115 +973,41 @@ public class CapgoUpdater {
973
973
  makeJsonRequest(updateUrl, json, callback);
974
974
  }
975
975
 
976
- public void unsetChannel(final Callback callback) {
977
- // Check if rate limit was exceeded
978
- if (rateLimitExceeded) {
979
- logger.debug("Skipping unsetChannel due to rate limit (429). Requests will resume after app restart.");
980
- final Map<String, Object> retError = new HashMap<>();
981
- retError.put("message", "Rate limit exceeded");
982
- retError.put("error", "rate_limit_exceeded");
983
- callback.callback(retError);
984
- return;
985
- }
976
+ public void unsetChannel(
977
+ final SharedPreferences.Editor editor,
978
+ final String defaultChannelKey,
979
+ final String configDefaultChannel,
980
+ final Callback callback
981
+ ) {
982
+ // Clear persisted defaultChannel and revert to config value
983
+ editor.remove(defaultChannelKey);
984
+ editor.apply();
985
+ this.defaultChannel = configDefaultChannel;
986
+ logger.info("Persisted defaultChannel cleared, reverted to config value: " + configDefaultChannel);
987
+
988
+ Map<String, Object> ret = new HashMap<>();
989
+ ret.put("status", "ok");
990
+ ret.put("message", "Channel override removed");
991
+ callback.callback(ret);
992
+ }
986
993
 
987
- String channelUrl = this.channelUrl;
988
- if (channelUrl == null || channelUrl.isEmpty()) {
989
- logger.error("Channel URL is not set");
994
+ public void setChannel(
995
+ final String channel,
996
+ final SharedPreferences.Editor editor,
997
+ final String defaultChannelKey,
998
+ final boolean allowSetDefaultChannel,
999
+ final Callback callback
1000
+ ) {
1001
+ // Check if setting defaultChannel is allowed
1002
+ if (!allowSetDefaultChannel) {
1003
+ logger.error("setChannel is disabled by allowSetDefaultChannel config");
990
1004
  final Map<String, Object> retError = new HashMap<>();
991
- retError.put("message", "channelUrl missing");
992
- retError.put("error", "missing_config");
1005
+ retError.put("message", "setChannel is disabled by configuration");
1006
+ retError.put("error", "disabled_by_config");
993
1007
  callback.callback(retError);
994
1008
  return;
995
1009
  }
996
- JSONObject json;
997
- try {
998
- json = this.createInfoObject();
999
- } catch (JSONException e) {
1000
- logger.error("Error unsetChannel JSONException " + e.getMessage());
1001
- final Map<String, Object> retError = new HashMap<>();
1002
- retError.put("message", "Cannot get info: " + e);
1003
- retError.put("error", "json_error");
1004
- callback.callback(retError);
1005
- return;
1006
- }
1007
-
1008
- Request request = new Request.Builder()
1009
- .url(channelUrl)
1010
- .delete(RequestBody.create(json.toString(), MediaType.get("application/json")))
1011
- .build();
1012
-
1013
- DownloadService.sharedClient
1014
- .newCall(request)
1015
- .enqueue(
1016
- new okhttp3.Callback() {
1017
- @Override
1018
- public void onFailure(@NonNull Call call, @NonNull IOException e) {
1019
- Map<String, Object> retError = new HashMap<>();
1020
- retError.put("message", "Request failed: " + e.getMessage());
1021
- retError.put("error", "network_error");
1022
- callback.callback(retError);
1023
- }
1024
1010
 
1025
- @Override
1026
- public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException {
1027
- try (ResponseBody responseBody = response.body()) {
1028
- // Check for 429 rate limit
1029
- if (checkAndHandleRateLimitResponse(response)) {
1030
- Map<String, Object> retError = new HashMap<>();
1031
- retError.put("message", "Rate limit exceeded");
1032
- retError.put("error", "rate_limit_exceeded");
1033
- callback.callback(retError);
1034
- return;
1035
- }
1036
-
1037
- if (!response.isSuccessful()) {
1038
- Map<String, Object> retError = new HashMap<>();
1039
- retError.put("message", "Server error: " + response.code());
1040
- retError.put("error", "response_error");
1041
- callback.callback(retError);
1042
- return;
1043
- }
1044
-
1045
- assert responseBody != null;
1046
- String responseData = responseBody.string();
1047
- JSONObject jsonResponse = new JSONObject(responseData);
1048
-
1049
- // Check for server-side errors first
1050
- if (jsonResponse.has("error")) {
1051
- Map<String, Object> retError = new HashMap<>();
1052
- retError.put("error", jsonResponse.getString("error"));
1053
- if (jsonResponse.has("message")) {
1054
- retError.put("message", jsonResponse.getString("message"));
1055
- } else {
1056
- retError.put("message", "server did not provide a message");
1057
- }
1058
- callback.callback(retError);
1059
- return;
1060
- }
1061
-
1062
- Map<String, Object> ret = new HashMap<>();
1063
-
1064
- Iterator<String> keys = jsonResponse.keys();
1065
- while (keys.hasNext()) {
1066
- String key = keys.next();
1067
- if (jsonResponse.has(key)) {
1068
- ret.put(key, jsonResponse.get(key));
1069
- }
1070
- }
1071
- logger.info("Channel unset");
1072
- callback.callback(ret);
1073
- } catch (JSONException e) {
1074
- Map<String, Object> retError = new HashMap<>();
1075
- retError.put("message", "JSON parse error: " + e.getMessage());
1076
- retError.put("error", "parse_error");
1077
- callback.callback(retError);
1078
- }
1079
- }
1080
- }
1081
- );
1082
- }
1083
-
1084
- public void setChannel(final String channel, final Callback callback) {
1085
1011
  // Check if rate limit was exceeded
1086
1012
  if (rateLimitExceeded) {
1087
1013
  logger.debug("Skipping setChannel due to rate limit (429). Requests will resume after app restart.");
@@ -1114,7 +1040,18 @@ public class CapgoUpdater {
1114
1040
  return;
1115
1041
  }
1116
1042
 
1117
- makeJsonRequest(channelUrl, json, callback);
1043
+ makeJsonRequest(channelUrl, json, (res) -> {
1044
+ if (res.containsKey("error")) {
1045
+ callback.callback(res);
1046
+ } else {
1047
+ // Success - persist defaultChannel
1048
+ this.defaultChannel = channel;
1049
+ editor.putString(defaultChannelKey, channel);
1050
+ editor.apply();
1051
+ logger.info("defaultChannel persisted locally: " + channel);
1052
+ callback.callback(res);
1053
+ }
1054
+ });
1118
1055
  }
1119
1056
 
1120
1057
  public void getChannel(final Callback callback) {
@@ -10,8 +10,6 @@ package ee.forgr.capacitor_updater;
10
10
  * Created by Awesometic
11
11
  * It's encrypt returns Base64 encoded, and also decrypt for Base64 encoded cipher
12
12
  * references: http://stackoverflow.com/questions/12471999/rsa-encryption-decryption-in-android
13
- *
14
- * V2 Encryption - uses publicKey (modern encryption from main branch)
15
13
  */
16
14
  import android.util.Base64;
17
15
  import java.io.BufferedInputStream;
@@ -37,7 +35,7 @@ import javax.crypto.SecretKey;
37
35
  import javax.crypto.spec.IvParameterSpec;
38
36
  import javax.crypto.spec.SecretKeySpec;
39
37
 
40
- public class CryptoCipherV2 {
38
+ public class CryptoCipher {
41
39
 
42
40
  private static Logger logger;
43
41
 
@@ -155,10 +153,10 @@ public class CryptoCipherV2 {
155
153
  String sessionKeyB64 = ivSessionKey.split(":")[1];
156
154
  byte[] iv = Base64.decode(ivB64.getBytes(), Base64.DEFAULT);
157
155
  byte[] sessionKey = Base64.decode(sessionKeyB64.getBytes(), Base64.DEFAULT);
158
- PublicKey pKey = CryptoCipherV2.stringToPublicKey(publicKey);
159
- byte[] decryptedSessionKey = CryptoCipherV2.decryptRSA(sessionKey, pKey);
156
+ PublicKey pKey = CryptoCipher.stringToPublicKey(publicKey);
157
+ byte[] decryptedSessionKey = CryptoCipher.decryptRSA(sessionKey, pKey);
160
158
 
161
- SecretKey sKey = CryptoCipherV2.byteToSessionKey(decryptedSessionKey);
159
+ SecretKey sKey = CryptoCipher.byteToSessionKey(decryptedSessionKey);
162
160
  byte[] content = new byte[(int) file.length()];
163
161
 
164
162
  try (
@@ -168,7 +166,7 @@ public class CryptoCipherV2 {
168
166
  ) {
169
167
  dis.readFully(content);
170
168
  dis.close();
171
- byte[] decrypted = CryptoCipherV2.decryptAES(content, sKey, iv);
169
+ byte[] decrypted = CryptoCipher.decryptAES(content, sKey, iv);
172
170
  // write the decrypted string to the file
173
171
  try (final FileOutputStream fos = new FileOutputStream(file.getAbsolutePath())) {
174
172
  fos.write(decrypted);
@@ -200,14 +198,26 @@ public class CryptoCipherV2 {
200
198
  // Determine if input is hex or base64 encoded
201
199
  // Hex strings only contain 0-9 and a-f, while base64 contains other characters
202
200
  byte[] checksumBytes;
201
+ String detectedFormat;
203
202
  if (checksum.matches("^[0-9a-fA-F]+$")) {
204
203
  // Hex encoded (new format from CLI for plugin versions >= 5.30.0, 6.30.0, 7.30.0)
205
204
  checksumBytes = hexStringToByteArray(checksum);
205
+ detectedFormat = "hex";
206
206
  } else {
207
207
  // TODO: remove backwards compatibility
208
208
  // Base64 encoded (old format for backwards compatibility)
209
209
  checksumBytes = Base64.decode(checksum, Base64.DEFAULT);
210
+ detectedFormat = "base64";
210
211
  }
212
+ logger.debug(
213
+ "Received encrypted checksum format: " +
214
+ detectedFormat +
215
+ " (length: " +
216
+ checksum.length() +
217
+ " chars, " +
218
+ checksumBytes.length +
219
+ " bytes)"
220
+ );
211
221
  PublicKey pKey = CryptoCipher.stringToPublicKey(publicKey);
212
222
  byte[] decryptedChecksum = CryptoCipher.decryptRSA(checksumBytes, pKey);
213
223
  // Return as hex string to match calcChecksum output format
@@ -217,13 +227,75 @@ public class CryptoCipherV2 {
217
227
  if (hex.length() == 1) hexString.append('0');
218
228
  hexString.append(hex);
219
229
  }
220
- return hexString.toString();
230
+ String result = hexString.toString();
231
+
232
+ // Detect checksum algorithm based on length
233
+ String detectedAlgorithm;
234
+ if (decryptedChecksum.length == 32) {
235
+ detectedAlgorithm = "SHA-256";
236
+ } else if (decryptedChecksum.length == 4) {
237
+ detectedAlgorithm = "CRC32 (deprecated)";
238
+ logger.error(
239
+ "CRC32 checksum detected. This algorithm is deprecated and no longer supported. Please update your CLI to use SHA-256 checksums."
240
+ );
241
+ } else {
242
+ detectedAlgorithm = "unknown (" + decryptedChecksum.length + " bytes)";
243
+ logger.error(
244
+ "Unknown checksum algorithm detected with " + decryptedChecksum.length + " bytes. Expected SHA-256 (32 bytes)."
245
+ );
246
+ }
247
+ logger.debug(
248
+ "Decrypted checksum: " +
249
+ detectedAlgorithm +
250
+ " hex format (length: " +
251
+ result.length() +
252
+ " chars, " +
253
+ decryptedChecksum.length +
254
+ " bytes)"
255
+ );
256
+ return result;
221
257
  } catch (GeneralSecurityException e) {
222
258
  logger.error("decryptChecksum fail: " + e.getMessage());
223
259
  throw new IOException("Decryption failed: " + e.getMessage());
224
260
  }
225
261
  }
226
262
 
263
+ /**
264
+ * Detect checksum algorithm based on hex string length.
265
+ * SHA-256 = 64 hex chars (32 bytes)
266
+ * CRC32 = 8 hex chars (4 bytes)
267
+ */
268
+ public static String detectChecksumAlgorithm(String hexChecksum) {
269
+ if (hexChecksum == null || hexChecksum.isEmpty()) {
270
+ return "empty";
271
+ }
272
+ int len = hexChecksum.length();
273
+ if (len == 64) {
274
+ return "SHA-256";
275
+ } else if (len == 8) {
276
+ return "CRC32 (deprecated)";
277
+ } else {
278
+ return "unknown (" + len + " hex chars)";
279
+ }
280
+ }
281
+
282
+ /**
283
+ * Log checksum info and warn if deprecated algorithm detected.
284
+ */
285
+ public static void logChecksumInfo(String label, String hexChecksum) {
286
+ String algorithm = detectChecksumAlgorithm(hexChecksum);
287
+ logger.debug(label + ": " + algorithm + " hex format (length: " + hexChecksum.length() + " chars)");
288
+ if (algorithm.contains("CRC32")) {
289
+ logger.error(
290
+ "CRC32 checksum detected. This algorithm is deprecated and no longer supported. Please update your CLI to use SHA-256 checksums."
291
+ );
292
+ } else if (algorithm.contains("unknown")) {
293
+ logger.error(
294
+ "Unknown checksum algorithm detected. Expected SHA-256 (64 hex chars) but got " + hexChecksum.length() + " chars."
295
+ );
296
+ }
297
+ }
298
+
227
299
  public static String calcChecksum(File file) {
228
300
  final int BUFFER_SIZE = 1024 * 1024 * 5; // 5 MB buffer size
229
301
  MessageDigest digest;
@@ -293,7 +293,7 @@ public class DownloadService extends Worker {
293
293
 
294
294
  if (!publicKey.isEmpty() && sessionKey != null && !sessionKey.isEmpty()) {
295
295
  try {
296
- fileHash = CryptoCipherV2.decryptChecksum(fileHash, publicKey);
296
+ fileHash = CryptoCipher.decryptChecksum(fileHash, publicKey);
297
297
  } catch (Exception e) {
298
298
  logger.error("Error decrypting checksum for " + fileName + "fileHash: " + fileHash);
299
299
  hasError.set(true);
@@ -302,7 +302,12 @@ public class DownloadService extends Worker {
302
302
  }
303
303
 
304
304
  final String finalFileHash = fileHash;
305
- File targetFile = new File(destFolder, fileName);
305
+
306
+ // Check if file is a Brotli file and remove .br extension from target
307
+ boolean isBrotli = fileName.endsWith(".br");
308
+ String targetFileName = isBrotli ? fileName.substring(0, fileName.length() - 3) : fileName;
309
+
310
+ File targetFile = new File(destFolder, targetFileName);
306
311
  File cacheFile = new File(cacheFolder, finalFileHash + "_" + new File(fileName).getName());
307
312
  File builtinFile = new File(builtinFolder, fileName);
308
313
 
@@ -313,6 +318,7 @@ public class DownloadService extends Worker {
313
318
  continue;
314
319
  }
315
320
 
321
+ final boolean finalIsBrotli = isBrotli;
316
322
  Future<?> future = executor.submit(() -> {
317
323
  try {
318
324
  if (builtinFile.exists() && verifyChecksum(builtinFile, finalFileHash)) {
@@ -322,7 +328,7 @@ public class DownloadService extends Worker {
322
328
  copyFile(cacheFile, targetFile);
323
329
  logger.debug("already cached " + fileName);
324
330
  } else {
325
- downloadAndVerify(downloadUrl, targetFile, cacheFile, finalFileHash, sessionKey, publicKey);
331
+ downloadAndVerify(downloadUrl, targetFile, cacheFile, finalFileHash, sessionKey, publicKey, finalIsBrotli);
326
332
  }
327
333
 
328
334
  long completed = completedFiles.incrementAndGet();
@@ -572,98 +578,103 @@ public class DownloadService extends Worker {
572
578
  File cacheFile,
573
579
  String expectedHash,
574
580
  String sessionKey,
575
- String publicKey
581
+ String publicKey,
582
+ boolean isBrotli
576
583
  ) throws Exception {
577
584
  logger.debug("downloadAndVerify " + downloadUrl);
578
585
 
579
586
  Request request = new Request.Builder().url(downloadUrl).build();
580
587
 
581
- // Check if file is a Brotli file
582
- boolean isBrotli = targetFile.getName().endsWith(".br");
583
-
584
- // Create final target file with .br extension removed if it's a Brotli file
585
- File finalTargetFile = isBrotli
586
- ? new File(targetFile.getParentFile(), targetFile.getName().substring(0, targetFile.getName().length() - 3))
587
- : targetFile;
588
+ // targetFile is already the final destination without .br extension
589
+ File finalTargetFile = targetFile;
588
590
 
589
591
  // Create a temporary file for the compressed data
590
592
  File compressedFile = new File(getApplicationContext().getCacheDir(), "temp_" + targetFile.getName() + ".tmp");
591
593
 
592
- try (Response response = sharedClient.newCall(request).execute()) {
593
- if (!response.isSuccessful()) {
594
- sendStatsAsync("download_manifest_file_fail", getInputData().getString(VERSION) + ":" + finalTargetFile.getName());
595
- throw new IOException("Unexpected response code: " + response.code());
596
- }
594
+ try {
595
+ try (Response response = sharedClient.newCall(request).execute()) {
596
+ if (!response.isSuccessful()) {
597
+ sendStatsAsync("download_manifest_file_fail", getInputData().getString(VERSION) + ":" + finalTargetFile.getName());
598
+ throw new IOException("Unexpected response code: " + response.code());
599
+ }
597
600
 
598
- // Download compressed file atomically
599
- ResponseBody responseBody = response.body();
600
- if (responseBody == null) {
601
- throw new IOException("Response body is null");
602
- }
601
+ // Download compressed file atomically
602
+ ResponseBody responseBody = response.body();
603
+ if (responseBody == null) {
604
+ throw new IOException("Response body is null");
605
+ }
603
606
 
604
- // Use OkIO for atomic write
605
- writeFileAtomic(compressedFile, responseBody.byteStream(), null);
607
+ // Use OkIO for atomic write
608
+ writeFileAtomic(compressedFile, responseBody.byteStream(), null);
606
609
 
607
- if (!publicKey.isEmpty() && sessionKey != null && !sessionKey.isEmpty()) {
608
- logger.debug("Decrypting file " + targetFile.getName());
609
- CryptoCipherV2.decryptFile(compressedFile, publicKey, sessionKey);
610
- }
610
+ if (!publicKey.isEmpty() && sessionKey != null && !sessionKey.isEmpty()) {
611
+ logger.debug("Decrypting file " + targetFile.getName());
612
+ CryptoCipher.decryptFile(compressedFile, publicKey, sessionKey);
613
+ }
611
614
 
612
- // Only decompress if file has .br extension
613
- if (isBrotli) {
614
- // Use new decompression method with atomic write
615
- try (FileInputStream fis = new FileInputStream(compressedFile)) {
616
- byte[] compressedData = new byte[(int) compressedFile.length()];
617
- fis.read(compressedData);
618
- byte[] decompressedData;
619
- try {
620
- decompressedData = decompressBrotli(compressedData, targetFile.getName());
621
- } catch (IOException e) {
622
- sendStatsAsync(
623
- "download_manifest_brotli_fail",
624
- getInputData().getString(VERSION) + ":" + finalTargetFile.getName()
625
- );
626
- throw e;
627
- }
615
+ // Only decompress if file has .br extension
616
+ if (isBrotli) {
617
+ // Use new decompression method with atomic write
618
+ try (FileInputStream fis = new FileInputStream(compressedFile)) {
619
+ byte[] compressedData = new byte[(int) compressedFile.length()];
620
+ fis.read(compressedData);
621
+ byte[] decompressedData;
622
+ try {
623
+ decompressedData = decompressBrotli(compressedData, targetFile.getName());
624
+ } catch (IOException e) {
625
+ sendStatsAsync(
626
+ "download_manifest_brotli_fail",
627
+ getInputData().getString(VERSION) + ":" + finalTargetFile.getName()
628
+ );
629
+ throw e;
630
+ }
628
631
 
629
- // Write decompressed data atomically
630
- try (java.io.ByteArrayInputStream bais = new java.io.ByteArrayInputStream(decompressedData)) {
631
- writeFileAtomic(finalTargetFile, bais, null);
632
+ // Write decompressed data atomically
633
+ try (java.io.ByteArrayInputStream bais = new java.io.ByteArrayInputStream(decompressedData)) {
634
+ writeFileAtomic(finalTargetFile, bais, null);
635
+ }
636
+ }
637
+ } else {
638
+ // Just copy the file without decompression using atomic operation
639
+ try (FileInputStream fis = new FileInputStream(compressedFile)) {
640
+ writeFileAtomic(finalTargetFile, fis, null);
632
641
  }
633
642
  }
634
- } else {
635
- // Just copy the file without decompression using atomic operation
636
- try (FileInputStream fis = new FileInputStream(compressedFile)) {
637
- writeFileAtomic(finalTargetFile, fis, null);
638
- }
639
- }
640
643
 
641
- // Delete the compressed file
642
- compressedFile.delete();
643
- String calculatedHash = CryptoCipherV2.calcChecksum(finalTargetFile);
644
-
645
- // Verify checksum
646
- if (calculatedHash.equals(expectedHash)) {
647
- // Only cache if checksum is correct - use atomic copy
648
- try (FileInputStream fis = new FileInputStream(finalTargetFile)) {
649
- writeFileAtomic(cacheFile, fis, expectedHash);
644
+ // Delete the compressed file
645
+ compressedFile.delete();
646
+ String calculatedHash = CryptoCipher.calcChecksum(finalTargetFile);
647
+ CryptoCipher.logChecksumInfo("Calculated checksum", calculatedHash);
648
+ CryptoCipher.logChecksumInfo("Expected checksum", expectedHash);
649
+
650
+ // Verify checksum
651
+ if (calculatedHash.equals(expectedHash)) {
652
+ // Only cache if checksum is correct - use atomic copy
653
+ try (FileInputStream fis = new FileInputStream(finalTargetFile)) {
654
+ writeFileAtomic(cacheFile, fis, expectedHash);
655
+ }
656
+ } else {
657
+ finalTargetFile.delete();
658
+ sendStatsAsync("download_manifest_checksum_fail", getInputData().getString(VERSION) + ":" + finalTargetFile.getName());
659
+ throw new IOException(
660
+ "Checksum verification failed for: " +
661
+ downloadUrl +
662
+ " " +
663
+ targetFile.getName() +
664
+ " expected: " +
665
+ expectedHash +
666
+ " calculated: " +
667
+ calculatedHash
668
+ );
650
669
  }
651
- } else {
652
- finalTargetFile.delete();
653
- sendStatsAsync("download_manifest_checksum_fail", getInputData().getString(VERSION) + ":" + finalTargetFile.getName());
654
- throw new IOException(
655
- "Checksum verification failed for: " +
656
- downloadUrl +
657
- " " +
658
- targetFile.getName() +
659
- " expected: " +
660
- expectedHash +
661
- " calculated: " +
662
- calculatedHash
663
- );
664
670
  }
665
671
  } catch (Exception e) {
666
672
  throw new IOException("Error in downloadAndVerify: " + e.getMessage());
673
+ } finally {
674
+ // Always cleanup the compressed temp file if it still exists
675
+ if (compressedFile.exists()) {
676
+ compressedFile.delete();
677
+ }
667
678
  }
668
679
  }
669
680
 
@@ -769,7 +780,7 @@ public class DownloadService extends Worker {
769
780
 
770
781
  // Verify checksum if provided
771
782
  if (expectedChecksum != null && !expectedChecksum.isEmpty()) {
772
- String actualChecksum = CryptoCipherV2.calcChecksum(tempFile);
783
+ String actualChecksum = CryptoCipher.calcChecksum(tempFile);
773
784
  if (!expectedChecksum.equalsIgnoreCase(actualChecksum)) {
774
785
  tempFile.delete();
775
786
  throw new IOException("Checksum verification failed");