@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.
- package/LICENSE +373 -0
- package/README.md +82 -0
- package/dist/esm/definitions.d.ts +2684 -0
- package/dist/esm/definitions.js +109 -0
- package/dist/esm/definitions.js.map +1 -0
- package/dist/esm/exec.d.ts +4 -0
- package/dist/esm/exec.js +25 -0
- package/dist/esm/exec.js.map +1 -0
- package/dist/esm/history.d.ts +1 -0
- package/dist/esm/history.js +283 -0
- package/dist/esm/history.js.map +1 -0
- package/dist/esm/index.d.ts +57 -0
- package/dist/esm/index.js +68 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/plugin.d.ts +57 -0
- package/dist/esm/plugin.js +15 -0
- package/dist/esm/plugin.js.map +1 -0
- package/dist/plugin.cjs.js +217 -0
- package/dist/plugin.cjs.js.map +1 -0
- package/dist/plugin.js +222 -0
- package/dist/plugin.js.map +1 -0
- package/package.json +96 -0
- package/plugin.xml +207 -0
- package/src/android/CordovaUpdaterPlugin.java +5662 -0
- package/src/android/UpdaterPathHandler.java +99 -0
- package/src/android/app/capgo/cordova/updater/AndroidAppExitReporter.java +92 -0
- package/src/android/app/capgo/cordova/updater/AppLifecycleObserver.java +115 -0
- package/src/android/app/capgo/cordova/updater/BundleInfo.java +246 -0
- package/src/android/app/capgo/cordova/updater/BundleStatus.java +45 -0
- package/src/android/app/capgo/cordova/updater/Callback.java +13 -0
- package/src/android/app/capgo/cordova/updater/CapgoUpdater.java +3404 -0
- package/src/android/app/capgo/cordova/updater/CordovaUpdaterConfig.java +56 -0
- package/src/android/app/capgo/cordova/updater/CryptoCipher.java +462 -0
- package/src/android/app/capgo/cordova/updater/DataManager.java +44 -0
- package/src/android/app/capgo/cordova/updater/DelayCondition.java +56 -0
- package/src/android/app/capgo/cordova/updater/DelayUntilNext.java +14 -0
- package/src/android/app/capgo/cordova/updater/DelayUpdateUtils.java +307 -0
- package/src/android/app/capgo/cordova/updater/DeviceIdHelper.java +230 -0
- package/src/android/app/capgo/cordova/updater/DownloadService.java +1219 -0
- package/src/android/app/capgo/cordova/updater/DownloadWorkerManager.java +260 -0
- package/src/android/app/capgo/cordova/updater/InternalUtils.java +54 -0
- package/src/android/app/capgo/cordova/updater/Logger.java +345 -0
- package/src/android/app/capgo/cordova/updater/ShakeDetector.java +72 -0
- package/src/android/app/capgo/cordova/updater/ShakeMenu.java +39 -0
- package/src/android/app/capgo/cordova/updater/ThreeFingerPinchDetector.java +323 -0
- package/src/android/app/capgo/cordova/updater/compat/JSArray.java +9 -0
- package/src/android/app/capgo/cordova/updater/compat/JSObject.java +83 -0
- package/src/android/app/capgo/cordova/updater/compat/PluginCall.java +165 -0
- package/src/android/capgo-cordova-updater.gradle +19 -0
- package/src/android/capgo-updater.xml +3 -0
- package/src/android/test/app/capgo/cordova/updater/CapacitorUpdaterUnitTest.java.skip +3039 -0
- package/src/android/test/app/capgo/cordova/updater/DelayUpdateUtilsTest.java +177 -0
- package/src/android/test/app/capgo/cordova/updater/NativeContractTest.java +105 -0
- package/src/ios/AES.swift +216 -0
- package/src/ios/AppHealthTracker.swift +82 -0
- package/src/ios/BundleInfo.swift +185 -0
- package/src/ios/BundleStatus.swift +127 -0
- package/src/ios/CapgoRawRsa.swift +36 -0
- package/src/ios/CapgoUpdater.swift +3544 -0
- package/src/ios/CordovaCompat.swift +11 -0
- package/src/ios/CordovaPluginCall.swift +190 -0
- package/src/ios/CordovaUpdaterCommands.swift +270 -0
- package/src/ios/CordovaUpdaterPlugin.swift +4706 -0
- package/src/ios/CryptoCipher.swift +326 -0
- package/src/ios/DelayCondition.swift +74 -0
- package/src/ios/DelayUntilNext.swift +30 -0
- package/src/ios/DelayUpdateUtils.swift +257 -0
- package/src/ios/DeviceIdHelper.swift +120 -0
- package/src/ios/Info.plist +28 -0
- package/src/ios/InternalUtils.swift +471 -0
- package/src/ios/Logger.swift +283 -0
- package/src/ios/RSA.swift +96 -0
- package/src/ios/ShakeMenu.swift +52 -0
- package/src/ios/UpdaterSchemeHandler.swift +81 -0
- package/src/ios/UserDefaultsExtension.swift +46 -0
- package/src/ios/WebViewStatsReporter.swift +303 -0
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
package app.capgo.cordova.updater;
|
|
2
|
+
|
|
3
|
+
import static org.junit.Assert.*;
|
|
4
|
+
import static org.mockito.ArgumentMatchers.anyLong;
|
|
5
|
+
import static org.mockito.ArgumentMatchers.anyString;
|
|
6
|
+
import static org.mockito.ArgumentMatchers.eq;
|
|
7
|
+
import static org.mockito.Mockito.*;
|
|
8
|
+
|
|
9
|
+
import android.content.SharedPreferences;
|
|
10
|
+
import io.github.g00fy2.versioncompare.Version;
|
|
11
|
+
import java.util.ArrayList;
|
|
12
|
+
import org.json.JSONArray;
|
|
13
|
+
import org.json.JSONObject;
|
|
14
|
+
import org.junit.Before;
|
|
15
|
+
import org.junit.Test;
|
|
16
|
+
import org.mockito.ArgumentCaptor;
|
|
17
|
+
import org.mockito.Mock;
|
|
18
|
+
import org.mockito.MockitoAnnotations;
|
|
19
|
+
|
|
20
|
+
public class DelayUpdateUtilsTest {
|
|
21
|
+
|
|
22
|
+
@Mock
|
|
23
|
+
private SharedPreferences prefs;
|
|
24
|
+
|
|
25
|
+
@Mock
|
|
26
|
+
private SharedPreferences.Editor editor;
|
|
27
|
+
|
|
28
|
+
@Mock
|
|
29
|
+
private Logger logger;
|
|
30
|
+
|
|
31
|
+
private DelayUpdateUtils utils;
|
|
32
|
+
|
|
33
|
+
@Before
|
|
34
|
+
public void setUp() {
|
|
35
|
+
MockitoAnnotations.openMocks(this);
|
|
36
|
+
|
|
37
|
+
when(prefs.getString(eq(DelayUpdateUtils.DELAY_CONDITION_PREFERENCES), anyString())).thenReturn("[]");
|
|
38
|
+
when(prefs.getLong(eq(DelayUpdateUtils.BACKGROUND_TIMESTAMP_KEY), anyLong())).thenReturn(0L);
|
|
39
|
+
when(editor.putString(anyString(), anyString())).thenReturn(editor);
|
|
40
|
+
when(editor.remove(anyString())).thenReturn(editor);
|
|
41
|
+
when(editor.commit()).thenReturn(true);
|
|
42
|
+
|
|
43
|
+
utils = new DelayUpdateUtils(prefs, editor, new Version("1.0.0"), logger);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
@Test
|
|
47
|
+
public void parseDelayConditions_returnsPopulatedListForValidJson() throws Exception {
|
|
48
|
+
JSONArray jsonArray = new JSONArray();
|
|
49
|
+
jsonArray.put(new JSONObject().put("kind", "background").put("value", "5000"));
|
|
50
|
+
jsonArray.put(new JSONObject().put("kind", "kill").put("value", ""));
|
|
51
|
+
jsonArray.put(new JSONObject().put("kind", "nativeVersion").put("value", "2.0.0"));
|
|
52
|
+
jsonArray.put(new JSONObject().put("kind", "date").put("value", "2023-01-01T00:00:00.000"));
|
|
53
|
+
|
|
54
|
+
ArrayList<DelayCondition> result = utils.parseDelayConditions(jsonArray.toString());
|
|
55
|
+
|
|
56
|
+
assertEquals(4, result.size());
|
|
57
|
+
assertEquals(DelayUntilNext.background, result.get(0).getKind());
|
|
58
|
+
assertEquals("5000", result.get(0).getValue());
|
|
59
|
+
assertEquals(DelayUntilNext.kill, result.get(1).getKind());
|
|
60
|
+
assertEquals(DelayUntilNext.nativeVersion, result.get(2).getKind());
|
|
61
|
+
assertEquals(DelayUntilNext.date, result.get(3).getKind());
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
@Test
|
|
65
|
+
public void parseDelayConditions_skipsInvalidEntries() throws Exception {
|
|
66
|
+
JSONArray jsonArray = new JSONArray();
|
|
67
|
+
jsonArray.put(new JSONObject().put("kind", "unknown").put("value", "123"));
|
|
68
|
+
jsonArray.put(new JSONObject().put("value", "missingKind"));
|
|
69
|
+
jsonArray.put(new JSONObject().put("kind", "kill").put("value", ""));
|
|
70
|
+
|
|
71
|
+
ArrayList<DelayCondition> result = utils.parseDelayConditions(jsonArray.toString());
|
|
72
|
+
|
|
73
|
+
assertEquals(1, result.size());
|
|
74
|
+
assertEquals(DelayUntilNext.kill, result.get(0).getKind());
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
@Test
|
|
78
|
+
public void parseDelayConditions_handlesInvalidJsonSafely() {
|
|
79
|
+
ArrayList<DelayCondition> result = utils.parseDelayConditions("not-json");
|
|
80
|
+
|
|
81
|
+
assertTrue(result.isEmpty());
|
|
82
|
+
verify(logger).error(contains("Failed to parse delay conditions"));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
@Test
|
|
86
|
+
public void checkCancelDelay_foregroundRemovesExpiredBackgroundCondition() throws Exception {
|
|
87
|
+
JSONArray stored = new JSONArray();
|
|
88
|
+
stored.put(new JSONObject().put("kind", "background").put("value", "1"));
|
|
89
|
+
stored.put(new JSONObject().put("kind", "kill").put("value", ""));
|
|
90
|
+
when(prefs.getString(eq(DelayUpdateUtils.DELAY_CONDITION_PREFERENCES), anyString())).thenReturn(stored.toString());
|
|
91
|
+
|
|
92
|
+
utils.checkCancelDelay(DelayUpdateUtils.CancelDelaySource.FOREGROUND);
|
|
93
|
+
|
|
94
|
+
ArgumentCaptor<String> captor = ArgumentCaptor.forClass(String.class);
|
|
95
|
+
verify(editor).putString(eq(DelayUpdateUtils.DELAY_CONDITION_PREFERENCES), captor.capture());
|
|
96
|
+
verify(editor).commit();
|
|
97
|
+
|
|
98
|
+
JSONArray updated = new JSONArray(captor.getValue());
|
|
99
|
+
assertEquals(1, updated.length());
|
|
100
|
+
JSONObject remaining = updated.getJSONObject(0);
|
|
101
|
+
assertEquals("kill", remaining.getString("kind"));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
@Test
|
|
105
|
+
public void checkCancelDelay_killedClearsDelaysWithoutInstalling() throws Exception {
|
|
106
|
+
JSONArray stored = new JSONArray();
|
|
107
|
+
stored.put(new JSONObject().put("kind", "kill").put("value", ""));
|
|
108
|
+
when(prefs.getString(eq(DelayUpdateUtils.DELAY_CONDITION_PREFERENCES), anyString())).thenReturn(stored.toString());
|
|
109
|
+
|
|
110
|
+
utils.checkCancelDelay(DelayUpdateUtils.CancelDelaySource.KILLED);
|
|
111
|
+
|
|
112
|
+
verify(editor).remove(eq(DelayUpdateUtils.DELAY_CONDITION_PREFERENCES));
|
|
113
|
+
verify(editor).commit();
|
|
114
|
+
verify(editor, never()).putString(eq(DelayUpdateUtils.DELAY_CONDITION_PREFERENCES), anyString());
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
@Test
|
|
118
|
+
public void checkCancelDelay_killedKeepsOtherConditions() throws Exception {
|
|
119
|
+
JSONArray stored = new JSONArray();
|
|
120
|
+
stored.put(new JSONObject().put("kind", "kill").put("value", ""));
|
|
121
|
+
stored.put(new JSONObject().put("kind", "background").put("value", "5000"));
|
|
122
|
+
when(prefs.getString(eq(DelayUpdateUtils.DELAY_CONDITION_PREFERENCES), anyString())).thenReturn(stored.toString());
|
|
123
|
+
|
|
124
|
+
utils.checkCancelDelay(DelayUpdateUtils.CancelDelaySource.KILLED);
|
|
125
|
+
|
|
126
|
+
ArgumentCaptor<String> captor = ArgumentCaptor.forClass(String.class);
|
|
127
|
+
verify(editor).putString(eq(DelayUpdateUtils.DELAY_CONDITION_PREFERENCES), captor.capture());
|
|
128
|
+
verify(editor).commit();
|
|
129
|
+
verify(editor, never()).remove(eq(DelayUpdateUtils.DELAY_CONDITION_PREFERENCES));
|
|
130
|
+
|
|
131
|
+
JSONArray updated = new JSONArray(captor.getValue());
|
|
132
|
+
assertEquals(1, updated.length());
|
|
133
|
+
JSONObject remaining = updated.getJSONObject(0);
|
|
134
|
+
assertEquals("background", remaining.getString("kind"));
|
|
135
|
+
assertEquals("5000", remaining.getString("value"));
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
@Test
|
|
139
|
+
public void checkCancelDelay_foregroundKeepsIsoDateWithoutMilliseconds() throws Exception {
|
|
140
|
+
JSONArray stored = new JSONArray();
|
|
141
|
+
stored.put(new JSONObject().put("kind", "date").put("value", "2099-12-31T23:59:59Z"));
|
|
142
|
+
when(prefs.getString(eq(DelayUpdateUtils.DELAY_CONDITION_PREFERENCES), anyString())).thenReturn(stored.toString());
|
|
143
|
+
|
|
144
|
+
utils.checkCancelDelay(DelayUpdateUtils.CancelDelaySource.FOREGROUND);
|
|
145
|
+
|
|
146
|
+
ArgumentCaptor<String> captor = ArgumentCaptor.forClass(String.class);
|
|
147
|
+
verify(editor).putString(eq(DelayUpdateUtils.DELAY_CONDITION_PREFERENCES), captor.capture());
|
|
148
|
+
verify(editor).commit();
|
|
149
|
+
verify(editor, never()).remove(eq(DelayUpdateUtils.DELAY_CONDITION_PREFERENCES));
|
|
150
|
+
|
|
151
|
+
JSONArray updated = new JSONArray(captor.getValue());
|
|
152
|
+
assertEquals(1, updated.length());
|
|
153
|
+
JSONObject remaining = updated.getJSONObject(0);
|
|
154
|
+
assertEquals("date", remaining.getString("kind"));
|
|
155
|
+
assertEquals("2099-12-31T23:59:59Z", remaining.getString("value"));
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
@Test
|
|
159
|
+
public void checkCancelDelay_foregroundKeepsIsoDateWithCompactOffset() throws Exception {
|
|
160
|
+
JSONArray stored = new JSONArray();
|
|
161
|
+
stored.put(new JSONObject().put("kind", "date").put("value", "2099-12-31T23:59:59+0000"));
|
|
162
|
+
when(prefs.getString(eq(DelayUpdateUtils.DELAY_CONDITION_PREFERENCES), anyString())).thenReturn(stored.toString());
|
|
163
|
+
|
|
164
|
+
utils.checkCancelDelay(DelayUpdateUtils.CancelDelaySource.FOREGROUND);
|
|
165
|
+
|
|
166
|
+
ArgumentCaptor<String> captor = ArgumentCaptor.forClass(String.class);
|
|
167
|
+
verify(editor).putString(eq(DelayUpdateUtils.DELAY_CONDITION_PREFERENCES), captor.capture());
|
|
168
|
+
verify(editor).commit();
|
|
169
|
+
verify(editor, never()).remove(eq(DelayUpdateUtils.DELAY_CONDITION_PREFERENCES));
|
|
170
|
+
|
|
171
|
+
JSONArray updated = new JSONArray(captor.getValue());
|
|
172
|
+
assertEquals(1, updated.length());
|
|
173
|
+
JSONObject remaining = updated.getJSONObject(0);
|
|
174
|
+
assertEquals("date", remaining.getString("kind"));
|
|
175
|
+
assertEquals("2099-12-31T23:59:59+0000", remaining.getString("value"));
|
|
176
|
+
}
|
|
177
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
package app.capgo.cordova.updater;
|
|
2
|
+
|
|
3
|
+
import static org.junit.Assert.assertEquals;
|
|
4
|
+
|
|
5
|
+
import java.io.IOException;
|
|
6
|
+
import java.nio.charset.StandardCharsets;
|
|
7
|
+
import java.nio.file.Files;
|
|
8
|
+
import java.nio.file.Path;
|
|
9
|
+
import org.json.JSONArray;
|
|
10
|
+
import org.json.JSONObject;
|
|
11
|
+
import org.junit.Test;
|
|
12
|
+
|
|
13
|
+
public class NativeContractTest {
|
|
14
|
+
|
|
15
|
+
private static JSONObject contract() {
|
|
16
|
+
try {
|
|
17
|
+
return new JSONObject(new String(Files.readAllBytes(contractFile()), StandardCharsets.UTF_8));
|
|
18
|
+
} catch (Exception error) {
|
|
19
|
+
throw new AssertionError("Unable to load native contract fixture", error);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
private static Path contractFile() throws IOException {
|
|
24
|
+
Path current = Path.of(System.getProperty("user.dir")).toAbsolutePath();
|
|
25
|
+
while (current != null) {
|
|
26
|
+
Path candidate = current.resolve("native-contract-tests/core.json");
|
|
27
|
+
if (Files.exists(candidate)) {
|
|
28
|
+
return candidate;
|
|
29
|
+
}
|
|
30
|
+
current = current.getParent();
|
|
31
|
+
}
|
|
32
|
+
throw new IOException("native-contract-tests/core.json not found");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
private static String nullableString(final JSONObject object, final String key) throws Exception {
|
|
36
|
+
if (!object.has(key) || object.isNull(key)) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
return object.getString(key);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
@Test
|
|
43
|
+
public void periodCheckDelayMatchesNativeContract() throws Exception {
|
|
44
|
+
JSONArray cases = contract().getJSONArray("periodCheckDelay");
|
|
45
|
+
for (int index = 0; index < cases.length(); index++) {
|
|
46
|
+
JSONObject testCase = cases.getJSONObject(index);
|
|
47
|
+
String id = testCase.getString("id");
|
|
48
|
+
int seconds = testCase.getJSONObject("input").getInt("seconds");
|
|
49
|
+
int expected = testCase.getJSONObject("expect").getInt("normalizedSeconds");
|
|
50
|
+
|
|
51
|
+
assertEquals(id, expected, CordovaUpdaterPlugin.normalizedPeriodCheckDelaySeconds(seconds));
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
@Test
|
|
56
|
+
public void autoUpdateModeMatchesNativeContract() throws Exception {
|
|
57
|
+
JSONArray cases = contract().getJSONArray("autoUpdateMode");
|
|
58
|
+
for (int index = 0; index < cases.length(); index++) {
|
|
59
|
+
JSONObject testCase = cases.getJSONObject(index);
|
|
60
|
+
String id = testCase.getString("id");
|
|
61
|
+
String mode = testCase.getJSONObject("input").getString("mode");
|
|
62
|
+
JSONObject expect = testCase.getJSONObject("expect");
|
|
63
|
+
String normalizedMode = CordovaUpdaterPlugin.normalizedAutoUpdateMode(mode);
|
|
64
|
+
|
|
65
|
+
assertEquals(id, expect.getString("mode"), normalizedMode);
|
|
66
|
+
assertEquals(id, expect.getBoolean("enabled"), CordovaUpdaterPlugin.isAutoUpdateModeEnabled(normalizedMode));
|
|
67
|
+
assertEquals(
|
|
68
|
+
id,
|
|
69
|
+
expect.getString("directUpdateMode"),
|
|
70
|
+
CordovaUpdaterPlugin.directUpdateModeForAutoUpdateMode(normalizedMode)
|
|
71
|
+
);
|
|
72
|
+
assertEquals(id, expect.getBoolean("setNextBundle"), CordovaUpdaterPlugin.shouldAutoUpdateModeSetNextBundle(normalizedMode));
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
@Test
|
|
77
|
+
public void onLaunchDirectUpdateConsumptionMatchesNativeContract() throws Exception {
|
|
78
|
+
JSONArray cases = contract().getJSONArray("onLaunchDirectUpdateConsumption");
|
|
79
|
+
for (int index = 0; index < cases.length(); index++) {
|
|
80
|
+
JSONObject testCase = cases.getJSONObject(index);
|
|
81
|
+
String id = testCase.getString("id");
|
|
82
|
+
JSONObject input = testCase.getJSONObject("input");
|
|
83
|
+
boolean expected = testCase.getJSONObject("expect").getBoolean("consume");
|
|
84
|
+
|
|
85
|
+
assertEquals(
|
|
86
|
+
id,
|
|
87
|
+
expected,
|
|
88
|
+
CordovaUpdaterPlugin.shouldConsumeOnLaunchDirectUpdate(input.getString("mode"), input.getBoolean("plannedDirectUpdate"))
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
@Test
|
|
94
|
+
public void updateResponseKindMatchesNativeContract() throws Exception {
|
|
95
|
+
JSONArray cases = contract().getJSONArray("updateResponseKind");
|
|
96
|
+
for (int index = 0; index < cases.length(); index++) {
|
|
97
|
+
JSONObject testCase = cases.getJSONObject(index);
|
|
98
|
+
String id = testCase.getString("id");
|
|
99
|
+
String kind = nullableString(testCase.getJSONObject("input"), "kind");
|
|
100
|
+
String expected = testCase.getJSONObject("expect").getString("kind");
|
|
101
|
+
|
|
102
|
+
assertEquals(id, expected, CordovaUpdaterPlugin.normalizedUpdateResponseKind(kind));
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import CommonCrypto
|
|
3
|
+
import CryptoKit
|
|
4
|
+
import Darwin
|
|
5
|
+
|
|
6
|
+
///
|
|
7
|
+
/// Constants
|
|
8
|
+
///
|
|
9
|
+
private enum AESConstants {
|
|
10
|
+
static let aesAlgorithm: CCAlgorithm = CCAlgorithm(kCCAlgorithmAES)
|
|
11
|
+
static let aesOptions: CCOptions = CCOptions(kCCOptionPKCS7Padding)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// We do all this stuff because ios is shit and open source libraries allow to do decryption with public key
|
|
15
|
+
// So we have to do it manually, while in nodejs or Java it's ok and done at language level.
|
|
16
|
+
|
|
17
|
+
///
|
|
18
|
+
/// The AES key. Contains both the initialization vector and secret key.
|
|
19
|
+
///
|
|
20
|
+
public struct AES128Key {
|
|
21
|
+
/// Initialization vector
|
|
22
|
+
private let initVector: Data
|
|
23
|
+
private let logger: Logger
|
|
24
|
+
private let aes128Key: Data
|
|
25
|
+
#if DEBUG
|
|
26
|
+
// swiftlint:disable:next identifier_name
|
|
27
|
+
public var __debug_iv: Data { initVector }
|
|
28
|
+
// swiftlint:disable:next identifier_name
|
|
29
|
+
public var __debug_aes128Key: Data { aes128Key }
|
|
30
|
+
#endif
|
|
31
|
+
// swiftlint:disable:next identifier_name
|
|
32
|
+
init(iv: Data, aes128Key: Data, logger: Logger) {
|
|
33
|
+
self.initVector = iv
|
|
34
|
+
self.aes128Key = aes128Key
|
|
35
|
+
self.logger = logger
|
|
36
|
+
}
|
|
37
|
+
///
|
|
38
|
+
/// Takes the data and uses the private key to decrypt it. Will call `CCCrypt` in CommonCrypto
|
|
39
|
+
/// and provide it `ivData` for the initialization vector. Will use cipher block chaining (CBC) as
|
|
40
|
+
/// the mode of operation.
|
|
41
|
+
///
|
|
42
|
+
/// Returns the decrypted data.
|
|
43
|
+
///
|
|
44
|
+
public func decrypt(data: Data) -> Data? {
|
|
45
|
+
let encryptedData: UnsafePointer<UInt8> = (data as NSData).bytes.bindMemory(
|
|
46
|
+
to: UInt8.self, capacity: data.count)
|
|
47
|
+
let encryptedDataLength: Int = data.count
|
|
48
|
+
|
|
49
|
+
if let result: NSMutableData = NSMutableData(length: encryptedDataLength) {
|
|
50
|
+
let keyData: UnsafePointer<UInt8> = (self.aes128Key as NSData).bytes.bindMemory(
|
|
51
|
+
to: UInt8.self, capacity: self.aes128Key.count)
|
|
52
|
+
let keyLength: size_t = size_t(self.aes128Key.count)
|
|
53
|
+
let ivData: UnsafePointer<UInt8> = (initVector as NSData).bytes.bindMemory(
|
|
54
|
+
to: UInt8.self, capacity: self.initVector.count)
|
|
55
|
+
|
|
56
|
+
let decryptedData: UnsafeMutablePointer<UInt8> = UnsafeMutablePointer<UInt8>(
|
|
57
|
+
result.mutableBytes.assumingMemoryBound(to: UInt8.self))
|
|
58
|
+
let decryptedDataLength: size_t = size_t(result.length)
|
|
59
|
+
|
|
60
|
+
var decryptedLength: size_t = 0
|
|
61
|
+
|
|
62
|
+
let status: CCCryptorStatus = CCCrypt(
|
|
63
|
+
CCOperation(kCCDecrypt),
|
|
64
|
+
AESConstants.aesAlgorithm,
|
|
65
|
+
AESConstants.aesOptions,
|
|
66
|
+
keyData,
|
|
67
|
+
keyLength,
|
|
68
|
+
ivData,
|
|
69
|
+
encryptedData,
|
|
70
|
+
encryptedDataLength,
|
|
71
|
+
decryptedData,
|
|
72
|
+
decryptedDataLength,
|
|
73
|
+
&decryptedLength)
|
|
74
|
+
|
|
75
|
+
if Int32(status) == Int32(kCCSuccess) {
|
|
76
|
+
result.length = Int(decryptedLength)
|
|
77
|
+
return result as Data
|
|
78
|
+
} else {
|
|
79
|
+
logger.error("AES decryption failed with status: \(status)")
|
|
80
|
+
return nil
|
|
81
|
+
}
|
|
82
|
+
} else {
|
|
83
|
+
logger.error("Failed to allocate memory for AES decryption")
|
|
84
|
+
return nil
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/// AES-CBC file-to-file. Never holds the whole ciphertext in RAM.
|
|
89
|
+
func decrypt(from source: URL, to destination: URL) throws {
|
|
90
|
+
var cryptor: CCCryptorRef?
|
|
91
|
+
let createStatus: CCCryptorStatus = aes128Key.withUnsafeBytes { keyBytes in
|
|
92
|
+
initVector.withUnsafeBytes { ivBytes in
|
|
93
|
+
guard let keyPtr = keyBytes.baseAddress, let ivPtr = ivBytes.baseAddress else {
|
|
94
|
+
return CCCryptorStatus(kCCParamError)
|
|
95
|
+
}
|
|
96
|
+
return CCCryptorCreate(
|
|
97
|
+
CCOperation(kCCDecrypt),
|
|
98
|
+
AESConstants.aesAlgorithm,
|
|
99
|
+
AESConstants.aesOptions,
|
|
100
|
+
keyPtr,
|
|
101
|
+
keyBytes.count,
|
|
102
|
+
ivPtr,
|
|
103
|
+
&cryptor
|
|
104
|
+
)
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
guard createStatus == kCCSuccess, let cryptor else {
|
|
108
|
+
logger.error("Failed to create AES cryptor")
|
|
109
|
+
throw NSError(domain: "AESDecryptError", code: Int(createStatus), userInfo: nil)
|
|
110
|
+
}
|
|
111
|
+
defer {
|
|
112
|
+
CCCryptorRelease(cryptor)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
guard let input = InputStream(url: source) else {
|
|
116
|
+
throw NSError(domain: "AESDecryptError", code: 1, userInfo: [NSLocalizedDescriptionKey: "Failed to open AES source"])
|
|
117
|
+
}
|
|
118
|
+
input.open()
|
|
119
|
+
defer {
|
|
120
|
+
input.close()
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
let tempURL = destination.deletingLastPathComponent().appendingPathComponent("capgo-aes-\(UUID().uuidString).tmp")
|
|
124
|
+
let fileManager = FileManager.default
|
|
125
|
+
fileManager.createFile(atPath: tempURL.path, contents: nil)
|
|
126
|
+
let output = try FileHandle(forWritingTo: tempURL)
|
|
127
|
+
defer {
|
|
128
|
+
try? output.close()
|
|
129
|
+
try? fileManager.removeItem(at: tempURL)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
let bufferSize = CryptoCipher.ioBufferBytes()
|
|
133
|
+
let outBufSize = bufferSize + kCCBlockSizeAES128
|
|
134
|
+
var inBuf = [UInt8](repeating: 0, count: bufferSize)
|
|
135
|
+
var outBuf = [UInt8](repeating: 0, count: outBufSize)
|
|
136
|
+
let outFd = output.fileDescriptor
|
|
137
|
+
|
|
138
|
+
while true {
|
|
139
|
+
let n = inBuf.withUnsafeMutableBufferPointer { ptr in
|
|
140
|
+
input.read(ptr.baseAddress!, maxLength: ptr.count)
|
|
141
|
+
}
|
|
142
|
+
if n == 0 {
|
|
143
|
+
break
|
|
144
|
+
}
|
|
145
|
+
if n < 0 {
|
|
146
|
+
throw input.streamError ?? NSError(domain: "AESDecryptError", code: 2, userInfo: [NSLocalizedDescriptionKey: "AES stream read failed"])
|
|
147
|
+
}
|
|
148
|
+
var moved: size_t = 0
|
|
149
|
+
let status: CCCryptorStatus = inBuf.withUnsafeBufferPointer { inRaw in
|
|
150
|
+
outBuf.withUnsafeMutableBytes { outRaw in
|
|
151
|
+
CCCryptorUpdate(
|
|
152
|
+
cryptor,
|
|
153
|
+
inRaw.baseAddress,
|
|
154
|
+
n,
|
|
155
|
+
outRaw.baseAddress,
|
|
156
|
+
outBufSize,
|
|
157
|
+
&moved
|
|
158
|
+
)
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
guard status == kCCSuccess else {
|
|
162
|
+
logger.error("AES stream update failed")
|
|
163
|
+
throw NSError(domain: "AESDecryptError", code: Int(status), userInfo: nil)
|
|
164
|
+
}
|
|
165
|
+
if moved > 0 {
|
|
166
|
+
try Self.writeAll(fd: outFd, buffer: &outBuf, count: moved)
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
var moved: size_t = 0
|
|
171
|
+
let finalStatus: CCCryptorStatus = outBuf.withUnsafeMutableBytes { outRaw in
|
|
172
|
+
CCCryptorFinal(cryptor, outRaw.baseAddress, outBufSize, &moved)
|
|
173
|
+
}
|
|
174
|
+
guard finalStatus == kCCSuccess else {
|
|
175
|
+
logger.error("AES stream finalize failed")
|
|
176
|
+
throw NSError(domain: "AESDecryptError", code: Int(finalStatus), userInfo: nil)
|
|
177
|
+
}
|
|
178
|
+
if moved > 0 {
|
|
179
|
+
try Self.writeAll(fd: outFd, buffer: &outBuf, count: moved)
|
|
180
|
+
}
|
|
181
|
+
try output.close()
|
|
182
|
+
|
|
183
|
+
let decryptedSize = (try fileManager.attributesOfItem(atPath: tempURL.path)[.size] as? NSNumber)?.uint64Value ?? 0
|
|
184
|
+
if decryptedSize == 0 {
|
|
185
|
+
throw NSError(domain: "Empty decrypted data", code: 7, userInfo: nil)
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
do {
|
|
189
|
+
_ = try fileManager.replaceItemAt(destination, withItemAt: tempURL)
|
|
190
|
+
} catch {
|
|
191
|
+
if fileManager.fileExists(atPath: destination.path) {
|
|
192
|
+
throw error
|
|
193
|
+
}
|
|
194
|
+
try fileManager.moveItem(at: tempURL, to: destination)
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
private static func writeAll(fd: Int32, buffer: inout [UInt8], count: Int) throws {
|
|
199
|
+
var offset = 0
|
|
200
|
+
while offset < count {
|
|
201
|
+
let written = buffer.withUnsafeBufferPointer { ptr -> Int in
|
|
202
|
+
Darwin.write(fd, ptr.baseAddress!.advanced(by: offset), count - offset)
|
|
203
|
+
}
|
|
204
|
+
if written < 0 {
|
|
205
|
+
if errno == EINTR {
|
|
206
|
+
continue
|
|
207
|
+
}
|
|
208
|
+
throw NSError(domain: NSPOSIXErrorDomain, code: Int(errno), userInfo: [NSLocalizedDescriptionKey: "AES stream write failed"])
|
|
209
|
+
}
|
|
210
|
+
if written == 0 {
|
|
211
|
+
throw NSError(domain: NSPOSIXErrorDomain, code: Int(EIO), userInfo: [NSLocalizedDescriptionKey: "AES stream write failed"])
|
|
212
|
+
}
|
|
213
|
+
offset += written
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
final class AppHealthTracker {
|
|
4
|
+
private let appSessionIdDefaultsKey = "CapacitorUpdater.appSessionId"
|
|
5
|
+
private let appSessionForegroundDefaultsKey = "CapacitorUpdater.appSessionForeground"
|
|
6
|
+
private let appSessionStartedAtDefaultsKey = "CapacitorUpdater.appSessionStartedAt"
|
|
7
|
+
private let lastReportedUncleanSessionDefaultsKey = "CapacitorUpdater.lastReportedUncleanSessionId"
|
|
8
|
+
|
|
9
|
+
private let implementation: CapgoUpdater
|
|
10
|
+
|
|
11
|
+
init(implementation: CapgoUpdater) {
|
|
12
|
+
self.implementation = implementation
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
static func shouldReportUncleanForegroundExit(
|
|
16
|
+
previousSessionId: String?,
|
|
17
|
+
lastReportedSessionId: String?,
|
|
18
|
+
wasForeground: Bool
|
|
19
|
+
) -> Bool {
|
|
20
|
+
guard wasForeground, let previousSessionId, !previousSessionId.isEmpty else {
|
|
21
|
+
return false
|
|
22
|
+
}
|
|
23
|
+
return previousSessionId != lastReportedSessionId
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
func reportPreviousUncleanForegroundExit() {
|
|
27
|
+
let defaults = UserDefaults.standard
|
|
28
|
+
let previousSessionId = defaults.string(forKey: appSessionIdDefaultsKey)
|
|
29
|
+
let lastReportedSessionId = defaults.string(forKey: lastReportedUncleanSessionDefaultsKey)
|
|
30
|
+
let wasForeground = defaults.bool(forKey: appSessionForegroundDefaultsKey)
|
|
31
|
+
|
|
32
|
+
guard Self.shouldReportUncleanForegroundExit(
|
|
33
|
+
previousSessionId: previousSessionId,
|
|
34
|
+
lastReportedSessionId: lastReportedSessionId,
|
|
35
|
+
wasForeground: wasForeground
|
|
36
|
+
), let previousSessionId else {
|
|
37
|
+
return
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
var metadata = [
|
|
41
|
+
"exit_reason": "unclean_foreground_exit",
|
|
42
|
+
"exit_source": "ios_session_marker",
|
|
43
|
+
"previous_session_id": previousSessionId
|
|
44
|
+
]
|
|
45
|
+
if let sessionStartedAt = defaults.string(forKey: appSessionStartedAtDefaultsKey), !sessionStartedAt.isEmpty {
|
|
46
|
+
metadata["session_started_at"] = sessionStartedAt
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
let current = implementation.getCurrentBundle()
|
|
50
|
+
implementation.sendStats(
|
|
51
|
+
action: "app_crash",
|
|
52
|
+
versionName: current.getVersionName(),
|
|
53
|
+
oldVersionName: "",
|
|
54
|
+
metadata: metadata
|
|
55
|
+
)
|
|
56
|
+
defaults.set(previousSessionId, forKey: lastReportedUncleanSessionDefaultsKey)
|
|
57
|
+
defaults.synchronize()
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
func startSession() {
|
|
61
|
+
let defaults = UserDefaults.standard
|
|
62
|
+
defaults.set(UUID().uuidString, forKey: appSessionIdDefaultsKey)
|
|
63
|
+
defaults.set(true, forKey: appSessionForegroundDefaultsKey)
|
|
64
|
+
defaults.set(String(Int64(Date().timeIntervalSince1970 * 1000)), forKey: appSessionStartedAtDefaultsKey)
|
|
65
|
+
defaults.synchronize()
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
func markForeground(_ isForeground: Bool) {
|
|
69
|
+
UserDefaults.standard.set(isForeground, forKey: appSessionForegroundDefaultsKey)
|
|
70
|
+
UserDefaults.standard.synchronize()
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
func reportMemoryWarning() {
|
|
74
|
+
let current = implementation.getCurrentBundle()
|
|
75
|
+
implementation.sendStats(
|
|
76
|
+
action: "app_memory_warning",
|
|
77
|
+
versionName: current.getVersionName(),
|
|
78
|
+
oldVersionName: "",
|
|
79
|
+
metadata: ["source": "ios_memory_warning"]
|
|
80
|
+
)
|
|
81
|
+
}
|
|
82
|
+
}
|