@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.
- package/android/src/main/java/ee/forgr/capacitor_updater/CapacitorUpdaterPlugin.java +2 -1
- package/android/src/main/java/ee/forgr/capacitor_updater/CapgoUpdater.java +19 -0
- package/android/src/main/java/ee/forgr/capacitor_updater/CryptoCipher.java +103 -4
- package/android/src/main/java/ee/forgr/capacitor_updater/DownloadService.java +2 -0
- package/ios/Sources/CapacitorUpdaterPlugin/CapacitorUpdaterPlugin.swift +6 -1
- package/ios/Sources/CapacitorUpdaterPlugin/CapgoUpdater.swift +16 -0
- package/ios/Sources/CapacitorUpdaterPlugin/CryptoCipher.swift +84 -4
- package/package.json +1 -1
|
@@ -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.
|
|
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
|
-
|
|
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
|
-
//
|
|
192
|
-
|
|
193
|
-
|
|
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.
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
|
|
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()
|