@capgo/capacitor-updater 7.29.0 → 7.32.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.
@@ -71,7 +71,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
71
71
  private static final String[] BREAKING_EVENT_NAMES = { "breakingAvailable", "majorAvailable" };
72
72
  private static final String LAST_FAILED_BUNDLE_PREF_KEY = "CapacitorUpdater.lastFailedBundle";
73
73
 
74
- private final String pluginVersion = "7.29.0";
74
+ private final String pluginVersion = "7.32.0";
75
75
  private static final String DELAY_CONDITION_PREFERENCES = "";
76
76
 
77
77
  private SharedPreferences.Editor editor;
@@ -703,6 +703,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
703
703
  }
704
704
  }
705
705
  this.implementation.cleanupDownloadDirectories(allowedIds);
706
+ this.implementation.cleanupDeltaCache();
706
707
  }
707
708
  this.editor.putString("LatestNativeBuildVersion", this.currentBuildVersion);
708
709
  this.editor.apply();
@@ -403,6 +403,8 @@ public class CapgoUpdater {
403
403
  } else {
404
404
  checksum = CryptoCipher.calcChecksum(downloaded);
405
405
  }
406
+ CryptoCipher.logChecksumInfo("Calculated checksum", checksum);
407
+ CryptoCipher.logChecksumInfo("Expected checksum", checksumDecrypted);
406
408
  if ((!checksumDecrypted.isEmpty() || !this.publicKey.isEmpty()) && !checksumDecrypted.equals(checksum)) {
407
409
  logger.error("Error checksum '" + checksumDecrypted + "' '" + checksum + "' '");
408
410
  this.sendStats("checksum_fail");
@@ -491,6 +493,23 @@ public class CapgoUpdater {
491
493
  }
492
494
  }
493
495
 
496
+ public void cleanupDeltaCache() {
497
+ if (this.activity == null) {
498
+ logger.warn("Activity is null, skipping delta cache cleanup");
499
+ return;
500
+ }
501
+ final File cacheFolder = new File(this.activity.getCacheDir(), "capgo_downloads");
502
+ if (!cacheFolder.exists()) {
503
+ return;
504
+ }
505
+ try {
506
+ this.deleteDirectory(cacheFolder);
507
+ logger.info("Cleaned up delta cache folder");
508
+ } catch (IOException e) {
509
+ logger.error("Failed to cleanup delta cache: " + e.getMessage());
510
+ }
511
+ }
512
+
494
513
  public void cleanupDownloadDirectories(final Set<String> allowedIds) {
495
514
  if (this.documentsDir == null) {
496
515
  logger.warn("Documents directory is null, skipping download cleanup");
@@ -179,24 +179,123 @@ public class CryptoCipher {
179
179
  }
180
180
  }
181
181
 
182
+ private static byte[] hexStringToByteArray(String s) {
183
+ int len = s.length();
184
+ byte[] data = new byte[len / 2];
185
+ for (int i = 0; i < len; i += 2) {
186
+ data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4) + Character.digit(s.charAt(i + 1), 16));
187
+ }
188
+ return data;
189
+ }
190
+
182
191
  public static String decryptChecksum(String checksum, String publicKey) throws IOException {
183
192
  if (publicKey.isEmpty()) {
184
193
  logger.error("No encryption set (public key) ignored");
185
194
  return checksum;
186
195
  }
187
196
  try {
188
- byte[] checksumBytes = Base64.decode(checksum, Base64.DEFAULT);
197
+ // TODO: remove this in a month or two
198
+ // Determine if input is hex or base64 encoded
199
+ // Hex strings only contain 0-9 and a-f, while base64 contains other characters
200
+ byte[] checksumBytes;
201
+ String detectedFormat;
202
+ if (checksum.matches("^[0-9a-fA-F]+$")) {
203
+ // Hex encoded (new format from CLI for plugin versions >= 5.30.0, 6.30.0, 7.30.0)
204
+ checksumBytes = hexStringToByteArray(checksum);
205
+ detectedFormat = "hex";
206
+ } else {
207
+ // TODO: remove backwards compatibility
208
+ // Base64 encoded (old format for backwards compatibility)
209
+ checksumBytes = Base64.decode(checksum, Base64.DEFAULT);
210
+ detectedFormat = "base64";
211
+ }
212
+ logger.debug(
213
+ "Received encrypted checksum format: " +
214
+ detectedFormat +
215
+ " (length: " +
216
+ checksum.length() +
217
+ " chars, " +
218
+ checksumBytes.length +
219
+ " bytes)"
220
+ );
189
221
  PublicKey pKey = CryptoCipher.stringToPublicKey(publicKey);
190
222
  byte[] decryptedChecksum = CryptoCipher.decryptRSA(checksumBytes, pKey);
191
- // return Base64.encodeToString(decryptedChecksum, Base64.DEFAULT);
192
- String result = Base64.encodeToString(decryptedChecksum, Base64.DEFAULT);
193
- return result.replaceAll("\\s", ""); // Remove all whitespace, including newlines
223
+ // Return as hex string to match calcChecksum output format
224
+ StringBuilder hexString = new StringBuilder();
225
+ for (byte b : decryptedChecksum) {
226
+ String hex = Integer.toHexString(0xff & b);
227
+ if (hex.length() == 1) hexString.append('0');
228
+ hexString.append(hex);
229
+ }
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;
194
257
  } catch (GeneralSecurityException e) {
195
258
  logger.error("decryptChecksum fail: " + e.getMessage());
196
259
  throw new IOException("Decryption failed: " + e.getMessage());
197
260
  }
198
261
  }
