@capgo/capacitor-updater 6.14.26 → 6.14.33

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/CapgoCapacitorUpdater.podspec +3 -2
  2. package/Package.swift +2 -2
  3. package/README.md +350 -74
  4. package/android/build.gradle +20 -8
  5. package/android/proguard-rules.pro +22 -5
  6. package/android/src/main/java/ee/forgr/capacitor_updater/BundleInfo.java +52 -16
  7. package/android/src/main/java/ee/forgr/capacitor_updater/Callback.java +2 -2
  8. package/android/src/main/java/ee/forgr/capacitor_updater/CapacitorUpdaterPlugin.java +1202 -510
  9. package/android/src/main/java/ee/forgr/capacitor_updater/{CapacitorUpdater.java → CapgoUpdater.java} +566 -154
  10. package/android/src/main/java/ee/forgr/capacitor_updater/{CryptoCipher.java → CryptoCipherV1.java} +17 -9
  11. package/android/src/main/java/ee/forgr/capacitor_updater/CryptoCipherV2.java +15 -26
  12. package/android/src/main/java/ee/forgr/capacitor_updater/DelayCondition.java +0 -3
  13. package/android/src/main/java/ee/forgr/capacitor_updater/DelayUpdateUtils.java +260 -0
  14. package/android/src/main/java/ee/forgr/capacitor_updater/DeviceIdHelper.java +221 -0
  15. package/android/src/main/java/ee/forgr/capacitor_updater/DownloadService.java +300 -119
  16. package/android/src/main/java/ee/forgr/capacitor_updater/DownloadWorkerManager.java +63 -25
  17. package/android/src/main/java/ee/forgr/capacitor_updater/Logger.java +338 -0
  18. package/android/src/main/java/ee/forgr/capacitor_updater/ShakeDetector.java +72 -0
  19. package/android/src/main/java/ee/forgr/capacitor_updater/ShakeMenu.java +169 -0
  20. package/dist/docs.json +652 -63
  21. package/dist/esm/definitions.d.ts +274 -15
  22. package/dist/esm/definitions.js.map +1 -1
  23. package/dist/esm/history.d.ts +1 -0
  24. package/dist/esm/history.js +283 -0
  25. package/dist/esm/history.js.map +1 -0
  26. package/dist/esm/index.d.ts +1 -0
  27. package/dist/esm/index.js +1 -0
  28. package/dist/esm/index.js.map +1 -1
  29. package/dist/esm/web.d.ts +12 -1
  30. package/dist/esm/web.js +29 -2
  31. package/dist/esm/web.js.map +1 -1
  32. package/dist/plugin.cjs.js +311 -2
  33. package/dist/plugin.cjs.js.map +1 -1
  34. package/dist/plugin.js +311 -2
  35. package/dist/plugin.js.map +1 -1
  36. package/ios/{Plugin → Sources/CapacitorUpdaterPlugin}/AES.swift +6 -3
  37. package/ios/Sources/CapacitorUpdaterPlugin/CapacitorUpdaterPlugin.swift +1578 -0
  38. package/ios/{Plugin/CapacitorUpdater.swift → Sources/CapacitorUpdaterPlugin/CapgoUpdater.swift} +408 -139
  39. package/ios/{Plugin/CryptoCipher.swift → Sources/CapacitorUpdaterPlugin/CryptoCipherV1.swift} +13 -6
  40. package/ios/{Plugin → Sources/CapacitorUpdaterPlugin}/CryptoCipherV2.swift +33 -27
  41. package/ios/Sources/CapacitorUpdaterPlugin/DelayUpdateUtils.swift +220 -0
  42. package/ios/Sources/CapacitorUpdaterPlugin/DeviceIdHelper.swift +120 -0
  43. package/ios/{Plugin → Sources/CapacitorUpdaterPlugin}/InternalUtils.swift +47 -0
  44. package/ios/Sources/CapacitorUpdaterPlugin/Logger.swift +310 -0
  45. package/ios/{Plugin → Sources/CapacitorUpdaterPlugin}/RSA.swift +1 -0
  46. package/ios/Sources/CapacitorUpdaterPlugin/ShakeMenu.swift +112 -0
  47. package/ios/{Plugin → Sources/CapacitorUpdaterPlugin}/UserDefaultsExtension.swift +0 -2
  48. package/package.json +20 -16
  49. package/ios/Plugin/CapacitorUpdaterPlugin.swift +0 -1030
  50. /package/{LICENCE → LICENSE} +0 -0
  51. /package/ios/{Plugin → Sources/CapacitorUpdaterPlugin}/BigInt.swift +0 -0
  52. /package/ios/{Plugin → Sources/CapacitorUpdaterPlugin}/BundleInfo.swift +0 -0
  53. /package/ios/{Plugin → Sources/CapacitorUpdaterPlugin}/BundleStatus.swift +0 -0
  54. /package/ios/{Plugin → Sources/CapacitorUpdaterPlugin}/DelayCondition.swift +0 -0
  55. /package/ios/{Plugin → Sources/CapacitorUpdaterPlugin}/DelayUntilNext.swift +0 -0
  56. /package/ios/{Plugin → Sources/CapacitorUpdaterPlugin}/Info.plist +0 -0
