@capgo/capacitor-updater 5.50.2 → 5.51.15

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.
Files changed (28) hide show
  1. package/CapgoCapacitorUpdater.podspec +1 -1
  2. package/Package.swift +3 -2
  3. package/README.md +53 -48
  4. package/android/build.gradle +1 -0
  5. package/android/src/main/java/ee/forgr/capacitor_updater/AppLifecycleObserver.java +29 -2
  6. package/android/src/main/java/ee/forgr/capacitor_updater/BundleInfo.java +7 -3
  7. package/android/src/main/java/ee/forgr/capacitor_updater/BundleStatus.java +1 -0
  8. package/android/src/main/java/ee/forgr/capacitor_updater/CapacitorUpdaterPlugin.java +452 -139
  9. package/android/src/main/java/ee/forgr/capacitor_updater/CapgoUpdater.java +1354 -412
  10. package/android/src/main/java/ee/forgr/capacitor_updater/CryptoCipher.java +102 -31
  11. package/android/src/main/java/ee/forgr/capacitor_updater/DataManager.java +23 -7
  12. package/android/src/main/java/ee/forgr/capacitor_updater/DelayCondition.java +2 -2
  13. package/android/src/main/java/ee/forgr/capacitor_updater/DownloadService.java +540 -224
  14. package/android/src/main/java/ee/forgr/capacitor_updater/DownloadWorkerManager.java +103 -3
  15. package/android/src/main/java/ee/forgr/capacitor_updater/InternalUtils.java +1 -1
  16. package/android/src/main/java/ee/forgr/capacitor_updater/ShakeMenu.java +131 -145
  17. package/dist/docs.json +32 -8
  18. package/dist/esm/definitions.d.ts +41 -17
  19. package/dist/esm/definitions.js.map +1 -1
  20. package/ios/Sources/CapacitorUpdaterPlugin/AES.swift +124 -0
  21. package/ios/Sources/CapacitorUpdaterPlugin/BundleInfo.swift +9 -1
  22. package/ios/Sources/CapacitorUpdaterPlugin/BundleStatus.swift +3 -0
  23. package/ios/Sources/CapacitorUpdaterPlugin/CapacitorUpdaterPlugin.swift +787 -92
  24. package/ios/Sources/CapacitorUpdaterPlugin/CapgoUpdater.swift +1014 -268
  25. package/ios/Sources/CapacitorUpdaterPlugin/CryptoCipher.swift +49 -31
  26. package/ios/Sources/CapacitorUpdaterPlugin/ShakeMenu.swift +44 -20
  27. package/ios/Sources/CapacitorUpdaterPlugin/WebViewStatsReporter.swift +28 -0
  28. package/package.json +12 -7
@@ -12,12 +12,11 @@ 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.BufferedInputStream;
16
- import java.io.DataInputStream;
17
15
  import java.io.File;
18
16
  import java.io.FileInputStream;
19
17
  import java.io.FileOutputStream;
20
18
  import java.io.IOException;
19
+ import java.io.InputStream;
21
20
  import java.security.GeneralSecurityException;
22
21
  import java.security.InvalidAlgorithmParameterException;
23
22
  import java.security.InvalidKeyException;
@@ -157,28 +156,60 @@ public class CryptoCipher {
157
156
  byte[] decryptedSessionKey = CryptoCipher.decryptRSA(sessionKey, pKey);
158
157
 
159
158
  SecretKey sKey = CryptoCipher.byteToSessionKey(decryptedSessionKey);
160
- byte[] content = new byte[(int) file.length()];
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
- }
159
+ decryptAesFile(file, sKey, iv);
175
160
  } catch (GeneralSecurityException e) {
176
161
  logger.info("decryptFile fail");
177
- e.printStackTrace();
178
- throw new IOException("GeneralSecurityException");
162
+ throw new IOException("GeneralSecurityException", e);
179
163
  }
180
164
  }
181
165
 
