@capgo/capacitor-updater 5.40.5 → 5.42.3

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.
@@ -210,7 +210,7 @@ public class CryptoCipher {
210
210
  detectedFormat = "base64";
211
211
  }
212
212
  logger.debug(
213
- "Received encrypted checksum format: " +
213
+ "Received checksum format: " +
214
214
  detectedFormat +
215
215
  " (length: " +
216
216
  checksum.length() +
@@ -218,6 +218,18 @@ public class CryptoCipher {
218
218
  checksumBytes.length +
219
219
  " bytes)"
220
220
  );
221
+
222
+ // RSA-2048 encrypted data must be exactly 256 bytes
223
+ // If the checksum is not 256 bytes, the bundle was not encrypted properly
224
+ if (checksumBytes.length != 256) {
225
+ logger.error(
226
+ "Checksum is not RSA encrypted (size: " +
227
+ checksumBytes.length +
228
+ " bytes, expected 256 for RSA-2048). Bundle must be uploaded with encryption when public key is configured."
229
+ );
230
+ throw new IOException("Bundle checksum is not encrypted. Upload bundle with --key flag when encryption is configured.");
231
+ }
232
+
221
233
  PublicKey pKey = CryptoCipher.stringToPublicKey(publicKey);
222
234
  byte[] decryptedChecksum = CryptoCipher.decryptRSA(checksumBytes, pKey);
223
235
  // Return as hex string to match calcChecksum output format
@@ -357,8 +369,10 @@ public class CryptoCipher {
357
369
  }
358
370
 
359
371
  /**
360
- * Get first 4 characters of the public key for identification.
361
- * Returns 4-character string or empty string if key is invalid/empty.
372
+ * Get first 20 characters of the public key for identification.
373
+ * Returns 20-character string or empty string if key is invalid/empty.
374
+ * The first 12 chars are always "MIIBCgKCAQEA" for RSA 2048-bit keys,
375
+ * so the unique part starts at character 13.
362
376
  */
363
377
  public static String calcKeyId(String publicKey) {
364
378
  if (publicKey == null || publicKey.isEmpty()) {
@@ -371,7 +385,7 @@ public class CryptoCipher {
371
385
  .replace("-----BEGINRSAPUBLICKEY-----", "")
372
386
  .replace("-----ENDRSAPUBLICKEY-----", "");
373
387
 
374
- // Return first 4 characters of the base64-encoded key
375
- return cleanedKey.length() >= 4 ? cleanedKey.substring(0, 4) : cleanedKey;
388
+ // Return first 20 characters of the base64-encoded key
389
+ return cleanedKey.length() >= 20 ? cleanedKey.substring(0, 20) : cleanedKey;
376
390
  }
377
391
  }
@@ -291,7 +291,7 @@ public class DownloadService extends Worker {
291
291
  String fileHash = entry.getString("file_hash");
292
292
  String downloadUrl = entry.getString("download_url");
293
293
 
294
- if (!publicKey.isEmpty() && sessionKey != null && !sessionKey.isEmpty()) {
294
+ if (publicKey != null && !publicKey.isEmpty() && sessionKey != null && !sessionKey.isEmpty()) {
295
295
  try {
296
296
  fileHash = CryptoCipher.decryptChecksum(fileHash, publicKey);
297
297
  } catch (Exception e) {
@@ -308,7 +308,9 @@ public class DownloadService extends Worker {
308
308
  String targetFileName = isBrotli ? fileName.substring(0, fileName.length() - 3) : fileName;
309
309
 
310
310
  File targetFile = new File(destFolder, targetFileName);
311
- File cacheFile = new File(cacheFolder, finalFileHash + "_" + new File(fileName).getName());
311
+ String cacheBaseName = new File(isBrotli ? targetFileName : fileName).getName();
312
+ File cacheFile = new File(cacheFolder, finalFileHash + "_" + cacheBaseName);
313
+ final File legacyCacheFile = isBrotli ? new File(cacheFolder, finalFileHash + "_" + new File(fileName).getName()) : null;
312
314
  File builtinFile = new File(builtinFolder, fileName);
313
315
 
314
316
  // Ensure parent directories of the target file exist
@@ -324,7 +326,10 @@ public class DownloadService extends Worker {
324
326
  if (builtinFile.exists() && verifyChecksum(builtinFile, finalFileHash)) {
325
327
  copyFile(builtinFile, targetFile);
326
328
  logger.debug("using builtin file " + fileName);
327
- } else if (tryCopyFromCache(cacheFile, targetFile, finalFileHash)) {
329
+ } else if (
330
+ tryCopyFromCache(cacheFile, targetFile, finalFileHash) ||
331
+ (legacyCacheFile != null && tryCopyFromCache(legacyCacheFile, targetFile, finalFileHash))
332
+ ) {
328
333
  logger.debug("already cached " + fileName);
329
334
  } else {
330
335
  downloadAndVerify(downloadUrl, targetFile, cacheFile, finalFileHash, sessionKey, publicKey, finalIsBrotli);
@@ -388,8 +393,9 @@ public class DownloadService extends Worker {
388
393
  sendStatsAsync("download_zip_start", version);
389
394
 
390
395
  File target = new File(documentsDir, dest);
391
- File infoFile = new File(documentsDir, UPDATE_FILE);
392
- File tempFile = new File(documentsDir, "temp" + ".tmp");
396
+ // Use bundle ID in temp file names to prevent collisions when multiple downloads run concurrently
397
+ File infoFile = new File(documentsDir, "update_" + id + ".dat");
398
+ File tempFile = new File(documentsDir, "temp_" + id + ".tmp");
393
399
 
394
400
  // Check available disk space before starting
395
401
  long availableSpace = target.getParentFile().getUsableSpace();
@@ -420,7 +426,7 @@ public class DownloadService extends Worker {
420
426
  reader = new BufferedReader(new FileReader(infoFile));
421
427
  String updateVersion = reader.readLine();
422
428
  if (updateVersion != null && !updateVersion.equals(version)) {
423
- clearDownloadData(documentsDir);
429
+ clearDownloadData(documentsDir, id);
424
430
  } else {
425
431
  downloadedBytes = tempFile.length();
426
432
  }
@@ -432,7 +438,7 @@ public class DownloadService extends Worker {
432
438
  }
433
439
  }
434
440
  } else {
435
- clearDownloadData(documentsDir);
441
+ clearDownloadData(documentsDir, id);
436
442
  }
437
443
 
438
444
  if (downloadedBytes > 0) {
@@ -544,9 +550,9 @@ public class DownloadService extends Worker {
544
550
  }
545
551
  }
546
552
 
547
- private void clearDownloadData(String docDir) {
548
- File tempFile = new File(docDir, "temp" + ".tmp");
549
- File infoFile = new File(docDir, UPDATE_FILE);
553
+ private void clearDownloadData(String docDir, String id) {
554
+ File tempFile = new File(docDir, "temp_" + id + ".tmp");
555
+ File infoFile = new File(docDir, "update_" + id + ".dat");
550
556
  try {
551
557
  tempFile.delete();
552
558
  infoFile.delete();
@@ -632,7 +638,7 @@ public class DownloadService extends Worker {
632
638
  // Use OkIO for atomic write
633
639
  writeFileAtomic(compressedFile, responseBody.byteStream(), null);
634
640
 
635
- if (!publicKey.isEmpty() && sessionKey != null && !sessionKey.isEmpty()) {
641
+ if (publicKey != null && !publicKey.isEmpty() && sessionKey != null && !sessionKey.isEmpty()) {
636
642
  logger.debug("Decrypting file " + targetFile.getName());
637
643
  CryptoCipher.decryptFile(compressedFile, publicKey, sessionKey);
638
644
  }
@@ -824,12 +830,14 @@ public class DownloadService extends Worker {
824
830
  }
825
831
 
826
832
  /**
827
- * Clean up old temporary files
833
+ * Clean up old temporary files (both .tmp and update_*.dat files)
828
834
  */
829
835
  private void cleanupOldTempFiles(File directory) {
830
836
  if (directory == null || !directory.exists()) return;
831
837
 
832
- File[] tempFiles = directory.listFiles((dir, name) -> name.endsWith(".tmp"));
838
+ File[] tempFiles = directory.listFiles(
839
+ (dir, name) -> name.endsWith(".tmp") || (name.startsWith("update_") && name.endsWith(".dat"))
840
+ );
833
841
  if (tempFiles != null) {
834
842
  long oneHourAgo = System.currentTimeMillis() - 3600000;
835
843
  for (File tempFile : tempFiles) {
@@ -28,30 +28,48 @@ public class Logger {
28
28
 
29
29
  LogLevel level;
30
30
  Map<String, String> labels;
31
+ boolean useSystemLog;
31
32
 
32
33
  Options() {
33
34
  level = LogLevel.info;
34
35
  labels = null;
36
+ useSystemLog = true; // Default to true for backward compatibility
35
37
  }
36
38
 
37
39
  Options(@NonNull Options other) {
38
40
  level = other.level;
39
41
  labels = other.labels;
42
+ useSystemLog = other.useSystemLog;
40
43
  }
41
44
 
42
45
  Options(LogLevel level) {
43
46
  this.level = level;
44
47
  this.labels = null;
48
+ useSystemLog = true;
45
49
  }
46
50
 
47
51
  Options(Map<String, String> labels) {
48
52
  this.level = LogLevel.info;
49
53
  this.labels = labels;
54
+ useSystemLog = true;
50
55
  }
51
56
 
52
57
  Options(LogLevel level, Map<String, String> labels) {
53
58
  this.level = level;
54
59
  this.labels = labels;
60
+ useSystemLog = true;
61
+ }
62
+
63
+ Options(LogLevel level, Map<String, String> labels, boolean useSystemLog) {
64
+ this.level = level;
65
+ this.labels = labels;
66
+ this.useSystemLog = useSystemLog;
67
+ }
68
+
69
+ Options(boolean useSystemLog) {
70
+ this.level = LogLevel.info;
71
+ this.labels = null;
72
+ this.useSystemLog = useSystemLog;
55
73
  }
56
74
  }
57
75
 
@@ -60,6 +78,7 @@ public class Logger {
60
78
  private String tag;
61
79
  private final ArrayMap<String, Long> timers = new ArrayMap<>();
62
80
  private final String kDefaultTimerLabel = "default";
81
+ private boolean useSystemLog;
63
82
 
64
83
  public void setBridge(Bridge bridge) {
65
84
  this.bridge = bridge;
@@ -77,6 +96,7 @@ public class Logger {
77
96
 
78
97
  private void init(String tag, @NonNull Options options) {
79
98
  this.level = options.level;
99
+ this.useSystemLog = options.useSystemLog;
80
100
  this.labels.putAll(
81
101
  Map.of(LogLevel.silent, "", LogLevel.error, "🔴", LogLevel.warn, "🟠", LogLevel.info, "🟢", LogLevel.debug, "\uD83D\uDD0E")
82
102
  );
@@ -210,20 +230,22 @@ public class Logger {
210
230
  tag = this.tag;
211
231
  }
212
232
 
213
- // Always log to Android system log
214
- switch (level) {
215
- case error:
216
- Log.e(tag, formattedMessage);
217
- break;
218
- case warn:
219
- Log.w(tag, formattedMessage);
220
- break;
221
- case info:
222
- Log.i(tag, formattedMessage);
223
- break;
224
- case debug:
225
- Log.d(tag, formattedMessage);
226
- break;
233
+ // Log to Android system log if enabled
234
+ if (useSystemLog) {
235
+ switch (level) {
236
+ case error:
237
+ Log.e(tag, formattedMessage);
238
+ break;
239
+ case warn:
240
+ Log.w(tag, formattedMessage);
241
+ break;
242
+ case info:
243
+ Log.i(tag, formattedMessage);
244
+ break;
245
+ case debug:
246
+ Log.d(tag, formattedMessage);
247
+ break;
248
+ }
227
249
  }
228
250
 
229
251
  // Send to JavaScript if webView is available
package/dist/docs.json CHANGED
@@ -3627,6 +3627,22 @@
3627
3627
  "complexTypes": [],
3628
3628
  "type": "boolean | undefined"
3629
3629
  },
3630
+ {
3631
+ "name": "osLogging",
3632
+ "tags": [
3633
+ {
3634
+ "text": "true",
3635
+ "name": "default"
3636
+ },
3637
+ {
3638
+ "text": "8.42.0",
3639
+ "name": "since"
3640
+ }
3641
+ ],
3642
+ "docs": "Enable OS-level logging. When enabled, logs are written to the system log which can be inspected in production builds.\n\n- **iOS**: Uses os_log instead of Swift.print, logs accessible via Console.app or Instruments\n- **Android**: Logs to Logcat (android.util.Log)\n\nWhen set to false, system logging is disabled on both platforms (only JavaScript console logging will occur if enabled).\n\nThis is useful for debugging production apps (App Store/TestFlight builds on iOS, or production APKs on Android).",
3643
+ "complexTypes": [],
3644
+ "type": "boolean | undefined"
3645
+ },
3630
3646
  {
3631
3647
  "name": "shakeMenu",
3632
3648
  "tags": [
@@ -1,4 +1,3 @@
1
- import '@capacitor/cli';
2
1
  import type { PluginListenerHandle } from '@capacitor/core';
3
2
  declare module '@capacitor/cli' {
4
3
  interface PluginsConfig {
@@ -309,6 +308,20 @@ declare module '@capacitor/cli' {
309
308
  * @since 7.3.0
310
309
  */
311
310
  disableJSLogging?: boolean;
311
+ /**
312
+ * Enable OS-level logging. When enabled, logs are written to the system log which can be inspected in production builds.
313
+ *
314
+ * - **iOS**: Uses os_log instead of Swift.print, logs accessible via Console.app or Instruments
315
+ * - **Android**: Logs to Logcat (android.util.Log)
316
+ *
317
+ * When set to false, system logging is disabled on both platforms (only JavaScript console logging will occur if enabled).
318
+ *
319
+ * This is useful for debugging production apps (App Store/TestFlight builds on iOS, or production APKs on Android).
320
+ *
321
+ * @default true
322
+ * @since 8.42.0
323
+ */
324
+ osLogging?: boolean;
312
325
  /**
313
326
  * Enable shake gesture to show update menu for debugging/testing purposes
314
327
  *
@@ -1,9 +1,3 @@
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
- import '@capacitor/cli';
7
1
  /**
8
2
  * Update availability status.
9
3
  *