@capgo/cordova-updater 8.1.1

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.
Files changed (76) hide show
  1. package/LICENSE +373 -0
  2. package/README.md +82 -0
  3. package/dist/esm/definitions.d.ts +2684 -0
  4. package/dist/esm/definitions.js +109 -0
  5. package/dist/esm/definitions.js.map +1 -0
  6. package/dist/esm/exec.d.ts +4 -0
  7. package/dist/esm/exec.js +25 -0
  8. package/dist/esm/exec.js.map +1 -0
  9. package/dist/esm/history.d.ts +1 -0
  10. package/dist/esm/history.js +283 -0
  11. package/dist/esm/history.js.map +1 -0
  12. package/dist/esm/index.d.ts +57 -0
  13. package/dist/esm/index.js +68 -0
  14. package/dist/esm/index.js.map +1 -0
  15. package/dist/esm/plugin.d.ts +57 -0
  16. package/dist/esm/plugin.js +15 -0
  17. package/dist/esm/plugin.js.map +1 -0
  18. package/dist/plugin.cjs.js +217 -0
  19. package/dist/plugin.cjs.js.map +1 -0
  20. package/dist/plugin.js +222 -0
  21. package/dist/plugin.js.map +1 -0
  22. package/package.json +96 -0
  23. package/plugin.xml +207 -0
  24. package/src/android/CordovaUpdaterPlugin.java +5662 -0
  25. package/src/android/UpdaterPathHandler.java +99 -0
  26. package/src/android/app/capgo/cordova/updater/AndroidAppExitReporter.java +92 -0
  27. package/src/android/app/capgo/cordova/updater/AppLifecycleObserver.java +115 -0
  28. package/src/android/app/capgo/cordova/updater/BundleInfo.java +246 -0
  29. package/src/android/app/capgo/cordova/updater/BundleStatus.java +45 -0
  30. package/src/android/app/capgo/cordova/updater/Callback.java +13 -0
  31. package/src/android/app/capgo/cordova/updater/CapgoUpdater.java +3404 -0
  32. package/src/android/app/capgo/cordova/updater/CordovaUpdaterConfig.java +56 -0
  33. package/src/android/app/capgo/cordova/updater/CryptoCipher.java +462 -0
  34. package/src/android/app/capgo/cordova/updater/DataManager.java +44 -0
  35. package/src/android/app/capgo/cordova/updater/DelayCondition.java +56 -0
  36. package/src/android/app/capgo/cordova/updater/DelayUntilNext.java +14 -0
  37. package/src/android/app/capgo/cordova/updater/DelayUpdateUtils.java +307 -0
  38. package/src/android/app/capgo/cordova/updater/DeviceIdHelper.java +230 -0
  39. package/src/android/app/capgo/cordova/updater/DownloadService.java +1219 -0
  40. package/src/android/app/capgo/cordova/updater/DownloadWorkerManager.java +260 -0
  41. package/src/android/app/capgo/cordova/updater/InternalUtils.java +54 -0
  42. package/src/android/app/capgo/cordova/updater/Logger.java +345 -0
  43. package/src/android/app/capgo/cordova/updater/ShakeDetector.java +72 -0
  44. package/src/android/app/capgo/cordova/updater/ShakeMenu.java +39 -0
  45. package/src/android/app/capgo/cordova/updater/ThreeFingerPinchDetector.java +323 -0
  46. package/src/android/app/capgo/cordova/updater/compat/JSArray.java +9 -0
  47. package/src/android/app/capgo/cordova/updater/compat/JSObject.java +83 -0
  48. package/src/android/app/capgo/cordova/updater/compat/PluginCall.java +165 -0
  49. package/src/android/capgo-cordova-updater.gradle +19 -0
  50. package/src/android/capgo-updater.xml +3 -0
  51. package/src/android/test/app/capgo/cordova/updater/CapacitorUpdaterUnitTest.java.skip +3039 -0
  52. package/src/android/test/app/capgo/cordova/updater/DelayUpdateUtilsTest.java +177 -0
  53. package/src/android/test/app/capgo/cordova/updater/NativeContractTest.java +105 -0
  54. package/src/ios/AES.swift +216 -0
  55. package/src/ios/AppHealthTracker.swift +82 -0
  56. package/src/ios/BundleInfo.swift +185 -0
  57. package/src/ios/BundleStatus.swift +127 -0
  58. package/src/ios/CapgoRawRsa.swift +36 -0
  59. package/src/ios/CapgoUpdater.swift +3544 -0
  60. package/src/ios/CordovaCompat.swift +11 -0
  61. package/src/ios/CordovaPluginCall.swift +190 -0
  62. package/src/ios/CordovaUpdaterCommands.swift +270 -0
  63. package/src/ios/CordovaUpdaterPlugin.swift +4706 -0
  64. package/src/ios/CryptoCipher.swift +326 -0
  65. package/src/ios/DelayCondition.swift +74 -0
  66. package/src/ios/DelayUntilNext.swift +30 -0
  67. package/src/ios/DelayUpdateUtils.swift +257 -0
  68. package/src/ios/DeviceIdHelper.swift +120 -0
  69. package/src/ios/Info.plist +28 -0
  70. package/src/ios/InternalUtils.swift +471 -0
  71. package/src/ios/Logger.swift +283 -0
  72. package/src/ios/RSA.swift +96 -0
  73. package/src/ios/ShakeMenu.swift +52 -0
  74. package/src/ios/UpdaterSchemeHandler.swift +81 -0
  75. package/src/ios/UserDefaultsExtension.swift +46 -0
  76. package/src/ios/WebViewStatsReporter.swift +303 -0
