@capawesome/capacitor-live-update 6.0.8 → 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 +11 -5
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/LiveUpdate.java +97 -2
- 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 +38 -6
- package/dist/esm/definitions.d.ts +25 -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 +112 -4
- 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 +1 -1
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
|
},
|
|
@@ -530,10 +535,11 @@ Only available on Android and iOS.
|
|
|
530
535
|
|
|
531
536
|
#### DownloadBundleOptions
|
|
532
537
|
|
|
533
|
-
| Prop | Type | Description
|
|
534
|
-
| -------------- | ------------------- |
|
|
535
|
-
| **`
|
|
536
|
-
| **`
|
|
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 |
|
|
537
543
|
|
|
538
544
|
|
|
539
545
|
#### GetBundleResult
|
|
@@ -630,7 +636,7 @@ If you want to disable the plugin to test other parts of your app, you can set t
|
|
|
630
636
|
|
|
631
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).
|
|
632
638
|
If you change native code, such as adding a new plugin, you need to resubmit your app to the app stores.
|
|
633
|
-
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.
|
|
634
640
|
|
|
635
641
|
## Changelog
|
|
636
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) {
|
|
@@ -215,6 +222,8 @@ public class LiveUpdate {
|
|
|
215
222
|
@Override
|
|
216
223
|
public void success(@NonNull GetLatestBundleResponse result) {
|
|
217
224
|
String latestBundleId = result.getBundleId();
|
|
225
|
+
String checksum = result.getChecksum();
|
|
226
|
+
String signature = result.getSignature();
|
|
218
227
|
String url = result.getUrl();
|
|
219
228
|
|
|
220
229
|
// Check if the bundle already exists
|
|
@@ -233,6 +242,8 @@ public class LiveUpdate {
|
|
|
233
242
|
// Download and unzip the bundle
|
|
234
243
|
downloadBundle(
|
|
235
244
|
latestBundleId,
|
|
245
|
+
checksum,
|
|
246
|
+
signature,
|
|
236
247
|
url,
|
|
237
248
|
new EmptyCallback() {
|
|
238
249
|
@Override
|
|
@@ -290,6 +301,31 @@ public class LiveUpdate {
|
|
|
290
301
|
return new File(plugin.getContext().getFilesDir(), bundlesDirectory + "/" + bundle);
|
|
291
302
|
}
|
|
292
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
|
+
|
|
293
329
|
private void createBundlesDirectory() {
|
|
294
330
|
File bundlesDirectory = buildBundlesDirectory();
|
|
295
331
|
if (!bundlesDirectory.exists()) {
|
|
@@ -297,6 +333,19 @@ public class LiveUpdate {
|
|
|
297
333
|
}
|
|
298
334
|
}
|
|
299
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
|
+
|
|
300
349
|
private void deleteBundle(@NonNull String bundleId) {
|
|
301
350
|
File bundleDirectory = buildBundleDirectoryFor(bundleId);
|
|
302
351
|
deleteFileRecursively(bundleDirectory);
|
|
@@ -320,7 +369,13 @@ public class LiveUpdate {
|
|
|
320
369
|
}
|
|
321
370
|
}
|
|
322
371
|
|
|
323
|
-
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
|
+
) {
|
|
324
379
|
downloadFile(
|
|
325
380
|
url,
|
|
326
381
|
new NonEmptyCallback<File>() {
|
|
@@ -332,6 +387,28 @@ public class LiveUpdate {
|
|
|
332
387
|
@Override
|
|
333
388
|
public void success(@NonNull File result) {
|
|
334
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
|
+
|
|
335
412
|
// Add the bundle
|
|
336
413
|
addBundle(bundleId, result);
|
|
337
414
|
|
|
@@ -617,6 +694,24 @@ public class LiveUpdate {
|
|
|
617
694
|
return destination;
|
|
618
695
|
}
|
|
619
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
|
+
|
|
620
715
|
private boolean wasUpdated() throws PackageManager.NameNotFoundException {
|
|
621
716
|
int lastVersionCode = preferences.getLastVersionCode();
|
|
622
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
|
@@ -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
|
*
|
|
@@ -193,6 +202,22 @@ export interface DeleteBundleOptions {
|
|
|
193
202
|
bundleId: string;
|
|
194
203
|
}
|
|
195
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;
|
|
196
221
|
/**
|
|
197
222
|
* The URL of the bundle to download.
|
|
198
223
|
*
|
|
@@ -202,13 +227,6 @@ export interface DownloadBundleOptions {
|
|
|
202
227
|
* @example 'https://example.com/bundle.zip'
|
|
203
228
|
*/
|
|
204
229
|
url: string;
|
|
205
|
-
/**
|
|
206
|
-
* The unique identifier of the bundle.
|
|
207
|
-
*
|
|
208
|
-
* @since 5.0.0
|
|
209
|
-
* @example '1.0.0'
|
|
210
|
-
*/
|
|
211
|
-
bundleId: string;
|
|
212
230
|
}
|
|
213
231
|
/**
|
|
214
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 * **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
|
|
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
|
|
@@ -187,7 +190,7 @@ import Alamofire
|
|
|
187
190
|
completion(result, nil)
|
|
188
191
|
} else {
|
|
189
192
|
// Download and unzip the bundle
|
|
190
|
-
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
|
|
191
194
|
if let error = error {
|
|
192
195
|
completion(nil, error)
|
|
193
196
|
return
|
|
@@ -241,6 +244,20 @@ import Alamofire
|
|
|
241
244
|
return url.path
|
|
242
245
|
}
|
|
243
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
|
+
|
|
244
261
|
private func createBundlesDirectory() {
|
|
245
262
|
let bundlesDirectoryUrl = libraryDirectoryUrl.appendingPathComponent(bundlesDirectory)
|
|
246
263
|
let exists = FileManager.default.fileExists(atPath: bundlesDirectoryUrl.path)
|
|
@@ -274,7 +291,7 @@ import Alamofire
|
|
|
274
291
|
}
|
|
275
292
|
}
|
|
276
293
|
|
|
277
|
-
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) {
|
|
278
295
|
// Download the bundle
|
|
279
296
|
downloadFile(url: url, completion: { url, error in
|
|
280
297
|
if let error = error {
|
|
@@ -282,6 +299,39 @@ import Alamofire
|
|
|
282
299
|
return
|
|
283
300
|
}
|
|
284
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
|
+
|
|
285
335
|
// Add the bundle
|
|
286
336
|
self.addBundle(bundleId: bundleId, zipFile: url, completion: { error in
|
|
287
337
|
// Delete the temporary file
|
|
@@ -492,6 +542,64 @@ import Alamofire
|
|
|
492
542
|
})
|
|
493
543
|
}
|
|
494
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
|
+
|
|
495
603
|
private func wasUpdated() -> Bool {
|
|
496
604
|
guard let lastBundleVersionString = preferences.getLastBundleVersion() else {
|
|
497
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
|
|