@capgo/capacitor-updater 4.41.0 → 4.43.5

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 (67) hide show
  1. package/CapgoCapacitorUpdater.podspec +7 -5
  2. package/Package.swift +40 -0
  3. package/README.md +1913 -303
  4. package/android/build.gradle +41 -8
  5. package/android/proguard-rules.pro +45 -0
  6. package/android/src/main/AndroidManifest.xml +1 -3
  7. package/android/src/main/java/ee/forgr/capacitor_updater/AppLifecycleObserver.java +88 -0
  8. package/android/src/main/java/ee/forgr/capacitor_updater/BundleInfo.java +223 -195
  9. package/android/src/main/java/ee/forgr/capacitor_updater/BundleStatus.java +23 -23
  10. package/android/src/main/java/ee/forgr/capacitor_updater/Callback.java +13 -0
  11. package/android/src/main/java/ee/forgr/capacitor_updater/CapacitorUpdaterPlugin.java +2720 -1242
  12. package/android/src/main/java/ee/forgr/capacitor_updater/CapgoUpdater.java +1854 -0
  13. package/android/src/main/java/ee/forgr/capacitor_updater/CryptoCipher.java +359 -121
  14. package/android/src/main/java/ee/forgr/capacitor_updater/DataManager.java +28 -0
  15. package/android/src/main/java/ee/forgr/capacitor_updater/DelayCondition.java +44 -49
  16. package/android/src/main/java/ee/forgr/capacitor_updater/DelayUntilNext.java +4 -4
  17. package/android/src/main/java/ee/forgr/capacitor_updater/DelayUpdateUtils.java +296 -0
  18. package/android/src/main/java/ee/forgr/capacitor_updater/DeviceIdHelper.java +215 -0
  19. package/android/src/main/java/ee/forgr/capacitor_updater/DownloadService.java +858 -117
  20. package/android/src/main/java/ee/forgr/capacitor_updater/DownloadWorkerManager.java +156 -0
  21. package/android/src/main/java/ee/forgr/capacitor_updater/InternalUtils.java +45 -0
  22. package/android/src/main/java/ee/forgr/capacitor_updater/Logger.java +360 -0
  23. package/android/src/main/java/ee/forgr/capacitor_updater/ShakeDetector.java +72 -0
  24. package/android/src/main/java/ee/forgr/capacitor_updater/ShakeMenu.java +603 -0
  25. package/dist/docs.json +3022 -765
  26. package/dist/esm/definitions.d.ts +1717 -198
  27. package/dist/esm/definitions.js +103 -1
  28. package/dist/esm/definitions.js.map +1 -1
  29. package/dist/esm/history.d.ts +1 -0
  30. package/dist/esm/history.js +283 -0
  31. package/dist/esm/history.js.map +1 -0
  32. package/dist/esm/index.d.ts +3 -2
  33. package/dist/esm/index.js +5 -4
  34. package/dist/esm/index.js.map +1 -1
  35. package/dist/esm/web.d.ts +43 -42
  36. package/dist/esm/web.js +122 -37
  37. package/dist/esm/web.js.map +1 -1
  38. package/dist/plugin.cjs.js +512 -37
  39. package/dist/plugin.cjs.js.map +1 -1
  40. package/dist/plugin.js +512 -37
  41. package/dist/plugin.js.map +1 -1
  42. package/ios/Sources/CapacitorUpdaterPlugin/AES.swift +87 -0
  43. package/ios/Sources/CapacitorUpdaterPlugin/BigInt.swift +55 -0
  44. package/ios/Sources/CapacitorUpdaterPlugin/BundleInfo.swift +177 -0
  45. package/ios/{Plugin → Sources/CapacitorUpdaterPlugin}/BundleStatus.swift +12 -12
  46. package/ios/Sources/CapacitorUpdaterPlugin/CapacitorUpdaterPlugin.swift +2020 -0
  47. package/ios/Sources/CapacitorUpdaterPlugin/CapgoUpdater.swift +1959 -0
  48. package/ios/Sources/CapacitorUpdaterPlugin/CryptoCipher.swift +313 -0
  49. package/ios/Sources/CapacitorUpdaterPlugin/DelayUpdateUtils.swift +257 -0
  50. package/ios/Sources/CapacitorUpdaterPlugin/DeviceIdHelper.swift +120 -0
  51. package/ios/Sources/CapacitorUpdaterPlugin/InternalUtils.swift +392 -0
  52. package/ios/Sources/CapacitorUpdaterPlugin/Logger.swift +310 -0
  53. package/ios/Sources/CapacitorUpdaterPlugin/RSA.swift +274 -0
  54. package/ios/Sources/CapacitorUpdaterPlugin/ShakeMenu.swift +441 -0
  55. package/ios/{Plugin → Sources/CapacitorUpdaterPlugin}/UserDefaultsExtension.swift +1 -2
  56. package/package.json +49 -41
  57. package/android/src/main/java/ee/forgr/capacitor_updater/CapacitorUpdater.java +0 -1131
  58. package/ios/Plugin/BundleInfo.swift +0 -113
  59. package/ios/Plugin/CapacitorUpdater.swift +0 -850
  60. package/ios/Plugin/CapacitorUpdaterPlugin.h +0 -10
  61. package/ios/Plugin/CapacitorUpdaterPlugin.m +0 -27
  62. package/ios/Plugin/CapacitorUpdaterPlugin.swift +0 -678
  63. package/ios/Plugin/CryptoCipher.swift +0 -240
  64. /package/{LICENCE → LICENSE} +0 -0
  65. /package/ios/{Plugin → Sources/CapacitorUpdaterPlugin}/DelayCondition.swift +0 -0
  66. /package/ios/{Plugin → Sources/CapacitorUpdaterPlugin}/DelayUntilNext.swift +0 -0
  67. /package/ios/{Plugin → Sources/CapacitorUpdaterPlugin}/Info.plist +0 -0
