@onekeyfe/hd-core 1.2.0-alpha.97 → 1.2.0-alpha.99
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__/check-all-firmware-release-protocol-v2.test.ts +411 -86
- package/__tests__/check-firmware-release-protocol-v2.test.ts +199 -0
- package/__tests__/firmware-memory-host.test.ts +126 -0
- package/__tests__/firmware-update/device-update-bootloader.test.ts +14 -2
- package/__tests__/firmware-update/firmware-host-binding.test.ts +9 -0
- package/__tests__/firmware-update/firmware-prepared-host-digest.test.ts +573 -0
- package/__tests__/firmware-update/firmware-prepared-resource-archive.test.ts +150 -0
- package/__tests__/firmware-update/firmware-update-bootloader-poll.test.ts +65 -2
- package/__tests__/firmware-update/firmware-update-plan-bootloader-identity.test.ts +80 -0
- package/__tests__/firmware-update/firmware-update-plan.test.ts +293 -71
- package/__tests__/firmware-update/firmware-update-prepared-plan.test.ts +53 -1
- 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-firmware-targets.test.ts +16 -0
- package/__tests__/protocol-v2-resources.test.ts +148 -232
- package/__tests__/protocol-v2.test.ts +1326 -510
- package/dist/api/CheckAllFirmwareRelease.d.ts +2 -3
- package/dist/api/CheckAllFirmwareRelease.d.ts.map +1 -1
- package/dist/api/CheckBLEFirmwareRelease.d.ts +3 -2
- package/dist/api/CheckBLEFirmwareRelease.d.ts.map +1 -1
- package/dist/api/CheckBootloaderRelease.d.ts +2 -1
- package/dist/api/CheckBootloaderRelease.d.ts.map +1 -1
- package/dist/api/CheckFirmwareRelease.d.ts +3 -2
- package/dist/api/CheckFirmwareRelease.d.ts.map +1 -1
- package/dist/api/FirmwareUpdateV2.d.ts +1 -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 +11 -9
- package/dist/api/FirmwareUpdateV4.d.ts.map +1 -1
- package/dist/api/UploadPortfolio.d.ts.map +1 -1
- package/dist/api/device/DeviceUpdateBootloader.d.ts.map +1 -1
- package/dist/api/firmware/FirmwareHostBinding.d.ts +1 -1
- package/dist/api/firmware/FirmwareHostBinding.d.ts.map +1 -1
- package/dist/api/firmware/FirmwareMemoryHost.d.ts +22 -0
- package/dist/api/firmware/FirmwareMemoryHost.d.ts.map +1 -0
- 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 +48 -1
- package/dist/api/firmware/FirmwareUpdatePlan.d.ts.map +1 -1
- package/dist/api/firmware/FirmwareUpdatePreparedPlan.d.ts +7 -1
- package/dist/api/firmware/FirmwareUpdatePreparedPlan.d.ts.map +1 -1
- package/dist/api/firmware/getBinary.d.ts +0 -1
- package/dist/api/firmware/getBinary.d.ts.map +1 -1
- package/dist/api/firmware/protocolV2Release.d.ts +33 -0
- package/dist/api/firmware/protocolV2Release.d.ts.map +1 -0
- package/dist/data-manager/DataManager.d.ts +4 -3
- package/dist/data-manager/DataManager.d.ts.map +1 -1
- package/dist/index.d.ts +108 -114
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1825 -1006
- package/dist/protocols/protocol-v2/resources.d.ts +10 -28
- package/dist/protocols/protocol-v2/resources.d.ts.map +1 -1
- package/dist/types/api/checkAllFirmwareRelease.d.ts +20 -8
- package/dist/types/api/checkAllFirmwareRelease.d.ts.map +1 -1
- package/dist/types/api/checkBLEFirmwareRelease.d.ts +2 -13
- package/dist/types/api/checkBLEFirmwareRelease.d.ts.map +1 -1
- package/dist/types/api/checkBootloaderRelease.d.ts +2 -6
- package/dist/types/api/checkBootloaderRelease.d.ts.map +1 -1
- package/dist/types/api/checkFirmwareRelease.d.ts +2 -13
- package/dist/types/api/checkFirmwareRelease.d.ts.map +1 -1
- package/dist/types/api/checkFirmwareTypeAvailable.d.ts +2 -2
- package/dist/types/api/checkFirmwareTypeAvailable.d.ts.map +1 -1
- package/dist/types/api/deviceUpdateBootloader.d.ts.map +1 -1
- package/dist/types/api/export.d.ts +1 -1
- package/dist/types/api/export.d.ts.map +1 -1
- package/dist/types/api/firmwareUpdate.d.ts +4 -12
- 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/dist/types/settings.d.ts +35 -36
- package/dist/types/settings.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/api/CheckAllFirmwareRelease.ts +113 -80
- package/src/api/CheckBLEFirmwareRelease.ts +37 -5
- package/src/api/CheckBootloaderRelease.ts +35 -3
- package/src/api/CheckFirmwareRelease.ts +35 -7
- package/src/api/FirmwareUpdateV2.ts +231 -138
- package/src/api/FirmwareUpdateV3.ts +101 -56
- package/src/api/FirmwareUpdateV4.ts +936 -408
- package/src/api/UploadPortfolio.ts +18 -0
- package/src/api/device/DeviceUpdateBootloader.ts +37 -11
- package/src/api/firmware/FirmwareHostBinding.ts +26 -3
- package/src/api/firmware/FirmwareMemoryHost.ts +143 -0
- package/src/api/firmware/FirmwarePreparedResourceArchive.ts +207 -0
- package/src/api/firmware/FirmwareUpdateBaseMethod.ts +122 -73
- package/src/api/firmware/FirmwareUpdatePlan.ts +295 -122
- package/src/api/firmware/FirmwareUpdatePreparedPlan.ts +49 -12
- package/src/api/firmware/protocolV2Release.ts +168 -0
- package/src/data-manager/DataManager.ts +35 -19
- package/src/index.ts +10 -0
- package/src/protocols/protocol-v2/resources.ts +157 -341
- package/src/types/api/checkAllFirmwareRelease.ts +27 -12
- package/src/types/api/checkBLEFirmwareRelease.ts +4 -13
- package/src/types/api/checkBootloaderRelease.ts +2 -6
- package/src/types/api/checkFirmwareRelease.ts +2 -13
- package/src/types/api/checkFirmwareTypeAvailable.ts +2 -2
- package/src/types/api/deviceUpdateBootloader.ts +1 -0
- package/src/types/api/export.ts +6 -1
- package/src/types/api/firmwareUpdate.ts +12 -17
- package/src/types/api/firmwareUpdatePlan.ts +2 -2
- package/src/types/settings.ts +35 -61
- package/src/utils/deviceFeaturesUtils.ts +2 -2
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { createDeviceNotSupportMethodError } from '@onekeyfe/hd-shared';
|
|
2
|
+
|
|
3
|
+
import { supportsProtocolV2Message } from '../protocols/protocol-v2/features';
|
|
1
4
|
import FileWrite from './FileWrite';
|
|
2
5
|
|
|
3
6
|
export type UploadPortfolioParams = {
|
|
@@ -7,6 +10,8 @@ export type UploadPortfolioParams = {
|
|
|
7
10
|
|
|
8
11
|
const PORTFOLIO_PENDING_PATH = 'vol1:/portfolio/portfolio.okpkg.pending';
|
|
9
12
|
const PORTFOLIO_CHUNK_SIZE = 2048;
|
|
13
|
+
const FILESYSTEM_FILE_WRITE_MESSAGE_TYPE = 60805;
|
|
14
|
+
const PORTFOLIO_UPDATE_MESSAGE_TYPE = 61400;
|
|
10
15
|
|
|
11
16
|
export default class UploadPortfolio extends FileWrite {
|
|
12
17
|
init() {
|
|
@@ -29,6 +34,19 @@ export default class UploadPortfolio extends FileWrite {
|
|
|
29
34
|
}
|
|
30
35
|
|
|
31
36
|
async run() {
|
|
37
|
+
const protocolInfo = await this.device.ensureProtocolV2RuntimeContext();
|
|
38
|
+
const hasFileWrite = supportsProtocolV2Message(
|
|
39
|
+
protocolInfo,
|
|
40
|
+
FILESYSTEM_FILE_WRITE_MESSAGE_TYPE
|
|
41
|
+
);
|
|
42
|
+
const hasPortfolioUpdate = supportsProtocolV2Message(
|
|
43
|
+
protocolInfo,
|
|
44
|
+
PORTFOLIO_UPDATE_MESSAGE_TYPE
|
|
45
|
+
);
|
|
46
|
+
if (!hasFileWrite || !hasPortfolioUpdate) {
|
|
47
|
+
throw createDeviceNotSupportMethodError(this.name, this.device.getCurrentFirmwareType());
|
|
48
|
+
}
|
|
49
|
+
|
|
32
50
|
const stagedFile = await super.run();
|
|
33
51
|
this.throwIfAborted();
|
|
34
52
|
await this.device.commands.typedCall('PortfolioUpdate', 'Success', {});
|
|
@@ -14,6 +14,8 @@ import { resolveFirmwareUpdateHostBinding } from '../firmware/FirmwareHostBindin
|
|
|
14
14
|
import {
|
|
15
15
|
assertFirmwareUpdatePreparedPlanBinding,
|
|
16
16
|
assertFirmwareUpdatePreparedPlanDeviceIdentity,
|
|
17
|
+
getFirmwareUpdatePreparedRawArtifact,
|
|
18
|
+
validateFirmwareUpdatePreparedPlan,
|
|
17
19
|
} from '../firmware/FirmwareUpdatePreparedPlan';
|
|
18
20
|
import { getDeviceType, getDeviceUUID } from '../../utils';
|
|
19
21
|
import { resolveDeviceBootloaderMode } from '../../utils/deviceFeaturesCompat';
|
|
@@ -157,33 +159,59 @@ export default class DeviceUpdateBootloader extends FirmwareUpdateBaseMethod<any
|
|
|
157
159
|
const { features } = device;
|
|
158
160
|
|
|
159
161
|
const payload = this.payload as DeviceUpdateBootloaderParams;
|
|
160
|
-
const
|
|
161
|
-
|
|
162
|
+
const hasPreparedPlan = payload.preparedPlan !== undefined;
|
|
163
|
+
if (hasPreparedPlan && payload.binary !== undefined) {
|
|
164
|
+
throw ERRORS.TypedError(
|
|
165
|
+
HardwareErrorCode.CallMethodInvalidParameter,
|
|
166
|
+
'Prepared bootloader plans cannot be combined with a legacy binary'
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
if (!hasPreparedPlan && payload.artifact !== undefined) {
|
|
170
|
+
throw ERRORS.TypedError(
|
|
171
|
+
HardwareErrorCode.CallMethodInvalidParameter,
|
|
172
|
+
'Bootloader artifacts require a prepared plan'
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
const preparedPlan = hasPreparedPlan
|
|
176
|
+
? validateFirmwareUpdatePreparedPlan(payload.preparedPlan)
|
|
177
|
+
: undefined;
|
|
178
|
+
const plannedArtifact = preparedPlan
|
|
179
|
+
? getFirmwareUpdatePreparedRawArtifact({
|
|
180
|
+
preparedPlan,
|
|
181
|
+
target: 'bootloader',
|
|
182
|
+
role: 'bootloader',
|
|
183
|
+
}).artifact
|
|
162
184
|
: undefined;
|
|
185
|
+
const artifactReader = preparedPlan
|
|
186
|
+
? resolveFirmwareUpdateHostBinding(
|
|
187
|
+
payload.hostBindingGeneration,
|
|
188
|
+
preparedPlan.preparedPlanDigest
|
|
189
|
+
).artifactReader
|
|
190
|
+
: payload.artifactReader;
|
|
163
191
|
const executionParams = {
|
|
164
192
|
...payload,
|
|
165
|
-
artifactReader
|
|
193
|
+
artifactReader,
|
|
166
194
|
};
|
|
167
|
-
if (
|
|
195
|
+
if (preparedPlan && plannedArtifact) {
|
|
168
196
|
assertFirmwareUpdatePreparedPlanDeviceIdentity({
|
|
169
|
-
preparedPlan
|
|
197
|
+
preparedPlan,
|
|
170
198
|
deviceIdentity: getDeviceUUID(features) || undefined,
|
|
171
199
|
bootloaderMode: resolveDeviceBootloaderMode(features),
|
|
172
200
|
deviceModel: String(getDeviceType(features)),
|
|
173
201
|
});
|
|
174
202
|
assertFirmwareUpdatePreparedPlanBinding({
|
|
175
|
-
preparedPlan
|
|
203
|
+
preparedPlan,
|
|
176
204
|
executor: 'v2',
|
|
177
205
|
scopeTargets: ['bootloader'],
|
|
178
206
|
bindings: [
|
|
179
207
|
{
|
|
180
208
|
target: 'bootloader',
|
|
181
|
-
artifact: payload.artifact,
|
|
209
|
+
artifact: payload.artifact ?? plannedArtifact,
|
|
182
210
|
},
|
|
183
211
|
],
|
|
184
212
|
});
|
|
185
213
|
const source = await openFirmwareByteSource({
|
|
186
|
-
artifact:
|
|
214
|
+
artifact: plannedArtifact,
|
|
187
215
|
reader: executionParams.artifactReader,
|
|
188
216
|
});
|
|
189
217
|
if (!source) {
|
|
@@ -194,13 +222,11 @@ export default class DeviceUpdateBootloader extends FirmwareUpdateBaseMethod<any
|
|
|
194
222
|
}
|
|
195
223
|
try {
|
|
196
224
|
const deviceType = device.getCurrentDeviceType();
|
|
197
|
-
const deviceFirmwareType = device.getCurrentFirmwareType();
|
|
198
|
-
const firmwareType = payload.firmwareType ?? deviceFirmwareType;
|
|
199
225
|
if (DeviceModelToTypes.model_touch.includes(deviceType)) {
|
|
200
226
|
return await this.updateTouchBootloader({
|
|
201
227
|
device,
|
|
202
228
|
features,
|
|
203
|
-
firmwareType,
|
|
229
|
+
firmwareType: preparedPlan.firmwareType,
|
|
204
230
|
source,
|
|
205
231
|
});
|
|
206
232
|
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { ERRORS, HardwareErrorCode } from '@onekeyfe/hd-shared';
|
|
2
2
|
|
|
3
|
+
import { clearFirmwareUpdatePreparedPlanDeviceIdentityPin } from './FirmwareUpdatePreparedPlan';
|
|
4
|
+
|
|
3
5
|
import type { FirmwareUpdateHostBinding } from '../../types/api/firmwareUpdate';
|
|
4
6
|
|
|
5
7
|
const bindingError = (message: string): never => {
|
|
@@ -16,14 +18,20 @@ class FirmwareHostBindingRegistry {
|
|
|
16
18
|
register(binding: FirmwareUpdateHostBinding): number {
|
|
17
19
|
if (
|
|
18
20
|
!binding ||
|
|
21
|
+
typeof binding.preparedPlanDigest !== 'string' ||
|
|
22
|
+
!/^[0-9a-f]{64}$/i.test(binding.preparedPlanDigest) ||
|
|
19
23
|
typeof binding.artifactReader?.open !== 'function' ||
|
|
20
24
|
typeof binding.artifactReader.read !== 'function' ||
|
|
21
25
|
typeof binding.artifactReader.close !== 'function'
|
|
22
26
|
) {
|
|
23
27
|
return bindingError('Firmware host binding is invalid');
|
|
24
28
|
}
|
|
29
|
+
const replacedPreparedPlanDigest = this.binding?.preparedPlanDigest;
|
|
25
30
|
this.generation += 1;
|
|
26
31
|
this.binding = binding;
|
|
32
|
+
if (replacedPreparedPlanDigest) {
|
|
33
|
+
clearFirmwareUpdatePreparedPlanDeviceIdentityPin(replacedPreparedPlanDigest);
|
|
34
|
+
}
|
|
27
35
|
return this.generation;
|
|
28
36
|
}
|
|
29
37
|
|
|
@@ -31,8 +39,12 @@ class FirmwareHostBindingRegistry {
|
|
|
31
39
|
if (generation !== undefined && generation !== this.generation) {
|
|
32
40
|
return false;
|
|
33
41
|
}
|
|
42
|
+
const releasedPreparedPlanDigest = this.binding?.preparedPlanDigest;
|
|
34
43
|
this.generation += 1;
|
|
35
44
|
this.binding = undefined;
|
|
45
|
+
if (releasedPreparedPlanDigest) {
|
|
46
|
+
clearFirmwareUpdatePreparedPlanDeviceIdentityPin(releasedPreparedPlanDigest);
|
|
47
|
+
}
|
|
36
48
|
return true;
|
|
37
49
|
}
|
|
38
50
|
|
|
@@ -40,7 +52,7 @@ class FirmwareHostBindingRegistry {
|
|
|
40
52
|
return this.generation;
|
|
41
53
|
}
|
|
42
54
|
|
|
43
|
-
resolve(generation: number): FirmwareUpdateHostBinding {
|
|
55
|
+
resolve(generation: number, preparedPlanDigest?: string): FirmwareUpdateHostBinding {
|
|
44
56
|
if (
|
|
45
57
|
!Number.isSafeInteger(generation) ||
|
|
46
58
|
generation <= 0 ||
|
|
@@ -50,12 +62,21 @@ class FirmwareHostBindingRegistry {
|
|
|
50
62
|
return bindingError(`Firmware host binding generation ${generation} is stale`);
|
|
51
63
|
}
|
|
52
64
|
const { binding } = this;
|
|
65
|
+
if (
|
|
66
|
+
preparedPlanDigest !== undefined &&
|
|
67
|
+
binding.preparedPlanDigest.toLowerCase() !== preparedPlanDigest.toLowerCase()
|
|
68
|
+
) {
|
|
69
|
+
return bindingError(
|
|
70
|
+
`Firmware host binding generation ${generation} does not match the prepared plan`
|
|
71
|
+
);
|
|
72
|
+
}
|
|
53
73
|
const assertCurrent = () => {
|
|
54
74
|
if (generation !== this.generation || binding !== this.binding) {
|
|
55
75
|
bindingError(`Firmware host binding generation ${generation} is stale`);
|
|
56
76
|
}
|
|
57
77
|
};
|
|
58
78
|
return {
|
|
79
|
+
preparedPlanDigest: binding.preparedPlanDigest,
|
|
59
80
|
artifactReader: {
|
|
60
81
|
open: async input => {
|
|
61
82
|
assertCurrent();
|
|
@@ -96,5 +117,7 @@ export const getFirmwareUpdateHostBindingGeneration = (): number =>
|
|
|
96
117
|
firmwareHostBindingRegistry.getGeneration();
|
|
97
118
|
|
|
98
119
|
export const resolveFirmwareUpdateHostBinding = (
|
|
99
|
-
generation: number | undefined
|
|
100
|
-
|
|
120
|
+
generation: number | undefined,
|
|
121
|
+
preparedPlanDigest?: string
|
|
122
|
+
): FirmwareUpdateHostBinding =>
|
|
123
|
+
firmwareHostBindingRegistry.resolve(generation ?? Number.NaN, preparedPlanDigest);
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { sha256 } from '@noble/hashes/sha256';
|
|
2
|
+
import { bytesToHex } from '@noble/hashes/utils';
|
|
3
|
+
import { ERRORS, HardwareErrorCode } from '@onekeyfe/hd-shared';
|
|
4
|
+
|
|
5
|
+
import type { CoreApi } from '../../types/api';
|
|
6
|
+
import type {
|
|
7
|
+
FirmwareArtifactReader,
|
|
8
|
+
FirmwareArtifactReference,
|
|
9
|
+
} from '../../types/api/firmwareUpdate';
|
|
10
|
+
import type { FirmwareUpdatePlan } from '../../types/api/firmwareUpdatePlan';
|
|
11
|
+
|
|
12
|
+
export type FirmwareMemoryArtifactEntry = {
|
|
13
|
+
entryName: string;
|
|
14
|
+
binary: ArrayBuffer;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export type FirmwareMemoryArtifact = {
|
|
18
|
+
artifactId: string;
|
|
19
|
+
binary: ArrayBuffer;
|
|
20
|
+
materializedEntries?: FirmwareMemoryArtifactEntry[];
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export type FirmwareUpdateV4MemoryHost = {
|
|
24
|
+
preparedPlan: ReturnType<CoreApi['prepareFirmwareUpdatePlan']>;
|
|
25
|
+
hostBindingGeneration: number;
|
|
26
|
+
release: () => void;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
let memoryHostSequence = 0;
|
|
30
|
+
|
|
31
|
+
const createReference = (binary: ArrayBuffer, prefix: string): FirmwareArtifactReference => {
|
|
32
|
+
const digest = bytesToHex(sha256(new Uint8Array(binary)));
|
|
33
|
+
return {
|
|
34
|
+
artifactRef: `fwmem:${prefix}:${digest.slice(0, 32)}`,
|
|
35
|
+
size: binary.byteLength,
|
|
36
|
+
sha256: digest,
|
|
37
|
+
};
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export function prepareFirmwareUpdateV4MemoryHost({
|
|
41
|
+
sdk,
|
|
42
|
+
plan,
|
|
43
|
+
artifacts,
|
|
44
|
+
}: {
|
|
45
|
+
sdk: Pick<
|
|
46
|
+
CoreApi,
|
|
47
|
+
| 'prepareFirmwareUpdatePlan'
|
|
48
|
+
| 'registerFirmwareUpdateHostBinding'
|
|
49
|
+
| 'unregisterFirmwareUpdateHostBinding'
|
|
50
|
+
>;
|
|
51
|
+
plan: FirmwareUpdatePlan;
|
|
52
|
+
artifacts: FirmwareMemoryArtifact[];
|
|
53
|
+
}): FirmwareUpdateV4MemoryHost {
|
|
54
|
+
if (plan.executor !== 'v4') {
|
|
55
|
+
throw ERRORS.TypedError(
|
|
56
|
+
HardwareErrorCode.RuntimeError,
|
|
57
|
+
'Firmware memory host only supports V4 plans'
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
memoryHostSequence += 1;
|
|
61
|
+
const hostId = `${Date.now()}:${memoryHostSequence}`;
|
|
62
|
+
const binaries = new Map<string, Uint8Array>();
|
|
63
|
+
const inputs = artifacts.map((input, artifactIndex) => {
|
|
64
|
+
const artifactBinary = new Uint8Array(input.binary).slice();
|
|
65
|
+
const artifact = createReference(
|
|
66
|
+
artifactBinary.buffer as ArrayBuffer,
|
|
67
|
+
`${hostId}:artifact:${artifactIndex}`
|
|
68
|
+
);
|
|
69
|
+
binaries.set(artifact.artifactRef, artifactBinary);
|
|
70
|
+
const materializedEntries = input.materializedEntries?.map((entry, entryIndex) => {
|
|
71
|
+
const entryArtifact = createReference(
|
|
72
|
+
entry.binary,
|
|
73
|
+
`${hostId}:entry:${artifactIndex}:${entryIndex}`
|
|
74
|
+
);
|
|
75
|
+
// Entry references are compact receipts for bytes that will be re-derived
|
|
76
|
+
// from the verified archive. Retaining another readable copy here doubles
|
|
77
|
+
// resource memory without adding an execution trust boundary.
|
|
78
|
+
return {
|
|
79
|
+
entryName: entry.entryName,
|
|
80
|
+
artifact: entryArtifact,
|
|
81
|
+
};
|
|
82
|
+
});
|
|
83
|
+
return {
|
|
84
|
+
artifactId: input.artifactId,
|
|
85
|
+
artifact,
|
|
86
|
+
...(materializedEntries?.length ? { materializedEntries } : {}),
|
|
87
|
+
};
|
|
88
|
+
});
|
|
89
|
+
const preparedPlan = sdk.prepareFirmwareUpdatePlan({
|
|
90
|
+
plan,
|
|
91
|
+
leaseRef: `fwmemlease:${hostId}`,
|
|
92
|
+
artifacts: inputs,
|
|
93
|
+
});
|
|
94
|
+
const readers = new Map<string, Uint8Array>();
|
|
95
|
+
let readerSequence = 0;
|
|
96
|
+
const artifactReader: FirmwareArtifactReader = {
|
|
97
|
+
open({ artifactRef }) {
|
|
98
|
+
const binary = binaries.get(artifactRef);
|
|
99
|
+
if (!binary) {
|
|
100
|
+
throw ERRORS.TypedError(
|
|
101
|
+
HardwareErrorCode.RuntimeError,
|
|
102
|
+
'Firmware memory artifact is unavailable'
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
readerSequence += 1;
|
|
106
|
+
const readerId = `fwmemreader:${hostId}:${readerSequence}`;
|
|
107
|
+
readers.set(readerId, binary);
|
|
108
|
+
return Promise.resolve({ readerId, size: binary.byteLength });
|
|
109
|
+
},
|
|
110
|
+
read({ readerId, offset, length }) {
|
|
111
|
+
const binary = readers.get(readerId);
|
|
112
|
+
if (!binary || offset < 0 || length <= 0 || offset + length > binary.byteLength) {
|
|
113
|
+
throw ERRORS.TypedError(
|
|
114
|
+
HardwareErrorCode.RuntimeError,
|
|
115
|
+
'Firmware memory artifact read is invalid'
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
const data = binary.slice(offset, offset + length).buffer;
|
|
119
|
+
return Promise.resolve({
|
|
120
|
+
data,
|
|
121
|
+
bytesRead: data.byteLength,
|
|
122
|
+
eof: offset + length === binary.byteLength,
|
|
123
|
+
});
|
|
124
|
+
},
|
|
125
|
+
close({ readerId }) {
|
|
126
|
+
readers.delete(readerId);
|
|
127
|
+
return Promise.resolve();
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
const hostBindingGeneration = sdk.registerFirmwareUpdateHostBinding({
|
|
131
|
+
artifactReader,
|
|
132
|
+
preparedPlanDigest: preparedPlan.preparedPlanDigest,
|
|
133
|
+
});
|
|
134
|
+
return {
|
|
135
|
+
preparedPlan,
|
|
136
|
+
hostBindingGeneration,
|
|
137
|
+
release: () => {
|
|
138
|
+
sdk.unregisterFirmwareUpdateHostBinding(hostBindingGeneration);
|
|
139
|
+
readers.clear();
|
|
140
|
+
binaries.clear();
|
|
141
|
+
},
|
|
142
|
+
};
|
|
143
|
+
}
|
|
@@ -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
|
+
};
|