@capgo/capacitor-updater 8.51.22 → 8.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.
- package/README.md +1 -1
- package/android/src/main/java/ee/forgr/capacitor_updater/CapacitorUpdaterPlugin.java +18 -2
- package/android/src/main/java/ee/forgr/capacitor_updater/CapgoUpdater.java +121 -34
- package/android/src/main/java/ee/forgr/capacitor_updater/CryptoCipher.java +9 -1
- package/android/src/main/java/ee/forgr/capacitor_updater/DownloadService.java +28 -4
- package/ios/Sources/CapacitorUpdaterPlugin/CapacitorUpdaterPlugin.swift +89 -42
- package/ios/Sources/CapacitorUpdaterPlugin/CapgoUpdater.swift +80 -12
- package/ios/Sources/CapacitorUpdaterPlugin/CryptoCipher.swift +9 -1
- package/ios/Sources/CapacitorUpdaterPlugin/ShakeMenu.swift +4 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -46,7 +46,7 @@ Perfect for fixing bugs immediately, A/B testing features, and maintaining contr
|
|
|
46
46
|
- 🔄 Auto Update: Automatically download and set the latest bundle for the app.
|
|
47
47
|
- 🛟 Rollback: Reset the app to last working bundle if an incompatible bundle has been set.
|
|
48
48
|
- 🔁 **Delta Updates**: Make instant updates by only downloading changed files.
|
|
49
|
-
- 🔒
|
|
49
|
+
- 🔒 Security: checksum on every update; encryption optional for high-security apps.
|
|
50
50
|
- ⚔️ **Battle-Tested**: Used in more than 3000 projects.
|
|
51
51
|
- 📊 View your deployment statistics
|
|
52
52
|
- 🔋 Supports Android and iOS
|
|
@@ -146,7 +146,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
146
146
|
static final int APPLICATION_EXIT_REASON_USER_REQUESTED = 10;
|
|
147
147
|
static final int APPLICATION_EXIT_REASON_DEPENDENCY_DIED = 12;
|
|
148
148
|
|
|
149
|
-
private final String pluginVersion = "8.51.
|
|
149
|
+
private final String pluginVersion = "8.51.23";
|
|
150
150
|
private static final String DELAY_CONDITION_PREFERENCES = "";
|
|
151
151
|
|
|
152
152
|
private SharedPreferences.Editor editor;
|
|
@@ -2842,6 +2842,16 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
2842
2842
|
final String checksum,
|
|
2843
2843
|
final JSONArray manifest
|
|
2844
2844
|
) throws IOException {
|
|
2845
|
+
if (!this.implementation.publicKey.isEmpty() && !CryptoCipher.isValidSessionKey(sessionKey)) {
|
|
2846
|
+
logger.error("Public key present but no valid session key provided");
|
|
2847
|
+
this.implementation.sendStats("session_key_required");
|
|
2848
|
+
throw new IOException("Session key required when public key is present");
|
|
2849
|
+
}
|
|
2850
|
+
if (manifest == null && (checksum == null || checksum.isEmpty())) {
|
|
2851
|
+
logger.error("No checksum provided");
|
|
2852
|
+
this.implementation.sendStats("checksum_required");
|
|
2853
|
+
throw new IOException("Checksum required");
|
|
2854
|
+
}
|
|
2845
2855
|
// Manual/preview downloads must wait too — launch orphan sweep can delete their temps.
|
|
2846
2856
|
waitForCleanupIfNeeded();
|
|
2847
2857
|
if (manifest != null) {
|
|
@@ -4936,7 +4946,13 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
4936
4946
|
);
|
|
4937
4947
|
return;
|
|
4938
4948
|
}
|
|
4939
|
-
|
|
4949
|
+
final String latestSessionKey = jsRes.has("sessionKey") ? jsRes.getString("sessionKey") : "";
|
|
4950
|
+
if (
|
|
4951
|
+
latest.isDownloaded() &&
|
|
4952
|
+
BundleStatus.DOWNLOADING != latest.getStatus() &&
|
|
4953
|
+
(CapacitorUpdaterPlugin.this.implementation.publicKey.isEmpty() ||
|
|
4954
|
+
CryptoCipher.isValidSessionKey(latestSessionKey))
|
|
4955
|
+
) {
|
|
4940
4956
|
logger.info("Latest bundle already exists and download is NOT required. " + messageUpdate);
|
|
4941
4957
|
final boolean directUpdateAllowedNow = CapacitorUpdaterPlugin.this.isDirectUpdateCurrentlyAllowed(
|
|
4942
4958
|
plannedDirectUpdate
|
|
@@ -285,6 +285,22 @@ public class CapgoUpdater {
|
|
|
285
285
|
this.cachedKeyId = CryptoCipher.calcKeyId(publicKey);
|
|
286
286
|
}
|
|
287
287
|
|
|
288
|
+
private void requireSessionKeyForEncryptedUpdate(final String sessionKey) throws IOException {
|
|
289
|
+
if (!this.publicKey.isEmpty() && !CryptoCipher.isValidSessionKey(sessionKey)) {
|
|
290
|
+
logger.error("Public key present but no valid session key provided");
|
|
291
|
+
this.sendStats("session_key_required");
|
|
292
|
+
throw new IOException("Session key required when public key is present");
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
private void requireBundleChecksum(final String checksum) throws IOException {
|
|
297
|
+
if (checksum == null || checksum.isEmpty()) {
|
|
298
|
+
logger.error("No checksum provided");
|
|
299
|
+
this.sendStats("checksum_required");
|
|
300
|
+
throw new IOException("Checksum required");
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
288
304
|
static boolean containsPathTraversalSegment(final String relativePath) {
|
|
289
305
|
for (final String segment : relativePath.split("/")) {
|
|
290
306
|
if ("..".equals(segment)) {
|
|
@@ -322,6 +338,10 @@ public class CapgoUpdater {
|
|
|
322
338
|
return canonicalTarget;
|
|
323
339
|
}
|
|
324
340
|
|
|
341
|
+
static File resolveBundleDirectory(final File documentsDir, final String bundleId) throws IOException {
|
|
342
|
+
return resolvePathInsideDirectory(new File(documentsDir, bundleDirectory), bundleId);
|
|
343
|
+
}
|
|
344
|
+
|
|
325
345
|
public String getKeyId() {
|
|
326
346
|
return this.cachedKeyId;
|
|
327
347
|
}
|
|
@@ -389,11 +409,11 @@ public class CapgoUpdater {
|
|
|
389
409
|
}
|
|
390
410
|
}
|
|
391
411
|
|
|
392
|
-
private void flattenAssets(final File sourceFile, final
|
|
412
|
+
private void flattenAssets(final File sourceFile, final File destinationFile) throws IOException {
|
|
393
413
|
if (!sourceFile.exists()) {
|
|
394
414
|
throw new FileNotFoundException("Source file not found: " + sourceFile.getPath());
|
|
395
415
|
}
|
|
396
|
-
|
|
416
|
+
assertPathInsideBundleRoot(destinationFile);
|
|
397
417
|
Objects.requireNonNull(destinationFile.getParentFile()).mkdirs();
|
|
398
418
|
final String[] entries = sourceFile.list(this.filter);
|
|
399
419
|
if (entries == null || entries.length == 0) {
|
|
@@ -422,7 +442,13 @@ public class CapgoUpdater {
|
|
|
422
442
|
return;
|
|
423
443
|
}
|
|
424
444
|
|
|
425
|
-
final File bundleDir
|
|
445
|
+
final File bundleDir;
|
|
446
|
+
try {
|
|
447
|
+
bundleDir = this.getBundleDirectory(id);
|
|
448
|
+
} catch (IOException e) {
|
|
449
|
+
logger.debug("Skip delta cache population: invalid bundle id");
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
426
452
|
if (!bundleDir.exists()) {
|
|
427
453
|
logger.debug("Skip delta cache population: bundle dir missing");
|
|
428
454
|
return;
|
|
@@ -510,7 +536,10 @@ public class CapgoUpdater {
|
|
|
510
536
|
if (fileHash.isEmpty()) {
|
|
511
537
|
return "";
|
|
512
538
|
}
|
|
513
|
-
if (this.publicKey != null && !this.publicKey.isEmpty()
|
|
539
|
+
if (this.publicKey != null && !this.publicKey.isEmpty()) {
|
|
540
|
+
if (!CryptoCipher.isValidSessionKey(sessionKey)) {
|
|
541
|
+
return "";
|
|
542
|
+
}
|
|
514
543
|
try {
|
|
515
544
|
fileHash = CryptoCipher.decryptChecksum(fileHash, this.publicKey);
|
|
516
545
|
} catch (Exception e) {
|
|
@@ -846,31 +875,24 @@ public class CapgoUpdater {
|
|
|
846
875
|
String checksum = "";
|
|
847
876
|
|
|
848
877
|
try {
|
|
878
|
+
this.requireSessionKeyForEncryptedUpdate(sessionKey);
|
|
849
879
|
this.notifyDownload(id, 71);
|
|
850
880
|
downloaded = new File(this.documentsDir, dest);
|
|
851
881
|
|
|
852
882
|
if (!isManifest) {
|
|
853
|
-
String
|
|
854
|
-
|
|
855
|
-
// If public key is present but no checksum provided, refuse installation
|
|
856
|
-
if (!this.publicKey.isEmpty() && checksumDecrypted.isEmpty()) {
|
|
857
|
-
logger.error("Public key present but no checksum provided");
|
|
858
|
-
this.sendStats("checksum_required");
|
|
859
|
-
throw new IOException("Checksum required when public key is present: " + id);
|
|
860
|
-
}
|
|
883
|
+
String expectedChecksum = Objects.requireNonNullElse(checksumRes, "");
|
|
884
|
+
this.requireBundleChecksum(expectedChecksum);
|
|
861
885
|
|
|
862
|
-
if (
|
|
886
|
+
if (CryptoCipher.isValidSessionKey(sessionKey)) {
|
|
863
887
|
CryptoCipher.decryptFile(downloaded, publicKey, sessionKey);
|
|
864
|
-
|
|
865
|
-
checksum = CryptoCipher.calcChecksum(downloaded);
|
|
866
|
-
} else {
|
|
867
|
-
checksum = CryptoCipher.calcChecksum(downloaded);
|
|
888
|
+
expectedChecksum = CryptoCipher.decryptChecksum(checksumRes, publicKey);
|
|
868
889
|
}
|
|
890
|
+
checksum = CryptoCipher.calcChecksum(downloaded);
|
|
869
891
|
CryptoCipher.logChecksumInfo("Calculated checksum", checksum);
|
|
870
|
-
CryptoCipher.logChecksumInfo("Expected checksum",
|
|
871
|
-
if (
|
|
892
|
+
CryptoCipher.logChecksumInfo("Expected checksum", expectedChecksum);
|
|
893
|
+
if (!expectedChecksum.equals(checksum)) {
|
|
872
894
|
logger.error("Checksum mismatch");
|
|
873
|
-
logger.debug("Expected: " +
|
|
895
|
+
logger.debug("Expected: " + expectedChecksum + ", Got: " + checksum);
|
|
874
896
|
this.sendStats("checksum_fail");
|
|
875
897
|
throw new IOException("Checksum failed: " + id);
|
|
876
898
|
}
|
|
@@ -898,13 +920,11 @@ public class CapgoUpdater {
|
|
|
898
920
|
if (!isManifest) {
|
|
899
921
|
extractedDir = this.unzip(id, downloaded, TEMP_UNZIP_PREFIX + this.randomString());
|
|
900
922
|
this.notifyDownload(id, 91);
|
|
901
|
-
|
|
902
|
-
this.flattenAssets(extractedDir, idName);
|
|
923
|
+
this.flattenAssets(extractedDir, this.getBundleDirectory(id));
|
|
903
924
|
this.cacheBundleFilesAsync(id);
|
|
904
925
|
} else {
|
|
905
926
|
this.notifyDownload(id, 91);
|
|
906
|
-
|
|
907
|
-
this.flattenAssets(downloaded, idName);
|
|
927
|
+
this.flattenAssets(downloaded, this.getBundleDirectory(id));
|
|
908
928
|
downloaded.delete();
|
|
909
929
|
}
|
|
910
930
|
// Remove old bundle info and set new one
|
|
@@ -1123,18 +1143,44 @@ public class CapgoUpdater {
|
|
|
1123
1143
|
}
|
|
1124
1144
|
}
|
|
1125
1145
|
|
|
1146
|
+
private void assertPathInsideDocumentsDir(final File target) throws IOException {
|
|
1147
|
+
if (this.documentsDir == null) {
|
|
1148
|
+
throw new IOException("Documents directory unavailable");
|
|
1149
|
+
}
|
|
1150
|
+
final File canonicalBase = this.documentsDir.getCanonicalFile();
|
|
1151
|
+
final File canonicalTarget = target.getCanonicalFile();
|
|
1152
|
+
final String basePath = canonicalBase.getPath();
|
|
1153
|
+
final String normalizedBasePath = basePath.endsWith(File.separator) ? basePath : basePath + File.separator;
|
|
1154
|
+
final String targetPath = canonicalTarget.getPath();
|
|
1155
|
+
if (!targetPath.equals(basePath) && !targetPath.startsWith(normalizedBasePath)) {
|
|
1156
|
+
throw new IOException("Path escapes updater storage: " + target.getPath());
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
private void assertPathInsideBundleRoot(final File target) throws IOException {
|
|
1161
|
+
final File bundleRoot = new File(this.documentsDir, bundleDirectory).getCanonicalFile();
|
|
1162
|
+
final File canonicalTarget = target.getCanonicalFile();
|
|
1163
|
+
final String basePath = bundleRoot.getPath();
|
|
1164
|
+
final String normalizedBasePath = basePath.endsWith(File.separator) ? basePath : basePath + File.separator;
|
|
1165
|
+
final String targetPath = canonicalTarget.getPath();
|
|
1166
|
+
if (!targetPath.equals(basePath) && !targetPath.startsWith(normalizedBasePath)) {
|
|
1167
|
+
throw new IOException("Path escapes bundle storage: " + target.getPath());
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1126
1171
|
private void safeDelete(final File target) {
|
|
1127
1172
|
if (target == null || !target.exists()) {
|
|
1128
1173
|
return;
|
|
1129
1174
|
}
|
|
1130
1175
|
try {
|
|
1176
|
+
this.assertPathInsideDocumentsDir(target);
|
|
1131
1177
|
if (target.isDirectory()) {
|
|
1132
1178
|
this.deleteDirectory(target);
|
|
1133
1179
|
} else if (!target.delete()) {
|
|
1134
1180
|
logger.warn("Failed to delete file: " + target.getAbsolutePath());
|
|
1135
1181
|
}
|
|
1136
1182
|
} catch (IOException cleanupError) {
|
|
1137
|
-
logger.warn("
|
|
1183
|
+
logger.warn("Refusing unsafe delete for " + target.getAbsolutePath() + ": " + cleanupError.getMessage());
|
|
1138
1184
|
}
|
|
1139
1185
|
}
|
|
1140
1186
|
|
|
@@ -1408,6 +1454,15 @@ public class CapgoUpdater {
|
|
|
1408
1454
|
final JSONArray manifest,
|
|
1409
1455
|
final boolean setNext
|
|
1410
1456
|
) {
|
|
1457
|
+
try {
|
|
1458
|
+
this.requireSessionKeyForEncryptedUpdate(sessionKey);
|
|
1459
|
+
if (manifest == null) {
|
|
1460
|
+
this.requireBundleChecksum(checksum);
|
|
1461
|
+
}
|
|
1462
|
+
} catch (final IOException e) {
|
|
1463
|
+
logger.error("Download blocked: " + e.getMessage());
|
|
1464
|
+
return;
|
|
1465
|
+
}
|
|
1411
1466
|
if (!this.runDownloadGateQuiet()) {
|
|
1412
1467
|
return;
|
|
1413
1468
|
}
|
|
@@ -1438,6 +1493,8 @@ public class CapgoUpdater {
|
|
|
1438
1493
|
}
|
|
1439
1494
|
|
|
1440
1495
|
public BundleInfo download(final String url, final String version, final String sessionKey, final String checksum) throws IOException {
|
|
1496
|
+
this.requireSessionKeyForEncryptedUpdate(sessionKey);
|
|
1497
|
+
this.requireBundleChecksum(checksum);
|
|
1441
1498
|
this.runDownloadGate();
|
|
1442
1499
|
// Check for existing bundle with same version and clean up if in error state
|
|
1443
1500
|
BundleInfo existingBundle = this.getBundleInfoByName(version);
|
|
@@ -1489,6 +1546,7 @@ public class CapgoUpdater {
|
|
|
1489
1546
|
final String checksum,
|
|
1490
1547
|
final JSONArray manifest
|
|
1491
1548
|
) throws IOException {
|
|
1549
|
+
this.requireSessionKeyForEncryptedUpdate(sessionKey);
|
|
1492
1550
|
this.runDownloadGate();
|
|
1493
1551
|
if (manifest == null) {
|
|
1494
1552
|
return download(url, version, sessionKey, checksum);
|
|
@@ -1570,6 +1628,14 @@ public class CapgoUpdater {
|
|
|
1570
1628
|
|
|
1571
1629
|
public Boolean delete(final String id, final Boolean removeInfo, final boolean cancelActiveDownload) throws IOException {
|
|
1572
1630
|
synchronized (this.deleteLock) {
|
|
1631
|
+
final File bundle;
|
|
1632
|
+
try {
|
|
1633
|
+
bundle = this.getBundleDirectory(id);
|
|
1634
|
+
} catch (IOException e) {
|
|
1635
|
+
logger.error("Cannot delete bundle with invalid id");
|
|
1636
|
+
logger.debug("Bundle ID: " + id + ", Error: " + e.getMessage());
|
|
1637
|
+
return false;
|
|
1638
|
+
}
|
|
1573
1639
|
final BundleInfo deleted = this.getBundleInfo(id);
|
|
1574
1640
|
if (deleted.isBuiltin() || this.getCurrentBundleId().equals(id)) {
|
|
1575
1641
|
logger.error("Cannot delete current or builtin bundle");
|
|
@@ -1595,7 +1661,6 @@ public class CapgoUpdater {
|
|
|
1595
1661
|
return false;
|
|
1596
1662
|
}
|
|
1597
1663
|
|
|
1598
|
-
final File bundle = this.getBundleDirectory(id);
|
|
1599
1664
|
final boolean hadRegistry = this.hasStoredBundleInfo(id);
|
|
1600
1665
|
final boolean hadFolder = bundle.exists();
|
|
1601
1666
|
if (!hadRegistry && !hadFolder) {
|
|
@@ -1743,12 +1808,17 @@ public class CapgoUpdater {
|
|
|
1743
1808
|
this.editor.commit();
|
|
1744
1809
|
}
|
|
1745
1810
|
|
|
1746
|
-
private File getBundleDirectory(final String id) {
|
|
1747
|
-
return
|
|
1811
|
+
private File getBundleDirectory(final String id) throws IOException {
|
|
1812
|
+
return resolveBundleDirectory(this.documentsDir, id);
|
|
1748
1813
|
}
|
|
1749
1814
|
|
|
1750
1815
|
private boolean bundleExists(final String id) {
|
|
1751
|
-
final File bundle
|
|
1816
|
+
final File bundle;
|
|
1817
|
+
try {
|
|
1818
|
+
bundle = this.getBundleDirectory(id);
|
|
1819
|
+
} catch (IOException e) {
|
|
1820
|
+
return false;
|
|
1821
|
+
}
|
|
1752
1822
|
final BundleInfo bundleInfo = this.getBundleInfo(id);
|
|
1753
1823
|
return (
|
|
1754
1824
|
bundle.isDirectory() &&
|
|
@@ -1825,7 +1895,16 @@ public class CapgoUpdater {
|
|
|
1825
1895
|
this.reset();
|
|
1826
1896
|
return true;
|
|
1827
1897
|
}
|
|
1828
|
-
final File bundle
|
|
1898
|
+
final File bundle;
|
|
1899
|
+
try {
|
|
1900
|
+
bundle = this.getBundleDirectory(id);
|
|
1901
|
+
} catch (IOException e) {
|
|
1902
|
+
logger.error("Invalid bundle id");
|
|
1903
|
+
logger.debug("Bundle ID: " + id + ", Error: " + e.getMessage());
|
|
1904
|
+
this.setBundleStatus(id, BundleStatus.ERROR);
|
|
1905
|
+
this.sendStats("set_fail", newBundle.getVersionName());
|
|
1906
|
+
return false;
|
|
1907
|
+
}
|
|
1829
1908
|
logger.info("Setting next active bundle: " + id);
|
|
1830
1909
|
if (this.bundleExists(id)) {
|
|
1831
1910
|
var currentBundleName = this.getCurrentBundle().getVersionName();
|
|
@@ -1843,8 +1922,12 @@ public class CapgoUpdater {
|
|
|
1843
1922
|
if (bundle == null || bundle.isBuiltin() || !this.bundleExists(bundle.getId())) {
|
|
1844
1923
|
return false;
|
|
1845
1924
|
}
|
|
1846
|
-
|
|
1847
|
-
|
|
1925
|
+
try {
|
|
1926
|
+
this.setCurrentBundle(this.getBundleDirectory(bundle.getId()));
|
|
1927
|
+
return true;
|
|
1928
|
+
} catch (IOException e) {
|
|
1929
|
+
return false;
|
|
1930
|
+
}
|
|
1848
1931
|
}
|
|
1849
1932
|
|
|
1850
1933
|
boolean stagePreviewFallbackReload(final BundleInfo bundle) {
|
|
@@ -1858,8 +1941,12 @@ public class CapgoUpdater {
|
|
|
1858
1941
|
if (!this.bundleExists(bundle.getId())) {
|
|
1859
1942
|
return false;
|
|
1860
1943
|
}
|
|
1861
|
-
|
|
1862
|
-
|
|
1944
|
+
try {
|
|
1945
|
+
this.setCurrentBundle(this.getBundleDirectory(bundle.getId()));
|
|
1946
|
+
return true;
|
|
1947
|
+
} catch (IOException e) {
|
|
1948
|
+
return false;
|
|
1949
|
+
}
|
|
1863
1950
|
}
|
|
1864
1951
|
|
|
1865
1952
|
void finalizePendingReload(final BundleInfo bundle, final String previousBundleName) {
|
|
@@ -135,8 +135,16 @@ public class CryptoCipher {
|
|
|
135
135
|
return buf;
|
|
136
136
|
}
|
|
137
137
|
|
|
138
|
+
public static boolean isValidSessionKey(final String sessionKey) {
|
|
139
|
+
if (sessionKey == null || sessionKey.isEmpty()) {
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
String[] sessionKeyParts = sessionKey.split(":", -1);
|
|
143
|
+
return sessionKeyParts.length == 2 && !sessionKeyParts[0].isEmpty() && !sessionKeyParts[1].isEmpty();
|
|
144
|
+
}
|
|
145
|
+
|
|
138
146
|
public static void decryptFile(final File file, final String publicKey, final String ivSessionKey) throws IOException {
|
|
139
|
-
if (publicKey.isEmpty() ||
|
|
147
|
+
if (publicKey.isEmpty() || !isValidSessionKey(ivSessionKey)) {
|
|
140
148
|
if (logger != null) {
|
|
141
149
|
logger.info("Encryption not set, no public key or session, ignored");
|
|
142
150
|
}
|
|
@@ -456,7 +456,7 @@ public class DownloadService extends Worker {
|
|
|
456
456
|
return createFailureResult("Manifest is null");
|
|
457
457
|
}
|
|
458
458
|
} else {
|
|
459
|
-
handleSingleFileDownload(url, id, documentsDir, dest, version, sessionKey, checksum);
|
|
459
|
+
handleSingleFileDownload(url, id, documentsDir, dest, version, sessionKey, checksum, publicKey);
|
|
460
460
|
return createSuccessResult(dest, version, sessionKey, checksum, false);
|
|
461
461
|
}
|
|
462
462
|
} catch (DownloadRetryException e) {
|
|
@@ -547,6 +547,12 @@ public class DownloadService extends Worker {
|
|
|
547
547
|
try {
|
|
548
548
|
logger.debug("handleManifestDownload");
|
|
549
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
|
+
|
|
550
556
|
// Send stats for manifest download start
|
|
551
557
|
sendStatsAsync("download_manifest_start", version);
|
|
552
558
|
|
|
@@ -582,7 +588,13 @@ public class DownloadService extends Worker {
|
|
|
582
588
|
continue;
|
|
583
589
|
}
|
|
584
590
|
|
|
585
|
-
if (publicKey != null && !publicKey.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
|
+
}
|
|
586
598
|
try {
|
|
587
599
|
fileHash = CryptoCipher.decryptChecksum(fileHash, publicKey);
|
|
588
600
|
} catch (Exception e) {
|
|
@@ -727,8 +739,20 @@ public class DownloadService extends Worker {
|
|
|
727
739
|
String dest,
|
|
728
740
|
String version,
|
|
729
741
|
String sessionKey,
|
|
730
|
-
String checksum
|
|
742
|
+
String checksum,
|
|
743
|
+
String publicKey
|
|
731
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
|
+
|
|
732
756
|
// Send stats for zip download start
|
|
733
757
|
sendStatsAsync("download_zip_start", version);
|
|
734
758
|
|
|
@@ -1110,7 +1134,7 @@ public class DownloadService extends Worker {
|
|
|
1110
1134
|
}
|
|
1111
1135
|
}
|
|
1112
1136
|
|
|
1113
|
-
boolean needDecrypt = publicKey != null && !publicKey.isEmpty() &&
|
|
1137
|
+
boolean needDecrypt = publicKey != null && !publicKey.isEmpty() && CryptoCipher.isValidSessionKey(sessionKey);
|
|
1114
1138
|
File source = partial;
|
|
1115
1139
|
if (needDecrypt) {
|
|
1116
1140
|
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 = "8.51.
|
|
99
|
+
private let pluginVersion: String = "8.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 =
|
|
588
|
+
dest = Bundle.main.resourceURL!.appendingPathComponent("public")
|
|
587
589
|
}
|
|
588
590
|
|
|
589
591
|
if !FileManager.default.fileExists(atPath: dest.path) {
|
|
@@ -1609,7 +1611,19 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1609
1611
|
throw makePreviewError("Invalid download URL")
|
|
1610
1612
|
}
|
|
1611
1613
|
|
|
1614
|
+
if !self.implementation.publicKey.isEmpty && !CryptoCipher.isValidSessionKey(sessionKey) {
|
|
1615
|
+
self.logger.error("Public key present but no valid session key provided")
|
|
1616
|
+
self.implementation.sendStats(action: "session_key_required", versionName: version)
|
|
1617
|
+
throw makePreviewError("Session key required when public key is present")
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1612
1620
|
var checksum = rawChecksum
|
|
1621
|
+
if manifestEntries == nil && checksum.isEmpty {
|
|
1622
|
+
self.logger.error("No checksum provided")
|
|
1623
|
+
self.implementation.sendStats(action: "checksum_required", versionName: version)
|
|
1624
|
+
throw makePreviewError("Checksum required")
|
|
1625
|
+
}
|
|
1626
|
+
|
|
1613
1627
|
let next: BundleInfo
|
|
1614
1628
|
if let manifestEntries = manifestEntries {
|
|
1615
1629
|
next = try self.implementation.downloadManifest(manifest: manifestEntries, version: version, sessionKey: sessionKey)
|
|
@@ -1617,29 +1631,20 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1617
1631
|
next = try self.implementation.download(url: url, version: version, sessionKey: sessionKey)
|
|
1618
1632
|
}
|
|
1619
1633
|
|
|
1620
|
-
if
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
self.
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
CryptoCipher.logChecksumInfo(label: "Expected checksum", hexChecksum: checksum)
|
|
1634
|
-
if (checksum != "" || self.implementation.publicKey != "") && next.getChecksum() != checksum {
|
|
1635
|
-
self.logger.error("Error checksum \(next.getChecksum()) \(checksum)")
|
|
1636
|
-
self.implementation.sendStats(action: "checksum_fail", versionName: next.getVersionName())
|
|
1637
|
-
let id = next.getId()
|
|
1638
|
-
let resDel = self.implementation.delete(id: id)
|
|
1639
|
-
if !resDel {
|
|
1640
|
-
self.logger.error("Delete failed, id \(id) doesn't exist")
|
|
1634
|
+
if manifestEntries == nil {
|
|
1635
|
+
checksum = try CryptoCipher.decryptChecksum(checksum: checksum, publicKey: self.implementation.publicKey)
|
|
1636
|
+
CryptoCipher.logChecksumInfo(label: "Bundle checksum", hexChecksum: next.getChecksum())
|
|
1637
|
+
CryptoCipher.logChecksumInfo(label: "Expected checksum", hexChecksum: checksum)
|
|
1638
|
+
if next.getChecksum() != checksum {
|
|
1639
|
+
self.logger.error("Error checksum \(next.getChecksum()) \(checksum)")
|
|
1640
|
+
self.implementation.sendStats(action: "checksum_fail", versionName: next.getVersionName())
|
|
1641
|
+
let id = next.getId()
|
|
1642
|
+
let resDel = self.implementation.delete(id: id)
|
|
1643
|
+
if !resDel {
|
|
1644
|
+
self.logger.error("Delete failed, id \(id) doesn't exist")
|
|
1645
|
+
}
|
|
1646
|
+
throw ObjectSavableError.checksum
|
|
1641
1647
|
}
|
|
1642
|
-
throw ObjectSavableError.checksum
|
|
1643
1648
|
}
|
|
1644
1649
|
|
|
1645
1650
|
self.logger.info("Good checksum \(next.getChecksum()) \(checksum)")
|
|
@@ -1691,8 +1696,10 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1691
1696
|
let id = self.implementation.getCurrentBundleId()
|
|
1692
1697
|
if BundleInfo.ID_BUILTIN == id {
|
|
1693
1698
|
return Bundle.main.resourceURL!.appendingPathComponent("public")
|
|
1699
|
+
} else if let bundleDir = try? self.implementation.getBundleDirectory(id: id) {
|
|
1700
|
+
return bundleDir
|
|
1694
1701
|
} else {
|
|
1695
|
-
return
|
|
1702
|
+
return Bundle.main.resourceURL!.appendingPathComponent("public")
|
|
1696
1703
|
}
|
|
1697
1704
|
}
|
|
1698
1705
|
|
|
@@ -4172,6 +4179,22 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
4172
4179
|
self.updateUrl = updateUrl
|
|
4173
4180
|
}
|
|
4174
4181
|
|
|
4182
|
+
func downloadBundleForTesting(
|
|
4183
|
+
urlString: String,
|
|
4184
|
+
version: String,
|
|
4185
|
+
sessionKey: String,
|
|
4186
|
+
checksum: String,
|
|
4187
|
+
manifestEntries: [ManifestEntry]?
|
|
4188
|
+
) throws -> BundleInfo {
|
|
4189
|
+
try downloadBundle(
|
|
4190
|
+
urlString: urlString,
|
|
4191
|
+
version: version,
|
|
4192
|
+
sessionKey: sessionKey,
|
|
4193
|
+
checksum: checksum,
|
|
4194
|
+
manifestEntries: manifestEntries
|
|
4195
|
+
)
|
|
4196
|
+
}
|
|
4197
|
+
|
|
4175
4198
|
func setAutoUpdateModeForTesting(_ autoUpdateMode: String) {
|
|
4176
4199
|
self.autoUpdateMode = Self.normalizedAutoUpdateMode(autoUpdateMode)
|
|
4177
4200
|
self.autoUpdate = Self.isAutoUpdateModeEnabled(self.autoUpdateMode)
|
|
@@ -4507,6 +4530,17 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
4507
4530
|
if latestVersionName != "" && current.getVersionName() != latestVersionName {
|
|
4508
4531
|
do {
|
|
4509
4532
|
self.logger.info("New bundle: \(latestVersionName) found. Current is: \(current.getVersionName()). \(messageUpdate)")
|
|
4533
|
+
if !self.implementation.publicKey.isEmpty && !CryptoCipher.isValidSessionKey(sessionKey) {
|
|
4534
|
+
self.logger.error("Public key present but no valid session key provided")
|
|
4535
|
+
self.implementation.sendStats(action: "session_key_required", versionName: latestVersionName)
|
|
4536
|
+
self.endBackGroundTaskWithNotif(
|
|
4537
|
+
msg: "Session key required when public key is present",
|
|
4538
|
+
latestVersionName: latestVersionName,
|
|
4539
|
+
current: current,
|
|
4540
|
+
plannedDirectUpdate: plannedDirectUpdate
|
|
4541
|
+
)
|
|
4542
|
+
return
|
|
4543
|
+
}
|
|
4510
4544
|
var nextImpl = self.implementation.getBundleInfoByVersionName(version: latestVersionName)
|
|
4511
4545
|
let needsDownload = nextImpl.map(Self.shouldRetryDownloadForExistingBundle) ?? true
|
|
4512
4546
|
if needsDownload {
|
|
@@ -4524,6 +4558,17 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
4524
4558
|
if res.manifest != nil {
|
|
4525
4559
|
nextImpl = try self.implementation.downloadManifest(manifest: res.manifest!, version: latestVersionName, sessionKey: sessionKey, link: res.link, comment: res.comment)
|
|
4526
4560
|
} else {
|
|
4561
|
+
if res.checksum.isEmpty {
|
|
4562
|
+
self.logger.error("No checksum provided")
|
|
4563
|
+
self.implementation.sendStats(action: "checksum_required", versionName: latestVersionName)
|
|
4564
|
+
self.endBackGroundTaskWithNotif(
|
|
4565
|
+
msg: "Checksum required",
|
|
4566
|
+
latestVersionName: latestVersionName,
|
|
4567
|
+
current: current,
|
|
4568
|
+
plannedDirectUpdate: plannedDirectUpdate
|
|
4569
|
+
)
|
|
4570
|
+
return
|
|
4571
|
+
}
|
|
4527
4572
|
nextImpl = try self.implementation.download(url: downloadUrl, version: latestVersionName, sessionKey: sessionKey, link: res.link, comment: res.comment)
|
|
4528
4573
|
}
|
|
4529
4574
|
}
|
|
@@ -4552,24 +4597,26 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
4552
4597
|
self.endBackGroundTask()
|
|
4553
4598
|
return
|
|
4554
4599
|
}
|
|
4555
|
-
res.
|
|
4556
|
-
|
|
4557
|
-
|
|
4558
|
-
|
|
4559
|
-
|
|
4560
|
-
|
|
4561
|
-
|
|
4562
|
-
|
|
4563
|
-
|
|
4564
|
-
|
|
4600
|
+
if res.manifest == nil {
|
|
4601
|
+
res.checksum = try CryptoCipher.decryptChecksum(checksum: res.checksum, publicKey: self.implementation.publicKey)
|
|
4602
|
+
CryptoCipher.logChecksumInfo(label: "Bundle checksum", hexChecksum: next.getChecksum())
|
|
4603
|
+
CryptoCipher.logChecksumInfo(label: "Expected checksum", hexChecksum: res.checksum)
|
|
4604
|
+
if next.getChecksum() != res.checksum {
|
|
4605
|
+
self.logger.error("Error checksum \(next.getChecksum()) \(res.checksum)")
|
|
4606
|
+
self.implementation.sendStats(action: "checksum_fail", versionName: next.getVersionName())
|
|
4607
|
+
let id = next.getId()
|
|
4608
|
+
let resDel = self.implementation.delete(id: id)
|
|
4609
|
+
if !resDel {
|
|
4610
|
+
self.logger.error("Delete failed, id \(id) doesn't exist")
|
|
4611
|
+
}
|
|
4612
|
+
self.endBackGroundTaskWithNotif(
|
|
4613
|
+
msg: "Error checksum",
|
|
4614
|
+
latestVersionName: latestVersionName,
|
|
4615
|
+
current: current,
|
|
4616
|
+
plannedDirectUpdate: plannedDirectUpdate
|
|
4617
|
+
)
|
|
4618
|
+
return
|
|
4565
4619
|
}
|
|
4566
|
-
self.endBackGroundTaskWithNotif(
|
|
4567
|
-
msg: "Error checksum",
|
|
4568
|
-
latestVersionName: latestVersionName,
|
|
4569
|
-
current: current,
|
|
4570
|
-
plannedDirectUpdate: plannedDirectUpdate
|
|
4571
|
-
)
|
|
4572
|
-
return
|
|
4573
4620
|
}
|
|
4574
4621
|
if self.shouldBlockAutoUpdateForPreviewSession() {
|
|
4575
4622
|
self.clearDownloadInProgressState()
|
|
@@ -166,6 +166,11 @@ import UIKit
|
|
|
166
166
|
return canonicalTarget
|
|
167
167
|
}
|
|
168
168
|
|
|
169
|
+
static func resolveBundleDirectory(libraryDir: URL, bundleId: String) throws -> URL {
|
|
170
|
+
let bundleRoot = libraryDir.appendingPathComponent("NoCloud/ionic_built_snapshots")
|
|
171
|
+
return try resolvePathInsideDirectory(baseDirectory: bundleRoot, relativePath: bundleId)
|
|
172
|
+
}
|
|
173
|
+
|
|
169
174
|
static func resolveManifestTargetPath(baseDirectory: URL, fileName: String) throws -> URL {
|
|
170
175
|
let isBrotli = fileName.hasSuffix(".br")
|
|
171
176
|
let targetFileName = isBrotli ? String(fileName.dropLast(3)) : fileName
|
|
@@ -456,6 +461,18 @@ import UIKit
|
|
|
456
461
|
return String((0..<length).map { _ in letters.randomElement()! })
|
|
457
462
|
}
|
|
458
463
|
|
|
464
|
+
private func requireSessionKeyForEncryptedUpdate(sessionKey: String, versionName: String? = nil) throws {
|
|
465
|
+
if !self.publicKey.isEmpty && !CryptoCipher.isValidSessionKey(sessionKey) {
|
|
466
|
+
logger.error("Public key present but no valid session key provided")
|
|
467
|
+
self.sendStats(action: "session_key_required", versionName: versionName)
|
|
468
|
+
throw NSError(
|
|
469
|
+
domain: "CapgoUpdater",
|
|
470
|
+
code: 1,
|
|
471
|
+
userInfo: [NSLocalizedDescriptionKey: "Session key required when public key is present"]
|
|
472
|
+
)
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
459
476
|
public func setPublicKey(_ publicKey: String) {
|
|
460
477
|
// Empty string means no encryption - proceed normally
|
|
461
478
|
if publicKey.isEmpty {
|
|
@@ -987,7 +1004,13 @@ import UIKit
|
|
|
987
1004
|
/// `manifest` must only contain entries the caller already checksum-verified
|
|
988
1005
|
/// (as `downloadManifest` does) — the hashes are trusted as-is, not re-checked.
|
|
989
1006
|
func populateDeltaCache(for id: String, manifest: [ManifestEntry]? = nil, sessionKey: String = "") {
|
|
990
|
-
let bundleDir
|
|
1007
|
+
let bundleDir: URL
|
|
1008
|
+
do {
|
|
1009
|
+
bundleDir = try self.getBundleDirectory(id: id)
|
|
1010
|
+
} catch {
|
|
1011
|
+
logger.debug("Skip delta cache population: invalid bundle id")
|
|
1012
|
+
return
|
|
1013
|
+
}
|
|
991
1014
|
let fileManager = FileManager.default
|
|
992
1015
|
|
|
993
1016
|
guard fileManager.fileExists(atPath: bundleDir.path) else {
|
|
@@ -1247,7 +1270,10 @@ import UIKit
|
|
|
1247
1270
|
guard var fileHash = entry.file_hash, !fileHash.isEmpty else {
|
|
1248
1271
|
return nil
|
|
1249
1272
|
}
|
|
1250
|
-
if !self.publicKey.isEmpty
|
|
1273
|
+
if !self.publicKey.isEmpty {
|
|
1274
|
+
if !CryptoCipher.isValidSessionKey(sessionKey) {
|
|
1275
|
+
return nil
|
|
1276
|
+
}
|
|
1251
1277
|
do {
|
|
1252
1278
|
fileHash = try CryptoCipher.decryptChecksum(checksum: fileHash, publicKey: self.publicKey)
|
|
1253
1279
|
} catch {
|
|
@@ -1408,10 +1434,11 @@ import UIKit
|
|
|
1408
1434
|
}
|
|
1409
1435
|
|
|
1410
1436
|
public func downloadManifest(manifest: [ManifestEntry], version: String, sessionKey: String, link: String? = nil, comment: String? = nil) throws -> BundleInfo {
|
|
1437
|
+
try self.requireSessionKeyForEncryptedUpdate(sessionKey: sessionKey, versionName: version)
|
|
1411
1438
|
try self.runBeforeDownload()
|
|
1412
1439
|
let id = self.randomString(length: 10)
|
|
1413
1440
|
logger.info("downloadManifest start \(id)")
|
|
1414
|
-
let destFolder = self.getBundleDirectory(id: id)
|
|
1441
|
+
let destFolder = try self.getBundleDirectory(id: id)
|
|
1415
1442
|
let builtinFolder = self.builtinFolderURL()
|
|
1416
1443
|
|
|
1417
1444
|
// Check disk space before starting manifest download (estimate 100KB per file, minimum 50MB)
|
|
@@ -1482,7 +1509,23 @@ import UIKit
|
|
|
1482
1509
|
var fileHash = entryFileHash
|
|
1483
1510
|
|
|
1484
1511
|
// Decrypt checksum if needed (done before creating operation)
|
|
1485
|
-
if !self.publicKey.isEmpty
|
|
1512
|
+
if !self.publicKey.isEmpty {
|
|
1513
|
+
if !CryptoCipher.isValidSessionKey(sessionKey) {
|
|
1514
|
+
let error = NSError(
|
|
1515
|
+
domain: "CapgoUpdater",
|
|
1516
|
+
code: 1,
|
|
1517
|
+
userInfo: [NSLocalizedDescriptionKey: "Session key required when public key is present"]
|
|
1518
|
+
)
|
|
1519
|
+
self.sendStats(action: "session_key_required", versionName: version)
|
|
1520
|
+
errorLock.lock()
|
|
1521
|
+
if downloadError == nil {
|
|
1522
|
+
downloadError = error
|
|
1523
|
+
}
|
|
1524
|
+
errorLock.unlock()
|
|
1525
|
+
hasError.value = true
|
|
1526
|
+
logger.error("Public key present but no valid session key provided")
|
|
1527
|
+
continue
|
|
1528
|
+
}
|
|
1486
1529
|
do {
|
|
1487
1530
|
fileHash = try CryptoCipher.decryptChecksum(checksum: fileHash, publicKey: self.publicKey)
|
|
1488
1531
|
} catch {
|
|
@@ -1762,7 +1805,7 @@ import UIKit
|
|
|
1762
1805
|
|
|
1763
1806
|
do {
|
|
1764
1807
|
var source = partialURL
|
|
1765
|
-
if !self.publicKey.isEmpty &&
|
|
1808
|
+
if !self.publicKey.isEmpty && CryptoCipher.isValidSessionKey(sessionKey) {
|
|
1766
1809
|
let work = cacheFolder.appendingPathComponent("work_\(UUID().uuidString)_\((fileName as NSString).lastPathComponent)")
|
|
1767
1810
|
try FileManager.default.copyItem(at: partialURL, to: work)
|
|
1768
1811
|
workURL = work
|
|
@@ -2064,6 +2107,7 @@ import UIKit
|
|
|
2064
2107
|
}
|
|
2065
2108
|
|
|
2066
2109
|
public func download(url: URL, version: String, sessionKey: String, link: String? = nil, comment: String? = nil) throws -> BundleInfo {
|
|
2110
|
+
try self.requireSessionKeyForEncryptedUpdate(sessionKey: sessionKey, versionName: version)
|
|
2067
2111
|
try self.runBeforeDownload()
|
|
2068
2112
|
let id: String = self.randomString(length: 10)
|
|
2069
2113
|
// Each download uses its own temp files keyed by bundle ID to prevent collisions
|
|
@@ -2344,6 +2388,15 @@ import UIKit
|
|
|
2344
2388
|
self.deleteLock.lock()
|
|
2345
2389
|
defer { self.deleteLock.unlock() }
|
|
2346
2390
|
|
|
2391
|
+
let destPersist: URL
|
|
2392
|
+
do {
|
|
2393
|
+
destPersist = try self.getBundleDirectory(id: id)
|
|
2394
|
+
} catch {
|
|
2395
|
+
logger.error("Cannot delete bundle with invalid id")
|
|
2396
|
+
logger.debug("Bundle ID: \(id), Error: \(error.localizedDescription)")
|
|
2397
|
+
return false
|
|
2398
|
+
}
|
|
2399
|
+
|
|
2347
2400
|
let deleted: BundleInfo = self.getBundleInfo(id: id)
|
|
2348
2401
|
if deleted.isBuiltin() || self.getCurrentBundleId() == id {
|
|
2349
2402
|
logger.info("Cannot delete current or builtin bundle")
|
|
@@ -2372,7 +2425,6 @@ import UIKit
|
|
|
2372
2425
|
return false
|
|
2373
2426
|
}
|
|
2374
2427
|
|
|
2375
|
-
let destPersist: URL = libraryDir.appendingPathComponent(bundleDirectory).appendingPathComponent(id)
|
|
2376
2428
|
let hadRegistry = self.hasStoredBundleInfo(id: id)
|
|
2377
2429
|
let hadFolder = FileManager.default.fileExists(atPath: destPersist.path)
|
|
2378
2430
|
if !hadRegistry && !hadFolder {
|
|
@@ -2676,8 +2728,8 @@ import UIKit
|
|
|
2676
2728
|
}
|
|
2677
2729
|
}
|
|
2678
2730
|
|
|
2679
|
-
public func getBundleDirectory(id: String) -> URL {
|
|
2680
|
-
return
|
|
2731
|
+
public func getBundleDirectory(id: String) throws -> URL {
|
|
2732
|
+
return try Self.resolveBundleDirectory(libraryDir: libraryDir, bundleId: id)
|
|
2681
2733
|
}
|
|
2682
2734
|
|
|
2683
2735
|
struct ResetState {
|
|
@@ -2731,7 +2783,12 @@ import UIKit
|
|
|
2731
2783
|
}
|
|
2732
2784
|
|
|
2733
2785
|
private func bundleExists(id: String) -> Bool {
|
|
2734
|
-
let destPersist: URL
|
|
2786
|
+
let destPersist: URL
|
|
2787
|
+
do {
|
|
2788
|
+
destPersist = try self.getBundleDirectory(id: id)
|
|
2789
|
+
} catch {
|
|
2790
|
+
return false
|
|
2791
|
+
}
|
|
2735
2792
|
let indexPersist: URL = destPersist.appendingPathComponent("index.html")
|
|
2736
2793
|
let bundleIndo: BundleInfo = self.getBundleInfo(id: id)
|
|
2737
2794
|
if
|
|
@@ -2754,7 +2811,12 @@ import UIKit
|
|
|
2754
2811
|
}
|
|
2755
2812
|
if bundleExists(id: id) {
|
|
2756
2813
|
let currentBundleName = self.getCurrentBundle().getVersionName()
|
|
2757
|
-
|
|
2814
|
+
guard let bundleDir = try? self.getBundleDirectory(id: id) else {
|
|
2815
|
+
self.setBundleStatus(id: id, status: BundleStatus.ERROR)
|
|
2816
|
+
self.sendStats(action: "set_fail", versionName: newBundle.getVersionName())
|
|
2817
|
+
return false
|
|
2818
|
+
}
|
|
2819
|
+
self.setCurrentBundle(bundle: bundleDir.path)
|
|
2758
2820
|
self.setBundleStatus(id: id, status: BundleStatus.PENDING)
|
|
2759
2821
|
self.sendStats(action: "set", versionName: newBundle.getVersionName(), oldVersionName: currentBundleName)
|
|
2760
2822
|
return true
|
|
@@ -2768,7 +2830,10 @@ import UIKit
|
|
|
2768
2830
|
guard !bundle.isBuiltin(), bundleExists(id: bundle.getId()) else {
|
|
2769
2831
|
return false
|
|
2770
2832
|
}
|
|
2771
|
-
|
|
2833
|
+
guard let bundleDir = try? self.getBundleDirectory(id: bundle.getId()) else {
|
|
2834
|
+
return false
|
|
2835
|
+
}
|
|
2836
|
+
self.setCurrentBundle(bundle: bundleDir.path)
|
|
2772
2837
|
return true
|
|
2773
2838
|
}
|
|
2774
2839
|
|
|
@@ -2783,7 +2848,10 @@ import UIKit
|
|
|
2783
2848
|
guard bundleExists(id: bundle.getId()) else {
|
|
2784
2849
|
return false
|
|
2785
2850
|
}
|
|
2786
|
-
|
|
2851
|
+
guard let bundleDir = try? self.getBundleDirectory(id: bundle.getId()) else {
|
|
2852
|
+
return false
|
|
2853
|
+
}
|
|
2854
|
+
self.setCurrentBundle(bundle: bundleDir.path)
|
|
2787
2855
|
return true
|
|
2788
2856
|
}
|
|
2789
2857
|
|
|
@@ -225,8 +225,16 @@ public struct CryptoCipher {
|
|
|
225
225
|
return String(hexString(from: sha256).prefix(16))
|
|
226
226
|
}
|
|
227
227
|
|
|
228
|
+
public static func isValidSessionKey(_ sessionKey: String) -> Bool {
|
|
229
|
+
if sessionKey.isEmpty {
|
|
230
|
+
return false
|
|
231
|
+
}
|
|
232
|
+
let sessionKeyParts = sessionKey.components(separatedBy: ":")
|
|
233
|
+
return sessionKeyParts.count == 2 && !sessionKeyParts[0].isEmpty && !sessionKeyParts[1].isEmpty
|
|
234
|
+
}
|
|
235
|
+
|
|
228
236
|
public static func decryptFile(filePath: URL, publicKey: String, sessionKey: String, version: String) throws {
|
|
229
|
-
if publicKey.isEmpty || sessionKey
|
|
237
|
+
if publicKey.isEmpty || !isValidSessionKey(sessionKey) {
|
|
230
238
|
logger.info("Encryption not set, no public key or session, ignored")
|
|
231
239
|
return
|
|
232
240
|
}
|
|
@@ -359,7 +359,10 @@ extension UIWindow {
|
|
|
359
359
|
if !next.isBuiltin() {
|
|
360
360
|
plugin.logger.info("Resetting to: \(next.toString())")
|
|
361
361
|
_ = updater.set(bundle: next)
|
|
362
|
-
let destHot = updater.getBundleDirectory(id: next.getId())
|
|
362
|
+
guard let destHot = try? updater.getBundleDirectory(id: next.getId()) else {
|
|
363
|
+
resetBuiltin()
|
|
364
|
+
return
|
|
365
|
+
}
|
|
363
366
|
plugin.logger.info("Reloading \(next.toString())")
|
|
364
367
|
bridge.setServerBasePath(destHot.path)
|
|
365
368
|
} else {
|