166
+ static void decryptAesFile(File file, SecretKey key, byte[] iv) throws IOException, GeneralSecurityException {
167
+ if (file.length() == 0) {
168
+ throw new IOException("Empty encrypted data");
169
+ }
170
+ Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
171
+ cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(key.getEncoded(), "AES"), new IvParameterSpec(iv));
172
+ File parent = file.getAbsoluteFile().getParentFile();
173
+ if (parent == null) {
174
+ throw new IOException("Cannot create temp file for " + file.getAbsolutePath());
175
+ }
176
+ File tempFile = File.createTempFile("capgo-aes-", ".tmp", parent);
177
+ try {
178
+ byte[] inBuf = new byte[ioBufferBytes()];
179
+ // Reuse one output buffer. cipher.update(in) allocates a new byte[] per chunk.
180
+ byte[] outBuf = new byte[inBuf.length + 16];
181
+ try (FileInputStream fis = new FileInputStream(file); FileOutputStream fos = new FileOutputStream(tempFile)) {
182
+ int n;
183
+ while ((n = fis.read(inBuf)) != -1) {
184
+ int outLen = cipher.update(inBuf, 0, n, outBuf, 0);
185
+ if (outLen > 0) {
186
+ fos.write(outBuf, 0, outLen);
187
+ }
188
+ }
189
+ int last = cipher.doFinal(outBuf, 0);
190
+ if (last > 0) {
191
+ fos.write(outBuf, 0, last);
192
+ }
193
+ }
194
+ if (tempFile.length() == 0) {
195
+ throw new IOException("Empty decrypted data");
196
+ }
197
+ replaceFile(tempFile, file);
198
+ tempFile = null;
199
+ } finally {
200
+ if (tempFile != null && tempFile.exists()) {
201
+ tempFile.delete();
202
+ }
203
+ }
204
+ }
205
+
206
+ static void replaceFile(File from, File to) throws IOException {
207
+ if (from.renameTo(to)) {
208
+ return;
209
+ }
210
+ throw new IOException("Failed to replace file: " + to.getAbsolutePath());
211
+ }
212
+
182
213
  private static byte[] hexStringToByteArray(String s) {
183
214
  int len = s.length();
184
215
  byte[] data = new byte[len / 2];
@@ -303,8 +334,34 @@ public class CryptoCipher {
303
334
  }
304
335
  }
305
336
 
