@capgo/cordova-updater 8.1.3 → 8.2.0
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/dist/esm/definitions.d.ts +116 -86
- package/dist/esm/definitions.js +2 -2
- package/dist/esm/definitions.js.map +1 -1
- package/dist/esm/exec.js +11 -1
- package/dist/esm/exec.js.map +1 -1
- package/dist/esm/history.js +7 -7
- package/dist/esm/history.js.map +1 -1
- package/dist/plugin.cjs.js +13 -3
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/plugin.js +13 -3
- package/dist/plugin.js.map +1 -1
- package/package.json +5 -3
- package/plugin.xml +9 -9
- package/src/android/CordovaUpdaterPlugin.java +173 -5
- package/src/android/app/capgo/cordova/updater/CapgoUpdater.java +313 -55
- package/src/android/app/capgo/cordova/updater/CryptoCipher.java +51 -18
- package/src/android/app/capgo/cordova/updater/DelayUpdateUtils.java +0 -1
- package/src/android/app/capgo/cordova/updater/DownloadService.java +404 -91
- package/src/android/app/capgo/cordova/updater/Version.java +312 -0
- package/src/android/capgo-cordova-updater.gradle +0 -2
- package/src/android/test/app/capgo/cordova/updater/BackgroundDownloadSettlementTest.java +29 -0
- package/src/android/test/app/capgo/cordova/updater/CapacitorUpdaterUnitTest.java.skip +2 -12
- package/src/android/test/app/capgo/cordova/updater/DelayUpdateUtilsTest.java +0 -1
- package/src/android/test/app/capgo/cordova/updater/RsaContractTest.java +137 -0
- package/src/android/test/app/capgo/cordova/updater/SecurityHardeningTest.java +197 -0
- package/src/android/test/app/capgo/cordova/updater/SessionKeyRequiredTest.java +344 -0
- package/src/ios/CapgoSemanticVersion.swift +181 -0
- package/src/ios/CapgoUpdater.swift +427 -114
- package/src/ios/CordovaUpdaterPlugin.swift +171 -40
- package/src/ios/CryptoCipher.swift +41 -38
- package/src/ios/DelayUpdateUtils.swift +19 -45
- package/src/ios/Logger.swift +81 -5
- package/src/ios/RedirectPolicyDelegate.swift +53 -0
- package/src/ios/WebViewStatsReporter.swift +5 -0
- package/src/ios/ZipArchiveReader.swift +263 -0
- package/src/ios/ZipCentralDirectory.swift +283 -0
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
package app.capgo.cordova.updater;
|
|
2
|
+
|
|
3
|
+
import static org.junit.Assert.assertArrayEquals;
|
|
4
|
+
import static org.junit.Assert.assertEquals;
|
|
5
|
+
import static org.junit.Assert.assertFalse;
|
|
6
|
+
import static org.junit.Assert.assertThrows;
|
|
7
|
+
import static org.junit.Assert.assertTrue;
|
|
8
|
+
import static org.junit.Assert.fail;
|
|
9
|
+
import static org.mockito.Mockito.mock;
|
|
10
|
+
import java.io.File;
|
|
11
|
+
import java.io.IOException;
|
|
12
|
+
import java.nio.charset.StandardCharsets;
|
|
13
|
+
import java.nio.file.Files;
|
|
14
|
+
import java.nio.file.Path;
|
|
15
|
+
import org.junit.Test;
|
|
16
|
+
import org.junit.runner.RunWith;
|
|
17
|
+
import org.robolectric.RobolectricTestRunner;
|
|
18
|
+
import org.robolectric.annotation.Config;
|
|
19
|
+
|
|
20
|
+
@RunWith(RobolectricTestRunner.class)
|
|
21
|
+
@Config(manifest = Config.NONE)
|
|
22
|
+
public class SecurityHardeningTest {
|
|
23
|
+
|
|
24
|
+
@Test
|
|
25
|
+
public void testResolvePathInsideDirectoryRejectsAbsolutePaths() throws Exception {
|
|
26
|
+
final Path base = Files.createTempDirectory("capgo-abs-path");
|
|
27
|
+
base.toFile().deleteOnExit();
|
|
28
|
+
|
|
29
|
+
assertThrows(IOException.class, () -> CapgoUpdater.resolvePathInsideDirectory(base.toFile(), "/etc/passwd"));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
@Test
|
|
33
|
+
public void testResolvePathInsideDirectoryRejectsBackslashes() throws Exception {
|
|
34
|
+
final Path base = Files.createTempDirectory("capgo-backslash-path");
|
|
35
|
+
base.toFile().deleteOnExit();
|
|
36
|
+
|
|
37
|
+
assertThrows(IOException.class, () -> CapgoUpdater.resolvePathInsideDirectory(base.toFile(), "assets\\app.js"));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
@Test
|
|
41
|
+
public void testResolvePathInsideDirectoryRejectsNullBytes() throws Exception {
|
|
42
|
+
final Path base = Files.createTempDirectory("capgo-null-path");
|
|
43
|
+
base.toFile().deleteOnExit();
|
|
44
|
+
|
|
45
|
+
assertThrows(IOException.class, () -> CapgoUpdater.resolvePathInsideDirectory(base.toFile(), "assets\0app.js"));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
@Test
|
|
49
|
+
public void testResolvePathInsideDirectoryRejectsDotDotSegments() throws Exception {
|
|
50
|
+
final Path base = Files.createTempDirectory("capgo-dotdot-path");
|
|
51
|
+
base.toFile().deleteOnExit();
|
|
52
|
+
|
|
53
|
+
assertThrows(IOException.class, () -> CapgoUpdater.resolvePathInsideDirectory(base.toFile(), "assets/../../secret.js"));
|
|
54
|
+
assertThrows(IOException.class, () -> CapgoUpdater.resolvePathInsideDirectory(base.toFile(), "../secret.js"));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
@Test
|
|
58
|
+
public void testResolvePathInsideDirectoryRejectsDotAsBaseDirectory() throws Exception {
|
|
59
|
+
final Path base = Files.createTempDirectory("capgo-dot-path");
|
|
60
|
+
base.toFile().deleteOnExit();
|
|
61
|
+
|
|
62
|
+
assertThrows(IOException.class, () -> CapgoUpdater.resolvePathInsideDirectory(base.toFile(), "."));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
@Test
|
|
66
|
+
public void testResolvePathInsideDirectoryAllowsNestedRelativePath() throws Exception {
|
|
67
|
+
final Path base = Files.createTempDirectory("capgo-nested-path");
|
|
68
|
+
base.toFile().deleteOnExit();
|
|
69
|
+
|
|
70
|
+
final File resolved = CapgoUpdater.resolvePathInsideDirectory(base.toFile(), "assets/app.js");
|
|
71
|
+
|
|
72
|
+
assertEquals(base.resolve("assets").resolve("app.js").toFile().getCanonicalFile(), resolved);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
@Test
|
|
76
|
+
public void resolveBundleDirectoryRejectsAbsolutePath() throws Exception {
|
|
77
|
+
final Path tempDir = Files.createTempDirectory("capgo-delete-abs");
|
|
78
|
+
tempDir.toFile().deleteOnExit();
|
|
79
|
+
assertThrows(IOException.class, () -> CapgoUpdater.resolveBundleDirectory(tempDir.toFile(), "/tmp/evil"));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
@Test
|
|
83
|
+
public void resolveBundleDirectoryRejectsPathTraversal() throws Exception {
|
|
84
|
+
final Path tempDir = Files.createTempDirectory("capgo-delete-traversal");
|
|
85
|
+
tempDir.toFile().deleteOnExit();
|
|
86
|
+
assertThrows(IOException.class, () -> CapgoUpdater.resolveBundleDirectory(tempDir.toFile(), "../outside-target"));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
@Test
|
|
90
|
+
public void resolveBundleDirectoryRejectsDotAsBundleRoot() throws Exception {
|
|
91
|
+
final Path tempDir = Files.createTempDirectory("capgo-delete-dot");
|
|
92
|
+
tempDir.toFile().deleteOnExit();
|
|
93
|
+
assertThrows(IOException.class, () -> CapgoUpdater.resolveBundleDirectory(tempDir.toFile(), "."));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
@Test
|
|
97
|
+
public void deleteRejectsDotBundleId() throws Exception {
|
|
98
|
+
final Path tempDir = Files.createTempDirectory("capgo-delete-dot-id");
|
|
99
|
+
tempDir.toFile().deleteOnExit();
|
|
100
|
+
final CapgoUpdater updater = new CapgoUpdater(mock(Logger.class));
|
|
101
|
+
updater.documentsDir = tempDir.toFile();
|
|
102
|
+
|
|
103
|
+
assertFalse(Boolean.TRUE.equals(updater.delete(".", true)));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
@Test
|
|
107
|
+
public void resolveBundleDirectoryRejectsWindowsSeparators() throws Exception {
|
|
108
|
+
final Path tempDir = Files.createTempDirectory("capgo-delete-windows");
|
|
109
|
+
tempDir.toFile().deleteOnExit();
|
|
110
|
+
assertThrows(IOException.class, () -> CapgoUpdater.resolveBundleDirectory(tempDir.toFile(), "..\\outside-target"));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
@Test
|
|
114
|
+
public void deleteRejectsPathTraversalOutsideBundleRoot() throws Exception {
|
|
115
|
+
final Path tempDir = Files.createTempDirectory("capgo-delete-escape");
|
|
116
|
+
tempDir.toFile().deleteOnExit();
|
|
117
|
+
final Path outsideTarget = tempDir.resolve("outside-target");
|
|
118
|
+
Files.createDirectories(outsideTarget);
|
|
119
|
+
Files.write(outsideTarget.resolve("marker.txt"), "keep".getBytes(StandardCharsets.UTF_8));
|
|
120
|
+
|
|
121
|
+
final CapgoUpdater updater = new CapgoUpdater(mock(Logger.class));
|
|
122
|
+
updater.documentsDir = tempDir.toFile();
|
|
123
|
+
|
|
124
|
+
assertFalse(Boolean.TRUE.equals(updater.delete("../outside-target", true)));
|
|
125
|
+
assertTrue("Path outside bundle root must remain untouched", Files.exists(outsideTarget.resolve("marker.txt")));
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
@Test
|
|
129
|
+
public void deleteRejectsAbsolutePathId() throws Exception {
|
|
130
|
+
final Path tempDir = Files.createTempDirectory("capgo-delete-abs-id");
|
|
131
|
+
tempDir.toFile().deleteOnExit();
|
|
132
|
+
final CapgoUpdater updater = new CapgoUpdater(mock(Logger.class));
|
|
133
|
+
updater.documentsDir = tempDir.toFile();
|
|
134
|
+
|
|
135
|
+
assertFalse(Boolean.TRUE.equals(updater.delete("/tmp/evil", true)));
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
@Test
|
|
139
|
+
public void decryptAesFileRejectsNullKeyWithoutNpe() throws Exception {
|
|
140
|
+
final Path dir = Files.createTempDirectory("capgo-aes-null-key");
|
|
141
|
+
dir.toFile().deleteOnExit();
|
|
142
|
+
File file = dir.resolve("cipher.bin").toFile();
|
|
143
|
+
byte[] iv = new byte[16];
|
|
144
|
+
Files.write(file.toPath(), new byte[] { 1, 2, 3, 4, 5, 6, 7, 8 });
|
|
145
|
+
try {
|
|
146
|
+
CryptoCipher.decryptAesFile(file, null, iv);
|
|
147
|
+
fail("expected null session key to be rejected");
|
|
148
|
+
} catch (NullPointerException e) {
|
|
149
|
+
fail("null session key must not cause NPE");
|
|
150
|
+
} catch (IOException e) {
|
|
151
|
+
assertTrue(e.getMessage().contains("missing session key"));
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
@Test
|
|
156
|
+
public void decryptAesFileRejectsBadKeyWithoutNpe() throws Exception {
|
|
157
|
+
final Path dir = Files.createTempDirectory("capgo-aes-bad-key");
|
|
158
|
+
dir.toFile().deleteOnExit();
|
|
159
|
+
File file = dir.resolve("cipher.bin").toFile();
|
|
160
|
+
byte[] iv = new byte[16];
|
|
161
|
+
byte[] keyBytes = new byte[16];
|
|
162
|
+
for (int i = 0; i < 16; i++) {
|
|
163
|
+
iv[i] = (byte) i;
|
|
164
|
+
keyBytes[i] = (byte) (31 - i);
|
|
165
|
+
}
|
|
166
|
+
javax.crypto.SecretKey encryptKey = new javax.crypto.spec.SecretKeySpec(keyBytes, "AES");
|
|
167
|
+
javax.crypto.SecretKey decryptKey = new javax.crypto.spec.SecretKeySpec(new byte[16], "AES");
|
|
168
|
+
byte[] plain = "capgo-aes-failure".getBytes(StandardCharsets.UTF_8);
|
|
169
|
+
javax.crypto.Cipher enc = javax.crypto.Cipher.getInstance("AES/CBC/PKCS5Padding");
|
|
170
|
+
enc.init(javax.crypto.Cipher.ENCRYPT_MODE, encryptKey, new javax.crypto.spec.IvParameterSpec(iv));
|
|
171
|
+
byte[] cipherBytes = enc.doFinal(plain);
|
|
172
|
+
Files.write(file.toPath(), cipherBytes);
|
|
173
|
+
byte[] originalCipher = Files.readAllBytes(file.toPath());
|
|
174
|
+
try {
|
|
175
|
+
CryptoCipher.decryptAesFile(file, decryptKey, iv);
|
|
176
|
+
fail("expected AES decrypt failure");
|
|
177
|
+
} catch (NullPointerException e) {
|
|
178
|
+
fail("AES decrypt failure must not cause NPE");
|
|
179
|
+
} catch (IOException e) {
|
|
180
|
+
assertTrue(e.getMessage().contains("AES file decryption failed"));
|
|
181
|
+
assertArrayEquals(originalCipher, Files.readAllBytes(file.toPath()));
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
@Test
|
|
186
|
+
public void decryptAESRejectsFailureWithoutNull() throws Exception {
|
|
187
|
+
byte[] iv = new byte[16];
|
|
188
|
+
byte[] keyBytes = new byte[16];
|
|
189
|
+
javax.crypto.SecretKey key = new javax.crypto.spec.SecretKeySpec(keyBytes, "AES");
|
|
190
|
+
try {
|
|
191
|
+
CryptoCipher.decryptAES(new byte[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 }, key, iv);
|
|
192
|
+
fail("expected AES decrypt failure");
|
|
193
|
+
} catch (javax.crypto.BadPaddingException expected) {
|
|
194
|
+
// expected decrypt failure
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
package app.capgo.cordova.updater;
|
|
2
|
+
|
|
3
|
+
import static org.junit.Assert.assertEquals;
|
|
4
|
+
import static org.junit.Assert.assertFalse;
|
|
5
|
+
import static org.junit.Assert.assertTrue;
|
|
6
|
+
import static org.junit.Assert.fail;
|
|
7
|
+
import static org.mockito.ArgumentMatchers.anyString;
|
|
8
|
+
import static org.mockito.Mockito.mock;
|
|
9
|
+
import static org.mockito.Mockito.mockConstruction;
|
|
10
|
+
import static org.mockito.Mockito.mockStatic;
|
|
11
|
+
import static org.mockito.Mockito.when;
|
|
12
|
+
|
|
13
|
+
import android.content.SharedPreferences;
|
|
14
|
+
import android.os.Handler;
|
|
15
|
+
import android.os.Looper;
|
|
16
|
+
import java.io.IOException;
|
|
17
|
+
import java.lang.reflect.InvocationTargetException;
|
|
18
|
+
import java.lang.reflect.Method;
|
|
19
|
+
import java.nio.charset.StandardCharsets;
|
|
20
|
+
import java.nio.file.Files;
|
|
21
|
+
import java.nio.file.Path;
|
|
22
|
+
import java.util.ArrayList;
|
|
23
|
+
import java.util.Date;
|
|
24
|
+
import java.util.HashMap;
|
|
25
|
+
import java.util.List;
|
|
26
|
+
import java.util.Map;
|
|
27
|
+
import org.json.JSONArray;
|
|
28
|
+
import org.junit.BeforeClass;
|
|
29
|
+
import org.junit.Test;
|
|
30
|
+
import org.junit.runner.RunWith;
|
|
31
|
+
import org.mockito.MockedConstruction;
|
|
32
|
+
import org.mockito.MockedStatic;
|
|
33
|
+
import org.robolectric.RobolectricTestRunner;
|
|
34
|
+
import org.robolectric.annotation.Config;
|
|
35
|
+
|
|
36
|
+
@RunWith(RobolectricTestRunner.class)
|
|
37
|
+
@Config(manifest = Config.NONE)
|
|
38
|
+
public class SessionKeyRequiredTest {
|
|
39
|
+
|
|
40
|
+
private static String fixturePublicKey;
|
|
41
|
+
|
|
42
|
+
@BeforeClass
|
|
43
|
+
public static void setUpClass() throws Exception {
|
|
44
|
+
CryptoCipher.setLogger(new Logger("SessionKeyRequiredTest", new Logger.Options(Logger.LogLevel.silent)));
|
|
45
|
+
Path contractFile = Path.of(System.getProperty("user.dir")).toAbsolutePath();
|
|
46
|
+
while (contractFile != null) {
|
|
47
|
+
Path candidate = contractFile.resolve("native-contract-tests/crypto-rsa.json");
|
|
48
|
+
if (Files.exists(candidate)) {
|
|
49
|
+
fixturePublicKey = new org.json.JSONObject(new String(Files.readAllBytes(candidate), StandardCharsets.UTF_8)).getString(
|
|
50
|
+
"publicKeyPem"
|
|
51
|
+
);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
contractFile = contractFile.getParent();
|
|
55
|
+
}
|
|
56
|
+
throw new IOException("native-contract-tests/crypto-rsa.json not found");
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
private static class StatsTrackingCapgoUpdater extends CapgoUpdater {
|
|
60
|
+
|
|
61
|
+
private final List<String> sentStatsActions = new ArrayList<>();
|
|
62
|
+
|
|
63
|
+
StatsTrackingCapgoUpdater() {
|
|
64
|
+
super(mock(Logger.class));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
@Override
|
|
68
|
+
public void sendStats(final String action) {
|
|
69
|
+
this.sentStatsActions.add(action);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
List<String> getSentStatsActions() {
|
|
73
|
+
return this.sentStatsActions;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
private static final class AutoUpdateSessionKeyCapgoUpdater extends StatsTrackingCapgoUpdater {
|
|
78
|
+
|
|
79
|
+
@Override
|
|
80
|
+
public void getLatest(final String updateUrl, final String channel, final Callback callback) {
|
|
81
|
+
final Map<String, Object> response = new HashMap<>();
|
|
82
|
+
response.put("version", "2.0.0");
|
|
83
|
+
response.put("url", "https://example.com/update.zip");
|
|
84
|
+
callback.callback(response);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
@Override
|
|
88
|
+
public BundleInfo getCurrentBundle() {
|
|
89
|
+
return new BundleInfo("current-id", "1.0.0", BundleStatus.SUCCESS, new Date(), "abc");
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
private static final class PendingBundleAutoUpdateCapgoUpdater extends StatsTrackingCapgoUpdater {
|
|
94
|
+
|
|
95
|
+
@Override
|
|
96
|
+
public void getLatest(final String updateUrl, final String channel, final Callback callback) {
|
|
97
|
+
final Map<String, Object> response = new HashMap<>();
|
|
98
|
+
response.put("version", "2.0.0");
|
|
99
|
+
response.put("url", "https://example.com/update.zip");
|
|
100
|
+
response.put("checksum", "abc123");
|
|
101
|
+
callback.callback(response);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
@Override
|
|
105
|
+
public BundleInfo getCurrentBundle() {
|
|
106
|
+
return new BundleInfo("current-id", "1.0.0", BundleStatus.SUCCESS, new Date(), "abc");
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
@Override
|
|
110
|
+
public BundleInfo getBundleInfoByName(final String versionName) {
|
|
111
|
+
if ("2.0.0".equals(versionName)) {
|
|
112
|
+
return new BundleInfo("pending-id", "2.0.0", BundleStatus.PENDING, new Date(), "abc123");
|
|
113
|
+
}
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
private static final class ImmediateThreadCordovaUpdaterPlugin extends CordovaUpdaterPlugin {
|
|
119
|
+
|
|
120
|
+
@Override
|
|
121
|
+
public Thread startNewThread(final Runnable function) {
|
|
122
|
+
function.run();
|
|
123
|
+
return new Thread();
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
private static void configureFinishDownloadTestState(final StatsTrackingCapgoUpdater updater) {
|
|
128
|
+
updater.prefs = mock(SharedPreferences.class);
|
|
129
|
+
updater.editor = mock(SharedPreferences.Editor.class);
|
|
130
|
+
when(updater.prefs.getString(anyString(), anyString())).thenReturn("");
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
private static void invokeBackgroundDownload(final CordovaUpdaterPlugin plugin) throws Exception {
|
|
134
|
+
final Method backgroundDownload = CordovaUpdaterPlugin.class.getDeclaredMethod("backgroundDownload");
|
|
135
|
+
backgroundDownload.setAccessible(true);
|
|
136
|
+
backgroundDownload.invoke(plugin);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
private static void invokeDownloadBundle(
|
|
140
|
+
final CordovaUpdaterPlugin plugin,
|
|
141
|
+
final String url,
|
|
142
|
+
final String version,
|
|
143
|
+
final String sessionKey,
|
|
144
|
+
final String checksum,
|
|
145
|
+
final JSONArray manifest
|
|
146
|
+
) throws Exception {
|
|
147
|
+
final Method downloadBundle = CordovaUpdaterPlugin.class.getDeclaredMethod(
|
|
148
|
+
"downloadBundle",
|
|
149
|
+
String.class,
|
|
150
|
+
String.class,
|
|
151
|
+
String.class,
|
|
152
|
+
String.class,
|
|
153
|
+
JSONArray.class
|
|
154
|
+
);
|
|
155
|
+
downloadBundle.setAccessible(true);
|
|
156
|
+
downloadBundle.invoke(plugin, url, version, sessionKey, checksum, manifest);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
@Test
|
|
160
|
+
public void finishDownloadRejectsManifestWhenSessionKeyMissing() throws Exception {
|
|
161
|
+
final StatsTrackingCapgoUpdater updater = new StatsTrackingCapgoUpdater();
|
|
162
|
+
configureFinishDownloadTestState(updater);
|
|
163
|
+
updater.setPublicKey(fixturePublicKey);
|
|
164
|
+
updater.documentsDir = Files.createTempDirectory("capgo-session-key-manifest").toFile();
|
|
165
|
+
|
|
166
|
+
final boolean success = updater.finishDownload("bundle-id", "dest", "1.0.0", "", "checksum", false, true);
|
|
167
|
+
|
|
168
|
+
assertFalse(success);
|
|
169
|
+
assertTrue(updater.getSentStatsActions().contains("session_key_required"));
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
@Test
|
|
173
|
+
public void finishDownloadRejectsZipWhenSessionKeyMissing() throws Exception {
|
|
174
|
+
final StatsTrackingCapgoUpdater updater = new StatsTrackingCapgoUpdater();
|
|
175
|
+
configureFinishDownloadTestState(updater);
|
|
176
|
+
updater.setPublicKey(fixturePublicKey);
|
|
177
|
+
final Path tempDir = Files.createTempDirectory("capgo-session-key-zip");
|
|
178
|
+
updater.documentsDir = tempDir.toFile();
|
|
179
|
+
final String dest = "bundle.zip";
|
|
180
|
+
Files.write(tempDir.resolve(dest), "plaintext".getBytes(StandardCharsets.UTF_8));
|
|
181
|
+
|
|
182
|
+
final boolean success = updater.finishDownload("bundle-id", dest, "1.0.0", "", "checksum", false, false);
|
|
183
|
+
|
|
184
|
+
assertFalse(success);
|
|
185
|
+
assertTrue(updater.getSentStatsActions().contains("session_key_required"));
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
@Test
|
|
189
|
+
public void downloadRejectsWhenSessionKeyMissing() {
|
|
190
|
+
final StatsTrackingCapgoUpdater updater = new StatsTrackingCapgoUpdater();
|
|
191
|
+
updater.setPublicKey(fixturePublicKey);
|
|
192
|
+
|
|
193
|
+
try {
|
|
194
|
+
updater.download("https://example.com/update.zip", "1.0.0", "", "checksum");
|
|
195
|
+
fail("Expected IOException when session key is missing");
|
|
196
|
+
} catch (IOException e) {
|
|
197
|
+
assertEquals("Session key required when public key is present", e.getMessage());
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
assertTrue(updater.getSentStatsActions().contains("session_key_required"));
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
@Test
|
|
204
|
+
public void isValidSessionKeyRejectsEmptyComponents() {
|
|
205
|
+
assertFalse(CryptoCipher.isValidSessionKey(null));
|
|
206
|
+
assertFalse(CryptoCipher.isValidSessionKey(""));
|
|
207
|
+
assertFalse(CryptoCipher.isValidSessionKey(":"));
|
|
208
|
+
assertFalse(CryptoCipher.isValidSessionKey("abc:"));
|
|
209
|
+
assertFalse(CryptoCipher.isValidSessionKey(":xyz"));
|
|
210
|
+
assertFalse(CryptoCipher.isValidSessionKey("invalid-format"));
|
|
211
|
+
assertTrue(CryptoCipher.isValidSessionKey("abc:def"));
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
@Test
|
|
215
|
+
public void downloadRejectsWhenSessionKeyHasEmptyComponents() {
|
|
216
|
+
final StatsTrackingCapgoUpdater updater = new StatsTrackingCapgoUpdater();
|
|
217
|
+
updater.setPublicKey(fixturePublicKey);
|
|
218
|
+
|
|
219
|
+
try {
|
|
220
|
+
updater.download("https://example.com/update.zip", "1.0.0", "abc:", "checksum");
|
|
221
|
+
fail("Expected IOException when session key has empty components");
|
|
222
|
+
} catch (IOException e) {
|
|
223
|
+
assertEquals("Session key required when public key is present", e.getMessage());
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
assertTrue(updater.getSentStatsActions().contains("session_key_required"));
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
@Test
|
|
230
|
+
public void downloadRejectsWhenSessionKeyFormatInvalid() {
|
|
231
|
+
final StatsTrackingCapgoUpdater updater = new StatsTrackingCapgoUpdater();
|
|
232
|
+
updater.setPublicKey(fixturePublicKey);
|
|
233
|
+
|
|
234
|
+
try {
|
|
235
|
+
updater.download("https://example.com/update.zip", "1.0.0", "invalid-format", "checksum");
|
|
236
|
+
fail("Expected IOException when session key format is invalid");
|
|
237
|
+
} catch (IOException e) {
|
|
238
|
+
assertEquals("Session key required when public key is present", e.getMessage());
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
assertTrue(updater.getSentStatsActions().contains("session_key_required"));
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
@Test
|
|
245
|
+
public void downloadManifestRejectsWhenSessionKeyMissing() {
|
|
246
|
+
final StatsTrackingCapgoUpdater updater = new StatsTrackingCapgoUpdater();
|
|
247
|
+
updater.setPublicKey(fixturePublicKey);
|
|
248
|
+
final JSONArray manifest = new JSONArray();
|
|
249
|
+
|
|
250
|
+
try {
|
|
251
|
+
updater.downloadManifest("https://example.com/update.zip", "1.0.0", "", "checksum", manifest);
|
|
252
|
+
fail("Expected IOException when session key is missing");
|
|
253
|
+
} catch (IOException e) {
|
|
254
|
+
assertEquals("Session key required when public key is present", e.getMessage());
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
assertTrue(updater.getSentStatsActions().contains("session_key_required"));
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
@Test
|
|
261
|
+
public void downloadBackgroundDoesNotStartWhenSessionKeyMissing() {
|
|
262
|
+
final StatsTrackingCapgoUpdater updater = new StatsTrackingCapgoUpdater();
|
|
263
|
+
updater.setPublicKey(fixturePublicKey);
|
|
264
|
+
|
|
265
|
+
updater.downloadBackground(
|
|
266
|
+
"https://example.com/update.zip",
|
|
267
|
+
"9.9.9-session-key-guard",
|
|
268
|
+
"",
|
|
269
|
+
"checksum",
|
|
270
|
+
null,
|
|
271
|
+
true,
|
|
272
|
+
1L
|
|
273
|
+
);
|
|
274
|
+
|
|
275
|
+
assertTrue(updater.getSentStatsActions().contains("session_key_required"));
|
|
276
|
+
assertEquals(null, updater.getBundleInfoByName("9.9.9-session-key-guard"));
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
@Test
|
|
280
|
+
public void downloadBundleRejectsWhenSessionKeyMissing() throws Exception {
|
|
281
|
+
final ImmediateThreadCordovaUpdaterPlugin plugin = new ImmediateThreadCordovaUpdaterPlugin();
|
|
282
|
+
final StatsTrackingCapgoUpdater updater = new StatsTrackingCapgoUpdater();
|
|
283
|
+
updater.setPublicKey(fixturePublicKey);
|
|
284
|
+
plugin.implementation = updater;
|
|
285
|
+
plugin.setLoggerForTesting(mock(Logger.class));
|
|
286
|
+
|
|
287
|
+
try {
|
|
288
|
+
invokeDownloadBundle(plugin, "https://example.com/update.zip", "1.0.0", "", "checksum", null);
|
|
289
|
+
fail("Expected IOException when session key is missing");
|
|
290
|
+
} catch (InvocationTargetException e) {
|
|
291
|
+
assertTrue(e.getCause() instanceof IOException);
|
|
292
|
+
assertEquals("Session key required when public key is present", e.getCause().getMessage());
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
assertTrue(updater.getSentStatsActions().contains("session_key_required"));
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
@Test
|
|
299
|
+
public void autoUpdateRejectsMissingSessionKeyWhenPendingBundleExists() throws Exception {
|
|
300
|
+
try (
|
|
301
|
+
MockedStatic<Looper> looperMock = mockStatic(Looper.class);
|
|
302
|
+
MockedConstruction<Handler> ignored = mockConstruction(Handler.class)
|
|
303
|
+
) {
|
|
304
|
+
looperMock.when(Looper::getMainLooper).thenReturn(mock(Looper.class));
|
|
305
|
+
|
|
306
|
+
final ImmediateThreadCordovaUpdaterPlugin plugin = new ImmediateThreadCordovaUpdaterPlugin();
|
|
307
|
+
final PendingBundleAutoUpdateCapgoUpdater updater = new PendingBundleAutoUpdateCapgoUpdater();
|
|
308
|
+
updater.setPublicKey(fixturePublicKey);
|
|
309
|
+
|
|
310
|
+
plugin.implementation = updater;
|
|
311
|
+
plugin.setAutoUpdateModeForTesting("onlyDownload");
|
|
312
|
+
plugin.setLoggerForTesting(mock(Logger.class));
|
|
313
|
+
|
|
314
|
+
invokeBackgroundDownload(plugin);
|
|
315
|
+
|
|
316
|
+
assertTrue(updater.getSentStatsActions().contains("session_key_required"));
|
|
317
|
+
final BundleInfo pending = updater.getBundleInfoByName("2.0.0");
|
|
318
|
+
assertTrue(pending != null && pending.getStatus() == BundleStatus.PENDING);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
@Test
|
|
323
|
+
public void autoUpdateBackgroundDownloadRejectsWhenSessionKeyMissing() throws Exception {
|
|
324
|
+
try (
|
|
325
|
+
MockedStatic<Looper> looperMock = mockStatic(Looper.class);
|
|
326
|
+
MockedConstruction<Handler> ignored = mockConstruction(Handler.class)
|
|
327
|
+
) {
|
|
328
|
+
looperMock.when(Looper::getMainLooper).thenReturn(mock(Looper.class));
|
|
329
|
+
|
|
330
|
+
final ImmediateThreadCordovaUpdaterPlugin plugin = new ImmediateThreadCordovaUpdaterPlugin();
|
|
331
|
+
final AutoUpdateSessionKeyCapgoUpdater updater = new AutoUpdateSessionKeyCapgoUpdater();
|
|
332
|
+
updater.setPublicKey(fixturePublicKey);
|
|
333
|
+
|
|
334
|
+
plugin.implementation = updater;
|
|
335
|
+
plugin.setAutoUpdateModeForTesting("onlyDownload");
|
|
336
|
+
plugin.setLoggerForTesting(mock(Logger.class));
|
|
337
|
+
|
|
338
|
+
invokeBackgroundDownload(plugin);
|
|
339
|
+
|
|
340
|
+
assertTrue(updater.getSentStatsActions().contains("session_key_required"));
|
|
341
|
+
assertEquals(null, updater.getBundleInfoByName("2.0.0"));
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|