@capawesome/capacitor-live-update 6.0.7 → 6.1.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 +14 -5
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/LiveUpdate.java +115 -10
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/LiveUpdateConfig.java +14 -0
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/LiveUpdatePlugin.java +15 -7
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/classes/api/GetLatestBundleResponse.java +29 -0
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/classes/options/DownloadBundleOptions.java +11 -1
- package/dist/docs.json +39 -7
- package/dist/esm/definitions.d.ts +28 -7
- package/dist/esm/definitions.js.map +1 -1
- package/ios/Plugin/Classes/CustomError.swift +12 -0
- package/ios/Plugin/Classes/Options/DownloadBundleOptions.swift +7 -1
- package/ios/Plugin/LiveUpdate.swift +130 -23
- package/ios/Plugin/LiveUpdateConfig.swift +1 -0
- package/ios/Plugin/LiveUpdatePlugin.swift +4 -2
- package/ios/Plugin/Protocols/Api/GetLatestBundleResponse.swift +2 -0
- package/package.json +9 -2
package/README.md
CHANGED
|
@@ -12,6 +12,8 @@ Capacitor plugin to update your app remotely in real-time.
|
|
|
12
12
|
- 🔄 **Auto Update**: Automatically download and set the latest bundle for the app.
|
|
13
13
|
- 🛟 **Rollback**: Reset the app to the default bundle if an incompatible bundle has been set.
|
|
14
14
|
- 🚀 **Rollout**: Gradually roll out new bundles to gather valuable feedback.
|
|
15
|
+
- 🔒 **Security**: Verify the authenticity and integrity of the bundle using a public key.
|
|
16
|
+
- 🌐 **Open Source**: Licensed under the MIT License.
|
|
15
17
|
|
|
16
18
|
## Installation
|
|
17
19
|
|
|
@@ -68,6 +70,7 @@ We recommend to declare [`CA92.1`](https://developer.apple.com/documentation/bun
|
|
|
68
70
|
| **`appId`** | <code>string</code> | The app ID is used to identify the app when using [Capawesome Cloud](https://capawesome.io/cloud). | | 5.0.0 |
|
|
69
71
|
| **`autoDeleteBundles`** | <code>boolean</code> | Whether or not to automatically delete unused bundles. When enabled, the plugin will automatically delete unused bundles after calling `ready()`. | <code>false</code> | 5.0.0 |
|
|
70
72
|
| **`enabled`** | <code>boolean</code> | Whether or not the plugin is enabled. | <code>true</code> | 5.0.0 |
|
|
73
|
+
| **`publicKey`** | <code>string</code> | The public key to verify the integrity of the bundle. The public key must be a PEM-encoded RSA public key. | | 6.1.0 |
|
|
71
74
|
| **`readyTimeout`** | <code>number</code> | The timeout in milliseconds to wait for the app to be ready before resetting to the default bundle. | <code>10000</code> | 5.0.0 |
|
|
72
75
|
| **`resetOnUpdate`** | <code>boolean</code> | Whether or not the app should be reset to the default bundle during an update. | <code>true</code> | 5.0.0 |
|
|
73
76
|
|
|
@@ -82,6 +85,7 @@ In `capacitor.config.json`:
|
|
|
82
85
|
"appId": '4100e356-e851-47fe-9d3c-b411eb325a99',
|
|
83
86
|
"autoDeleteBundles": undefined,
|
|
84
87
|
"enabled": undefined,
|
|
88
|
+
"publicKey": '-----BEGIN PUBLIC KEY-----MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDDodf1SD0OOn6hIlDuKBza0Ed0OqtwyVJwiyjmE9BJaZ7y8ZUfcF+SKmd0l2cDPM45XIg2tAFux5n29uoKyHwSt+6tCi5CJA5Z1/1eZruRRqABLonV77KS3HUtvOgqRLDnKSV89dYZkM++NwmzOPgIF422mvc+VukcVOBfc8/AHQIDAQAB-----END PUBLIC KEY-----',
|
|
85
89
|
"readyTimeout": undefined,
|
|
86
90
|
"resetOnUpdate": undefined
|
|
87
91
|
}
|
|
@@ -102,6 +106,7 @@ const config: CapacitorConfig = {
|
|
|
102
106
|
appId: '4100e356-e851-47fe-9d3c-b411eb325a99',
|
|
103
107
|
autoDeleteBundles: undefined,
|
|
104
108
|
enabled: undefined,
|
|
109
|
+
publicKey: '-----BEGIN PUBLIC KEY-----MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDDodf1SD0OOn6hIlDuKBza0Ed0OqtwyVJwiyjmE9BJaZ7y8ZUfcF+SKmd0l2cDPM45XIg2tAFux5n29uoKyHwSt+6tCi5CJA5Z1/1eZruRRqABLonV77KS3HUtvOgqRLDnKSV89dYZkM++NwmzOPgIF422mvc+VukcVOBfc8/AHQIDAQAB-----END PUBLIC KEY-----',
|
|
105
110
|
readyTimeout: undefined,
|
|
106
111
|
resetOnUpdate: undefined,
|
|
107
112
|
},
|
|
@@ -398,6 +403,9 @@ ready() => Promise<void>
|
|
|
398
403
|
|
|
399
404
|
Notify the plugin that the app is ready to use and no rollback is needed.
|
|
400
405
|
|
|
406
|
+
**Attention**: This method should be called as soon as the app is ready to use
|
|
407
|
+
to prevent the app from being reset to the default bundle.
|
|
408
|
+
|
|
401
409
|
Only available on Android and iOS.
|
|
402
410
|
|
|
403
411
|
**Since:** 5.0.0
|
|
@@ -527,10 +535,11 @@ Only available on Android and iOS.
|
|
|
527
535
|
|
|
528
536
|
#### DownloadBundleOptions
|
|
529
537
|
|
|
530
|
-
| Prop | Type | Description
|
|
531
|
-
| -------------- | ------------------- |
|
|
532
|
-
| **`
|
|
533
|
-
| **`
|
|
538
|
+
| Prop | Type | Description | Since |
|
|
539
|
+
| -------------- | ------------------- | ----------------------------------------------------------------------------------------------------------------- | ----- |
|
|
540
|
+
| **`bundleId`** | <code>string</code> | The unique identifier of the bundle. | 5.0.0 |
|
|
541
|
+
| **`checksum`** | <code>string</code> | The checksum of the bundle to verify the integrity of the ZIP file. Must be a SHA-256 hash in hexadecimal format. | 6.1.0 |
|
|
542
|
+
| **`url`** | <code>string</code> | The URL of the bundle to download. The bundle must be a ZIP file containing at least a `index.html` file. | 5.0.0 |
|
|
534
543
|
|
|
535
544
|
|
|
536
545
|
#### GetBundleResult
|
|
@@ -627,7 +636,7 @@ If you want to disable the plugin to test other parts of your app, you can set t
|
|
|
627
636
|
|
|
628
637
|
Live updates are only supported for [binary-compatible changes](https://capawesome.io/cloud/live-updates/faq/#what-are-binary-compatible-changes) (e.g. HTML, CSS, JavaScript).
|
|
629
638
|
If you change native code, such as adding a new plugin, you need to resubmit your app to the app stores.
|
|
630
|
-
For this reason, you must be careful to
|
|
639
|
+
For this reason, you must be careful to [restrict live updates to compatible native versions](https://capawesome.io/blog/how-to-restrict-capacitor-live-updates-to-native-versions/) of your app.
|
|
631
640
|
|
|
632
641
|
## Changelog
|
|
633
642
|
|
|
@@ -6,6 +6,7 @@ import android.content.pm.PackageInfo;
|
|
|
6
6
|
import android.content.pm.PackageManager;
|
|
7
7
|
import android.os.Build;
|
|
8
8
|
import android.os.Handler;
|
|
9
|
+
import android.util.Base64;
|
|
9
10
|
import androidx.annotation.NonNull;
|
|
10
11
|
import androidx.annotation.Nullable;
|
|
11
12
|
import com.getcapacitor.Logger;
|
|
@@ -29,6 +30,11 @@ import io.capawesome.capacitorjs.plugins.liveupdate.interfaces.NonEmptyCallback;
|
|
|
29
30
|
import io.capawesome.capacitorjs.plugins.liveupdate.interfaces.Result;
|
|
30
31
|
import java.io.File;
|
|
31
32
|
import java.io.IOException;
|
|
33
|
+
import java.security.KeyFactory;
|
|
34
|
+
import java.security.MessageDigest;
|
|
35
|
+
import java.security.PublicKey;
|
|
36
|
+
import java.security.Signature;
|
|
37
|
+
import java.security.spec.X509EncodedKeySpec;
|
|
32
38
|
import java.util.Arrays;
|
|
33
39
|
import java.util.UUID;
|
|
34
40
|
import net.lingala.zip4j.ZipFile;
|
|
@@ -97,6 +103,7 @@ public class LiveUpdate {
|
|
|
97
103
|
|
|
98
104
|
public void downloadBundle(@NonNull DownloadBundleOptions options, @NonNull EmptyCallback callback) throws Exception {
|
|
99
105
|
String bundleId = options.getBundleId();
|
|
106
|
+
String checksum = options.getChecksum();
|
|
100
107
|
String url = options.getUrl();
|
|
101
108
|
|
|
102
109
|
// Check if the bundle already exists
|
|
@@ -107,7 +114,7 @@ public class LiveUpdate {
|
|
|
107
114
|
}
|
|
108
115
|
|
|
109
116
|
// Download the bundle
|
|
110
|
-
downloadBundle(bundleId, url, callback);
|
|
117
|
+
downloadBundle(bundleId, checksum, null, url, callback);
|
|
111
118
|
}
|
|
112
119
|
|
|
113
120
|
public void getBundle(@NonNull NonEmptyCallback callback) {
|
|
@@ -164,9 +171,7 @@ public class LiveUpdate {
|
|
|
164
171
|
}
|
|
165
172
|
|
|
166
173
|
public void reload() {
|
|
167
|
-
String
|
|
168
|
-
String nextPath = getNextCapacitorServerPath();
|
|
169
|
-
String path = nextPath == null ? currentPath : nextPath;
|
|
174
|
+
String path = getNextCapacitorServerPath();
|
|
170
175
|
setCurrentCapacitorServerPath(path);
|
|
171
176
|
startRollbackTimer();
|
|
172
177
|
}
|
|
@@ -217,6 +222,8 @@ public class LiveUpdate {
|
|
|
217
222
|
@Override
|
|
218
223
|
public void success(@NonNull GetLatestBundleResponse result) {
|
|
219
224
|
String latestBundleId = result.getBundleId();
|
|
225
|
+
String checksum = result.getChecksum();
|
|
226
|
+
String signature = result.getSignature();
|
|
220
227
|
String url = result.getUrl();
|
|
221
228
|
|
|
222
229
|
// Check if the bundle already exists
|
|
@@ -235,6 +242,8 @@ public class LiveUpdate {
|
|
|
235
242
|
// Download and unzip the bundle
|
|
236
243
|
downloadBundle(
|
|
237
244
|
latestBundleId,
|
|
245
|
+
checksum,
|
|
246
|
+
signature,
|
|
238
247
|
url,
|
|
239
248
|
new EmptyCallback() {
|
|
240
249
|
@Override
|
|
@@ -292,6 +301,31 @@ public class LiveUpdate {
|
|
|
292
301
|
return new File(plugin.getContext().getFilesDir(), bundlesDirectory + "/" + bundle);
|
|
293
302
|
}
|
|
294
303
|
|
|
304
|
+
private byte[] getChecksumForFileAsBytes(@NonNull File file) throws Exception {
|
|
305
|
+
try {
|
|
306
|
+
MessageDigest digest = MessageDigest.getInstance("SHA-256");
|
|
307
|
+
BufferedSource source = Okio.buffer(Okio.source(file));
|
|
308
|
+
Buffer buffer = new Buffer();
|
|
309
|
+
for (long bytesRead; (bytesRead = source.read(buffer, 2048)) != -1;) {
|
|
310
|
+
digest.update(buffer.readByteArray());
|
|
311
|
+
}
|
|
312
|
+
source.close();
|
|
313
|
+
return digest.digest();
|
|
314
|
+
} catch (IOException exception) {
|
|
315
|
+
Logger.error(LiveUpdatePlugin.TAG, exception.getMessage(), exception);
|
|
316
|
+
throw new Exception(LiveUpdatePlugin.ERROR_CHECKSUM_CALCULATION_FAILED);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
private String getChecksumForFileAsString(@NonNull File file) throws Exception {
|
|
321
|
+
byte[] checksumBytes = getChecksumForFileAsBytes(file);
|
|
322
|
+
StringBuilder checksum = new StringBuilder();
|
|
323
|
+
for (byte checksumByte : checksumBytes) {
|
|
324
|
+
checksum.append(Integer.toString((checksumByte & 0xff) + 0x100, 16).substring(1));
|
|
325
|
+
}
|
|
326
|
+
return checksum.toString();
|
|
327
|
+
}
|
|
328
|
+
|
|
295
329
|
private void createBundlesDirectory() {
|
|
296
330
|
File bundlesDirectory = buildBundlesDirectory();
|
|
297
331
|
if (!bundlesDirectory.exists()) {
|
|
@@ -299,6 +333,19 @@ public class LiveUpdate {
|
|
|
299
333
|
}
|
|
300
334
|
}
|
|
301
335
|
|
|
336
|
+
private PublicKey createPublicKeyFromString(@NonNull String value) throws Exception {
|
|
337
|
+
try {
|
|
338
|
+
value = value.replace("-----BEGIN PUBLIC KEY-----", "").replace("-----END PUBLIC KEY-----", "").replace("\n", "");
|
|
339
|
+
byte[] byteKey = Base64.decode(value, Base64.DEFAULT);
|
|
340
|
+
X509EncodedKeySpec X509publicKey = new X509EncodedKeySpec(byteKey);
|
|
341
|
+
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
|
|
342
|
+
return keyFactory.generatePublic(X509publicKey);
|
|
343
|
+
} catch (Exception exception) {
|
|
344
|
+
Logger.error(LiveUpdatePlugin.TAG, exception.getMessage(), exception);
|
|
345
|
+
throw new Exception(LiveUpdatePlugin.ERROR_PUBLIC_KEY_INVALID);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
302
349
|
private void deleteBundle(@NonNull String bundleId) {
|
|
303
350
|
File bundleDirectory = buildBundleDirectoryFor(bundleId);
|
|
304
351
|
deleteFileRecursively(bundleDirectory);
|
|
@@ -322,7 +369,13 @@ public class LiveUpdate {
|
|
|
322
369
|
}
|
|
323
370
|
}
|
|
324
371
|
|
|
325
|
-
private void downloadBundle(
|
|
372
|
+
private void downloadBundle(
|
|
373
|
+
@NonNull String bundleId,
|
|
374
|
+
@Nullable String expectedChecksum,
|
|
375
|
+
@Nullable String signature,
|
|
376
|
+
@NonNull String url,
|
|
377
|
+
@NonNull EmptyCallback callback
|
|
378
|
+
) {
|
|
326
379
|
downloadFile(
|
|
327
380
|
url,
|
|
328
381
|
new NonEmptyCallback<File>() {
|
|
@@ -334,6 +387,28 @@ public class LiveUpdate {
|
|
|
334
387
|
@Override
|
|
335
388
|
public void success(@NonNull File result) {
|
|
336
389
|
try {
|
|
390
|
+
// Verify the signature
|
|
391
|
+
String publicKey = config.getPublicKey();
|
|
392
|
+
if (publicKey != null) {
|
|
393
|
+
if (signature == null) {
|
|
394
|
+
throw new Exception(LiveUpdatePlugin.ERROR_SIGNATURE_MISSING);
|
|
395
|
+
}
|
|
396
|
+
PublicKey key = createPublicKeyFromString(publicKey);
|
|
397
|
+
// Verify the signature
|
|
398
|
+
boolean verified = verifySignatureForFile(result, signature, key);
|
|
399
|
+
if (!verified) {
|
|
400
|
+
throw new Exception(LiveUpdatePlugin.ERROR_SIGNATURE_VERIFICATION_FAILED);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
// Verify the checksum
|
|
404
|
+
else if (expectedChecksum != null) {
|
|
405
|
+
// Calculate the checksum
|
|
406
|
+
String receivedChecksum = getChecksumForFileAsString(result);
|
|
407
|
+
if (!expectedChecksum.equals(receivedChecksum)) {
|
|
408
|
+
throw new Exception(LiveUpdatePlugin.ERROR_CHECKSUM_MISMATCH);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
337
412
|
// Add the bundle
|
|
338
413
|
addBundle(bundleId, result);
|
|
339
414
|
|
|
@@ -415,6 +490,7 @@ public class LiveUpdate {
|
|
|
415
490
|
.addQueryParameter("platform", "0")
|
|
416
491
|
.addQueryParameter("pluginVersion", LiveUpdatePlugin.VERSION)
|
|
417
492
|
.build();
|
|
493
|
+
Logger.debug(LiveUpdatePlugin.TAG, "Fetching latest bundle from " + url);
|
|
418
494
|
OkHttpClient client = new OkHttpClient();
|
|
419
495
|
okhttp3.Request request = new okhttp3.Request.Builder().url(url).build();
|
|
420
496
|
client
|
|
@@ -423,6 +499,7 @@ public class LiveUpdate {
|
|
|
423
499
|
new okhttp3.Callback() {
|
|
424
500
|
@Override
|
|
425
501
|
public void onFailure(@NonNull okhttp3.Call call, @NonNull IOException exception) {
|
|
502
|
+
Logger.error(LiveUpdatePlugin.TAG, exception.getMessage(), exception);
|
|
426
503
|
callback.error(exception);
|
|
427
504
|
}
|
|
428
505
|
|
|
@@ -472,6 +549,9 @@ public class LiveUpdate {
|
|
|
472
549
|
return bundleIds;
|
|
473
550
|
}
|
|
474
551
|
|
|
552
|
+
/**
|
|
553
|
+
* @return The current bundle ID (`public` for the built-in bundle).
|
|
554
|
+
*/
|
|
475
555
|
private String getCurrentBundleId() {
|
|
476
556
|
String currentPath = getCurrentCapacitorServerPath();
|
|
477
557
|
if (currentPath.equals(defaultWebAssetDir)) {
|
|
@@ -480,6 +560,9 @@ public class LiveUpdate {
|
|
|
480
560
|
return new File(currentPath).getName();
|
|
481
561
|
}
|
|
482
562
|
|
|
563
|
+
/**
|
|
564
|
+
* @return The absolute path to the current bundle directory (`public` for the built-in bundle).
|
|
565
|
+
*/
|
|
483
566
|
private String getCurrentCapacitorServerPath() {
|
|
484
567
|
return plugin.getBridge().getServerBasePath();
|
|
485
568
|
}
|
|
@@ -494,23 +577,27 @@ public class LiveUpdate {
|
|
|
494
577
|
return deviceId;
|
|
495
578
|
}
|
|
496
579
|
|
|
580
|
+
/**
|
|
581
|
+
* @return The next bundle ID (`public` for the built-in bundle).
|
|
582
|
+
*/
|
|
583
|
+
@NonNull
|
|
497
584
|
private String getNextBundleId() {
|
|
498
585
|
String nextPath = getNextCapacitorServerPath();
|
|
499
|
-
if (nextPath == null) {
|
|
500
|
-
return null;
|
|
501
|
-
}
|
|
502
586
|
if (nextPath.equals(defaultWebAssetDir)) {
|
|
503
587
|
return defaultWebAssetDir;
|
|
504
588
|
}
|
|
505
589
|
return new File(nextPath).getName();
|
|
506
590
|
}
|
|
507
591
|
|
|
508
|
-
|
|
592
|
+
/**
|
|
593
|
+
* @return The absolute path to the next bundle directory (`public` for the built-in bundle).
|
|
594
|
+
*/
|
|
595
|
+
@NonNull
|
|
509
596
|
private String getNextCapacitorServerPath() {
|
|
510
597
|
return plugin
|
|
511
598
|
.getContext()
|
|
512
599
|
.getSharedPreferences(WebView.WEBVIEW_PREFS_NAME, Activity.MODE_PRIVATE)
|
|
513
|
-
.getString(WebView.CAP_SERVER_PATH,
|
|
600
|
+
.getString(WebView.CAP_SERVER_PATH, defaultWebAssetDir);
|
|
514
601
|
}
|
|
515
602
|
|
|
516
603
|
private boolean hasBundle(@NonNull String bundleId) {
|
|
@@ -607,6 +694,24 @@ public class LiveUpdate {
|
|
|
607
694
|
return destination;
|
|
608
695
|
}
|
|
609
696
|
|
|
697
|
+
private boolean verifySignatureForFile(@NonNull File file, @NonNull String signature, @NonNull PublicKey key) throws Exception {
|
|
698
|
+
try {
|
|
699
|
+
byte[] signatureBytes = Base64.decode(signature, Base64.DEFAULT);
|
|
700
|
+
Signature sig = Signature.getInstance("SHA256withRSA");
|
|
701
|
+
sig.initVerify(key);
|
|
702
|
+
BufferedSource source = Okio.buffer(Okio.source(file));
|
|
703
|
+
Buffer buffer = new Buffer();
|
|
704
|
+
for (long bytesRead; (bytesRead = source.read(buffer, 2048)) != -1;) {
|
|
705
|
+
sig.update(buffer.readByteArray());
|
|
706
|
+
}
|
|
707
|
+
source.close();
|
|
708
|
+
return sig.verify(signatureBytes);
|
|
709
|
+
} catch (Exception exception) {
|
|
710
|
+
Logger.error(LiveUpdatePlugin.TAG, exception.getMessage(), exception);
|
|
711
|
+
throw new Exception(LiveUpdatePlugin.ERROR_SIGNATURE_VERIFICATION_FAILED);
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
|
|
610
715
|
private boolean wasUpdated() throws PackageManager.NameNotFoundException {
|
|
611
716
|
int lastVersionCode = preferences.getLastVersionCode();
|
|
612
717
|
int currentVersionCode = getPackageInfo().versionCode;
|
package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/LiveUpdateConfig.java
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
package io.capawesome.capacitorjs.plugins.liveupdate;
|
|
2
2
|
|
|
3
3
|
import androidx.annotation.Nullable;
|
|
4
|
+
import java.security.Key;
|
|
4
5
|
|
|
5
6
|
public class LiveUpdateConfig {
|
|
6
7
|
|
|
@@ -9,6 +10,10 @@ public class LiveUpdateConfig {
|
|
|
9
10
|
|
|
10
11
|
private boolean autoDeleteBundles = false;
|
|
11
12
|
private boolean enabled = true;
|
|
13
|
+
|
|
14
|
+
@Nullable
|
|
15
|
+
private String publicKey = null;
|
|
16
|
+
|
|
12
17
|
private int readyTimeout = 10000;
|
|
13
18
|
private boolean resetOnUpdate = true;
|
|
14
19
|
|
|
@@ -25,6 +30,11 @@ public class LiveUpdateConfig {
|
|
|
25
30
|
return enabled;
|
|
26
31
|
}
|
|
27
32
|
|
|
33
|
+
@Nullable
|
|
34
|
+
public String getPublicKey() {
|
|
35
|
+
return publicKey;
|
|
36
|
+
}
|
|
37
|
+
|
|
28
38
|
public int getReadyTimeout() {
|
|
29
39
|
return readyTimeout;
|
|
30
40
|
}
|
|
@@ -45,6 +55,10 @@ public class LiveUpdateConfig {
|
|
|
45
55
|
this.enabled = enabled;
|
|
46
56
|
}
|
|
47
57
|
|
|
58
|
+
public void setPublicKey(@Nullable String publicKey) {
|
|
59
|
+
this.publicKey = publicKey;
|
|
60
|
+
}
|
|
61
|
+
|
|
48
62
|
public void setReadyTimeout(int readyTimeout) {
|
|
49
63
|
this.readyTimeout = readyTimeout;
|
|
50
64
|
}
|
package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/LiveUpdatePlugin.java
CHANGED
|
@@ -19,16 +19,21 @@ import io.capawesome.capacitorjs.plugins.liveupdate.interfaces.Result;
|
|
|
19
19
|
public class LiveUpdatePlugin extends Plugin {
|
|
20
20
|
|
|
21
21
|
public static final String TAG = "LiveUpdate";
|
|
22
|
-
public static final String VERSION = "6.0
|
|
22
|
+
public static final String VERSION = "6.1.0";
|
|
23
23
|
public static final String SHARED_PREFERENCES_NAME = "CapawesomeLiveUpdate"; // DO NOT CHANGE
|
|
24
24
|
public static final String ERROR_APP_ID_MISSING = "appId must be configured.";
|
|
25
25
|
public static final String ERROR_BUNDLE_EXISTS = "bundle already exists.";
|
|
26
26
|
public static final String ERROR_BUNDLE_ID_MISSING = "bundleId must be provided.";
|
|
27
27
|
public static final String ERROR_BUNDLE_INDEX_HTML_MISSING = "The bundle does not contain an index.html file.";
|
|
28
28
|
public static final String ERROR_BUNDLE_NOT_FOUND = "bundle not found.";
|
|
29
|
+
public static final String ERROR_CHECKSUM_CALCULATION_FAILED = "Failed to calculate checksum.";
|
|
30
|
+
public static final String ERROR_CHECKSUM_MISMATCH = "Checksum mismatch.";
|
|
29
31
|
public static final String ERROR_CUSTOM_ID_MISSING = "customId must be provided.";
|
|
30
32
|
public static final String ERROR_DOWNLOAD_FAILED = "Bundle could not be downloaded.";
|
|
31
33
|
public static final String ERROR_URL_MISSING = "url must be provided.";
|
|
34
|
+
public static final String ERROR_SIGNATURE_VERIFICATION_FAILED = "Signature verification failed.";
|
|
35
|
+
public static final String ERROR_PUBLIC_KEY_INVALID = "Invalid public key.";
|
|
36
|
+
public static final String ERROR_SIGNATURE_MISSING = "Bundle does not contain a signature.";
|
|
32
37
|
|
|
33
38
|
@Nullable
|
|
34
39
|
private LiveUpdateConfig config;
|
|
@@ -78,18 +83,19 @@ public class LiveUpdatePlugin extends Plugin {
|
|
|
78
83
|
@PluginMethod
|
|
79
84
|
public void downloadBundle(PluginCall call) {
|
|
80
85
|
try {
|
|
81
|
-
String url = call.getString("url");
|
|
82
|
-
if (url == null) {
|
|
83
|
-
call.reject(ERROR_URL_MISSING);
|
|
84
|
-
return;
|
|
85
|
-
}
|
|
86
86
|
String bundleId = call.getString("bundleId");
|
|
87
87
|
if (bundleId == null) {
|
|
88
88
|
call.reject(ERROR_BUNDLE_ID_MISSING);
|
|
89
89
|
return;
|
|
90
90
|
}
|
|
91
|
+
String checksum = call.getString("checksum");
|
|
92
|
+
String url = call.getString("url");
|
|
93
|
+
if (url == null) {
|
|
94
|
+
call.reject(ERROR_URL_MISSING);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
91
97
|
|
|
92
|
-
DownloadBundleOptions options = new DownloadBundleOptions(bundleId, url);
|
|
98
|
+
DownloadBundleOptions options = new DownloadBundleOptions(bundleId, checksum, url);
|
|
93
99
|
EmptyCallback callback = new EmptyCallback() {
|
|
94
100
|
@Override
|
|
95
101
|
public void success() {
|
|
@@ -427,6 +433,8 @@ public class LiveUpdatePlugin extends Plugin {
|
|
|
427
433
|
config.setAutoDeleteBundles(autoDeleteBundles);
|
|
428
434
|
boolean enabled = getConfig().getBoolean("enabled", config.getEnabled());
|
|
429
435
|
config.setEnabled(enabled);
|
|
436
|
+
String publicKey = getConfig().getString("publicKey", config.getPublicKey());
|
|
437
|
+
config.setPublicKey(publicKey);
|
|
430
438
|
int readyTimeout = getConfig().getInt("readyTimeout", config.getReadyTimeout());
|
|
431
439
|
config.setReadyTimeout(readyTimeout);
|
|
432
440
|
boolean resetOnUpdate = getConfig().getBoolean("resetOnUpdate", config.getResetOnUpdate());
|
|
@@ -6,10 +6,29 @@ import org.json.JSONObject;
|
|
|
6
6
|
public class GetLatestBundleResponse {
|
|
7
7
|
|
|
8
8
|
private String bundleId;
|
|
9
|
+
|
|
10
|
+
@Nullable
|
|
11
|
+
private String checksum;
|
|
12
|
+
|
|
13
|
+
@Nullable
|
|
14
|
+
private String signature;
|
|
15
|
+
|
|
9
16
|
private String url;
|
|
10
17
|
|
|
11
18
|
public GetLatestBundleResponse(JSONObject responseJson) {
|
|
12
19
|
this.bundleId = responseJson.optString("bundleId");
|
|
20
|
+
String checksum = responseJson.optString("checksum", "null");
|
|
21
|
+
if (checksum.equals("null")) {
|
|
22
|
+
this.checksum = null;
|
|
23
|
+
} else {
|
|
24
|
+
this.checksum = checksum;
|
|
25
|
+
}
|
|
26
|
+
String signature = responseJson.optString("signature", "null");
|
|
27
|
+
if (signature.equals("null")) {
|
|
28
|
+
this.signature = null;
|
|
29
|
+
} else {
|
|
30
|
+
this.signature = signature;
|
|
31
|
+
}
|
|
13
32
|
this.url = responseJson.optString("url");
|
|
14
33
|
}
|
|
15
34
|
|
|
@@ -17,6 +36,16 @@ public class GetLatestBundleResponse {
|
|
|
17
36
|
return bundleId;
|
|
18
37
|
}
|
|
19
38
|
|
|
39
|
+
@Nullable
|
|
40
|
+
public String getChecksum() {
|
|
41
|
+
return checksum;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
@Nullable
|
|
45
|
+
public String getSignature() {
|
|
46
|
+
return signature;
|
|
47
|
+
}
|
|
48
|
+
|
|
20
49
|
public String getUrl() {
|
|
21
50
|
return url;
|
|
22
51
|
}
|
|
@@ -1,17 +1,22 @@
|
|
|
1
1
|
package io.capawesome.capacitorjs.plugins.liveupdate.classes.options;
|
|
2
2
|
|
|
3
3
|
import androidx.annotation.NonNull;
|
|
4
|
+
import androidx.annotation.Nullable;
|
|
4
5
|
|
|
5
6
|
public class DownloadBundleOptions {
|
|
6
7
|
|
|
7
8
|
@NonNull
|
|
8
9
|
private String bundleId;
|
|
9
10
|
|
|
11
|
+
@Nullable
|
|
12
|
+
private String checksum;
|
|
13
|
+
|
|
10
14
|
@NonNull
|
|
11
15
|
private String url;
|
|
12
16
|
|
|
13
|
-
public DownloadBundleOptions(@NonNull String bundleId, @NonNull String url) {
|
|
17
|
+
public DownloadBundleOptions(@NonNull String bundleId, @Nullable String checksum, @NonNull String url) {
|
|
14
18
|
this.bundleId = bundleId;
|
|
19
|
+
this.checksum = checksum;
|
|
15
20
|
this.url = url;
|
|
16
21
|
}
|
|
17
22
|
|
|
@@ -20,6 +25,11 @@ public class DownloadBundleOptions {
|
|
|
20
25
|
return bundleId;
|
|
21
26
|
}
|
|
22
27
|
|
|
28
|
+
@Nullable
|
|
29
|
+
public String getChecksum() {
|
|
30
|
+
return checksum;
|
|
31
|
+
}
|
|
32
|
+
|
|
23
33
|
@NonNull
|
|
24
34
|
public String getUrl() {
|
|
25
35
|
return url;
|
package/dist/docs.json
CHANGED
|
@@ -181,7 +181,7 @@
|
|
|
181
181
|
"text": "5.0.0"
|
|
182
182
|
}
|
|
183
183
|
],
|
|
184
|
-
"docs": "Notify the plugin that the app is ready to use and no rollback is needed.\n\nOnly available on Android and iOS.",
|
|
184
|
+
"docs": "Notify the plugin that the app is ready to use and no rollback is needed.\n\n**Attention**: This method should be called as soon as the app is ready to use\nto prevent the app from being reset to the default bundle.\n\nOnly available on Android and iOS.",
|
|
185
185
|
"complexTypes": [],
|
|
186
186
|
"slug": "ready"
|
|
187
187
|
},
|
|
@@ -343,34 +343,50 @@
|
|
|
343
343
|
"methods": [],
|
|
344
344
|
"properties": [
|
|
345
345
|
{
|
|
346
|
-
"name": "
|
|
346
|
+
"name": "bundleId",
|
|
347
347
|
"tags": [
|
|
348
348
|
{
|
|
349
349
|
"text": "5.0.0",
|
|
350
350
|
"name": "since"
|
|
351
351
|
},
|
|
352
352
|
{
|
|
353
|
-
"text": "'
|
|
353
|
+
"text": "'1.0.0'",
|
|
354
354
|
"name": "example"
|
|
355
355
|
}
|
|
356
356
|
],
|
|
357
|
-
"docs": "The
|
|
357
|
+
"docs": "The unique identifier of the bundle.",
|
|
358
358
|
"complexTypes": [],
|
|
359
359
|
"type": "string"
|
|
360
360
|
},
|
|
361
361
|
{
|
|
362
|
-
"name": "
|
|
362
|
+
"name": "checksum",
|
|
363
|
+
"tags": [
|
|
364
|
+
{
|
|
365
|
+
"text": "6.1.0",
|
|
366
|
+
"name": "since"
|
|
367
|
+
},
|
|
368
|
+
{
|
|
369
|
+
"text": "'aab8e0a312e807c8eaeb923e2bdccd7731c5508869c62ad8e6e43ee881414c84'",
|
|
370
|
+
"name": "example"
|
|
371
|
+
}
|
|
372
|
+
],
|
|
373
|
+
"docs": "The checksum of the bundle to verify the integrity of the ZIP file.\n\nMust be a SHA-256 hash in hexadecimal format.",
|
|
374
|
+
"complexTypes": [],
|
|
375
|
+
"type": "string | undefined"
|
|
376
|
+
},
|
|
377
|
+
{
|
|
378
|
+
"name": "url",
|
|
363
379
|
"tags": [
|
|
364
380
|
{
|
|
365
381
|
"text": "5.0.0",
|
|
366
382
|
"name": "since"
|
|
367
383
|
},
|
|
368
384
|
{
|
|
369
|
-
"text": "'
|
|
385
|
+
"text": "'https://example.com/bundle.zip'",
|
|
370
386
|
"name": "example"
|
|
371
387
|
}
|
|
372
388
|
],
|
|
373
|
-
"docs": "The
|
|
389
|
+
"docs": "The URL of the bundle to download.\n\nThe bundle must be a ZIP file containing at least a `index.html` file.",
|
|
374
390
|
"complexTypes": [],
|
|
375
391
|
"type": "string"
|
|
376
392
|
}
|
|
@@ -746,6 +762,22 @@
|
|
|
746
762
|
"complexTypes": [],
|
|
747
763
|
"type": "boolean | undefined"
|
|
748
764
|
},
|
|
765
|
+
{
|
|
766
|
+
"name": "publicKey",
|
|
767
|
+
"tags": [
|
|
768
|
+
{
|
|
769
|
+
"text": "6.1.0",
|
|
770
|
+
"name": "since"
|
|
771
|
+
},
|
|
772
|
+
{
|
|
773
|
+
"text": "'-----BEGIN PUBLIC KEY-----MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDDodf1SD0OOn6hIlDuKBza0Ed0OqtwyVJwiyjmE9BJaZ7y8ZUfcF+SKmd0l2cDPM45XIg2tAFux5n29uoKyHwSt+6tCi5CJA5Z1/1eZruRRqABLonV77KS3HUtvOgqRLDnKSV89dYZkM++NwmzOPgIF422mvc+VukcVOBfc8/AHQIDAQAB-----END PUBLIC KEY-----'",
|
|
774
|
+
"name": "example"
|
|
775
|
+
}
|
|
776
|
+
],
|
|
777
|
+
"docs": "The public key to verify the integrity of the bundle.\n\nThe public key must be a PEM-encoded RSA public key.",
|
|
778
|
+
"complexTypes": [],
|
|
779
|
+
"type": "string | undefined"
|
|
780
|
+
},
|
|
749
781
|
{
|
|
750
782
|
"name": "readyTimeout",
|
|
751
783
|
"tags": [
|
|
@@ -24,6 +24,15 @@ declare module '@capacitor/cli' {
|
|
|
24
24
|
* @default true
|
|
25
25
|
*/
|
|
26
26
|
enabled?: boolean;
|
|
27
|
+
/**
|
|
28
|
+
* The public key to verify the integrity of the bundle.
|
|
29
|
+
*
|
|
30
|
+
* The public key must be a PEM-encoded RSA public key.
|
|
31
|
+
*
|
|
32
|
+
* @since 6.1.0
|
|
33
|
+
* @example '-----BEGIN PUBLIC KEY-----MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDDodf1SD0OOn6hIlDuKBza0Ed0OqtwyVJwiyjmE9BJaZ7y8ZUfcF+SKmd0l2cDPM45XIg2tAFux5n29uoKyHwSt+6tCi5CJA5Z1/1eZruRRqABLonV77KS3HUtvOgqRLDnKSV89dYZkM++NwmzOPgIF422mvc+VukcVOBfc8/AHQIDAQAB-----END PUBLIC KEY-----'
|
|
34
|
+
*/
|
|
35
|
+
publicKey?: string;
|
|
27
36
|
/**
|
|
28
37
|
* The timeout in milliseconds to wait for the app to be ready before resetting to the default bundle.
|
|
29
38
|
*
|
|
@@ -117,6 +126,9 @@ export interface LiveUpdatePlugin {
|
|
|
117
126
|
/**
|
|
118
127
|
* Notify the plugin that the app is ready to use and no rollback is needed.
|
|
119
128
|
*
|
|
129
|
+
* **Attention**: This method should be called as soon as the app is ready to use
|
|
130
|
+
* to prevent the app from being reset to the default bundle.
|
|
131
|
+
*
|
|
120
132
|
* Only available on Android and iOS.
|
|
121
133
|
*
|
|
122
134
|
* @since 5.0.0
|
|
@@ -190,6 +202,22 @@ export interface DeleteBundleOptions {
|
|
|
190
202
|
bundleId: string;
|
|
191
203
|
}
|
|
192
204
|
export interface DownloadBundleOptions {
|
|
205
|
+
/**
|
|
206
|
+
* The unique identifier of the bundle.
|
|
207
|
+
*
|
|
208
|
+
* @since 5.0.0
|
|
209
|
+
* @example '1.0.0'
|
|
210
|
+
*/
|
|
211
|
+
bundleId: string;
|
|
212
|
+
/**
|
|
213
|
+
* The checksum of the bundle to verify the integrity of the ZIP file.
|
|
214
|
+
*
|
|
215
|
+
* Must be a SHA-256 hash in hexadecimal format.
|
|
216
|
+
*
|
|
217
|
+
* @since 6.1.0
|
|
218
|
+
* @example 'aab8e0a312e807c8eaeb923e2bdccd7731c5508869c62ad8e6e43ee881414c84'
|
|
219
|
+
*/
|
|
220
|
+
checksum?: string;
|
|
193
221
|
/**
|
|
194
222
|
* The URL of the bundle to download.
|
|
195
223
|
*
|
|
@@ -199,13 +227,6 @@ export interface DownloadBundleOptions {
|
|
|
199
227
|
* @example 'https://example.com/bundle.zip'
|
|
200
228
|
*/
|
|
201
229
|
url: string;
|
|
202
|
-
/**
|
|
203
|
-
* The unique identifier of the bundle.
|
|
204
|
-
*
|
|
205
|
-
* @since 5.0.0
|
|
206
|
-
* @example '1.0.0'
|
|
207
|
-
*/
|
|
208
|
-
bundleId: string;
|
|
209
230
|
}
|
|
210
231
|
/**
|
|
211
232
|
* @since 5.0.0
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"definitions.js","sourceRoot":"","sources":["../../src/definitions.ts"],"names":[],"mappings":"AAAA,wCAAwC","sourcesContent":["/// <reference types=\"@capacitor/cli\" />\n\ndeclare module '@capacitor/cli' {\n export interface PluginsConfig {\n LiveUpdate?: {\n /**\n * The app ID is used to identify the app when using [Capawesome Cloud](https://capawesome.io/cloud).\n *\n * @since 5.0.0\n * @example '4100e356-e851-47fe-9d3c-b411eb325a99'\n */\n appId?: string;\n /**\n * Whether or not to automatically delete unused bundles.\n *\n * When enabled, the plugin will automatically delete unused bundles after calling `ready()`.\n *\n * @since 5.0.0\n * @default false\n */\n autoDeleteBundles?: boolean;\n /**\n * Whether or not the plugin is enabled.\n *\n * @since 5.0.0\n * @default true\n */\n enabled?: boolean;\n /**\n * The timeout in milliseconds to wait for the app to be ready before resetting to the default bundle.\n *\n * @since 5.0.0\n * @default 10000\n */\n readyTimeout?: number;\n /**\n * Whether or not the app should be reset to the default bundle during an update.\n *\n * @since 5.0.0\n * @default true\n */\n resetOnUpdate?: boolean;\n };\n }\n}\n\nexport interface LiveUpdatePlugin {\n /**\n * Delete a bundle from the app.\n *\n * Only available on Android and iOS.\n *\n * @since 5.0.0\n */\n deleteBundle(options: DeleteBundleOptions): Promise<void>;\n /**\n * Download a bundle.\n *\n * Only available on Android and iOS.\n *\n * @since 5.0.0\n */\n downloadBundle(options: DownloadBundleOptions): Promise<void>;\n /**\n * Get the active bundle identifier.\n *\n * Only available on Android and iOS.\n *\n * @since 5.0.0\n */\n getBundle(): Promise<GetBundleResult>;\n /**\n * Get all available bundle identifiers.\n *\n * Only available on Android and iOS.\n *\n * @since 5.0.0\n */\n getBundles(): Promise<GetBundlesResult>;\n /**\n * Get the channel of the app.\n *\n * Only available on Android and iOS.\n *\n * @since 5.0.0\n */\n getChannel(): Promise<GetChannelResult>;\n /**\n * Get the custom identifier of the app.\n *\n * Only available on Android and iOS.\n *\n * @since 5.0.0\n */\n getCustomId(): Promise<GetCustomIdResult>;\n /**\n * Get the device identifier of the app.\n *\n * Only available on Android and iOS.\n *\n * @since 5.0.0\n */\n getDeviceId(): Promise<GetDeviceIdResult>;\n /**\n * Get the version code of the app.\n *\n * Only available on Android and iOS.\n *\n * @since 5.0.0\n */\n getVersionCode(): Promise<GetVersionCodeResult>;\n /**\n * Get the version name of the app.\n *\n * Only available on Android and iOS.\n *\n * @since 5.0.0\n */\n getVersionName(): Promise<GetVersionNameResult>;\n /**\n * Notify the plugin that the app is ready to use and no rollback is needed.\n *\n * Only available on Android and iOS.\n *\n * @since 5.0.0\n */\n ready(): Promise<void>;\n /**\n * Reload the app to apply the new bundle.\n *\n * Only available on Android and iOS.\n *\n * @since 5.0.0\n */\n reload(): Promise<void>;\n /**\n * Reset the app to the default bundle.\n *\n * Call `reload()` or restart the app to apply the changes.\n *\n * Only available on Android and iOS.\n *\n * @since 5.0.0\n */\n reset(): Promise<void>;\n /**\n * Set the next bundle to use for the app.\n *\n * Call `reload()` or restart the app to apply the changes.\n *\n * Only available on Android and iOS.\n *\n * @since 5.0.0\n */\n setBundle(options: SetBundleOptions): Promise<void>;\n /**\n * Set the channel of the app.\n *\n * Only available on Android and iOS.\n *\n * @since 5.0.0\n */\n setChannel(options: SetChannelOptions): Promise<void>;\n /**\n * Set the custom identifier of the app.\n *\n * Only available on Android and iOS.\n *\n * @since 5.0.0\n */\n setCustomId(options: SetCustomIdOptions): Promise<void>;\n /**\n * Automatically download and set the latest bundle for the app using the [Capawesome Cloud](https://capawesome.io/cloud/).\n *\n * Call `reload()` or restart the app to apply the changes.\n *\n * Only available on Android and iOS.\n *\n * @since 5.0.0\n */\n sync(): Promise<SyncResult>;\n}\n\n/**\n * @since 5.0.0\n */\nexport interface DeleteBundleOptions {\n /**\n * The unique identifier of the bundle to delete.\n *\n * @since 5.0.0\n * @example '1.0.0'\n */\n bundleId: string;\n}\n\nexport interface DownloadBundleOptions {\n /**\n * The
|
|
1
|
+
{"version":3,"file":"definitions.js","sourceRoot":"","sources":["../../src/definitions.ts"],"names":[],"mappings":"AAAA,wCAAwC","sourcesContent":["/// <reference types=\"@capacitor/cli\" />\n\ndeclare module '@capacitor/cli' {\n export interface PluginsConfig {\n LiveUpdate?: {\n /**\n * The app ID is used to identify the app when using [Capawesome Cloud](https://capawesome.io/cloud).\n *\n * @since 5.0.0\n * @example '4100e356-e851-47fe-9d3c-b411eb325a99'\n */\n appId?: string;\n /**\n * Whether or not to automatically delete unused bundles.\n *\n * When enabled, the plugin will automatically delete unused bundles after calling `ready()`.\n *\n * @since 5.0.0\n * @default false\n */\n autoDeleteBundles?: boolean;\n /**\n * Whether or not the plugin is enabled.\n *\n * @since 5.0.0\n * @default true\n */\n enabled?: boolean;\n /**\n * The public key to verify the integrity of the bundle.\n *\n * The public key must be a PEM-encoded RSA public key.\n *\n * @since 6.1.0\n * @example '-----BEGIN PUBLIC KEY-----MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDDodf1SD0OOn6hIlDuKBza0Ed0OqtwyVJwiyjmE9BJaZ7y8ZUfcF+SKmd0l2cDPM45XIg2tAFux5n29uoKyHwSt+6tCi5CJA5Z1/1eZruRRqABLonV77KS3HUtvOgqRLDnKSV89dYZkM++NwmzOPgIF422mvc+VukcVOBfc8/AHQIDAQAB-----END PUBLIC KEY-----'\n */\n publicKey?: string;\n /**\n * The timeout in milliseconds to wait for the app to be ready before resetting to the default bundle.\n *\n * @since 5.0.0\n * @default 10000\n */\n readyTimeout?: number;\n /**\n * Whether or not the app should be reset to the default bundle during an update.\n *\n * @since 5.0.0\n * @default true\n */\n resetOnUpdate?: boolean;\n };\n }\n}\n\nexport interface LiveUpdatePlugin {\n /**\n * Delete a bundle from the app.\n *\n * Only available on Android and iOS.\n *\n * @since 5.0.0\n */\n deleteBundle(options: DeleteBundleOptions): Promise<void>;\n /**\n * Download a bundle.\n *\n * Only available on Android and iOS.\n *\n * @since 5.0.0\n */\n downloadBundle(options: DownloadBundleOptions): Promise<void>;\n /**\n * Get the active bundle identifier.\n *\n * Only available on Android and iOS.\n *\n * @since 5.0.0\n */\n getBundle(): Promise<GetBundleResult>;\n /**\n * Get all available bundle identifiers.\n *\n * Only available on Android and iOS.\n *\n * @since 5.0.0\n */\n getBundles(): Promise<GetBundlesResult>;\n /**\n * Get the channel of the app.\n *\n * Only available on Android and iOS.\n *\n * @since 5.0.0\n */\n getChannel(): Promise<GetChannelResult>;\n /**\n * Get the custom identifier of the app.\n *\n * Only available on Android and iOS.\n *\n * @since 5.0.0\n */\n getCustomId(): Promise<GetCustomIdResult>;\n /**\n * Get the device identifier of the app.\n *\n * Only available on Android and iOS.\n *\n * @since 5.0.0\n */\n getDeviceId(): Promise<GetDeviceIdResult>;\n /**\n * Get the version code of the app.\n *\n * Only available on Android and iOS.\n *\n * @since 5.0.0\n */\n getVersionCode(): Promise<GetVersionCodeResult>;\n /**\n * Get the version name of the app.\n *\n * Only available on Android and iOS.\n *\n * @since 5.0.0\n */\n getVersionName(): Promise<GetVersionNameResult>;\n /**\n * Notify the plugin that the app is ready to use and no rollback is needed.\n *\n * **Attention**: This method should be called as soon as the app is ready to use\n * to prevent the app from being reset to the default bundle.\n *\n * Only available on Android and iOS.\n *\n * @since 5.0.0\n */\n ready(): Promise<void>;\n /**\n * Reload the app to apply the new bundle.\n *\n * Only available on Android and iOS.\n *\n * @since 5.0.0\n */\n reload(): Promise<void>;\n /**\n * Reset the app to the default bundle.\n *\n * Call `reload()` or restart the app to apply the changes.\n *\n * Only available on Android and iOS.\n *\n * @since 5.0.0\n */\n reset(): Promise<void>;\n /**\n * Set the next bundle to use for the app.\n *\n * Call `reload()` or restart the app to apply the changes.\n *\n * Only available on Android and iOS.\n *\n * @since 5.0.0\n */\n setBundle(options: SetBundleOptions): Promise<void>;\n /**\n * Set the channel of the app.\n *\n * Only available on Android and iOS.\n *\n * @since 5.0.0\n */\n setChannel(options: SetChannelOptions): Promise<void>;\n /**\n * Set the custom identifier of the app.\n *\n * Only available on Android and iOS.\n *\n * @since 5.0.0\n */\n setCustomId(options: SetCustomIdOptions): Promise<void>;\n /**\n * Automatically download and set the latest bundle for the app using the [Capawesome Cloud](https://capawesome.io/cloud/).\n *\n * Call `reload()` or restart the app to apply the changes.\n *\n * Only available on Android and iOS.\n *\n * @since 5.0.0\n */\n sync(): Promise<SyncResult>;\n}\n\n/**\n * @since 5.0.0\n */\nexport interface DeleteBundleOptions {\n /**\n * The unique identifier of the bundle to delete.\n *\n * @since 5.0.0\n * @example '1.0.0'\n */\n bundleId: string;\n}\n\nexport interface DownloadBundleOptions {\n /**\n * The unique identifier of the bundle.\n *\n * @since 5.0.0\n * @example '1.0.0'\n */\n bundleId: string;\n /**\n * The checksum of the bundle to verify the integrity of the ZIP file.\n *\n * Must be a SHA-256 hash in hexadecimal format.\n *\n * @since 6.1.0\n * @example 'aab8e0a312e807c8eaeb923e2bdccd7731c5508869c62ad8e6e43ee881414c84'\n */\n checksum?: string;\n /**\n * The URL of the bundle to download.\n *\n * The bundle must be a ZIP file containing at least a `index.html` file.\n *\n * @since 5.0.0\n * @example 'https://example.com/bundle.zip'\n */\n url: string;\n}\n\n/**\n * @since 5.0.0\n */\nexport interface GetBundleResult {\n /**\n * The unique identifier of the active bundle.\n *\n * If `null`, the default bundle is being used.\n *\n * @since 5.0.0\n * @example '1.0.0'\n */\n bundleId: string | null;\n}\n\n/**\n * @since 5.0.0\n */\nexport interface GetBundlesResult {\n /**\n * An array of unique identifiers of all available bundles.\n *\n * @since 5.0.0\n */\n bundleIds: string[];\n}\n\n/**\n * @since 5.0.0\n */\nexport interface GetChannelResult {\n /**\n * The channel of the app.\n *\n * If `null`, the app is using the default channel.\n *\n * @since 5.0.0\n * @example 'production'\n */\n channel: string | null;\n}\n\n/**\n * @since 5.0.0\n */\nexport interface GetDeviceIdResult {\n /**\n * The unique identifier of the device.\n *\n * On iOS, [`identifierForVendor`](https://developer.apple.com/documentation/uikit/uidevice/1620059-identifierforvendor) is used.\n * The value of this property is the same for apps that come from the same vendor running on the same device.\n *\n * @since 5.0.0\n *\n * @example '50d2a548-80b7-4dad-adc7-97c0e79d8a89'\n */\n deviceId: string;\n}\n\n/**\n * @since 5.0.0\n */\nexport interface GetVersionCodeResult {\n /**\n * The version code of the app.\n *\n * On **Android**, this is the `versionCode` from the `android/app/build.gradle` file.\n * On **iOS**, this is the `CFBundleVersion` from the `Info.plist` file.\n *\n * @since 5.0.0\n * @example \"1\"\n */\n versionCode: string;\n}\n\n/**\n * @since 5.0.0\n */\nexport interface GetVersionNameResult {\n /**\n * The version name of the app.\n *\n * On **Android**, this is the `versionName` from the `android/app/build.gradle` file.\n * On **iOS**, this is the `CFBundleShortVersionString` from the `Info.plist` file.\n *\n * @since 5.0.0\n * @example \"1.0.0\"\n */\n versionName: string;\n}\n\n/**\n * @since 5.0.0\n */\nexport interface GetCustomIdResult {\n /**\n * The custom identifier of the app.\n *\n * If `null`, no custom identifier is set.\n *\n * @since 5.0.0\n * @example '50d2a548-80b7-4dad-adc7-97c0e79d8a89'\n */\n customId: string | null;\n}\n\n/**\n * @since 5.0.0\n */\nexport interface SetBundleOptions {\n /**\n * The unique identifier of the bundle to use.\n *\n * @since 5.0.0\n * @example '1.0.0'\n */\n bundleId: string;\n}\n\n/**\n * @since 5.0.0\n */\nexport interface SetChannelOptions {\n /**\n * The channel of the app.\n *\n * Set `null` to remove the channel.\n *\n * @since 5.0.0\n */\n channel: string | null;\n}\n\n/**\n * @since 5.0.0\n */\nexport interface SetCustomIdOptions {\n /**\n * The custom identifier of the app.\n *\n * Set `null` to remove the custom identifier.\n *\n * @since 5.0.0\n */\n customId: string | null;\n}\n\n/**\n * @since 5.0.0\n */\nexport interface SyncResult {\n /**\n * The identifier of the next bundle to use.\n *\n * If `null`, the app is up-to-date and no new bundle is available.\n *\n * @since 5.0.0\n */\n nextBundleId: string | null;\n}\n"]}
|
|
@@ -7,8 +7,12 @@ public enum CustomError: Error {
|
|
|
7
7
|
case bundleIndexHtmlMissing
|
|
8
8
|
case bundleNotFound
|
|
9
9
|
case channelMissing
|
|
10
|
+
case checksumCalculationFailed
|
|
11
|
+
case checksumMismatch
|
|
10
12
|
case customIdMissing
|
|
11
13
|
case downloadFailed
|
|
14
|
+
case signatureMissing
|
|
15
|
+
case signatureVerificationFailed
|
|
12
16
|
case urlMissing
|
|
13
17
|
}
|
|
14
18
|
|
|
@@ -27,10 +31,18 @@ extension CustomError: LocalizedError {
|
|
|
27
31
|
return NSLocalizedString("bundle not found.", comment: "bundleNotFound")
|
|
28
32
|
case .channelMissing:
|
|
29
33
|
return NSLocalizedString("channel must be provided.", comment: "channelMissing")
|
|
34
|
+
case .checksumCalculationFailed:
|
|
35
|
+
return NSLocalizedString("Failed to calculate checksum.", comment: "checksumCalculationFailed")
|
|
36
|
+
case .checksumMismatch:
|
|
37
|
+
return NSLocalizedString("Checksum mismatch.", comment: "checksumMismatch")
|
|
30
38
|
case .customIdMissing:
|
|
31
39
|
return NSLocalizedString("customId must be provided.", comment: "customIdMissing")
|
|
32
40
|
case .downloadFailed:
|
|
33
41
|
return NSLocalizedString("Bundle could not be downloaded.", comment: "downloadFailed")
|
|
42
|
+
case .signatureMissing:
|
|
43
|
+
return NSLocalizedString("Bundle does not contain a signature.", comment: "signatureMissing")
|
|
44
|
+
case .signatureVerificationFailed:
|
|
45
|
+
return NSLocalizedString("Signature verification failed.", comment: "signatureVerificationFailed")
|
|
34
46
|
case .urlMissing:
|
|
35
47
|
return NSLocalizedString("url must be provided.", comment: "urlMissing")
|
|
36
48
|
}
|
|
@@ -3,10 +3,12 @@ import Capacitor
|
|
|
3
3
|
|
|
4
4
|
@objc public class DownloadBundleOptions: NSObject {
|
|
5
5
|
private var bundleId: String
|
|
6
|
+
private var checksum: String?
|
|
6
7
|
private var url: String
|
|
7
8
|
|
|
8
|
-
init(bundleId: String, url: String) {
|
|
9
|
+
init(bundleId: String, checksum: String?, url: String) {
|
|
9
10
|
self.bundleId = bundleId
|
|
11
|
+
self.checksum = checksum
|
|
10
12
|
self.url = url
|
|
11
13
|
}
|
|
12
14
|
|
|
@@ -14,6 +16,10 @@ import Capacitor
|
|
|
14
16
|
return bundleId
|
|
15
17
|
}
|
|
16
18
|
|
|
19
|
+
func getChecksum() -> String? {
|
|
20
|
+
return checksum
|
|
21
|
+
}
|
|
22
|
+
|
|
17
23
|
func getUrl() -> String {
|
|
18
24
|
return url
|
|
19
25
|
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import Foundation
|
|
2
|
+
import CryptoKit
|
|
2
3
|
import SSZipArchive
|
|
3
4
|
import Capacitor
|
|
4
5
|
import Alamofire
|
|
6
|
+
import CommonCrypto
|
|
5
7
|
|
|
6
8
|
// swiftlint:disable type_body_length
|
|
7
9
|
@objc public class LiveUpdate: NSObject {
|
|
@@ -50,8 +52,9 @@ import Alamofire
|
|
|
50
52
|
}
|
|
51
53
|
|
|
52
54
|
@objc public func downloadBundle(_ options: DownloadBundleOptions, completion: @escaping (Error?) -> Void) {
|
|
53
|
-
let url = options.getUrl()
|
|
54
55
|
let bundleId = options.getBundleId()
|
|
56
|
+
let checksum = options.getChecksum()
|
|
57
|
+
let url = options.getUrl()
|
|
55
58
|
|
|
56
59
|
// Check if the bundle already exists
|
|
57
60
|
if hasBundle(bundleId: bundleId) {
|
|
@@ -61,7 +64,7 @@ import Alamofire
|
|
|
61
64
|
}
|
|
62
65
|
|
|
63
66
|
// Download the bundle
|
|
64
|
-
downloadBundle(bundleId: bundleId, url: url, completion: { error in
|
|
67
|
+
downloadBundle(bundleId: bundleId, checksum: checksum, signature: nil, url: url, completion: { error in
|
|
65
68
|
if let error = error {
|
|
66
69
|
completion(error)
|
|
67
70
|
return
|
|
@@ -125,17 +128,9 @@ import Alamofire
|
|
|
125
128
|
}
|
|
126
129
|
|
|
127
130
|
@objc public func reload() {
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
}
|
|
132
|
-
if let nextPath = getNextCapacitorServerPath() {
|
|
133
|
-
path = nextPath
|
|
134
|
-
}
|
|
135
|
-
if let path = path {
|
|
136
|
-
setCurrentCapacitorServerPath(path: path)
|
|
137
|
-
startRollbackTimer()
|
|
138
|
-
}
|
|
131
|
+
let path = getNextCapacitorServerPath()
|
|
132
|
+
setCurrentCapacitorServerPath(path: path)
|
|
133
|
+
startRollbackTimer()
|
|
139
134
|
}
|
|
140
135
|
|
|
141
136
|
@objc public func reset() {
|
|
@@ -195,7 +190,7 @@ import Alamofire
|
|
|
195
190
|
completion(result, nil)
|
|
196
191
|
} else {
|
|
197
192
|
// Download and unzip the bundle
|
|
198
|
-
self.downloadBundle(bundleId: latestBundleId, url: response.url, completion: { error in
|
|
193
|
+
self.downloadBundle(bundleId: latestBundleId, checksum: response.checksum, signature: response.signature, url: response.url, completion: { error in
|
|
199
194
|
if let error = error {
|
|
200
195
|
completion(nil, error)
|
|
201
196
|
return
|
|
@@ -249,6 +244,20 @@ import Alamofire
|
|
|
249
244
|
return url.path
|
|
250
245
|
}
|
|
251
246
|
|
|
247
|
+
/// - Returns: The sha256 checksum of the file at the given URL.
|
|
248
|
+
private func getChecksumForFile(url: URL) throws -> String {
|
|
249
|
+
let handle = try FileHandle(forReadingFrom: url)
|
|
250
|
+
var hasher = SHA256()
|
|
251
|
+
while autoreleasepool(invoking: {
|
|
252
|
+
let nextChunk = handle.readData(ofLength: 2048)
|
|
253
|
+
guard !nextChunk.isEmpty else { return false }
|
|
254
|
+
hasher.update(data: nextChunk)
|
|
255
|
+
return true
|
|
256
|
+
}) { }
|
|
257
|
+
let digest = hasher.finalize()
|
|
258
|
+
return digest.map { String(format: "%02hhx", $0) }.joined()
|
|
259
|
+
}
|
|
260
|
+
|
|
252
261
|
private func createBundlesDirectory() {
|
|
253
262
|
let bundlesDirectoryUrl = libraryDirectoryUrl.appendingPathComponent(bundlesDirectory)
|
|
254
263
|
let exists = FileManager.default.fileExists(atPath: bundlesDirectoryUrl.path)
|
|
@@ -282,7 +291,7 @@ import Alamofire
|
|
|
282
291
|
}
|
|
283
292
|
}
|
|
284
293
|
|
|
285
|
-
private func downloadBundle(bundleId: String, url: String, completion: @escaping (Error?) -> Void) {
|
|
294
|
+
private func downloadBundle(bundleId: String, checksum: String?, signature: String?, url: String, completion: @escaping (Error?) -> Void) {
|
|
286
295
|
// Download the bundle
|
|
287
296
|
downloadFile(url: url, completion: { url, error in
|
|
288
297
|
if let error = error {
|
|
@@ -290,6 +299,39 @@ import Alamofire
|
|
|
290
299
|
return
|
|
291
300
|
}
|
|
292
301
|
if let url = url {
|
|
302
|
+
// Verify the signature
|
|
303
|
+
if let publicKey = self.config.publicKey {
|
|
304
|
+
guard let signature = signature else {
|
|
305
|
+
completion(CustomError.signatureMissing)
|
|
306
|
+
return
|
|
307
|
+
}
|
|
308
|
+
// Verify the signature
|
|
309
|
+
do {
|
|
310
|
+
let verified = try self.verifySignatureForFile(url: url, signature: signature, publicKey: publicKey)
|
|
311
|
+
if !verified {
|
|
312
|
+
completion(CustomError.signatureVerificationFailed)
|
|
313
|
+
return
|
|
314
|
+
}
|
|
315
|
+
} catch {
|
|
316
|
+
completion(CustomError.signatureVerificationFailed)
|
|
317
|
+
return
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
// Verify the checksum
|
|
321
|
+
else if let expectedChecksum = checksum {
|
|
322
|
+
// Calculate the checksum
|
|
323
|
+
do {
|
|
324
|
+
let receivedChecksum = try self.getChecksumForFile(url: url)
|
|
325
|
+
if receivedChecksum != expectedChecksum {
|
|
326
|
+
completion(CustomError.checksumMismatch)
|
|
327
|
+
return
|
|
328
|
+
}
|
|
329
|
+
} catch {
|
|
330
|
+
completion(CustomError.checksumCalculationFailed)
|
|
331
|
+
return
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
293
335
|
// Add the bundle
|
|
294
336
|
self.addBundle(bundleId: bundleId, zipFile: url, completion: { error in
|
|
295
337
|
// Delete the temporary file
|
|
@@ -338,8 +380,12 @@ import Alamofire
|
|
|
338
380
|
parameters["pluginVersion"] = LiveUpdatePlugin.version
|
|
339
381
|
let url = URL(string: "https://api.cloud.capawesome.io/v1/apps/\(config.appId ?? "")/bundles/latest")!
|
|
340
382
|
AF.request(url, method: .get, parameters: parameters).validate().responseDecodable(of: GetLatestBundleResponse.self) { response in
|
|
383
|
+
CAPLog.print("[", LiveUpdatePlugin.tag, "] Fetching latest bundle from ", response.request?.url?.absoluteString ?? "")
|
|
341
384
|
if let error = response.error {
|
|
342
385
|
CAPLog.print("[", LiveUpdatePlugin.tag, "] ", error)
|
|
386
|
+
if let data = response.data {
|
|
387
|
+
CAPLog.print("[", LiveUpdatePlugin.tag, "] ", String(decoding: data, as: UTF8.self))
|
|
388
|
+
}
|
|
343
389
|
completion(nil, error)
|
|
344
390
|
return
|
|
345
391
|
}
|
|
@@ -375,6 +421,7 @@ import Alamofire
|
|
|
375
421
|
}
|
|
376
422
|
}
|
|
377
423
|
|
|
424
|
+
/// - Returns: The current bundle ID (`public` for the built-in bundle) or `nil` if no view controller was found.
|
|
378
425
|
private func getCurrentBundleId() -> String? {
|
|
379
426
|
guard let path = getCurrentCapacitorServerPath() else {
|
|
380
427
|
return nil
|
|
@@ -382,6 +429,7 @@ import Alamofire
|
|
|
382
429
|
return URL(fileURLWithPath: path).lastPathComponent
|
|
383
430
|
}
|
|
384
431
|
|
|
432
|
+
/// - Returns: The path to the current bundle directory or `nil` if no view controller was found.
|
|
385
433
|
private func getCurrentCapacitorServerPath() -> String? {
|
|
386
434
|
guard let viewController = self.plugin.bridge?.viewController as? CAPBridgeViewController else {
|
|
387
435
|
return nil
|
|
@@ -394,18 +442,19 @@ import Alamofire
|
|
|
394
442
|
return deviceId.lowercased()
|
|
395
443
|
}
|
|
396
444
|
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
}
|
|
445
|
+
/// - Returns: The next bundle ID (`public` for the built-in bundle).
|
|
446
|
+
private func getNextBundleId() -> String {
|
|
447
|
+
let path = getNextCapacitorServerPath()
|
|
401
448
|
return URL(fileURLWithPath: path).lastPathComponent
|
|
402
449
|
}
|
|
403
450
|
|
|
404
|
-
|
|
451
|
+
/// - Returns: The absolute path to the next bundle directory.
|
|
452
|
+
private func getNextCapacitorServerPath() -> String {
|
|
453
|
+
let defaultCapacitorServerPath = buildCapacitorServerPathFor(bundleId: defaultWebAssetDir)
|
|
405
454
|
if let path = KeyValueStore.standard[self.defaultServerPathKey, as: String.self] {
|
|
406
|
-
return path.isEmpty ?
|
|
455
|
+
return path.isEmpty ? defaultCapacitorServerPath : path
|
|
407
456
|
}
|
|
408
|
-
return
|
|
457
|
+
return defaultCapacitorServerPath
|
|
409
458
|
}
|
|
410
459
|
|
|
411
460
|
private func hasBundle(bundleId: String) -> Bool {
|
|
@@ -459,7 +508,7 @@ import Alamofire
|
|
|
459
508
|
|
|
460
509
|
private func setNextCapacitorServerPath(path: String) {
|
|
461
510
|
if path.hasSuffix("/public") {
|
|
462
|
-
//
|
|
511
|
+
// Must set an empty string to reset the custom server base path
|
|
463
512
|
KeyValueStore.standard[self.defaultServerPathKey] = ""
|
|
464
513
|
} else {
|
|
465
514
|
// Attention: Only the lastPathComponent is used (see https://dub.sh/BLluidt)
|
|
@@ -493,6 +542,64 @@ import Alamofire
|
|
|
493
542
|
})
|
|
494
543
|
}
|
|
495
544
|
|
|
545
|
+
private func verifySignatureForFile(url: URL, signature: String, publicKey: String) throws -> Bool {
|
|
546
|
+
let publicKeyAsBase64 = publicKey
|
|
547
|
+
.replacingOccurrences(of: "-----BEGIN PUBLIC KEY-----", with: "")
|
|
548
|
+
.replacingOccurrences(of: "-----END PUBLIC KEY-----", with: "")
|
|
549
|
+
.replacingOccurrences(of: "\n", with: "")
|
|
550
|
+
guard let publicKeyData = Data(base64Encoded: publicKeyAsBase64) else {
|
|
551
|
+
CAPLog.print("[", LiveUpdatePlugin.tag, "] ", "Failed to decode public key.")
|
|
552
|
+
return false
|
|
553
|
+
}
|
|
554
|
+
let publicKeyAttributes: [CFString: Any] = [
|
|
555
|
+
kSecAttrKeyType: kSecAttrKeyTypeRSA,
|
|
556
|
+
kSecAttrKeyClass: kSecAttrKeyClassPublic,
|
|
557
|
+
kSecAttrKeySizeInBits: 2048,
|
|
558
|
+
kSecReturnPersistentRef: true
|
|
559
|
+
]
|
|
560
|
+
var secKeyCreateWithDataError: Unmanaged<CFError>?
|
|
561
|
+
guard let publicKey = SecKeyCreateWithData(publicKeyData as CFData, publicKeyAttributes as CFDictionary, &secKeyCreateWithDataError) else {
|
|
562
|
+
if let error = secKeyCreateWithDataError?.takeRetainedValue() {
|
|
563
|
+
CAPLog.print("[", LiveUpdatePlugin.tag, "] ", "Failed to create public key with error: \(error)")
|
|
564
|
+
}
|
|
565
|
+
return false
|
|
566
|
+
}
|
|
567
|
+
guard let signatureData = Data(base64Encoded: signature) else {
|
|
568
|
+
CAPLog.print("[", LiveUpdatePlugin.tag, "] ", "Failed to decode signature.")
|
|
569
|
+
return false
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// Create SHA256 digest
|
|
573
|
+
var digestContext = CC_SHA256_CTX()
|
|
574
|
+
CC_SHA256_Init(&digestContext)
|
|
575
|
+
|
|
576
|
+
// Update the digest with the file's data
|
|
577
|
+
let handle = try FileHandle(forReadingFrom: url)
|
|
578
|
+
while autoreleasepool(invoking: {
|
|
579
|
+
let nextChunk = handle.readData(ofLength: 2048)
|
|
580
|
+
guard !nextChunk.isEmpty else { return false }
|
|
581
|
+
nextChunk.withUnsafeBytes {
|
|
582
|
+
_ = CC_SHA256_Update(&digestContext, $0.baseAddress, CC_LONG(nextChunk.count))
|
|
583
|
+
}
|
|
584
|
+
return true
|
|
585
|
+
}) { }
|
|
586
|
+
|
|
587
|
+
// Compute the digest
|
|
588
|
+
var digest = Data(count: Int(CC_SHA256_DIGEST_LENGTH))
|
|
589
|
+
digest.withUnsafeMutableBytes {
|
|
590
|
+
_ = CC_SHA256_Final($0.bindMemory(to: UInt8.self).baseAddress, &digestContext)
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
// Verify the signature
|
|
594
|
+
var secKeyVerifySignatureError: Unmanaged<CFError>?
|
|
595
|
+
let signatureAlgorithm = SecKeyAlgorithm.rsaSignatureDigestPKCS1v15SHA256
|
|
596
|
+
let verificationResult = SecKeyVerifySignature(publicKey, signatureAlgorithm, digest as CFData, signatureData as CFData, &secKeyVerifySignatureError)
|
|
597
|
+
if let error = secKeyVerifySignatureError?.takeRetainedValue() {
|
|
598
|
+
CAPLog.print("[", LiveUpdatePlugin.tag, "] ", "Failed to verify signature with error: \(error)")
|
|
599
|
+
}
|
|
600
|
+
return verificationResult
|
|
601
|
+
}
|
|
602
|
+
|
|
496
603
|
private func wasUpdated() -> Bool {
|
|
497
604
|
guard let lastBundleVersionString = preferences.getLastBundleVersion() else {
|
|
498
605
|
return false
|
|
@@ -8,7 +8,7 @@ import Capacitor
|
|
|
8
8
|
@objc(LiveUpdatePlugin)
|
|
9
9
|
public class LiveUpdatePlugin: CAPPlugin {
|
|
10
10
|
public static let tag = "LiveUpdate"
|
|
11
|
-
public static let version = "6.0
|
|
11
|
+
public static let version = "6.1.0"
|
|
12
12
|
public static let userDefaultsPrefix = "CapawesomeLiveUpdate" // DO NOT CHANGE
|
|
13
13
|
|
|
14
14
|
private var config: LiveUpdateConfig?
|
|
@@ -42,12 +42,13 @@ public class LiveUpdatePlugin: CAPPlugin {
|
|
|
42
42
|
call.reject(CustomError.bundleIdMissing.localizedDescription)
|
|
43
43
|
return
|
|
44
44
|
}
|
|
45
|
+
let checksum = call.getString("checksum")
|
|
45
46
|
guard let url = call.getString("url") else {
|
|
46
47
|
call.reject(CustomError.urlMissing.localizedDescription)
|
|
47
48
|
return
|
|
48
49
|
}
|
|
49
50
|
|
|
50
|
-
let options = DownloadBundleOptions(bundleId: bundleId, url: url)
|
|
51
|
+
let options = DownloadBundleOptions(bundleId: bundleId, checksum: checksum, url: url)
|
|
51
52
|
|
|
52
53
|
implementation?.downloadBundle(options, completion: { error in
|
|
53
54
|
if let error = error {
|
|
@@ -239,6 +240,7 @@ public class LiveUpdatePlugin: CAPPlugin {
|
|
|
239
240
|
config.appId = getConfig().getString("appId", config.appId)
|
|
240
241
|
config.autoDeleteBundles = getConfig().getBoolean("autoDeleteBundles", config.autoDeleteBundles)
|
|
241
242
|
config.enabled = getConfig().getBoolean("enabled", config.enabled)
|
|
243
|
+
config.publicKey = getConfig().getString("publicKey", config.publicKey)
|
|
242
244
|
config.readyTimeout = getConfig().getInt("readyTimeout", config.readyTimeout)
|
|
243
245
|
config.resetOnUpdate = getConfig().getBoolean("resetOnUpdate", config.resetOnUpdate)
|
|
244
246
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@capawesome/capacitor-live-update",
|
|
3
|
-
"version": "6.0
|
|
3
|
+
"version": "6.1.0",
|
|
4
4
|
"description": "Capacitor plugin to update your app remotely in real-time.",
|
|
5
5
|
"main": "dist/plugin.cjs.js",
|
|
6
6
|
"module": "dist/esm/index.js",
|
|
@@ -36,7 +36,14 @@
|
|
|
36
36
|
"keywords": [
|
|
37
37
|
"capacitor",
|
|
38
38
|
"plugin",
|
|
39
|
-
"native"
|
|
39
|
+
"native",
|
|
40
|
+
"ionic",
|
|
41
|
+
"android",
|
|
42
|
+
"ios",
|
|
43
|
+
"live update",
|
|
44
|
+
"remote update",
|
|
45
|
+
"ota",
|
|
46
|
+
"capgo"
|
|
40
47
|
],
|
|
41
48
|
"scripts": {
|
|
42
49
|
"verify": "npm run verify:ios && npm run verify:android && npm run verify:web",
|