@@ -0,0 +1,296 @@
1
+ package ee.forgr.capacitor_updater;
2
+
3
+ import android.content.SharedPreferences;
4
+ import io.github.g00fy2.versioncompare.Version;
5
+ import java.text.ParsePosition;
6
+ import java.text.SimpleDateFormat;
7
+ import java.util.ArrayList;
8
+ import java.util.Date;
9
+ import java.util.Locale;
10
+ import java.util.TimeZone;
11
+ import org.json.JSONArray;
12
+ import org.json.JSONException;
13
+ import org.json.JSONObject;
14
+
15
+ public class DelayUpdateUtils {
16
+
17
+ private final Logger logger;
18
+
19
+ public static final String DELAY_CONDITION_PREFERENCES = "DELAY_CONDITION_PREFERENCES_CAPGO";
20
+ public static final String BACKGROUND_TIMESTAMP_KEY = "BACKGROUND_TIMESTAMP_KEY_CAPGO";
21
+
22
+ private final SharedPreferences prefs;
23
+ private final SharedPreferences.Editor editor;
24
+ private final Version currentVersionNative;
25
+
26
+ public DelayUpdateUtils(SharedPreferences prefs, SharedPreferences.Editor editor, Version currentVersionNative, Logger logger) {
27
+ this.prefs = prefs;
28
+ this.editor = editor;
29
+ this.currentVersionNative = currentVersionNative;
30
+ this.logger = logger;
31
+ }
32
+
33
+ public enum CancelDelaySource {
34
+ KILLED,
35
+ BACKGROUND,
36
+ FOREGROUND
37
+ }
38
+
39
+ public void checkCancelDelay(CancelDelaySource source) {
40
+ String delayUpdatePreferences = prefs.getString(DELAY_CONDITION_PREFERENCES, "[]");
41
+ ArrayList<DelayCondition> delayConditionList = parseDelayConditions(delayUpdatePreferences);
42
+ ArrayList<DelayCondition> delayConditionListToKeep = new ArrayList<>(delayConditionList.size());
43
+ int index = 0;
44
+
45
+ for (DelayCondition condition : delayConditionList) {
46
+ DelayUntilNext kind = condition.getKind();
47
+ String value = condition.getValue();
48
+ switch (kind) {
49
+ case background:
50
+ if (source == CancelDelaySource.FOREGROUND) {
51
+ long backgroundedAt = getBackgroundTimestamp();
52
+ long now = System.currentTimeMillis();
53
+ long delta = Math.max(0, now - backgroundedAt);
54
+ long longValue = 0L;
55
+ try {
56
+ longValue = Long.parseLong(value);
57
+ } catch (NumberFormatException e) {
58
+ logger.error(
59
+ "Background condition (value: " +
60
+ value +
61
+ ") had an invalid value at index " +
62
+ index +
63
+ ". We will likely remove it."
64
+ );
65
+ }
66
+
67
+ if (delta > longValue) {
68
+ logger.info(
69
+ "Background condition (value: " +
70
+ value +
71
+ ") deleted at index " +
72
+ index +
73
+ ". Delta: " +
74
+ delta +
75
+ ", longValue: " +
76
+ longValue
77
+ );
78
+ }
79
+ } else {
80
+ delayConditionListToKeep.add(condition);
81
+ logger.info(
82
+ "Background delay (value: " +
83
+ value +
84
+ ") condition kept at index " +
85
+ index +
86
+ " (source: " +
87
+ source.toString() +
88
+ ")"
89
+ );
90
+ }
91
+ break;
92
+ case kill:
93
+ if (source == CancelDelaySource.KILLED) {
94
+ logger.info("Kill delay (value: " + value + ") condition removed at index " + index + " after app kill");
95
+ } else {
96
+ delayConditionListToKeep.add(condition);
97
+ logger.info(
98
+ "Kill delay (value: " + value + ") condition kept at index " + index + " (source: " + source.toString() + ")"
99
+ );
100
+ }
101
+ break;
102
+ case date:
103
+ if (!"".equals(value)) {
104
+ Date date = parseDateCondition(value);
105
+ if (date != null) {
106
+ if (new Date().compareTo(date) > 0) {
107
+ logger.info("Date delay (value: " + value + ") condition removed due to expired date at index " + index);
108
+ } else {
109
+ delayConditionListToKeep.add(condition);
110
+ logger.info("Date delay (value: " + value + ") condition kept at index " + index);
111
+ }
112
+ } else {
113
+ logger.error("Date delay (value: " + value + ") condition removed due to parsing issue at index " + index);
114
+ }
115
+ } else {
116
+ logger.debug("Date delay (value: " + value + ") condition removed due to empty value at index " + index);
117
+ }
118
+ break;
119
+ case nativeVersion:
120
+ if (!"".equals(value)) {
121
+ try {
122
+ final Version versionLimit = new Version(value);
123
+ if (this.currentVersionNative.isAtLeast(versionLimit)) {
124
+ logger.info(
125
+ "Native version delay (value: " + value + ") condition removed due to above limit at index " + index
126
+ );
127
+ } else {
128
+ delayConditionListToKeep.add(condition);
129
+ logger.info("Native version delay (value: " + value + ") condition kept at index " + index);
130
+ }
131
+ } catch (final Exception e) {
132
+ logger.error(
133
+ "Native version delay (value: " +
134
+ value +
135
+ ") condition removed due to parsing issue at index " +
136
+ index +
137
+ " " +
138
+ e.getMessage()
139
+ );
140
+ }
141
+ } else {
142
+ logger.debug("Native version delay (value: " + value + ") condition removed due to empty value at index " + index);
143
+ }
144
+ break;
145
+ }
146
+ index++;
147
+ }
148
+
149
+ if (delayConditionListToKeep.isEmpty()) {
150
+ this.cancelDelay("checkCancelDelay");
151
+ } else {
152
+ this.setMultiDelay(convertDelayConditionsToJson(delayConditionListToKeep));
153
+ }
154
+ }
155
+
156
+ public ArrayList<DelayCondition> parseDelayConditions(String json) {
157
+ ArrayList<DelayCondition> conditions = new ArrayList<>();
158
+ if (json == null || json.isEmpty()) {
159
+ return conditions;
160
+ }
161
+ try {
162
+ JSONArray array = new JSONArray(json);
163
+ for (int i = 0; i < array.length(); i++) {
164
+ JSONObject item = array.optJSONObject(i);
165
+ if (item == null) {
166
+ continue;
167
+ }
168
+ String kindValue = item.optString("kind", "");
169
+ String value = item.optString("value", "");
170
+ if (kindValue.isEmpty()) {
171
+ logger.warn("Delay condition missing kind at index " + i);
172
+ continue;
173
+ }
174
+ try {
175
+ DelayUntilNext kind = DelayUntilNext.valueOf(kindValue);
176
+ conditions.add(new DelayCondition(kind, value));
177
+ } catch (IllegalArgumentException e) {
178
+ logger.warn("Unknown delay condition kind '" + kindValue + "' at index " + i);
179
+ }
180
+ }
181
+ } catch (JSONException e) {
182
+ logger.error("Failed to parse delay conditions: " + e.getMessage());
183
+ }
184
+ return conditions;
185
+ }
186
+
187
+ private Date parseDateCondition(String value) {
188
+ String[] patterns = {
189
+ "yyyy-MM-dd'T'HH:mm:ss.SSSXXX",
190
+ "yyyy-MM-dd'T'HH:mm:ssXXX",
191
+ "yyyy-MM-dd'T'HH:mm:ss.SSSXX",
192
+ "yyyy-MM-dd'T'HH:mm:ssXX",
193
+ "yyyy-MM-dd'T'HH:mm:ss.SSSX",
194
+ "yyyy-MM-dd'T'HH:mm:ssX",
195
+ "yyyy-MM-dd'T'HH:mm:ss.SSS",
196
+ "yyyy-MM-dd'T'HH:mm:ss"
197
+ };
198
+
199
+ for (String pattern : patterns) {
200
+ Date parsed = parseDateWithPattern(value, pattern);
201
+ if (parsed != null) {
202
+ return parsed;
203
+ }
204
+ }
205
+
206
+ return null;
207
+ }
208
+
209
+ private Date parseDateWithPattern(String value, String pattern) {
210
+ try {
211
+ SimpleDateFormat sdf = new SimpleDateFormat(pattern, Locale.US);
212
+ sdf.setLenient(false);
213
+
214
+ // If no timezone is provided, keep historical behavior and interpret as local time.
215
+ if (!pattern.contains("X")) {
216
+ sdf.setTimeZone(TimeZone.getDefault());
217
+ }
218
+
219
+ ParsePosition position = new ParsePosition(0);
220
+ Date parsed = sdf.parse(value, position);
221
+ if (parsed != null && position.getIndex() == value.length()) {
222
+ return parsed;
223
+ }
224
+ } catch (Exception ignored) {}
225
+
226
+ return null;
227
+ }
228
+
229
+ private String convertDelayConditionsToJson(ArrayList<DelayCondition> conditions) {
230
+ JSONArray array = new JSONArray();
231
+ for (DelayCondition condition : conditions) {
232
+ try {
233
+ JSONObject obj = new JSONObject();
234
+ obj.put("kind", condition.getKind().name());
235
+ obj.put("value", condition.getValue());
236
+ array.put(obj);
237
+ } catch (JSONException e) {
238
+ logger.error("Failed to serialize delay condition: " + e.getMessage());
239
+ }
240
+ }
241
+ return array.toString();
242
+ }
243
+
244
+ public Boolean setMultiDelay(String delayConditions) {
245
+ try {
246
+ this.editor.putString(DELAY_CONDITION_PREFERENCES, delayConditions);
247
+ this.editor.commit();
248
+ logger.info("Delay update saved");
249
+ return true;
250
+ } catch (final Exception e) {
251
+ logger.error("Failed to delay update, [Error calling '_setMultiDelay()'] " + e.getMessage());
252
+ return false;
253
+ }
254
+ }
255
+
256
+ public void setBackgroundTimestamp(long backgroundTimestamp) {
257
+ try {
258
+ this.editor.putLong(BACKGROUND_TIMESTAMP_KEY, backgroundTimestamp);
259
+ this.editor.commit();
260
+ logger.info("Background timestamp set");
261
+ } catch (final Exception e) {
262
+ logger.error("Failed to delay update, [Error calling '_setBackgroundTimestamp()'] " + e.getMessage());
263
+ }
264
+ }
265
+
266
+ public void unsetBackgroundTimestamp() {
267
+ try {
268
+ this.editor.remove(BACKGROUND_TIMESTAMP_KEY);
269
+ this.editor.commit();
270
+ logger.info("Background timestamp unset");
271
+ } catch (final Exception e) {
272
+ logger.error("Failed to delay update, [Error calling '_unsetBackgroundTimestamp()'] " + e.getMessage());
273
+ }
274
+ }
275
+
276
+ private long getBackgroundTimestamp() {
277
+ try {
278
+ return this.prefs.getLong(BACKGROUND_TIMESTAMP_KEY, 0);
279
+ } catch (final Exception e) {
280
+ logger.error("Failed to delay update, [Error calling '_getBackgroundTimestamp()'] " + e.getMessage());
281
+ return 0;
282
+ }
283
+ }
284
+
285
+ public boolean cancelDelay(String source) {
286
+ try {
287
+ this.editor.remove(DELAY_CONDITION_PREFERENCES);
288
+ this.editor.commit();
289
+ logger.info("All delays canceled from " + source);
290
+ return true;
291
+ } catch (final Exception e) {
292
+ logger.error("Failed to cancel update delay " + e.getMessage());
293
+ return false;
294
+ }
295
+ }
296
+ }
@@ -0,0 +1,215 @@
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.security.keystore.KeyGenParameterSpec;
12
+ import android.security.keystore.KeyProperties;
13
+ import java.io.IOException;
14
+ import java.nio.charset.StandardCharsets;
15
+ import java.security.KeyStore;
16
+ import java.security.KeyStoreException;
17
+ import java.security.NoSuchAlgorithmException;
18
+ import java.security.NoSuchProviderException;
19
+ import java.security.UnrecoverableEntryException;
20
+ import java.security.cert.CertificateException;
21
+ import java.util.UUID;
22
+ import javax.crypto.Cipher;
23
+ import javax.crypto.KeyGenerator;
24
+ import javax.crypto.SecretKey;
25
+ import javax.crypto.spec.GCMParameterSpec;
26
+
27
+ /**
28
+ * Helper class to manage device ID persistence across app installations.
29
+ * Uses Android Keystore to persist the device ID across reinstalls.
30
+ *
31
+ * The device ID is a random UUID stored in the Android Keystore, which persists
32
+ * even after app uninstall/reinstall on Android 6.0+ (API 23+).
33
+ */
34
+ public class DeviceIdHelper {
35
+
36
+ private static final String KEYSTORE_ALIAS = "capgo_device_id_key";
37
+ private static final String ANDROID_KEYSTORE = "AndroidKeyStore";
38
+ private static final String LEGACY_PREFS_KEY = "appUUID";
39
+ private static final String DEVICE_ID_PREFS = "capgo_device_id";
40
+ private static final String DEVICE_ID_KEY = "deviceId";
41
+ private static final String IV_KEY = "iv";
42
+ private static final int GCM_TAG_LENGTH = 128;
43
+
44
+ /**
45
+ * Gets or creates a device ID that persists across reinstalls.
46
+ *
47
+ * This method:
48
+ * 1. First checks for an existing ID in Keystore-encrypted storage (persists across reinstalls)
49
+ * 2. Falls back to legacy SharedPreferences (for migration)
50
+ * 3. Generates a new UUID if neither exists
51
+ * 4. Stores the ID in Keystore-encrypted storage for future use
52
+ *
53
+ * @param context Application context
54
+ * @param legacyPrefs Legacy SharedPreferences (for migration)
55
+ * @return Device ID as a lowercase UUID string
56
+ */
57
+ public static String getOrCreateDeviceId(Context context, SharedPreferences legacyPrefs) {
58
+ try {
59
+ // Try to get device ID from Keystore storage
60
+ String deviceId = getDeviceIdFromKeystore(context);
61
+
62
+ if (deviceId != null && !deviceId.isEmpty()) {
63
+ return deviceId.toLowerCase();
64
+ }
65
+
66
+ // Migration: Check legacy SharedPreferences for existing device ID
67
+ deviceId = legacyPrefs.getString(LEGACY_PREFS_KEY, null);
68
+
69
+ if (deviceId == null || deviceId.isEmpty()) {
70
+ // Generate new device ID if none exists
71
+ deviceId = UUID.randomUUID().toString();
72
+ }
73
+
74
+ // Ensure lowercase for consistency
75
+ deviceId = deviceId.toLowerCase();
76
+
77
+ // Save to Keystore storage
78
+ saveDeviceIdToKeystore(context, deviceId);
79
+
80
+ return deviceId;
81
+ } catch (Exception e) {
82
+ // Fallback to legacy method if Keystore fails
83
+ return getFallbackDeviceId(legacyPrefs);
84
+ }
85
+ }
86
+
87
+ /**
88
+ * Retrieves the device ID from Keystore-encrypted storage.
89
+ *
90
+ * @param context Application context
91
+ * @return Device ID string or null if not found
92
+ */
93
+ private static String getDeviceIdFromKeystore(Context context) throws Exception {
94
+ SharedPreferences prefs = context.getSharedPreferences(DEVICE_ID_PREFS, Context.MODE_PRIVATE);
95
+ String encryptedDeviceId = prefs.getString(DEVICE_ID_KEY, null);
96
+ String ivString = prefs.getString(IV_KEY, null);
97
+
98
+ if (encryptedDeviceId == null || ivString == null) {
99
+ return null;
100
+ }
101
+
102
+ // Get the encryption key from Keystore
103
+ SecretKey key = getOrCreateKey();
104
+ if (key == null) {
105
+ return null;
106
+ }
107
+
108
+ // Decrypt the device ID
109
+ byte[] encryptedBytes = android.util.Base64.decode(encryptedDeviceId, android.util.Base64.DEFAULT);
110
+ byte[] iv = android.util.Base64.decode(ivString, android.util.Base64.DEFAULT);
111
+
112
+ Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
113
+ GCMParameterSpec spec = new GCMParameterSpec(GCM_TAG_LENGTH, iv);
114
+ cipher.init(Cipher.DECRYPT_MODE, key, spec);
115
+
116
+ byte[] decryptedBytes = cipher.doFinal(encryptedBytes);
117
+ return new String(decryptedBytes, StandardCharsets.UTF_8);
118
+ }
119
+
120
+ /**
121
+ * Saves the device ID to Keystore-encrypted storage.
122
+ *
123
+ * The device ID is encrypted using AES/GCM with a key stored in Android Keystore.
124
+ * The Keystore key persists across reinstalls on Android 6.0+ (API 23+).
125
+ *
126
+ * @param context Application context
127
+ * @param deviceId The device ID to save
128
+ */
129
+ private static void saveDeviceIdToKeystore(Context context, String deviceId) throws Exception {
130
+ // Get or create encryption key in Keystore
131
+ SecretKey key = getOrCreateKey();
132
+ if (key == null) {
133
+ throw new Exception("Failed to get encryption key");
134
+ }
135
+
136
+ // Encrypt the device ID
137
+ Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
138
+ cipher.init(Cipher.ENCRYPT_MODE, key);
139
+
140
+ byte[] iv = cipher.getIV();
141
+ byte[] encryptedBytes = cipher.doFinal(deviceId.getBytes(StandardCharsets.UTF_8));
142
+
143
+ // Store encrypted device ID and IV in SharedPreferences
144
+ SharedPreferences prefs = context.getSharedPreferences(DEVICE_ID_PREFS, Context.MODE_PRIVATE);
145
+ prefs
146
+ .edit()
147
+ .putString(DEVICE_ID_KEY, android.util.Base64.encodeToString(encryptedBytes, android.util.Base64.DEFAULT))
148
+ .putString(IV_KEY, android.util.Base64.encodeToString(iv, android.util.Base64.DEFAULT))
149
+ .apply();
150
+ }
151
+
152
+ /**
153
+ * Gets or creates the encryption key in Android Keystore.
154
+ *
155
+ * The key is configured to persist across reinstalls and not require user authentication.
156
+ *
157
+ * @return SecretKey from Keystore or null if failed
158
+ */
159
+ private static SecretKey getOrCreateKey() {
160
+ try {
161
+ KeyStore keyStore = KeyStore.getInstance(ANDROID_KEYSTORE);
162
+ keyStore.load(null);
163
+
164
+ // Check if key already exists
165
+ if (keyStore.containsAlias(KEYSTORE_ALIAS)) {
166
+ KeyStore.SecretKeyEntry entry = (KeyStore.SecretKeyEntry) keyStore.getEntry(KEYSTORE_ALIAS, null);
167
+ return entry.getSecretKey();
168
+ }
169
+
170
+ // Create new key
171
+ KeyGenerator keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, ANDROID_KEYSTORE);
172
+
173
+ KeyGenParameterSpec keySpec = new KeyGenParameterSpec.Builder(
174
+ KEYSTORE_ALIAS,
175
+ KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT
176
+ )
177
+ .setBlockModes(KeyProperties.BLOCK_MODE_GCM)
178
+ .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
179
+ .setKeySize(256)
180
+ .setRandomizedEncryptionRequired(true)
181
+ .build();
182
+
183
+ keyGenerator.init(keySpec);
184
+ return keyGenerator.generateKey();
185
+ } catch (
186
+ KeyStoreException
187
+ | CertificateException
188
+ | NoSuchAlgorithmException
189
+ | IOException
190
+ | NoSuchProviderException
191
+ | UnrecoverableEntryException e
192
+ ) {
193
+ return null;
194
+ } catch (Exception e) {
195
+ return null;
196
+ }
197
+ }
198
+
199
+ /**
200
+ * Fallback method using legacy SharedPreferences if Keystore fails or API < 23.
201
+ *
202
+ * @param legacyPrefs Legacy SharedPreferences
203
+ * @return Device ID string
204
+ */
205
+ private static String getFallbackDeviceId(SharedPreferences legacyPrefs) {
206
+ String deviceId = legacyPrefs.getString(LEGACY_PREFS_KEY, null);
207
+
208
+ if (deviceId == null || deviceId.isEmpty()) {
209
+ deviceId = UUID.randomUUID().toString();
210
+ legacyPrefs.edit().putString(LEGACY_PREFS_KEY, deviceId).apply();
211
+ }
212
+
213
+ return deviceId.toLowerCase();
214
+ }
215
+ }