@capgo/capacitor-updater 8.51.21 → 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 +135 -35
- package/android/src/main/java/ee/forgr/capacitor_updater/CryptoCipher.java +51 -18
- package/android/src/main/java/ee/forgr/capacitor_updater/DownloadService.java +103 -28
- package/ios/Sources/CapacitorUpdaterPlugin/CapacitorUpdaterPlugin.swift +218 -94
- package/ios/Sources/CapacitorUpdaterPlugin/CapgoUpdater.swift +177 -32
- 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,31 @@ 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
|
+
|
|
304
|
+
static boolean containsPathTraversalSegment(final String relativePath) {
|
|
305
|
+
for (final String segment : relativePath.split("/")) {
|
|
306
|
+
if ("..".equals(segment)) {
|
|
307
|
+
return true;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
return false;
|
|
311
|
+
}
|
|
312
|
+
|
|
288
313
|
static File resolvePathInsideDirectory(final File baseDirectory, final String relativePath) throws IOException {
|
|
289
314
|
if (relativePath == null || relativePath.isEmpty()) {
|
|
290
315
|
throw new IOException("Invalid empty path");
|
|
@@ -292,6 +317,9 @@ public class CapgoUpdater {
|
|
|
292
317
|
if (relativePath.contains("\\") || relativePath.indexOf('\0') >= 0) {
|
|
293
318
|
throw new IOException("Invalid path separator");
|
|
294
319
|
}
|
|
320
|
+
if (containsPathTraversalSegment(relativePath)) {
|
|
321
|
+
throw new IOException("Path traversal segments are not allowed");
|
|
322
|
+
}
|
|
295
323
|
if (new File(relativePath).isAbsolute()) {
|
|
296
324
|
throw new IOException("Absolute paths are not allowed");
|
|
297
325
|
}
|
|
@@ -302,13 +330,18 @@ public class CapgoUpdater {
|
|
|
302
330
|
final String targetPath = canonicalTarget.getPath();
|
|
303
331
|
final String normalizedBasePath = basePath.endsWith(File.separator) ? basePath : basePath + File.separator;
|
|
304
332
|
|
|
305
|
-
|
|
333
|
+
// Require a strict child of the base. Equality would accept "." and wipe/write the root.
|
|
334
|
+
if (!targetPath.startsWith(normalizedBasePath)) {
|
|
306
335
|
throw new IOException("Path escapes base directory: " + relativePath);
|
|
307
336
|
}
|
|
308
337
|
|
|
309
338
|
return canonicalTarget;
|
|
310
339
|
}
|
|
311
340
|
|
|
341
|
+
static File resolveBundleDirectory(final File documentsDir, final String bundleId) throws IOException {
|
|
342
|
+
return resolvePathInsideDirectory(new File(documentsDir, bundleDirectory), bundleId);
|
|
343
|
+
}
|
|
344
|
+
|
|
312
345
|
public String getKeyId() {
|
|
313
346
|
return this.cachedKeyId;
|
|
314
347
|
}
|
|
@@ -376,11 +409,11 @@ public class CapgoUpdater {
|
|
|
376
409
|
}
|
|
377
410
|
}
|
|
378
411
|
|
|
379
|
-
private void flattenAssets(final File sourceFile, final
|
|
412
|
+
private void flattenAssets(final File sourceFile, final File destinationFile) throws IOException {
|
|
380
413
|
if (!sourceFile.exists()) {
|
|
381
414
|
throw new FileNotFoundException("Source file not found: " + sourceFile.getPath());
|
|
382
415
|
}
|
|
383
|
-
|
|
416
|
+
assertPathInsideBundleRoot(destinationFile);
|
|
384
417
|
Objects.requireNonNull(destinationFile.getParentFile()).mkdirs();
|
|
385
418
|
final String[] entries = sourceFile.list(this.filter);
|
|
386
419
|
if (entries == null || entries.length == 0) {
|
|
@@ -409,7 +442,13 @@ public class CapgoUpdater {
|
|
|
409
442
|
return;
|
|
410
443
|
}
|
|
411
444
|
|
|
412
|
-
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
|
+
}
|
|
413
452
|
if (!bundleDir.exists()) {
|
|
414
453
|
logger.debug("Skip delta cache population: bundle dir missing");
|
|
415
454
|
return;
|
|
@@ -497,7 +536,10 @@ public class CapgoUpdater {
|
|
|
497
536
|
if (fileHash.isEmpty()) {
|
|
498
537
|
return "";
|
|
499
538
|
}
|
|
500
|
-
if (this.publicKey != null && !this.publicKey.isEmpty()
|
|
539
|
+
if (this.publicKey != null && !this.publicKey.isEmpty()) {
|
|
540
|
+
if (!CryptoCipher.isValidSessionKey(sessionKey)) {
|
|
541
|
+
return "";
|
|
542
|
+
}
|
|
501
543
|
try {
|
|
502
544
|
fileHash = CryptoCipher.decryptChecksum(fileHash, this.publicKey);
|
|
503
545
|
} catch (Exception e) {
|
|
@@ -833,31 +875,24 @@ public class CapgoUpdater {
|
|
|
833
875
|
String checksum = "";
|
|
834
876
|
|
|
835
877
|
try {
|
|
878
|
+
this.requireSessionKeyForEncryptedUpdate(sessionKey);
|
|
836
879
|
this.notifyDownload(id, 71);
|
|
837
880
|
downloaded = new File(this.documentsDir, dest);
|
|
838
881
|
|
|
839
882
|
if (!isManifest) {
|
|
840
|
-
String
|
|
883
|
+
String expectedChecksum = Objects.requireNonNullElse(checksumRes, "");
|
|
884
|
+
this.requireBundleChecksum(expectedChecksum);
|
|
841
885
|
|
|
842
|
-
|
|
843
|
-
if (!this.publicKey.isEmpty() && checksumDecrypted.isEmpty()) {
|
|
844
|
-
logger.error("Public key present but no checksum provided");
|
|
845
|
-
this.sendStats("checksum_required");
|
|
846
|
-
throw new IOException("Checksum required when public key is present: " + id);
|
|
847
|
-
}
|
|
848
|
-
|
|
849
|
-
if (!sessionKey.isEmpty()) {
|
|
886
|
+
if (CryptoCipher.isValidSessionKey(sessionKey)) {
|
|
850
887
|
CryptoCipher.decryptFile(downloaded, publicKey, sessionKey);
|
|
851
|
-
|
|
852
|
-
checksum = CryptoCipher.calcChecksum(downloaded);
|
|
853
|
-
} else {
|
|
854
|
-
checksum = CryptoCipher.calcChecksum(downloaded);
|
|
888
|
+
expectedChecksum = CryptoCipher.decryptChecksum(checksumRes, publicKey);
|
|
855
889
|
}
|
|
890
|
+
checksum = CryptoCipher.calcChecksum(downloaded);
|
|
856
891
|
CryptoCipher.logChecksumInfo("Calculated checksum", checksum);
|
|
857
|
-
CryptoCipher.logChecksumInfo("Expected checksum",
|
|
858
|
-
if (
|
|
892
|
+
CryptoCipher.logChecksumInfo("Expected checksum", expectedChecksum);
|
|
893
|
+
if (!expectedChecksum.equals(checksum)) {
|
|
859
894
|
logger.error("Checksum mismatch");
|
|
860
|
-
logger.debug("Expected: " +
|
|
895
|
+
logger.debug("Expected: " + expectedChecksum + ", Got: " + checksum);
|
|
861
896
|
this.sendStats("checksum_fail");
|
|
862
897
|
throw new IOException("Checksum failed: " + id);
|
|
863
898
|
}
|
|
@@ -885,13 +920,11 @@ public class CapgoUpdater {
|
|
|
885
920
|
if (!isManifest) {
|
|
886
921
|
extractedDir = this.unzip(id, downloaded, TEMP_UNZIP_PREFIX + this.randomString());
|
|
887
922
|
this.notifyDownload(id, 91);
|
|
888
|
-
|
|
889
|
-
this.flattenAssets(extractedDir, idName);
|
|
923
|
+
this.flattenAssets(extractedDir, this.getBundleDirectory(id));
|
|
890
924
|
this.cacheBundleFilesAsync(id);
|
|
891
925
|
} else {
|
|
892
926
|
this.notifyDownload(id, 91);
|
|
893
|
-
|
|
894
|
-
this.flattenAssets(downloaded, idName);
|
|
927
|
+
this.flattenAssets(downloaded, this.getBundleDirectory(id));
|
|
895
928
|
downloaded.delete();
|
|
896
929
|
}
|
|
897
930
|
// Remove old bundle info and set new one
|
|
@@ -1110,18 +1143,44 @@ public class CapgoUpdater {
|
|
|
1110
1143
|
}
|
|
1111
1144
|
}
|
|
1112
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
|
+
|
|
1113
1171
|
private void safeDelete(final File target) {
|
|
1114
1172
|
if (target == null || !target.exists()) {
|
|
1115
1173
|
return;
|
|
1116
1174
|
}
|
|
1117
1175
|
try {
|
|
1176
|
+
this.assertPathInsideDocumentsDir(target);
|
|
1118
1177
|
if (target.isDirectory()) {
|
|
1119
1178
|
this.deleteDirectory(target);
|
|
1120
1179
|
} else if (!target.delete()) {
|
|
1121
1180
|
logger.warn("Failed to delete file: " + target.getAbsolutePath());
|
|
1122
1181
|
}
|
|
1123
1182
|
} catch (IOException cleanupError) {
|
|
1124
|
-
logger.warn("
|
|
1183
|
+
logger.warn("Refusing unsafe delete for " + target.getAbsolutePath() + ": " + cleanupError.getMessage());
|
|
1125
1184
|
}
|
|
1126
1185
|
}
|
|
1127
1186
|
|
|
@@ -1395,6 +1454,15 @@ public class CapgoUpdater {
|
|
|
1395
1454
|
final JSONArray manifest,
|
|
1396
1455
|
final boolean setNext
|
|
1397
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
|
+
}
|
|
1398
1466
|
if (!this.runDownloadGateQuiet()) {
|
|
1399
1467
|
return;
|
|
1400
1468
|
}
|
|
@@ -1425,6 +1493,8 @@ public class CapgoUpdater {
|
|
|
1425
1493
|
}
|
|
1426
1494
|
|
|
1427
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);
|
|
1428
1498
|
this.runDownloadGate();
|
|
1429
1499
|
// Check for existing bundle with same version and clean up if in error state
|
|
1430
1500
|
BundleInfo existingBundle = this.getBundleInfoByName(version);
|
|
@@ -1476,6 +1546,7 @@ public class CapgoUpdater {
|
|
|
1476
1546
|
final String checksum,
|
|
1477
1547
|
final JSONArray manifest
|
|
1478
1548
|
) throws IOException {
|
|
1549
|
+
this.requireSessionKeyForEncryptedUpdate(sessionKey);
|
|
1479
1550
|
this.runDownloadGate();
|
|
1480
1551
|
if (manifest == null) {
|
|
1481
1552
|
return download(url, version, sessionKey, checksum);
|
|
@@ -1557,6 +1628,14 @@ public class CapgoUpdater {
|
|
|
1557
1628
|
|
|
1558
1629
|
public Boolean delete(final String id, final Boolean removeInfo, final boolean cancelActiveDownload) throws IOException {
|
|
1559
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
|
+
}
|
|
1560
1639
|
final BundleInfo deleted = this.getBundleInfo(id);
|
|
1561
1640
|
if (deleted.isBuiltin() || this.getCurrentBundleId().equals(id)) {
|
|
1562
1641
|
logger.error("Cannot delete current or builtin bundle");
|
|
@@ -1582,7 +1661,6 @@ public class CapgoUpdater {
|
|
|
1582
1661
|
return false;
|
|
1583
1662
|
}
|
|
1584
1663
|
|
|
1585
|
-
final File bundle = this.getBundleDirectory(id);
|
|
1586
1664
|
final boolean hadRegistry = this.hasStoredBundleInfo(id);
|
|
1587
1665
|
final boolean hadFolder = bundle.exists();
|
|
1588
1666
|
if (!hadRegistry && !hadFolder) {
|
|
@@ -1730,12 +1808,17 @@ public class CapgoUpdater {
|
|
|
1730
1808
|
this.editor.commit();
|
|
1731
1809
|
}
|
|
1732
1810
|
|
|
1733
|
-
private File getBundleDirectory(final String id) {
|
|
1734
|
-
return
|
|
1811
|
+
private File getBundleDirectory(final String id) throws IOException {
|
|
1812
|
+
return resolveBundleDirectory(this.documentsDir, id);
|
|
1735
1813
|
}
|
|
1736
1814
|
|
|
1737
1815
|
private boolean bundleExists(final String id) {
|
|
1738
|
-
final File bundle
|
|
1816
|
+
final File bundle;
|
|
1817
|
+
try {
|
|
1818
|
+
bundle = this.getBundleDirectory(id);
|
|
1819
|
+
} catch (IOException e) {
|
|
1820
|
+
return false;
|
|
1821
|
+
}
|
|
1739
1822
|
final BundleInfo bundleInfo = this.getBundleInfo(id);
|
|
1740
1823
|
return (
|
|
1741
1824
|
bundle.isDirectory() &&
|
|
@@ -1812,7 +1895,16 @@ public class CapgoUpdater {
|
|
|
1812
1895
|
this.reset();
|
|
1813
1896
|
return true;
|
|
1814
1897
|
}
|
|
1815
|
-
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
|
+
}
|
|
1816
1908
|
logger.info("Setting next active bundle: " + id);
|
|
1817
1909
|
if (this.bundleExists(id)) {
|
|
1818
1910
|
var currentBundleName = this.getCurrentBundle().getVersionName();
|
|
@@ -1830,8 +1922,12 @@ public class CapgoUpdater {
|
|
|
1830
1922
|
if (bundle == null || bundle.isBuiltin() || !this.bundleExists(bundle.getId())) {
|
|
1831
1923
|
return false;
|
|
1832
1924
|
}
|
|
1833
|
-
|
|
1834
|
-
|
|
1925
|
+
try {
|
|
1926
|
+
this.setCurrentBundle(this.getBundleDirectory(bundle.getId()));
|
|
1927
|
+
return true;
|
|
1928
|
+
} catch (IOException e) {
|
|
1929
|
+
return false;
|
|
1930
|
+
}
|
|
1835
1931
|
}
|
|
1836
1932
|
|
|
1837
1933
|
boolean stagePreviewFallbackReload(final BundleInfo bundle) {
|
|
@@ -1845,8 +1941,12 @@ public class CapgoUpdater {
|
|
|
1845
1941
|
if (!this.bundleExists(bundle.getId())) {
|
|
1846
1942
|
return false;
|
|
1847
1943
|
}
|
|
1848
|
-
|
|
1849
|
-
|
|
1944
|
+
try {
|
|
1945
|
+
this.setCurrentBundle(this.getBundleDirectory(bundle.getId()));
|
|
1946
|
+
return true;
|
|
1947
|
+
} catch (IOException e) {
|
|
1948
|
+
return false;
|
|
1949
|
+
}
|
|
1850
1950
|
}
|
|
1851
1951
|
|
|
1852
1952
|
void finalizePendingReload(final BundleInfo bundle, final String previousBundleName) {
|
|
@@ -50,17 +50,15 @@ public class CryptoCipher {
|
|
|
50
50
|
return decryptedBytes;
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
public static byte[] decryptAES(byte[] cipherText, SecretKey key, byte[] iv) {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
|
|
57
|
-
SecretKeySpec keySpec = new SecretKeySpec(key.getEncoded(), "AES");
|
|
58
|
-
cipher.init(Cipher.DECRYPT_MODE, keySpec, ivParameterSpec);
|
|
59
|
-
return cipher.doFinal(cipherText);
|
|
60
|
-
} catch (Exception e) {
|
|
61
|
-
e.printStackTrace();
|
|
53
|
+
public static byte[] decryptAES(byte[] cipherText, SecretKey key, byte[] iv) throws GeneralSecurityException {
|
|
54
|
+
if (key == null || iv == null) {
|
|
55
|
+
throw new IllegalArgumentException("AES key and IV must not be null");
|
|
62
56
|
}
|
|
63
|
-
|
|
57
|
+
IvParameterSpec ivParameterSpec = new IvParameterSpec(iv);
|
|
58
|
+
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
|
|
59
|
+
SecretKeySpec keySpec = new SecretKeySpec(key.getEncoded(), "AES");
|
|
60
|
+
cipher.init(Cipher.DECRYPT_MODE, keySpec, ivParameterSpec);
|
|
61
|
+
return cipher.doFinal(cipherText);
|
|
64
62
|
}
|
|
65
63
|
|
|
66
64
|
public static SecretKey byteToSessionKey(byte[] sessionKey) {
|
|
@@ -137,13 +135,25 @@ public class CryptoCipher {
|
|
|
137
135
|
return buf;
|
|
138
136
|
}
|
|
139
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
|
+
|
|
140
146
|
public static void decryptFile(final File file, final String publicKey, final String ivSessionKey) throws IOException {
|
|
141
|
-
if (publicKey.isEmpty() ||
|
|
142
|
-
logger
|
|
147
|
+
if (publicKey.isEmpty() || !isValidSessionKey(ivSessionKey)) {
|
|
148
|
+
if (logger != null) {
|
|
149
|
+
logger.info("Encryption not set, no public key or session, ignored");
|
|
150
|
+
}
|
|
143
151
|
return;
|
|
144
152
|
}
|
|
145
153
|
if (!publicKey.startsWith("-----BEGIN RSA PUBLIC KEY-----")) {
|
|
146
|
-
logger
|
|
154
|
+
if (logger != null) {
|
|
155
|
+
logger.error("The public key is not a valid RSA Public key");
|
|
156
|
+
}
|
|
147
157
|
return;
|
|
148
158
|
}
|
|
149
159
|
|
|
@@ -152,29 +162,50 @@ public class CryptoCipher {
|
|
|
152
162
|
String sessionKeyB64 = ivSessionKey.split(":")[1];
|
|
153
163
|
byte[] iv = Base64.decode(ivB64.getBytes(), Base64.DEFAULT);
|
|
154
164
|
byte[] sessionKey = Base64.decode(sessionKeyB64.getBytes(), Base64.DEFAULT);
|
|
165
|
+
if (iv.length != 16) {
|
|
166
|
+
throw new IOException("AES file decryption failed: IV must be 16 bytes");
|
|
167
|
+
}
|
|
155
168
|
PublicKey pKey = CryptoCipher.stringToPublicKey(publicKey);
|
|
156
169
|
byte[] decryptedSessionKey = CryptoCipher.decryptRSA(sessionKey, pKey);
|
|
170
|
+
if (decryptedSessionKey == null || decryptedSessionKey.length != 16) {
|
|
171
|
+
throw new IOException("AES file decryption failed: decrypted session key must be 16 bytes");
|
|
172
|
+
}
|
|
157
173
|
|
|
158
174
|
SecretKey sKey = CryptoCipher.byteToSessionKey(decryptedSessionKey);
|
|
159
175
|
decryptAesFile(file, sKey, iv);
|
|
160
176
|
} catch (GeneralSecurityException e) {
|
|
161
|
-
logger
|
|
162
|
-
|
|
177
|
+
if (logger != null) {
|
|
178
|
+
logger.info("decryptFile fail");
|
|
179
|
+
}
|
|
180
|
+
throw new IOException("AES file decryption failed: " + e.getMessage(), e);
|
|
163
181
|
}
|
|
164
182
|
}
|
|
165
183
|
|
|
166
|
-
static void decryptAesFile(File file, SecretKey key, byte[] iv) throws IOException
|
|
184
|
+
static void decryptAesFile(File file, SecretKey key, byte[] iv) throws IOException {
|
|
185
|
+
if (key == null) {
|
|
186
|
+
throw new IOException("AES file decryption failed: missing session key");
|
|
187
|
+
}
|
|
188
|
+
if (iv == null) {
|
|
189
|
+
throw new IOException("AES file decryption failed: missing IV");
|
|
190
|
+
}
|
|
191
|
+
if (iv.length != 16) {
|
|
192
|
+
throw new IOException("AES file decryption failed: IV must be 16 bytes");
|
|
193
|
+
}
|
|
194
|
+
byte[] keyBytes = key.getEncoded();
|
|
195
|
+
if (keyBytes == null || keyBytes.length != 16) {
|
|
196
|
+
throw new IOException("AES file decryption failed: session key must be 16 bytes");
|
|
197
|
+
}
|
|
167
198
|
if (file.length() == 0) {
|
|
168
199
|
throw new IOException("Empty encrypted data");
|
|
169
200
|
}
|
|
170
|
-
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
|
|
171
|
-
cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(key.getEncoded(), "AES"), new IvParameterSpec(iv));
|
|
172
201
|
File parent = file.getAbsoluteFile().getParentFile();
|
|
173
202
|
if (parent == null) {
|
|
174
203
|
throw new IOException("Cannot create temp file for " + file.getAbsolutePath());
|
|
175
204
|
}
|
|
176
205
|
File tempFile = File.createTempFile("capgo-aes-", ".tmp", parent);
|
|
177
206
|
try {
|
|
207
|
+
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
|
|
208
|
+
cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(keyBytes, "AES"), new IvParameterSpec(iv));
|
|
178
209
|
byte[] inBuf = new byte[ioBufferBytes()];
|
|
179
210
|
// Reuse one output buffer. cipher.update(in) allocates a new byte[] per chunk.
|
|
180
211
|
byte[] outBuf = new byte[inBuf.length + 16];
|
|
@@ -196,6 +227,8 @@ public class CryptoCipher {
|
|
|
196
227
|
}
|
|
197
228
|
replaceFile(tempFile, file);
|
|
198
229
|
tempFile = null;
|
|
230
|
+
} catch (GeneralSecurityException e) {
|
|
231
|
+
throw new IOException("AES file decryption failed: " + e.getMessage(), e);
|
|
199
232
|
} finally {
|
|
200
233
|
if (tempFile != null && tempFile.exists()) {
|
|
201
234
|
tempFile.delete();
|