@onekeyfe/hd-core 1.2.0-alpha.96 → 1.2.0-alpha.98
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/__tests__/firmware-update/device-update-bootloader.test.ts +8 -1
- package/__tests__/firmware-update/firmware-prepared-host-digest.test.ts +106 -2
- package/__tests__/firmware-update/firmware-prepared-resource-archive.test.ts +150 -0
- package/__tests__/firmware-update/firmware-update-bootloader-poll.test.ts +112 -0
- package/__tests__/firmware-update/firmware-update-plan.test.ts +101 -0
- package/__tests__/firmware-update/firmware-update-prepared-resource-executors.test.ts +326 -0
- package/__tests__/firmware-update/firmware-update-v2-download-before-boot.test.ts +2 -2
- package/__tests__/protocol-v2.test.ts +131 -0
- package/dist/api/FirmwareUpdateV2.d.ts.map +1 -1
- package/dist/api/FirmwareUpdateV3.d.ts.map +1 -1
- package/dist/api/FirmwareUpdateV4.d.ts +2 -0
- package/dist/api/FirmwareUpdateV4.d.ts.map +1 -1
- package/dist/api/device/DeviceUpdateBootloader.d.ts.map +1 -1
- package/dist/api/firmware/FirmwarePreparedResourceArchive.d.ts +11 -0
- package/dist/api/firmware/FirmwarePreparedResourceArchive.d.ts.map +1 -0
- package/dist/api/firmware/FirmwareUpdateBaseMethod.d.ts.map +1 -1
- package/dist/api/firmware/FirmwareUpdatePlan.d.ts.map +1 -1
- package/dist/api/firmware/FirmwareUpdatePreparedPlan.d.ts +6 -1
- package/dist/api/firmware/FirmwareUpdatePreparedPlan.d.ts.map +1 -1
- package/dist/index.d.ts +5 -3
- package/dist/index.js +359 -157
- package/dist/types/api/deviceUpdateBootloader.d.ts.map +1 -1
- package/dist/types/api/firmwareUpdate.d.ts +1 -1
- package/dist/types/api/firmwareUpdate.d.ts.map +1 -1
- package/dist/types/api/firmwareUpdatePlan.d.ts +2 -2
- package/dist/types/api/firmwareUpdatePlan.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/api/FirmwareUpdateV2.ts +95 -72
- package/src/api/FirmwareUpdateV3.ts +74 -51
- package/src/api/FirmwareUpdateV4.ts +72 -7
- package/src/api/device/DeviceUpdateBootloader.ts +17 -12
- package/src/api/firmware/FirmwarePreparedResourceArchive.ts +207 -0
- package/src/api/firmware/FirmwareUpdateBaseMethod.ts +2 -0
- package/src/api/firmware/FirmwareUpdatePlan.ts +70 -48
- package/src/api/firmware/FirmwareUpdatePreparedPlan.ts +17 -0
- package/src/types/api/deviceUpdateBootloader.ts +1 -0
- package/src/types/api/firmwareUpdate.ts +2 -1
- package/src/types/api/firmwareUpdatePlan.ts +2 -2
|
@@ -14,6 +14,7 @@ import { resolveFirmwareUpdateHostBinding } from '../firmware/FirmwareHostBindin
|
|
|
14
14
|
import {
|
|
15
15
|
assertFirmwareUpdatePreparedPlanBinding,
|
|
16
16
|
assertFirmwareUpdatePreparedPlanDeviceIdentity,
|
|
17
|
+
getFirmwareUpdatePreparedRawArtifact,
|
|
17
18
|
validateFirmwareUpdatePreparedPlan,
|
|
18
19
|
} from '../firmware/FirmwareUpdatePreparedPlan';
|
|
19
20
|
import { getDeviceType, getDeviceUUID } from '../../utils';
|
|
@@ -159,22 +160,28 @@ export default class DeviceUpdateBootloader extends FirmwareUpdateBaseMethod<any
|
|
|
159
160
|
|
|
160
161
|
const payload = this.payload as DeviceUpdateBootloaderParams;
|
|
161
162
|
const hasPreparedPlan = payload.preparedPlan !== undefined;
|
|
162
|
-
|
|
163
|
-
if (hasPreparedPlan !== hasPreparedArtifact) {
|
|
163
|
+
if (hasPreparedPlan && payload.binary !== undefined) {
|
|
164
164
|
throw ERRORS.TypedError(
|
|
165
165
|
HardwareErrorCode.CallMethodInvalidParameter,
|
|
166
|
-
'Prepared bootloader plans
|
|
166
|
+
'Prepared bootloader plans cannot be combined with a legacy binary'
|
|
167
167
|
);
|
|
168
168
|
}
|
|
169
|
-
if (payload.
|
|
169
|
+
if (!hasPreparedPlan && payload.artifact !== undefined) {
|
|
170
170
|
throw ERRORS.TypedError(
|
|
171
171
|
HardwareErrorCode.CallMethodInvalidParameter,
|
|
172
|
-
'Bootloader
|
|
172
|
+
'Bootloader artifacts require a prepared plan'
|
|
173
173
|
);
|
|
174
174
|
}
|
|
175
|
-
const preparedPlan =
|
|
175
|
+
const preparedPlan = hasPreparedPlan
|
|
176
176
|
? validateFirmwareUpdatePreparedPlan(payload.preparedPlan)
|
|
177
177
|
: undefined;
|
|
178
|
+
const plannedArtifact = preparedPlan
|
|
179
|
+
? getFirmwareUpdatePreparedRawArtifact({
|
|
180
|
+
preparedPlan,
|
|
181
|
+
target: 'bootloader',
|
|
182
|
+
role: 'bootloader',
|
|
183
|
+
}).artifact
|
|
184
|
+
: undefined;
|
|
178
185
|
const artifactReader = preparedPlan
|
|
179
186
|
? resolveFirmwareUpdateHostBinding(
|
|
180
187
|
payload.hostBindingGeneration,
|
|
@@ -185,7 +192,7 @@ export default class DeviceUpdateBootloader extends FirmwareUpdateBaseMethod<any
|
|
|
185
192
|
...payload,
|
|
186
193
|
artifactReader,
|
|
187
194
|
};
|
|
188
|
-
if (
|
|
195
|
+
if (preparedPlan && plannedArtifact) {
|
|
189
196
|
assertFirmwareUpdatePreparedPlanDeviceIdentity({
|
|
190
197
|
preparedPlan,
|
|
191
198
|
deviceIdentity: getDeviceUUID(features) || undefined,
|
|
@@ -199,12 +206,12 @@ export default class DeviceUpdateBootloader extends FirmwareUpdateBaseMethod<any
|
|
|
199
206
|
bindings: [
|
|
200
207
|
{
|
|
201
208
|
target: 'bootloader',
|
|
202
|
-
artifact: payload.artifact,
|
|
209
|
+
artifact: payload.artifact ?? plannedArtifact,
|
|
203
210
|
},
|
|
204
211
|
],
|
|
205
212
|
});
|
|
206
213
|
const source = await openFirmwareByteSource({
|
|
207
|
-
artifact:
|
|
214
|
+
artifact: plannedArtifact,
|
|
208
215
|
reader: executionParams.artifactReader,
|
|
209
216
|
});
|
|
210
217
|
if (!source) {
|
|
@@ -215,13 +222,11 @@ export default class DeviceUpdateBootloader extends FirmwareUpdateBaseMethod<any
|
|
|
215
222
|
}
|
|
216
223
|
try {
|
|
217
224
|
const deviceType = device.getCurrentDeviceType();
|
|
218
|
-
const deviceFirmwareType = device.getCurrentFirmwareType();
|
|
219
|
-
const firmwareType = payload.firmwareType ?? deviceFirmwareType;
|
|
220
225
|
if (DeviceModelToTypes.model_touch.includes(deviceType)) {
|
|
221
226
|
return await this.updateTouchBootloader({
|
|
222
227
|
device,
|
|
223
228
|
features,
|
|
224
|
-
firmwareType,
|
|
229
|
+
firmwareType: preparedPlan.firmwareType,
|
|
225
230
|
source,
|
|
226
231
|
});
|
|
227
232
|
}
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { sha256 } from '@noble/hashes/sha256';
|
|
2
|
+
import { bytesToHex } from '@noble/hashes/utils';
|
|
3
|
+
import { ERRORS, HardwareErrorCode } from '@onekeyfe/hd-shared';
|
|
4
|
+
import JSZip from 'jszip';
|
|
5
|
+
|
|
6
|
+
import { openFirmwareByteSource, readFirmwareByteSourceFully } from './FirmwareArtifactSource';
|
|
7
|
+
import { getFirmwareUpdateResourceName } from './FirmwareUpdatePreparedPlan';
|
|
8
|
+
|
|
9
|
+
import type { FirmwareArtifactReader } from '../../types/api/firmwareUpdate';
|
|
10
|
+
import type { FirmwareUpdatePreparedPlan } from '../../types/api/firmwareUpdatePreparedPlan';
|
|
11
|
+
|
|
12
|
+
const PREPARED_RESOURCE_ARCHIVE_MAX_BYTES = 256 * 1024 * 1024;
|
|
13
|
+
const PREPARED_RESOURCE_ENTRY_MAX_COUNT = 512;
|
|
14
|
+
|
|
15
|
+
type JSZipSizedEntry = JSZip.JSZipObject & {
|
|
16
|
+
_data?: {
|
|
17
|
+
compressedSize?: unknown;
|
|
18
|
+
uncompressedSize?: unknown;
|
|
19
|
+
};
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export type VerifiedPreparedResourceEntry = {
|
|
23
|
+
entryName: string;
|
|
24
|
+
binary: ArrayBuffer;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const resourceArchiveError = (
|
|
28
|
+
message: string,
|
|
29
|
+
firmwareUpdateCode: 'FirmwareArtifactsNotPrepared' | 'FirmwareArtifactReceiptMismatch'
|
|
30
|
+
): never => {
|
|
31
|
+
throw ERRORS.TypedError(HardwareErrorCode.RuntimeError, message, { firmwareUpdateCode });
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const getZipEntrySizes = (entry: JSZip.JSZipObject) => {
|
|
35
|
+
const { compressedSize, uncompressedSize } = (entry as JSZipSizedEntry)._data ?? {};
|
|
36
|
+
if (
|
|
37
|
+
!Number.isSafeInteger(compressedSize) ||
|
|
38
|
+
Number(compressedSize) < 0 ||
|
|
39
|
+
!Number.isSafeInteger(uncompressedSize) ||
|
|
40
|
+
Number(uncompressedSize) <= 0
|
|
41
|
+
) {
|
|
42
|
+
return resourceArchiveError(
|
|
43
|
+
`Firmware prepared resource ZIP entry size is invalid: ${entry.name}`,
|
|
44
|
+
'FirmwareArtifactsNotPrepared'
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
compressedSize: Number(compressedSize),
|
|
49
|
+
uncompressedSize: Number(uncompressedSize),
|
|
50
|
+
};
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* 从 PreparedPlan 批准的 ZIP 本体生成资源条目。
|
|
55
|
+
* 宿主提供的 materializedEntries 仅作为 receipt,设备写入始终使用 ZIP 内的规范字节。
|
|
56
|
+
*/
|
|
57
|
+
export const readVerifiedPreparedResourceArchive = async ({
|
|
58
|
+
preparedPlan,
|
|
59
|
+
reader,
|
|
60
|
+
}: {
|
|
61
|
+
preparedPlan: FirmwareUpdatePreparedPlan;
|
|
62
|
+
reader: FirmwareArtifactReader | undefined;
|
|
63
|
+
}): Promise<VerifiedPreparedResourceEntry[]> => {
|
|
64
|
+
const resourceArtifacts = preparedPlan.artifacts.filter(
|
|
65
|
+
artifact => artifact.target === 'resource'
|
|
66
|
+
);
|
|
67
|
+
if (!preparedPlan.targetsToUpdate.includes('resource')) {
|
|
68
|
+
if (resourceArtifacts.length > 0) {
|
|
69
|
+
return resourceArchiveError(
|
|
70
|
+
'Firmware prepared resource artifact is outside the approved targets',
|
|
71
|
+
'FirmwareArtifactsNotPrepared'
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
return [];
|
|
75
|
+
}
|
|
76
|
+
if (resourceArtifacts.length !== 1) {
|
|
77
|
+
return resourceArchiveError(
|
|
78
|
+
'Firmware prepared plan must contain exactly one materialized resource ZIP',
|
|
79
|
+
'FirmwareArtifactsNotPrepared'
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const archiveArtifact = resourceArtifacts[0];
|
|
84
|
+
const preparedEntries = archiveArtifact.materializedEntries;
|
|
85
|
+
if (
|
|
86
|
+
archiveArtifact.role !== 'resource' ||
|
|
87
|
+
archiveArtifact.container !== 'zip' ||
|
|
88
|
+
!preparedEntries?.length
|
|
89
|
+
) {
|
|
90
|
+
return resourceArchiveError(
|
|
91
|
+
'Firmware prepared plan must contain exactly one materialized resource ZIP',
|
|
92
|
+
'FirmwareArtifactsNotPrepared'
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
const archiveSource = await openFirmwareByteSource({
|
|
96
|
+
artifact: archiveArtifact.artifact,
|
|
97
|
+
reader,
|
|
98
|
+
});
|
|
99
|
+
if (!archiveSource) {
|
|
100
|
+
return resourceArchiveError(
|
|
101
|
+
'Firmware prepared resource ZIP is unavailable',
|
|
102
|
+
'FirmwareArtifactsNotPrepared'
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
if (archiveSource.size > PREPARED_RESOURCE_ARCHIVE_MAX_BYTES) {
|
|
106
|
+
await archiveSource.close().catch(() => undefined);
|
|
107
|
+
return resourceArchiveError(
|
|
108
|
+
'Firmware prepared resource ZIP exceeds the archive size limit',
|
|
109
|
+
'FirmwareArtifactsNotPrepared'
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
let archiveBinary: ArrayBuffer;
|
|
114
|
+
try {
|
|
115
|
+
archiveBinary = await readFirmwareByteSourceFully(archiveSource);
|
|
116
|
+
} finally {
|
|
117
|
+
await archiveSource.close().catch(() => undefined);
|
|
118
|
+
}
|
|
119
|
+
const archiveDigest = bytesToHex(sha256(new Uint8Array(archiveBinary)));
|
|
120
|
+
if (archiveDigest !== archiveArtifact.artifact.sha256.toLowerCase()) {
|
|
121
|
+
return resourceArchiveError(
|
|
122
|
+
'Firmware prepared resource ZIP does not match its approved receipt',
|
|
123
|
+
'FirmwareArtifactReceiptMismatch'
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
let zip: JSZip;
|
|
128
|
+
try {
|
|
129
|
+
zip = await JSZip.loadAsync(archiveBinary);
|
|
130
|
+
} catch {
|
|
131
|
+
return resourceArchiveError(
|
|
132
|
+
'Firmware prepared resource ZIP cannot be parsed',
|
|
133
|
+
'FirmwareArtifactsNotPrepared'
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
const zipEntries = Object.values(zip.files);
|
|
137
|
+
if (
|
|
138
|
+
zipEntries.some(entry => entry.unsafeOriginalName && entry.unsafeOriginalName !== entry.name)
|
|
139
|
+
) {
|
|
140
|
+
return resourceArchiveError(
|
|
141
|
+
'Firmware prepared resource ZIP contains an unsafe entry path',
|
|
142
|
+
'FirmwareArtifactsNotPrepared'
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
const files = zipEntries.filter(entry => !entry.dir);
|
|
146
|
+
if (files.length === 0 || files.length > PREPARED_RESOURCE_ENTRY_MAX_COUNT) {
|
|
147
|
+
return resourceArchiveError(
|
|
148
|
+
'Firmware prepared resource ZIP entry set is invalid',
|
|
149
|
+
'FirmwareArtifactsNotPrepared'
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
let totalSize = 0;
|
|
154
|
+
const canonicalNames = new Set<string>();
|
|
155
|
+
for (const entry of files) {
|
|
156
|
+
const { compressedSize, uncompressedSize } = getZipEntrySizes(entry);
|
|
157
|
+
const resourceName = getFirmwareUpdateResourceName(entry.name);
|
|
158
|
+
const canonicalName = resourceName.toLowerCase();
|
|
159
|
+
totalSize += uncompressedSize;
|
|
160
|
+
if (
|
|
161
|
+
compressedSize > archiveBinary.byteLength ||
|
|
162
|
+
uncompressedSize > PREPARED_RESOURCE_ARCHIVE_MAX_BYTES ||
|
|
163
|
+
totalSize > PREPARED_RESOURCE_ARCHIVE_MAX_BYTES ||
|
|
164
|
+
canonicalNames.has(canonicalName)
|
|
165
|
+
) {
|
|
166
|
+
return resourceArchiveError(
|
|
167
|
+
`Firmware prepared resource ZIP entry bounds are invalid: ${entry.name}`,
|
|
168
|
+
'FirmwareArtifactsNotPrepared'
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
canonicalNames.add(canonicalName);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const preparedEntriesByName = new Map(
|
|
175
|
+
preparedEntries.map(entry => [entry.entryName, entry] as const)
|
|
176
|
+
);
|
|
177
|
+
if (preparedEntriesByName.size !== files.length || preparedEntries.length !== files.length) {
|
|
178
|
+
return resourceArchiveError(
|
|
179
|
+
'Firmware prepared resource entries do not match the approved ZIP',
|
|
180
|
+
'FirmwareArtifactReceiptMismatch'
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const verifiedEntries: VerifiedPreparedResourceEntry[] = [];
|
|
185
|
+
for (const entry of files) {
|
|
186
|
+
const { uncompressedSize } = getZipEntrySizes(entry);
|
|
187
|
+
const binary = await entry.async('arraybuffer');
|
|
188
|
+
const preparedEntry = preparedEntriesByName.get(entry.name);
|
|
189
|
+
const digest = bytesToHex(sha256(new Uint8Array(binary)));
|
|
190
|
+
if (
|
|
191
|
+
binary.byteLength !== uncompressedSize ||
|
|
192
|
+
!preparedEntry ||
|
|
193
|
+
preparedEntry.artifact.size !== binary.byteLength ||
|
|
194
|
+
preparedEntry.artifact.sha256.toLowerCase() !== digest
|
|
195
|
+
) {
|
|
196
|
+
return resourceArchiveError(
|
|
197
|
+
`Firmware prepared resource entry does not match the approved ZIP: ${entry.name}`,
|
|
198
|
+
'FirmwareArtifactReceiptMismatch'
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
verifiedEntries.push({
|
|
202
|
+
entryName: getFirmwareUpdateResourceName(entry.name),
|
|
203
|
+
binary,
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
return verifiedEntries;
|
|
207
|
+
};
|
|
@@ -191,6 +191,8 @@ export class FirmwareUpdateBaseMethod<Params> extends BaseMethod<Params> {
|
|
|
191
191
|
!hasPromptedWebDevice &&
|
|
192
192
|
!isPromptingWebDevice
|
|
193
193
|
) {
|
|
194
|
+
clearInterval(intervalTimer);
|
|
195
|
+
clearTimeout(timeoutTimer);
|
|
194
196
|
isPromptingWebDevice = true;
|
|
195
197
|
try {
|
|
196
198
|
this.postTipMessage(FirmwareUpdateTipMessage.SelectDeviceInBootloaderForWebDevice);
|
|
@@ -95,7 +95,7 @@ const asIntegrity = ({
|
|
|
95
95
|
}: {
|
|
96
96
|
size: unknown;
|
|
97
97
|
sha256: unknown;
|
|
98
|
-
}): Pick<FirmwareUpdatePlanArtifact, 'expectedSize' | 'expectedSha256'
|
|
98
|
+
}): Partial<Pick<FirmwareUpdatePlanArtifact, 'expectedSize' | 'expectedSha256'>> => {
|
|
99
99
|
const sha256Value =
|
|
100
100
|
typeof value === 'string' && /^[a-f0-9]{64}$/iu.test(value) ? value.toLowerCase() : undefined;
|
|
101
101
|
return {
|
|
@@ -172,6 +172,20 @@ const planError = (message: string): never => {
|
|
|
172
172
|
});
|
|
173
173
|
};
|
|
174
174
|
|
|
175
|
+
const requireArtifactIntegrity = (
|
|
176
|
+
input: Parameters<typeof asIntegrity>[0],
|
|
177
|
+
label: string
|
|
178
|
+
): Pick<FirmwareUpdatePlanArtifact, 'expectedSize' | 'expectedSha256'> => {
|
|
179
|
+
const integrity = asIntegrity(input);
|
|
180
|
+
if (integrity.expectedSize === undefined || integrity.expectedSha256 === undefined) {
|
|
181
|
+
return planError(`${label} integrity metadata is invalid`);
|
|
182
|
+
}
|
|
183
|
+
return {
|
|
184
|
+
expectedSize: integrity.expectedSize,
|
|
185
|
+
expectedSha256: integrity.expectedSha256,
|
|
186
|
+
};
|
|
187
|
+
};
|
|
188
|
+
|
|
175
189
|
const FIRMWARE_UPDATE_PLAN_FORCE_TARGETS = new Set<FirmwareUpdatePlanForceTarget>([
|
|
176
190
|
'firmware',
|
|
177
191
|
'ble',
|
|
@@ -309,8 +323,8 @@ export const assertFirmwareUpdatePlan = (value: unknown): FirmwareUpdatePlan =>
|
|
|
309
323
|
}
|
|
310
324
|
assertExactKeys(
|
|
311
325
|
artifact,
|
|
312
|
-
['artifactId', 'role', 'target', 'url', 'container'],
|
|
313
|
-
['logicalName', '
|
|
326
|
+
['artifactId', 'role', 'target', 'url', 'container', 'expectedSize', 'expectedSha256'],
|
|
327
|
+
['logicalName', 'targetVersion']
|
|
314
328
|
);
|
|
315
329
|
const artifactId = assertBoundedString(artifact.artifactId, 'artifact id', 160);
|
|
316
330
|
if (artifactIds.has(artifactId)) {
|
|
@@ -329,16 +343,12 @@ export const assertFirmwareUpdatePlan = (value: unknown): FirmwareUpdatePlan =>
|
|
|
329
343
|
if (artifact.logicalName !== undefined) {
|
|
330
344
|
assertBoundedString(artifact.logicalName, 'logical name', 256);
|
|
331
345
|
}
|
|
332
|
-
if (
|
|
333
|
-
artifact.expectedSize !== undefined &&
|
|
334
|
-
(!Number.isSafeInteger(artifact.expectedSize) || (artifact.expectedSize as number) <= 0)
|
|
335
|
-
) {
|
|
346
|
+
if (!Number.isSafeInteger(artifact.expectedSize) || (artifact.expectedSize as number) <= 0) {
|
|
336
347
|
return planError('Firmware update plan artifact size is invalid');
|
|
337
348
|
}
|
|
338
349
|
if (
|
|
339
|
-
artifact.expectedSha256 !==
|
|
340
|
-
(
|
|
341
|
-
!/^[a-f0-9]{64}$/u.test(artifact.expectedSha256))
|
|
350
|
+
typeof artifact.expectedSha256 !== 'string' ||
|
|
351
|
+
!/^[a-f0-9]{64}$/u.test(artifact.expectedSha256)
|
|
342
352
|
) {
|
|
343
353
|
return planError('Firmware update plan artifact digest is invalid');
|
|
344
354
|
}
|
|
@@ -502,13 +512,10 @@ const buildProtocolV2Artifacts = (
|
|
|
502
512
|
);
|
|
503
513
|
}
|
|
504
514
|
componentTargetSet.add(target);
|
|
505
|
-
const integrity =
|
|
506
|
-
size: component.expectedSize,
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
if (integrity.expectedSize === undefined || integrity.expectedSha256 === undefined) {
|
|
510
|
-
planError(`Protocol V2 component ${key} integrity metadata is invalid`);
|
|
511
|
-
}
|
|
515
|
+
const integrity = requireArtifactIntegrity(
|
|
516
|
+
{ size: component.expectedSize, sha256: component.fingerprint },
|
|
517
|
+
`Protocol V2 component ${key}`
|
|
518
|
+
);
|
|
512
519
|
artifacts.push({
|
|
513
520
|
artifactId: `component:${target}`,
|
|
514
521
|
role: 'component',
|
|
@@ -714,13 +721,10 @@ export const buildProtocolV2FirmwareUpdatePlan = ({
|
|
|
714
721
|
if (!archive) {
|
|
715
722
|
return planError('Protocol V2 resource target has no resource archive');
|
|
716
723
|
}
|
|
717
|
-
const integrity =
|
|
718
|
-
size: archive.archiveSize,
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
if (integrity.expectedSize === undefined || integrity.expectedSha256 === undefined) {
|
|
722
|
-
planError('Protocol V2 resource archive integrity metadata is invalid');
|
|
723
|
-
}
|
|
724
|
+
const integrity = requireArtifactIntegrity(
|
|
725
|
+
{ size: archive.archiveSize, sha256: archive.archiveSha256 },
|
|
726
|
+
'Protocol V2 resource archive'
|
|
727
|
+
);
|
|
724
728
|
protocolV2.artifacts.push({
|
|
725
729
|
artifactId: 'resource:archive',
|
|
726
730
|
role: 'resourceBundle',
|
|
@@ -791,7 +795,11 @@ export const buildFirmwareUpdatePlan = ({
|
|
|
791
795
|
!!asRelease(release);
|
|
792
796
|
const shouldUpdateFirmware =
|
|
793
797
|
isUpgrade(firmware) || isRecoveryInstall(firmware) || forcedTargets.has('firmware');
|
|
794
|
-
|
|
798
|
+
// V2 cannot write legacy resources once the device is already in bootloader
|
|
799
|
+
// recovery. Do not approve an artifact that its executor cannot apply.
|
|
800
|
+
const shouldUpdateResource =
|
|
801
|
+
!(executor === 'v2' && bootloaderMode) &&
|
|
802
|
+
(isUpgrade(firmware) || forcedTargets.has('resource'));
|
|
795
803
|
|
|
796
804
|
if (
|
|
797
805
|
executor === 'v4' &&
|
|
@@ -808,19 +816,24 @@ export const buildFirmwareUpdatePlan = ({
|
|
|
808
816
|
targetsToUpdate = protocolV2.targets;
|
|
809
817
|
} else {
|
|
810
818
|
if (isUpgrade(bootloader) || forcedTargets.has('bootloader')) {
|
|
819
|
+
const bootloaderRelease = asRelease(bootloader);
|
|
820
|
+
const integrity = requireArtifactIntegrity(
|
|
821
|
+
{
|
|
822
|
+
size: bootloaderRelease?.bootloaderExpectedSize,
|
|
823
|
+
sha256: bootloaderRelease?.bootloaderFingerprint,
|
|
824
|
+
},
|
|
825
|
+
'Bootloader release'
|
|
826
|
+
);
|
|
811
827
|
artifacts.push({
|
|
812
828
|
artifactId: 'bootloader',
|
|
813
829
|
role: 'bootloader',
|
|
814
830
|
target: 'bootloader',
|
|
815
|
-
url: assertArtifactUrl(
|
|
831
|
+
url: assertArtifactUrl(bootloaderRelease?.bootloaderResource, 'Bootloader release'),
|
|
816
832
|
container: 'raw',
|
|
817
|
-
...
|
|
818
|
-
|
|
819
|
-
sha256: asRelease(bootloader)?.bootloaderFingerprint,
|
|
820
|
-
}),
|
|
821
|
-
...(asVersion(asRelease(bootloader)?.bootloaderVersion)
|
|
833
|
+
...integrity,
|
|
834
|
+
...(asVersion(bootloaderRelease?.bootloaderVersion)
|
|
822
835
|
? {
|
|
823
|
-
targetVersion: asVersion(
|
|
836
|
+
targetVersion: asVersion(bootloaderRelease?.bootloaderVersion),
|
|
824
837
|
}
|
|
825
838
|
: {}),
|
|
826
839
|
});
|
|
@@ -832,16 +845,17 @@ export const buildFirmwareUpdatePlan = ({
|
|
|
832
845
|
});
|
|
833
846
|
}
|
|
834
847
|
if (shouldUpdateFirmware) {
|
|
848
|
+
const integrity = requireArtifactIntegrity(
|
|
849
|
+
{ size: firmwareRelease.expectedSize, sha256: firmwareRelease.fingerprint },
|
|
850
|
+
'Firmware release'
|
|
851
|
+
);
|
|
835
852
|
artifacts.push({
|
|
836
853
|
artifactId: 'firmware',
|
|
837
854
|
role: 'firmware',
|
|
838
855
|
target: 'firmware',
|
|
839
856
|
url: assertArtifactUrl(firmwareRelease.url, 'Firmware release'),
|
|
840
857
|
container: 'raw',
|
|
841
|
-
...
|
|
842
|
-
size: firmwareRelease.expectedSize,
|
|
843
|
-
sha256: firmwareRelease.fingerprint,
|
|
844
|
-
}),
|
|
858
|
+
...integrity,
|
|
845
859
|
...(asVersion(firmwareRelease.version)
|
|
846
860
|
? { targetVersion: asVersion(firmwareRelease.version) }
|
|
847
861
|
: {}),
|
|
@@ -854,13 +868,8 @@ export const buildFirmwareUpdatePlan = ({
|
|
|
854
868
|
platform,
|
|
855
869
|
});
|
|
856
870
|
if (resourceUrl) {
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
role: 'resource',
|
|
860
|
-
target: 'resource',
|
|
861
|
-
url: assertArtifactUrl(resourceUrl, 'Firmware resource release'),
|
|
862
|
-
container: 'zip',
|
|
863
|
-
...asIntegrity({
|
|
871
|
+
const integrity = requireArtifactIntegrity(
|
|
872
|
+
{
|
|
864
873
|
size:
|
|
865
874
|
resourceUrl === asString(firmwareRelease.fullResource)
|
|
866
875
|
? firmwareRelease.fullResourceExpectedSize
|
|
@@ -869,23 +878,36 @@ export const buildFirmwareUpdatePlan = ({
|
|
|
869
878
|
resourceUrl === asString(firmwareRelease.fullResource)
|
|
870
879
|
? firmwareRelease.fullResourceFingerprint
|
|
871
880
|
: firmwareRelease.resourceFingerprint,
|
|
872
|
-
}
|
|
881
|
+
},
|
|
882
|
+
'Legacy resource archive'
|
|
883
|
+
);
|
|
884
|
+
artifacts.push({
|
|
885
|
+
artifactId: 'resource',
|
|
886
|
+
role: 'resource',
|
|
887
|
+
target: 'resource',
|
|
888
|
+
url: assertArtifactUrl(resourceUrl, 'Firmware resource release'),
|
|
889
|
+
container: 'zip',
|
|
890
|
+
...integrity,
|
|
873
891
|
});
|
|
874
892
|
}
|
|
875
893
|
}
|
|
876
894
|
}
|
|
877
895
|
if (isUpgrade(ble) || isRecoveryInstall(ble) || forcedTargets.has('ble')) {
|
|
878
896
|
const bleRelease = asRelease(ble);
|
|
897
|
+
const integrity = requireArtifactIntegrity(
|
|
898
|
+
{
|
|
899
|
+
size: bleRelease?.expectedSize,
|
|
900
|
+
sha256: bleRelease?.fingerprintWeb ?? bleRelease?.fingerprint,
|
|
901
|
+
},
|
|
902
|
+
'BLE release'
|
|
903
|
+
);
|
|
879
904
|
artifacts.push({
|
|
880
905
|
artifactId: 'ble',
|
|
881
906
|
role: 'ble',
|
|
882
907
|
target: 'ble',
|
|
883
908
|
url: assertArtifactUrl(bleRelease?.webUpdate ?? bleRelease?.url, 'BLE release'),
|
|
884
909
|
container: 'raw',
|
|
885
|
-
...
|
|
886
|
-
size: bleRelease?.expectedSize,
|
|
887
|
-
sha256: bleRelease?.fingerprintWeb ?? bleRelease?.fingerprint,
|
|
888
|
-
}),
|
|
910
|
+
...integrity,
|
|
889
911
|
...(asVersion(bleRelease?.version)
|
|
890
912
|
? { targetVersion: asVersion(bleRelease?.version) }
|
|
891
913
|
: {}),
|
|
@@ -302,6 +302,23 @@ export const validateFirmwareUpdatePreparedPlan = (value: unknown): FirmwareUpda
|
|
|
302
302
|
return preparedPlan;
|
|
303
303
|
};
|
|
304
304
|
|
|
305
|
+
export const getFirmwareUpdatePreparedRawArtifact = ({
|
|
306
|
+
preparedPlan: value,
|
|
307
|
+
target,
|
|
308
|
+
role,
|
|
309
|
+
}: {
|
|
310
|
+
preparedPlan: unknown;
|
|
311
|
+
target: FirmwareUpdatePreparedArtifact['target'];
|
|
312
|
+
role: FirmwareUpdatePreparedArtifact['role'];
|
|
313
|
+
}): FirmwareUpdatePreparedArtifact => {
|
|
314
|
+
const preparedPlan = validateFirmwareUpdatePreparedPlan(value);
|
|
315
|
+
const artifacts = preparedPlan.artifacts.filter(artifact => artifact.target === target);
|
|
316
|
+
if (artifacts.length !== 1 || artifacts[0].role !== role || artifacts[0].container !== 'raw') {
|
|
317
|
+
return preparedPlanError(`Firmware prepared plan ${target} artifact is invalid`);
|
|
318
|
+
}
|
|
319
|
+
return artifacts[0];
|
|
320
|
+
};
|
|
321
|
+
|
|
305
322
|
/**
|
|
306
323
|
* Identity observed on the live device while a degraded recovery plan runs, keyed by
|
|
307
324
|
* the plan's opaque lease. Bootloader recovery starts with no serial and the device
|
|
@@ -8,6 +8,7 @@ export type DeviceUpdateBootloaderParams = {
|
|
|
8
8
|
preparedPlan?: FirmwareUpdatePreparedPlan;
|
|
9
9
|
hostBindingGeneration?: number;
|
|
10
10
|
binary?: ArrayBuffer;
|
|
11
|
+
/** @deprecated Core derives the bootloader artifact from preparedPlan. */
|
|
11
12
|
artifact?: FirmwareArtifactReference;
|
|
12
13
|
artifactReader?: FirmwareArtifactReader;
|
|
13
14
|
firmwareType?: EFirmwareType;
|
|
@@ -38,7 +38,8 @@ export interface FirmwareUpdateBinaryParams {
|
|
|
38
38
|
export interface FirmwareUpdateArtifactParams {
|
|
39
39
|
preparedPlan: FirmwareUpdatePreparedPlan;
|
|
40
40
|
hostBindingGeneration: number;
|
|
41
|
-
artifact
|
|
41
|
+
/** @deprecated Core derives the component artifact from preparedPlan. */
|
|
42
|
+
artifact?: FirmwareArtifactReference;
|
|
42
43
|
resourceEntries?: Array<{
|
|
43
44
|
entryName: string;
|
|
44
45
|
artifact: FirmwareArtifactReference;
|
|
@@ -20,8 +20,8 @@ export interface FirmwareUpdatePlanArtifact {
|
|
|
20
20
|
url: string;
|
|
21
21
|
container: 'raw' | 'zip';
|
|
22
22
|
logicalName?: string;
|
|
23
|
-
expectedSize
|
|
24
|
-
expectedSha256
|
|
23
|
+
expectedSize: number;
|
|
24
|
+
expectedSha256: string;
|
|
25
25
|
targetVersion?: string;
|
|
26
26
|
}
|
|
27
27
|
|