337
+ // 256 KiB: one size for checksum, copy, and decode.
338
+ // 64 workers * 256 KiB = 16 MiB for one buffer; AES/Brotli hold two (~32 MiB).
339
+ static final int IO_BUFFER_BYTES = 256 * 1024;
340
+
341
+ static int ioBufferBytes() {
342
+ return IO_BUFFER_BYTES;
343
+ }
344
+
345
+ static int checksumBufferBytes() {
346
+ return IO_BUFFER_BYTES;
347
+ }
348
+
349
+ static int copyBufferBytes() {
350
+ return IO_BUFFER_BYTES;
351
+ }
352
+
306
353
  public static String calcChecksum(File file) {
307
- final int BUFFER_SIZE = 1024 * 1024 * 5; // 5 MB buffer size
354
+ try (FileInputStream fis = new FileInputStream(file)) {
355
+ return calcChecksum(fis);
356
+ } catch (IOException e) {
357
+ logger.error("Cannot calculate checksum");
358
+ logger.debug("Path: " + file.getPath() + ", Error: " + e.getMessage());
359
+ return "";
360
+ }
361
+ }
362
+
363
+ public static String calcChecksum(InputStream inputStream) {
364
+ final int BUFFER_SIZE = checksumBufferBytes();
308
365
  MessageDigest digest;
309
366
  try {
310
367
  digest = MessageDigest.getInstance("SHA-256");
@@ -313,27 +370,41 @@ public class CryptoCipher {
313
370
  return "";
314
371
  }
315
372
 
316
- try (FileInputStream fis = new FileInputStream(file)) {
373
+ try {
317
374
  byte[] buffer = new byte[BUFFER_SIZE];
318
375
  int length;
319
- while ((length = fis.read(buffer)) != -1) {
376
+ while ((length = inputStream.read(buffer)) != -1) {
320
377
  digest.update(buffer, 0, length);
321
378
  }
322
- byte[] hash = digest.digest();
323
- StringBuilder hexString = new StringBuilder();
324
- for (byte b : hash) {
325
- String hex = Integer.toHexString(0xff & b);
326
- if (hex.length() == 1) hexString.append('0');
327
- hexString.append(hex);
328
- }
329
- return hexString.toString();
379
+ return digestToHex(digest);
330
380
  } catch (IOException e) {
331
381
  logger.error("Cannot calculate checksum");
332
- logger.debug("Path: " + file.getPath() + ", Error: " + e.getMessage());
382
+ logger.debug("Error: " + e.getMessage());
333
383
  return "";
334
384
  }
335
385
  }
336
386
 
387
+ static String digestToHex(MessageDigest digest) {
388
+ byte[] hash = digest.digest();
389
+ StringBuilder hexString = new StringBuilder(hash.length * 2);
390
+ for (byte b : hash) {
391
+ String hex = Integer.toHexString(0xff & b);
392
+ if (hex.length() == 1) hexString.append('0');
393
+ hexString.append(hex);
394
+ }
395
+ return hexString.toString();
396
+ }
397
+
398
+ static String shortPathKey(String fileName) {
399
+ try {
400
+ MessageDigest digest = MessageDigest.getInstance("SHA-256");
401
+ digest.update((fileName == null ? "" : fileName).getBytes(java.nio.charset.StandardCharsets.UTF_8));
402
+ return digestToHex(digest).substring(0, 16);
403
+ } catch (java.security.NoSuchAlgorithmException e) {
404
+ return Integer.toHexString((fileName == null ? "" : fileName).hashCode());
405
+ }
406
+ }
407
+
337
408
  private static byte[] createDEREncoding(int tag, byte[] value) {
338
409
  if (tag < 0 || tag >= 0xFF) {
339
410
  throw new IllegalArgumentException("Currently only single byte tags supported");
@@ -1,11 +1,13 @@
1
1
  package ee.forgr.capacitor_updater;
2
2
 
3
+ import java.util.HashMap;
4
+ import java.util.Map;
3
5
  import org.json.JSONArray;
4
6
 
5
7
  public class DataManager {
6
8
 
7
9
  private static DataManager instance;
8
- private JSONArray currentManifest;
10
+ private final Map<String, JSONArray> manifestsById = new HashMap<>();
9
11
 
10
12
  private DataManager() {}
11
13
 
@@ -16,13 +18,27 @@ public class DataManager {
16
18
  return instance;
17
19
  }
18
20
 
19
- public void setManifest(JSONArray manifest) {
20
- this.currentManifest = manifest;
21
+ public synchronized void setManifest(String downloadId, JSONArray manifest) {
22
+ if (downloadId == null || manifest == null) {
23
+ return;
24
+ }
25
+ this.manifestsById.put(downloadId, manifest);
26
+ }
27
+
28
+ public synchronized JSONArray getAndClearManifest(String downloadId) {
29
+ if (downloadId == null) {
30
+ return null;
31
+ }
32
+ return this.manifestsById.remove(downloadId);
33
+ }
34
+
35
+ public synchronized void clearManifest(String downloadId) {
36
+ if (downloadId != null) {
37
+ this.manifestsById.remove(downloadId);
38
+ }
21
39
  }
22
40
 
23
- public JSONArray getAndClearManifest() {
24
- JSONArray manifest = this.currentManifest;
25
- this.currentManifest = null;
26
- return manifest;
41
+ public synchronized void clearAllManifests() {
42
+ this.manifestsById.clear();
27
43
  }
28
44
  }
@@ -40,7 +40,7 @@ public class DelayCondition {
40
40
  public boolean equals(Object o) {
41
41
  if (this == o) return true;
42
42
  if (!(o instanceof DelayCondition that)) return false;
43
- return (getKind() == that.getKind() && Objects.equals(getValue(), that.getValue()));
43
+ return getKind() == that.getKind() && Objects.equals(getValue(), that.getValue());
44
44
  }
45
45
 
46
46
  @Override
@@ -51,6 +51,6 @@ public class DelayCondition {
51
51
  @NonNull
52
52
  @Override
53
53
  public String toString() {
54
- return ("DelayCondition{" + "kind=" + kind + ", value='" + value + '\'' + '}');
54
+ return "DelayCondition{" + "kind=" + kind + ", value='" + value + '\'' + '}';
55
55
  }
56
56
  }