199
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
+
200
299
  public static String calcChecksum(File file) {
201
300
  final int BUFFER_SIZE = 1024 * 1024 * 5; // 5 MB buffer size
202
301
  MessageDigest digest;
@@ -641,6 +641,8 @@ public class DownloadService extends Worker {
641
641
  // Delete the compressed file
642
642
  compressedFile.delete();
643
643
  String calculatedHash = CryptoCipher.calcChecksum(finalTargetFile);
644
+ CryptoCipher.logChecksumInfo("Calculated checksum", calculatedHash);
645
+ CryptoCipher.logChecksumInfo("Expected checksum", expectedHash);
644
646
 
645
647
  // Verify checksum
646
648
  if (calculatedHash.equals(expectedHash)) {
@@ -54,7 +54,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
54
54
  CAPPluginMethod(name: "isShakeMenuEnabled", returnType: CAPPluginReturnPromise)
55
55
  ]
56
56
  public var implementation = CapgoUpdater()
57
- private let pluginVersion: String = "7.29.0"
57
+ private let pluginVersion: String = "7.32.0"
58
58
  static let updateUrlDefault = "https://plugin.capgo.app/updates"
59
59
  static let statsUrlDefault = "https://plugin.capgo.app/stats"
60
60
  static let channelUrlDefault = "https://plugin.capgo.app/channel_self"
@@ -366,6 +366,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
366
366
  return id.isEmpty ? nil : id
367
367
  })
368
368
  implementation.cleanupDownloadDirectories(allowedIds: allowedIds)
369
+ implementation.cleanupDeltaCache()
369
370
  }
370
371
  UserDefaults.standard.set(self.currentBuildVersion, forKey: "LatestNativeBuildVersion")
371
372
  UserDefaults.standard.synchronize()
@@ -501,6 +502,8 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
501
502
  }
502
503
 
503
504
  checksum = try CryptoCipher.decryptChecksum(checksum: checksum, publicKey: self.implementation.publicKey)
505
+ CryptoCipher.logChecksumInfo(label: "Bundle checksum", hexChecksum: next.getChecksum())
506
+ CryptoCipher.logChecksumInfo(label: "Expected checksum", hexChecksum: checksum)
504
507
  if (checksum != "" || self.implementation.publicKey != "") && next.getChecksum() != checksum {
505
508
  self.logger.error("Error checksum \(next.getChecksum()) \(checksum)")
506
509
  self.implementation.sendStats(action: "checksum_fail", versionName: next.getVersionName())
@@ -1332,6 +1335,8 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
1332
1335
  return
1333
1336
  }
1334
1337
  res.checksum = try CryptoCipher.decryptChecksum(checksum: res.checksum, publicKey: self.implementation.publicKey)
