@capgo/capacitor-updater 7.51.21 → 7.51.23

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.
@@ -363,6 +363,38 @@ public class DownloadService extends Worker {
363
363
  return seenTargets.add(targetFile.getCanonicalPath());
364
364
  }
365
365
 
366
+ private static final class ManifestDownloadTask {
367
+
368
+ final String fileName;
369
+ final String finalFileHash;
370
+ final String downloadUrl;
371
+ final boolean isBrotli;
372
+ final File targetFile;
373
+ final File builtinFile;
374
+ final File cacheFile;
375
+ final File legacyCacheFile;
376
+
377
+ ManifestDownloadTask(
378
+ final String fileName,
379
+ final String finalFileHash,
380
+ final String downloadUrl,
381
+ final boolean isBrotli,
382
+ final File targetFile,
383
+ final File builtinFile,
384
+ final File cacheFile,
385
+ final File legacyCacheFile
386
+ ) {
387
+ this.fileName = fileName;
388
+ this.finalFileHash = finalFileHash;
389
+ this.downloadUrl = downloadUrl;
390
+ this.isBrotli = isBrotli;
391
+ this.targetFile = targetFile;
392
+ this.builtinFile = builtinFile;
393
+ this.cacheFile = cacheFile;
394
+ this.legacyCacheFile = legacyCacheFile;
395
+ }
396
+ }
397
+
366
398
  static boolean tryCopyBuiltinAsset(final AssetManager assets, final String fileName, final File dest, final String expectedHash) {
367
399
  if (assets == null || fileName == null || dest == null) {
368
400
  return false;
@@ -424,7 +456,7 @@ public class DownloadService extends Worker {
424
456
  return createFailureResult("Manifest is null");
425
457
  }
426
458
  } else {
427
- handleSingleFileDownload(url, id, documentsDir, dest, version, sessionKey, checksum);
459
+ handleSingleFileDownload(url, id, documentsDir, dest, version, sessionKey, checksum, publicKey);
428
460
  return createSuccessResult(dest, version, sessionKey, checksum, false);
429
461
  }
430
462
  } catch (DownloadRetryException e) {
@@ -515,6 +547,12 @@ public class DownloadService extends Worker {
515
547
  try {
516
548
  logger.debug("handleManifestDownload");
517
549
 
550
+ if (publicKey != null && !publicKey.isEmpty() && !CryptoCipher.isValidSessionKey(sessionKey)) {
551
+ logger.error("Public key present but no valid session key provided");
552
+ sendStatsAsync("session_key_required", version);
553
+ throw new IOException("Session key required when public key is present");
554
+ }
555
+
518
556
  // Send stats for manifest download start
519
557
  sendStatsAsync("download_manifest_start", version);
520
558
 
@@ -535,9 +573,7 @@ public class DownloadService extends Worker {
535
573
  int totalFiles = manifest.length();
536
574
  final AtomicLong completedFiles = new AtomicLong(0);
537
575
  final AtomicBoolean hasError = new AtomicBoolean(false);
538
-
539
- ExecutorService executor = Executors.newFixedThreadPool(Math.min(MANIFEST_MAX_CONCURRENT_FILES, Math.max(1, totalFiles)));
540
- List<Future<?>> futures = new ArrayList<>();
576
+ final List<ManifestDownloadTask> tasks = new ArrayList<>();
541
577
  final Set<String> seenTargets = new HashSet<>();
542
578
 
543
579
  for (int i = 0; i < totalFiles; i++) {
@@ -552,7 +588,13 @@ public class DownloadService extends Worker {
552
588
  continue;
553
589
  }
554
590
 
555
- if (publicKey != null && !publicKey.isEmpty() && sessionKey != null && !sessionKey.isEmpty()) {
591
+ if (publicKey != null && !publicKey.isEmpty()) {
592
+ if (!CryptoCipher.isValidSessionKey(sessionKey)) {
593
+ logger.error("Public key present but no valid session key provided");
594
+ sendStatsAsync("session_key_required", version);
595
+ hasError.set(true);
596
+ continue;
597
+ }
556
598
  try {
557
599
  fileHash = CryptoCipher.decryptChecksum(fileHash, publicKey);
558
600
  } catch (Exception e) {
@@ -592,35 +634,56 @@ public class DownloadService extends Worker {
592
634
  final File legacyCacheFile =
593
635
  isBrotli && cacheFile != null ? new File(cacheFolder, finalFileHash + "_" + new File(fileName).getName()) : null;
594
636
 
595
- // Ensure parent directories of the target file exist
596
- if (!Objects.requireNonNull(targetFile.getParentFile()).exists() && !targetFile.getParentFile().mkdirs()) {
597
- logger.error("Failed to create parent directory for: " + targetFile.getAbsolutePath());
598
- hasError.set(true);
599
- continue;
637
+ tasks.add(
638
+ new ManifestDownloadTask(
639
+ fileName,
640
+ finalFileHash,
641
+ downloadUrl,
642
+ isBrotli,
643
+ targetFile,
644
+ builtinFile,
645
+ cacheFile,
646
+ legacyCacheFile
647
+ )
648
+ );
649
+ }
650
+
651
+ if (hasError.get()) {
652
+ throw new IOException("Manifest contains invalid or duplicate file paths");
653
+ }
654
+
655
+ for (final ManifestDownloadTask task : tasks) {
656
+ if (!Objects.requireNonNull(task.targetFile.getParentFile()).exists() && !task.targetFile.getParentFile().mkdirs()) {
657
+ logger.error("Failed to create parent directory for: " + task.targetFile.getAbsolutePath());
658
+ throw new IOException("Failed to create parent directory for: " + task.targetFile.getAbsolutePath());
600
659
  }
660
+ }
601
661
 
602
- final boolean finalIsBrotli = isBrotli;
662
+ ExecutorService executor = Executors.newFixedThreadPool(Math.min(MANIFEST_MAX_CONCURRENT_FILES, Math.max(1, totalFiles)));
663
+ List<Future<?>> futures = new ArrayList<>();
664
+
665
+ for (final ManifestDownloadTask task : tasks) {
603
666
  Future<?> future = executor.submit(() -> {
604
667
  try {
605
- if (tryCopyBuiltinAsset(assets, fileName, targetFile, finalFileHash)) {
606
- logger.debug("using builtin asset " + fileName);
607
- } else if (tryCopyBuiltinFile(builtinFile, targetFile, finalFileHash)) {
608
- logger.debug("using builtin file " + fileName);
668
+ if (tryCopyBuiltinAsset(assets, task.fileName, task.targetFile, task.finalFileHash)) {
669
+ logger.debug("using builtin asset " + task.fileName);
670
+ } else if (tryCopyBuiltinFile(task.builtinFile, task.targetFile, task.finalFileHash)) {
671
+ logger.debug("using builtin file " + task.fileName);
609
672
  } else if (
610
- tryCopyFromCache(cacheFile, targetFile, finalFileHash) ||
611
- (legacyCacheFile != null && tryCopyFromCache(legacyCacheFile, targetFile, finalFileHash))
673
+ tryCopyFromCache(task.cacheFile, task.targetFile, task.finalFileHash) ||
674
+ (task.legacyCacheFile != null && tryCopyFromCache(task.legacyCacheFile, task.targetFile, task.finalFileHash))
612
675
  ) {
613
- logger.debug("already cached " + fileName);
676
+ logger.debug("already cached " + task.fileName);
614
677
  } else {
615
678
  downloadAndVerify(
616
- downloadUrl,
617
- targetFile,
618
- cacheFile,
619
- finalFileHash,
679
+ task.downloadUrl,
680
+ task.targetFile,
681
+ task.cacheFile,
682
+ task.finalFileHash,
620
683
  sessionKey,
621
684
  publicKey,
622
- finalIsBrotli,
623
- fileName
685
+ task.isBrotli,
686
+ task.fileName
624
687
  );
625
688
  }
626
689
 
@@ -628,8 +691,8 @@ public class DownloadService extends Worker {
628
691
  int percent = calcTotalPercent(completed, totalFiles);
629
692
  setProgress(percent);
630
693
  } catch (Exception e) {
631
- logger.error("Error processing file: " + fileName + " " + e.getMessage());
632
- sendStatsAsync("download_manifest_file_fail", version + ":" + fileName);
694
+ logger.error("Error processing file: " + task.fileName + " " + e.getMessage());
695
+ sendStatsAsync("download_manifest_file_fail", version + ":" + task.fileName);
633
696
  hasError.set(true);
634
697
  }
635
698
  });
@@ -676,8 +739,20 @@ public class DownloadService extends Worker {
676
739
  String dest,
677
740
  String version,
678
741
  String sessionKey,
679
- String checksum
742
+ String checksum,
743
+ String publicKey
680
744
  ) {
745
+ if (publicKey != null && !publicKey.isEmpty() && !CryptoCipher.isValidSessionKey(sessionKey)) {
746
+ logger.error("Public key present but no valid session key provided");
747
+ sendStatsAsync("session_key_required", version);
748
+ throw new RuntimeException("Session key required when public key is present");
749
+ }
750
+ if (checksum == null || checksum.isEmpty()) {
751
+ logger.error("No checksum provided");
752
+ sendStatsAsync("checksum_required", version);
753
+ throw new RuntimeException("Checksum required");
754
+ }
755
+
681
756
  // Send stats for zip download start
682
757
  sendStatsAsync("download_zip_start", version);
683
758
 
@@ -1061,7 +1136,7 @@ public class DownloadService extends Worker {
1061
1136
  }
1062
1137
  }
1063
1138
 
1064
- boolean needDecrypt = publicKey != null && !publicKey.isEmpty() && sessionKey != null && !sessionKey.isEmpty();
1139
+ boolean needDecrypt = publicKey != null && !publicKey.isEmpty() && CryptoCipher.isValidSessionKey(sessionKey);
1065
1140
  File source = partial;
1066
1141
  if (needDecrypt) {
1067
1142
  workFile = new File(cacheFolder, "work_" + UUID.randomUUID() + "_" + targetFile.getName() + ".tmp");
@@ -96,7 +96,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
96
96
  deinit {
97
97
  implementation.shutdown()
98
98
  }
99
- private let pluginVersion: String = "7.51.21"
99
+ private let pluginVersion: String = "7.51.23"
100
100
  private let launchStartedAtMs = Int64(Date().timeIntervalSince1970 * 1000)
101
101
  static let updateUrlDefault = "https://plugin.capgo.app/updates"
102
102
  static let statsUrlDefault = "https://plugin.capgo.app/stats"
@@ -582,8 +582,10 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
582
582
  var dest: URL
583
583
  if BundleInfo.ID_BUILTIN == id {
584
584
  dest = Bundle.main.resourceURL!.appendingPathComponent("public")
585
+ } else if let bundleDir = try? self.implementation.getBundleDirectory(id: id) {
586
+ dest = bundleDir
585
587
  } else {
586
- dest = self.implementation.getBundleDirectory(id: id)
588
+ dest = Bundle.main.resourceURL!.appendingPathComponent("public")
587
589
  }
588
590
 
589
591
  if !FileManager.default.fileExists(atPath: dest.path) {
@@ -1611,7 +1613,19 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
1611
1613
  throw makePreviewError("Invalid download URL")
1612
1614
  }
1613
1615
 
1616
+ if !self.implementation.publicKey.isEmpty && !CryptoCipher.isValidSessionKey(sessionKey) {
1617
+ self.logger.error("Public key present but no valid session key provided")
1618
+ self.implementation.sendStats(action: "session_key_required", versionName: version)
1619
+ throw makePreviewError("Session key required when public key is present")
1620
+ }
1621
+
1614
1622
  var checksum = rawChecksum
1623
+ if manifestEntries == nil && checksum.isEmpty {
1624
+ self.logger.error("No checksum provided")
1625
+ self.implementation.sendStats(action: "checksum_required", versionName: version)
1626
+ throw makePreviewError("Checksum required")
1627
+ }
1628
+
1615
1629
  let next: BundleInfo
1616
1630
  if let manifestEntries = manifestEntries {
1617
1631
  next = try self.implementation.downloadManifest(manifest: manifestEntries, version: version, sessionKey: sessionKey)
@@ -1619,29 +1633,20 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
1619
1633
  next = try self.implementation.download(url: url, version: version, sessionKey: sessionKey)
1620
1634
  }
1621
1635
 
1622
- if self.implementation.publicKey != "" && checksum == "" {
1623
- self.logger.error("Public key present but no checksum provided")
1624
- self.implementation.sendStats(action: "checksum_required", versionName: next.getVersionName())
1625
- let id = next.getId()
1626
- let resDel = self.implementation.delete(id: id)
1627
- if !resDel {
1628
- self.logger.error("Delete failed, id \(id) doesn't exist")
1629
- }
1630
- throw ObjectSavableError.checksum
1631
- }
1632
-
1633
- checksum = try CryptoCipher.decryptChecksum(checksum: checksum, publicKey: self.implementation.publicKey)
1634
- CryptoCipher.logChecksumInfo(label: "Bundle checksum", hexChecksum: next.getChecksum())
1635
- CryptoCipher.logChecksumInfo(label: "Expected checksum", hexChecksum: checksum)
1636
- if (checksum != "" || self.implementation.publicKey != "") && next.getChecksum() != checksum {
1637
- self.logger.error("Error checksum \(next.getChecksum()) \(checksum)")
1638
- self.implementation.sendStats(action: "checksum_fail", versionName: next.getVersionName())
1639
- let id = next.getId()
1640
- let resDel = self.implementation.delete(id: id)
1641
- if !resDel {
1642
- self.logger.error("Delete failed, id \(id) doesn't exist")
1636
+ if manifestEntries == nil {
1637
+ checksum = try CryptoCipher.decryptChecksum(checksum: checksum, publicKey: self.implementation.publicKey)
1638
+ CryptoCipher.logChecksumInfo(label: "Bundle checksum", hexChecksum: next.getChecksum())
1639
+ CryptoCipher.logChecksumInfo(label: "Expected checksum", hexChecksum: checksum)
1640
+ if next.getChecksum() != checksum {
1641
+ self.logger.error("Error checksum \(next.getChecksum()) \(checksum)")
1642
+ self.implementation.sendStats(action: "checksum_fail", versionName: next.getVersionName())
1643
+ let id = next.getId()
1644
+ let resDel = self.implementation.delete(id: id)
1645
+ if !resDel {
1646
+ self.logger.error("Delete failed, id \(id) doesn't exist")
1647
+ }
1648
+ throw ObjectSavableError.checksum
1643
1649
  }
1644
- throw ObjectSavableError.checksum
1645
1650
  }
1646
1651
 
1647
1652
  self.logger.info("Good checksum \(next.getChecksum()) \(checksum)")
@@ -1693,8 +1698,10 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
1693
1698
  let id = self.implementation.getCurrentBundleId()
1694
1699
  if BundleInfo.ID_BUILTIN == id {
1695
1700
  return Bundle.main.resourceURL!.appendingPathComponent("public")
1701
+ } else if let bundleDir = try? self.implementation.getBundleDirectory(id: id) {
1702
+ return bundleDir
1696
1703
  } else {
1697
- return self.implementation.getBundleDirectory(id: id)
1704
+ return Bundle.main.resourceURL!.appendingPathComponent("public")
1698
1705
  }
1699
1706
  }
1700
1707
 
@@ -4174,6 +4181,22 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
4174
4181
  self.updateUrl = updateUrl
4175
4182
  }
4176
4183
 
4184
+ func downloadBundleForTesting(
4185
+ urlString: String,
4186
+ version: String,
4187
+ sessionKey: String,
4188
+ checksum: String,
4189
+ manifestEntries: [ManifestEntry]?
4190
+ ) throws -> BundleInfo {
4191
+ try downloadBundle(
4192
+ urlString: urlString,
4193
+ version: version,
4194
+ sessionKey: sessionKey,
4195
+ checksum: checksum,
4196
+ manifestEntries: manifestEntries
4197
+ )
4198
+ }
4199
+
4177
4200
  func setAutoUpdateModeForTesting(_ autoUpdateMode: String) {
4178
4201
  self.autoUpdateMode = Self.normalizedAutoUpdateMode(autoUpdateMode)
4179
4202
  self.autoUpdate = Self.isAutoUpdateModeEnabled(self.autoUpdateMode)
@@ -4509,6 +4532,17 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
4509
4532
  if latestVersionName != "" && current.getVersionName() != latestVersionName {
4510
4533
  do {
4511
4534
  self.logger.info("New bundle: \(latestVersionName) found. Current is: \(current.getVersionName()). \(messageUpdate)")
4535
+ if !self.implementation.publicKey.isEmpty && !CryptoCipher.isValidSessionKey(sessionKey) {
4536
+ self.logger.error("Public key present but no valid session key provided")
4537
+ self.implementation.sendStats(action: "session_key_required", versionName: latestVersionName)
4538
+ self.endBackGroundTaskWithNotif(
4539
+ msg: "Session key required when public key is present",
4540
+ latestVersionName: latestVersionName,
4541
+ current: current,
4542
+ plannedDirectUpdate: plannedDirectUpdate
4543
+ )
4544
+ return
4545
+ }
4512
4546
  var nextImpl = self.implementation.getBundleInfoByVersionName(version: latestVersionName)
4513
4547
  let needsDownload = nextImpl.map(Self.shouldRetryDownloadForExistingBundle) ?? true
4514
4548
  if needsDownload {
@@ -4526,6 +4560,17 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
4526
4560
  if res.manifest != nil {
4527
4561
  nextImpl = try self.implementation.downloadManifest(manifest: res.manifest!, version: latestVersionName, sessionKey: sessionKey, link: res.link, comment: res.comment)
4528
4562
  } else {
4563
+ if res.checksum.isEmpty {
4564
+ self.logger.error("No checksum provided")
4565
+ self.implementation.sendStats(action: "checksum_required", versionName: latestVersionName)
4566
+ self.endBackGroundTaskWithNotif(
4567
+ msg: "Checksum required",
4568
+ latestVersionName: latestVersionName,
4569
+ current: current,
4570
+ plannedDirectUpdate: plannedDirectUpdate
4571
+ )
4572
+ return
4573
+ }
4529
4574
  nextImpl = try self.implementation.download(url: downloadUrl, version: latestVersionName, sessionKey: sessionKey, link: res.link, comment: res.comment)
4530
4575
  }
4531
4576
  }
@@ -4554,24 +4599,26 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
4554
4599
  self.endBackGroundTask()
4555
4600
  return
4556
4601
  }
4557
- res.checksum = try CryptoCipher.decryptChecksum(checksum: res.checksum, publicKey: self.implementation.publicKey)
4558
- CryptoCipher.logChecksumInfo(label: "Bundle checksum", hexChecksum: next.getChecksum())
4559
- CryptoCipher.logChecksumInfo(label: "Expected checksum", hexChecksum: res.checksum)
4560
- if res.checksum != "" && next.getChecksum() != res.checksum && res.manifest == nil {
4561
- self.logger.error("Error checksum \(next.getChecksum()) \(res.checksum)")
4562
- self.implementation.sendStats(action: "checksum_fail", versionName: next.getVersionName())
4563
- let id = next.getId()
4564
- let resDel = self.implementation.delete(id: id)
4565
- if !resDel {
4566
- self.logger.error("Delete failed, id \(id) doesn't exist")
4602
+ if res.manifest == nil {
4603
+ res.checksum = try CryptoCipher.decryptChecksum(checksum: res.checksum, publicKey: self.implementation.publicKey)
4604
+ CryptoCipher.logChecksumInfo(label: "Bundle checksum", hexChecksum: next.getChecksum())
4605
+ CryptoCipher.logChecksumInfo(label: "Expected checksum", hexChecksum: res.checksum)
4606
+ if next.getChecksum() != res.checksum {
4607
+ self.logger.error("Error checksum \(next.getChecksum()) \(res.checksum)")
4608
+ self.implementation.sendStats(action: "checksum_fail", versionName: next.getVersionName())
4609
+ let id = next.getId()
4610
+ let resDel = self.implementation.delete(id: id)
4611
+ if !resDel {
4612
+ self.logger.error("Delete failed, id \(id) doesn't exist")
4613
+ }
4614
+ self.endBackGroundTaskWithNotif(
4615
+ msg: "Error checksum",
4616
+ latestVersionName: latestVersionName,
4617
+ current: current,
4618
+ plannedDirectUpdate: plannedDirectUpdate
4619
+ )
4620
+ return
4567
4621
  }
4568
- self.endBackGroundTaskWithNotif(
4569
- msg: "Error checksum",
4570
- latestVersionName: latestVersionName,
4571
- current: current,
4572
- plannedDirectUpdate: plannedDirectUpdate
4573
- )
4574
- return
4575
4622
  }
4576
4623
  if self.shouldBlockAutoUpdateForPreviewSession() {
4577
4624
  self.clearDownloadInProgressState()