react-native-stallion 2.1.0 → 2.2.0-alpha.2

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 (37) hide show
  1. package/android/src/main/java/com/stallion/events/StallionEventConstants.java +4 -1
  2. package/android/src/main/java/com/stallion/events/StallionEventManager.java +7 -0
  3. package/android/src/main/java/com/stallion/networkmanager/StallionDownloadCacheManager.java +48 -0
  4. package/android/src/main/java/com/stallion/networkmanager/StallionFileDownloader.java +33 -24
  5. package/android/src/main/java/com/stallion/networkmanager/StallionStageManager.java +12 -4
  6. package/android/src/main/java/com/stallion/networkmanager/StallionSyncHandler.java +47 -9
  7. package/android/src/main/java/com/stallion/storage/StallionConfig.java +40 -0
  8. package/android/src/main/java/com/stallion/storage/StallionConfigConstants.java +3 -0
  9. package/android/src/main/java/com/stallion/utils/StallionSignatureVerification.java +129 -0
  10. package/ios/main/Stallion.swift +1 -0
  11. package/ios/main/StallionConfig.h +4 -1
  12. package/ios/main/StallionConfig.m +9 -2
  13. package/ios/main/StallionConfigConstants.h +5 -1
  14. package/ios/main/StallionConfigConstants.m +5 -1
  15. package/ios/main/StallionConstants.swift +1 -0
  16. package/ios/main/StallionEventHandler.h +1 -1
  17. package/ios/main/StallionEventHandler.m +9 -3
  18. package/ios/main/StallionExceptionHandler.h +1 -1
  19. package/ios/main/StallionExceptionHandler.m +7 -2
  20. package/ios/main/StallionFileManager.h +1 -1
  21. package/ios/main/StallionFileManager.m +1 -1
  22. package/ios/main/StallionMeta.h +1 -1
  23. package/ios/main/StallionMeta.m +1 -1
  24. package/ios/main/StallionMetaConstants.h +1 -1
  25. package/ios/main/StallionMetaConstants.m +1 -1
  26. package/ios/main/StallionSignatureVerification.swift +135 -0
  27. package/ios/main/StallionSlotManager.h +1 -1
  28. package/ios/main/StallionSlotManager.m +1 -1
  29. package/ios/main/StallionStageManager.swift +1 -1
  30. package/ios/main/StallionStateManager.h +1 -1
  31. package/ios/main/StallionStateManager.m +1 -1
  32. package/ios/main/StallionSyncHandler.swift +52 -20
  33. package/package.json +1 -1
  34. package/src/main/utils/StallionNativeUtils.js +4 -1
  35. package/src/main/utils/StallionNativeUtils.js.map +1 -1
  36. package/types/main/utils/StallionNativeUtils.d.ts +1 -1
  37. package/types/main/utils/StallionNativeUtils.d.ts.map +1 -1