1338
+ CryptoCipher.logChecksumInfo(label: "Bundle checksum", hexChecksum: next.getChecksum())
1339
+ CryptoCipher.logChecksumInfo(label: "Expected checksum", hexChecksum: res.checksum)
1335
1340
  if res.checksum != "" && next.getChecksum() != res.checksum && res.manifest == nil {
1336
1341
  self.logger.error("Error checksum \(next.getChecksum()) \(res.checksum)")
1337
1342
  self.implementation.sendStats(action: "checksum_fail", versionName: next.getVersionName())
@@ -520,6 +520,8 @@ import UIKit
520
520
  if !self.publicKey.isEmpty && !sessionKey.isEmpty {
521
521
  // assume that calcChecksum != null
522
522
  let calculatedChecksum = CryptoCipher.calcChecksum(filePath: destFilePath)
523
+ CryptoCipher.logChecksumInfo(label: "Calculated checksum", hexChecksum: calculatedChecksum)
524
+ CryptoCipher.logChecksumInfo(label: "Expected checksum", hexChecksum: fileHash)
523
525
  if calculatedChecksum != fileHash {
524
526
  self.sendStats(action: "download_manifest_checksum_fail", versionName: "\(version):\(finalFileName)")
525
527
  throw NSError(domain: "ChecksumError", code: 1, userInfo: [NSLocalizedDescriptionKey: "Computed checksum is not equal to required checksum (\(calculatedChecksum) != \(fileHash)) for file \(fileName) at url \(downloadUrl)"])
@@ -781,6 +783,7 @@ import UIKit
781
783
 
782
784
  do {
783
785
  checksum = CryptoCipher.calcChecksum(filePath: finalPath)
786
+ CryptoCipher.logChecksumInfo(label: "Calculated bundle checksum", hexChecksum: checksum)
784
787
  logger.info("Downloading: 80% (unzipping)")
785
788
  try self.saveDownloaded(sourceZip: finalPath, id: id, base: self.libraryDir.appendingPathComponent(self.bundleDirectory), notify: true)
786
789
 
@@ -975,6 +978,19 @@ import UIKit
975
978
  return self.delete(id: id, removeInfo: true)
976
979
  }
977
980
 
981
+ public func cleanupDeltaCache() {
982
+ let fileManager = FileManager.default
983
+ guard fileManager.fileExists(atPath: cacheFolder.path) else {
984
+ return
985
+ }
986
+ do {
987
+ try fileManager.removeItem(at: cacheFolder)
988
+ logger.info("Cleaned up delta cache folder")
989
+ } catch {
990
+ logger.error("Failed to cleanup delta cache: \(error.localizedDescription)")
991
+ }
992
+ }
993
+
978
994
  public func cleanupDownloadDirectories(allowedIds: Set<String>) {
979
995
  let bundleRoot = libraryDir.appendingPathComponent(bundleDirectory)
980
996
  let fileManager = FileManager.default
@@ -15,16 +15,52 @@ public struct CryptoCipher {
15
15
  self.logger = logger
16
16
  }
17
17
 
18
+ private static func hexStringToData(_ hex: String) -> Data? {
19
+ var data = Data()
20
+ var hexIterator = hex.makeIterator()
21
+ while let c1 = hexIterator.next(), let c2 = hexIterator.next() {
22
+ guard let byte = UInt8(String([c1, c2]), radix: 16) else {
23
+ return nil
24
+ }
25
+ data.append(byte)
26
+ }
27
+ return data
28
+ }
29
+
30
+ private static func isHexString(_ str: String) -> Bool {
31
+ let hexCharacterSet = CharacterSet(charactersIn: "0123456789abcdefABCDEF")
32
+ return str.unicodeScalars.allSatisfy { hexCharacterSet.contains($0) }
33
+ }
34
+
18
35
  public static func decryptChecksum(checksum: String, publicKey: String) throws -> String {
19
36
  if publicKey.isEmpty {
20
37
  logger.info("No encryption set (public key) ignored")
21
38
  return checksum
22
39
  }
23
40
  do {
24
- guard let checksumBytes = Data(base64Encoded: checksum) else {
25
- logger.error("Cannot decode checksum as base64: \(checksum)")
26
- throw CustomError.cannotDecode
41
+ // Determine if input is hex or base64 encoded
42
+ // Hex strings only contain 0-9 and a-f, while base64 contains other characters
43
+ let checksumBytes: Data
44
+ let detectedFormat: String
45
+ if isHexString(checksum) {
46
+ // Hex encoded (new format from CLI for plugin versions >= 5.30.0, 6.30.0, 7.30.0)
47
+ guard let hexData = hexStringToData(checksum) else {
48
+ logger.error("Cannot decode checksum as hex: \(checksum)")
49
+ throw CustomError.cannotDecode
50
+ }
51
+ checksumBytes = hexData
52
+ detectedFormat = "hex"
53
+ } else {
54
+ // TODO: remove backwards compatibility
55
+ // Base64 encoded (old format for backwards compatibility)
56
+ guard let base64Data = Data(base64Encoded: checksum) else {
57
+ logger.error("Cannot decode checksum as base64: \(checksum)")
58
+ throw CustomError.cannotDecode
59
+ }
60
+ checksumBytes = base64Data
61
+ detectedFormat = "base64"
27
62
  }
63
+ logger.debug("Received encrypted checksum format: \(detectedFormat) (length: \(checksum.count) chars, \(checksumBytes.count) bytes)")
28
64
 
29
65
  if checksumBytes.isEmpty {
30
66
  logger.error("Decoded checksum is empty")
@@ -41,12 +77,56 @@ public struct CryptoCipher {
41
77
  throw NSError(domain: "Failed to decrypt session key data", code: 2, userInfo: nil)
42
78
  }
43
79
 
44
- return decryptedChecksum.base64EncodedString()
80
+ // Return as hex string to match calcChecksum output format
81
+ let result = decryptedChecksum.map { String(format: "%02x", $0) }.joined()
82
+
83
+ // Detect checksum algorithm based on length
84
+ let detectedAlgorithm: String
85
+ if decryptedChecksum.count == 32 {
86
+ detectedAlgorithm = "SHA-256"
87
+ } else if decryptedChecksum.count == 4 {
88
+ detectedAlgorithm = "CRC32 (deprecated)"
89
+ logger.error("CRC32 checksum detected. This algorithm is deprecated and no longer supported. Please update your CLI to use SHA-256 checksums.")
90
+ } else {
91
+ detectedAlgorithm = "unknown (\(decryptedChecksum.count) bytes)"
92
+ logger.error("Unknown checksum algorithm detected with \(decryptedChecksum.count) bytes. Expected SHA-256 (32 bytes).")
93
+ }
94
+ logger.debug("Decrypted checksum: \(detectedAlgorithm) hex format (length: \(result.count) chars, \(decryptedChecksum.count) bytes)")
95
+ return result
45
96
  } catch {
46
97
  logger.error("decryptChecksum fail: \(error.localizedDescription)")
47
98
  throw CustomError.cannotDecode
48
99
  }
49
100
  }
101
+
102
+ /// Detect checksum algorithm based on hex string length.
103
+ /// SHA-256 = 64 hex chars (32 bytes)
104
+ /// CRC32 = 8 hex chars (4 bytes)
105
+ public static func detectChecksumAlgorithm(_ hexChecksum: String) -> String {
106
+ if hexChecksum.isEmpty {
107
+ return "empty"
108
+ }
109
+ let len = hexChecksum.count
110
+ if len == 64 {
111
+ return "SHA-256"
112
+ } else if len == 8 {
113
+ return "CRC32 (deprecated)"
114
+ } else {
115
+ return "unknown (\(len) hex chars)"
116
+ }
117
+ }
118
+
119
+ /// Log checksum info and warn if deprecated algorithm detected.
120
+ public static func logChecksumInfo(label: String, hexChecksum: String) {
121
+ let algorithm = detectChecksumAlgorithm(hexChecksum)
122
+ logger.debug("\(label): \(algorithm) hex format (length: \(hexChecksum.count) chars)")
123
+ if algorithm.contains("CRC32") {
124
+ logger.error("CRC32 checksum detected. This algorithm is deprecated and no longer supported. Please update your CLI to use SHA-256 checksums.")
125
+ } else if algorithm.contains("unknown") {
126
+ logger.error("Unknown checksum algorithm detected. Expected SHA-256 (64 hex chars) but got \(hexChecksum.count) chars.")
127
+ }
128
+ }
129
+
50
130
  public static func calcChecksum(filePath: URL) -> String {
51
131
  let bufferSize = 1024 * 1024 * 5 // 5 MB
52
132
  var sha256 = SHA256()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@capgo/capacitor-updater",
3
- "version": "7.29.0",
3
+ "version": "7.32.0",
4
4
  "license": "MPL-2.0",
5
5
  "description": "Live update for capacitor apps",
6
6
  "main": "dist/plugin.cjs.js",