@capgo/capacitor-updater 4.11.12 → 4.12.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CapgoCapacitorUpdater.podspec +1 -0
- package/android/src/main/java/ee/forgr/capacitor_updater/CapacitorUpdater.java +54 -5
- package/android/src/main/java/ee/forgr/capacitor_updater/CapacitorUpdaterPlugin.java +5 -0
- package/android/src/main/java/ee/forgr/capacitor_updater/RSACipher.java +57 -0
- package/dist/docs.json +12 -0
- package/dist/esm/definitions.d.ts +8 -0
- package/ios/Plugin/CapacitorUpdater.swift +33 -1
- package/ios/Plugin/CapacitorUpdaterPlugin.swift +3 -3
- package/package.json +1 -1
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
package ee.forgr.capacitor_updater;
|
|
2
2
|
|
|
3
|
+
import static ee.forgr.capacitor_updater.RSACipher.stringToPrivateKey;
|
|
4
|
+
|
|
3
5
|
import android.content.SharedPreferences;
|
|
4
6
|
import android.os.Build;
|
|
5
7
|
import android.util.Log;
|
|
@@ -9,6 +11,7 @@ import com.android.volley.Response;
|
|
|
9
11
|
import com.android.volley.VolleyError;
|
|
10
12
|
import com.android.volley.toolbox.JsonObjectRequest;
|
|
11
13
|
import com.getcapacitor.JSObject;
|
|
14
|
+
import com.getcapacitor.android.BuildConfig;
|
|
12
15
|
import com.getcapacitor.plugin.WebView;
|
|
13
16
|
import java.io.BufferedInputStream;
|
|
14
17
|
import java.io.DataInputStream;
|
|
@@ -21,6 +24,9 @@ import java.io.IOException;
|
|
|
21
24
|
import java.io.InputStream;
|
|
22
25
|
import java.net.URL;
|
|
23
26
|
import java.net.URLConnection;
|
|
27
|
+
import java.security.InvalidKeyException;
|
|
28
|
+
import java.security.NoSuchAlgorithmException;
|
|
29
|
+
import java.security.PrivateKey;
|
|
24
30
|
import java.security.SecureRandom;
|
|
25
31
|
import java.util.ArrayList;
|
|
26
32
|
import java.util.Date;
|
|
@@ -29,6 +35,9 @@ import java.util.List;
|
|
|
29
35
|
import java.util.zip.CRC32;
|
|
30
36
|
import java.util.zip.ZipEntry;
|
|
31
37
|
import java.util.zip.ZipInputStream;
|
|
38
|
+
import javax.crypto.BadPaddingException;
|
|
39
|
+
import javax.crypto.IllegalBlockSizeException;
|
|
40
|
+
import javax.crypto.NoSuchPaddingException;
|
|
32
41
|
import org.json.JSONException;
|
|
33
42
|
import org.json.JSONObject;
|
|
34
43
|
|
|
@@ -52,7 +61,7 @@ public class CapacitorUpdater {
|
|
|
52
61
|
private static final String bundleDirectory = "versions";
|
|
53
62
|
|
|
54
63
|
public static final String TAG = "Capacitor-updater";
|
|
55
|
-
public static final String pluginVersion = "4.
|
|
64
|
+
public static final String pluginVersion = "4.12.2";
|
|
56
65
|
|
|
57
66
|
public SharedPreferences.Editor editor;
|
|
58
67
|
public SharedPreferences prefs;
|
|
@@ -68,6 +77,7 @@ public class CapacitorUpdater {
|
|
|
68
77
|
public String statsUrl = "";
|
|
69
78
|
public String channelUrl = "";
|
|
70
79
|
public String appId = "";
|
|
80
|
+
public String privateKey = "";
|
|
71
81
|
public String deviceID = "";
|
|
72
82
|
|
|
73
83
|
private final FilenameFilter filter = new FilenameFilter() {
|
|
@@ -254,6 +264,44 @@ public class CapacitorUpdater {
|
|
|
254
264
|
return enc.toLowerCase();
|
|
255
265
|
}
|
|
256
266
|
|
|
267
|
+
private void decodeFile(File file) throws IOException {
|
|
268
|
+
if (this.privateKey.equals("")) {
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
try {
|
|
272
|
+
PrivateKey pKey = RSACipher.stringToPrivateKey(this.privateKey);
|
|
273
|
+
FileInputStream fis = new FileInputStream(file.getAbsolutePath());
|
|
274
|
+
byte[] buffer = new byte[10];
|
|
275
|
+
StringBuilder sb = new StringBuilder();
|
|
276
|
+
while (fis.read(buffer) != -1) {
|
|
277
|
+
sb.append(new String(buffer));
|
|
278
|
+
buffer = new byte[10];
|
|
279
|
+
}
|
|
280
|
+
fis.close();
|
|
281
|
+
String content = sb.toString();
|
|
282
|
+
String decrypted = RSACipher.decryptRSA(content, pKey);
|
|
283
|
+
// write the decrypted string to the file
|
|
284
|
+
FileOutputStream fos = new FileOutputStream(file.getAbsolutePath());
|
|
285
|
+
fos.write(decrypted.getBytes());
|
|
286
|
+
fos.close();
|
|
287
|
+
} catch (NoSuchPaddingException e) {
|
|
288
|
+
e.printStackTrace();
|
|
289
|
+
throw new IOException("NoSuchPaddingException");
|
|
290
|
+
} catch (IllegalBlockSizeException e) {
|
|
291
|
+
e.printStackTrace();
|
|
292
|
+
throw new IOException("IllegalBlockSizeException");
|
|
293
|
+
} catch (NoSuchAlgorithmException e) {
|
|
294
|
+
e.printStackTrace();
|
|
295
|
+
throw new IOException("NoSuchAlgorithmException");
|
|
296
|
+
} catch (BadPaddingException e) {
|
|
297
|
+
e.printStackTrace();
|
|
298
|
+
throw new IOException("BadPaddingException");
|
|
299
|
+
} catch (InvalidKeyException e) {
|
|
300
|
+
e.printStackTrace();
|
|
301
|
+
throw new IOException("InvalidKeyException");
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
257
305
|
public BundleInfo download(final String url, final String version) throws IOException {
|
|
258
306
|
final String id = this.randomString(10);
|
|
259
307
|
this.saveBundleInfo(id, new BundleInfo(id, version, BundleStatus.DOWNLOADING, new Date(System.currentTimeMillis()), ""));
|
|
@@ -262,6 +310,7 @@ public class CapacitorUpdater {
|
|
|
262
310
|
this.notifyDownload(id, 5);
|
|
263
311
|
final File downloaded = this.downloadFile(id, url, this.randomString(10));
|
|
264
312
|
final String checksum = this.getChecksum(downloaded);
|
|
313
|
+
this.decodeFile(downloaded);
|
|
265
314
|
this.notifyDownload(id, 71);
|
|
266
315
|
final File unzipped = this.unzip(id, downloaded, this.randomString(10));
|
|
267
316
|
downloaded.delete();
|
|
@@ -420,7 +469,7 @@ public class CapacitorUpdater {
|
|
|
420
469
|
new Response.ErrorListener() {
|
|
421
470
|
@Override
|
|
422
471
|
public void onErrorResponse(VolleyError error) {
|
|
423
|
-
Log.e(TAG, "Error getting Latest"
|
|
472
|
+
Log.e(TAG, "Error getting Latest" + error.toString());
|
|
424
473
|
}
|
|
425
474
|
}
|
|
426
475
|
);
|
|
@@ -467,7 +516,7 @@ public class CapacitorUpdater {
|
|
|
467
516
|
new Response.ErrorListener() {
|
|
468
517
|
@Override
|
|
469
518
|
public void onErrorResponse(VolleyError error) {
|
|
470
|
-
Log.e(TAG, "Error set channel: " + error);
|
|
519
|
+
Log.e(TAG, "Error set channel: " + error.toString());
|
|
471
520
|
}
|
|
472
521
|
}
|
|
473
522
|
);
|
|
@@ -513,7 +562,7 @@ public class CapacitorUpdater {
|
|
|
513
562
|
new Response.ErrorListener() {
|
|
514
563
|
@Override
|
|
515
564
|
public void onErrorResponse(VolleyError error) {
|
|
516
|
-
Log.e(TAG, "Error get channel: " + error);
|
|
565
|
+
Log.e(TAG, "Error get channel: " + error.toString());
|
|
517
566
|
}
|
|
518
567
|
}
|
|
519
568
|
);
|
|
@@ -547,7 +596,7 @@ public class CapacitorUpdater {
|
|
|
547
596
|
new Response.ErrorListener() {
|
|
548
597
|
@Override
|
|
549
598
|
public void onErrorResponse(VolleyError error) {
|
|
550
|
-
Log.e(TAG, "Error sending stats: " + error);
|
|
599
|
+
Log.e(TAG, "Error sending stats: " + error.toString());
|
|
551
600
|
}
|
|
552
601
|
}
|
|
553
602
|
);
|
|
@@ -88,6 +88,7 @@ public class CapacitorUpdaterPlugin extends Plugin implements Application.Activi
|
|
|
88
88
|
|
|
89
89
|
final CapConfig config = CapConfig.loadDefault(this.getActivity());
|
|
90
90
|
this.implementation.appId = config.getString("appId", "");
|
|
91
|
+
this.implementation.privateKey = this.getConfig().getString("privateKey", "");
|
|
91
92
|
this.implementation.statsUrl = this.getConfig().getString("statsUrl", statsUrlDefault);
|
|
92
93
|
this.implementation.channelUrl = this.getConfig().getString("channelUrl", channelUrlDefault);
|
|
93
94
|
this.implementation.documentsDir = this.getContext().getFilesDir();
|
|
@@ -771,6 +772,10 @@ public class CapacitorUpdaterPlugin extends Plugin implements Application.Activi
|
|
|
771
772
|
CapacitorUpdater.TAG,
|
|
772
773
|
"Error checksum " + next.getChecksum() + " " + checksum
|
|
773
774
|
);
|
|
775
|
+
CapacitorUpdaterPlugin.this.implementation.sendStats(
|
|
776
|
+
"checksum_fail",
|
|
777
|
+
current.getVersionName()
|
|
778
|
+
);
|
|
774
779
|
final Boolean res =
|
|
775
780
|
CapacitorUpdaterPlugin.this.implementation.delete(next.getId());
|
|
776
781
|
if (res) {
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
package ee.forgr.capacitor_updater;
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Created by Awesometic
|
|
5
|
+
* It's encrypt returns Base64 encoded, and also decrypt for Base64 encoded cipher
|
|
6
|
+
* references: http://stackoverflow.com/questions/12471999/rsa-encryption-decryption-in-android
|
|
7
|
+
*/
|
|
8
|
+
import android.util.Base64;
|
|
9
|
+
import java.security.InvalidKeyException;
|
|
10
|
+
import java.security.KeyFactory;
|
|
11
|
+
import java.security.NoSuchAlgorithmException;
|
|
12
|
+
import java.security.PrivateKey;
|
|
13
|
+
import java.security.spec.InvalidKeySpecException;
|
|
14
|
+
import java.security.spec.PKCS8EncodedKeySpec;
|
|
15
|
+
import javax.crypto.BadPaddingException;
|
|
16
|
+
import javax.crypto.Cipher;
|
|
17
|
+
import javax.crypto.IllegalBlockSizeException;
|
|
18
|
+
import javax.crypto.NoSuchPaddingException;
|
|
19
|
+
|
|
20
|
+
public class RSACipher {
|
|
21
|
+
|
|
22
|
+
public static String decryptRSA(String source, PrivateKey privateKey)
|
|
23
|
+
throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, IllegalBlockSizeException, BadPaddingException {
|
|
24
|
+
Cipher cipher = Cipher.getInstance("RSA/ECB/OAEPWithSHA1AndMGF1Padding");
|
|
25
|
+
cipher.init(Cipher.DECRYPT_MODE, privateKey);
|
|
26
|
+
byte[] decryptedBytes = cipher.doFinal(Base64.decode(source, Base64.DEFAULT));
|
|
27
|
+
String decrypted = new String(decryptedBytes);
|
|
28
|
+
|
|
29
|
+
return decrypted;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
public static PrivateKey stringToPrivateKey(String private_key)
|
|
33
|
+
throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, IllegalBlockSizeException, BadPaddingException {
|
|
34
|
+
try {
|
|
35
|
+
// Remove the "BEGIN" and "END" lines, as well as any whitespace
|
|
36
|
+
String pkcs8Pem = private_key.toString();
|
|
37
|
+
pkcs8Pem = pkcs8Pem.replace("-----BEGIN PRIVATE KEY-----", "");
|
|
38
|
+
pkcs8Pem = pkcs8Pem.replace("-----END PRIVATE KEY-----", "");
|
|
39
|
+
pkcs8Pem = pkcs8Pem.replaceAll("\\s+", "");
|
|
40
|
+
|
|
41
|
+
// Base64 decode the result
|
|
42
|
+
|
|
43
|
+
byte[] pkcs8EncodedBytes = Base64.decode(pkcs8Pem, Base64.DEFAULT);
|
|
44
|
+
|
|
45
|
+
// extract the private key
|
|
46
|
+
|
|
47
|
+
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(pkcs8EncodedBytes);
|
|
48
|
+
KeyFactory kf = KeyFactory.getInstance("RSA");
|
|
49
|
+
PrivateKey privKey = kf.generatePrivate(keySpec);
|
|
50
|
+
return privKey;
|
|
51
|
+
} catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
|
|
52
|
+
e.printStackTrace();
|
|
53
|
+
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
package/dist/docs.json
CHANGED
|
@@ -1442,6 +1442,18 @@
|
|
|
1442
1442
|
"docs": "Configure the URL / endpoint to which update statistics are sent.\n\nOnly available for Android and iOS. Set to \"\" to disable stats reporting.",
|
|
1443
1443
|
"complexTypes": [],
|
|
1444
1444
|
"type": "string | undefined"
|
|
1445
|
+
},
|
|
1446
|
+
{
|
|
1447
|
+
"name": "privateKey",
|
|
1448
|
+
"tags": [
|
|
1449
|
+
{
|
|
1450
|
+
"text": "undefined",
|
|
1451
|
+
"name": "default"
|
|
1452
|
+
}
|
|
1453
|
+
],
|
|
1454
|
+
"docs": "Configure the private key for end to end live update encryption.\n\nOnly available for Android and iOS.",
|
|
1455
|
+
"complexTypes": [],
|
|
1456
|
+
"type": "string | undefined"
|
|
1445
1457
|
}
|
|
1446
1458
|
],
|
|
1447
1459
|
"docs": "These configuration values are available:"
|
|
@@ -68,6 +68,14 @@ declare module '@capacitor/cli' {
|
|
|
68
68
|
* @example https://example.com/api/stats
|
|
69
69
|
*/
|
|
70
70
|
statsUrl?: string;
|
|
71
|
+
/**
|
|
72
|
+
* Configure the private key for end to end live update encryption.
|
|
73
|
+
*
|
|
74
|
+
* Only available for Android and iOS.
|
|
75
|
+
*
|
|
76
|
+
* @default undefined
|
|
77
|
+
*/
|
|
78
|
+
privateKey?: string;
|
|
71
79
|
};
|
|
72
80
|
}
|
|
73
81
|
}
|
|
@@ -2,6 +2,7 @@ import Foundation
|
|
|
2
2
|
import SSZipArchive
|
|
3
3
|
import Alamofire
|
|
4
4
|
import zlib
|
|
5
|
+
import SwiftyRSA
|
|
5
6
|
|
|
6
7
|
extension URL {
|
|
7
8
|
var isDirectory: Bool {
|
|
@@ -151,6 +152,8 @@ extension String {
|
|
|
151
152
|
enum CustomError: Error {
|
|
152
153
|
// Throw when an unzip fail
|
|
153
154
|
case cannotUnzip
|
|
155
|
+
case cannotWrite
|
|
156
|
+
case cannotDecode
|
|
154
157
|
case cannotUnflat
|
|
155
158
|
case cannotCreateDirectory
|
|
156
159
|
case cannotDeleteDirectory
|
|
@@ -187,6 +190,16 @@ extension CustomError: LocalizedError {
|
|
|
187
190
|
"An unexpected error occurred.",
|
|
188
191
|
comment: "Unexpected Error"
|
|
189
192
|
)
|
|
193
|
+
case .cannotDecode:
|
|
194
|
+
return NSLocalizedString(
|
|
195
|
+
"Decoding the zip failed with this key",
|
|
196
|
+
comment: "Invalid private key"
|
|
197
|
+
)
|
|
198
|
+
case .cannotWrite:
|
|
199
|
+
return NSLocalizedString(
|
|
200
|
+
"Cannot write to the destination",
|
|
201
|
+
comment: "Invalid destination"
|
|
202
|
+
)
|
|
190
203
|
}
|
|
191
204
|
}
|
|
192
205
|
}
|
|
@@ -208,11 +221,12 @@ extension CustomError: LocalizedError {
|
|
|
208
221
|
public let TAG = "✨ Capacitor-updater:"
|
|
209
222
|
public let CAP_SERVER_PATH = "serverBasePath"
|
|
210
223
|
public var customId = ""
|
|
211
|
-
public let pluginVersion = "4.
|
|
224
|
+
public let pluginVersion = "4.12.2"
|
|
212
225
|
public var statsUrl = ""
|
|
213
226
|
public var channelUrl = ""
|
|
214
227
|
public var appId = ""
|
|
215
228
|
public var deviceID = UIDevice.current.identifierForVendor?.uuidString ?? ""
|
|
229
|
+
public var privateKey = ""
|
|
216
230
|
|
|
217
231
|
public var notifyDownload: (String, Int) -> Void = { _, _ in }
|
|
218
232
|
|
|
@@ -311,6 +325,23 @@ extension CustomError: LocalizedError {
|
|
|
311
325
|
}
|
|
312
326
|
}
|
|
313
327
|
|
|
328
|
+
private func decodeFile(filePath: URL) throws {
|
|
329
|
+
if self.privateKey == "" {
|
|
330
|
+
return
|
|
331
|
+
}
|
|
332
|
+
do {
|
|
333
|
+
let privateKey = try PrivateKey(base64Encoded: self.privateKey)
|
|
334
|
+
let base64String = try Data(contentsOf: filePath).base64EncodedString()
|
|
335
|
+
let encrypted = try EncryptedMessage(base64Encoded: base64String)
|
|
336
|
+
let clear = try encrypted.decrypted(with: privateKey, padding: .PKCS1)
|
|
337
|
+
let str = try clear.string(encoding: String.Encoding.utf8)
|
|
338
|
+
try str.write(to: filePath, atomically: true, encoding: String.Encoding.utf8)
|
|
339
|
+
} catch {
|
|
340
|
+
print("\(self.TAG) Cannot decode: \(filePath.path)", error)
|
|
341
|
+
throw CustomError.cannotDecode
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
314
345
|
private func saveDownloaded(sourceZip: URL, id: String, base: URL) throws {
|
|
315
346
|
try prepareFolder(source: base)
|
|
316
347
|
let destHot = base.appendingPathComponent(id)
|
|
@@ -405,6 +436,7 @@ extension CustomError: LocalizedError {
|
|
|
405
436
|
self.notifyDownload(id, 71)
|
|
406
437
|
do {
|
|
407
438
|
checksum = self.getChecksum(filePath: fileURL)
|
|
439
|
+
try self.decodeFile(filePath: fileURL)
|
|
408
440
|
try self.saveDownloaded(sourceZip: fileURL, id: id, base: self.documentsDir.appendingPathComponent(self.bundleDirectoryHot))
|
|
409
441
|
self.notifyDownload(id, 85)
|
|
410
442
|
try self.saveDownloaded(sourceZip: fileURL, id: id, base: self.libraryDir.appendingPathComponent(self.bundleDirectory))
|
|
@@ -39,7 +39,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin {
|
|
|
39
39
|
appReadyTimeout = getConfig().getInt("appReadyTimeout", 10000)
|
|
40
40
|
resetWhenUpdate = getConfig().getBoolean("resetWhenUpdate", true)
|
|
41
41
|
|
|
42
|
-
implementation.
|
|
42
|
+
implementation.privateKey = getConfig().getString("privateKey", "")!
|
|
43
43
|
implementation.notifyDownload = notifyDownload
|
|
44
44
|
let config = (self.bridge?.viewController as? CAPBridgeViewController)?.instanceDescriptor().legacyConfig
|
|
45
45
|
if config?["appId"] != nil {
|
|
@@ -85,8 +85,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin {
|
|
|
85
85
|
if percent == 100 {
|
|
86
86
|
self.notifyListeners("downloadComplete", data: ["bundle": bundle.toJSON()])
|
|
87
87
|
self.implementation.sendStats(action: "download_complete", versionName: bundle.getVersionName())
|
|
88
|
-
}
|
|
89
|
-
else if percent.isMultiple(of: 10) {
|
|
88
|
+
} else if percent.isMultiple(of: 10) {
|
|
90
89
|
self.implementation.sendStats(action: "download_\(percent)", versionName: bundle.getVersionName())
|
|
91
90
|
}
|
|
92
91
|
}
|
|
@@ -513,6 +512,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin {
|
|
|
513
512
|
let next = try self.implementation.download(url: downloadUrl, version: latestVersionName)
|
|
514
513
|
if res.checksum != "" && next.getChecksum() != res.checksum {
|
|
515
514
|
print("\(self.implementation.TAG) Error checksum", next.getChecksum(), res.checksum)
|
|
515
|
+
self.implementation.sendStats(action: "checksum_fail", versionName: next.getVersionName())
|
|
516
516
|
let resDel = self.implementation.delete(id: next.getId())
|
|
517
517
|
if !resDel {
|
|
518
518
|
print("\(self.implementation.TAG) Delete failed, id \(next.getId()) doesn't exist")
|