@otakit/capacitor-updater 1.0.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +53 -7
- package/android/src/main/java/com/updatekit/updater/BundleInfo.java +34 -2
- package/android/src/main/java/com/updatekit/updater/BundleStore.java +13 -2
- package/android/src/main/java/com/updatekit/updater/ManifestClient.java +56 -47
- package/android/src/main/java/com/updatekit/updater/ManifestVerifier.java +92 -19
- package/android/src/main/java/com/updatekit/updater/UpdaterPlugin.java +183 -16
- package/dist/esm/definitions.d.ts +13 -3
- package/dist/esm/definitions.d.ts.map +1 -1
- package/ios/Sources/UpdaterPlugin/BundleInfo.swift +5 -0
- package/ios/Sources/UpdaterPlugin/BundleStore.swift +3 -0
- package/ios/Sources/UpdaterPlugin/ManifestClient.swift +57 -34
- package/ios/Sources/UpdaterPlugin/ManifestVerifier.swift +73 -18
- package/ios/Sources/UpdaterPlugin/UpdaterPlugin.swift +209 -16
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -7,8 +7,10 @@ Capacitor OTA updater plugin for OtaKit.
|
|
|
7
7
|
- checks the manifest endpoint for a newer bundle
|
|
8
8
|
- downloads and verifies OTA bundles
|
|
9
9
|
- stages updates safely
|
|
10
|
-
- activates them on the next launch or immediately
|
|
10
|
+
- activates them on the next launch, next resume, or immediately
|
|
11
|
+
- checks for updates on cold start and app resume (configurable interval)
|
|
11
12
|
- also supports fully manual update prompts when the app wants control
|
|
13
|
+
- supports optional `runtimeVersion` lanes for native compatibility boundaries
|
|
12
14
|
- requires `notifyAppReady()` as the success handshake
|
|
13
15
|
- rolls back automatically if the new bundle does not prove healthy
|
|
14
16
|
|
|
@@ -45,6 +47,8 @@ plugins: {
|
|
|
45
47
|
appReadyTimeout: 10000,
|
|
46
48
|
// Optional:
|
|
47
49
|
// channel: "staging",
|
|
50
|
+
// runtimeVersion: "2026.04",
|
|
51
|
+
// updateMode: "next-resume",
|
|
48
52
|
// updateMode: "manual",
|
|
49
53
|
// updateMode: "immediate",
|
|
50
54
|
}
|
|
@@ -60,6 +64,22 @@ Advanced overrides for self-hosting or custom trust only:
|
|
|
60
64
|
Hosted OtaKit already points at `https://otakit.app/api/v1` and already trusts
|
|
61
65
|
the managed manifest signing keys.
|
|
62
66
|
|
|
67
|
+
## Channels vs runtimeVersion
|
|
68
|
+
|
|
69
|
+
- `channel` answers "who should get this rollout?"
|
|
70
|
+
- `runtimeVersion` answers "which native app shell can safely run this bundle?"
|
|
71
|
+
|
|
72
|
+
Use channels for rollout tracks such as `beta`, `staging`, or `production`.
|
|
73
|
+
|
|
74
|
+
Use `runtimeVersion` when a new store build creates a new compatibility boundary and you do not
|
|
75
|
+
want devices on that new native shell to keep receiving older OTA bundles.
|
|
76
|
+
|
|
77
|
+
When `runtimeVersion` is set:
|
|
78
|
+
|
|
79
|
+
- the plugin requests bundle updates only for that runtime lane
|
|
80
|
+
- bundle uploads inherit the same runtime value automatically through the CLI
|
|
81
|
+
- releases stay simple: publish the bundle, and it naturally stays inside its own runtime lane
|
|
82
|
+
|
|
63
83
|
## Trust model
|
|
64
84
|
|
|
65
85
|
The plugin does not just download from a URL and trust the result.
|
|
@@ -74,14 +94,32 @@ In the hosted path, managed signing keys are already built in.
|
|
|
74
94
|
|
|
75
95
|
## Update modes
|
|
76
96
|
|
|
97
|
+
All automatic modes check for updates on **cold start** (always) and **app
|
|
98
|
+
resume**.
|
|
99
|
+
|
|
100
|
+
### Production modes
|
|
101
|
+
|
|
102
|
+
- `next-launch` (default)
|
|
103
|
+
check and download in the background on cold start and resume.
|
|
104
|
+
activate the staged bundle only on the next cold start.
|
|
105
|
+
zero disruption during a session — the user never sees a surprise reload.
|
|
106
|
+
|
|
107
|
+
- `next-resume` (more eager)
|
|
108
|
+
check and download in the background on cold start and resume.
|
|
109
|
+
activate the staged bundle on the next resume or cold start.
|
|
110
|
+
|
|
111
|
+
### Manual mode
|
|
112
|
+
|
|
77
113
|
- `manual`
|
|
78
|
-
no automatic
|
|
79
|
-
|
|
80
|
-
|
|
114
|
+
no automatic checks, no automatic staged activation.
|
|
115
|
+
the app integration drives everything via `check()`, `download()`, `apply()`, or `update()`.
|
|
116
|
+
|
|
117
|
+
### Dev/debug mode
|
|
118
|
+
|
|
81
119
|
- `immediate`
|
|
82
|
-
|
|
120
|
+
checks, downloads, and activates in one shot as soon as possible on cold start and resume (the user may briefly see the previous version before a reload).
|
|
121
|
+
primarily for development and testing — not recommended for production.
|
|
83
122
|
|
|
84
|
-
The default is `next-launch`.
|
|
85
123
|
|
|
86
124
|
## Runtime model
|
|
87
125
|
|
|
@@ -127,7 +165,8 @@ await OtaKit.notifyAppReady();
|
|
|
127
165
|
```
|
|
128
166
|
|
|
129
167
|
The plugin handles checking, downloading, activation, and rollback based on
|
|
130
|
-
`updateMode`.
|
|
168
|
+
`updateMode`. It checks on cold start and every time the app comes back from
|
|
169
|
+
the background (throttled by `checkInterval`).
|
|
131
170
|
|
|
132
171
|
For most apps, this is the entire runtime integration.
|
|
133
172
|
|
|
@@ -154,6 +193,13 @@ await OtaKit.apply();
|
|
|
154
193
|
Use the split flow when the app wants to download in the background and switch
|
|
155
194
|
only after explicit user confirmation.
|
|
156
195
|
|
|
196
|
+
## Throttle
|
|
197
|
+
|
|
198
|
+
All server checks are rate-limited by `checkInterval` (default 10 min).
|
|
199
|
+
This applies to automatic resume checks and to manual `check()` / `download()`
|
|
200
|
+
calls. Within the interval, calls return the staged bundle if one exists, or
|
|
201
|
+
null.
|
|
202
|
+
|
|
157
203
|
## Retention and deletion
|
|
158
204
|
|
|
159
205
|
- the builtin, current, fallback, and staged bundles are protected
|
|
@@ -8,6 +8,7 @@ class BundleInfo {
|
|
|
8
8
|
|
|
9
9
|
final String id;
|
|
10
10
|
final String version;
|
|
11
|
+
final String runtimeVersion;
|
|
11
12
|
final BundleStatus status;
|
|
12
13
|
final Long downloadedAt;
|
|
13
14
|
final String sha256;
|
|
@@ -18,6 +19,7 @@ class BundleInfo {
|
|
|
18
19
|
BundleInfo(
|
|
19
20
|
String id,
|
|
20
21
|
String version,
|
|
22
|
+
String runtimeVersion,
|
|
21
23
|
BundleStatus status,
|
|
22
24
|
Long downloadedAt,
|
|
23
25
|
String sha256,
|
|
@@ -27,6 +29,7 @@ class BundleInfo {
|
|
|
27
29
|
) {
|
|
28
30
|
this.id = id;
|
|
29
31
|
this.version = version;
|
|
32
|
+
this.runtimeVersion = runtimeVersion;
|
|
30
33
|
this.status = status;
|
|
31
34
|
this.downloadedAt = downloadedAt;
|
|
32
35
|
this.sha256 = sha256;
|
|
@@ -43,6 +46,9 @@ class BundleInfo {
|
|
|
43
46
|
JSObject object = new JSObject();
|
|
44
47
|
object.put("id", id);
|
|
45
48
|
object.put("version", version);
|
|
49
|
+
if (runtimeVersion != null) {
|
|
50
|
+
object.put("runtimeVersion", runtimeVersion);
|
|
51
|
+
}
|
|
46
52
|
object.put("status", status.value());
|
|
47
53
|
if (downloadedAt != null) {
|
|
48
54
|
object.put("downloadedAt", DateUtils.toIsoString(downloadedAt));
|
|
@@ -63,6 +69,9 @@ class BundleInfo {
|
|
|
63
69
|
JSONObject object = new JSONObject();
|
|
64
70
|
object.put("id", id);
|
|
65
71
|
object.put("version", version);
|
|
72
|
+
if (runtimeVersion != null) {
|
|
73
|
+
object.put("runtimeVersion", runtimeVersion);
|
|
74
|
+
}
|
|
66
75
|
object.put("status", status.value());
|
|
67
76
|
if (downloadedAt != null) {
|
|
68
77
|
object.put("downloadedAt", downloadedAt);
|
|
@@ -85,16 +94,39 @@ class BundleInfo {
|
|
|
85
94
|
static BundleInfo fromJSONObject(JSONObject object) {
|
|
86
95
|
String id = object.optString("id", "builtin");
|
|
87
96
|
String version = object.optString("version", "0.0.0");
|
|
97
|
+
String runtimeVersion = object.has("runtimeVersion")
|
|
98
|
+
? object.optString("runtimeVersion", null)
|
|
99
|
+
: null;
|
|
88
100
|
BundleStatus status = BundleStatus.from(object.optString("status", "pending"));
|
|
89
101
|
Long downloadedAt = object.has("downloadedAt") ? object.optLong("downloadedAt") : null;
|
|
90
102
|
String sha256 = object.has("sha256") ? object.optString("sha256", null) : null;
|
|
91
103
|
String channel = object.has("channel") ? object.optString("channel", null) : null;
|
|
92
104
|
String releaseId = object.has("releaseId") ? object.optString("releaseId", null) : null;
|
|
93
105
|
String path = object.has("path") ? object.optString("path", null) : null;
|
|
94
|
-
return new BundleInfo(
|
|
106
|
+
return new BundleInfo(
|
|
107
|
+
id,
|
|
108
|
+
version,
|
|
109
|
+
runtimeVersion,
|
|
110
|
+
status,
|
|
111
|
+
downloadedAt,
|
|
112
|
+
sha256,
|
|
113
|
+
path,
|
|
114
|
+
channel,
|
|
115
|
+
releaseId
|
|
116
|
+
);
|
|
95
117
|
}
|
|
96
118
|
|
|
97
119
|
BundleInfo withStatus(BundleStatus nextStatus) {
|
|
98
|
-
return new BundleInfo(
|
|
120
|
+
return new BundleInfo(
|
|
121
|
+
id,
|
|
122
|
+
version,
|
|
123
|
+
runtimeVersion,
|
|
124
|
+
nextStatus,
|
|
125
|
+
downloadedAt,
|
|
126
|
+
sha256,
|
|
127
|
+
path,
|
|
128
|
+
channel,
|
|
129
|
+
releaseId
|
|
130
|
+
);
|
|
99
131
|
}
|
|
100
132
|
}
|
|
@@ -27,12 +27,14 @@ final class BundleStore {
|
|
|
27
27
|
private final File bundlesDirectory;
|
|
28
28
|
private final String builtinVersion;
|
|
29
29
|
private final String nativeBuild;
|
|
30
|
+
private final String appRuntimeVersion;
|
|
30
31
|
|
|
31
|
-
BundleStore(Context context, String builtinVersion, String nativeBuild) {
|
|
32
|
+
BundleStore(Context context, String builtinVersion, String nativeBuild, String appRuntimeVersion) {
|
|
32
33
|
this.context = context.getApplicationContext();
|
|
33
34
|
this.prefs = this.context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
|
|
34
35
|
this.builtinVersion = builtinVersion;
|
|
35
36
|
this.nativeBuild = nativeBuild;
|
|
37
|
+
this.appRuntimeVersion = appRuntimeVersion;
|
|
36
38
|
this.bundlesDirectory = new File(this.context.getFilesDir(), "otakit_bundles");
|
|
37
39
|
if (!bundlesDirectory.exists()) {
|
|
38
40
|
//noinspection ResultOfMethodCallIgnored
|
|
@@ -48,10 +50,15 @@ final class BundleStore {
|
|
|
48
50
|
return builtinVersion;
|
|
49
51
|
}
|
|
50
52
|
|
|
53
|
+
SharedPreferences getPrefs() {
|
|
54
|
+
return prefs;
|
|
55
|
+
}
|
|
56
|
+
|
|
51
57
|
BundleInfo builtinBundle() {
|
|
52
58
|
return new BundleInfo(
|
|
53
59
|
"builtin",
|
|
54
60
|
builtinVersion,
|
|
61
|
+
appRuntimeVersion,
|
|
55
62
|
BundleStatus.BUILTIN,
|
|
56
63
|
null,
|
|
57
64
|
null,
|
|
@@ -225,7 +232,7 @@ final class BundleStore {
|
|
|
225
232
|
}
|
|
226
233
|
}
|
|
227
234
|
|
|
228
|
-
synchronized
|
|
235
|
+
synchronized List<BundleInfo> listDownloadedBundleInfos() {
|
|
229
236
|
List<BundleInfo> result = new ArrayList<>();
|
|
230
237
|
File[] entries = bundlesDirectory.listFiles();
|
|
231
238
|
if (entries != null) {
|
|
@@ -237,7 +244,11 @@ final class BundleStore {
|
|
|
237
244
|
}
|
|
238
245
|
}
|
|
239
246
|
}
|
|
247
|
+
return result;
|
|
248
|
+
}
|
|
240
249
|
|
|
250
|
+
synchronized JSArray listDownloadedBundles() {
|
|
251
|
+
List<BundleInfo> result = listDownloadedBundleInfos();
|
|
241
252
|
JSArray array = new JSArray();
|
|
242
253
|
for (BundleInfo bundle : result) {
|
|
243
254
|
array.put(bundle.toJSObject());
|
|
@@ -30,26 +30,23 @@ final class ManifestClient {
|
|
|
30
30
|
final String url;
|
|
31
31
|
final String sha256;
|
|
32
32
|
final int size;
|
|
33
|
-
final
|
|
33
|
+
final String runtimeVersion;
|
|
34
34
|
final String releaseId;
|
|
35
|
-
final ManifestSignature signature;
|
|
36
35
|
|
|
37
36
|
LatestManifest(
|
|
38
37
|
String version,
|
|
39
38
|
String url,
|
|
40
39
|
String sha256,
|
|
41
40
|
int size,
|
|
42
|
-
|
|
43
|
-
String releaseId
|
|
44
|
-
ManifestSignature signature
|
|
41
|
+
String runtimeVersion,
|
|
42
|
+
String releaseId
|
|
45
43
|
) {
|
|
46
44
|
this.version = version;
|
|
47
45
|
this.url = url;
|
|
48
46
|
this.sha256 = sha256;
|
|
49
47
|
this.size = size;
|
|
50
|
-
this.
|
|
48
|
+
this.runtimeVersion = runtimeVersion;
|
|
51
49
|
this.releaseId = releaseId;
|
|
52
|
-
this.signature = signature;
|
|
53
50
|
}
|
|
54
51
|
}
|
|
55
52
|
|
|
@@ -71,7 +68,7 @@ final class ManifestClient {
|
|
|
71
68
|
String channel,
|
|
72
69
|
String currentVersion,
|
|
73
70
|
String currentReleaseId,
|
|
74
|
-
String
|
|
71
|
+
String runtimeVersion,
|
|
75
72
|
String platform,
|
|
76
73
|
boolean allowInsecureUrls,
|
|
77
74
|
java.util.List<ManifestVerifier.KeyEntry> manifestKeys
|
|
@@ -93,7 +90,9 @@ final class ManifestClient {
|
|
|
93
90
|
if (currentReleaseId != null && !currentReleaseId.trim().isEmpty()) {
|
|
94
91
|
connection.setRequestProperty("X-Release-Id", currentReleaseId);
|
|
95
92
|
}
|
|
96
|
-
|
|
93
|
+
if (runtimeVersion != null && !runtimeVersion.trim().isEmpty()) {
|
|
94
|
+
connection.setRequestProperty("X-Runtime-Version", runtimeVersion);
|
|
95
|
+
}
|
|
97
96
|
connection.setConnectTimeout(15_000);
|
|
98
97
|
connection.setReadTimeout(30_000);
|
|
99
98
|
|
|
@@ -118,31 +117,15 @@ final class ManifestClient {
|
|
|
118
117
|
String sha256 = json.getString("sha256");
|
|
119
118
|
int size = json.getInt("size");
|
|
120
119
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
} else if (raw instanceof String) {
|
|
127
|
-
String value = ((String) raw).trim();
|
|
128
|
-
if (!value.isEmpty()) {
|
|
129
|
-
minNativeBuild = Integer.parseInt(value);
|
|
130
|
-
}
|
|
131
|
-
}
|
|
120
|
+
String responseRuntimeVersion = json.has("runtimeVersion") && !json.isNull("runtimeVersion")
|
|
121
|
+
? json.getString("runtimeVersion").trim()
|
|
122
|
+
: null;
|
|
123
|
+
if (responseRuntimeVersion != null && responseRuntimeVersion.isEmpty()) {
|
|
124
|
+
responseRuntimeVersion = null;
|
|
132
125
|
}
|
|
133
126
|
|
|
134
|
-
ManifestSignature signature =
|
|
135
|
-
|
|
136
|
-
JSONObject sigObj = json.getJSONObject("signature");
|
|
137
|
-
if (sigObj.has("kid") && sigObj.has("sig") && sigObj.has("iat") && sigObj.has("exp")) {
|
|
138
|
-
signature = new ManifestSignature(
|
|
139
|
-
sigObj.getString("kid"),
|
|
140
|
-
sigObj.getString("sig"),
|
|
141
|
-
sigObj.getInt("iat"),
|
|
142
|
-
sigObj.getInt("exp")
|
|
143
|
-
);
|
|
144
|
-
}
|
|
145
|
-
}
|
|
127
|
+
ManifestSignature signature = parseSignature(json.optJSONObject("signature"));
|
|
128
|
+
ManifestSignature signatureV2 = parseSignature(json.optJSONObject("signatureV2"));
|
|
146
129
|
|
|
147
130
|
String releaseId = null;
|
|
148
131
|
if (json.has("releaseId") && !json.isNull("releaseId")) {
|
|
@@ -161,22 +144,34 @@ final class ManifestClient {
|
|
|
161
144
|
|
|
162
145
|
// Verify manifest signature if signing keys are configured
|
|
163
146
|
if (manifestKeys != null && !manifestKeys.isEmpty()) {
|
|
164
|
-
if (
|
|
147
|
+
if (signatureV2 != null) {
|
|
148
|
+
ManifestVerifier.verify(
|
|
149
|
+
appId,
|
|
150
|
+
channel,
|
|
151
|
+
platform,
|
|
152
|
+
version,
|
|
153
|
+
sha256,
|
|
154
|
+
size,
|
|
155
|
+
responseRuntimeVersion,
|
|
156
|
+
signatureV2,
|
|
157
|
+
manifestKeys
|
|
158
|
+
);
|
|
159
|
+
} else if (signature != null) {
|
|
160
|
+
ManifestVerifier.verifyLegacy(
|
|
161
|
+
appId,
|
|
162
|
+
channel,
|
|
163
|
+
platform,
|
|
164
|
+
version,
|
|
165
|
+
sha256,
|
|
166
|
+
size,
|
|
167
|
+
signature,
|
|
168
|
+
manifestKeys
|
|
169
|
+
);
|
|
170
|
+
} else {
|
|
165
171
|
throw new IllegalStateException(
|
|
166
172
|
"Manifest signature missing but signing keys are configured"
|
|
167
173
|
);
|
|
168
174
|
}
|
|
169
|
-
ManifestVerifier.verify(
|
|
170
|
-
appId,
|
|
171
|
-
channel,
|
|
172
|
-
platform,
|
|
173
|
-
version,
|
|
174
|
-
sha256,
|
|
175
|
-
size,
|
|
176
|
-
minNativeBuild,
|
|
177
|
-
signature,
|
|
178
|
-
manifestKeys
|
|
179
|
-
);
|
|
180
175
|
}
|
|
181
176
|
|
|
182
177
|
return new LatestManifest(
|
|
@@ -184,9 +179,8 @@ final class ManifestClient {
|
|
|
184
179
|
downloadUrl,
|
|
185
180
|
sha256,
|
|
186
181
|
size,
|
|
187
|
-
|
|
188
|
-
releaseId
|
|
189
|
-
signature
|
|
182
|
+
responseRuntimeVersion,
|
|
183
|
+
releaseId
|
|
190
184
|
);
|
|
191
185
|
} finally {
|
|
192
186
|
connection.disconnect();
|
|
@@ -206,4 +200,19 @@ final class ManifestClient {
|
|
|
206
200
|
return new String(out.toByteArray(), StandardCharsets.UTF_8);
|
|
207
201
|
}
|
|
208
202
|
}
|
|
203
|
+
|
|
204
|
+
private static ManifestSignature parseSignature(JSONObject sigObj) {
|
|
205
|
+
if (sigObj == null) {
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
if (!sigObj.has("kid") || !sigObj.has("sig") || !sigObj.has("iat") || !sigObj.has("exp")) {
|
|
209
|
+
return null;
|
|
210
|
+
}
|
|
211
|
+
return new ManifestSignature(
|
|
212
|
+
sigObj.getString("kid"),
|
|
213
|
+
sigObj.getString("sig"),
|
|
214
|
+
sigObj.getInt("iat"),
|
|
215
|
+
sigObj.getInt("exp")
|
|
216
|
+
);
|
|
217
|
+
}
|
|
209
218
|
}
|
|
@@ -34,7 +34,51 @@ final class ManifestVerifier {
|
|
|
34
34
|
String version,
|
|
35
35
|
String sha256,
|
|
36
36
|
int size,
|
|
37
|
-
|
|
37
|
+
String runtimeVersion,
|
|
38
|
+
ManifestClient.ManifestSignature signature,
|
|
39
|
+
List<KeyEntry> trustedKeys
|
|
40
|
+
) throws Exception {
|
|
41
|
+
String payload = buildCanonicalPayload(
|
|
42
|
+
appId,
|
|
43
|
+
channel,
|
|
44
|
+
platform,
|
|
45
|
+
version,
|
|
46
|
+
sha256,
|
|
47
|
+
size,
|
|
48
|
+
runtimeVersion,
|
|
49
|
+
signature.kid,
|
|
50
|
+
signature.iat,
|
|
51
|
+
signature.exp
|
|
52
|
+
);
|
|
53
|
+
verifyPayload(payload, signature, trustedKeys);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
static void verifyLegacy(
|
|
57
|
+
String appId,
|
|
58
|
+
String channel,
|
|
59
|
+
String platform,
|
|
60
|
+
String version,
|
|
61
|
+
String sha256,
|
|
62
|
+
int size,
|
|
63
|
+
ManifestClient.ManifestSignature signature,
|
|
64
|
+
List<KeyEntry> trustedKeys
|
|
65
|
+
) throws Exception {
|
|
66
|
+
String payload = buildLegacyCanonicalPayload(
|
|
67
|
+
appId,
|
|
68
|
+
channel,
|
|
69
|
+
platform,
|
|
70
|
+
version,
|
|
71
|
+
sha256,
|
|
72
|
+
size,
|
|
73
|
+
signature.kid,
|
|
74
|
+
signature.iat,
|
|
75
|
+
signature.exp
|
|
76
|
+
);
|
|
77
|
+
verifyPayload(payload, signature, trustedKeys);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
private static void verifyPayload(
|
|
81
|
+
String payload,
|
|
38
82
|
ManifestClient.ManifestSignature signature,
|
|
39
83
|
List<KeyEntry> trustedKeys
|
|
40
84
|
) throws Exception {
|
|
@@ -56,20 +100,6 @@ final class ManifestVerifier {
|
|
|
56
100
|
throw new IllegalStateException("Unknown signing key ID: " + signature.kid);
|
|
57
101
|
}
|
|
58
102
|
|
|
59
|
-
// Build canonical payload (must match server exactly)
|
|
60
|
-
String payload = buildCanonicalPayload(
|
|
61
|
-
appId,
|
|
62
|
-
channel,
|
|
63
|
-
platform,
|
|
64
|
-
version,
|
|
65
|
-
sha256,
|
|
66
|
-
size,
|
|
67
|
-
minNativeBuild,
|
|
68
|
-
signature.kid,
|
|
69
|
-
signature.iat,
|
|
70
|
-
signature.exp
|
|
71
|
-
);
|
|
72
|
-
|
|
73
103
|
// Decode base64url signature
|
|
74
104
|
byte[] sigBytes = base64UrlDecode(signature.sig);
|
|
75
105
|
|
|
@@ -94,12 +124,56 @@ final class ManifestVerifier {
|
|
|
94
124
|
String version,
|
|
95
125
|
String sha256,
|
|
96
126
|
int size,
|
|
97
|
-
|
|
127
|
+
String runtimeVersion,
|
|
128
|
+
String kid,
|
|
129
|
+
int iat,
|
|
130
|
+
int exp
|
|
131
|
+
) {
|
|
132
|
+
return (
|
|
133
|
+
"MANIFEST_V2\n" +
|
|
134
|
+
"appId:" +
|
|
135
|
+
appId +
|
|
136
|
+
"\n" +
|
|
137
|
+
"channel:" +
|
|
138
|
+
(channel != null ? channel : "null") +
|
|
139
|
+
"\n" +
|
|
140
|
+
"platform:" +
|
|
141
|
+
platform +
|
|
142
|
+
"\n" +
|
|
143
|
+
"version:" +
|
|
144
|
+
version +
|
|
145
|
+
"\n" +
|
|
146
|
+
"sha256:" +
|
|
147
|
+
sha256 +
|
|
148
|
+
"\n" +
|
|
149
|
+
"size:" +
|
|
150
|
+
size +
|
|
151
|
+
"\n" +
|
|
152
|
+
"runtimeVersion:" +
|
|
153
|
+
(runtimeVersion != null ? runtimeVersion : "null") +
|
|
154
|
+
"\n" +
|
|
155
|
+
"kid:" +
|
|
156
|
+
kid +
|
|
157
|
+
"\n" +
|
|
158
|
+
"iat:" +
|
|
159
|
+
iat +
|
|
160
|
+
"\n" +
|
|
161
|
+
"exp:" +
|
|
162
|
+
exp
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
private static String buildLegacyCanonicalPayload(
|
|
167
|
+
String appId,
|
|
168
|
+
String channel,
|
|
169
|
+
String platform,
|
|
170
|
+
String version,
|
|
171
|
+
String sha256,
|
|
172
|
+
int size,
|
|
98
173
|
String kid,
|
|
99
174
|
int iat,
|
|
100
175
|
int exp
|
|
101
176
|
) {
|
|
102
|
-
String minBuildStr = minNativeBuild != null ? String.valueOf(minNativeBuild) : "null";
|
|
103
177
|
return (
|
|
104
178
|
"MANIFEST_V1\n" +
|
|
105
179
|
"appId:" +
|
|
@@ -120,8 +194,7 @@ final class ManifestVerifier {
|
|
|
120
194
|
"size:" +
|
|
121
195
|
size +
|
|
122
196
|
"\n" +
|
|
123
|
-
"minNativeBuild:" +
|
|
124
|
-
minBuildStr +
|
|
197
|
+
"minNativeBuild:null" +
|
|
125
198
|
"\n" +
|
|
126
199
|
"kid:" +
|
|
127
200
|
kid +
|