@capgo/capacitor-updater 6.30.0 → 6.35.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/README.md +100 -12
- package/android/build.gradle +3 -3
- package/android/src/main/java/ee/forgr/capacitor_updater/BundleInfo.java +60 -8
- package/android/src/main/java/ee/forgr/capacitor_updater/CapacitorUpdaterPlugin.java +76 -38
- package/android/src/main/java/ee/forgr/capacitor_updater/CapgoUpdater.java +49 -112
- package/android/src/main/java/ee/forgr/capacitor_updater/{CryptoCipherV2.java → CryptoCipher.java} +80 -8
- package/android/src/main/java/ee/forgr/capacitor_updater/DownloadService.java +86 -75
- package/dist/docs.json +105 -8
- package/dist/esm/definitions.d.ts +97 -1
- package/dist/esm/definitions.js.map +1 -1
- package/ios/Sources/CapacitorUpdaterPlugin/BundleInfo.swift +37 -10
- package/ios/Sources/CapacitorUpdaterPlugin/CapacitorUpdaterPlugin.swift +33 -13
- package/ios/Sources/CapacitorUpdaterPlugin/CapgoUpdater.swift +80 -89
- package/ios/Sources/CapacitorUpdaterPlugin/{CryptoCipherV2.swift → CryptoCipher.swift} +49 -3
- package/ios/Sources/CapacitorUpdaterPlugin/InternalUtils.swift +4 -0
- package/package.json +2 -1
- package/android/src/main/java/ee/forgr/capacitor_updater/CryptoCipherV1.java +0 -222
- package/ios/Sources/CapacitorUpdaterPlugin/CryptoCipherV1.swift +0 -245
|
@@ -400,16 +400,16 @@ public class CapgoUpdater {
|
|
|
400
400
|
|
|
401
401
|
if (!this.hasOldPrivateKeyPropertyInConfig && !sessionKey.isEmpty()) {
|
|
402
402
|
// V2 Encryption (publicKey)
|
|
403
|
-
|
|
404
|
-
checksumDecrypted =
|
|
405
|
-
checksum =
|
|
403
|
+
CryptoCipher.decryptFile(downloaded, publicKey, sessionKey);
|
|
404
|
+
checksumDecrypted = CryptoCipher.decryptChecksum(checksumRes, publicKey);
|
|
405
|
+
checksum = CryptoCipher.calcChecksum(downloaded);
|
|
406
406
|
} else if (this.hasOldPrivateKeyPropertyInConfig) {
|
|
407
|
-
// V1 Encryption (privateKey) - deprecated
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
} else {
|
|
411
|
-
checksum = CryptoCipherV2.calcChecksum(downloaded);
|
|
407
|
+
// V1 Encryption (privateKey) - deprecated not supported
|
|
408
|
+
this.sendStats("checksum_fail");
|
|
409
|
+
throw new IOException("V1 decryption is no longer supported for security reasons.");
|
|
412
410
|
}
|
|
411
|
+
CryptoCipher.logChecksumInfo("Calculated checksum", checksum);
|
|
412
|
+
CryptoCipher.logChecksumInfo("Expected checksum", checksumDecrypted);
|
|
413
413
|
if ((!checksumDecrypted.isEmpty() || !this.publicKey.isEmpty()) && !checksumDecrypted.equals(checksum)) {
|
|
414
414
|
logger.error("Error checksum '" + checksumDecrypted + "' '" + checksum + "' '");
|
|
415
415
|
this.sendStats("checksum_fail");
|
|
@@ -973,115 +973,41 @@ public class CapgoUpdater {
|
|
|
973
973
|
makeJsonRequest(updateUrl, json, callback);
|
|
974
974
|
}
|
|
975
975
|
|
|
976
|
-
public void unsetChannel(
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
976
|
+
public void unsetChannel(
|
|
977
|
+
final SharedPreferences.Editor editor,
|
|
978
|
+
final String defaultChannelKey,
|
|
979
|
+
final String configDefaultChannel,
|
|
980
|
+
final Callback callback
|
|
981
|
+
) {
|
|
982
|
+
// Clear persisted defaultChannel and revert to config value
|
|
983
|
+
editor.remove(defaultChannelKey);
|
|
984
|
+
editor.apply();
|
|
985
|
+
this.defaultChannel = configDefaultChannel;
|
|
986
|
+
logger.info("Persisted defaultChannel cleared, reverted to config value: " + configDefaultChannel);
|
|
987
|
+
|
|
988
|
+
Map<String, Object> ret = new HashMap<>();
|
|
989
|
+
ret.put("status", "ok");
|
|
990
|
+
ret.put("message", "Channel override removed");
|
|
991
|
+
callback.callback(ret);
|
|
992
|
+
}
|
|
986
993
|
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
994
|
+
public void setChannel(
|
|
995
|
+
final String channel,
|
|
996
|
+
final SharedPreferences.Editor editor,
|
|
997
|
+
final String defaultChannelKey,
|
|
998
|
+
final boolean allowSetDefaultChannel,
|
|
999
|
+
final Callback callback
|
|
1000
|
+
) {
|
|
1001
|
+
// Check if setting defaultChannel is allowed
|
|
1002
|
+
if (!allowSetDefaultChannel) {
|
|
1003
|
+
logger.error("setChannel is disabled by allowSetDefaultChannel config");
|
|
990
1004
|
final Map<String, Object> retError = new HashMap<>();
|
|
991
|
-
retError.put("message", "
|
|
992
|
-
retError.put("error", "
|
|
1005
|
+
retError.put("message", "setChannel is disabled by configuration");
|
|
1006
|
+
retError.put("error", "disabled_by_config");
|
|
993
1007
|
callback.callback(retError);
|
|
994
1008
|
return;
|
|
995
1009
|
}
|
|
996
|
-
JSONObject json;
|
|
997
|
-
try {
|
|
998
|
-
json = this.createInfoObject();
|
|
999
|
-
} catch (JSONException e) {
|
|
1000
|
-
logger.error("Error unsetChannel JSONException " + e.getMessage());
|
|
1001
|
-
final Map<String, Object> retError = new HashMap<>();
|
|
1002
|
-
retError.put("message", "Cannot get info: " + e);
|
|
1003
|
-
retError.put("error", "json_error");
|
|
1004
|
-
callback.callback(retError);
|
|
1005
|
-
return;
|
|
1006
|
-
}
|
|
1007
|
-
|
|
1008
|
-
Request request = new Request.Builder()
|
|
1009
|
-
.url(channelUrl)
|
|
1010
|
-
.delete(RequestBody.create(json.toString(), MediaType.get("application/json")))
|
|
1011
|
-
.build();
|
|
1012
|
-
|
|
1013
|
-
DownloadService.sharedClient
|
|
1014
|
-
.newCall(request)
|
|
1015
|
-
.enqueue(
|
|
1016
|
-
new okhttp3.Callback() {
|
|
1017
|
-
@Override
|
|
1018
|
-
public void onFailure(@NonNull Call call, @NonNull IOException e) {
|
|
1019
|
-
Map<String, Object> retError = new HashMap<>();
|
|
1020
|
-
retError.put("message", "Request failed: " + e.getMessage());
|
|
1021
|
-
retError.put("error", "network_error");
|
|
1022
|
-
callback.callback(retError);
|
|
1023
|
-
}
|
|
1024
1010
|
|
|
1025
|
-
@Override
|
|
1026
|
-
public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException {
|
|
1027
|
-
try (ResponseBody responseBody = response.body()) {
|
|
1028
|
-
// Check for 429 rate limit
|
|
1029
|
-
if (checkAndHandleRateLimitResponse(response)) {
|
|
1030
|
-
Map<String, Object> retError = new HashMap<>();
|
|
1031
|
-
retError.put("message", "Rate limit exceeded");
|
|
1032
|
-
retError.put("error", "rate_limit_exceeded");
|
|
1033
|
-
callback.callback(retError);
|
|
1034
|
-
return;
|
|
1035
|
-
}
|
|
1036
|
-
|
|
1037
|
-
if (!response.isSuccessful()) {
|
|
1038
|
-
Map<String, Object> retError = new HashMap<>();
|
|
1039
|
-
retError.put("message", "Server error: " + response.code());
|
|
1040
|
-
retError.put("error", "response_error");
|
|
1041
|
-
callback.callback(retError);
|
|
1042
|
-
return;
|
|
1043
|
-
}
|
|
1044
|
-
|
|
1045
|
-
assert responseBody != null;
|
|
1046
|
-
String responseData = responseBody.string();
|
|
1047
|
-
JSONObject jsonResponse = new JSONObject(responseData);
|
|
1048
|
-
|
|
1049
|
-
// Check for server-side errors first
|
|
1050
|
-
if (jsonResponse.has("error")) {
|
|
1051
|
-
Map<String, Object> retError = new HashMap<>();
|
|
1052
|
-
retError.put("error", jsonResponse.getString("error"));
|
|
1053
|
-
if (jsonResponse.has("message")) {
|
|
1054
|
-
retError.put("message", jsonResponse.getString("message"));
|
|
1055
|
-
} else {
|
|
1056
|
-
retError.put("message", "server did not provide a message");
|
|
1057
|
-
}
|
|
1058
|
-
callback.callback(retError);
|
|
1059
|
-
return;
|
|
1060
|
-
}
|
|
1061
|
-
|
|
1062
|
-
Map<String, Object> ret = new HashMap<>();
|
|
1063
|
-
|
|
1064
|
-
Iterator<String> keys = jsonResponse.keys();
|
|
1065
|
-
while (keys.hasNext()) {
|
|
1066
|
-
String key = keys.next();
|
|
1067
|
-
if (jsonResponse.has(key)) {
|
|
1068
|
-
ret.put(key, jsonResponse.get(key));
|
|
1069
|
-
}
|
|
1070
|
-
}
|
|
1071
|
-
logger.info("Channel unset");
|
|
1072
|
-
callback.callback(ret);
|
|
1073
|
-
} catch (JSONException e) {
|
|
1074
|
-
Map<String, Object> retError = new HashMap<>();
|
|
1075
|
-
retError.put("message", "JSON parse error: " + e.getMessage());
|
|
1076
|
-
retError.put("error", "parse_error");
|
|
1077
|
-
callback.callback(retError);
|
|
1078
|
-
}
|
|
1079
|
-
}
|
|
1080
|
-
}
|
|
1081
|
-
);
|
|
1082
|
-
}
|
|
1083
|
-
|
|
1084
|
-
public void setChannel(final String channel, final Callback callback) {
|
|
1085
1011
|
// Check if rate limit was exceeded
|
|
1086
1012
|
if (rateLimitExceeded) {
|
|
1087
1013
|
logger.debug("Skipping setChannel due to rate limit (429). Requests will resume after app restart.");
|
|
@@ -1114,7 +1040,18 @@ public class CapgoUpdater {
|
|
|
1114
1040
|
return;
|
|
1115
1041
|
}
|
|
1116
1042
|
|
|
1117
|
-
makeJsonRequest(channelUrl, json,
|
|
1043
|
+
makeJsonRequest(channelUrl, json, (res) -> {
|
|
1044
|
+
if (res.containsKey("error")) {
|
|
1045
|
+
callback.callback(res);
|
|
1046
|
+
} else {
|
|
1047
|
+
// Success - persist defaultChannel
|
|
1048
|
+
this.defaultChannel = channel;
|
|
1049
|
+
editor.putString(defaultChannelKey, channel);
|
|
1050
|
+
editor.apply();
|
|
1051
|
+
logger.info("defaultChannel persisted locally: " + channel);
|
|
1052
|
+
callback.callback(res);
|
|
1053
|
+
}
|
|
1054
|
+
});
|
|
1118
1055
|
}
|
|
1119
1056
|
|
|
1120
1057
|
public void getChannel(final Callback callback) {
|
package/android/src/main/java/ee/forgr/capacitor_updater/{CryptoCipherV2.java → CryptoCipher.java}
RENAMED
|
@@ -10,8 +10,6 @@ package ee.forgr.capacitor_updater;
|
|
|
10
10
|
* Created by Awesometic
|
|
11
11
|
* It's encrypt returns Base64 encoded, and also decrypt for Base64 encoded cipher
|
|
12
12
|
* references: http://stackoverflow.com/questions/12471999/rsa-encryption-decryption-in-android
|
|
13
|
-
*
|
|
14
|
-
* V2 Encryption - uses publicKey (modern encryption from main branch)
|
|
15
13
|
*/
|
|
16
14
|
import android.util.Base64;
|
|
17
15
|
import java.io.BufferedInputStream;
|
|
@@ -37,7 +35,7 @@ import javax.crypto.SecretKey;
|
|
|
37
35
|
import javax.crypto.spec.IvParameterSpec;
|
|
38
36
|
import javax.crypto.spec.SecretKeySpec;
|
|
39
37
|
|
|
40
|
-
public class
|
|
38
|
+
public class CryptoCipher {
|
|
41
39
|
|
|
42
40
|
private static Logger logger;
|
|
43
41
|
|
|
@@ -155,10 +153,10 @@ public class CryptoCipherV2 {
|
|
|
155
153
|
String sessionKeyB64 = ivSessionKey.split(":")[1];
|
|
156
154
|
byte[] iv = Base64.decode(ivB64.getBytes(), Base64.DEFAULT);
|
|
157
155
|
byte[] sessionKey = Base64.decode(sessionKeyB64.getBytes(), Base64.DEFAULT);
|
|
158
|
-
PublicKey pKey =
|
|
159
|
-
byte[] decryptedSessionKey =
|
|
156
|
+
PublicKey pKey = CryptoCipher.stringToPublicKey(publicKey);
|
|
157
|
+
byte[] decryptedSessionKey = CryptoCipher.decryptRSA(sessionKey, pKey);
|
|
160
158
|
|
|
161
|
-
SecretKey sKey =
|
|
159
|
+
SecretKey sKey = CryptoCipher.byteToSessionKey(decryptedSessionKey);
|
|
162
160
|
byte[] content = new byte[(int) file.length()];
|
|
163
161
|
|
|
164
162
|
try (
|
|
@@ -168,7 +166,7 @@ public class CryptoCipherV2 {
|
|
|
168
166
|
) {
|
|
169
167
|
dis.readFully(content);
|
|
170
168
|
dis.close();
|
|
171
|
-
byte[] decrypted =
|
|
169
|
+
byte[] decrypted = CryptoCipher.decryptAES(content, sKey, iv);
|
|
172
170
|
// write the decrypted string to the file
|
|
173
171
|
try (final FileOutputStream fos = new FileOutputStream(file.getAbsolutePath())) {
|
|
174
172
|
fos.write(decrypted);
|
|
@@ -200,14 +198,26 @@ public class CryptoCipherV2 {
|
|
|
200
198
|
// Determine if input is hex or base64 encoded
|
|
201
199
|
// Hex strings only contain 0-9 and a-f, while base64 contains other characters
|
|
202
200
|
byte[] checksumBytes;
|
|
201
|
+
String detectedFormat;
|
|
203
202
|
if (checksum.matches("^[0-9a-fA-F]+$")) {
|
|
204
203
|
// Hex encoded (new format from CLI for plugin versions >= 5.30.0, 6.30.0, 7.30.0)
|
|
205
204
|
checksumBytes = hexStringToByteArray(checksum);
|
|
205
|
+
detectedFormat = "hex";
|
|
206
206
|
} else {
|
|
207
207
|
// TODO: remove backwards compatibility
|
|
208
208
|
// Base64 encoded (old format for backwards compatibility)
|
|
209
209
|
checksumBytes = Base64.decode(checksum, Base64.DEFAULT);
|
|
210
|
+
detectedFormat = "base64";
|
|
210
211
|
}
|
|
212
|
+
logger.debug(
|
|
213
|
+
"Received encrypted checksum format: " +
|
|
214
|
+
detectedFormat +
|
|
215
|
+
" (length: " +
|
|
216
|
+
checksum.length() +
|
|
217
|
+
" chars, " +
|
|
218
|
+
checksumBytes.length +
|
|
219
|
+
" bytes)"
|
|
220
|
+
);
|
|
211
221
|
PublicKey pKey = CryptoCipher.stringToPublicKey(publicKey);
|
|
212
222
|
byte[] decryptedChecksum = CryptoCipher.decryptRSA(checksumBytes, pKey);
|
|
213
223
|
// Return as hex string to match calcChecksum output format
|
|
@@ -217,13 +227,75 @@ public class CryptoCipherV2 {
|
|
|
217
227
|
if (hex.length() == 1) hexString.append('0');
|
|
218
228
|
hexString.append(hex);
|
|
219
229
|
}
|
|
220
|
-
|
|
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;
|
|
221
257
|
} catch (GeneralSecurityException e) {
|
|
222
258
|
logger.error("decryptChecksum fail: " + e.getMessage());
|
|
223
259
|
throw new IOException("Decryption failed: " + e.getMessage());
|
|
224
260
|
}
|
|
225
261
|
}
|
|
226
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
|
+
|
|
227
299
|
public static String calcChecksum(File file) {
|
|
228
300
|
final int BUFFER_SIZE = 1024 * 1024 * 5; // 5 MB buffer size
|
|
229
301
|
MessageDigest digest;
|
|
@@ -293,7 +293,7 @@ public class DownloadService extends Worker {
|
|
|
293
293
|
|
|
294
294
|
if (!publicKey.isEmpty() && sessionKey != null && !sessionKey.isEmpty()) {
|
|
295
295
|
try {
|
|
296
|
-
fileHash =
|
|
296
|
+
fileHash = CryptoCipher.decryptChecksum(fileHash, publicKey);
|
|
297
297
|
} catch (Exception e) {
|
|
298
298
|
logger.error("Error decrypting checksum for " + fileName + "fileHash: " + fileHash);
|
|
299
299
|
hasError.set(true);
|
|
@@ -302,7 +302,12 @@ public class DownloadService extends Worker {
|
|
|
302
302
|
}
|
|
303
303
|
|
|
304
304
|
final String finalFileHash = fileHash;
|
|
305
|
-
|
|
305
|
+
|
|
306
|
+
// Check if file is a Brotli file and remove .br extension from target
|
|
307
|
+
boolean isBrotli = fileName.endsWith(".br");
|
|
308
|
+
String targetFileName = isBrotli ? fileName.substring(0, fileName.length() - 3) : fileName;
|
|
309
|
+
|
|
310
|
+
File targetFile = new File(destFolder, targetFileName);
|
|
306
311
|
File cacheFile = new File(cacheFolder, finalFileHash + "_" + new File(fileName).getName());
|
|
307
312
|
File builtinFile = new File(builtinFolder, fileName);
|
|
308
313
|
|
|
@@ -313,6 +318,7 @@ public class DownloadService extends Worker {
|
|
|
313
318
|
continue;
|
|
314
319
|
}
|
|
315
320
|
|
|
321
|
+
final boolean finalIsBrotli = isBrotli;
|
|
316
322
|
Future<?> future = executor.submit(() -> {
|
|
317
323
|
try {
|
|
318
324
|
if (builtinFile.exists() && verifyChecksum(builtinFile, finalFileHash)) {
|
|
@@ -322,7 +328,7 @@ public class DownloadService extends Worker {
|
|
|
322
328
|
copyFile(cacheFile, targetFile);
|
|
323
329
|
logger.debug("already cached " + fileName);
|
|
324
330
|
} else {
|
|
325
|
-
downloadAndVerify(downloadUrl, targetFile, cacheFile, finalFileHash, sessionKey, publicKey);
|
|
331
|
+
downloadAndVerify(downloadUrl, targetFile, cacheFile, finalFileHash, sessionKey, publicKey, finalIsBrotli);
|
|
326
332
|
}
|
|
327
333
|
|
|
328
334
|
long completed = completedFiles.incrementAndGet();
|
|
@@ -572,98 +578,103 @@ public class DownloadService extends Worker {
|
|
|
572
578
|
File cacheFile,
|
|
573
579
|
String expectedHash,
|
|
574
580
|
String sessionKey,
|
|
575
|
-
String publicKey
|
|
581
|
+
String publicKey,
|
|
582
|
+
boolean isBrotli
|
|
576
583
|
) throws Exception {
|
|
577
584
|
logger.debug("downloadAndVerify " + downloadUrl);
|
|
578
585
|
|
|
579
586
|
Request request = new Request.Builder().url(downloadUrl).build();
|
|
580
587
|
|
|
581
|
-
//
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
// Create final target file with .br extension removed if it's a Brotli file
|
|
585
|
-
File finalTargetFile = isBrotli
|
|
586
|
-
? new File(targetFile.getParentFile(), targetFile.getName().substring(0, targetFile.getName().length() - 3))
|
|
587
|
-
: targetFile;
|
|
588
|
+
// targetFile is already the final destination without .br extension
|
|
589
|
+
File finalTargetFile = targetFile;
|
|
588
590
|
|
|
589
591
|
// Create a temporary file for the compressed data
|
|
590
592
|
File compressedFile = new File(getApplicationContext().getCacheDir(), "temp_" + targetFile.getName() + ".tmp");
|
|
591
593
|
|
|
592
|
-
try
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
594
|
+
try {
|
|
595
|
+
try (Response response = sharedClient.newCall(request).execute()) {
|
|
596
|
+
if (!response.isSuccessful()) {
|
|
597
|
+
sendStatsAsync("download_manifest_file_fail", getInputData().getString(VERSION) + ":" + finalTargetFile.getName());
|
|
598
|
+
throw new IOException("Unexpected response code: " + response.code());
|
|
599
|
+
}
|
|
597
600
|
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
601
|
+
// Download compressed file atomically
|
|
602
|
+
ResponseBody responseBody = response.body();
|
|
603
|
+
if (responseBody == null) {
|
|
604
|
+
throw new IOException("Response body is null");
|
|
605
|
+
}
|
|
603
606
|
|
|
604
|
-
|
|
605
|
-
|
|
607
|
+
// Use OkIO for atomic write
|
|
608
|
+
writeFileAtomic(compressedFile, responseBody.byteStream(), null);
|
|
606
609
|
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
610
|
+
if (!publicKey.isEmpty() && sessionKey != null && !sessionKey.isEmpty()) {
|
|
611
|
+
logger.debug("Decrypting file " + targetFile.getName());
|
|
612
|
+
CryptoCipher.decryptFile(compressedFile, publicKey, sessionKey);
|
|
613
|
+
}
|
|
611
614
|
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
615
|
+
// Only decompress if file has .br extension
|
|
616
|
+
if (isBrotli) {
|
|
617
|
+
// Use new decompression method with atomic write
|
|
618
|
+
try (FileInputStream fis = new FileInputStream(compressedFile)) {
|
|
619
|
+
byte[] compressedData = new byte[(int) compressedFile.length()];
|
|
620
|
+
fis.read(compressedData);
|
|
621
|
+
byte[] decompressedData;
|
|
622
|
+
try {
|
|
623
|
+
decompressedData = decompressBrotli(compressedData, targetFile.getName());
|
|
624
|
+
} catch (IOException e) {
|
|
625
|
+
sendStatsAsync(
|
|
626
|
+
"download_manifest_brotli_fail",
|
|
627
|
+
getInputData().getString(VERSION) + ":" + finalTargetFile.getName()
|
|
628
|
+
);
|
|
629
|
+
throw e;
|
|
630
|
+
}
|
|
628
631
|
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
+
// Write decompressed data atomically
|
|
633
|
+
try (java.io.ByteArrayInputStream bais = new java.io.ByteArrayInputStream(decompressedData)) {
|
|
634
|
+
writeFileAtomic(finalTargetFile, bais, null);
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
} else {
|
|
638
|
+
// Just copy the file without decompression using atomic operation
|
|
639
|
+
try (FileInputStream fis = new FileInputStream(compressedFile)) {
|
|
640
|
+
writeFileAtomic(finalTargetFile, fis, null);
|
|
632
641
|
}
|
|
633
642
|
}
|
|
634
|
-
} else {
|
|
635
|
-
// Just copy the file without decompression using atomic operation
|
|
636
|
-
try (FileInputStream fis = new FileInputStream(compressedFile)) {
|
|
637
|
-
writeFileAtomic(finalTargetFile, fis, null);
|
|
638
|
-
}
|
|
639
|
-
}
|
|
640
643
|
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
//
|
|
648
|
-
|
|
649
|
-
|
|
644
|
+
// Delete the compressed file
|
|
645
|
+
compressedFile.delete();
|
|
646
|
+
String calculatedHash = CryptoCipher.calcChecksum(finalTargetFile);
|
|
647
|
+
CryptoCipher.logChecksumInfo("Calculated checksum", calculatedHash);
|
|
648
|
+
CryptoCipher.logChecksumInfo("Expected checksum", expectedHash);
|
|
649
|
+
|
|
650
|
+
// Verify checksum
|
|
651
|
+
if (calculatedHash.equals(expectedHash)) {
|
|
652
|
+
// Only cache if checksum is correct - use atomic copy
|
|
653
|
+
try (FileInputStream fis = new FileInputStream(finalTargetFile)) {
|
|
654
|
+
writeFileAtomic(cacheFile, fis, expectedHash);
|
|
655
|
+
}
|
|
656
|
+
} else {
|
|
657
|
+
finalTargetFile.delete();
|
|
658
|
+
sendStatsAsync("download_manifest_checksum_fail", getInputData().getString(VERSION) + ":" + finalTargetFile.getName());
|
|
659
|
+
throw new IOException(
|
|
660
|
+
"Checksum verification failed for: " +
|
|
661
|
+
downloadUrl +
|
|
662
|
+
" " +
|
|
663
|
+
targetFile.getName() +
|
|
664
|
+
" expected: " +
|
|
665
|
+
expectedHash +
|
|
666
|
+
" calculated: " +
|
|
667
|
+
calculatedHash
|
|
668
|
+
);
|
|
650
669
|
}
|
|
651
|
-
} else {
|
|
652
|
-
finalTargetFile.delete();
|
|
653
|
-
sendStatsAsync("download_manifest_checksum_fail", getInputData().getString(VERSION) + ":" + finalTargetFile.getName());
|
|
654
|
-
throw new IOException(
|
|
655
|
-
"Checksum verification failed for: " +
|
|
656
|
-
downloadUrl +
|
|
657
|
-
" " +
|
|
658
|
-
targetFile.getName() +
|
|
659
|
-
" expected: " +
|
|
660
|
-
expectedHash +
|
|
661
|
-
" calculated: " +
|
|
662
|
-
calculatedHash
|
|
663
|
-
);
|
|
664
670
|
}
|
|
665
671
|
} catch (Exception e) {
|
|
666
672
|
throw new IOException("Error in downloadAndVerify: " + e.getMessage());
|
|
673
|
+
} finally {
|
|
674
|
+
// Always cleanup the compressed temp file if it still exists
|
|
675
|
+
if (compressedFile.exists()) {
|
|
676
|
+
compressedFile.delete();
|
|
677
|
+
}
|
|
667
678
|
}
|
|
668
679
|
}
|
|
669
680
|
|
|
@@ -769,7 +780,7 @@ public class DownloadService extends Worker {
|
|
|
769
780
|
|
|
770
781
|
// Verify checksum if provided
|
|
771
782
|
if (expectedChecksum != null && !expectedChecksum.isEmpty()) {
|
|
772
|
-
String actualChecksum =
|
|
783
|
+
String actualChecksum = CryptoCipher.calcChecksum(tempFile);
|
|
773
784
|
if (!expectedChecksum.equalsIgnoreCase(actualChecksum)) {
|
|
774
785
|
tempFile.delete();
|
|
775
786
|
throw new IOException("Checksum verification failed");
|