@@ -10,6 +10,8 @@ package ee.forgr.capacitor_updater;
10
10
  * Created by Awesometic
11
11
  * It's encrypt returns Base64 encoded, and also decrypt for Base64 encoded cipher
12
12
  * references: http://stackoverflow.com/questions/12471999/rsa-encryption-decryption-in-android
13
+ *
14
+ * V1 Encryption - uses privateKey (deprecated but kept for backwards compatibility)
13
15
  */
14
16
  import android.util.Base64;
15
17
  import android.util.Log;
@@ -41,7 +43,13 @@ import javax.crypto.spec.OAEPParameterSpec;
41
43
  import javax.crypto.spec.PSource;
42
44
  import javax.crypto.spec.SecretKeySpec;
43
45
 
44
- public class CryptoCipher {
46
+ public class CryptoCipherV1 {
47
+
48
+ private static Logger logger;
49
+
50
+ public static void setLogger(Logger loggerInstance) {
51
+ logger = loggerInstance;
52
+ }
45
53
 
46
54
  public static byte[] decryptRSA(byte[] source, PrivateKey privateKey)
47
55
  throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidAlgorithmParameterException, InvalidKeyException, IllegalBlockSizeException, BadPaddingException {
@@ -145,10 +153,10 @@ public class CryptoCipher {
145
153
  throws IOException {
146
154
  // (str != null && !str.isEmpty())
147
155
  if (privateKey == null || privateKey.isEmpty()) {
148
- Log.i(CapacitorUpdater.TAG, "Cannot found privateKey");
156
+ Log.i("[Capacitor-updater]", "Cannot found privateKey");
149
157
  return;
150
158
  } else if (ivSessionKey == null || ivSessionKey.isEmpty() || ivSessionKey.split(":").length != 2) {
151
- Log.i(CapacitorUpdater.TAG, "Cannot found sessionKey");
159
+ Log.i("[Capacitor-updater]", "Cannot found sessionKey");
152
160
  return;
153
161
  }
154
162
  try {
@@ -156,9 +164,9 @@ public class CryptoCipher {
156
164
  String sessionKeyB64 = ivSessionKey.split(":")[1];
157
165
  byte[] iv = Base64.decode(ivB64.getBytes(), Base64.DEFAULT);
158
166
  byte[] sessionKey = Base64.decode(sessionKeyB64.getBytes(), Base64.DEFAULT);
159
- PrivateKey pKey = CryptoCipher.stringToPrivateKey(privateKey);
160
- byte[] decryptedSessionKey = CryptoCipher.decryptRSA(sessionKey, pKey);
161
- SecretKey sKey = CryptoCipher.byteToSessionKey(decryptedSessionKey);
167
+ PrivateKey pKey = CryptoCipherV1.stringToPrivateKey(privateKey);
168
+ byte[] decryptedSessionKey = CryptoCipherV1.decryptRSA(sessionKey, pKey);
169
+ SecretKey sKey = CryptoCipherV1.byteToSessionKey(decryptedSessionKey);
162
170
  byte[] content = new byte[(int) file.length()];
163
171
 
164
172
  try (
@@ -168,14 +176,14 @@ public class CryptoCipher {
168
176
  ) {
169
177
  dis.readFully(content);
170
178
  dis.close();
171
- byte[] decrypted = CryptoCipher.decryptAES(content, sKey, iv);
179
+ byte[] decrypted = CryptoCipherV1.decryptAES(content, sKey, iv);
172
180
  // write the decrypted string to the file
173
181
  try (final FileOutputStream fos = new FileOutputStream(file.getAbsolutePath())) {
174
182
  fos.write(decrypted);
175
183
  }
176
184
  }
177
185
  } catch (GeneralSecurityException e) {
178
- Log.i(CapacitorUpdater.TAG, "decryptFile fail");
186
+ Log.i("[Capacitor-updater]", "decryptFile fail");
179
187
  e.printStackTrace();
180
188
  throw new IOException("GeneralSecurityException");
181
189
  }
@@ -193,7 +201,7 @@ public class CryptoCipher {
193
201
  }
194
202
  return String.format("%08x", crc.getValue());
195
203
  } catch (IOException e) {
196
- System.err.println(CapacitorUpdater.TAG + " Cannot calc checksum: " + file.getPath() + " " + e.getMessage());
204
+ System.err.println("[Capacitor-updater]" + " Cannot calc checksum: " + file.getPath() + " " + e.getMessage());
197
205
  return "";
198
206
  }
199
207
  }
@@ -10,9 +10,10 @@ package ee.forgr.capacitor_updater;
10
10
  * Created by Awesometic
11
11
  * It's encrypt returns Base64 encoded, and also decrypt for Base64 encoded cipher
12
12
  * references: http://stackoverflow.com/questions/12471999/rsa-encryption-decryption-in-android
13
+ *
14
+ * V2 Encryption - uses publicKey (modern encryption from main branch)
13
15
  */
14
16
  import android.util.Base64;
15
- import android.util.Log;
16
17
  import java.io.BufferedInputStream;
17
18
  import java.io.DataInputStream;
18
19
  import java.io.File;
@@ -38,6 +39,12 @@ import javax.crypto.spec.SecretKeySpec;
38
39
 
39
40
  public class CryptoCipherV2 {
40
41
 
42
+ private static Logger logger;
43
+
44
+ public static void setLogger(Logger loggerInstance) {
45
+ logger = loggerInstance;
46
+ }
47
+
41
48
  public static byte[] decryptRSA(byte[] source, PublicKey publicKey)
42
49
  throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidAlgorithmParameterException, InvalidKeyException, IllegalBlockSizeException, BadPaddingException {
43
50
  Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
@@ -135,11 +142,11 @@ public class CryptoCipherV2 {
135
142
 
136
143
  public static void decryptFile(final File file, final String publicKey, final String ivSessionKey) throws IOException {
137
144
  if (publicKey.isEmpty() || ivSessionKey == null || ivSessionKey.isEmpty() || ivSessionKey.split(":").length != 2) {
138
- Log.i(CapacitorUpdater.TAG, "Cannot found public key or sessionKey");
145
+ logger.info("Encryption not set, no public key or seesion, ignored");
139
146
  return;
140
147
  }
141
148
  if (!publicKey.startsWith("-----BEGIN RSA PUBLIC KEY-----")) {
142
- Log.e(CapacitorUpdater.TAG, "The public key is not a valid RSA Public key");
149
+ logger.error("The public key is not a valid RSA Public key");
143
150
  return;
144
151
  }
145
152
 
@@ -168,7 +175,7 @@ public class CryptoCipherV2 {
168
175
  }
169
176
  }
170
177
  } catch (GeneralSecurityException e) {
171
- Log.i(CapacitorUpdater.TAG, "decryptFile fail");
178
+ logger.info("decryptFile fail");
172
179
  e.printStackTrace();
173
180
  throw new IOException("GeneralSecurityException");
174
181
  }
@@ -176,25 +183,7 @@ public class CryptoCipherV2 {
176
183
 
177
184
  public static String decryptChecksum(String checksum, String publicKey) throws IOException {
178
185
  if (publicKey.isEmpty()) {
179
- Log.e(CapacitorUpdater.TAG, "The public key is empty");
180
- return checksum;
181
- }
182
- try {
183
- byte[] checksumBytes = Base64.decode(checksum, Base64.DEFAULT);
184
- PublicKey pKey = CryptoCipherV2.stringToPublicKey(publicKey);
185
- byte[] decryptedChecksum = CryptoCipherV2.decryptRSA(checksumBytes, pKey);
186
- // return Base64.encodeToString(decryptedChecksum, Base64.DEFAULT);
187
- String result = Base64.encodeToString(decryptedChecksum, Base64.DEFAULT);
188
- return result.replaceAll("\\s", ""); // Remove all whitespace, including newlines
189
- } catch (GeneralSecurityException e) {
190
- Log.e(CapacitorUpdater.TAG, "decryptChecksum fail: " + e.getMessage());
191
- throw new IOException("Decryption failed: " + e.getMessage());
192
- }
193
- }
194
-
195
- public static String decryptChecksum(String checksum, String publicKey, String version) throws IOException {
196
- if (publicKey.isEmpty()) {
197
- Log.e(CapacitorUpdater.TAG, "The public key is empty");
186
+ logger.error("No encryption set (public key) ignored");
198
187
  return checksum;
199
188
  }
200
189
  try {
@@ -205,7 +194,7 @@ public class CryptoCipherV2 {
205
194
  String result = Base64.encodeToString(decryptedChecksum, Base64.DEFAULT);
206
195
  return result.replaceAll("\\s", ""); // Remove all whitespace, including newlines
207
196
  } catch (GeneralSecurityException e) {
208
- Log.e(CapacitorUpdater.TAG, "decryptChecksum fail: " + e.getMessage());
197
+ logger.error("decryptChecksum fail: " + e.getMessage());
209
198
  throw new IOException("Decryption failed: " + e.getMessage());
210
199
  }
211
200
  }
@@ -216,7 +205,7 @@ public class CryptoCipherV2 {
216
205
  try {
217
206
  digest = MessageDigest.getInstance("SHA-256");
218
207
  } catch (java.security.NoSuchAlgorithmException e) {
219
- System.err.println(CapacitorUpdater.TAG + " SHA-256 algorithm not available");
208
+ logger.error("SHA-256 algorithm not available");
220
209
  return "";
221
210
  }
222
211
 
@@ -235,7 +224,7 @@ public class CryptoCipherV2 {
235
224
  }
236
225
  return hexString.toString();
237
226
  } catch (IOException e) {
238
- System.err.println(CapacitorUpdater.TAG + " Cannot calc checksum v2: " + file.getPath() + " " + e.getMessage());
227
+ logger.error("Cannot calc checksum v2: " + file.getPath() + " " + e.getMessage());
239
228
  return "";
240
229
  }
241
230
  }
@@ -7,15 +7,12 @@
7
7
  package ee.forgr.capacitor_updater;
8
8
 
9
9
  import androidx.annotation.NonNull;
10
- import com.google.gson.annotations.SerializedName;
11
10
  import java.util.Objects;
12
11
 
13
12
  public class DelayCondition {
14
13
 
15
- @SerializedName("kind")
16
14
  private DelayUntilNext kind;
17
15
 
18
- @SerializedName("value")
19
16
  private String value;
20
17
 
21
18
  public DelayCondition(DelayUntilNext kind, String value) {
@@ -0,0 +1,260 @@
1
+ package ee.forgr.capacitor_updater;
2
+
3
+ import android.content.SharedPreferences;
4
+ import io.github.g00fy2.versioncompare.Version;
5
+ import java.text.SimpleDateFormat;
6
+ import java.util.ArrayList;
7
+ import java.util.Date;
8
+ import org.json.JSONArray;
9
+ import org.json.JSONException;
10
+ import org.json.JSONObject;
11
+
12
+ public class DelayUpdateUtils {
13
+
14
+ private final Logger logger;
15
+
16
+ public static final String DELAY_CONDITION_PREFERENCES = "DELAY_CONDITION_PREFERENCES_CAPGO";
17
+ public static final String BACKGROUND_TIMESTAMP_KEY = "BACKGROUND_TIMESTAMP_KEY_CAPGO";
18
+
19
+ private final SharedPreferences prefs;
20
+ private final SharedPreferences.Editor editor;
21
+ private final Version currentVersionNative;
22
+
23
+ public DelayUpdateUtils(SharedPreferences prefs, SharedPreferences.Editor editor, Version currentVersionNative, Logger logger) {
24
+ this.prefs = prefs;
25
+ this.editor = editor;
26
+ this.currentVersionNative = currentVersionNative;
27
+ this.logger = logger;
28
+ }
29
+
30
+ public enum CancelDelaySource {
31
+ KILLED,
32
+ BACKGROUND,
33
+ FOREGROUND
34
+ }
35
+
36
+ public void checkCancelDelay(CancelDelaySource source) {
37
+ String delayUpdatePreferences = prefs.getString(DELAY_CONDITION_PREFERENCES, "[]");
38
+ ArrayList<DelayCondition> delayConditionList = parseDelayConditions(delayUpdatePreferences);
39
+ ArrayList<DelayCondition> delayConditionListToKeep = new ArrayList<>(delayConditionList.size());
40
+ int index = 0;
41
+
42
+ for (DelayCondition condition : delayConditionList) {
43
+ DelayUntilNext kind = condition.getKind();
44
+ String value = condition.getValue();
45
+ switch (kind) {
46
+ case background:
47
+ if (source == CancelDelaySource.FOREGROUND) {
48
+ long backgroundedAt = getBackgroundTimestamp();
49
+ long now = System.currentTimeMillis();
50
+ long delta = Math.max(0, now - backgroundedAt);
51
+ long longValue = 0L;
52
+ try {
53
+ longValue = Long.parseLong(value);
54
+ } catch (NumberFormatException e) {
55
+ logger.error(
56
+ "Background condition (value: " +
57
+ value +
58
+ ") had an invalid value at index " +
59
+ index +
60
+ ". We will likely remove it."
61
+ );
62
+ }
63
+
64
+ if (delta > longValue) {
65
+ logger.info(
66
+ "Background condition (value: " +
67
+ value +
68
+ ") deleted at index " +
69
+ index +
70
+ ". Delta: " +
71
+ delta +
72
+ ", longValue: " +
73
+ longValue
74
+ );
75
+ }
76
+ } else {
77
+ delayConditionListToKeep.add(condition);
78
+ logger.info(
79
+ "Background delay (value: " +
80
+ value +
81
+ ") condition kept at index " +
82
+ index +
83
+ " (source: " +
84
+ source.toString() +
85
+ ")"
86
+ );
87
+ }
88
+ break;
89
+ case kill:
90
+ if (source == CancelDelaySource.KILLED) {
91
+ logger.info("Kill delay (value: " + value + ") condition removed at index " + index + " after app kill");
92
+ } else {
93
+ delayConditionListToKeep.add(condition);
94
+ logger.info(
95
+ "Kill delay (value: " + value + ") condition kept at index " + index + " (source: " + source.toString() + ")"
96
+ );
97
+ }
98
+ break;
99
+ case date:
100
+ if (!"".equals(value)) {
101
+ try {
102
+ final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS");
103
+ Date date = sdf.parse(value);
104
+ assert date != null;
105
+ if (new Date().compareTo(date) > 0) {
106
+ logger.info("Date delay (value: " + value + ") condition removed due to expired date at index " + index);
107
+ } else {
108
+ delayConditionListToKeep.add(condition);
109
+ logger.info("Date delay (value: " + value + ") condition kept at index " + index);
110
+ }
111
+ } catch (final Exception e) {
112
+ logger.error(
113
+ "Date delay (value: " +
114
+ value +
115
+ ") condition removed due to parsing issue at index " +
116
+ index +
117
+ " " +
118
+ e.getMessage()
119
+ );
120
+ }
121
+ } else {
122
+ logger.debug("Date delay (value: " + value + ") condition removed due to empty value at index " + index);
123
+ }
124
+ break;
125
+ case nativeVersion:
126
+ if (!"".equals(value)) {
127
+ try {
128
+ final Version versionLimit = new Version(value);
129
+ if (this.currentVersionNative.isAtLeast(versionLimit)) {
130
+ logger.info(
131
+ "Native version delay (value: " + value + ") condition removed due to above limit at index " + index
132
+ );
133
+ } else {
134
+ delayConditionListToKeep.add(condition);
135
+ logger.info("Native version delay (value: " + value + ") condition kept at index " + index);
136
+ }
137
+ } catch (final Exception e) {
138
+ logger.error(
139
+ "Native version delay (value: " +
140
+ value +
141
+ ") condition removed due to parsing issue at index " +
142
+ index +
143
+ " " +
144
+ e.getMessage()
145
+ );
146
+ }
147
+ } else {
148
+ logger.debug("Native version delay (value: " + value + ") condition removed due to empty value at index " + index);
149
+ }
150
+ break;
151
+ }
152
+ index++;
153
+ }
154
+
155
+ if (delayConditionListToKeep.isEmpty()) {
156
+ this.cancelDelay("checkCancelDelay");
157
+ } else {
158
+ this.setMultiDelay(convertDelayConditionsToJson(delayConditionListToKeep));
159
+ }
160
+ }
161
+
162
+ public ArrayList<DelayCondition> parseDelayConditions(String json) {
163
+ ArrayList<DelayCondition> conditions = new ArrayList<>();
164
+ if (json == null || json.isEmpty()) {
165
+ return conditions;
166
+ }
167
+ try {
168
+ JSONArray array = new JSONArray(json);
169
+ for (int i = 0; i < array.length(); i++) {
170
+ JSONObject item = array.optJSONObject(i);
171
+ if (item == null) {
172
+ continue;
173
+ }
174
+ String kindValue = item.optString("kind", "");
175
+ String value = item.optString("value", "");
176
+ if (kindValue.isEmpty()) {
177
+ logger.warn("Delay condition missing kind at index " + i);
178
+ continue;
179
+ }
180
+ try {
181
+ DelayUntilNext kind = DelayUntilNext.valueOf(kindValue);
182
+ conditions.add(new DelayCondition(kind, value));
183
+ } catch (IllegalArgumentException e) {
184
+ logger.warn("Unknown delay condition kind '" + kindValue + "' at index " + i);
185
+ }
186
+ }
187
+ } catch (JSONException e) {
188
+ logger.error("Failed to parse delay conditions: " + e.getMessage());
189
+ }
190
+ return conditions;
191
+ }
192
+
193
+ private String convertDelayConditionsToJson(ArrayList<DelayCondition> conditions) {
194
+ JSONArray array = new JSONArray();
195
+ for (DelayCondition condition : conditions) {
196
+ try {
197
+ JSONObject obj = new JSONObject();
198
+ obj.put("kind", condition.getKind().name());
199
+ obj.put("value", condition.getValue());
200
+ array.put(obj);
201
+ } catch (JSONException e) {
202
+ logger.error("Failed to serialize delay condition: " + e.getMessage());
203
+ }
204
+ }
205
+ return array.toString();
206
+ }
207
+
208
+ public Boolean setMultiDelay(String delayConditions) {
209
+ try {
210
+ this.editor.putString(DELAY_CONDITION_PREFERENCES, delayConditions);
211
+ this.editor.commit();
212
+ logger.info("Delay update saved");
213
+ return true;
214
+ } catch (final Exception e) {
215
+ logger.error("Failed to delay update, [Error calling '_setMultiDelay()'] " + e.getMessage());
216
+ return false;
217
+ }
218
+ }
219
+
220
+ public void setBackgroundTimestamp(long backgroundTimestamp) {
221
+ try {
222
+ this.editor.putLong(BACKGROUND_TIMESTAMP_KEY, backgroundTimestamp);
223
+ this.editor.commit();
224
+ logger.info("Background timestamp set");
225
+ } catch (final Exception e) {
226
+ logger.error("Failed to delay update, [Error calling '_setBackgroundTimestamp()'] " + e.getMessage());
227
+ }
228
+ }
229
+
230
+ public void unsetBackgroundTimestamp() {
231
+ try {
232
+ this.editor.remove(BACKGROUND_TIMESTAMP_KEY);
233
+ this.editor.commit();
234
+ logger.info("Background timestamp unset");
235
+ } catch (final Exception e) {
236
+ logger.error("Failed to delay update, [Error calling '_unsetBackgroundTimestamp()'] " + e.getMessage());
237
+ }
238
+ }
239
+
240
+ private long getBackgroundTimestamp() {
241
+ try {
242
+ return this.prefs.getLong(BACKGROUND_TIMESTAMP_KEY, 0);
243
+ } catch (final Exception e) {
244
+ logger.error("Failed to delay update, [Error calling '_getBackgroundTimestamp()'] " + e.getMessage());
245
+ return 0;
246
+ }
247
+ }
248
+
249
+ public boolean cancelDelay(String source) {
250
+ try {
251
+ this.editor.remove(DELAY_CONDITION_PREFERENCES);
252
+ this.editor.commit();
253
+ logger.info("All delays canceled from " + source);
254
+ return true;
255
+ } catch (final Exception e) {
256
+ logger.error("Failed to cancel update delay " + e.getMessage());
257
+ return false;
258
+ }
259
+ }
260
+ }
@@ -0,0 +1,221 @@
1
+ /*
2
+ * This Source Code Form is subject to the terms of the Mozilla Public
3
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
4
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
5
+ */
6
+
7
+ package ee.forgr.capacitor_updater;
8
+
9
+ import android.content.Context;
10
+ import android.content.SharedPreferences;
11
+ import android.os.Build;
12
+ import android.security.keystore.KeyGenParameterSpec;
13
+ import android.security.keystore.KeyProperties;
14
+ import java.io.IOException;
15
+ import java.nio.charset.StandardCharsets;
16
+ import java.security.KeyStore;
17
+ import java.security.KeyStoreException;
18
+ import java.security.NoSuchAlgorithmException;
19
+ import java.security.NoSuchProviderException;
20
+ import java.security.UnrecoverableEntryException;
21
+ import java.security.cert.CertificateException;
22
+ import java.util.UUID;
23
+ import javax.crypto.Cipher;
24
+ import javax.crypto.KeyGenerator;
25
+ import javax.crypto.SecretKey;
26
+ import javax.crypto.spec.GCMParameterSpec;
27
+
28
+ /**
29
+ * Helper class to manage device ID persistence across app installations.
30
+ * Uses Android Keystore to persist the device ID across reinstalls.
31
+ *
32
+ * The device ID is a random UUID stored in the Android Keystore, which persists
33
+ * even after app uninstall/reinstall on Android 6.0+ (API 23+).
34
+ */
35
+ public class DeviceIdHelper {
36
+
37
+ private static final String KEYSTORE_ALIAS = "capgo_device_id_key";
38
+ private static final String ANDROID_KEYSTORE = "AndroidKeyStore";
39
+ private static final String LEGACY_PREFS_KEY = "appUUID";
40
+ private static final String DEVICE_ID_PREFS = "capgo_device_id";
41
+ private static final String DEVICE_ID_KEY = "deviceId";
42
+ private static final String IV_KEY = "iv";
43
+ private static final int GCM_TAG_LENGTH = 128;
44
+
45
+ /**
46
+ * Gets or creates a device ID that persists across reinstalls.
47
+ *
48
+ * This method:
49
+ * 1. First checks for an existing ID in Keystore-encrypted storage (persists across reinstalls)
50
+ * 2. Falls back to legacy SharedPreferences (for migration)
51
+ * 3. Generates a new UUID if neither exists
52
+ * 4. Stores the ID in Keystore-encrypted storage for future use
53
+ *
54
+ * @param context Application context
55
+ * @param legacyPrefs Legacy SharedPreferences (for migration)
56
+ * @return Device ID as a lowercase UUID string
57
+ */
58
+ public static String getOrCreateDeviceId(Context context, SharedPreferences legacyPrefs) {
59
+ // API 23+ required for Android Keystore
60
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
61
+ return getFallbackDeviceId(legacyPrefs);
62
+ }
63
+
64
+ try {
65
+ // Try to get device ID from Keystore storage
66
+ String deviceId = getDeviceIdFromKeystore(context);
67
+
68
+ if (deviceId != null && !deviceId.isEmpty()) {
69
+ return deviceId.toLowerCase();
70
+ }
71
+
72
+ // Migration: Check legacy SharedPreferences for existing device ID
73
+ deviceId = legacyPrefs.getString(LEGACY_PREFS_KEY, null);
74
+
75
+ if (deviceId == null || deviceId.isEmpty()) {
76
+ // Generate new device ID if none exists
77
+ deviceId = UUID.randomUUID().toString();
78
+ }
79
+
80
+ // Ensure lowercase for consistency
81
+ deviceId = deviceId.toLowerCase();
82
+
83
+ // Save to Keystore storage
84
+ saveDeviceIdToKeystore(context, deviceId);
85
+
86
+ return deviceId;
87
+ } catch (Exception e) {
88
+ // Fallback to legacy method if Keystore fails
89
+ return getFallbackDeviceId(legacyPrefs);
90
+ }
91
+ }
92
+
93
+ /**
94
+ * Retrieves the device ID from Keystore-encrypted storage.
95
+ *
96
+ * @param context Application context
97
+ * @return Device ID string or null if not found
98
+ */
99
+ private static String getDeviceIdFromKeystore(Context context) throws Exception {
100
+ SharedPreferences prefs = context.getSharedPreferences(DEVICE_ID_PREFS, Context.MODE_PRIVATE);
101
+ String encryptedDeviceId = prefs.getString(DEVICE_ID_KEY, null);
102
+ String ivString = prefs.getString(IV_KEY, null);
103
+
104
+ if (encryptedDeviceId == null || ivString == null) {
105
+ return null;
106
+ }
107
+
108
+ // Get the encryption key from Keystore
109
+ SecretKey key = getOrCreateKey();
110
+ if (key == null) {
111
+ return null;
112
+ }
113
+
114
+ // Decrypt the device ID
115
+ byte[] encryptedBytes = android.util.Base64.decode(encryptedDeviceId, android.util.Base64.DEFAULT);
116
+ byte[] iv = android.util.Base64.decode(ivString, android.util.Base64.DEFAULT);
117
+
118
+ Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
119
+ GCMParameterSpec spec = new GCMParameterSpec(GCM_TAG_LENGTH, iv);
120
+ cipher.init(Cipher.DECRYPT_MODE, key, spec);
121
+
122
+ byte[] decryptedBytes = cipher.doFinal(encryptedBytes);
123
+ return new String(decryptedBytes, StandardCharsets.UTF_8);
124
+ }
125
+
126
+ /**
127
+ * Saves the device ID to Keystore-encrypted storage.
128
+ *
129
+ * The device ID is encrypted using AES/GCM with a key stored in Android Keystore.
130
+ * The Keystore key persists across reinstalls on Android 6.0+ (API 23+).
131
+ *
132
+ * @param context Application context
133
+ * @param deviceId The device ID to save
134
+ */
135
+ private static void saveDeviceIdToKeystore(Context context, String deviceId) throws Exception {
136
+ // Get or create encryption key in Keystore
137
+ SecretKey key = getOrCreateKey();
138
+ if (key == null) {
139
+ throw new Exception("Failed to get encryption key");
140
+ }
141
+
142
+ // Encrypt the device ID
143
+ Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
144
+ cipher.init(Cipher.ENCRYPT_MODE, key);
145
+
146
+ byte[] iv = cipher.getIV();
147
+ byte[] encryptedBytes = cipher.doFinal(deviceId.getBytes(StandardCharsets.UTF_8));
148
+
149
+ // Store encrypted device ID and IV in SharedPreferences
150
+ SharedPreferences prefs = context.getSharedPreferences(DEVICE_ID_PREFS, Context.MODE_PRIVATE);
151
+ prefs
152
+ .edit()
153
+ .putString(DEVICE_ID_KEY, android.util.Base64.encodeToString(encryptedBytes, android.util.Base64.DEFAULT))
154
+ .putString(IV_KEY, android.util.Base64.encodeToString(iv, android.util.Base64.DEFAULT))
155
+ .apply();
156
+ }
157
+
158
+ /**
159
+ * Gets or creates the encryption key in Android Keystore.
160
+ *
161
+ * The key is configured to persist across reinstalls and not require user authentication.
162
+ *
163
+ * @return SecretKey from Keystore or null if failed
164
+ */
165
+ private static SecretKey getOrCreateKey() {
166
+ try {
167
+ KeyStore keyStore = KeyStore.getInstance(ANDROID_KEYSTORE);
168
+ keyStore.load(null);
169
+
170
+ // Check if key already exists
171
+ if (keyStore.containsAlias(KEYSTORE_ALIAS)) {
172
+ KeyStore.SecretKeyEntry entry = (KeyStore.SecretKeyEntry) keyStore.getEntry(KEYSTORE_ALIAS, null);
173
+ return entry.getSecretKey();
174
+ }
175
+
176
+ // Create new key
177
+ KeyGenerator keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, ANDROID_KEYSTORE);
178
+
179
+ KeyGenParameterSpec keySpec = new KeyGenParameterSpec.Builder(
180
+ KEYSTORE_ALIAS,
181
+ KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT
182
+ )
183
+ .setBlockModes(KeyProperties.BLOCK_MODE_GCM)
184
+ .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
185
+ .setKeySize(256)
186
+ .setRandomizedEncryptionRequired(true)
187
+ .build();
188
+
189
+ keyGenerator.init(keySpec);
190
+ return keyGenerator.generateKey();
191
+ } catch (
192
+ KeyStoreException
193
+ | CertificateException
194
+ | NoSuchAlgorithmException
195
+ | IOException
196
+ | NoSuchProviderException
197
+ | UnrecoverableEntryException e
198
+ ) {
199
+ return null;
200
+ } catch (Exception e) {
201
+ return null;
202
+ }
203
+ }
204
+
205
+ /**
206
+ * Fallback method using legacy SharedPreferences if Keystore fails or API < 23.
207
+ *
208
+ * @param legacyPrefs Legacy SharedPreferences
209
+ * @return Device ID string
210
+ */
211
+ private static String getFallbackDeviceId(SharedPreferences legacyPrefs) {
212
+ String deviceId = legacyPrefs.getString(LEGACY_PREFS_KEY, null);
213
+
214
+ if (deviceId == null || deviceId.isEmpty()) {
215
+ deviceId = UUID.randomUUID().toString();
216
+ legacyPrefs.edit().putString(LEGACY_PREFS_KEY, deviceId).apply();
217
+ }
218
+
219
+ return deviceId.toLowerCase();
220
+ }
221
+ }