@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.
@@ -16,5 +16,6 @@ Pod::Spec.new do |s|
16
16
  s.dependency 'SSZipArchive'
17
17
  s.dependency 'Alamofire'
18
18
  s.dependency 'Version'
19
+ s.dependency 'SwiftyRSA'
19
20
  s.swift_version = '5.1'
20
21
  end
@@ -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.11.12";
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", error);
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.11.12"
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.appId = Bundle.main.bundleIdentifier ?? ""
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")
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@capgo/capacitor-updater",
3
- "version": "4.11.12",
3
+ "version": "4.12.2",
4
4
  "license": "LGPL-3.0-only",
5
5
  "description": "OTA update for capacitor apps",
6
6
  "main": "dist/plugin.cjs.js",