@@ -0,0 +1,56 @@
1
+ package app.capgo.cordova.updater;
2
+
3
+ import android.content.Context;
4
+ import android.content.SharedPreferences;
5
+ import android.content.res.Resources;
6
+ import androidx.annotation.Nullable;
7
+
8
+ public class CordovaUpdaterConfig {
9
+ private final Context context;
10
+ private final SharedPreferences preferences;
11
+
12
+ public CordovaUpdaterConfig(Context context) {
13
+ this.context = context.getApplicationContext();
14
+ this.preferences = this.context.getSharedPreferences("CapgoCordovaUpdaterConfig", Context.MODE_PRIVATE);
15
+ }
16
+
17
+ @Nullable
18
+ public String getString(String key, @Nullable String defaultValue) {
19
+ String fromPrefs = preferences.getString(prefKey(key), null);
20
+ if (fromPrefs != null && !fromPrefs.trim().isEmpty()) {
21
+ return fromPrefs.trim();
22
+ }
23
+ int resId = context.getResources().getIdentifier(stringResName(key), "string", context.getPackageName());
24
+ if (resId != 0) {
25
+ String value = context.getString(resId);
26
+ if (value != null && !value.trim().isEmpty()) {
27
+ return value.trim();
28
+ }
29
+ }
30
+ return defaultValue;
31
+ }
32
+
33
+ public boolean getBoolean(String key, boolean defaultValue) {
34
+ String value = getString(key, null);
35
+ if (value == null) return defaultValue;
36
+ return Boolean.parseBoolean(value);
37
+ }
38
+
39
+ public int getInt(String key, int defaultValue) {
40
+ String value = getString(key, null);
41
+ if (value == null) return defaultValue;
42
+ try { return Integer.parseInt(value.trim()); } catch (NumberFormatException e) { return defaultValue; }
43
+ }
44
+
45
+ private String prefKey(String key) {
46
+ return "capgo_updater_" + key;
47
+ }
48
+
49
+ private String stringResName(String key) {
50
+ return "capgo_updater_" + camelToSnake(key);
51
+ }
52
+
53
+ private String camelToSnake(String input) {
54
+ return input.replaceAll("([a-z])([A-Z])", "$1_$2").toLowerCase();
55
+ }
56
+ }
@@ -0,0 +1,462 @@
1
+ /*
2
+ * This Source Code Form is subject to the terms of the Mozilla Public
3
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
4
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
5
+ */
6
+
7
+ package app.capgo.cordova.updater;
8
+
9
+ /**
10
+ * Created by Awesometic
11
+ * It's encrypt returns Base64 encoded, and also decrypt for Base64 encoded cipher
12
+ * references: http://stackoverflow.com/questions/12471999/rsa-encryption-decryption-in-android
13
+ */
14
+ import android.util.Base64;
15
+ import java.io.File;
16
+ import java.io.FileInputStream;
17
+ import java.io.FileOutputStream;
18
+ import java.io.IOException;
19
+ import java.io.InputStream;
20
+ import java.security.GeneralSecurityException;
21
+ import java.security.InvalidAlgorithmParameterException;
22
+ import java.security.InvalidKeyException;
23
+ import java.security.KeyFactory;
24
+ import java.security.MessageDigest;
25
+ import java.security.NoSuchAlgorithmException;
26
+ import java.security.PublicKey;
27
+ import java.security.spec.InvalidKeySpecException;
28
+ import java.security.spec.X509EncodedKeySpec;
29
+ import javax.crypto.BadPaddingException;
30
+ import javax.crypto.Cipher;
31
+ import javax.crypto.IllegalBlockSizeException;
32
+ import javax.crypto.NoSuchPaddingException;
33
+ import javax.crypto.SecretKey;
34
+ import javax.crypto.spec.IvParameterSpec;
35
+ import javax.crypto.spec.SecretKeySpec;
36
+
37
+ public class CryptoCipher {
38
+
39
+ private static Logger logger;
40
+
41
+ public static void setLogger(Logger loggerInstance) {
42
+ logger = loggerInstance;
43
+ }
44
+
45
+ public static byte[] decryptRSA(byte[] source, PublicKey publicKey)
46
+ throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidAlgorithmParameterException, InvalidKeyException, IllegalBlockSizeException, BadPaddingException {
47
+ Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
48
+ cipher.init(Cipher.DECRYPT_MODE, publicKey);
49
+ byte[] decryptedBytes = cipher.doFinal(source);
50
+ return decryptedBytes;
51
+ }
52
+
53
+ public static byte[] decryptAES(byte[] cipherText, SecretKey key, byte[] iv) {
54
+ try {
55
+ IvParameterSpec ivParameterSpec = new IvParameterSpec(iv);
56
+ Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
57
+ SecretKeySpec keySpec = new SecretKeySpec(key.getEncoded(), "AES");
58
+ cipher.init(Cipher.DECRYPT_MODE, keySpec, ivParameterSpec);
59
+ return cipher.doFinal(cipherText);
60
+ } catch (Exception e) {
61
+ e.printStackTrace();
62
+ }
63
+ return null;
64
+ }
65
+
66
+ public static SecretKey byteToSessionKey(byte[] sessionKey) {
67
+ // rebuild key using SecretKeySpec
68
+ return new SecretKeySpec(sessionKey, 0, sessionKey.length, "AES");
69
+ }
70
+
71
+ private static PublicKey readX509PublicKey(byte[] x509Bytes) throws GeneralSecurityException {
72
+ KeyFactory keyFactory = KeyFactory.getInstance("RSA");
73
+ X509EncodedKeySpec keySpec = new X509EncodedKeySpec(x509Bytes);
74
+ try {
75
+ return keyFactory.generatePublic(keySpec);
76
+ } catch (InvalidKeySpecException e) {
77
+ throw new IllegalArgumentException("Unexpected key format!", e);
78
+ }
79
+ }
80
+
81
+ public static PublicKey stringToPublicKey(String public_key) throws GeneralSecurityException {
82
+ String pkcs1Pem = public_key
83
+ .replaceAll("\\s+", "")
84
+ .replace("-----BEGINRSAPUBLICKEY-----", "")
85
+ .replace("-----ENDRSAPUBLICKEY-----", "");
86
+
87
+ byte[] pkcs1EncodedBytes = Base64.decode(pkcs1Pem, Base64.DEFAULT);
88
+ return readPkcs1PublicKey(pkcs1EncodedBytes);
89
+ }
90
+
91
+ // since the public key is in pkcs1 format, we have to convert it to x509 format similar
92
+ // to what needs done with the private key converting to pkcs8 format
93
+ // so, the rest of the code below here is adapted from here https://stackoverflow.com/a/54246646
94
+ private static final int SEQUENCE_TAG = 0x30;
95
+ private static final int BIT_STRING_TAG = 0x03;
96
+ private static final byte[] NO_UNUSED_BITS = new byte[] { 0x00 };
97
+ private static final byte[] RSA_ALGORITHM_IDENTIFIER_SEQUENCE = {
98
+ (byte) 0x30,
99
+ (byte) 0x0d,
100
+ (byte) 0x06,
101
+ (byte) 0x09,
102
+ (byte) 0x2a,
103
+ (byte) 0x86,
104
+ (byte) 0x48,
105
+ (byte) 0x86,
106
+ (byte) 0xf7,
107
+ (byte) 0x0d,
108
+ (byte) 0x01,
109
+ (byte) 0x01,
110
+ (byte) 0x01,
111
+ (byte) 0x05,
112
+ (byte) 0x00
113
+ };
114
+
115
+ private static PublicKey readPkcs1PublicKey(byte[] pkcs1Bytes)
116
+ throws NoSuchAlgorithmException, InvalidKeySpecException, GeneralSecurityException {
117
+ // convert the pkcs1 public key to an x509 favorable format
118
+ byte[] keyBitString = createDEREncoding(BIT_STRING_TAG, joinPublic(NO_UNUSED_BITS, pkcs1Bytes));
119
+ byte[] keyInfoValue = joinPublic(RSA_ALGORITHM_IDENTIFIER_SEQUENCE, keyBitString);
120
+ byte[] keyInfoSequence = createDEREncoding(SEQUENCE_TAG, keyInfoValue);
121
+ return readX509PublicKey(keyInfoSequence);
122
+ }
123
+
124
+ private static byte[] joinPublic(byte[]... bas) {
125
+ int len = 0;
126
+ for (int i = 0; i < bas.length; i++) {
127
+ len += bas[i].length;
128
+ }
129
+
130
+ byte[] buf = new byte[len];
131
+ int off = 0;
132
+ for (int i = 0; i < bas.length; i++) {
133
+ System.arraycopy(bas[i], 0, buf, off, bas[i].length);
134
+ off += bas[i].length;
135
+ }
136
+
137
+ return buf;
138
+ }
139
+
140
+ public static void decryptFile(final File file, final String publicKey, final String ivSessionKey) throws IOException {
141
+ if (publicKey.isEmpty() || ivSessionKey == null || ivSessionKey.isEmpty() || ivSessionKey.split(":").length != 2) {
142
+ logger.info("Encryption not set, no public key or session, ignored");
143
+ return;
144
+ }
145
+ if (!publicKey.startsWith("-----BEGIN RSA PUBLIC KEY-----")) {
146
+ logger.error("The public key is not a valid RSA Public key");
147
+ return;
148
+ }
149
+
150
+ try {
151
+ String ivB64 = ivSessionKey.split(":")[0];
152
+ String sessionKeyB64 = ivSessionKey.split(":")[1];
153
+ byte[] iv = Base64.decode(ivB64.getBytes(), Base64.DEFAULT);
154
+ byte[] sessionKey = Base64.decode(sessionKeyB64.getBytes(), Base64.DEFAULT);
155
+ PublicKey pKey = CryptoCipher.stringToPublicKey(publicKey);
156
+ byte[] decryptedSessionKey = CryptoCipher.decryptRSA(sessionKey, pKey);
157
+
158
+ SecretKey sKey = CryptoCipher.byteToSessionKey(decryptedSessionKey);
159
+ decryptAesFile(file, sKey, iv);
160
+ } catch (GeneralSecurityException e) {
161
+ logger.info("decryptFile fail");
162
+ throw new IOException("GeneralSecurityException", e);
163
+ }
164
+ }
165
+
166
+ static void decryptAesFile(File file, SecretKey key, byte[] iv) throws IOException, GeneralSecurityException {
167
+ if (file.length() == 0) {
168
+ throw new IOException("Empty encrypted data");
169
+ }
170
+ Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
171
+ cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(key.getEncoded(), "AES"), new IvParameterSpec(iv));
172
+ File parent = file.getAbsoluteFile().getParentFile();
173
+ if (parent == null) {
174
+ throw new IOException("Cannot create temp file for " + file.getAbsolutePath());
175
+ }
176
+ File tempFile = File.createTempFile("capgo-aes-", ".tmp", parent);
177
+ try {
178
+ byte[] inBuf = new byte[ioBufferBytes()];
179
+ // Reuse one output buffer. cipher.update(in) allocates a new byte[] per chunk.
180
+ byte[] outBuf = new byte[inBuf.length + 16];
181
+ try (FileInputStream fis = new FileInputStream(file); FileOutputStream fos = new FileOutputStream(tempFile)) {
182
+ int n;
183
+ while ((n = fis.read(inBuf)) != -1) {
184
+ int outLen = cipher.update(inBuf, 0, n, outBuf, 0);
185
+ if (outLen > 0) {
186
+ fos.write(outBuf, 0, outLen);
187
+ }
188
+ }
189
+ int last = cipher.doFinal(outBuf, 0);
190
+ if (last > 0) {
191
+ fos.write(outBuf, 0, last);
192
+ }
193
+ }
194
+ if (tempFile.length() == 0) {
195
+ throw new IOException("Empty decrypted data");
196
+ }
197
+ replaceFile(tempFile, file);
198
+ tempFile = null;
199
+ } finally {
200
+ if (tempFile != null && tempFile.exists()) {
201
+ tempFile.delete();
202
+ }
203
+ }
204
+ }
205
+
206
+ static void replaceFile(File from, File to) throws IOException {
207
+ if (from.renameTo(to)) {
208
+ return;
209
+ }
210
+ throw new IOException("Failed to replace file: " + to.getAbsolutePath());
211
+ }
212
+
213
+ private static byte[] hexStringToByteArray(String s) {
214
+ int len = s.length();
215
+ byte[] data = new byte[len / 2];
216
+ for (int i = 0; i < len; i += 2) {
217
+ data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4) + Character.digit(s.charAt(i + 1), 16));
218
+ }
219
+ return data;
220
+ }
221
+
222
+ public static String decryptChecksum(String checksum, String publicKey) throws IOException {
223
+ if (publicKey.isEmpty()) {
224
+ logger.error("No encryption set (public key) ignored");
225
+ return checksum;
226
+ }
227
+ try {
228
+ // TODO: remove this in a month or two
229
+ // Determine if input is hex or base64 encoded
230
+ // Hex strings only contain 0-9 and a-f, while base64 contains other characters
231
+ byte[] checksumBytes;
232
+ String detectedFormat;
233
+ if (checksum.matches("^[0-9a-fA-F]+$")) {
234
+ // Hex encoded (new format from CLI for plugin versions >= 5.30.0, 6.30.0, 7.30.0)
235
+ checksumBytes = hexStringToByteArray(checksum);
236
+ detectedFormat = "hex";
237
+ } else {
238
+ // TODO: remove backwards compatibility
239
+ // Base64 encoded (old format for backwards compatibility)
240
+ checksumBytes = Base64.decode(checksum, Base64.DEFAULT);
241
+ detectedFormat = "base64";
242
+ }
243
+ logger.debug(
244
+ "Received checksum format: " +
245
+ detectedFormat +
246
+ " (length: " +
247
+ checksum.length() +
248
+ " chars, " +
249
+ checksumBytes.length +
250
+ " bytes)"
251
+ );
252
+
253
+ // RSA-2048 encrypted data must be exactly 256 bytes
254
+ // If the checksum is not 256 bytes, the bundle was not encrypted properly
255
+ if (checksumBytes.length != 256) {
256
+ logger.error(
257
+ "Checksum is not RSA encrypted (size: " +
258
+ checksumBytes.length +
259
+ " bytes, expected 256 for RSA-2048). Bundle must be uploaded with encryption when public key is configured."
260
+ );
261
+ throw new IOException("Bundle checksum is not encrypted. Upload bundle with --key flag when encryption is configured.");
262
+ }
263
+
264
+ PublicKey pKey = CryptoCipher.stringToPublicKey(publicKey);
265
+ byte[] decryptedChecksum = CryptoCipher.decryptRSA(checksumBytes, pKey);
266
+ // Return as hex string to match calcChecksum output format
267
+ StringBuilder hexString = new StringBuilder();
268
+ for (byte b : decryptedChecksum) {
269
+ String hex = Integer.toHexString(0xff & b);
270
+ if (hex.length() == 1) hexString.append('0');
271
+ hexString.append(hex);
272
+ }
273
+ String result = hexString.toString();
274
+
275
+ // Detect checksum algorithm based on length
276
+ String detectedAlgorithm;
277
+ if (decryptedChecksum.length == 32) {
278
+ detectedAlgorithm = "SHA-256";
279
+ } else if (decryptedChecksum.length == 4) {
280
+ detectedAlgorithm = "CRC32 (deprecated)";
281
+ logger.error("CRC32 checksum detected - deprecated algorithm");
282
+ } else {
283
+ detectedAlgorithm = "unknown (" + decryptedChecksum.length + " bytes)";
284
+ logger.error("Unknown checksum algorithm detected");
285
+ logger.debug("Byte count: " + decryptedChecksum.length + ", Expected: 32 (SHA-256)");
286
+ }
287
+ logger.debug(
288
+ "Decrypted checksum: " +
289
+ detectedAlgorithm +
290
+ " hex format (length: " +
291
+ result.length() +
292
+ " chars, " +
293
+ decryptedChecksum.length +
294
+ " bytes)"
295
+ );
296
+ return result;
297
+ } catch (GeneralSecurityException e) {
298
+ logger.error("Checksum decryption failed");
299
+ logger.debug("Error: " + e.getMessage());
300
+ throw new IOException("Decryption failed: " + e.getMessage());
301
+ }
302
+ }
303
+
304
+ /**
305
+ * Detect checksum algorithm based on hex string length.
306
+ * SHA-256 = 64 hex chars (32 bytes)
307
+ * CRC32 = 8 hex chars (4 bytes)
308
+ */
309
+ public static String detectChecksumAlgorithm(String hexChecksum) {
310
+ if (hexChecksum == null || hexChecksum.isEmpty()) {
311
+ return "empty";
312
+ }
313
+ int len = hexChecksum.length();
314
+ if (len == 64) {
315
+ return "SHA-256";
316
+ } else if (len == 8) {
317
+ return "CRC32 (deprecated)";
318
+ } else {
319
+ return "unknown (" + len + " hex chars)";
320
+ }
321
+ }
322
+
323
+ /**
324
+ * Log checksum info and warn if deprecated algorithm detected.
325
+ */
326
+ public static void logChecksumInfo(String label, String hexChecksum) {
327
+ String algorithm = detectChecksumAlgorithm(hexChecksum);
328
+ logger.debug(label + ": " + algorithm + " hex format (length: " + hexChecksum.length() + " chars)");
329
+ if (algorithm.contains("CRC32")) {
330
+ logger.error("CRC32 checksum detected - deprecated algorithm");
331
+ } else if (algorithm.contains("unknown")) {
332
+ logger.error("Unknown checksum algorithm detected");
333
+ logger.debug("Char count: " + hexChecksum.length() + ", Expected: 64 (SHA-256)");
334
+ }
335
+ }
336
+
337
+ // 256 KiB: one size for checksum, copy, and decode.
338
+ // 64 workers * 256 KiB = 16 MiB for one buffer; AES/Brotli hold two (~32 MiB).
339
+ static final int IO_BUFFER_BYTES = 256 * 1024;
340
+
341
+ static int ioBufferBytes() {
342
+ return IO_BUFFER_BYTES;
343
+ }
344
+
345
+ static int checksumBufferBytes() {
346
+ return IO_BUFFER_BYTES;
347
+ }
348
+
349
+ static int copyBufferBytes() {
350
+ return IO_BUFFER_BYTES;
351
+ }
352
+
353
+ public static String calcChecksum(File file) {
354
+ try (FileInputStream fis = new FileInputStream(file)) {
355
+ return calcChecksum(fis);
356
+ } catch (IOException e) {
357
+ logger.error("Cannot calculate checksum");
358
+ logger.debug("Path: " + file.getPath() + ", Error: " + e.getMessage());
359
+ return "";
360
+ }
361
+ }
362
+
363
+ public static String calcChecksum(InputStream inputStream) {
364
+ final int BUFFER_SIZE = checksumBufferBytes();
365
+ MessageDigest digest;
366
+ try {
367
+ digest = MessageDigest.getInstance("SHA-256");
368
+ } catch (java.security.NoSuchAlgorithmException e) {
369
+ logger.error("SHA-256 algorithm not available");
370
+ return "";
371
+ }
372
+
373
+ try {
374
+ byte[] buffer = new byte[BUFFER_SIZE];
375
+ int length;
376
+ while ((length = inputStream.read(buffer)) != -1) {
377
+ digest.update(buffer, 0, length);
378
+ }
379
+ return digestToHex(digest);
380
+ } catch (IOException e) {
381
+ logger.error("Cannot calculate checksum");
382
+ logger.debug("Error: " + e.getMessage());
383
+ return "";
384
+ }
385
+ }
386
+
387
+ static String digestToHex(MessageDigest digest) {
388
+ byte[] hash = digest.digest();
389
+ StringBuilder hexString = new StringBuilder(hash.length * 2);
390
+ for (byte b : hash) {
391
+ String hex = Integer.toHexString(0xff & b);
392
+ if (hex.length() == 1) hexString.append('0');
393
+ hexString.append(hex);
394
+ }
395
+ return hexString.toString();
396
+ }
397
+
398
+ static String shortPathKey(String fileName) {
399
+ try {
400
+ MessageDigest digest = MessageDigest.getInstance("SHA-256");
401
+ digest.update((fileName == null ? "" : fileName).getBytes(java.nio.charset.StandardCharsets.UTF_8));
402
+ return digestToHex(digest).substring(0, 16);
403
+ } catch (java.security.NoSuchAlgorithmException e) {
404
+ return Integer.toHexString((fileName == null ? "" : fileName).hashCode());
405
+ }
406
+ }
407
+
408
+ private static byte[] createDEREncoding(int tag, byte[] value) {
409
+ if (tag < 0 || tag >= 0xFF) {
410
+ throw new IllegalArgumentException("Currently only single byte tags supported");
411
+ }
412
+
413
+ byte[] lengthEncoding = createDERLengthEncoding(value.length);
414
+
415
+ int size = 1 + lengthEncoding.length + value.length;
416
+ byte[] derEncodingBuf = new byte[size];
417
+
418
+ int off = 0;
419
+ derEncodingBuf[off++] = (byte) tag;
420
+ System.arraycopy(lengthEncoding, 0, derEncodingBuf, off, lengthEncoding.length);
421
+ off += lengthEncoding.length;
422
+ System.arraycopy(value, 0, derEncodingBuf, off, value.length);
423
+
424
+ return derEncodingBuf;
425
+ }
426
+
427
+ private static byte[] createDERLengthEncoding(int size) {
428
+ if (size <= 0x7F) {
429
+ // single byte length encoding
430
+ return new byte[] { (byte) size };
431
+ } else if (size <= 0xFF) {
432
+ // double byte length encoding
433
+ return new byte[] { (byte) 0x81, (byte) size };
434
+ } else if (size <= 0xFFFF) {
435
+ // triple byte length encoding
436
+ return new byte[] { (byte) 0x82, (byte) (size >> Byte.SIZE), (byte) size };
437
+ }
438
+
439
+ throw new IllegalArgumentException("size too large, only up to 64KiB length encoding supported: " + size);
440
+ }
441
+
442
+ /**
443
+ * Get first 20 characters of the public key for identification.
444
+ * Returns 20-character string or empty string if key is invalid/empty.
445
+ * The first 12 chars are always "MIIBCgKCAQEA" for RSA 2048-bit keys,
446
+ * so the unique part starts at character 13.
447
+ */
448
+ public static String calcKeyId(String publicKey) {
449
+ if (publicKey == null || publicKey.isEmpty()) {
450
+ return "";
451
+ }
452
+
453
+ // Remove PEM headers and whitespace to get the raw key data
454
+ String cleanedKey = publicKey
455
+ .replaceAll("\\s+", "")
456
+ .replace("-----BEGINRSAPUBLICKEY-----", "")
457
+ .replace("-----ENDRSAPUBLICKEY-----", "");
458
+
459
+ // Return first 20 characters of the base64-encoded key
460
+ return cleanedKey.length() >= 20 ? cleanedKey.substring(0, 20) : cleanedKey;
461
+ }
462
+ }
@@ -0,0 +1,44 @@
1
+ package app.capgo.cordova.updater;
2
+
3
+ import java.util.HashMap;
4
+ import java.util.Map;
5
+ import org.json.JSONArray;
6
+
7
+ public class DataManager {
8
+
9
+ private static DataManager instance;
10
+ private final Map<String, JSONArray> manifestsById = new HashMap<>();
11
+
12
+ private DataManager() {}
13
+
14
+ public static synchronized DataManager getInstance() {
15
+ if (instance == null) {
16
+ instance = new DataManager();
17
+ }
18
+ return instance;
19
+ }
20
+
21
+ public synchronized void setManifest(String downloadId, JSONArray manifest) {
22
+ if (downloadId == null || manifest == null) {
23
+ return;
24
+ }
25
+ this.manifestsById.put(downloadId, manifest);
26
+ }
27
+
28
+ public synchronized JSONArray getAndClearManifest(String downloadId) {
29
+ if (downloadId == null) {
30
+ return null;
31
+ }
32
+ return this.manifestsById.remove(downloadId);
33
+ }
34
+
35
+ public synchronized void clearManifest(String downloadId) {
36
+ if (downloadId != null) {
37
+ this.manifestsById.remove(downloadId);
38
+ }
39
+ }
40
+
41
+ public synchronized void clearAllManifests() {
42
+ this.manifestsById.clear();
43
+ }
44
+ }
@@ -0,0 +1,56 @@
1
+ /*
2
+ * This Source Code Form is subject to the terms of the Mozilla Public
3
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
4
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
5
+ */
6
+
7
+ package app.capgo.cordova.updater;
8
+
9
+ import androidx.annotation.NonNull;
10
+ import java.util.Objects;
11
+
12
+ public class DelayCondition {
13
+
14
+ private DelayUntilNext kind;
15
+
16
+ private String value;
17
+
18
+ public DelayCondition(DelayUntilNext kind, String value) {
19
+ this.kind = kind;
20
+ this.value = value;
21
+ }
22
+
23
+ public DelayUntilNext getKind() {
24
+ return kind;
25
+ }
26
+
27
+ public void setKind(DelayUntilNext kind) {
28
+ this.kind = kind;
29
+ }
30
+
31
+ public String getValue() {
32
+ return value;
33
+ }
34
+
35
+ public void setValue(String value) {
36
+ this.value = value;
37
+ }
38
+
39
+ @Override
40
+ public boolean equals(Object o) {
41
+ if (this == o) return true;
42
+ if (!(o instanceof DelayCondition that)) return false;
43
+ return getKind() == that.getKind() && Objects.equals(getValue(), that.getValue());
44
+ }
45
+
46
+ @Override
47
+ public int hashCode() {
48
+ return Objects.hash(getKind(), getValue());
49
+ }
50
+
51
+ @NonNull
52
+ @Override
53
+ public String toString() {
54
+ return "DelayCondition{" + "kind=" + kind + ", value='" + value + '\'' + '}';
55
+ }
56
+ }
@@ -0,0 +1,14 @@
1
+ /*
2
+ * This Source Code Form is subject to the terms of the Mozilla Public
3
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
4
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
5
+ */
6
+
7
+ package app.capgo.cordova.updater;
8
+
9
+ public enum DelayUntilNext {
10
+ background,
11
+ kill,
12
+ nativeVersion,
13
+ date
14
+ }