@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.
- package/CapgoCapacitorUpdater.podspec +1 -1
- package/Package.swift +3 -2
- package/README.md +53 -48
- package/android/build.gradle +1 -0
- package/android/src/main/java/ee/forgr/capacitor_updater/AppLifecycleObserver.java +29 -2
- package/android/src/main/java/ee/forgr/capacitor_updater/BundleInfo.java +7 -3
- package/android/src/main/java/ee/forgr/capacitor_updater/BundleStatus.java +1 -0
- package/android/src/main/java/ee/forgr/capacitor_updater/CapacitorUpdaterPlugin.java +452 -139
- package/android/src/main/java/ee/forgr/capacitor_updater/CapgoUpdater.java +1354 -412
- package/android/src/main/java/ee/forgr/capacitor_updater/CryptoCipher.java +102 -31
- package/android/src/main/java/ee/forgr/capacitor_updater/DataManager.java +23 -7
- package/android/src/main/java/ee/forgr/capacitor_updater/DelayCondition.java +2 -2
- package/android/src/main/java/ee/forgr/capacitor_updater/DownloadService.java +540 -224
- package/android/src/main/java/ee/forgr/capacitor_updater/DownloadWorkerManager.java +103 -3
- package/android/src/main/java/ee/forgr/capacitor_updater/InternalUtils.java +1 -1
- package/android/src/main/java/ee/forgr/capacitor_updater/ShakeMenu.java +131 -145
- package/dist/docs.json +32 -8
- package/dist/esm/definitions.d.ts +41 -17
- package/dist/esm/definitions.js.map +1 -1
- package/ios/Sources/CapacitorUpdaterPlugin/AES.swift +124 -0
- package/ios/Sources/CapacitorUpdaterPlugin/BundleInfo.swift +9 -1
- package/ios/Sources/CapacitorUpdaterPlugin/BundleStatus.swift +3 -0
- package/ios/Sources/CapacitorUpdaterPlugin/CapacitorUpdaterPlugin.swift +787 -92
- package/ios/Sources/CapacitorUpdaterPlugin/CapgoUpdater.swift +1014 -268
- package/ios/Sources/CapacitorUpdaterPlugin/CryptoCipher.swift +49 -31
- package/ios/Sources/CapacitorUpdaterPlugin/ShakeMenu.swift +44 -20
- package/ios/Sources/CapacitorUpdaterPlugin/WebViewStatsReporter.swift +28 -0
- 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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
373
|
+
try {
|
|
317
374
|
byte[] buffer = new byte[BUFFER_SIZE];
|
|
318
375
|
int length;
|
|
319
|
-
while ((length =
|
|
376
|
+
while ((length = inputStream.read(buffer)) != -1) {
|
|
320
377
|
digest.update(buffer, 0, length);
|
|
321
378
|
}
|
|
322
|
-
|
|
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("
|
|
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
|
|
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
|
-
|
|
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
|
|
24
|
-
|
|
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
|
|
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
|
|
54
|
+
return "DelayCondition{" + "kind=" + kind + ", value='" + value + '\'' + '}';
|
|
55
55
|
}
|
|
56
56
|
}
|