@otakit/capacitor-updater 1.1.0 → 2.0.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 +39 -14
- package/android/src/main/java/com/updatekit/updater/BundleInfo.java +34 -2
- package/android/src/main/java/com/updatekit/updater/BundleStore.java +9 -2
- package/android/src/main/java/com/updatekit/updater/HostedManifestKeys.java +5 -6
- package/android/src/main/java/com/updatekit/updater/ManifestClient.java +44 -55
- package/android/src/main/java/com/updatekit/updater/ManifestVerifier.java +24 -25
- package/android/src/main/java/com/updatekit/updater/UpdaterPlugin.java +164 -74
- package/dist/esm/definitions.d.ts +11 -25
- package/dist/esm/definitions.d.ts.map +1 -1
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js +0 -2
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/web.d.ts +0 -4
- package/dist/esm/web.d.ts.map +1 -1
- package/dist/esm/web.js +0 -8
- package/dist/esm/web.js.map +1 -1
- package/dist/plugin.cjs.js +0 -10
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/plugin.js +0 -10
- package/dist/plugin.js.map +1 -1
- package/ios/Sources/UpdaterPlugin/BundleInfo.swift +5 -0
- package/ios/Sources/UpdaterPlugin/BundleStore.swift +3 -0
- package/ios/Sources/UpdaterPlugin/HostedManifestKeys.swift +5 -5
- package/ios/Sources/UpdaterPlugin/ManifestClient.swift +44 -45
- package/ios/Sources/UpdaterPlugin/ManifestVerifier.swift +25 -24
- package/ios/Sources/UpdaterPlugin/UpdaterPlugin.m +0 -2
- package/ios/Sources/UpdaterPlugin/UpdaterPlugin.swift +141 -67
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,15 +4,21 @@ Capacitor OTA updater plugin for OtaKit.
|
|
|
4
4
|
|
|
5
5
|
## What it does
|
|
6
6
|
|
|
7
|
-
-
|
|
7
|
+
- fetches the latest manifest for its release lane from the CDN
|
|
8
8
|
- downloads and verifies OTA bundles
|
|
9
9
|
- stages updates safely
|
|
10
10
|
- activates them on the next launch, next resume, or immediately
|
|
11
11
|
- checks for updates on cold start and app resume (configurable interval)
|
|
12
12
|
- also supports fully manual update prompts when the app wants control
|
|
13
|
+
- supports optional `runtimeVersion` lanes for native compatibility boundaries
|
|
13
14
|
- requires `notifyAppReady()` as the success handshake
|
|
14
15
|
- rolls back automatically if the new bundle does not prove healthy
|
|
15
16
|
|
|
17
|
+
OtaKit publishes signed static manifests into object storage behind a CDN. The
|
|
18
|
+
plugin fetches the manifest for its `appId + channel + runtimeVersion` lane,
|
|
19
|
+
verifies it, compares it against the current and staged bundle locally, and
|
|
20
|
+
only downloads when the manifest actually points at something newer.
|
|
21
|
+
|
|
16
22
|
For normal app code, the main public methods are:
|
|
17
23
|
|
|
18
24
|
```ts
|
|
@@ -46,6 +52,7 @@ plugins: {
|
|
|
46
52
|
appReadyTimeout: 10000,
|
|
47
53
|
// Optional:
|
|
48
54
|
// channel: "staging",
|
|
55
|
+
// runtimeVersion: "2026.04",
|
|
49
56
|
// updateMode: "next-resume",
|
|
50
57
|
// updateMode: "manual",
|
|
51
58
|
// updateMode: "immediate",
|
|
@@ -55,22 +62,40 @@ plugins: {
|
|
|
55
62
|
|
|
56
63
|
Advanced overrides for self-hosting or custom trust only:
|
|
57
64
|
|
|
58
|
-
- `
|
|
65
|
+
- `cdnUrl` for manifest and bundle delivery
|
|
66
|
+
- `serverUrl` for stats and control-plane requests
|
|
59
67
|
- `manifestKeys`
|
|
60
68
|
- `allowInsecureUrls`
|
|
61
69
|
|
|
62
|
-
Hosted OtaKit already points at
|
|
63
|
-
the managed manifest signing keys.
|
|
70
|
+
Hosted OtaKit already points at the managed control-plane API and CDN and already
|
|
71
|
+
trusts the managed manifest signing keys.
|
|
72
|
+
|
|
73
|
+
## Channels vs runtimeVersion
|
|
74
|
+
|
|
75
|
+
- `channel` answers "who should get this rollout?"
|
|
76
|
+
- `runtimeVersion` answers "which native app shell can safely run this bundle?"
|
|
77
|
+
|
|
78
|
+
Use channels for rollout tracks such as `beta`, `staging`, or `production`.
|
|
79
|
+
|
|
80
|
+
Use `runtimeVersion` when a new store build creates a new compatibility boundary and you do not
|
|
81
|
+
want devices on that new native shell to keep receiving older OTA bundles.
|
|
82
|
+
|
|
83
|
+
When `runtimeVersion` is set:
|
|
84
|
+
|
|
85
|
+
- the plugin requests bundle updates only for that runtime lane
|
|
86
|
+
- bundle uploads inherit the same runtime value automatically through the CLI
|
|
87
|
+
- releases stay simple: publish the bundle, and it naturally stays inside its own runtime lane
|
|
64
88
|
|
|
65
89
|
## Trust model
|
|
66
90
|
|
|
67
91
|
The plugin does not just download from a URL and trust the result.
|
|
68
92
|
|
|
69
|
-
1. it fetches
|
|
93
|
+
1. it fetches the latest manifest from the CDN for its app + channel + runtimeVersion lane
|
|
70
94
|
2. it verifies the manifest signature when manifest keys are configured
|
|
71
|
-
3. it
|
|
72
|
-
4.
|
|
73
|
-
5. it
|
|
95
|
+
3. it compares that manifest against the current and staged bundle already on the device
|
|
96
|
+
4. if the manifest is newer, it downloads the bundle zip
|
|
97
|
+
5. it verifies the zip against the manifest `sha256`
|
|
98
|
+
6. it stages and activates the bundle
|
|
74
99
|
|
|
75
100
|
In the hosted path, managed signing keys are already built in.
|
|
76
101
|
|
|
@@ -147,8 +172,9 @@ await OtaKit.notifyAppReady();
|
|
|
147
172
|
```
|
|
148
173
|
|
|
149
174
|
The plugin handles checking, downloading, activation, and rollback based on
|
|
150
|
-
`updateMode`.
|
|
151
|
-
the background
|
|
175
|
+
`updateMode`. In `next-launch` and `next-resume`, it checks on cold start and
|
|
176
|
+
every time the app comes back from the background, throttled by `checkInterval`.
|
|
177
|
+
`immediate` bypasses that throttle.
|
|
152
178
|
|
|
153
179
|
For most apps, this is the entire runtime integration.
|
|
154
180
|
|
|
@@ -177,10 +203,9 @@ only after explicit user confirmation.
|
|
|
177
203
|
|
|
178
204
|
## Throttle
|
|
179
205
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
null.
|
|
206
|
+
`checkInterval` (default 10 min) only applies to automatic checks in
|
|
207
|
+
`next-launch` and `next-resume`. Manual `check()` / `download()` calls are
|
|
208
|
+
always live, and `immediate` mode ignores the interval entirely.
|
|
184
209
|
|
|
185
210
|
## Retention and deletion
|
|
186
211
|
|
|
@@ -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
|
|
@@ -56,6 +58,7 @@ final class BundleStore {
|
|
|
56
58
|
return new BundleInfo(
|
|
57
59
|
"builtin",
|
|
58
60
|
builtinVersion,
|
|
61
|
+
appRuntimeVersion,
|
|
59
62
|
BundleStatus.BUILTIN,
|
|
60
63
|
null,
|
|
61
64
|
null,
|
|
@@ -229,7 +232,7 @@ final class BundleStore {
|
|
|
229
232
|
}
|
|
230
233
|
}
|
|
231
234
|
|
|
232
|
-
synchronized
|
|
235
|
+
synchronized List<BundleInfo> listDownloadedBundleInfos() {
|
|
233
236
|
List<BundleInfo> result = new ArrayList<>();
|
|
234
237
|
File[] entries = bundlesDirectory.listFiles();
|
|
235
238
|
if (entries != null) {
|
|
@@ -241,7 +244,11 @@ final class BundleStore {
|
|
|
241
244
|
}
|
|
242
245
|
}
|
|
243
246
|
}
|
|
247
|
+
return result;
|
|
248
|
+
}
|
|
244
249
|
|
|
250
|
+
synchronized JSArray listDownloadedBundles() {
|
|
251
|
+
List<BundleInfo> result = listDownloadedBundleInfos();
|
|
245
252
|
JSArray array = new JSArray();
|
|
246
253
|
for (BundleInfo bundle : result) {
|
|
247
254
|
array.put(bundle.toJSObject());
|
|
@@ -7,18 +7,17 @@ import java.util.Locale;
|
|
|
7
7
|
|
|
8
8
|
final class HostedManifestKeys {
|
|
9
9
|
|
|
10
|
-
private static final String
|
|
10
|
+
private static final String MANAGED_CDN_URL = "https://cdn.otakit.app";
|
|
11
11
|
|
|
12
12
|
private HostedManifestKeys() {}
|
|
13
13
|
|
|
14
|
-
static boolean
|
|
15
|
-
if (
|
|
14
|
+
static boolean matchesManagedManifestUrl(String cdnUrl) {
|
|
15
|
+
if (cdnUrl == null) {
|
|
16
16
|
return false;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
String normalized =
|
|
20
|
-
return normalized.equals(
|
|
21
|
-
|| normalized.equals("https://otakit.app/api/v1");
|
|
19
|
+
String normalized = cdnUrl.trim().replaceAll("/+$", "").toLowerCase(Locale.ROOT);
|
|
20
|
+
return normalized.equals(MANAGED_CDN_URL) || normalized.equals("https://www.otakit.app");
|
|
22
21
|
}
|
|
23
22
|
|
|
24
23
|
static List<ManifestVerifier.KeyEntry> createDefaultKeys() {
|
|
@@ -9,6 +9,9 @@ import org.json.JSONObject;
|
|
|
9
9
|
|
|
10
10
|
final class ManifestClient {
|
|
11
11
|
|
|
12
|
+
private static final String BASE_CHANNEL_KEY = "__base__";
|
|
13
|
+
private static final String DEFAULT_RUNTIME_KEY = "__default__";
|
|
14
|
+
|
|
12
15
|
static final class ManifestSignature {
|
|
13
16
|
|
|
14
17
|
final String kid;
|
|
@@ -30,26 +33,23 @@ final class ManifestClient {
|
|
|
30
33
|
final String url;
|
|
31
34
|
final String sha256;
|
|
32
35
|
final int size;
|
|
33
|
-
final
|
|
36
|
+
final String runtimeVersion;
|
|
34
37
|
final String releaseId;
|
|
35
|
-
final ManifestSignature signature;
|
|
36
38
|
|
|
37
39
|
LatestManifest(
|
|
38
40
|
String version,
|
|
39
41
|
String url,
|
|
40
42
|
String sha256,
|
|
41
43
|
int size,
|
|
42
|
-
|
|
43
|
-
String releaseId
|
|
44
|
-
ManifestSignature signature
|
|
44
|
+
String runtimeVersion,
|
|
45
|
+
String releaseId
|
|
45
46
|
) {
|
|
46
47
|
this.version = version;
|
|
47
48
|
this.url = url;
|
|
48
49
|
this.sha256 = sha256;
|
|
49
50
|
this.size = size;
|
|
50
|
-
this.
|
|
51
|
+
this.runtimeVersion = runtimeVersion;
|
|
51
52
|
this.releaseId = releaseId;
|
|
52
|
-
this.signature = signature;
|
|
53
53
|
}
|
|
54
54
|
}
|
|
55
55
|
|
|
@@ -66,39 +66,33 @@ final class ManifestClient {
|
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
static LatestManifest fetchLatest(
|
|
69
|
-
String
|
|
69
|
+
String cdnUrl,
|
|
70
70
|
String appId,
|
|
71
71
|
String channel,
|
|
72
|
-
String
|
|
73
|
-
String currentReleaseId,
|
|
74
|
-
String nativeBuild,
|
|
75
|
-
String platform,
|
|
72
|
+
String runtimeVersion,
|
|
76
73
|
boolean allowInsecureUrls,
|
|
77
74
|
java.util.List<ManifestVerifier.KeyEntry> manifestKeys
|
|
78
75
|
) throws Exception {
|
|
79
|
-
String base =
|
|
80
|
-
|
|
76
|
+
String base = cdnUrl.replaceAll("/+$", "");
|
|
77
|
+
String channelKey = channel != null && !channel.trim().isEmpty() ? channel.trim() : BASE_CHANNEL_KEY;
|
|
78
|
+
String runtimeKey =
|
|
79
|
+
runtimeVersion != null && !runtimeVersion.trim().isEmpty()
|
|
80
|
+
? runtimeVersion.trim()
|
|
81
|
+
: DEFAULT_RUNTIME_KEY;
|
|
82
|
+
URL url = new URL(
|
|
83
|
+
base + "/manifests/" + appId + "/" + channelKey + "/" + runtimeKey + "/manifest.json"
|
|
84
|
+
);
|
|
81
85
|
|
|
82
86
|
requireHTTPS(url, allowInsecureUrls);
|
|
83
87
|
|
|
84
88
|
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
|
|
85
89
|
try {
|
|
86
90
|
connection.setRequestMethod("GET");
|
|
87
|
-
connection.setRequestProperty("X-App-Id", appId);
|
|
88
|
-
connection.setRequestProperty("X-Platform", platform);
|
|
89
|
-
if (channel != null && !channel.trim().isEmpty()) {
|
|
90
|
-
connection.setRequestProperty("X-Channel", channel);
|
|
91
|
-
}
|
|
92
|
-
connection.setRequestProperty("X-Current-Version", currentVersion);
|
|
93
|
-
if (currentReleaseId != null && !currentReleaseId.trim().isEmpty()) {
|
|
94
|
-
connection.setRequestProperty("X-Release-Id", currentReleaseId);
|
|
95
|
-
}
|
|
96
|
-
connection.setRequestProperty("X-Native-Build", nativeBuild);
|
|
97
91
|
connection.setConnectTimeout(15_000);
|
|
98
92
|
connection.setReadTimeout(30_000);
|
|
99
93
|
|
|
100
94
|
int status = connection.getResponseCode();
|
|
101
|
-
if (status == 204) {
|
|
95
|
+
if (status == 404 || status == 204) {
|
|
102
96
|
return null;
|
|
103
97
|
}
|
|
104
98
|
if (status != 200) {
|
|
@@ -118,38 +112,20 @@ final class ManifestClient {
|
|
|
118
112
|
String sha256 = json.getString("sha256");
|
|
119
113
|
int size = json.getInt("size");
|
|
120
114
|
|
|
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
|
-
}
|
|
115
|
+
String responseRuntimeVersion = json.has("runtimeVersion") && !json.isNull("runtimeVersion")
|
|
116
|
+
? json.getString("runtimeVersion").trim()
|
|
117
|
+
: null;
|
|
118
|
+
if (responseRuntimeVersion != null && responseRuntimeVersion.isEmpty()) {
|
|
119
|
+
responseRuntimeVersion = null;
|
|
132
120
|
}
|
|
133
121
|
|
|
134
|
-
ManifestSignature signature =
|
|
135
|
-
if (json.has("signature") && !json.isNull("signature")) {
|
|
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
|
-
}
|
|
122
|
+
ManifestSignature signature = parseSignature(json.optJSONObject("signature"));
|
|
146
123
|
|
|
147
124
|
String releaseId = null;
|
|
148
125
|
if (json.has("releaseId") && !json.isNull("releaseId")) {
|
|
149
126
|
releaseId = json.getString("releaseId");
|
|
150
127
|
}
|
|
151
128
|
|
|
152
|
-
// Validate download URL scheme
|
|
153
129
|
requireHTTPS(new URL(downloadUrl), allowInsecureUrls);
|
|
154
130
|
|
|
155
131
|
if (manifestKeys == null || manifestKeys.isEmpty()) {
|
|
@@ -159,21 +135,20 @@ final class ManifestClient {
|
|
|
159
135
|
);
|
|
160
136
|
}
|
|
161
137
|
|
|
162
|
-
// Verify manifest signature if signing keys are configured
|
|
163
138
|
if (manifestKeys != null && !manifestKeys.isEmpty()) {
|
|
164
139
|
if (signature == null) {
|
|
165
140
|
throw new IllegalStateException(
|
|
166
141
|
"Manifest signature missing but signing keys are configured"
|
|
167
142
|
);
|
|
168
143
|
}
|
|
144
|
+
|
|
169
145
|
ManifestVerifier.verify(
|
|
170
146
|
appId,
|
|
171
147
|
channel,
|
|
172
|
-
platform,
|
|
173
148
|
version,
|
|
174
149
|
sha256,
|
|
175
150
|
size,
|
|
176
|
-
|
|
151
|
+
responseRuntimeVersion,
|
|
177
152
|
signature,
|
|
178
153
|
manifestKeys
|
|
179
154
|
);
|
|
@@ -184,9 +159,8 @@ final class ManifestClient {
|
|
|
184
159
|
downloadUrl,
|
|
185
160
|
sha256,
|
|
186
161
|
size,
|
|
187
|
-
|
|
188
|
-
releaseId
|
|
189
|
-
signature
|
|
162
|
+
responseRuntimeVersion,
|
|
163
|
+
releaseId
|
|
190
164
|
);
|
|
191
165
|
} finally {
|
|
192
166
|
connection.disconnect();
|
|
@@ -206,4 +180,19 @@ final class ManifestClient {
|
|
|
206
180
|
return new String(out.toByteArray(), StandardCharsets.UTF_8);
|
|
207
181
|
}
|
|
208
182
|
}
|
|
183
|
+
|
|
184
|
+
private static ManifestSignature parseSignature(JSONObject sigObj) {
|
|
185
|
+
if (sigObj == null) {
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
if (!sigObj.has("kid") || !sigObj.has("sig") || !sigObj.has("iat") || !sigObj.has("exp")) {
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
return new ManifestSignature(
|
|
192
|
+
sigObj.getString("kid"),
|
|
193
|
+
sigObj.getString("sig"),
|
|
194
|
+
sigObj.getInt("iat"),
|
|
195
|
+
sigObj.getInt("exp")
|
|
196
|
+
);
|
|
197
|
+
}
|
|
209
198
|
}
|
|
@@ -30,11 +30,29 @@ final class ManifestVerifier {
|
|
|
30
30
|
static void verify(
|
|
31
31
|
String appId,
|
|
32
32
|
String channel,
|
|
33
|
-
String platform,
|
|
34
33
|
String version,
|
|
35
34
|
String sha256,
|
|
36
35
|
int size,
|
|
37
|
-
|
|
36
|
+
String runtimeVersion,
|
|
37
|
+
ManifestClient.ManifestSignature signature,
|
|
38
|
+
List<KeyEntry> trustedKeys
|
|
39
|
+
) throws Exception {
|
|
40
|
+
String payload = buildCanonicalPayload(
|
|
41
|
+
appId,
|
|
42
|
+
channel,
|
|
43
|
+
version,
|
|
44
|
+
sha256,
|
|
45
|
+
size,
|
|
46
|
+
runtimeVersion,
|
|
47
|
+
signature.kid,
|
|
48
|
+
signature.iat,
|
|
49
|
+
signature.exp
|
|
50
|
+
);
|
|
51
|
+
verifyPayload(payload, signature, trustedKeys);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
private static void verifyPayload(
|
|
55
|
+
String payload,
|
|
38
56
|
ManifestClient.ManifestSignature signature,
|
|
39
57
|
List<KeyEntry> trustedKeys
|
|
40
58
|
) throws Exception {
|
|
@@ -56,20 +74,6 @@ final class ManifestVerifier {
|
|
|
56
74
|
throw new IllegalStateException("Unknown signing key ID: " + signature.kid);
|
|
57
75
|
}
|
|
58
76
|
|
|
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
77
|
// Decode base64url signature
|
|
74
78
|
byte[] sigBytes = base64UrlDecode(signature.sig);
|
|
75
79
|
|
|
@@ -90,27 +94,22 @@ final class ManifestVerifier {
|
|
|
90
94
|
private static String buildCanonicalPayload(
|
|
91
95
|
String appId,
|
|
92
96
|
String channel,
|
|
93
|
-
String platform,
|
|
94
97
|
String version,
|
|
95
98
|
String sha256,
|
|
96
99
|
int size,
|
|
97
|
-
|
|
100
|
+
String runtimeVersion,
|
|
98
101
|
String kid,
|
|
99
102
|
int iat,
|
|
100
103
|
int exp
|
|
101
104
|
) {
|
|
102
|
-
String minBuildStr = minNativeBuild != null ? String.valueOf(minNativeBuild) : "null";
|
|
103
105
|
return (
|
|
104
|
-
"
|
|
106
|
+
"MANIFEST\n" +
|
|
105
107
|
"appId:" +
|
|
106
108
|
appId +
|
|
107
109
|
"\n" +
|
|
108
110
|
"channel:" +
|
|
109
111
|
(channel != null ? channel : "null") +
|
|
110
112
|
"\n" +
|
|
111
|
-
"platform:" +
|
|
112
|
-
platform +
|
|
113
|
-
"\n" +
|
|
114
113
|
"version:" +
|
|
115
114
|
version +
|
|
116
115
|
"\n" +
|
|
@@ -120,8 +119,8 @@ final class ManifestVerifier {
|
|
|
120
119
|
"size:" +
|
|
121
120
|
size +
|
|
122
121
|
"\n" +
|
|
123
|
-
"
|
|
124
|
-
|
|
122
|
+
"runtimeVersion:" +
|
|
123
|
+
(runtimeVersion != null ? runtimeVersion : "null") +
|
|
125
124
|
"\n" +
|
|
126
125
|
"kid:" +
|
|
127
126
|
kid +
|