react-native-bspatch 1.1.4 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -32
- package/android/src/main/java/vn/reactnativebspatch/BsPatchModule.kt +15 -102
- package/android/src/main/java/vn/reactnativebspatch/OtaBundleResolver.kt +21 -1
- package/cpp/bsdiff/bspatch.c +1 -0
- package/ios/OtaBundleResolver.swift +16 -1
- package/ios/RNBsPatch.mm +61 -154
- package/lib/commonjs/NativeBsPatch.js.map +1 -1
- package/lib/commonjs/index.js +4 -4
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/ota/OtaProvider.js +26 -22
- package/lib/commonjs/ota/OtaProvider.js.map +1 -1
- package/lib/commonjs/ota/otaAssetResolver.js +8 -2
- package/lib/commonjs/ota/otaAssetResolver.js.map +1 -1
- package/lib/commonjs/ota/otaCore.js +89 -141
- package/lib/commonjs/ota/otaCore.js.map +1 -1
- package/lib/commonjs/ota/otaLogger.js +0 -2
- package/lib/commonjs/ota/otaLogger.js.map +1 -1
- package/lib/commonjs/ota/otaMetadata.js +18 -7
- package/lib/commonjs/ota/otaMetadata.js.map +1 -1
- package/lib/commonjs/ota/storage/GitHubRawStorage.js +3 -1
- package/lib/commonjs/ota/storage/GitHubRawStorage.js.map +1 -1
- package/lib/commonjs/ota/types.js +1 -10
- package/lib/commonjs/ota/types.js.map +1 -1
- package/lib/module/NativeBsPatch.js.map +1 -1
- package/lib/module/index.js +1 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/ota/OtaProvider.js +26 -22
- package/lib/module/ota/OtaProvider.js.map +1 -1
- package/lib/module/ota/otaAssetResolver.js +8 -2
- package/lib/module/ota/otaAssetResolver.js.map +1 -1
- package/lib/module/ota/otaCore.js +88 -139
- package/lib/module/ota/otaCore.js.map +1 -1
- package/lib/module/ota/otaLogger.js +0 -2
- package/lib/module/ota/otaLogger.js.map +1 -1
- package/lib/module/ota/otaMetadata.js +18 -7
- package/lib/module/ota/otaMetadata.js.map +1 -1
- package/lib/module/ota/storage/GitHubRawStorage.js +3 -1
- package/lib/module/ota/storage/GitHubRawStorage.js.map +1 -1
- package/lib/module/ota/types.js +0 -9
- package/lib/module/ota/types.js.map +1 -1
- package/lib/typescript/NativeBsPatch.d.ts +3 -15
- package/lib/typescript/NativeBsPatch.d.ts.map +1 -1
- package/lib/typescript/index.d.ts +1 -1
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/ota/OtaProvider.d.ts.map +1 -1
- package/lib/typescript/ota/otaAssetResolver.d.ts.map +1 -1
- package/lib/typescript/ota/otaCore.d.ts +5 -42
- package/lib/typescript/ota/otaCore.d.ts.map +1 -1
- package/lib/typescript/ota/otaLogger.d.ts +1 -3
- package/lib/typescript/ota/otaLogger.d.ts.map +1 -1
- package/lib/typescript/ota/otaMetadata.d.ts +2 -2
- package/lib/typescript/ota/otaMetadata.d.ts.map +1 -1
- package/lib/typescript/ota/types.d.ts +18 -54
- package/lib/typescript/ota/types.d.ts.map +1 -1
- package/package.json +3 -6
- package/react-native-bspatch.podspec +0 -1
- package/src/NativeBsPatch.ts +3 -16
- package/src/index.ts +1 -1
- package/src/ota/OtaProvider.tsx +24 -23
- package/src/ota/otaAssetResolver.ts +5 -2
- package/src/ota/otaCore.ts +118 -176
- package/src/ota/otaLogger.ts +0 -2
- package/src/ota/otaMetadata.ts +18 -8
- package/src/ota/storage/GitHubRawStorage.ts +1 -1
- package/src/ota/types.ts +18 -57
- package/tools/build_ota.sh +117 -183
- package/tools/setup_project.js +33 -14
- package/tools/update_registry.js +24 -28
- package/tools/{upload_ota_custom.sh → upload_ota.sh} +33 -114
- package/lib/commonjs/crypto-js.d.js +0 -2
- package/lib/commonjs/crypto-js.d.js.map +0 -1
- package/lib/commonjs/ota/storage/CustomStorage.js +0 -160
- package/lib/commonjs/ota/storage/CustomStorage.js.map +0 -1
- package/lib/commonjs/ota/storage/GitHubReleaseStorage.js +0 -99
- package/lib/commonjs/ota/storage/GitHubReleaseStorage.js.map +0 -1
- package/lib/module/crypto-js.d.js +0 -2
- package/lib/module/crypto-js.d.js.map +0 -1
- package/lib/module/ota/storage/CustomStorage.js +0 -154
- package/lib/module/ota/storage/CustomStorage.js.map +0 -1
- package/lib/module/ota/storage/GitHubReleaseStorage.js +0 -93
- package/lib/module/ota/storage/GitHubReleaseStorage.js.map +0 -1
- package/lib/typescript/ota/storage/CustomStorage.d.ts +0 -12
- package/lib/typescript/ota/storage/CustomStorage.d.ts.map +0 -1
- package/lib/typescript/ota/storage/GitHubReleaseStorage.d.ts +0 -20
- package/lib/typescript/ota/storage/GitHubReleaseStorage.d.ts.map +0 -1
- package/src/crypto-js.d.ts +0 -1
- package/src/ota/storage/CustomStorage.ts +0 -166
- package/src/ota/storage/GitHubReleaseStorage.ts +0 -103
- package/tools/bsdiff +0 -0
package/README.md
CHANGED
|
@@ -17,7 +17,6 @@ Premium OTA update solution for React Native using **TurboModules** + **bsdiff b
|
|
|
17
17
|
- **Crash detection + auto-rollback** — native-side protection runs **before** JS boots; if the updated bundle crashes, the app automatically rolls back to the previous version
|
|
18
18
|
- **Integrity verification** — SHA-256 hash check on both source bundle and result bundle
|
|
19
19
|
- **Disk Garbage Collection** — automatically removes obsolete bundle versions and dangling temporary files
|
|
20
|
-
- **Gzip decompression** — native 64KB-buffered gzip decompression on both Android and iOS
|
|
21
20
|
- **Content-addressed asset store** — images/assets downloaded per-version and redirected via `setCustomSourceTransformer`
|
|
22
21
|
- **Structured telemetry** — all OTA events forwarded to your analytics via `onEvent` callback
|
|
23
22
|
- **New Architecture ready** — built with TurboModules (no legacy Bridge)
|
|
@@ -71,12 +70,9 @@ const config = {
|
|
|
71
70
|
updateMode: OtaUpdateMode.BACKGROUND_DOWNLOAD, // Silent download without interrupting user
|
|
72
71
|
allowDebugUpdates: false, // default: OTA is blocked in __DEV__
|
|
73
72
|
customServer: {
|
|
74
|
-
baseUrl: 'https://
|
|
75
|
-
username: 'your-user',
|
|
76
|
-
password: 'your-pass',
|
|
73
|
+
baseUrl: 'https://raw.githubusercontent.com/<owner>/<storage-repo>/main',
|
|
77
74
|
channel: 'production',
|
|
78
|
-
baseFolder: '
|
|
79
|
-
useMinio: false,
|
|
75
|
+
baseFolder: 'ota',
|
|
80
76
|
},
|
|
81
77
|
};
|
|
82
78
|
|
|
@@ -176,22 +172,20 @@ type OtaConfig = {
|
|
|
176
172
|
onEvent?: (entry: OtaLogEntry) => void;
|
|
177
173
|
|
|
178
174
|
/**
|
|
179
|
-
*
|
|
175
|
+
* SPKI public key (PEM or base64, RSA or EC P-256) for signature verification.
|
|
176
|
+
* When set, unsigned or wrongly signed patches/bundles are rejected.
|
|
180
177
|
* Recommended for production to prevent MITM attacks.
|
|
181
178
|
*/
|
|
182
179
|
publicKey?: string;
|
|
183
180
|
|
|
184
|
-
/** Custom storage provider. Omit to use
|
|
181
|
+
/** Custom storage provider. Omit to use GitHubRawStorageProvider. */
|
|
185
182
|
storage?: OtaStorageProvider;
|
|
186
183
|
|
|
187
|
-
/**
|
|
184
|
+
/** Static storage layout read by GITHUB_RAW (public, read-only — no credentials) */
|
|
188
185
|
customServer?: {
|
|
189
|
-
baseUrl: string;
|
|
190
|
-
username: string;
|
|
191
|
-
password: string;
|
|
186
|
+
baseUrl: string; // https://raw.githubusercontent.com/<owner>/<repo>/<branch>
|
|
192
187
|
channel: string;
|
|
193
|
-
baseFolder?: string; // default: '
|
|
194
|
-
useMinio?: boolean;
|
|
188
|
+
baseFolder?: string; // default: 'ota'
|
|
195
189
|
};
|
|
196
190
|
};
|
|
197
191
|
```
|
|
@@ -254,7 +248,6 @@ Your OTA server must expose a `ota_registry.json` at the configured path (e.g. `
|
|
|
254
248
|
"baseHash": "sha256_of_v1_bundle",
|
|
255
249
|
"bundleHash": "sha256_of_v2_bundle",
|
|
256
250
|
"platform": "android",
|
|
257
|
-
"compressed": false,
|
|
258
251
|
"mandatory": false,
|
|
259
252
|
"fullBundleUrl": "v2-full.zip"
|
|
260
253
|
}
|
|
@@ -292,7 +285,7 @@ This means crash protection is **native-side and zero-cost** — it doesn't requ
|
|
|
292
285
|
|
|
293
286
|
## Custom Storage Provider
|
|
294
287
|
|
|
295
|
-
The library ships with `
|
|
288
|
+
The library ships with `GitHubRawStorageProvider` (default). To use a different backend (e.g. Cloudflare), implement the interface:
|
|
296
289
|
|
|
297
290
|
```ts
|
|
298
291
|
interface OtaStorageProvider {
|
|
@@ -434,7 +427,7 @@ yarn ota:build
|
|
|
434
427
|
|
|
435
428
|
### Environment Variables
|
|
436
429
|
|
|
437
|
-
|
|
430
|
+
Builds publish to a GitHub storage repository. Authentication uses `gh auth token` (or `GH_TOKEN` / `GITHUB_TOKEN`), so run `gh auth login` once.
|
|
438
431
|
|
|
439
432
|
**Option 1: `.env` file (recommended)**
|
|
440
433
|
|
|
@@ -442,8 +435,8 @@ Create a `.env` file in your **React Native project root** (not in the module di
|
|
|
442
435
|
|
|
443
436
|
```bash
|
|
444
437
|
# In your project root (e.g., /path/to/YourApp/)
|
|
445
|
-
|
|
446
|
-
|
|
438
|
+
OTA_BASE_URL=https://raw.githubusercontent.com/<owner>/<storage-repo>/main
|
|
439
|
+
OTA_FOLDER=ota
|
|
447
440
|
```
|
|
448
441
|
|
|
449
442
|
The build script automatically loads `.env` from the project root.
|
|
@@ -451,17 +444,15 @@ The build script automatically loads `.env` from the project root.
|
|
|
451
444
|
**Option 2: Export environment variables**
|
|
452
445
|
|
|
453
446
|
```bash
|
|
454
|
-
export
|
|
455
|
-
export OTA_PASSWORD="your-password"
|
|
447
|
+
export OTA_BASE_URL="https://raw.githubusercontent.com/<owner>/<storage-repo>/main"
|
|
456
448
|
yarn ota:build
|
|
457
449
|
```
|
|
458
450
|
|
|
459
451
|
| Variable | Required | Default | Description |
|
|
460
452
|
|----------|----------|---------|-------------|
|
|
461
|
-
| `
|
|
462
|
-
| `
|
|
463
|
-
| `
|
|
464
|
-
| `OTA_FOLDER` | No | `congplqg` | Base folder path |
|
|
453
|
+
| `OTA_BASE_URL` | Yes | - | `https://raw.githubusercontent.com/<owner>/<repo>/<branch>` |
|
|
454
|
+
| `OTA_FOLDER` | No | `ota` | Base folder in the storage repo |
|
|
455
|
+
| `OTA_PRIVATE_KEY` | No | - | PEM private key (or `ota_private_key.pem` in project root) |
|
|
465
456
|
|
|
466
457
|
> **Security Warning:** Never commit credentials to source control. The `.env` file is gitignored.
|
|
467
458
|
|
|
@@ -497,12 +488,12 @@ const otaConfig: OtaConfig = {
|
|
|
497
488
|
|
|
498
489
|
### 2. Automated Signing at Build Time
|
|
499
490
|
|
|
500
|
-
When you run `tools/build_ota.sh`, it automatically searches for `ota_private_key.pem` (or the `OTA_PRIVATE_KEY` environment variable). If found, it **
|
|
491
|
+
When you run `tools/build_ota.sh`, it automatically searches for `ota_private_key.pem` (or the `OTA_PRIVATE_KEY` environment variable). If found, it **signs each patch (`signature`) and the resulting bundle (`bundleSignature`) with SHA-256** and injects the signatures into `ota_registry.json`. The bundle signature is checked on both the patch path and the full-bundle fallback. If the key is present but a file can't be signed, the build fails.
|
|
501
492
|
|
|
502
493
|
On the client device:
|
|
503
494
|
- **Android**: Uses Android Security Provider (`SHA256withRSA` / `SHA256withECDSA`).
|
|
504
495
|
- **iOS**: Uses Apple Security framework (`SecKeyVerifySignature`).
|
|
505
|
-
- **
|
|
496
|
+
- **Fail closed**: when `publicKey` is set, a missing or invalid signature rejects the update. HMAC/symmetric keys are no longer supported (the public key ships in the app, so it can't be a secret).
|
|
506
497
|
|
|
507
498
|
---
|
|
508
499
|
|
|
@@ -547,7 +538,7 @@ When executing `tools/build_ota.sh`, the script auto-detects the version from `b
|
|
|
547
538
|
|
|
548
539
|
- **POSIX `mmap()` Zero-Copy Patching**: Unlike standard bspatch which allocates heap memory with `malloc()`, `react-native-bspatch` memory-maps the input and output files using `mmap()` and `MADV_SEQUENTIAL`. Memory overhead is reduced by **~85–90%**, virtually eliminating out-of-memory crashes on low-end devices.
|
|
549
540
|
- **SIMD Vectorization**: The inner patching loop includes `#pragma clang loop vectorize(enable)` for Clang ARM NEON 128-bit vectorization.
|
|
550
|
-
- **64KB Streaming Buffers**: File hashing
|
|
541
|
+
- **64KB Streaming Buffers**: File hashing and stream copying are upgraded to 64KB chunk sizes across Android and iOS.
|
|
551
542
|
- **Disk Garbage Collection**: Calling `markHealthy()` automatically deletes obsolete bundle versions and dangling `.tmp` files from disk, keeping app storage minimal.
|
|
552
543
|
- **HTTP 304 ETag Caching**: Registry queries send `If-None-Match`. On `304 Not Modified`, cached patches are returned instantly with zero JSON parsing overhead.
|
|
553
544
|
|
|
@@ -566,13 +557,12 @@ src/native_modules/react-native-bspatch/
|
|
|
566
557
|
│ ├── OtaProvider.tsx # React Context provider + auto-update logic
|
|
567
558
|
│ ├── types.ts # All TypeScript type definitions
|
|
568
559
|
│ └── storage/
|
|
569
|
-
│
|
|
570
|
-
│ └── GitHubReleaseStorage.ts # GitHub Releases storage (public / token-auth)
|
|
560
|
+
│ └── GitHubRawStorage.ts # GitHub raw repository storage (default)
|
|
571
561
|
├── android/src/main/java/vn/reactnativebspatch/
|
|
572
|
-
│ ├── BsPatchModule.kt # Native: applyPatch,
|
|
562
|
+
│ ├── BsPatchModule.kt # Native: applyPatch, extract, verifySignature
|
|
573
563
|
│ └── OtaBundleResolver.kt # Native: bundle resolution + rollback
|
|
574
564
|
├── ios/
|
|
575
|
-
│ ├── RNBsPatch.mm # Native iOS bridge +
|
|
565
|
+
│ ├── RNBsPatch.mm # Native iOS bridge + signature verify
|
|
576
566
|
│ └── OtaBundleResolver.swift # Native iOS bundle resolution + rollback
|
|
577
567
|
├── cpp/
|
|
578
568
|
│ ├── CMakeLists.txt # CMake build for bspatch C library
|
|
@@ -11,8 +11,6 @@ import java.io.FileInputStream
|
|
|
11
11
|
import java.io.FileOutputStream
|
|
12
12
|
import java.io.BufferedInputStream
|
|
13
13
|
import java.io.BufferedOutputStream
|
|
14
|
-
import java.util.zip.ZipInputStream
|
|
15
|
-
import java.util.zip.GZIPInputStream
|
|
16
14
|
import java.util.concurrent.Executors
|
|
17
15
|
|
|
18
16
|
class BsPatchModule(private val reactContext: ReactApplicationContext) : NativeBsPatchSpec(reactContext) {
|
|
@@ -44,36 +42,6 @@ class BsPatchModule(private val reactContext: ReactApplicationContext) : NativeB
|
|
|
44
42
|
}
|
|
45
43
|
}
|
|
46
44
|
|
|
47
|
-
override fun unzip(source: String, targetDir: String, promise: Promise) {
|
|
48
|
-
backgroundExecutor.execute {
|
|
49
|
-
try {
|
|
50
|
-
val sourceFile = File(source)
|
|
51
|
-
val destDir = File(targetDir)
|
|
52
|
-
if (!destDir.exists()) destDir.mkdirs()
|
|
53
|
-
|
|
54
|
-
ZipInputStream(BufferedInputStream(FileInputStream(sourceFile), IO_BUFFER_SIZE)).use { zis ->
|
|
55
|
-
var entry = zis.nextEntry
|
|
56
|
-
while (entry != null) {
|
|
57
|
-
val file = File(destDir, entry.name)
|
|
58
|
-
if (entry.isDirectory) {
|
|
59
|
-
file.mkdirs()
|
|
60
|
-
} else {
|
|
61
|
-
file.parentFile?.mkdirs()
|
|
62
|
-
BufferedOutputStream(FileOutputStream(file), IO_BUFFER_SIZE).use { bos ->
|
|
63
|
-
zis.copyTo(bos, bufferSize = IO_BUFFER_SIZE)
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
zis.closeEntry()
|
|
67
|
-
entry = zis.nextEntry
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
promise.resolve(true)
|
|
71
|
-
} catch (e: Exception) {
|
|
72
|
-
promise.reject("ERR_UNZIP", e.message, e)
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
45
|
/**
|
|
78
46
|
* Extracts the JS bundle from APK assets using Android AssetManager.
|
|
79
47
|
* Guarantees byte-exact copy — RNFS.copyFileAssets can produce different
|
|
@@ -119,25 +87,6 @@ class BsPatchModule(private val reactContext: ReactApplicationContext) : NativeB
|
|
|
119
87
|
android.util.Log.d("JS-OTA", message ?: "")
|
|
120
88
|
}
|
|
121
89
|
|
|
122
|
-
override fun decompressGzip(sourcePath: String, destPath: String, promise: Promise) {
|
|
123
|
-
backgroundExecutor.execute {
|
|
124
|
-
try {
|
|
125
|
-
val source = File(sourcePath)
|
|
126
|
-
val dest = File(destPath)
|
|
127
|
-
dest.parentFile?.mkdirs()
|
|
128
|
-
|
|
129
|
-
GZIPInputStream(BufferedInputStream(FileInputStream(source), IO_BUFFER_SIZE), IO_BUFFER_SIZE).use { gis ->
|
|
130
|
-
BufferedOutputStream(FileOutputStream(dest), IO_BUFFER_SIZE).use { bos ->
|
|
131
|
-
gis.copyTo(bos, bufferSize = IO_BUFFER_SIZE)
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
promise.resolve(true)
|
|
135
|
-
} catch (e: Exception) {
|
|
136
|
-
promise.reject("ERR_GZIP_DECOMPRESS", e.message, e)
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
|
|
141
90
|
override fun verifySignature(filePath: String, signature: String, publicKey: String, promise: Promise) {
|
|
142
91
|
backgroundExecutor.execute {
|
|
143
92
|
try {
|
|
@@ -157,65 +106,29 @@ class BsPatchModule(private val reactContext: ReactApplicationContext) : NativeB
|
|
|
157
106
|
val keyBytes = android.util.Base64.decode(cleanKey, android.util.Base64.DEFAULT)
|
|
158
107
|
val expectedSigBytes = android.util.Base64.decode(signature.trim(), android.util.Base64.DEFAULT)
|
|
159
108
|
|
|
160
|
-
//
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
sig.initVerify(pubKey)
|
|
167
|
-
|
|
168
|
-
val buffer = ByteArray(IO_BUFFER_SIZE)
|
|
169
|
-
FileInputStream(file).use { fis ->
|
|
170
|
-
var read: Int
|
|
171
|
-
while (fis.read(buffer).also { read = it } != -1) {
|
|
172
|
-
sig.update(buffer, 0, read)
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
val isValid = sig.verify(expectedSigBytes)
|
|
176
|
-
promise.resolve(isValid)
|
|
177
|
-
return@execute
|
|
178
|
-
} catch (rsaEx: Exception) {
|
|
179
|
-
// Not RSA, continue
|
|
109
|
+
// Pick the algorithm from the key itself; never fall back to another scheme
|
|
110
|
+
val keySpec = java.security.spec.X509EncodedKeySpec(keyBytes)
|
|
111
|
+
val pubKey = try {
|
|
112
|
+
java.security.KeyFactory.getInstance("RSA").generatePublic(keySpec)
|
|
113
|
+
} catch (e: Exception) {
|
|
114
|
+
java.security.KeyFactory.getInstance("EC").generatePublic(keySpec)
|
|
180
115
|
}
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
val keyFactory = java.security.KeyFactory.getInstance("EC")
|
|
186
|
-
val pubKey = keyFactory.generatePublic(keySpec)
|
|
187
|
-
val sig = java.security.Signature.getInstance("SHA256withECDSA")
|
|
188
|
-
sig.initVerify(pubKey)
|
|
189
|
-
|
|
190
|
-
val buffer = ByteArray(IO_BUFFER_SIZE)
|
|
191
|
-
FileInputStream(file).use { fis ->
|
|
192
|
-
var read: Int
|
|
193
|
-
while (fis.read(buffer).also { read = it } != -1) {
|
|
194
|
-
sig.update(buffer, 0, read)
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
val isValid = sig.verify(expectedSigBytes)
|
|
198
|
-
promise.resolve(isValid)
|
|
199
|
-
return@execute
|
|
200
|
-
} catch (ecEx: Exception) {
|
|
201
|
-
// Not EC, continue
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
// 3. Fallback: HMAC-SHA256
|
|
205
|
-
val mac = javax.crypto.Mac.getInstance("HmacSHA256")
|
|
206
|
-
val secretKey = javax.crypto.spec.SecretKeySpec(keyBytes, "HmacSHA256")
|
|
207
|
-
mac.init(secretKey)
|
|
116
|
+
val sig = java.security.Signature.getInstance(
|
|
117
|
+
if (pubKey.algorithm == "RSA") "SHA256withRSA" else "SHA256withECDSA"
|
|
118
|
+
)
|
|
119
|
+
sig.initVerify(pubKey)
|
|
208
120
|
|
|
209
121
|
val buffer = ByteArray(IO_BUFFER_SIZE)
|
|
210
122
|
FileInputStream(file).use { fis ->
|
|
211
123
|
var read: Int
|
|
212
124
|
while (fis.read(buffer).also { read = it } != -1) {
|
|
213
|
-
|
|
125
|
+
sig.update(buffer, 0, read)
|
|
214
126
|
}
|
|
215
127
|
}
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
128
|
+
promise.resolve(sig.verify(expectedSigBytes))
|
|
129
|
+
} catch (e: java.security.GeneralSecurityException) {
|
|
130
|
+
// Malformed key/signature → invalid (fail closed)
|
|
131
|
+
promise.resolve(false)
|
|
219
132
|
} catch (e: Exception) {
|
|
220
133
|
promise.reject("ERR_VERIFY_SIG", e.message, e)
|
|
221
134
|
}
|
|
@@ -55,6 +55,18 @@ object OtaBundleResolver {
|
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
var metadata = JSONObject(metadataFile.readText())
|
|
58
|
+
|
|
59
|
+
// ── Step 0: Store update → OTA bundles were built for another native version ──
|
|
60
|
+
val otaAppVersion = metadata.optString("appVersion", "")
|
|
61
|
+
val installedAppVersion = context.packageManager
|
|
62
|
+
.getPackageInfo(context.packageName, 0).versionName ?: ""
|
|
63
|
+
if (otaAppVersion.isNotEmpty() && otaAppVersion != installedAppVersion) {
|
|
64
|
+
Log.i(TAG, "Native app $otaAppVersion → $installedAppVersion: discarding OTA state")
|
|
65
|
+
// JS bootstrap deletes ota_trash_* in the background
|
|
66
|
+
otaDir.renameTo(File(context.filesDir, "ota_trash_${System.currentTimeMillis()}"))
|
|
67
|
+
return null
|
|
68
|
+
}
|
|
69
|
+
|
|
58
70
|
val currentVersion = metadata.optInt("currentVersion", baseVersion)
|
|
59
71
|
val lastLaunchOk = metadata.optBoolean("lastLaunchSuccessful", true)
|
|
60
72
|
val rollbackCount = metadata.optInt("rollbackCount", 0)
|
|
@@ -62,7 +74,12 @@ object OtaBundleResolver {
|
|
|
62
74
|
// ── Step 1: Crash detection & rollback ──────────────────────────
|
|
63
75
|
if (!lastLaunchOk && currentVersion > baseVersion) {
|
|
64
76
|
Log.w(TAG, "Previous launch failed! Rolling back from V$currentVersion...")
|
|
77
|
+
// Remember the crashing version so JS never re-applies it
|
|
78
|
+
val failed = metadata.optJSONArray("failedVersions") ?: JSONArray()
|
|
79
|
+
failed.put(currentVersion)
|
|
65
80
|
metadata = performRollback(metadata, baseVersion, rollbackCount, otaDir)
|
|
81
|
+
metadata.put("failedVersions", failed)
|
|
82
|
+
metadata.put("appVersion", otaAppVersion)
|
|
66
83
|
writeMetadata(metadataFile, metadata)
|
|
67
84
|
|
|
68
85
|
val newVersion = metadata.optInt("currentVersion", baseVersion)
|
|
@@ -219,7 +236,10 @@ object OtaBundleResolver {
|
|
|
219
236
|
private fun writeMetadata(file: File, metadata: JSONObject) {
|
|
220
237
|
try {
|
|
221
238
|
file.parentFile?.mkdirs()
|
|
222
|
-
|
|
239
|
+
// Write-then-rename: a crash mid-write never leaves truncated JSON
|
|
240
|
+
val tmp = File(file.parentFile, "${file.name}.tmp")
|
|
241
|
+
tmp.writeText(metadata.toString(2))
|
|
242
|
+
if (!tmp.renameTo(file)) throw java.io.IOException("rename failed")
|
|
223
243
|
} catch (e: Exception) {
|
|
224
244
|
Log.e(TAG, "Failed to write metadata: ${e.message}")
|
|
225
245
|
}
|
package/cpp/bsdiff/bspatch.c
CHANGED
|
@@ -62,6 +62,17 @@ public class OtaBundleResolver: NSObject {
|
|
|
62
62
|
return nil
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
+
// ── Step 0: Store update → OTA bundles were built for another native version ──
|
|
66
|
+
let otaAppVersion = metadata["appVersion"] as? String ?? ""
|
|
67
|
+
let installedAppVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? ""
|
|
68
|
+
if !otaAppVersion.isEmpty && otaAppVersion != installedAppVersion {
|
|
69
|
+
NSLog("[OtaBundleResolver] Native app \(otaAppVersion) → \(installedAppVersion): discarding OTA state")
|
|
70
|
+
// JS bootstrap deletes ota_trash_* in the background
|
|
71
|
+
let trash = documentsDir.appendingPathComponent("ota_trash_\(Int(Date().timeIntervalSince1970 * 1000))")
|
|
72
|
+
try? FileManager.default.moveItem(at: otaDir, to: trash)
|
|
73
|
+
return nil
|
|
74
|
+
}
|
|
75
|
+
|
|
65
76
|
let currentVersion = metadata["currentVersion"] as? Int ?? baseVersion
|
|
66
77
|
let lastLaunchOk = metadata["lastLaunchSuccessful"] as? Bool ?? true
|
|
67
78
|
let rollbackCount = metadata["rollbackCount"] as? Int ?? 0
|
|
@@ -70,7 +81,11 @@ public class OtaBundleResolver: NSObject {
|
|
|
70
81
|
if !lastLaunchOk && currentVersion > baseVersion {
|
|
71
82
|
NSLog("[OtaBundleResolver] Previous launch failed! Rolling back from V\(currentVersion)...")
|
|
72
83
|
|
|
84
|
+
// Remember the crashing version so JS never re-applies it
|
|
85
|
+
let failed = (metadata["failedVersions"] as? [Int] ?? []) + [currentVersion]
|
|
73
86
|
metadata = performRollback(metadata: metadata, baseVersion: baseVersion, currentRollbackCount: rollbackCount, otaDir: otaDir)
|
|
87
|
+
metadata["failedVersions"] = failed
|
|
88
|
+
metadata["appVersion"] = otaAppVersion
|
|
74
89
|
writeMetadata(url: metadataURL, metadata: metadata)
|
|
75
90
|
|
|
76
91
|
let newVersion = metadata["currentVersion"] as? Int ?? baseVersion
|
|
@@ -233,7 +248,7 @@ public class OtaBundleResolver: NSObject {
|
|
|
233
248
|
private static func writeMetadata(url: URL, metadata: [String: Any]) {
|
|
234
249
|
do {
|
|
235
250
|
let data = try JSONSerialization.data(withJSONObject: metadata, options: .prettyPrinted)
|
|
236
|
-
try data.write(to: url)
|
|
251
|
+
try data.write(to: url, options: .atomic)
|
|
237
252
|
} catch {
|
|
238
253
|
NSLog("[OtaBundleResolver] Failed to write metadata: \(error.localizedDescription)")
|
|
239
254
|
}
|