@@ -3,6 +3,7 @@ package com.stallion.events;
3
3
  public class StallionEventConstants {
4
4
  public enum NativeProdEventTypes {
5
5
  DOWNLOAD_STARTED_PROD,
6
+ DOWNLOAD_RESUME_PROD,
6
7
  DOWNLOAD_ERROR_PROD,
7
8
  DOWNLOAD_COMPLETE_PROD,
8
9
  SYNC_ERROR_PROD,
@@ -12,7 +13,8 @@ public class StallionEventConstants {
12
13
  EXCEPTION_PROD,
13
14
  AUTO_ROLLED_BACK_PROD,
14
15
  CORRUPTED_FILE_ERROR,
15
- FILE_MOUNTING_ERROR
16
+ FILE_MOUNTING_ERROR,
17
+ SIGNATURE_VERIFICATION_FAILED
16
18
  }
17
19
 
18
20
  public enum NativeStageEventTypes {
@@ -20,6 +22,7 @@ public class StallionEventConstants {
20
22
  DOWNLOAD_COMPLETE_STAGE,
21
23
  EXCEPTION_STAGE,
22
24
  DOWNLOAD_STARTED_STAGE,
25
+ DOWNLOAD_RESUME_STAGE,
23
26
  DOWNLOAD_ERROR_STAGE,
24
27
  INSTALLED_STAGE,
25
28
  }
@@ -18,6 +18,7 @@ public class StallionEventManager {
18
18
  public static final String STALLION_NATIVE_EVENT_NAME = "STALLION_NATIVE_EVENT";
19
19
  private static final String EVENTS_KEY = "stored_events";
20
20
  private static final int MAX_BATCH_COUNT_SIZE = 9;
21
+ private static final int MAX_EVENT_STORAGE_LIMIT = 20;
21
22
 
22
23
  private static StallionEventManager instance;
23
24
  private final StallionStateManager stallionStateManager;
@@ -105,6 +106,12 @@ public class StallionEventManager {
105
106
  try {
106
107
  String eventsString = stallionStateManager.getString(EVENTS_KEY, "{}");
107
108
  JSONObject eventsObject = new JSONObject(eventsString);
109
+
110
+ // Flush all if limit reached
111
+ if (eventsObject.length() >= MAX_EVENT_STORAGE_LIMIT) {
112
+ eventsObject = new JSONObject(); // reset
113
+ }
114
+
108
115
  eventsObject.put(uniqueId, eventPayload.toString());
109
116
  stallionStateManager.setString(EVENTS_KEY, eventsObject.toString());
110
117
  } catch (JSONException e) {
@@ -0,0 +1,48 @@
1
+ package com.stallion.networkmanager;
2
+
3
+ import com.stallion.storage.StallionConfig;
4
+ import com.stallion.utils.StallionFileManager;
5
+
6
+ import java.io.File;
7
+ import java.io.FileInputStream;
8
+ import java.io.FileOutputStream;
9
+ import java.util.Objects;
10
+
11
+ public class StallionDownloadCacheManager {
12
+ private static final String metaFilePath = "/download-cache.meta";
13
+
14
+ public static long getDownloadCache(StallionConfig config, String downloadUrl, String downloadPath) {
15
+ String lastDownloadingUrl = config.getLastDownloadingUrl();
16
+ long alreadyDownloaded = readMetaFile(downloadPath);
17
+ if(!Objects.equals(lastDownloadingUrl, downloadUrl) || alreadyDownloaded <= 0) {
18
+ config.setLastDownloadingUrl(downloadUrl);
19
+ StallionFileManager.deleteFileOrFolderSilently(new File(downloadPath));
20
+ return 0;
21
+ } else {
22
+ return alreadyDownloaded;
23
+ }
24
+ }
25
+
26
+ public static void saveDownloadCache(String path, long bytes) {
27
+ try (FileOutputStream fos = new FileOutputStream(path + metaFilePath)) {
28
+ fos.write(Long.toString(bytes).getBytes());
29
+ } catch (Exception ignored) {}
30
+ }
31
+
32
+ private static long readMetaFile(String path) {
33
+ File meta = new File(path + metaFilePath);
34
+ if (!meta.exists()) return 0;
35
+ try (FileInputStream fis = new FileInputStream(meta)) {
36
+ byte[] data = new byte[(int) meta.length()];
37
+ fis.read(data);
38
+ return Long.parseLong(new String(data));
39
+ } catch (Exception e) {
40
+ return 0;
41
+ }
42
+ }
43
+
44
+ public static void deleteDownloadCache(String path) {
45
+ File meta = new File(path + metaFilePath);
46
+ if (meta.exists()) meta.delete();
47
+ }
48
+ }
@@ -8,6 +8,7 @@ import java.io.File;
8
8
  import java.io.FileInputStream;
9
9
  import java.io.FileOutputStream;
10
10
  import java.io.IOException;
11
+ import java.io.RandomAccessFile;
11
12
  import java.net.HttpURLConnection;
12
13
  import java.net.URL;
13
14
  import java.nio.ByteBuffer;
@@ -28,6 +29,7 @@ public class StallionFileDownloader {
28
29
  public static void downloadBundle(
29
30
  String downloadUrl,
30
31
  String downloadDirectory,
32
+ long alreadyDownloaded,
31
33
  StallionDownloadCallback stallionDownloadCallback
32
34
  ) {
33
35
  executor.execute(() -> {
@@ -61,7 +63,7 @@ public class StallionFileDownloader {
61
63
  }
62
64
 
63
65
  // Download file
64
- downloadFile(downloadUrl, downloadedZip, appToken, sdkToken, stallionDownloadCallback);
66
+ downloadFile(downloadUrl, downloadedZip, appToken, sdkToken, stallionDownloadCallback, alreadyDownloaded, downloadDirectory);
65
67
 
66
68
  // Validate and unzip the downloaded file
67
69
  validateAndUnzip(downloadedZip, downloadDirectory, stallionDownloadCallback);
@@ -75,7 +77,7 @@ public class StallionFileDownloader {
75
77
  private static long getFileSize(String downloadUrl, String appToken, String apiKey) throws IOException {
76
78
  HttpURLConnection connection = null;
77
79
  try {
78
- connection = setupConnection(downloadUrl, appToken, apiKey);
80
+ connection = setupConnection(downloadUrl, appToken, apiKey, 0);
79
81
  return connection.getContentLength();
80
82
  } finally {
81
83
  if (connection != null) {
@@ -96,11 +98,10 @@ public class StallionFileDownloader {
96
98
 
97
99
  private static File prepareForDownload(String downloadDirectory) throws IOException {
98
100
  File downloadFolder = new File(downloadDirectory);
99
- if (downloadFolder.exists()) {
100
- StallionFileManager.deleteFileOrFolderSilently(downloadFolder);
101
- }
102
- if (!downloadFolder.mkdirs()) {
103
- throw new IOException("Failed to create download directory: " + downloadDirectory);
101
+ if (!downloadFolder.exists()) {
102
+ if (!downloadFolder.mkdirs()) {
103
+ throw new IOException("Failed to create download directory: " + downloadDirectory);
104
+ }
104
105
  }
105
106
  return new File(downloadFolder, StallionApiConstants.ZIP_FILE_NAME);
106
107
  }
@@ -110,21 +111,21 @@ public class StallionFileDownloader {
110
111
  File destinationFile,
111
112
  String appToken,
112
113
  String sdkToken,
113
- StallionDownloadCallback callback
114
+ StallionDownloadCallback callback,
115
+ long alreadyDownloaded,
116
+ String downloadDirectory
114
117
  ) throws IOException {
115
- HttpURLConnection connection = null;
118
+ HttpURLConnection connection = setupConnection(downloadUrl, appToken, sdkToken, alreadyDownloaded);
116
119
  try (
117
- BufferedInputStream inputStream = new BufferedInputStream(setupConnection(downloadUrl, appToken, sdkToken).getInputStream());
118
- FileOutputStream fout = new FileOutputStream(destinationFile);
119
- BufferedOutputStream bout = new BufferedOutputStream(fout, StallionApiConstants.DOWNLOAD_BUFFER_SIZE)
120
+ BufferedInputStream inputStream = new BufferedInputStream(connection.getInputStream());
121
+ RandomAccessFile raf = new RandomAccessFile(destinationFile, "rw")
120
122
  ) {
121
- connection = setupConnection(downloadUrl, appToken, sdkToken);
122
-
123
+ raf.seek(alreadyDownloaded);
123
124
  byte[] buffer = new byte[StallionApiConstants.DOWNLOAD_BUFFER_SIZE];
124
- long totalBytes = connection.getContentLength();
125
- long receivedBytes = 0;
125
+ long totalBytes = connection.getContentLength() + alreadyDownloaded;
126
+ long receivedBytes = alreadyDownloaded;
126
127
  int bytesRead;
127
- double lastProgress = 0;
128
+ double lastProgress = (double) receivedBytes / totalBytes;
128
129
 
129
130
  // Ensure totalBytes is valid
130
131
  if (totalBytes <= 0) {
@@ -133,9 +134,11 @@ public class StallionFileDownloader {
133
134
  }
134
135
 
135
136
  while ((bytesRead = inputStream.read(buffer)) != -1) {
136
- bout.write(buffer, 0, bytesRead);
137
+ raf.write(buffer, 0, bytesRead);
137
138
  receivedBytes += bytesRead;
138
139
 
140
+ StallionDownloadCacheManager.saveDownloadCache(downloadDirectory, receivedBytes);
141
+
139
142
  double progress = (double) receivedBytes / totalBytes;
140
143
  if (Double.isNaN(progress) || Double.isInfinite(progress)) {
141
144
  callback.onReject(StallionApiConstants.DOWNLOAD_ERROR_PREFIX, "Invalid progress calculation");
@@ -148,8 +151,7 @@ public class StallionFileDownloader {
148
151
  }
149
152
  }
150
153
 
151
- bout.close();
152
- fout.close();
154
+ raf.close();
153
155
  inputStream.close();
154
156
 
155
157
  // Check for incomplete download
@@ -161,18 +163,25 @@ public class StallionFileDownloader {
161
163
  callback.onReject(StallionApiConstants.DOWNLOAD_ERROR_PREFIX, "IOException occurred: ");
162
164
  throw e;
163
165
  } finally {
164
- if (connection != null) {
165
- connection.disconnect();
166
- }
166
+ connection.disconnect();
167
167
  }
168
168
  }
169
169
 
170
170
 
171
- private static HttpURLConnection setupConnection(String downloadUrl, String appToken, String sdkToken) throws IOException {
171
+ private static HttpURLConnection setupConnection(
172
+ String downloadUrl,
173
+ String appToken,
174
+ String sdkToken,
175
+ long offset
176
+ ) throws IOException {
172
177
  URL url = new URL(downloadUrl);
173
178
  HttpURLConnection connection = (HttpURLConnection) url.openConnection();
174
179
  connection.setRequestMethod("GET");
175
180
 
181
+ if (offset > 0) {
182
+ connection.setRequestProperty("Range", "bytes=" + offset + "-");
183
+ }
184
+
176
185
  if(!appToken.isEmpty()) {
177
186
  connection.setRequestProperty(StallionApiConstants.STALLION_APP_TOKEN_KEY, appToken);
178
187
  }
@@ -4,6 +4,7 @@ import com.facebook.react.bridge.Promise;
4
4
  import com.facebook.react.bridge.ReadableMap;
5
5
  import com.stallion.events.StallionEventConstants;
6
6
  import com.stallion.events.StallionEventManager;
7
+ import com.stallion.storage.StallionConfig;
7
8
  import com.stallion.storage.StallionConfigConstants;
8
9
  import com.stallion.storage.StallionMetaConstants;
9
10
  import com.stallion.storage.StallionStateManager;
@@ -21,14 +22,18 @@ public class StallionStageManager {
21
22
  && receivedHash != null
22
23
  && !receivedHash.isEmpty()
23
24
  ) {
24
- String downloadPath = stallionStateManager.getStallionConfig().getFilesDirectory()
25
+ StallionConfig config = stallionStateManager.getStallionConfig();
26
+ String downloadPath = config.getFilesDirectory()
25
27
  + StallionConfigConstants.STAGE_DIRECTORY
26
28
  + StallionConfigConstants.TEMP_FOLDER_SLOT;
29
+ long alreadyDownloaded = StallionDownloadCacheManager.getDownloadCache(config, receivedDownloadUrl, downloadPath);
30
+
31
+ emitDownloadStartedStage(receivedHash, alreadyDownloaded > 0);
27
32
 
28
- emitDownloadStartedStage(receivedHash);
29
33
  StallionFileDownloader.downloadBundle(
30
34
  receivedDownloadUrl,
31
35
  downloadPath,
36
+ alreadyDownloaded,
32
37
  new StallionDownloadCallback() {
33
38
  @Override
34
39
  public void onReject(String prefix, String error) {
@@ -42,6 +47,7 @@ public class StallionStageManager {
42
47
  stallionStateManager.stallionMeta.setStageTempHash(receivedHash);
43
48
  stallionStateManager.syncStallionMeta();
44
49
  emitDownloadSuccessStage(receivedHash);
50
+ StallionDownloadCacheManager.deleteDownloadCache(downloadPath);
45
51
  promise.resolve(successPayload);
46
52
  }
47
53
 
@@ -77,13 +83,15 @@ public class StallionStageManager {
77
83
  );
78
84
  }
79
85
 
80
- private static void emitDownloadStartedStage(String releaseHash) {
86
+ private static void emitDownloadStartedStage(String releaseHash, Boolean isResume) {
81
87
  JSONObject startedPayload = new JSONObject();
82
88
  try {
83
89
  startedPayload.put("releaseHash", releaseHash);
84
90
  } catch (Exception ignored) { }
85
91
  StallionEventManager.getInstance().sendEvent(
86
- StallionEventConstants.NativeStageEventTypes.DOWNLOAD_STARTED_STAGE.toString(),
92
+ isResume ?
93
+ StallionEventConstants.NativeStageEventTypes.DOWNLOAD_RESUME_STAGE.toString()
94
+ : StallionEventConstants.NativeStageEventTypes.DOWNLOAD_STARTED_STAGE.toString(),
87
95
  startedPayload
88
96
  );
89
97
  }
@@ -5,10 +5,14 @@ import com.stallion.storage.StallionConfigConstants;
5
5
  import com.stallion.storage.StallionMetaConstants;
6
6
  import com.stallion.storage.StallionStateManager;
7
7
  import com.stallion.storage.StallionConfig;
8
+ import com.stallion.utils.StallionFileManager;
9
+ import com.stallion.utils.StallionSignatureVerification;
8
10
  import com.stallion.utils.StallionSlotManager;
9
11
  import com.stallion.events.StallionEventConstants.NativeProdEventTypes;
10
12
 
11
13
  import org.json.JSONObject;
14
+
15
+ import java.io.File;
12
16
  import java.util.concurrent.atomic.AtomicBoolean;
13
17
 
14
18
  public class StallionSyncHandler {
@@ -85,11 +89,13 @@ public class StallionSyncHandler {
85
89
 
86
90
  StallionStateManager stateManager = StallionStateManager.getInstance();
87
91
  String lastRolledBackHash = stateManager.stallionMeta.getLastRolledBackHash();
92
+ String lastUnverifiedHash = stateManager.getStallionConfig().getLastUnverifiedHash();
88
93
 
89
94
  if (
90
- !newReleaseHash.isEmpty()
91
- && !newReleaseUrl.isEmpty()
92
- && !newReleaseHash.equals(lastRolledBackHash)
95
+ !newReleaseHash.isEmpty()
96
+ && !newReleaseUrl.isEmpty()
97
+ && !newReleaseHash.equals(lastRolledBackHash)
98
+ && !newReleaseHash.equals(lastUnverifiedHash)
93
99
  ) {
94
100
  if(stateManager.getIsMounted()) {
95
101
  downloadNewRelease(newReleaseHash, newReleaseUrl);
@@ -106,16 +112,22 @@ public class StallionSyncHandler {
106
112
  }
107
113
  try {
108
114
  StallionStateManager stateManager = StallionStateManager.getInstance();
109
- String downloadPath = stateManager.getStallionConfig().getFilesDirectory()
115
+ StallionConfig config = stateManager.getStallionConfig();
116
+ String downloadPath = config.getFilesDirectory()
110
117
  + StallionConfigConstants.PROD_DIRECTORY
111
118
  + StallionConfigConstants.TEMP_FOLDER_SLOT;
112
- String projectId = stateManager.getStallionConfig().getProjectId();
119
+ String projectId = config.getProjectId();
120
+ String downloadUrl = newReleaseUrl + "?projectId=" + projectId;
121
+ String publicSigningKey = config.getPublicSigningKey();
122
+
123
+ long alreadyDownloaded = StallionDownloadCacheManager.getDownloadCache(config, downloadUrl, downloadPath);
113
124
 
114
- emitDownloadStarted(newReleaseHash);
125
+ emitDownloadStarted(newReleaseHash, alreadyDownloaded > 0);
115
126
 
116
127
  StallionFileDownloader.downloadBundle(
117
- newReleaseUrl + "?projectId=" + projectId,
128
+ downloadUrl,
118
129
  downloadPath,
130
+ alreadyDownloaded,
119
131
  new StallionDownloadCallback() {
120
132
  @Override
121
133
  public void onReject(String prefix, String error) {
@@ -126,6 +138,22 @@ public class StallionSyncHandler {
126
138
  @Override
127
139
  public void onSuccess(String successPayload) {
128
140
  isDownloadInProgress.set(false);
141
+ StallionDownloadCacheManager.deleteDownloadCache(downloadPath);
142
+
143
+ if(publicSigningKey != null && !publicSigningKey.isEmpty()) {
144
+ if(
145
+ !StallionSignatureVerification.verifyReleaseSignature(
146
+ downloadPath + StallionConfigConstants.UNZIP_FOLDER_NAME,
147
+ publicSigningKey)
148
+ ) {
149
+ // discard the downloaded release
150
+ config.setLastUnverifiedHash(newReleaseHash);
151
+ emitSignatureVerificationFailed(newReleaseHash);
152
+ StallionFileManager.deleteFileOrFolderSilently(new File(downloadPath));
153
+ return;
154
+ }
155
+ }
156
+
129
157
  stateManager.stallionMeta.setCurrentProdSlot(StallionMetaConstants.SlotStates.NEW_SLOT);
130
158
  stateManager.stallionMeta.setProdTempHash(newReleaseHash);
131
159
  String currentProdNewHash = stateManager.stallionMeta.getProdNewHash();
@@ -182,13 +210,23 @@ public class StallionSyncHandler {
182
210
  );
183
211
  }
184
212
 
185
- private static void emitDownloadStarted(String releaseHash) {
213
+ private static void emitDownloadStarted(String releaseHash, Boolean isResume) {
214
+ JSONObject successPayload = new JSONObject();
215
+ try {
216
+ successPayload.put("releaseHash", releaseHash);
217
+ } catch (Exception ignored) { }
218
+ StallionEventManager.getInstance().sendEvent(
219
+ isResume ? NativeProdEventTypes.DOWNLOAD_RESUME_PROD.toString(): NativeProdEventTypes.DOWNLOAD_STARTED_PROD.toString(),
220
+ successPayload
221
+ );
222
+ }
223
+ private static void emitSignatureVerificationFailed(String releaseHash) {
186
224
  JSONObject successPayload = new JSONObject();
187
225
  try {
188
226
  successPayload.put("releaseHash", releaseHash);
189
227
  } catch (Exception ignored) { }
190
228
  StallionEventManager.getInstance().sendEvent(
191
- NativeProdEventTypes.DOWNLOAD_STARTED_PROD.toString(),
229
+ NativeProdEventTypes.SIGNATURE_VERIFICATION_FAILED.toString(),
192
230
  successPayload
193
231
  );
194
232
  }
@@ -19,6 +19,10 @@ public class StallionConfig {
19
19
  private final String appVersion;
20
20
  private final SharedPreferences sharedPreferences;
21
21
  private final String filesDirectory;
22
+ private String lastDownloadingUrl;
23
+ private String lastUnverifiedHash;
24
+ private final String publicSigningKey;
25
+
22
26
 
23
27
  public StallionConfig(Context context, SharedPreferences sharedPreferences) {
24
28
  this.sharedPreferences = sharedPreferences;
@@ -31,6 +35,7 @@ public class StallionConfig {
31
35
  parentPackageName
32
36
  );
33
37
  this.projectId = stallionProjectIdRes != 0 ?context.getString(stallionProjectIdRes) : "";
38
+
34
39
  int stallionAppTokenRes = res.getIdentifier(
35
40
  StallionConfigConstants.STALLION_APP_TOKEN_IDENTIFIER,
36
41
  "string",
@@ -38,6 +43,13 @@ public class StallionConfig {
38
43
  );
39
44
  this.appToken = stallionAppTokenRes != 0 ? context.getString(stallionAppTokenRes) : "";
40
45
 
46
+ int stallionPublicKeyRes = res.getIdentifier(
47
+ StallionConfigConstants.STALLION_PUBLIC_SIGNING_KEY_IDENTIFIER,
48
+ "string",
49
+ parentPackageName
50
+ );
51
+ this.publicSigningKey = stallionPublicKeyRes != 0 ? context.getString(stallionPublicKeyRes) : "";
52
+
41
53
  // get or generate UID
42
54
  String cachedUniqueId = sharedPreferences.getString(
43
55
  StallionConfigConstants.UNIQUE_ID_IDENTIFIER,
@@ -63,6 +75,30 @@ public class StallionConfig {
63
75
 
64
76
  this.appVersion = fetchAppVersion(context);
65
77
  this.filesDirectory = context.getFilesDir().getAbsolutePath();
78
+ this.lastDownloadingUrl = sharedPreferences.getString(StallionConfigConstants.LAST_DOWNLOADING_URL_IDENTIFIER, "");
79
+ this.lastUnverifiedHash = sharedPreferences.getString(StallionConfigConstants.LAST_UNVERIFIED_HASH, "");
80
+ }
81
+
82
+ public String getLastDownloadingUrl() {
83
+ return this.lastDownloadingUrl;
84
+ }
85
+
86
+ public void setLastDownloadingUrl(String newUrl) {
87
+ this.lastDownloadingUrl = newUrl;
88
+ SharedPreferences.Editor editor = sharedPreferences.edit();
89
+ editor.putString(StallionConfigConstants.LAST_DOWNLOADING_URL_IDENTIFIER, newUrl);
90
+ editor.apply();
91
+ }
92
+
93
+ public String getLastUnverifiedHash() {
94
+ return this.lastUnverifiedHash;
95
+ }
96
+
97
+ public void setLastUnverifiedHash(String newUnverifiedHash) {
98
+ this.lastUnverifiedHash = newUnverifiedHash;
99
+ SharedPreferences.Editor editor = sharedPreferences.edit();
100
+ editor.putString(StallionConfigConstants.LAST_UNVERIFIED_HASH, newUnverifiedHash);
101
+ editor.apply();
66
102
  }
67
103
 
68
104
  private String fetchAppVersion(Context context) {
@@ -105,6 +141,10 @@ public class StallionConfig {
105
141
 
106
142
  public String getFilesDirectory() { return this.filesDirectory; }
107
143
 
144
+ public String getPublicSigningKey() {
145
+ return this.publicSigningKey;
146
+ }
147
+
108
148
  public JSONObject toJSON() {
109
149
  JSONObject configJson = new JSONObject();
110
150
  try {
@@ -7,8 +7,11 @@ public class StallionConfigConstants {
7
7
 
8
8
  public static final String STALLION_PROJECT_ID_IDENTIFIER = "StallionProjectId";
9
9
  public static final String STALLION_APP_TOKEN_IDENTIFIER = "StallionAppToken";
10
+ public static final String STALLION_PUBLIC_SIGNING_KEY_IDENTIFIER = "StallionPublicSigningKey";
10
11
  public static final String UNIQUE_ID_IDENTIFIER = "stallionDeviceId";
11
12
  public static final String API_KEY_IDENTIFIER = "x-sdk-access-token";
13
+ public static final String LAST_DOWNLOADING_URL_IDENTIFIER = "StallionLastDownloadingUrl";
14
+ public static final String LAST_UNVERIFIED_HASH = "LastUnverifiedHash";
12
15
 
13
16
  public static final String PROD_DIRECTORY = "/StallionProd";
14
17
  public static final String STAGE_DIRECTORY = "/StallionStage";
@@ -0,0 +1,129 @@
1
+ package com.stallion.utils;
2
+
3
+ import java.io.ByteArrayInputStream;
4
+ import java.io.File;
5
+ import java.io.FileInputStream;
6
+ import java.io.IOException;
7
+ import java.io.InputStream;
8
+ import java.nio.charset.StandardCharsets;
9
+ import java.security.DigestInputStream;
10
+ import java.security.KeyFactory;
11
+ import java.security.MessageDigest;
12
+ import java.security.PublicKey;
13
+ import java.security.Signature;
14
+ import java.security.spec.X509EncodedKeySpec;
15
+ import java.util.ArrayList;
16
+ import java.util.Collections;
17
+ import java.util.Scanner;
18
+
19
+ import org.json.JSONArray;
20
+ import org.json.JSONObject;
21
+
22
+ public class StallionSignatureVerification {
23
+
24
+ public static final String SIGNATURE_FILE_NAME = ".stallionsigned";
25
+
26
+ public static boolean verifyReleaseSignature(String downloadedBundlePath, String publicKeyPem) {
27
+ try {
28
+ File folderPath = new File(downloadedBundlePath);
29
+ File signatureFile = new File(folderPath, SIGNATURE_FILE_NAME);
30
+ if (!signatureFile.exists()) return false;
31
+
32
+ String jwt = readFileLegacy(signatureFile);
33
+ String[] jwtParts = jwt.split("\\.");
34
+ if (jwtParts.length != 3) return false;
35
+
36
+ String header = jwtParts[0];
37
+ String payload = jwtParts[1];
38
+ String signature = jwtParts[2];
39
+
40
+ byte[] signatureBytes = base64UrlDecode(signature);
41
+ byte[] signedContent = (header + "." + payload).getBytes(StandardCharsets.UTF_8);
42
+
43
+ String cleanedPem = publicKeyPem
44
+ .replace("-----BEGIN PUBLIC KEY-----", "")
45
+ .replace("-----END PUBLIC KEY-----", "")
46
+ .replaceAll("\\s", "");
47
+
48
+ byte[] decodedKey = android.util.Base64.decode(cleanedPem, android.util.Base64.DEFAULT);
49
+ X509EncodedKeySpec keySpec = new X509EncodedKeySpec(decodedKey);
50
+ KeyFactory keyFactory = KeyFactory.getInstance("RSA");
51
+ PublicKey pubKey = keyFactory.generatePublic(keySpec);
52
+
53
+ Signature sig = Signature.getInstance("SHA256withRSA");
54
+ sig.initVerify(pubKey);
55
+ sig.update(signedContent);
56
+ if (!sig.verify(signatureBytes)) return false;
57
+
58
+ String jsonPayload = new String(base64UrlDecode(payload), StandardCharsets.UTF_8);
59
+ JSONObject payloadObj = new JSONObject(jsonPayload);
60
+ String expectedHash = payloadObj.optString("packageHash", "");
61
+
62
+ String actualHash = computeFolderHash(folderPath);
63
+ return expectedHash.equals(actualHash);
64
+
65
+ } catch (Exception e) {
66
+ e.printStackTrace();
67
+ return false;
68
+ }
69
+ }
70
+
71
+ private static String computeFolderHash(File folder) throws Exception {
72
+ ArrayList<String> manifest = new ArrayList<>();
73
+ addFolderContentsToManifest(folder, "", manifest);
74
+ Collections.sort(manifest);
75
+ JSONArray jsonArray = new JSONArray();
76
+ for (String entry : manifest) {
77
+ jsonArray.put(entry);
78
+ }
79
+ String jsonString = jsonArray.toString().replace("\\/", "/");
80
+ return computeHash(new ByteArrayInputStream(jsonString.getBytes(StandardCharsets.UTF_8)));
81
+ }
82
+
83
+ private static void addFolderContentsToManifest(File folder, String pathPrefix, ArrayList<String> manifest) throws Exception {
84
+ File[] files = folder.listFiles();
85
+ for (File file : files) {
86
+ String fileName = file.getName();
87
+ String relativePath = pathPrefix.isEmpty() ? fileName : pathPrefix + "/" + fileName;
88
+ if (isIgnored(relativePath)) continue;
89
+ if (file.isDirectory()) {
90
+ addFolderContentsToManifest(file, relativePath, manifest);
91
+ } else {
92
+ manifest.add(relativePath + ":" + computeHash(new FileInputStream(file)));
93
+ }
94
+ }
95
+ }
96
+
97
+ private static boolean isIgnored(String path) {
98
+ return path.equals(".DS_Store") || path.equals(SIGNATURE_FILE_NAME) || path.startsWith("__MACOSX/");
99
+ }
100
+
101
+ private static String computeHash(InputStream stream) throws Exception {
102
+ MessageDigest digest = MessageDigest.getInstance("SHA-256");
103
+ DigestInputStream dis = new DigestInputStream(stream, digest);
104
+ byte[] buffer = new byte[8192];
105
+ while (dis.read(buffer) != -1);
106
+ dis.close();
107
+ byte[] hash = digest.digest();
108
+ return String.format("%064x", new java.math.BigInteger(1, hash));
109
+ }
110
+
111
+ private static String readFileLegacy(File file) throws IOException {
112
+ Scanner scanner = new Scanner(file, "UTF-8");
113
+ StringBuilder sb = new StringBuilder();
114
+ while (scanner.hasNextLine()) {
115
+ sb.append(scanner.nextLine());
116
+ }
117
+ scanner.close();
118
+ return sb.toString();
119
+ }
120
+
121
+ private static byte[] base64UrlDecode(String input) {
122
+ String converted = input.replace('-', '+').replace('_', '/');
123
+ switch (converted.length() % 4) {
124
+ case 2: converted += "=="; break;
125
+ case 3: converted += "="; break;
126
+ }
127
+ return android.util.Base64.decode(converted, android.util.Base64.NO_WRAP);
128
+ }
129
+ }
@@ -149,6 +149,7 @@ class Stallion: RCTEventEmitter {
149
149
 
150
150
  @objc func restart() {
151
151
  DispatchQueue.main.async {
152
+ self.stallionStateManager.isMounted = false
152
153
  RCTTriggerReloadCommandListeners("Stallion: Restart")
153
154
  }
154
155
  }
@@ -2,7 +2,7 @@
2
2
  // StallionConfig.h
3
3
  // DoubleConversion
4
4
  //
5
- // Created by Jasbir Singh Shergill on 28/01/25.
5
+ // Created by Thor963 on 28/01/25.
6
6
  //
7
7
 
8
8
  #import <Foundation/Foundation.h>
@@ -15,10 +15,13 @@
15
15
  @property (nonatomic, copy) NSString *sdkToken;
16
16
  @property (nonatomic, copy, readonly) NSString *appVersion;
17
17
  @property (nonatomic, copy, readonly) NSString *filesDirectory;
18
+ @property (nonatomic, copy, readonly) NSString *publicSigningKey;
19
+ @property (nonatomic, copy) NSString *lastUnverifiedHash;
18
20
 
19
21
  - (instancetype)initWithDefaults:(NSUserDefaults *)defaults;
20
22
 
21
23
  - (void)updateSdkToken:(NSString *)newSdkToken;
24
+ - (void)updateLastUnverifiedHash:(NSString *)newUnverifiedHash;
22
25
  - (NSDictionary *)toDictionary;
23
26
 
24
27
  @end
@@ -2,7 +2,7 @@
2
2
  // StallionConfig.m
3
3
  // DoubleConversion
4
4
  //
5
- // Created by Jasbir Singh Shergill on 28/01/25.
5
+ // Created by Thor963 on 28/01/25.
6
6
  //
7
7
 
8
8
  #import "StallionConfig.h"
@@ -17,8 +17,9 @@
17
17
  _appToken = [[NSBundle mainBundle] objectForInfoDictionaryKey:STALLION_APP_TOKEN_IDENTIFIER] ?: @"";
18
18
  _sdkToken = [defaults stringForKey:API_KEY_IDENTIFIER] ?: @"";
19
19
  _appVersion = [[NSBundle mainBundle] objectForInfoDictionaryKey:STALLION_APP_VERSION_IDENTIFIER] ?: @"";
20
-
21
20
  _filesDirectory = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject ?: @"";
21
+ _publicSigningKey = [[NSBundle mainBundle] objectForInfoDictionaryKey:STALLION_PUBLIC_SIGNING_KEY_IDENTIFIER] ?: @"";
22
+ _lastUnverifiedHash = [defaults stringForKey:LAST_UNVERIFIED_KEY_IDENTIFIER] ?: @"";
22
23
 
23
24
  NSString *cachedUid = [defaults stringForKey:UNIQUE_ID_IDENTIFIER];
24
25
  if (cachedUid && ![cachedUid isEqualToString:@""]) {
@@ -44,6 +45,12 @@
44
45
  [[NSUserDefaults standardUserDefaults] synchronize];
45
46
  }
46
47
 
48
+ - (void)updateLastUnverifiedHash:(NSString *)newUnverifiedHash {
49
+ _lastUnverifiedHash = newUnverifiedHash ?: @"";
50
+ [[NSUserDefaults standardUserDefaults] setObject:_lastUnverifiedHash forKey:LAST_UNVERIFIED_KEY_IDENTIFIER];
51
+ [[NSUserDefaults standardUserDefaults] synchronize];
52
+ }
53
+
47
54
  - (NSDictionary *)toDictionary {
48
55
  @try {
49
56
  return @{
@@ -2,7 +2,7 @@
2
2
  // StallionConfigConstants.h
3
3
  // DoubleConversion
4
4
  //
5
- // Created by Jasbir Singh Shergill on 28/01/25.
5
+ // Created by Thor963 on 28/01/25.
6
6
  //
7
7
 
8
8
  #import <Foundation/Foundation.h>
@@ -29,4 +29,8 @@ extern NSString *const UNZIP_FOLDER_NAME;
29
29
 
30
30
  extern NSString *const STALLION_META_IDENTIFIER;
31
31
 
32
+ extern NSString *const STALLION_PUBLIC_SIGNING_KEY_IDENTIFIER;
33
+
34
+ extern NSString *const LAST_UNVERIFIED_KEY_IDENTIFIER;
35
+
32
36
  @end
@@ -2,7 +2,7 @@
2
2
  // StallionConfigConstants.m
3
3
  // DoubleConversion
4
4
  //
5
- // Created by Jasbir Singh Shergill on 28/01/25.
5
+ // Created by Thor963 on 28/01/25.
6
6
  //
7
7
 
8
8
  #import "StallionConfigConstants.h"
@@ -29,4 +29,8 @@ NSString *const ANDROID_BUNDLE_FILE_NAME = @"/index.android.bundle";
29
29
  NSString *const DEFAULT_JS_BUNDLE_LOCATION_BASE = @"assets:/";
30
30
  NSString *const UNZIP_FOLDER_NAME = @"/build";
31
31
 
32
+ NSString *const STALLION_PUBLIC_SIGNING_KEY_IDENTIFIER = @"StallionPublicSigningKey";
33
+
34
+ NSString *const LAST_UNVERIFIED_KEY_IDENTIFIER = @"LastUnverifiedHash";
35
+
32
36
  @end
@@ -78,6 +78,7 @@ class StallionConstants {
78
78
  static let INSTALLED_PROD = "INSTALLED_PROD"
79
79
  static let STABILIZED_PROD = "STABILIZED_PROD"
80
80
  static let EXCEPTION_PROD = "EXCEPTION_PROD"
81
+ static let SIGNATURE_VERIFICATION_FAILED = "SIGNATURE_VERIFICATION_FAILED"
81
82
  }
82
83
  public struct NativeEventTypesStage {
83
84
  static let DOWNLOAD_STARTED_STAGE = "DOWNLOAD_STARTED_STAGE"
@@ -2,7 +2,7 @@
2
2
  // StallionEventHandler.h
3
3
  // DoubleConversion
4
4
  //
5
- // Created by Jasbir Singh Shergill on 28/01/25.
5
+ // Created by Thor963 on 28/01/25.
6
6
  //
7
7
 
8
8
  #import <Foundation/Foundation.h>
@@ -2,7 +2,7 @@
2
2
  // StallionEventHandler.m
3
3
  // DoubleConversion
4
4
  //
5
- // Created by Jasbir Singh Shergill on 28/01/25.
5
+ // Created by Thor963 on 28/01/25.
6
6
  //
7
7
 
8
8
  #import "StallionEventHandler.h"
@@ -15,6 +15,7 @@
15
15
  static NSString *const STALLION_NATIVE_EVENT_NAME = @"STALLION_NATIVE_EVENT";
16
16
  static NSString *const EVENTS_KEY = @"stored_events";
17
17
  static NSInteger const MAX_BATCH_COUNT_SIZE = 9;
18
+ static NSInteger const MAX_EVENT_STORAGE_LIMIT = 20;
18
19
 
19
20
  @implementation StallionEventHandler
20
21
 
@@ -30,11 +31,11 @@ static NSInteger const MAX_BATCH_COUNT_SIZE = 9;
30
31
  // Emit event to React Native and store locally
31
32
  - (void)cacheEvent:(NSString *)eventName eventPayload:(NSDictionary *)eventPayload {
32
33
  StallionStateManager *stallionStateManager = [StallionStateManager sharedInstance];
33
-
34
+
34
35
  NSString *projectId = stallionStateManager.stallionConfig.projectId ?: @"";
35
36
  NSString *appVersion = stallionStateManager.stallionConfig.appVersion ?: @"";
36
37
  NSString *uid = stallionStateManager.stallionConfig.uid ?: @"";
37
-
38
+
38
39
  NSMutableDictionary *mutablePayload = [eventPayload mutableCopy];
39
40
  NSString *uniqueId = [[NSUUID UUID] UUIDString];
40
41
  mutablePayload[@"eventId"] = uniqueId;
@@ -62,6 +63,11 @@ static NSInteger const MAX_BATCH_COUNT_SIZE = 9;
62
63
  eventsObject = [NSMutableDictionary dictionary];
63
64
  }
64
65
 
66
+ // Enforce max size
67
+ if (eventsObject.count >= MAX_EVENT_STORAGE_LIMIT) {
68
+ eventsObject = [NSMutableDictionary dictionary];
69
+ }
70
+
65
71
  eventsObject[uniqueId] = eventPayload;
66
72
 
67
73
  NSData *updatedData = [NSJSONSerialization dataWithJSONObject:eventsObject options:0 error:nil];
@@ -2,7 +2,7 @@
2
2
  // StallionExceptionHandler.h
3
3
  // react-native-stallion
4
4
  //
5
- // Created by Jasbir Singh Shergill on 29/01/25.
5
+ // Created by Thor963 on 29/01/25.
6
6
  //
7
7
 
8
8
  #import <Foundation/Foundation.h>
@@ -2,7 +2,7 @@
2
2
  // StallionExceptionHandler.m
3
3
  // react-native-stallion
4
4
  //
5
- // Created by Jasbir Singh Shergill on 29/01/25.
5
+ // Created by Thor963 on 29/01/25.
6
6
  //
7
7
 
8
8
  #import "StallionExceptionHandler.h"
@@ -51,7 +51,10 @@ void handleException(NSException *exception) {
51
51
  }
52
52
 
53
53
  } else if (meta.switchState == SwitchStateStage) {
54
- [StallionSlotManager rollbackStage];
54
+
55
+ if(isAutoRollback) {
56
+ [StallionSlotManager rollbackStage];
57
+ }
55
58
 
56
59
  [[StallionEventHandler sharedInstance] cacheEvent:StallionObjConstants.exception_stage_event
57
60
  eventPayload:@{
@@ -60,6 +63,7 @@ void handleException(NSException *exception) {
60
63
  StallionObjConstants.is_auto_rollback_key: isAutoRollback ? @"true" : @"false"
61
64
  }];
62
65
 
66
+ if(!exceptionAlertDismissed) {
63
67
  UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"Stallion Exception Handler"
64
68
  message:[NSString stringWithFormat:@"%@\n%@",
65
69
  @"A crash occurred in the app. Build was rolled back. Check crash report below. Continue crash to invoke other exception handlers. \n \n",
@@ -82,6 +86,7 @@ void handleException(NSException *exception) {
82
86
  while (exceptionAlertDismissed == FALSE) {
83
87
  [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.1]];
84
88
  }
89
+ }
85
90
  }
86
91
 
87
92
  // Call default exception handler if available
@@ -2,7 +2,7 @@
2
2
  // StallionFileManager.h
3
3
  // react-native-stallion
4
4
  //
5
- // Created by Jasbir Singh Shergill on 29/01/25.
5
+ // Created by Thor963 on 29/01/25.
6
6
  //
7
7
 
8
8
  #import <Foundation/Foundation.h>
@@ -2,7 +2,7 @@
2
2
  // StallionFileManager.m
3
3
  // react-native-stallion
4
4
  //
5
- // Created by Jasbir Singh Shergill on 29/01/25.
5
+ // Created by Thor963 on 29/01/25.
6
6
  //
7
7
  #import "StallionFileManager.h"
8
8
 
@@ -2,7 +2,7 @@
2
2
  // StallionMeta.h
3
3
  // DoubleConversion
4
4
  //
5
- // Created by Jasbir Singh Shergill on 28/01/25.
5
+ // Created by Thor963 on 28/01/25.
6
6
  //
7
7
 
8
8
  #import <Foundation/Foundation.h>
@@ -2,7 +2,7 @@
2
2
  // StallionMeta.m
3
3
  // DoubleConversion
4
4
  //
5
- // Created by Jasbir Singh Shergill on 28/01/25.
5
+ // Created by Thor963 on 28/01/25.
6
6
  //
7
7
 
8
8
  #import "StallionMeta.h"
@@ -2,7 +2,7 @@
2
2
  // StallionMetaConstants.h
3
3
  // DoubleConversion
4
4
  //
5
- // Created by Jasbir Singh Shergill on 28/01/25.
5
+ // Created by Thor963 on 28/01/25.
6
6
  //
7
7
 
8
8
  #import <Foundation/Foundation.h>
@@ -2,7 +2,7 @@
2
2
  // StallionMetaConstants.m
3
3
  // DoubleConversion
4
4
  //
5
- // Created by Jasbir Singh Shergill on 28/01/25.
5
+ // Created by Thor963 on 28/01/25.
6
6
  //
7
7
 
8
8
  #import "StallionMetaConstants.h"
@@ -0,0 +1,135 @@
1
+ import Foundation
2
+ import CommonCrypto
3
+ import Security
4
+
5
+ class StallionSignatureVerification {
6
+
7
+ static let signatureFileName = ".stallionsigned"
8
+
9
+ static func verifyReleaseSignature(downloadedBundlePath: String, publicKeyPem: String) -> Bool {
10
+ do {
11
+ let signatureFilePath = (downloadedBundlePath as NSString).appendingPathComponent(signatureFileName)
12
+ guard FileManager.default.fileExists(atPath: signatureFilePath) else { return false }
13
+
14
+ let jwt = try String(contentsOfFile: signatureFilePath, encoding: .utf8)
15
+ let parts = jwt.components(separatedBy: ".")
16
+ guard parts.count == 3 else { return false }
17
+ let header = parts[0]
18
+ let payload = parts[1]
19
+ let signatureB64 = parts[2]
20
+
21
+ guard let signatureData = base64UrlDecode(signatureB64),
22
+ let signedData = (header + "." + payload).data(using: .utf8) else { return false }
23
+
24
+ guard let pubKey = try? convertPemToPublicKey(pemString: publicKeyPem) else { return false }
25
+
26
+ var error: Unmanaged<CFError>?
27
+ let verified = SecKeyVerifySignature(
28
+ pubKey,
29
+ .rsaSignatureMessagePKCS1v15SHA256,
30
+ signedData as CFData,
31
+ signatureData as CFData,
32
+ &error
33
+ )
34
+
35
+ guard verified else { return false }
36
+
37
+ guard let payloadData = base64UrlDecode(payload),
38
+ let payloadJson = try JSONSerialization.jsonObject(with: payloadData) as? [String: Any],
39
+ let expectedHash = payloadJson["packageHash"] as? String else { return false }
40
+
41
+ let actualHash = try computeCodePushStyleHash(folderPath: downloadedBundlePath)
42
+ return expectedHash == actualHash
43
+
44
+ } catch {
45
+ print("Signature verification error: \(error)")
46
+ return false
47
+ }
48
+ }
49
+
50
+ private static func computeCodePushStyleHash(folderPath: String) throws -> String {
51
+ var manifest: [String] = []
52
+ try addContentsOfFolder(to: &manifest, folderPath: folderPath, pathPrefix: "")
53
+ let sortedManifest = manifest.sorted()
54
+ let jsonData = try JSONSerialization.data(withJSONObject: sortedManifest, options: [])
55
+ var jsonString = String(data: jsonData, encoding: .utf8) ?? ""
56
+ jsonString = jsonString.replacingOccurrences(of: "\\/", with: "/")
57
+ return sha256Hex(jsonString)
58
+ }
59
+
60
+ private static func addContentsOfFolder(to manifest: inout [String], folderPath: String, pathPrefix: String) throws {
61
+ let items = try FileManager.default.contentsOfDirectory(atPath: folderPath)
62
+ for item in items {
63
+ let fullPath = (folderPath as NSString).appendingPathComponent(item)
64
+ let relativePath = pathPrefix.isEmpty ? item : (pathPrefix as NSString).appendingPathComponent(item)
65
+
66
+ if isIgnored(relativePath) { continue }
67
+
68
+ var isDir: ObjCBool = false
69
+ FileManager.default.fileExists(atPath: fullPath, isDirectory: &isDir)
70
+
71
+ if isDir.boolValue {
72
+ try addContentsOfFolder(to: &manifest, folderPath: fullPath, pathPrefix: relativePath)
73
+ } else {
74
+ let data = try Data(contentsOf: URL(fileURLWithPath: fullPath))
75
+ let hash = sha256Hex(data)
76
+ manifest.append("\(relativePath):\(hash)")
77
+ }
78
+ }
79
+ }
80
+
81
+ private static func isIgnored(_ path: String) -> Bool {
82
+ return path.hasPrefix("__MACOSX/") ||
83
+ path == ".DS_Store" ||
84
+ path.hasSuffix("/.DS_Store") ||
85
+ path == signatureFileName ||
86
+ path.hasSuffix("/\(signatureFileName)")
87
+ }
88
+
89
+ private static func sha256Hex(_ input: String) -> String {
90
+ guard let data = input.data(using: .utf8) else { return "" }
91
+ return sha256Hex(data)
92
+ }
93
+
94
+ private static func sha256Hex(_ data: Data) -> String {
95
+ var digest = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH))
96
+ data.withUnsafeBytes {
97
+ _ = CC_SHA256($0.baseAddress, CC_LONG(data.count), &digest)
98
+ }
99
+ return digest.map { String(format: "%02x", $0) }.joined()
100
+ }
101
+
102
+ private static func base64UrlDecode(_ input: String) -> Data? {
103
+ var base64 = input.replacingOccurrences(of: "-", with: "+")
104
+ .replacingOccurrences(of: "_", with: "/")
105
+ let padding = base64.count % 4
106
+ if padding > 0 {
107
+ base64 += String(repeating: "=", count: 4 - padding)
108
+ }
109
+ return Data(base64Encoded: base64)
110
+ }
111
+
112
+ private static func convertPemToPublicKey(pemString: String) throws -> SecKey {
113
+ let cleaned = pemString
114
+ .replacingOccurrences(of: "-----BEGIN PUBLIC KEY-----\n", with: "")
115
+ .replacingOccurrences(of: "-----END PUBLIC KEY-----", with: "")
116
+ .replacingOccurrences(of: "\n", with: "")
117
+ .replacingOccurrences(of: "\r", with: "")
118
+
119
+ guard let keyData = Data(base64Encoded: cleaned, options: .ignoreUnknownCharacters) else {
120
+ throw NSError(domain: "Stallion", code: -1, userInfo: [NSLocalizedDescriptionKey: "Invalid base64 key"])
121
+ }
122
+
123
+ let keyDict: [String: Any] = [
124
+ kSecAttrKeyType as String: kSecAttrKeyTypeRSA,
125
+ kSecAttrKeyClass as String: kSecAttrKeyClassPublic,
126
+ kSecAttrKeySizeInBits as String: 2048
127
+ ]
128
+
129
+ var error: Unmanaged<CFError>?
130
+ guard let secKey = SecKeyCreateWithData(keyData as CFData, keyDict as CFDictionary, &error) else {
131
+ throw error!.takeRetainedValue() as Error
132
+ }
133
+ return secKey
134
+ }
135
+ }
@@ -2,7 +2,7 @@
2
2
  // StallionSlotManager.h
3
3
  // react-native-stallion
4
4
  //
5
- // Created by Jasbir Singh Shergill on 29/01/25.
5
+ // Created by Thor963 on 29/01/25.
6
6
  //
7
7
 
8
8
  #import <Foundation/Foundation.h>
@@ -2,7 +2,7 @@
2
2
  // StallionSlotManager.m
3
3
  // react-native-stallion
4
4
  //
5
- // Created by Jasbir Singh Shergill on 29/01/25.
5
+ // Created by Thor963 on 29/01/25.
6
6
  //
7
7
 
8
8
  #import "StallionSlotManager.h"
@@ -2,7 +2,7 @@
2
2
  // StallionStageManager.swift
3
3
  // react-native-stallion
4
4
  //
5
- // Created by Jasbir Singh Shergill on 29/01/25.
5
+ // Created by Thor963 on 29/01/25.
6
6
  //
7
7
 
8
8
  import Foundation
@@ -2,7 +2,7 @@
2
2
  // StallionStateManager.h
3
3
  // DoubleConversion
4
4
  //
5
- // Created by Jasbir Singh Shergill on 28/01/25.
5
+ // Created by Thor963 on 28/01/25.
6
6
  //
7
7
 
8
8
  #import <Foundation/Foundation.h>
@@ -2,7 +2,7 @@
2
2
  // StallionStateManager.m
3
3
  // DoubleConversion
4
4
  //
5
- // Created by Jasbir Singh Shergill on 28/01/25.
5
+ // Created by Thor963 on 28/01/25.
6
6
  //
7
7
 
8
8
  #import "StallionStateManager.h"
@@ -2,7 +2,7 @@
2
2
  // StallionSyncHandler.swift
3
3
  // react-native-stallion
4
4
  //
5
- // Created by Jasbir Singh Shergill on 29/01/25.
5
+ // Created by Thor963 on 29/01/25.
6
6
  //
7
7
 
8
8
  import Foundation
@@ -152,8 +152,9 @@ class StallionSyncHandler {
152
152
 
153
153
  let stateManager = StallionStateManager.sharedInstance()
154
154
  let lastRolledBackHash = stateManager?.stallionMeta?.lastRolledBackHash ?? ""
155
+ let lastUnverifiedHash = stateManager?.stallionConfig?.lastUnverifiedHash ?? ""
155
156
 
156
- if newReleaseHash != lastRolledBackHash {
157
+ if newReleaseHash != lastRolledBackHash && newReleaseHash != lastUnverifiedHash {
157
158
  if stateManager?.isMounted == true {
158
159
  downloadNewRelease(newReleaseHash: newReleaseHash, newReleaseUrl: newReleaseUrl)
159
160
  } else {
@@ -179,29 +180,52 @@ class StallionSyncHandler {
179
180
  let downloadPath = config.filesDirectory + "/" + StallionConstants.PROD_DIRECTORY + "/" + StallionConstants.TEMP_FOLDER_SLOT
180
181
  let projectId = config.projectId ?? ""
181
182
 
183
+ let publicSigningKey = config.publicSigningKey ?? ""
184
+
182
185
  guard let fromUrl = URL(string: newReleaseUrl + "?projectId=" + projectId) else { return }
183
186
 
184
187
  emitDownloadStarted(releaseHash: newReleaseHash)
185
188
 
186
- StallionFileDownloader().downloadBundle(url: fromUrl, downloadDirectory: downloadPath, onProgress: { progress in
187
- // Handle progress updates if necessary
188
- }, resolve: { _ in
189
- completeDownload()
190
- stateManager.stallionMeta?.currentProdSlot = SlotStates.newSlot
191
- stateManager.stallionMeta?.prodTempHash = newReleaseHash
192
- if let currentProdNewHash = stateManager.stallionMeta?.prodNewHash,
193
- !currentProdNewHash.isEmpty {
194
- StallionSlotManager.stabilizeProd()
195
- }
196
- stateManager.syncStallionMeta()
197
- emitDownloadSuccess(releaseHash: newReleaseHash)
198
- }, reject: { code, prefix, error in
199
- completeDownload()
200
- emitDownloadError(
201
- releaseHash: newReleaseHash,
202
- error: "\(String(describing: prefix))\(String(describing: error))"
189
+ StallionFileDownloader().downloadBundle(
190
+ url: fromUrl,
191
+ downloadDirectory: downloadPath,
192
+ onProgress: { progress in
193
+ // Handle progress updates if necessary
194
+ },
195
+ resolve: { _ in
196
+ completeDownload()
197
+
198
+ if(publicSigningKey != nil && !publicSigningKey.isEmpty) {
199
+ if(
200
+ !StallionSignatureVerification.verifyReleaseSignature(
201
+ downloadedBundlePath: downloadPath + "/" + StallionConstants.FilePaths.ZipFolderName,
202
+ publicKeyPem: publicSigningKey
203
+ )
204
+ ) {
205
+ // discard downloaded release
206
+ config.updateLastUnverifiedHash(newReleaseHash)
207
+ emitSignatureVerificationFailed(releaseHash: newReleaseHash)
208
+ StallionFileManager.deleteFileOrFolderSilently(downloadPath)
209
+ return;
210
+ }
211
+ }
212
+ stateManager.stallionMeta?.currentProdSlot = SlotStates.newSlot
213
+ stateManager.stallionMeta?.prodTempHash = newReleaseHash
214
+ if let currentProdNewHash = stateManager.stallionMeta?.prodNewHash,
215
+ !currentProdNewHash.isEmpty {
216
+ StallionSlotManager.stabilizeProd()
217
+ }
218
+ stateManager.syncStallionMeta()
219
+ emitDownloadSuccess(releaseHash: newReleaseHash)
220
+ },
221
+ reject: { code, prefix, error in
222
+ completeDownload()
223
+ emitDownloadError(
224
+ releaseHash: newReleaseHash,
225
+ error: "\(String(describing: prefix))\(String(describing: error))"
226
+ )
227
+ }
203
228
  )
204
- })
205
229
  }
206
230
 
207
231
  private static func completeDownload() {
@@ -246,5 +270,13 @@ class StallionSyncHandler {
246
270
  shouldCache: true
247
271
  )
248
272
  }
273
+
274
+ private static func emitSignatureVerificationFailed(releaseHash: String) {
275
+ let verificationFailurePayload: NSDictionary = ["releaseHash": releaseHash]
276
+ Stallion.sendEventToRn(eventName: StallionConstants.NativeEventTypesProd.SIGNATURE_VERIFICATION_FAILED,
277
+ eventBody: verificationFailurePayload,
278
+ shouldCache: true
279
+ )
280
+ }
249
281
  }
250
282
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-stallion",
3
- "version": "2.1.0",
3
+ "version": "2.2.0-alpha.2",
4
4
  "description": "Offical React Native SDK for Stallion",
5
5
  "main": "index",
6
6
  "types": "types/index.d.ts",
@@ -32,5 +32,8 @@ export const onLaunchNative = StallionNativeModule === null || StallionNativeMod
32
32
  export const sync = StallionNativeModule === null || StallionNativeModule === void 0 ? void 0 : StallionNativeModule.sync;
33
33
  export const popEventsNative = StallionNativeModule === null || StallionNativeModule === void 0 ? void 0 : StallionNativeModule.popEvents;
34
34
  export const acknowledgeEventsNative = StallionNativeModule === null || StallionNativeModule === void 0 ? void 0 : StallionNativeModule.acknowledgeEvents;
35
- export const restart = StallionNativeModule === null || StallionNativeModule === void 0 ? void 0 : StallionNativeModule.restart;
35
+ export const restart = () => {
36
+ var _StallionNativeModule;
37
+ StallionNativeModule === null || StallionNativeModule === void 0 ? void 0 : (_StallionNativeModule = StallionNativeModule.restart) === null || _StallionNativeModule === void 0 ? void 0 : _StallionNativeModule.call(StallionNativeModule);
38
+ };
36
39
  //# sourceMappingURL=StallionNativeUtils.js.map
@@ -1 +1 @@
1
- {"version":3,"names":["StallionNativeModule","setSdkTokenNative","updateSdkToken","getStallionMetaNative","Promise","resolve","reject","getStallionMeta","then","metaString","JSON","parse","_","catch","getStallionConfigNative","getStallionConfig","configString","toggleStallionSwitchNative","toggleStallionSwitch","downloadBundleNative","downloadStageBundle","onLaunchNative","onLaunch","sync","popEventsNative","popEvents","acknowledgeEventsNative","acknowledgeEvents","restart"],"sourceRoot":"../../../../src","sources":["main/utils/StallionNativeUtils.ts"],"mappings":"AAAA,OAAOA,oBAAoB,MAAM,4BAA4B;AAW7D,OAAO,MAAMC,iBAAqC,GAChDD,oBAAoB,aAApBA,oBAAoB,uBAApBA,oBAAoB,CAAEE,cAAc;AAEtC,OAAO,MAAMC,qBAA6C,GAAGA,CAAA,KAAM;EACjE,OAAO,IAAIC,OAAO,CAAC,CAACC,OAAO,EAAEC,MAAM,KAAK;IACtCN,oBAAoB,aAApBA,oBAAoB,uBAApBA,oBAAoB,CAAEO,eAAe,CAAC,CAAC,CACpCC,IAAI,CAAEC,UAAkB,IAAK;MAC5B,IAAI;QACFJ,OAAO,CAACK,IAAI,CAACC,KAAK,CAACF,UAAU,CAAC,CAAC;MACjC,CAAC,CAAC,OAAOG,CAAC,EAAE;QACVN,MAAM,CAAC,qBAAqB,CAAC;MAC/B;IACF,CAAC,CAAC,CACDO,KAAK,CAAC,MAAM;MACXP,MAAM,CAAC,6BAA6B,CAAC;IACvC,CAAC,CAAC;EACN,CAAC,CAAC;AACJ,CAAC;AAED,OAAO,MAAMQ,uBAAiD,GAAGA,CAAA,KAAM;EACrE,OAAO,IAAIV,OAAO,CAAC,CAACC,OAAO,EAAEC,MAAM,KAAK;IACtCN,oBAAoB,aAApBA,oBAAoB,uBAApBA,oBAAoB,CAAEe,iBAAiB,CAAC,CAAC,CACtCP,IAAI,CAAEQ,YAAoB,IAAK;MAC9B,IAAI;QACFX,OAAO,CAACK,IAAI,CAACC,KAAK,CAACK,YAAY,CAAC,CAAC;MACnC,CAAC,CAAC,OAAOJ,CAAC,EAAE;QACVN,MAAM,CAAC,uBAAuB,CAAC;MACjC;IACF,CAAC,CAAC,CACDO,KAAK,CAAC,MAAM;MACXP,MAAM,CAAC,+BAA+B,CAAC;IACzC,CAAC,CAAC;EACN,CAAC,CAAC;AACJ,CAAC;AAED,OAAO,MAAMW,0BAAuD,GAClEjB,oBAAoB,aAApBA,oBAAoB,uBAApBA,oBAAoB,CAAEkB,oBAAoB;AAE5C,OAAO,MAAMC,oBAA2C,GACtDnB,oBAAoB,aAApBA,oBAAoB,uBAApBA,oBAAoB,CAAEoB,mBAAmB;AAE3C,OAAO,MAAMC,cAAqC,GAChDrB,oBAAoB,aAApBA,oBAAoB,uBAApBA,oBAAoB,CAAEsB,QAAQ;AAEhC,OAAO,MAAMC,IAAgB,GAAGvB,oBAAoB,aAApBA,oBAAoB,uBAApBA,oBAAoB,CAAEuB,IAAI;AAE1D,OAAO,MAAMC,eAAsC,GACjDxB,oBAAoB,aAApBA,oBAAoB,uBAApBA,oBAAoB,CAAEyB,SAAS;AAEjC,OAAO,MAAMC,uBAA8D,GACzE1B,oBAAoB,aAApBA,oBAAoB,uBAApBA,oBAAoB,CAAE2B,iBAAiB;AAEzC,OAAO,MAAMC,OAAmB,GAAG5B,oBAAoB,aAApBA,oBAAoB,uBAApBA,oBAAoB,CAAE4B,OAAO"}
1
+ {"version":3,"names":["StallionNativeModule","setSdkTokenNative","updateSdkToken","getStallionMetaNative","Promise","resolve","reject","getStallionMeta","then","metaString","JSON","parse","_","catch","getStallionConfigNative","getStallionConfig","configString","toggleStallionSwitchNative","toggleStallionSwitch","downloadBundleNative","downloadStageBundle","onLaunchNative","onLaunch","sync","popEventsNative","popEvents","acknowledgeEventsNative","acknowledgeEvents","restart","_StallionNativeModule","call"],"sourceRoot":"../../../../src","sources":["main/utils/StallionNativeUtils.ts"],"mappings":"AAAA,OAAOA,oBAAoB,MAAM,4BAA4B;AAW7D,OAAO,MAAMC,iBAAqC,GAChDD,oBAAoB,aAApBA,oBAAoB,uBAApBA,oBAAoB,CAAEE,cAAc;AAEtC,OAAO,MAAMC,qBAA6C,GAAGA,CAAA,KAAM;EACjE,OAAO,IAAIC,OAAO,CAAC,CAACC,OAAO,EAAEC,MAAM,KAAK;IACtCN,oBAAoB,aAApBA,oBAAoB,uBAApBA,oBAAoB,CAAEO,eAAe,CAAC,CAAC,CACpCC,IAAI,CAAEC,UAAkB,IAAK;MAC5B,IAAI;QACFJ,OAAO,CAACK,IAAI,CAACC,KAAK,CAACF,UAAU,CAAC,CAAC;MACjC,CAAC,CAAC,OAAOG,CAAC,EAAE;QACVN,MAAM,CAAC,qBAAqB,CAAC;MAC/B;IACF,CAAC,CAAC,CACDO,KAAK,CAAC,MAAM;MACXP,MAAM,CAAC,6BAA6B,CAAC;IACvC,CAAC,CAAC;EACN,CAAC,CAAC;AACJ,CAAC;AAED,OAAO,MAAMQ,uBAAiD,GAAGA,CAAA,KAAM;EACrE,OAAO,IAAIV,OAAO,CAAC,CAACC,OAAO,EAAEC,MAAM,KAAK;IACtCN,oBAAoB,aAApBA,oBAAoB,uBAApBA,oBAAoB,CAAEe,iBAAiB,CAAC,CAAC,CACtCP,IAAI,CAAEQ,YAAoB,IAAK;MAC9B,IAAI;QACFX,OAAO,CAACK,IAAI,CAACC,KAAK,CAACK,YAAY,CAAC,CAAC;MACnC,CAAC,CAAC,OAAOJ,CAAC,EAAE;QACVN,MAAM,CAAC,uBAAuB,CAAC;MACjC;IACF,CAAC,CAAC,CACDO,KAAK,CAAC,MAAM;MACXP,MAAM,CAAC,+BAA+B,CAAC;IACzC,CAAC,CAAC;EACN,CAAC,CAAC;AACJ,CAAC;AAED,OAAO,MAAMW,0BAAuD,GAClEjB,oBAAoB,aAApBA,oBAAoB,uBAApBA,oBAAoB,CAAEkB,oBAAoB;AAE5C,OAAO,MAAMC,oBAA2C,GACtDnB,oBAAoB,aAApBA,oBAAoB,uBAApBA,oBAAoB,CAAEoB,mBAAmB;AAE3C,OAAO,MAAMC,cAAqC,GAChDrB,oBAAoB,aAApBA,oBAAoB,uBAApBA,oBAAoB,CAAEsB,QAAQ;AAEhC,OAAO,MAAMC,IAAgB,GAAGvB,oBAAoB,aAApBA,oBAAoB,uBAApBA,oBAAoB,CAAEuB,IAAI;AAE1D,OAAO,MAAMC,eAAsC,GACjDxB,oBAAoB,aAApBA,oBAAoB,uBAApBA,oBAAoB,CAAEyB,SAAS;AAEjC,OAAO,MAAMC,uBAA8D,GACzE1B,oBAAoB,aAApBA,oBAAoB,uBAApBA,oBAAoB,CAAE2B,iBAAiB;AAEzC,OAAO,MAAMC,OAAO,GAAGA,CAAA,KAAM;EAAA,IAAAC,qBAAA;EAC3B7B,oBAAoB,aAApBA,oBAAoB,wBAAA6B,qBAAA,GAApB7B,oBAAoB,CAAE4B,OAAO,cAAAC,qBAAA,uBAA7BA,qBAAA,CAAAC,IAAA,CAAA9B,oBAAgC,CAAC;AACnC,CAAC"}
@@ -8,5 +8,5 @@ export declare const onLaunchNative: TOnLaunchBundleNative;
8
8
  export declare const sync: () => void;
9
9
  export declare const popEventsNative: () => Promise<string>;
10
10
  export declare const acknowledgeEventsNative: (eventIds: string) => Promise<string>;
11
- export declare const restart: () => null;
11
+ export declare const restart: () => void;
12
12
  //# sourceMappingURL=StallionNativeUtils.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"StallionNativeUtils.d.ts","sourceRoot":"","sources":["../../../../src/main/utils/StallionNativeUtils.ts"],"names":[],"mappings":"AAEA,OAAO,EACL,qBAAqB,EACrB,kBAAkB,EAClB,sBAAsB,EACtB,2BAA2B,EAC3B,qBAAqB,EACrB,wBAAwB,EACzB,MAAM,uBAAuB,CAAC;AAE/B,eAAO,MAAM,iBAAiB,EAAE,kBACM,CAAC;AAEvC,eAAO,MAAM,qBAAqB,EAAE,sBAcnC,CAAC;AAEF,eAAO,MAAM,uBAAuB,EAAE,wBAcrC,CAAC;AAEF,eAAO,MAAM,0BAA0B,EAAE,2BACG,CAAC;AAE7C,eAAO,MAAM,oBAAoB,EAAE,qBACQ,CAAC;AAE5C,eAAO,MAAM,cAAc,EAAE,qBACG,CAAC;AAEjC,eAAO,MAAM,IAAI,EAAE,MAAM,IAAiC,CAAC;AAE3D,eAAO,MAAM,eAAe,EAAE,MAAM,OAAO,CAAC,MAAM,CACjB,CAAC;AAElC,eAAO,MAAM,uBAAuB,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CACjC,CAAC;AAE1C,eAAO,MAAM,OAAO,EAAE,MAAM,IAAoC,CAAC"}
1
+ {"version":3,"file":"StallionNativeUtils.d.ts","sourceRoot":"","sources":["../../../../src/main/utils/StallionNativeUtils.ts"],"names":[],"mappings":"AAEA,OAAO,EACL,qBAAqB,EACrB,kBAAkB,EAClB,sBAAsB,EACtB,2BAA2B,EAC3B,qBAAqB,EACrB,wBAAwB,EACzB,MAAM,uBAAuB,CAAC;AAE/B,eAAO,MAAM,iBAAiB,EAAE,kBACM,CAAC;AAEvC,eAAO,MAAM,qBAAqB,EAAE,sBAcnC,CAAC;AAEF,eAAO,MAAM,uBAAuB,EAAE,wBAcrC,CAAC;AAEF,eAAO,MAAM,0BAA0B,EAAE,2BACG,CAAC;AAE7C,eAAO,MAAM,oBAAoB,EAAE,qBACQ,CAAC;AAE5C,eAAO,MAAM,cAAc,EAAE,qBACG,CAAC;AAEjC,eAAO,MAAM,IAAI,EAAE,MAAM,IAAiC,CAAC;AAE3D,eAAO,MAAM,eAAe,EAAE,MAAM,OAAO,CAAC,MAAM,CACjB,CAAC;AAElC,eAAO,MAAM,uBAAuB,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CACjC,CAAC;AAE1C,eAAO,MAAM,OAAO,YAEnB,CAAC"}