@capgo/capacitor-updater 8.51.8 → 8.51.10
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 +1 -1
- package/android/src/main/java/ee/forgr/capacitor_updater/CapgoUpdater.java +72 -23
- package/android/src/main/java/ee/forgr/capacitor_updater/CryptoCipher.java +129 -23
- package/android/src/main/java/ee/forgr/capacitor_updater/DownloadService.java +306 -115
- package/ios/Sources/CapacitorUpdaterPlugin/AES.swift +119 -0
- package/ios/Sources/CapacitorUpdaterPlugin/CapacitorUpdaterPlugin.swift +1 -1
- package/ios/Sources/CapacitorUpdaterPlugin/CapgoUpdater.swift +249 -160
- package/ios/Sources/CapacitorUpdaterPlugin/CryptoCipher.swift +41 -29
- package/package.json +1 -1
|
@@ -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.10";
|
|
150
150
|
private static final String DELAY_CONDITION_PREFERENCES = "";
|
|
151
151
|
|
|
152
152
|
private SharedPreferences.Editor editor;
|
|
@@ -449,7 +449,7 @@ public class CapgoUpdater {
|
|
|
449
449
|
continue;
|
|
450
450
|
}
|
|
451
451
|
try {
|
|
452
|
-
|
|
452
|
+
copyFileAtomically(file, cacheFile);
|
|
453
453
|
} catch (IOException e) {
|
|
454
454
|
logger.debug("Delta cache copy failed: " + file.getPath());
|
|
455
455
|
}
|
|
@@ -475,7 +475,7 @@ public class CapgoUpdater {
|
|
|
475
475
|
|
|
476
476
|
private void copyFile(final File source, final File dest) throws IOException {
|
|
477
477
|
try (final FileInputStream input = new FileInputStream(source); final FileOutputStream output = new FileOutputStream(dest)) {
|
|
478
|
-
final byte[] buffer = new byte[
|
|
478
|
+
final byte[] buffer = new byte[CryptoCipher.copyBufferBytes()];
|
|
479
479
|
int length;
|
|
480
480
|
while ((length = input.read(buffer)) != -1) {
|
|
481
481
|
output.write(buffer, 0, length);
|
|
@@ -515,28 +515,74 @@ public class CapgoUpdater {
|
|
|
515
515
|
return false;
|
|
516
516
|
}
|
|
517
517
|
|
|
518
|
-
|
|
519
|
-
if (verifyChecksum(builtinFile, fileHash)) {
|
|
518
|
+
if (DownloadService.builtinAssetMatches(this.activity.getAssets(), fileName, fileHash)) {
|
|
520
519
|
return true;
|
|
521
520
|
}
|
|
522
521
|
|
|
522
|
+
try {
|
|
523
|
+
final File builtinFile = DownloadService.resolveManifestBuiltinFile(new File(this.activity.getFilesDir(), "public"), fileName);
|
|
524
|
+
if (verifyChecksum(builtinFile, fileHash)) {
|
|
525
|
+
return true;
|
|
526
|
+
}
|
|
527
|
+
} catch (IOException ignored) {
|
|
528
|
+
// Invalid path; fall through to cache lookup.
|
|
529
|
+
}
|
|
530
|
+
|
|
523
531
|
final boolean isBrotli = fileName.endsWith(".br");
|
|
524
532
|
final String fileNameWithoutPath = new File(fileName).getName();
|
|
525
533
|
final String cacheBaseName = isBrotli ? fileNameWithoutPath.substring(0, fileNameWithoutPath.length() - 3) : fileNameWithoutPath;
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
534
|
+
if (isSafeCacheHash(fileHash)) {
|
|
535
|
+
final File cacheFolder = new File(this.activity.getCacheDir(), "capgo_downloads");
|
|
536
|
+
final File cacheFile = new File(cacheFolder, fileHash + "_" + cacheBaseName);
|
|
537
|
+
// Cache files are named `{hash}_{filename}` and were checksum-verified
|
|
538
|
+
// when written. Re-hashing every hit re-reads the whole bundle and
|
|
539
|
+
// OOMs/janks low-RAM devices during getMissing / delta apply.
|
|
540
|
+
if (isReusableCacheFile(cacheFile, fileHash)) {
|
|
541
|
+
return true;
|
|
542
|
+
}
|
|
531
543
|
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
544
|
+
if (isBrotli) {
|
|
545
|
+
final File legacyCacheFile = new File(cacheFolder, fileHash + "_" + fileNameWithoutPath);
|
|
546
|
+
return isReusableCacheFile(legacyCacheFile, fileHash);
|
|
547
|
+
}
|
|
535
548
|
}
|
|
536
549
|
|
|
537
550
|
return false;
|
|
538
551
|
}
|
|
539
552
|
|
|
553
|
+
static final String EMPTY_SHA256 = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
|
|
554
|
+
|
|
555
|
+
static boolean isSafeCacheHash(final String hash) {
|
|
556
|
+
if (hash == null) {
|
|
557
|
+
return false;
|
|
558
|
+
}
|
|
559
|
+
final int len = hash.length();
|
|
560
|
+
if (len != 64 && len != 8) {
|
|
561
|
+
return false;
|
|
562
|
+
}
|
|
563
|
+
for (int i = 0; i < len; i++) {
|
|
564
|
+
final char c = hash.charAt(i);
|
|
565
|
+
if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'))) {
|
|
566
|
+
return false;
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
return true;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// SHA-256 hash-named cache files were verified when written. Existence is
|
|
573
|
+
// enough for non-empty files; empty files are reused only for the empty SHA-256.
|
|
574
|
+
// CRC32 (8 hex) is too collision-prone to trust without a re-read.
|
|
575
|
+
static boolean isReusableCacheFile(final File file, final String expectedHash) {
|
|
576
|
+
if (file == null || !file.isFile() || !isSafeCacheHash(expectedHash) || expectedHash.length() != 64) {
|
|
577
|
+
return false;
|
|
578
|
+
}
|
|
579
|
+
final long length = file.length();
|
|
580
|
+
if (length > 0) {
|
|
581
|
+
return true;
|
|
582
|
+
}
|
|
583
|
+
return length == 0 && EMPTY_SHA256.equalsIgnoreCase(expectedHash);
|
|
584
|
+
}
|
|
585
|
+
|
|
540
586
|
public JSONArray getMissingBundleFiles(final JSONArray manifest, final String sessionKey) throws JSONException {
|
|
541
587
|
final JSONArray missing = new JSONArray();
|
|
542
588
|
for (int i = 0; i < manifest.length(); i++) {
|
|
@@ -1171,19 +1217,22 @@ public class CapgoUpdater {
|
|
|
1171
1217
|
throw new IOException("Failed to create parent directory: " + parent.getAbsolutePath());
|
|
1172
1218
|
}
|
|
1173
1219
|
|
|
1174
|
-
final File tempFile =
|
|
1175
|
-
try
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1220
|
+
final File tempFile = File.createTempFile("capgo-", ".tmp", parent);
|
|
1221
|
+
try {
|
|
1222
|
+
try (
|
|
1223
|
+
final FileInputStream input = new FileInputStream(source);
|
|
1224
|
+
final FileOutputStream output = new FileOutputStream(tempFile)
|
|
1225
|
+
) {
|
|
1226
|
+
final byte[] buffer = new byte[CryptoCipher.copyBufferBytes()];
|
|
1227
|
+
int length;
|
|
1228
|
+
while ((length = input.read(buffer)) != -1) {
|
|
1229
|
+
output.write(buffer, 0, length);
|
|
1230
|
+
}
|
|
1180
1231
|
}
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
if (!dest.delete() || !tempFile.renameTo(dest)) {
|
|
1232
|
+
CryptoCipher.replaceFile(tempFile, dest);
|
|
1233
|
+
} finally {
|
|
1234
|
+
if (tempFile.exists()) {
|
|
1185
1235
|
tempFile.delete();
|
|
1186
|
-
throw new IOException("Failed to replace file: " + dest.getAbsolutePath());
|
|
1187
1236
|
}
|
|
1188
1237
|
}
|
|
1189
1238
|
}
|
|
@@ -12,12 +12,13 @@ package ee.forgr.capacitor_updater;
|
|
|
12
12
|
* references: http://stackoverflow.com/questions/12471999/rsa-encryption-decryption-in-android
|
|
13
13
|
*/
|
|
14
14
|
import android.util.Base64;
|
|
15
|
-
import java.io.
|
|
16
|
-
import java.io.DataInputStream;
|
|
15
|
+
import java.io.BufferedReader;
|
|
17
16
|
import java.io.File;
|
|
18
17
|
import java.io.FileInputStream;
|
|
19
18
|
import java.io.FileOutputStream;
|
|
19
|
+
import java.io.FileReader;
|
|
20
20
|
import java.io.IOException;
|
|
21
|
+
import java.io.InputStream;
|
|
21
22
|
import java.security.GeneralSecurityException;
|
|
22
23
|
import java.security.InvalidAlgorithmParameterException;
|
|
23
24
|
import java.security.InvalidKeyException;
|
|
@@ -157,26 +158,52 @@ public class CryptoCipher {
|
|
|
157
158
|
byte[] decryptedSessionKey = CryptoCipher.decryptRSA(sessionKey, pKey);
|
|
158
159
|
|
|
159
160
|
SecretKey sKey = CryptoCipher.byteToSessionKey(decryptedSessionKey);
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
try (
|
|
163
|
-
final FileInputStream fis = new FileInputStream(file);
|
|
164
|
-
final BufferedInputStream bis = new BufferedInputStream(fis);
|
|
165
|
-
final DataInputStream dis = new DataInputStream(bis)
|
|
166
|
-
) {
|
|
167
|
-
dis.readFully(content);
|
|
168
|
-
dis.close();
|
|
169
|
-
byte[] decrypted = CryptoCipher.decryptAES(content, sKey, iv);
|
|
170
|
-
// write the decrypted string to the file
|
|
171
|
-
try (final FileOutputStream fos = new FileOutputStream(file.getAbsolutePath())) {
|
|
172
|
-
fos.write(decrypted);
|
|
173
|
-
}
|
|
174
|
-
}
|
|
161
|
+
decryptAesFile(file, sKey, iv);
|
|
175
162
|
} catch (GeneralSecurityException e) {
|
|
176
163
|
logger.info("decryptFile fail");
|
|
177
|
-
e
|
|
178
|
-
|
|
164
|
+
throw new IOException("GeneralSecurityException", e);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
static void decryptAesFile(File file, SecretKey key, byte[] iv) throws IOException, GeneralSecurityException {
|
|
169
|
+
if (file.length() == 0) {
|
|
170
|
+
throw new IOException("Empty encrypted data");
|
|
171
|
+
}
|
|
172
|
+
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
|
|
173
|
+
cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(key.getEncoded(), "AES"), new IvParameterSpec(iv));
|
|
174
|
+
File tempFile = File.createTempFile("capgo-aes-", ".tmp", file.getParentFile());
|
|
175
|
+
try {
|
|
176
|
+
byte[] inBuf = new byte[ioBufferBytes()];
|
|
177
|
+
// Reuse one output buffer. cipher.update(in) allocates a new byte[] per chunk
|
|
178
|
+
// and 64 workers * 5MB was why AES barely won on heap in local benches.
|
|
179
|
+
byte[] outBuf = new byte[inBuf.length + 16];
|
|
180
|
+
try (FileInputStream fis = new FileInputStream(file); FileOutputStream fos = new FileOutputStream(tempFile)) {
|
|
181
|
+
int n;
|
|
182
|
+
while ((n = fis.read(inBuf)) != -1) {
|
|
183
|
+
int outLen = cipher.update(inBuf, 0, n, outBuf, 0);
|
|
184
|
+
if (outLen > 0) {
|
|
185
|
+
fos.write(outBuf, 0, outLen);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
int last = cipher.doFinal(outBuf, 0);
|
|
189
|
+
if (last > 0) {
|
|
190
|
+
fos.write(outBuf, 0, last);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
replaceFile(tempFile, file);
|
|
194
|
+
tempFile = null;
|
|
195
|
+
} finally {
|
|
196
|
+
if (tempFile != null && tempFile.exists()) {
|
|
197
|
+
tempFile.delete();
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
static void replaceFile(File from, File to) throws IOException {
|
|
203
|
+
if (from.renameTo(to)) {
|
|
204
|
+
return;
|
|
179
205
|
}
|
|
206
|
+
throw new IOException("Failed to replace file: " + to.getAbsolutePath());
|
|
180
207
|
}
|
|
181
208
|
|
|
182
209
|
private static byte[] hexStringToByteArray(String s) {
|
|
@@ -303,8 +330,87 @@ public class CryptoCipher {
|
|
|
303
330
|
}
|
|
304
331
|
}
|
|
305
332
|
|
|
333
|
+
private static final long TWO_GIB = 2L * 1024 * 1024 * 1024;
|
|
334
|
+
private static final long THREE_GIB = 3L * 1024 * 1024 * 1024;
|
|
335
|
+
private static final long FOUR_GIB = 4L * 1024 * 1024 * 1024;
|
|
336
|
+
private static final long EIGHT_GIB = 8L * 1024 * 1024 * 1024;
|
|
337
|
+
private static final int FLAGSHIP_IO_BUFFER_BYTES = 5 * 1024 * 1024;
|
|
338
|
+
private static volatile long cachedPhysicalRamBytes = -1;
|
|
339
|
+
|
|
340
|
+
// Checksum and copy share one ladder. 64-wide peak RAM = 64 * buffer.
|
|
341
|
+
// <2GB: 64KB. <3GB: 256KB. <4GB: 512KB. <8GB: 1MB. Else 5MB (flagship / unknown).
|
|
342
|
+
static int ioBufferBytes(long physicalRamBytes) {
|
|
343
|
+
if (physicalRamBytes <= 0) {
|
|
344
|
+
return FLAGSHIP_IO_BUFFER_BYTES;
|
|
345
|
+
}
|
|
346
|
+
if (physicalRamBytes < TWO_GIB) {
|
|
347
|
+
return 64 * 1024;
|
|
348
|
+
}
|
|
349
|
+
if (physicalRamBytes < THREE_GIB) {
|
|
350
|
+
return 256 * 1024;
|
|
351
|
+
}
|
|
352
|
+
if (physicalRamBytes < FOUR_GIB) {
|
|
353
|
+
return 512 * 1024;
|
|
354
|
+
}
|
|
355
|
+
if (physicalRamBytes < EIGHT_GIB) {
|
|
356
|
+
return 1024 * 1024;
|
|
357
|
+
}
|
|
358
|
+
return FLAGSHIP_IO_BUFFER_BYTES;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
static int checksumBufferBytes(long physicalRamBytes) {
|
|
362
|
+
return ioBufferBytes(physicalRamBytes);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
static int copyBufferBytes(long physicalRamBytes) {
|
|
366
|
+
return ioBufferBytes(physicalRamBytes);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
static int ioBufferBytes() {
|
|
370
|
+
return ioBufferBytes(physicalRamBytes());
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
static int checksumBufferBytes() {
|
|
374
|
+
return ioBufferBytes(physicalRamBytes());
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
static int copyBufferBytes() {
|
|
378
|
+
return ioBufferBytes(physicalRamBytes());
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
static long physicalRamBytes() {
|
|
382
|
+
long cached = cachedPhysicalRamBytes;
|
|
383
|
+
if (cached >= 0) {
|
|
384
|
+
return cached;
|
|
385
|
+
}
|
|
386
|
+
long parsed = 0;
|
|
387
|
+
try (BufferedReader reader = new BufferedReader(new FileReader("/proc/meminfo"))) {
|
|
388
|
+
String line = reader.readLine();
|
|
389
|
+
if (line != null && line.startsWith("MemTotal:")) {
|
|
390
|
+
String[] parts = line.split("\\s+");
|
|
391
|
+
if (parts.length >= 2) {
|
|
392
|
+
parsed = Long.parseLong(parts[1]) * 1024L;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
} catch (Exception ignored) {
|
|
396
|
+
parsed = 0;
|
|
397
|
+
}
|
|
398
|
+
cachedPhysicalRamBytes = parsed;
|
|
399
|
+
return parsed;
|
|
400
|
+
}
|
|
401
|
+
|
|
306
402
|
public static String calcChecksum(File file) {
|
|
307
|
-
|
|
403
|
+
try (FileInputStream fis = new FileInputStream(file)) {
|
|
404
|
+
return calcChecksum(fis);
|
|
405
|
+
} catch (IOException e) {
|
|
406
|
+
logger.error("Cannot calculate checksum");
|
|
407
|
+
logger.debug("Path: " + file.getPath() + ", Error: " + e.getMessage());
|
|
408
|
+
return "";
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
public static String calcChecksum(InputStream inputStream) {
|
|
413
|
+
final int BUFFER_SIZE = checksumBufferBytes();
|
|
308
414
|
MessageDigest digest;
|
|
309
415
|
try {
|
|
310
416
|
digest = MessageDigest.getInstance("SHA-256");
|
|
@@ -313,10 +419,10 @@ public class CryptoCipher {
|
|
|
313
419
|
return "";
|
|
314
420
|
}
|
|
315
421
|
|
|
316
|
-
try
|
|
422
|
+
try {
|
|
317
423
|
byte[] buffer = new byte[BUFFER_SIZE];
|
|
318
424
|
int length;
|
|
319
|
-
while ((length =
|
|
425
|
+
while ((length = inputStream.read(buffer)) != -1) {
|
|
320
426
|
digest.update(buffer, 0, length);
|
|
321
427
|
}
|
|
322
428
|
byte[] hash = digest.digest();
|
|
@@ -329,7 +435,7 @@ public class CryptoCipher {
|
|
|
329
435
|
return hexString.toString();
|
|
330
436
|
} catch (IOException e) {
|
|
331
437
|
logger.error("Cannot calculate checksum");
|
|
332
|
-
logger.debug("
|
|
438
|
+
logger.debug("Error: " + e.getMessage());
|
|
333
439
|
return "";
|
|
334
440
|
}
|
|
335
441
|
}
|