@capgo/capacitor-updater 8.51.15 → 8.51.16
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 +0 -2
- package/Package.swift +2 -4
- package/android/build.gradle +3 -3
- package/android/src/main/java/ee/forgr/capacitor_updater/CapacitorUpdaterPlugin.java +19 -7
- package/android/src/main/java/ee/forgr/capacitor_updater/CapgoUpdater.java +13 -6
- package/android/src/main/java/ee/forgr/capacitor_updater/DelayUpdateUtils.java +11 -0
- package/ios/Sources/CapacitorUpdaterPlugin/BundleInfo.swift +4 -0
- package/ios/Sources/CapacitorUpdaterPlugin/CapacitorUpdaterPlugin.swift +110 -27
- package/ios/Sources/CapacitorUpdaterPlugin/CapgoRawRsa.swift +36 -0
- package/ios/Sources/CapacitorUpdaterPlugin/CapgoUpdater.swift +19 -15
- package/ios/Sources/CapacitorUpdaterPlugin/RSA.swift +73 -251
- package/package.json +2 -2
- package/ios/Sources/CapacitorUpdaterPlugin/BigInt.swift +0 -39
|
@@ -16,7 +16,5 @@ Pod::Spec.new do |s|
|
|
|
16
16
|
s.dependency 'ZIPFoundation', '~> 0.9'
|
|
17
17
|
s.dependency 'Alamofire', '5.10.2'
|
|
18
18
|
s.dependency 'Version', '0.8.0'
|
|
19
|
-
# We cannot update to latest because it not published anymore in cocoa https://github.com/attaswift/BigInt/issues/94
|
|
20
|
-
s.dependency 'BigInt', '5.2.0'
|
|
21
19
|
s.swift_version = '5.1'
|
|
22
20
|
end
|
package/Package.swift
CHANGED
|
@@ -13,8 +13,7 @@ let package = Package(
|
|
|
13
13
|
.package(url: "https://github.com/ionic-team/capacitor-swift-pm.git", from: "8.0.0"),
|
|
14
14
|
.package(url: "https://github.com/Alamofire/Alamofire.git", .upToNextMajor(from: "5.12.0")),
|
|
15
15
|
.package(url: "https://github.com/weichsel/ZIPFoundation.git", from: "0.9.20"),
|
|
16
|
-
.package(url: "https://github.com/mrackwitz/Version.git", exact: "0.8.0")
|
|
17
|
-
.package(url: "https://github.com/attaswift/BigInt.git", from: "5.7.0")
|
|
16
|
+
.package(url: "https://github.com/mrackwitz/Version.git", exact: "0.8.0")
|
|
18
17
|
],
|
|
19
18
|
targets: [
|
|
20
19
|
.target(
|
|
@@ -24,8 +23,7 @@ let package = Package(
|
|
|
24
23
|
.product(name: "Cordova", package: "capacitor-swift-pm"),
|
|
25
24
|
.product(name: "ZIPFoundation", package: "ZIPFoundation"),
|
|
26
25
|
.product(name: "Alamofire", package: "Alamofire"),
|
|
27
|
-
.product(name: "Version", package: "Version")
|
|
28
|
-
.product(name: "BigInt", package: "BigInt")
|
|
26
|
+
.product(name: "Version", package: "Version")
|
|
29
27
|
],
|
|
30
28
|
path: "ios/Sources/CapacitorUpdaterPlugin"),
|
|
31
29
|
.testTarget(
|
package/android/build.gradle
CHANGED
|
@@ -3,6 +3,7 @@ ext {
|
|
|
3
3
|
androidxAppCompatVersion = project.hasProperty('androidxAppCompatVersion') ? rootProject.ext.androidxAppCompatVersion : '1.7.1'
|
|
4
4
|
androidxJunitVersion = project.hasProperty('androidxJunitVersion') ? rootProject.ext.androidxJunitVersion : '1.3.0'
|
|
5
5
|
androidxEspressoCoreVersion = project.hasProperty('androidxEspressoCoreVersion') ? rootProject.ext.androidxEspressoCoreVersion : '3.7.0'
|
|
6
|
+
okhttpVersion = project.hasProperty('okhttpVersion') ? rootProject.ext.okhttpVersion : '4.12.0'
|
|
6
7
|
}
|
|
7
8
|
|
|
8
9
|
buildscript {
|
|
@@ -54,7 +55,6 @@ dependencies {
|
|
|
54
55
|
implementation "androidx.work:work-runtime:$work_version"
|
|
55
56
|
implementation "androidx.lifecycle:lifecycle-process:$lifecycle_version"
|
|
56
57
|
implementation "com.google.android.gms:play-services-tasks:18.4.1"
|
|
57
|
-
implementation "com.google.guava:guava:33.6.0-android"
|
|
58
58
|
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
|
59
59
|
implementation project(':capacitor-android')
|
|
60
60
|
implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion"
|
|
@@ -65,11 +65,11 @@ dependencies {
|
|
|
65
65
|
testImplementation "junit:junit:$junitVersion"
|
|
66
66
|
testImplementation 'org.mockito:mockito-core:5.23.0'
|
|
67
67
|
testImplementation 'org.json:json:20250517'
|
|
68
|
-
testImplementation 'org.robolectric:robolectric:4.
|
|
68
|
+
testImplementation 'org.robolectric:robolectric:4.17'
|
|
69
69
|
androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion"
|
|
70
70
|
androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion"
|
|
71
71
|
implementation 'org.brotli:dec:0.1.2'
|
|
72
|
-
implementation
|
|
72
|
+
implementation "com.squareup.okhttp3:okhttp:$okhttpVersion"
|
|
73
73
|
}
|
|
74
74
|
|
|
75
75
|
tasks.withType(Test) {
|
|
@@ -146,7 +146,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
146
146
|
static final int APPLICATION_EXIT_REASON_USER_REQUESTED = 10;
|
|
147
147
|
static final int APPLICATION_EXIT_REASON_DEPENDENCY_DIED = 12;
|
|
148
148
|
|
|
149
|
-
private final String pluginVersion = "8.51.
|
|
149
|
+
private final String pluginVersion = "8.51.16";
|
|
150
150
|
private static final String DELAY_CONDITION_PREFERENCES = "";
|
|
151
151
|
|
|
152
152
|
private SharedPreferences.Editor editor;
|
|
@@ -2532,11 +2532,15 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
2532
2532
|
call.reject("setUpdateUrl called without url");
|
|
2533
2533
|
return;
|
|
2534
2534
|
}
|
|
2535
|
-
this.updateUrl = url;
|
|
2536
2535
|
if (Boolean.TRUE.equals(this.persistModifyUrl)) {
|
|
2537
2536
|
this.editor.putString(UPDATE_URL_PREF_KEY, url);
|
|
2538
|
-
this.editor.
|
|
2537
|
+
if (!this.editor.commit()) {
|
|
2538
|
+
logger.error("Failed to persist updateUrl");
|
|
2539
|
+
call.reject("Failed to persist updateUrl");
|
|
2540
|
+
return;
|
|
2541
|
+
}
|
|
2539
2542
|
}
|
|
2543
|
+
this.updateUrl = url;
|
|
2540
2544
|
call.resolve();
|
|
2541
2545
|
}
|
|
2542
2546
|
|
|
@@ -2553,11 +2557,15 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
2553
2557
|
call.reject("setStatsUrl called without url");
|
|
2554
2558
|
return;
|
|
2555
2559
|
}
|
|
2556
|
-
this.implementation.statsUrl = url;
|
|
2557
2560
|
if (Boolean.TRUE.equals(this.persistModifyUrl)) {
|
|
2558
2561
|
this.editor.putString(STATS_URL_PREF_KEY, url);
|
|
2559
|
-
this.editor.
|
|
2562
|
+
if (!this.editor.commit()) {
|
|
2563
|
+
logger.error("Failed to persist statsUrl");
|
|
2564
|
+
call.reject("Failed to persist statsUrl");
|
|
2565
|
+
return;
|
|
2566
|
+
}
|
|
2560
2567
|
}
|
|
2568
|
+
this.implementation.statsUrl = url;
|
|
2561
2569
|
call.resolve();
|
|
2562
2570
|
}
|
|
2563
2571
|
|
|
@@ -2574,11 +2582,15 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
2574
2582
|
call.reject("setChannelUrl called without url");
|
|
2575
2583
|
return;
|
|
2576
2584
|
}
|
|
2577
|
-
this.implementation.channelUrl = url;
|
|
2578
2585
|
if (Boolean.TRUE.equals(this.persistModifyUrl)) {
|
|
2579
2586
|
this.editor.putString(CHANNEL_URL_PREF_KEY, url);
|
|
2580
|
-
this.editor.
|
|
2587
|
+
if (!this.editor.commit()) {
|
|
2588
|
+
logger.error("Failed to persist channelUrl");
|
|
2589
|
+
call.reject("Failed to persist channelUrl");
|
|
2590
|
+
return;
|
|
2591
|
+
}
|
|
2581
2592
|
}
|
|
2593
|
+
this.implementation.channelUrl = url;
|
|
2582
2594
|
call.resolve();
|
|
2583
2595
|
}
|
|
2584
2596
|
|
|
@@ -23,8 +23,6 @@ import androidx.work.OneTimeWorkRequest;
|
|
|
23
23
|
import androidx.work.PeriodicWorkRequest;
|
|
24
24
|
import androidx.work.WorkInfo;
|
|
25
25
|
import androidx.work.WorkManager;
|
|
26
|
-
import com.google.common.util.concurrent.Futures;
|
|
27
|
-
import com.google.common.util.concurrent.ListenableFuture;
|
|
28
26
|
import java.io.BufferedInputStream;
|
|
29
27
|
import java.io.BufferedReader;
|
|
30
28
|
import java.io.File;
|
|
@@ -2901,6 +2899,11 @@ public class CapgoUpdater {
|
|
|
2901
2899
|
statsQueue.add(new QueuedStatsEvent(arr.getJSONObject(i), null));
|
|
2902
2900
|
}
|
|
2903
2901
|
}
|
|
2902
|
+
if (backup.exists() && !backup.delete()) {
|
|
2903
|
+
if (logger != null) {
|
|
2904
|
+
logger.error("Failed to delete stats backup");
|
|
2905
|
+
}
|
|
2906
|
+
}
|
|
2904
2907
|
if (!statsQueue.isEmpty()) {
|
|
2905
2908
|
if (logger != null) {
|
|
2906
2909
|
logger.info("Restored " + statsQueue.size() + " pending stats events");
|
|
@@ -2956,9 +2959,11 @@ public class CapgoUpdater {
|
|
|
2956
2959
|
}
|
|
2957
2960
|
try {
|
|
2958
2961
|
if (arr.length() == 0) {
|
|
2959
|
-
|
|
2962
|
+
writeFileAtomically(file, "[]".getBytes(StandardCharsets.UTF_8));
|
|
2963
|
+
File backup = new File(file.getAbsolutePath() + ".bak");
|
|
2964
|
+
if (backup.exists() && !backup.delete()) {
|
|
2960
2965
|
if (logger != null) {
|
|
2961
|
-
logger.error("Failed to delete empty stats
|
|
2966
|
+
logger.error("Failed to delete empty stats backup");
|
|
2962
2967
|
}
|
|
2963
2968
|
}
|
|
2964
2969
|
return;
|
|
@@ -2989,7 +2994,7 @@ public class CapgoUpdater {
|
|
|
2989
2994
|
}
|
|
2990
2995
|
}
|
|
2991
2996
|
|
|
2992
|
-
private
|
|
2997
|
+
private void writeFileAtomically(final File file, final byte[] bytes) throws IOException {
|
|
2993
2998
|
final File tmp = new File(file.getAbsolutePath() + ".tmp");
|
|
2994
2999
|
File backup = null;
|
|
2995
3000
|
try {
|
|
@@ -3011,7 +3016,9 @@ public class CapgoUpdater {
|
|
|
3011
3016
|
}
|
|
3012
3017
|
if (tmp.renameTo(file)) {
|
|
3013
3018
|
if (backup != null && backup.exists() && !backup.delete()) {
|
|
3014
|
-
|
|
3019
|
+
if (logger != null) {
|
|
3020
|
+
logger.error("Failed to delete stats backup");
|
|
3021
|
+
}
|
|
3015
3022
|
}
|
|
3016
3023
|
backup = null;
|
|
3017
3024
|
return;
|
|
@@ -75,6 +75,17 @@ public class DelayUpdateUtils {
|
|
|
75
75
|
", longValue: " +
|
|
76
76
|
longValue
|
|
77
77
|
);
|
|
78
|
+
} else {
|
|
79
|
+
delayConditionListToKeep.add(condition);
|
|
80
|
+
logger.info(
|
|
81
|
+
"Background delay (value: " +
|
|
82
|
+
value +
|
|
83
|
+
") condition kept at index " +
|
|
84
|
+
index +
|
|
85
|
+
" (source: " +
|
|
86
|
+
source.toString() +
|
|
87
|
+
")"
|
|
88
|
+
);
|
|
78
89
|
}
|
|
79
90
|
} else {
|
|
80
91
|
delayConditionListToKeep.add(condition);
|
|
@@ -69,6 +69,10 @@ import Foundation
|
|
|
69
69
|
return BundleStatus.DELETING == self.status
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
+
public func isDownloading() -> Bool {
|
|
73
|
+
return BundleStatus.DOWNLOADING == self.status
|
|
74
|
+
}
|
|
75
|
+
|
|
72
76
|
public func isDownloaded() -> Bool {
|
|
73
77
|
return !self.isBuiltin() && self.downloaded != "" &&
|
|
74
78
|
self.downloaded != BundleInfo.DOWNLOADED_BUILTIN && !self.isDeleted() && !self.isDeleting()
|
|
@@ -96,7 +96,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
96
96
|
deinit {
|
|
97
97
|
implementation.shutdown()
|
|
98
98
|
}
|
|
99
|
-
private let pluginVersion: String = "8.51.
|
|
99
|
+
private let pluginVersion: String = "8.51.16"
|
|
100
100
|
private let launchStartedAtMs = Int64(Date().timeIntervalSince1970 * 1000)
|
|
101
101
|
static let updateUrlDefault = "https://plugin.capgo.app/updates"
|
|
102
102
|
static let statsUrlDefault = "https://plugin.capgo.app/stats"
|
|
@@ -191,9 +191,12 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
191
191
|
|
|
192
192
|
// Lock to ensure cleanup completes before downloads start
|
|
193
193
|
private let cleanupLock = NSLock()
|
|
194
|
+
// Short lock for cleanupComplete/cleanupTimedOut only. Never hold across cleanupGroup.wait.
|
|
195
|
+
private let cleanupStateLock = NSLock()
|
|
194
196
|
private let defaultChannelStateLock = NSLock()
|
|
195
197
|
private let cleanupGroup = DispatchGroup()
|
|
196
198
|
private var cleanupComplete = false
|
|
199
|
+
private var cleanupTimedOut = false
|
|
197
200
|
private var cleanupThread: Thread?
|
|
198
201
|
private var defaultChannelCleanupMustRetry = false
|
|
199
202
|
private var persistCustomId = false
|
|
@@ -216,6 +219,8 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
216
219
|
// Best-effort flag: set before we expect notifyAppReady (load/reload).
|
|
217
220
|
// No lock — never held across waits; a rare race only mis-times one wait.
|
|
218
221
|
private var pendingNotifyAppReady = false
|
|
222
|
+
private let semaphoreWaitTestingLock = NSLock()
|
|
223
|
+
private var didEnterSemaphoreWaitForTesting = false
|
|
219
224
|
|
|
220
225
|
private var delayUpdateUtils: DelayUpdateUtils!
|
|
221
226
|
|
|
@@ -427,7 +432,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
427
432
|
|
|
428
433
|
// Downloads (including shake-menu / CapgoUpdater entry points) wait on this gate.
|
|
429
434
|
self.implementation.beforeDownload = { [weak self] in
|
|
430
|
-
self?.waitForCleanupIfNeeded()
|
|
435
|
+
try self?.waitForCleanupIfNeeded()
|
|
431
436
|
}
|
|
432
437
|
// Always run async cleanup: delete obsolete bundles on native update (when enabled)
|
|
433
438
|
// and sweep orphan directories every launch. Must not block app startup.
|
|
@@ -594,6 +599,9 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
594
599
|
|
|
595
600
|
private func semaphoreWait(waitTime: Int) {
|
|
596
601
|
// print("\\(CapgoUpdater.TAG) semaphoreWait \\(waitTime)")
|
|
602
|
+
semaphoreWaitTestingLock.lock()
|
|
603
|
+
didEnterSemaphoreWaitForTesting = true
|
|
604
|
+
semaphoreWaitTestingLock.unlock()
|
|
597
605
|
let result = semaphoreReady.wait(timeout: .now() + .milliseconds(waitTime))
|
|
598
606
|
if result == .timedOut {
|
|
599
607
|
logger.error("Semaphore wait timed out after \(waitTime)ms")
|
|
@@ -1035,9 +1043,12 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1035
1043
|
}
|
|
1036
1044
|
|
|
1037
1045
|
private func cleanupObsoleteVersions(resetWhenUpdate: Bool = true, didResetCurrentBundle: Bool = false) {
|
|
1038
|
-
// Enter before
|
|
1046
|
+
// Enter before publishing incomplete state so waiters cannot hit an empty group.
|
|
1047
|
+
self.cleanupStateLock.lock()
|
|
1039
1048
|
self.cleanupComplete = false
|
|
1049
|
+
self.cleanupTimedOut = false
|
|
1040
1050
|
self.cleanupGroup.enter()
|
|
1051
|
+
self.cleanupStateLock.unlock()
|
|
1041
1052
|
cleanupThread = Thread {
|
|
1042
1053
|
let bgTaskLock = NSLock()
|
|
1043
1054
|
var cleanupBackgroundTask = UIBackgroundTaskIdentifier.invalid
|
|
@@ -1054,7 +1065,9 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1054
1065
|
}
|
|
1055
1066
|
self.cleanupLock.lock()
|
|
1056
1067
|
defer {
|
|
1068
|
+
self.cleanupStateLock.lock()
|
|
1057
1069
|
self.cleanupComplete = true
|
|
1070
|
+
self.cleanupStateLock.unlock()
|
|
1058
1071
|
self.cleanupLock.unlock()
|
|
1059
1072
|
self.cleanupGroup.leave()
|
|
1060
1073
|
endCleanupBackgroundTask()
|
|
@@ -1139,17 +1152,39 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1139
1152
|
cleanupThread?.start()
|
|
1140
1153
|
}
|
|
1141
1154
|
|
|
1142
|
-
private func
|
|
1143
|
-
|
|
1155
|
+
private func cleanupTimeoutError() -> NSError {
|
|
1156
|
+
NSError(
|
|
1157
|
+
domain: "CapacitorUpdater",
|
|
1158
|
+
code: 1,
|
|
1159
|
+
userInfo: [NSLocalizedDescriptionKey: "Cleanup did not finish before download"]
|
|
1160
|
+
)
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
private func waitForCleanupIfNeeded() throws {
|
|
1164
|
+
cleanupStateLock.lock()
|
|
1165
|
+
let alreadyDone = cleanupComplete
|
|
1166
|
+
let alreadyTimedOut = cleanupTimedOut
|
|
1167
|
+
cleanupStateLock.unlock()
|
|
1168
|
+
if alreadyDone {
|
|
1144
1169
|
return // Already done, no need to wait
|
|
1145
1170
|
}
|
|
1171
|
+
if alreadyTimedOut {
|
|
1172
|
+
throw cleanupTimeoutError()
|
|
1173
|
+
}
|
|
1146
1174
|
|
|
1147
1175
|
logger.info("Waiting for cleanup to complete before starting download...")
|
|
1148
1176
|
let result = cleanupGroup.wait(timeout: .now() + .seconds(60))
|
|
1149
1177
|
if result == .timedOut {
|
|
1150
1178
|
logger.warn("Cleanup wait timed out after 60s, cancelling leftover cleanup")
|
|
1151
1179
|
cleanupThread?.cancel()
|
|
1152
|
-
|
|
1180
|
+
let cancelled = cleanupGroup.wait(timeout: .now() + .seconds(60))
|
|
1181
|
+
if cancelled == .timedOut {
|
|
1182
|
+
cleanupStateLock.lock()
|
|
1183
|
+
cleanupTimedOut = true
|
|
1184
|
+
cleanupStateLock.unlock()
|
|
1185
|
+
logger.error("Cleanup did not finish after cancel, aborting download")
|
|
1186
|
+
throw cleanupTimeoutError()
|
|
1187
|
+
}
|
|
1153
1188
|
return
|
|
1154
1189
|
}
|
|
1155
1190
|
logger.info("Cleanup finished, proceeding with download")
|
|
@@ -1568,8 +1603,6 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1568
1603
|
}
|
|
1569
1604
|
|
|
1570
1605
|
private func downloadBundle(urlString: String, version: String, sessionKey: String, checksum rawChecksum: String, manifestEntries: [ManifestEntry]?) throws -> BundleInfo {
|
|
1571
|
-
// Manual/preview downloads must wait too - launch orphan sweep can delete their temps.
|
|
1572
|
-
self.waitForCleanupIfNeeded()
|
|
1573
1606
|
guard let url = URL(string: urlString) else {
|
|
1574
1607
|
throw makePreviewError("Invalid download URL")
|
|
1575
1608
|
}
|
|
@@ -1809,6 +1842,14 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1809
1842
|
}
|
|
1810
1843
|
}
|
|
1811
1844
|
|
|
1845
|
+
private func queueBundleForNextBackgroundInstall(_ next: BundleInfo) -> Bool {
|
|
1846
|
+
guard self.implementation.setNextBundle(next: next.getId()) else {
|
|
1847
|
+
self.logger.error("Failed to queue downloaded bundle as next: \(next.toString())")
|
|
1848
|
+
return false
|
|
1849
|
+
}
|
|
1850
|
+
return true
|
|
1851
|
+
}
|
|
1852
|
+
|
|
1812
1853
|
private func applyDownloadedBundleForDirectUpdate(_ next: BundleInfo) -> Bool {
|
|
1813
1854
|
let previousState = self.implementation.captureResetState()
|
|
1814
1855
|
let previousBundleName = self.implementation.getCurrentBundle().getVersionName()
|
|
@@ -3993,6 +4034,13 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
3993
4034
|
isAutoUpdateModeEnabled(autoUpdateMode) && autoUpdateMode != autoUpdateModeOnlyDownload
|
|
3994
4035
|
}
|
|
3995
4036
|
|
|
4037
|
+
/// Incomplete leftovers must be downloaded again. Android already skips
|
|
4038
|
+
/// `DOWNLOADING` here; iOS used to treat them as ready, then `setNextBundle`
|
|
4039
|
+
/// failed quietly and the update never applied.
|
|
4040
|
+
static func shouldRetryDownloadForExistingBundle(_ bundle: BundleInfo) -> Bool {
|
|
4041
|
+
bundle.isDeleted() || bundle.isDeleting() || bundle.isDownloading()
|
|
4042
|
+
}
|
|
4043
|
+
|
|
3996
4044
|
static func isDirectUpdateMode(_ directUpdateMode: String) -> Bool {
|
|
3997
4045
|
directUpdateMode == autoUpdateModeInstall || directUpdateMode == autoUpdateModeLaunch || directUpdateMode == autoUpdateModeAlways
|
|
3998
4046
|
}
|
|
@@ -4063,6 +4111,18 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
4063
4111
|
pendingNotifyAppReady = true
|
|
4064
4112
|
}
|
|
4065
4113
|
|
|
4114
|
+
func resetSemaphoreWaitTestingStateForTesting() {
|
|
4115
|
+
semaphoreWaitTestingLock.lock()
|
|
4116
|
+
didEnterSemaphoreWaitForTesting = false
|
|
4117
|
+
semaphoreWaitTestingLock.unlock()
|
|
4118
|
+
}
|
|
4119
|
+
|
|
4120
|
+
var didEnterSemaphoreWaitForTestingState: Bool {
|
|
4121
|
+
semaphoreWaitTestingLock.lock()
|
|
4122
|
+
defer { semaphoreWaitTestingLock.unlock() }
|
|
4123
|
+
return didEnterSemaphoreWaitForTesting
|
|
4124
|
+
}
|
|
4125
|
+
|
|
4066
4126
|
func clearPendingNotifyAppReadyForTesting() {
|
|
4067
4127
|
clearPendingNotifyAppReady()
|
|
4068
4128
|
}
|
|
@@ -4298,7 +4358,14 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
4298
4358
|
return
|
|
4299
4359
|
}
|
|
4300
4360
|
// File mutations wait here. getLatest already ran in parallel with cleanup.
|
|
4301
|
-
|
|
4361
|
+
do {
|
|
4362
|
+
try self.waitForCleanupIfNeeded()
|
|
4363
|
+
} catch {
|
|
4364
|
+
self.logger.error("Cleanup still running, skipping download")
|
|
4365
|
+
self.clearDownloadInProgressState()
|
|
4366
|
+
self.endBackGroundTask()
|
|
4367
|
+
return
|
|
4368
|
+
}
|
|
4302
4369
|
current = self.implementation.getCurrentBundle()
|
|
4303
4370
|
if res.version == "builtin" {
|
|
4304
4371
|
self.logger.info("Latest version is builtin")
|
|
@@ -4361,14 +4428,16 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
4361
4428
|
do {
|
|
4362
4429
|
self.logger.info("New bundle: \(latestVersionName) found. Current is: \(current.getVersionName()). \(messageUpdate)")
|
|
4363
4430
|
var nextImpl = self.implementation.getBundleInfoByVersionName(version: latestVersionName)
|
|
4364
|
-
|
|
4365
|
-
|
|
4366
|
-
|
|
4367
|
-
|
|
4368
|
-
|
|
4369
|
-
|
|
4431
|
+
let needsDownload = nextImpl.map(Self.shouldRetryDownloadForExistingBundle) ?? true
|
|
4432
|
+
if needsDownload {
|
|
4433
|
+
if let existing = nextImpl {
|
|
4434
|
+
self.logger.info("Latest bundle already exists in incomplete state (\(existing.getStatus())) and will be deleted, download will overwrite it.")
|
|
4435
|
+
_ = self.implementation.setNextBundle(next: Optional<String>.none)
|
|
4436
|
+
let deleted = self.implementation.delete(id: existing.getId(), removeInfo: true)
|
|
4437
|
+
if deleted {
|
|
4438
|
+
self.logger.info("Incomplete bundle deleted: \(existing.toString())")
|
|
4370
4439
|
} else {
|
|
4371
|
-
self.logger.error("Failed to delete
|
|
4440
|
+
self.logger.error("Failed to delete incomplete bundle: \(existing.toString())")
|
|
4372
4441
|
}
|
|
4373
4442
|
}
|
|
4374
4443
|
self.consumeOnLaunchDirectUpdateAttempt(plannedDirectUpdate: plannedDirectUpdate)
|
|
@@ -4455,8 +4524,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
4455
4524
|
error: false,
|
|
4456
4525
|
plannedDirectUpdate: plannedDirectUpdate
|
|
4457
4526
|
)
|
|
4458
|
-
} else {
|
|
4459
|
-
_ = self.implementation.setNextBundle(next: next.getId())
|
|
4527
|
+
} else if self.queueBundleForNextBackgroundInstall(next) {
|
|
4460
4528
|
self.notifyListeners("updateAvailable", data: ["bundle": next.toJSON()])
|
|
4461
4529
|
self.endBackGroundTaskWithNotif(
|
|
4462
4530
|
msg: "Direct update reload failed, update will install next background",
|
|
@@ -4465,20 +4533,35 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
4465
4533
|
error: false,
|
|
4466
4534
|
plannedDirectUpdate: plannedDirectUpdate
|
|
4467
4535
|
)
|
|
4536
|
+
} else {
|
|
4537
|
+
self.endBackGroundTaskWithNotif(
|
|
4538
|
+
msg: "Direct update reload failed, and next bundle could not be queued",
|
|
4539
|
+
latestVersionName: latestVersionName,
|
|
4540
|
+
current: current,
|
|
4541
|
+
plannedDirectUpdate: plannedDirectUpdate
|
|
4542
|
+
)
|
|
4468
4543
|
}
|
|
4469
4544
|
} else if self.shouldAutoSetNextBundle() {
|
|
4470
4545
|
if plannedDirectUpdate && !directUpdateAllowed {
|
|
4471
4546
|
self.logger.info("Direct update skipped because splashscreen timeout occurred. Update will install on next app background.")
|
|
4472
4547
|
}
|
|
4473
|
-
self.
|
|
4474
|
-
|
|
4475
|
-
|
|
4476
|
-
|
|
4477
|
-
|
|
4478
|
-
|
|
4479
|
-
|
|
4480
|
-
|
|
4481
|
-
|
|
4548
|
+
if self.queueBundleForNextBackgroundInstall(next) {
|
|
4549
|
+
self.notifyListeners("updateAvailable", data: ["bundle": next.toJSON()])
|
|
4550
|
+
self.endBackGroundTaskWithNotif(
|
|
4551
|
+
msg: "update downloaded, will install next background",
|
|
4552
|
+
latestVersionName: latestVersionName,
|
|
4553
|
+
current: current,
|
|
4554
|
+
error: false,
|
|
4555
|
+
plannedDirectUpdate: plannedDirectUpdate
|
|
4556
|
+
)
|
|
4557
|
+
} else {
|
|
4558
|
+
self.endBackGroundTaskWithNotif(
|
|
4559
|
+
msg: "Update downloaded, but next bundle could not be queued",
|
|
4560
|
+
latestVersionName: latestVersionName,
|
|
4561
|
+
current: current,
|
|
4562
|
+
plannedDirectUpdate: plannedDirectUpdate
|
|
4563
|
+
)
|
|
4564
|
+
}
|
|
4482
4565
|
} else {
|
|
4483
4566
|
self.logger.info("autoUpdate is set to onlyDownload, downloaded update will not be set as next bundle")
|
|
4484
4567
|
self.notifyListeners("updateAvailable", data: ["bundle": next.toJSON()], retainUntilConsumed: true)
|
|
@@ -0,0 +1,36 @@
|
|
|
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
|
+
import Foundation
|
|
8
|
+
import Security
|
|
9
|
+
import os.log
|
|
10
|
+
|
|
11
|
+
/// Isolated raw RSA primitives for Capgo v2 checksum/session-key recovery.
|
|
12
|
+
/// SonarCloud automatic analysis has no Swift // NOSONAR; swift:S5542 is ignored
|
|
13
|
+
/// via `.sonarcloud.properties` multicriteria because `.rsaEncryptionRaw` is
|
|
14
|
+
/// intentional wire compatibility with Node `privateEncrypt` / `publicDecrypt`.
|
|
15
|
+
enum CapgoRawRsa {
|
|
16
|
+
private static let log = OSLog(subsystem: "ee.forgr.capacitor_updater", category: "RSA")
|
|
17
|
+
|
|
18
|
+
static func supportsCapgoRecovery(key: SecKey) -> Bool {
|
|
19
|
+
SecKeyGetBlockSize(key) == 256
|
|
20
|
+
&& SecKeyIsAlgorithmSupported(key, .encrypt, .rsaEncryptionRaw) // NOSONAR intentional raw RSA for Node privateEncrypt / Capgo checksum recovery
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
static func recoverSignatureBlock(key: SecKey, data: Data) -> Data? {
|
|
24
|
+
var error: Unmanaged<CFError>?
|
|
25
|
+
guard let recovered = SecKeyCreateEncryptedData(key, .rsaEncryptionRaw, data as CFData, &error) as Data? else { // NOSONAR intentional raw RSA for Node privateEncrypt / Capgo checksum recovery
|
|
26
|
+
os_log(
|
|
27
|
+
"RSA raw recovery failed: %{public}@",
|
|
28
|
+
log: log,
|
|
29
|
+
type: .error,
|
|
30
|
+
String(describing: error?.takeRetainedValue())
|
|
31
|
+
)
|
|
32
|
+
return nil
|
|
33
|
+
}
|
|
34
|
+
return recovered
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -45,7 +45,7 @@ import UIKit
|
|
|
45
45
|
public var timeout: Double = 20
|
|
46
46
|
public var statsUrl: String = ""
|
|
47
47
|
/// Optional gate run before any download touches disk (e.g. wait for launch cleanup).
|
|
48
|
-
public var beforeDownload: (() -> Void)?
|
|
48
|
+
public var beforeDownload: (() throws -> Void)?
|
|
49
49
|
public var channelUrl: String = ""
|
|
50
50
|
public var defaultChannel: String = ""
|
|
51
51
|
public var appId: String = ""
|
|
@@ -928,10 +928,7 @@ import UIKit
|
|
|
928
928
|
|
|
929
929
|
struct ManifestLookupEntry {
|
|
930
930
|
let hash: String
|
|
931
|
-
/// The manifest's own file name, `.br` suffix included when present.
|
|
932
|
-
/// built-in bundle stores files under this exact name (see
|
|
933
|
-
/// isManifestEntryAvailableLocally), unlike the extracted/cached copy which
|
|
934
|
-
/// is always named without the suffix.
|
|
931
|
+
/// The manifest's own file name, `.br` suffix included when present.
|
|
935
932
|
let originalFileName: String
|
|
936
933
|
}
|
|
937
934
|
|
|
@@ -994,10 +991,14 @@ import UIKit
|
|
|
994
991
|
|
|
995
992
|
// Builtin is already a permanent reuse source (see isManifestEntryAvailableLocally),
|
|
996
993
|
// so there's no need to also duplicate this file into the delta cache
|
|
997
|
-
let
|
|
998
|
-
let
|
|
999
|
-
let
|
|
1000
|
-
|
|
994
|
+
let builtinLookupName = knownEntry?.originalFileName ?? relativePath
|
|
995
|
+
let isBuiltinOrigin: Bool
|
|
996
|
+
if let builtinFilePath = try? Self.resolveManifestTargetPath(baseDirectory: builtinFolder, fileName: builtinLookupName) {
|
|
997
|
+
isBuiltinOrigin = fileManager.fileExists(atPath: builtinFilePath.path) &&
|
|
998
|
+
verifyChecksum(file: builtinFilePath, expectedHash: checksum)
|
|
999
|
+
} else {
|
|
1000
|
+
isBuiltinOrigin = false
|
|
1001
|
+
}
|
|
1001
1002
|
if isBuiltinOrigin {
|
|
1002
1003
|
continue
|
|
1003
1004
|
}
|
|
@@ -1231,7 +1232,10 @@ import UIKit
|
|
|
1231
1232
|
}
|
|
1232
1233
|
|
|
1233
1234
|
let builtinFolder = self.builtinFolderURL()
|
|
1234
|
-
|
|
1235
|
+
// The .br suffix describes transport; builtin assets are uncompressed.
|
|
1236
|
+
guard let builtinFilePath = try? Self.resolveManifestTargetPath(baseDirectory: builtinFolder, fileName: fileName) else {
|
|
1237
|
+
return false
|
|
1238
|
+
}
|
|
1235
1239
|
if FileManager.default.fileExists(atPath: builtinFilePath.path) && verifyChecksum(file: builtinFilePath, expectedHash: fileHash) {
|
|
1236
1240
|
return true
|
|
1237
1241
|
}
|
|
@@ -1365,12 +1369,12 @@ import UIKit
|
|
|
1365
1369
|
return json
|
|
1366
1370
|
}
|
|
1367
1371
|
|
|
1368
|
-
private func runBeforeDownload() {
|
|
1369
|
-
beforeDownload?()
|
|
1372
|
+
private func runBeforeDownload() throws {
|
|
1373
|
+
try beforeDownload?()
|
|
1370
1374
|
}
|
|
1371
1375
|
|
|
1372
1376
|
public func downloadManifest(manifest: [ManifestEntry], version: String, sessionKey: String, link: String? = nil, comment: String? = nil) throws -> BundleInfo {
|
|
1373
|
-
self.runBeforeDownload()
|
|
1377
|
+
try self.runBeforeDownload()
|
|
1374
1378
|
let id = self.randomString(length: 10)
|
|
1375
1379
|
logger.info("downloadManifest start \(id)")
|
|
1376
1380
|
let destFolder = self.getBundleDirectory(id: id)
|
|
@@ -1474,7 +1478,7 @@ import UIKit
|
|
|
1474
1478
|
let builtinFilePath: URL
|
|
1475
1479
|
do {
|
|
1476
1480
|
destFilePath = try Self.resolveManifestTargetPath(baseDirectory: destFolder, fileName: fileName)
|
|
1477
|
-
builtinFilePath = try Self.
|
|
1481
|
+
builtinFilePath = try Self.resolveManifestTargetPath(baseDirectory: builtinFolder, fileName: fileName)
|
|
1478
1482
|
} catch {
|
|
1479
1483
|
logger.error("Invalid manifest file path: \(fileName)")
|
|
1480
1484
|
self.sendStats(action: "manifest_path_fail", versionName: "\(version):\(fileName)")
|
|
@@ -1973,7 +1977,7 @@ import UIKit
|
|
|
1973
1977
|
}
|
|
1974
1978
|
|
|
1975
1979
|
public func download(url: URL, version: String, sessionKey: String, link: String? = nil, comment: String? = nil) throws -> BundleInfo {
|
|
1976
|
-
self.runBeforeDownload()
|
|
1980
|
+
try self.runBeforeDownload()
|
|
1977
1981
|
let id: String = self.randomString(length: 10)
|
|
1978
1982
|
// Each download uses its own temp files keyed by bundle ID to prevent collisions
|
|
1979
1983
|
if version != getLocalUpdateVersion(for: id) {
|
|
@@ -1,274 +1,96 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
///
|
|
7
|
-
/// Constants
|
|
8
|
-
///
|
|
9
|
-
private enum RSAConstants {
|
|
10
|
-
static let rsaKeySizeInBits: NSNumber = 2048
|
|
11
|
-
static let rsaAlgorithm: SecKeyAlgorithm = .rsaEncryptionOAEPSHA256
|
|
12
|
-
}
|
|
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
|
+
*/
|
|
13
6
|
|
|
14
|
-
|
|
15
|
-
|
|
7
|
+
import Foundation
|
|
8
|
+
import Security
|
|
9
|
+
import os.log
|
|
16
10
|
|
|
17
|
-
///
|
|
18
|
-
///
|
|
19
|
-
///
|
|
11
|
+
/// Capgo v2 uses Node's privateEncrypt with PKCS#1 v1.5 type-1 padding for
|
|
12
|
+
/// checksums and session keys. Recover the signed payload using Apple's RSA
|
|
13
|
+
/// implementation instead of custom modular exponentiation.
|
|
20
14
|
public struct RSAPublicKey {
|
|
21
|
-
private let
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
///
|
|
28
|
-
/// Takes the data and uses the public key to decrypt it.
|
|
29
|
-
/// Returns the decrypted data.
|
|
30
|
-
///
|
|
31
|
-
public func decrypt(data: Data) -> Data? {
|
|
32
|
-
return manualKey.decrypt(data)
|
|
33
|
-
}
|
|
15
|
+
private let key: SecKey
|
|
16
|
+
private static let keyLock = NSLock()
|
|
17
|
+
private static let log = OSLog(subsystem: "ee.forgr.capacitor_updater", category: "RSA")
|
|
18
|
+
// One public key only: bounded storage, invalid/rotated keys never fall back
|
|
19
|
+
// to a cached key with different material. SecKey is immutable after import.
|
|
20
|
+
private static var cachedKey: (der: Data, key: SecKey)?
|
|
34
21
|
|
|
35
|
-
///
|
|
36
|
-
/// Allows you to load an RSA public key (i.e. one downloaded from the net).
|
|
37
|
-
///
|
|
38
22
|
public static func load(rsaPublicKey: String) -> RSAPublicKey? {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
pubKey = pubKey.replacingOccurrences(of: "-----BEGIN RSA PUBLIC KEY-----", with: "")
|
|
42
|
-
pubKey = pubKey.replacingOccurrences(of: "-----END RSA PUBLIC KEY-----", with: "")
|
|
43
|
-
pubKey = pubKey.replacingOccurrences(of: "-----BEGIN PUBLIC KEY-----", with: "")
|
|
44
|
-
pubKey = pubKey.replacingOccurrences(of: "-----END PUBLIC KEY-----", with: "")
|
|
45
|
-
pubKey = pubKey.replacingOccurrences(of: "\\n+", with: "", options: .regularExpression)
|
|
46
|
-
pubKey = pubKey.replacingOccurrences(of: "\n", with: "")
|
|
47
|
-
pubKey = pubKey.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
48
|
-
|
|
49
|
-
do {
|
|
50
|
-
guard let rsaPublicKeyData: Data = Data(base64Encoded: String(pubKey)) else {
|
|
51
|
-
throw CustomError.cannotDecode
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
// Try parsing as PKCS#1
|
|
55
|
-
if let manualKey = ManualRSAPublicKey.fromPKCS1(rsaPublicKeyData) {
|
|
56
|
-
return RSAPublicKey(manualKey: manualKey)
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
// Most common public exponent is 65537 (0x010001)
|
|
60
|
-
let commonExponent = Data([0x01, 0x00, 0x01]) // 65537 in big-endian
|
|
61
|
-
|
|
62
|
-
// Assume the entire key data is the modulus
|
|
63
|
-
let lastResortKey = ManualRSAPublicKey(modulus: rsaPublicKeyData, exponent: commonExponent)
|
|
64
|
-
return RSAPublicKey(manualKey: lastResortKey)
|
|
65
|
-
} catch {
|
|
66
|
-
return nil
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// Manual RSA Public Key Implementation using the BigInt library
|
|
72
|
-
struct ManualRSAPublicKey {
|
|
73
|
-
let modulus: BigInt
|
|
74
|
-
let exponent: BigInt
|
|
75
|
-
|
|
76
|
-
init(modulus: Data, exponent: Data) {
|
|
77
|
-
// Create positive BigInts from Data
|
|
78
|
-
let modulusBytes = [UInt8](modulus)
|
|
79
|
-
var modulusValue = BigUInt(0)
|
|
80
|
-
for byte in modulusBytes {
|
|
81
|
-
modulusValue = (modulusValue << 8) | BigUInt(byte)
|
|
82
|
-
}
|
|
83
|
-
self.modulus = BigInt(modulusValue)
|
|
84
|
-
let exponentBytes = [UInt8](exponent)
|
|
85
|
-
var exponentValue = BigUInt(0)
|
|
86
|
-
for byte in exponentBytes {
|
|
87
|
-
exponentValue = (exponentValue << 8) | BigUInt(byte)
|
|
88
|
-
}
|
|
89
|
-
self.exponent = BigInt(exponentValue)
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
// Parse PKCS#1 format public key
|
|
93
|
-
static func fromPKCS1(_ publicKeyData: Data) -> ManualRSAPublicKey? {
|
|
94
|
-
// Parse ASN.1 DER encoded RSA public key
|
|
95
|
-
// Format: RSAPublicKey ::= SEQUENCE { modulus INTEGER, publicExponent INTEGER }
|
|
96
|
-
|
|
97
|
-
guard publicKeyData.count > 0 else {
|
|
98
|
-
return nil
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
let bytes = [UInt8](publicKeyData)
|
|
102
|
-
|
|
103
|
-
// Check for sequence tag (0x30)
|
|
104
|
-
guard bytes[0] == 0x30 else {
|
|
105
|
-
// Try direct modulus/exponent approach as fallback
|
|
106
|
-
if publicKeyData.count >= 3 {
|
|
107
|
-
// Assume this is a raw RSA public key with modulus + exponent
|
|
108
|
-
// Most common: modulus is 256 bytes (2048 bits), exponent is 3 bytes (0x010001 = 65537)
|
|
109
|
-
let modulusSize = publicKeyData.count - 3
|
|
110
|
-
if modulusSize > 0 {
|
|
111
|
-
let modulusData = publicKeyData.prefix(modulusSize)
|
|
112
|
-
let exponentData = publicKeyData.suffix(3)
|
|
113
|
-
return ManualRSAPublicKey(modulus: modulusData, exponent: exponentData)
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
return nil
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
var index = 1
|
|
121
|
-
|
|
122
|
-
// Skip length
|
|
123
|
-
if bytes[index] & 0x80 != 0 {
|
|
124
|
-
let lenBytes = Int(bytes[index] & 0x7F)
|
|
125
|
-
if (index + 1 + lenBytes) >= bytes.count {
|
|
126
|
-
return nil
|
|
127
|
-
}
|
|
128
|
-
index += 1 + lenBytes
|
|
129
|
-
} else {
|
|
130
|
-
index += 1
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
// Check for INTEGER tag for modulus (0x02)
|
|
134
|
-
if index >= bytes.count {
|
|
135
|
-
return nil
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
guard bytes[index] == 0x02 else {
|
|
139
|
-
return nil
|
|
140
|
-
}
|
|
141
|
-
index += 1
|
|
142
|
-
|
|
143
|
-
// Get modulus length
|
|
144
|
-
if index >= bytes.count {
|
|
145
|
-
return nil
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
var modulusLength = 0
|
|
149
|
-
if bytes[index] & 0x80 != 0 {
|
|
150
|
-
let lenBytes = Int(bytes[index] & 0x7F)
|
|
151
|
-
if (index + 1 + lenBytes) >= bytes.count {
|
|
152
|
-
return nil
|
|
153
|
-
}
|
|
154
|
-
index += 1
|
|
155
|
-
for i in 0..<lenBytes {
|
|
156
|
-
modulusLength = (modulusLength << 8) | Int(bytes[index + i])
|
|
157
|
-
}
|
|
158
|
-
index += lenBytes
|
|
159
|
-
} else {
|
|
160
|
-
modulusLength = Int(bytes[index])
|
|
161
|
-
index += 1
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
// Skip any leading zero in modulus (for unsigned integer)
|
|
165
|
-
if index < bytes.count && bytes[index] == 0x00 {
|
|
166
|
-
index += 1
|
|
167
|
-
modulusLength -= 1
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
// Extract modulus
|
|
171
|
-
if (index + modulusLength) > bytes.count {
|
|
172
|
-
return nil
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
let modulusData = Data(bytes[index..<(index + modulusLength)])
|
|
176
|
-
index += modulusLength
|
|
177
|
-
|
|
178
|
-
// Check for INTEGER tag for exponent
|
|
179
|
-
if index >= bytes.count {
|
|
23
|
+
guard let der = pemToDer(rsaPublicKey) else {
|
|
24
|
+
os_log("RSA key import failed: invalid PEM/base64", log: Self.log, type: .error)
|
|
180
25
|
return nil
|
|
181
26
|
}
|
|
182
27
|
|
|
183
|
-
|
|
28
|
+
keyLock.lock()
|
|
29
|
+
defer { keyLock.unlock() }
|
|
30
|
+
if let cached = cachedKey, cached.der == der {
|
|
31
|
+
return RSAPublicKey(key: cached.key)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
let attributes: [CFString: Any] = [
|
|
35
|
+
kSecAttrKeyType: kSecAttrKeyTypeRSA,
|
|
36
|
+
kSecAttrKeyClass: kSecAttrKeyClassPublic
|
|
37
|
+
]
|
|
38
|
+
var error: Unmanaged<CFError>?
|
|
39
|
+
guard let key = SecKeyCreateWithData(der as CFData, attributes as CFDictionary, &error) else {
|
|
40
|
+
os_log(
|
|
41
|
+
"RSA key import failed: %{public}@",
|
|
42
|
+
log: Self.log,
|
|
43
|
+
type: .error,
|
|
44
|
+
String(describing: error?.takeRetainedValue())
|
|
45
|
+
)
|
|
184
46
|
return nil
|
|
185
47
|
}
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
48
|
+
guard CapgoRawRsa.supportsCapgoRecovery(key: key) else {
|
|
49
|
+
os_log(
|
|
50
|
+
"RSA key import failed: unsupported key (Capgo requires RSA-2048 with raw encryption)",
|
|
51
|
+
log: Self.log,
|
|
52
|
+
type: .error
|
|
53
|
+
)
|
|
190
54
|
return nil
|
|
191
55
|
}
|
|
56
|
+
cachedKey = (der, key)
|
|
57
|
+
return RSAPublicKey(key: key)
|
|
58
|
+
}
|
|
192
59
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
for i in 0..<lenBytes {
|
|
201
|
-
exponentLength = (exponentLength << 8) | Int(bytes[index + i])
|
|
202
|
-
}
|
|
203
|
-
index += lenBytes
|
|
204
|
-
} else {
|
|
205
|
-
exponentLength = Int(bytes[index])
|
|
206
|
-
index += 1
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
// Extract exponent
|
|
210
|
-
if (index + exponentLength) > bytes.count {
|
|
60
|
+
public func decrypt(data: Data) -> Data? {
|
|
61
|
+
let blockSize = SecKeyGetBlockSize(key)
|
|
62
|
+
guard data.count == blockSize else { return nil }
|
|
63
|
+
// Raw public-key encryption is the same c^e mod n operation as Node's
|
|
64
|
+
// publicDecrypt. Do NOT use encryption PKCS#1 padding here: the recovered
|
|
65
|
+
// block already contains signature (type-1) padding, checked below.
|
|
66
|
+
guard let recovered = CapgoRawRsa.recoverSignatureBlock(key: key, data: data) else {
|
|
211
67
|
return nil
|
|
212
68
|
}
|
|
213
|
-
|
|
214
|
-
let exponentData = Data(bytes[index..<(index + exponentLength)])
|
|
215
|
-
return ManualRSAPublicKey(modulus: modulusData, exponent: exponentData)
|
|
69
|
+
return Self.unpadSignature(recovered, blockSize: blockSize)
|
|
216
70
|
}
|
|
217
71
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
//
|
|
221
|
-
|
|
222
|
-
var
|
|
223
|
-
|
|
224
|
-
|
|
72
|
+
static func unpadSignature(_ block: Data, blockSize: Int = 256) -> Data? {
|
|
73
|
+
let bytes = [UInt8](block)
|
|
74
|
+
// RFC 8017: 00 01, at least eight FF bytes, 00, then the payload.
|
|
75
|
+
guard bytes.count == blockSize, bytes[0] == 0, bytes[1] == 1 else { return nil }
|
|
76
|
+
var separator = 2
|
|
77
|
+
while separator < bytes.count && bytes[separator] == 0xff {
|
|
78
|
+
separator += 1
|
|
225
79
|
}
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
// In Node.js:
|
|
229
|
-
// privateEncrypt uses the private key (d) to encrypt
|
|
230
|
-
// publicDecrypt uses the public key (e) to decrypt
|
|
231
|
-
// The operation we want is: ciphertext^e mod n
|
|
232
|
-
|
|
233
|
-
// RSA operation: c^e mod n
|
|
234
|
-
let decrypted = encrypted.manualPower(exponent, modulus: modulus)
|
|
235
|
-
|
|
236
|
-
// Convert to bytes with proper padding
|
|
237
|
-
guard let bigUIntValue = decrypted.magnitude as? BigUInt else {
|
|
80
|
+
guard separator >= 10, separator < bytes.count - 1, bytes[separator] == 0 else {
|
|
238
81
|
return nil
|
|
239
82
|
}
|
|
83
|
+
return Data(bytes[(separator + 1)...])
|
|
84
|
+
}
|
|
240
85
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
// Ensure we have at least 256 bytes (2048 bits) with leading zeros
|
|
251
|
-
let paddedBytes = [UInt8](repeating: 0, count: max(0, 256 - resultBytes.count)) + resultBytes
|
|
252
|
-
|
|
253
|
-
// For PKCS1 padding from Node.js privateEncrypt, the format is:
|
|
254
|
-
// 0x00 || 0x01 || PS || 0x00 || actual data
|
|
255
|
-
// where PS is a string of 0xFF bytes
|
|
256
|
-
|
|
257
|
-
// Check for privateEncrypt padding format (0x00 || 0x01 || PS || 0x00)
|
|
258
|
-
var startIndex = 0
|
|
259
|
-
if paddedBytes.count > 2 && paddedBytes[0] == 0x00 && paddedBytes[1] == 0x01 {
|
|
260
|
-
for i in 2..<paddedBytes.count {
|
|
261
|
-
if paddedBytes[i] == 0x00 {
|
|
262
|
-
startIndex = i + 1
|
|
263
|
-
break
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
if startIndex < paddedBytes.count {
|
|
268
|
-
let result = Data(paddedBytes[startIndex...])
|
|
269
|
-
return result
|
|
270
|
-
} else {
|
|
271
|
-
return Data(paddedBytes)
|
|
272
|
-
}
|
|
86
|
+
private static func pemToDer(_ pem: String) -> Data? {
|
|
87
|
+
let encoded = pem
|
|
88
|
+
.replacingOccurrences(of: "-----BEGIN RSA PUBLIC KEY-----", with: "")
|
|
89
|
+
.replacingOccurrences(of: "-----END RSA PUBLIC KEY-----", with: "")
|
|
90
|
+
.replacingOccurrences(of: "-----BEGIN PUBLIC KEY-----", with: "")
|
|
91
|
+
.replacingOccurrences(of: "-----END PUBLIC KEY-----", with: "")
|
|
92
|
+
.replacingOccurrences(of: "\\n", with: "")
|
|
93
|
+
.components(separatedBy: .whitespacesAndNewlines).joined()
|
|
94
|
+
return Data(base64Encoded: encoded)
|
|
273
95
|
}
|
|
274
96
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@capgo/capacitor-updater",
|
|
3
|
-
"version": "8.51.
|
|
3
|
+
"version": "8.51.16",
|
|
4
4
|
"license": "MPL-2.0",
|
|
5
5
|
"description": "Live update for capacitor apps",
|
|
6
6
|
"main": "dist/plugin.cjs.js",
|
|
@@ -89,7 +89,7 @@
|
|
|
89
89
|
"@ionic/eslint-config": "^0.4.0",
|
|
90
90
|
"@ionic/prettier-config": "^4.0.0",
|
|
91
91
|
"@ionic/swiftlint-config": "^2.0.0",
|
|
92
|
-
"@types/node": "^
|
|
92
|
+
"@types/node": "^26.0.0",
|
|
93
93
|
"eslint": "^8.57.0",
|
|
94
94
|
"eslint-plugin-import": "^2.32.0",
|
|
95
95
|
"husky": "^9.1.7",
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
import BigInt
|
|
2
|
-
|
|
3
|
-
// Add this custom power function to ensure safer handling of power operations
|
|
4
|
-
|
|
5
|
-
// Manual exponentiation using the square-and-multiply algorithm
|
|
6
|
-
// which is more efficient and avoids using the built-in functions that might handle BigInt differently
|
|
7
|
-
extension BigInt {
|
|
8
|
-
func manualPower(_ exponent: BigInt, modulus: BigInt) -> BigInt {
|
|
9
|
-
// Quick checks
|
|
10
|
-
if modulus == 0 {
|
|
11
|
-
return 0
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
if exponent == 0 {
|
|
15
|
-
return 1
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
guard let base = self.magnitude as? BigUInt,
|
|
19
|
-
let exp = exponent.magnitude as? BigUInt,
|
|
20
|
-
let mod = modulus.magnitude as? BigUInt else {
|
|
21
|
-
return 0
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
// Square and multiply algorithm for modular exponentiation
|
|
25
|
-
var result = BigUInt(1)
|
|
26
|
-
var currentBase = base % mod
|
|
27
|
-
var currentExp = exp
|
|
28
|
-
|
|
29
|
-
while currentExp > 0 {
|
|
30
|
-
if currentExp & 1 == 1 {
|
|
31
|
-
result = (result * currentBase) % mod
|
|
32
|
-
}
|
|
33
|
-
currentBase = (currentBase * currentBase) % mod
|
|
34
|
-
currentExp >>= 1
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
return BigInt(result)
|
|
38
|
-
}
|
|
39
|
-
}
|