@onekeyfe/hd-core 1.2.0-alpha.97 → 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.
Files changed (98) hide show
  1. package/__tests__/check-all-firmware-release-protocol-v2.test.ts +404 -84
  2. package/__tests__/check-firmware-release-protocol-v2.test.ts +199 -0
  3. package/__tests__/firmware-memory-host.test.ts +112 -0
  4. package/__tests__/firmware-update/device-update-bootloader.test.ts +13 -2
  5. package/__tests__/firmware-update/firmware-host-binding.test.ts +9 -0
  6. package/__tests__/firmware-update/firmware-prepared-host-digest.test.ts +573 -0
  7. package/__tests__/firmware-update/firmware-prepared-resource-archive.test.ts +150 -0
  8. package/__tests__/firmware-update/firmware-update-plan.test.ts +232 -70
  9. package/__tests__/firmware-update/firmware-update-prepared-plan.test.ts +53 -1
  10. package/__tests__/firmware-update/firmware-update-prepared-resource-executors.test.ts +326 -0
  11. package/__tests__/firmware-update/firmware-update-v2-download-before-boot.test.ts +2 -2
  12. package/__tests__/protocol-v2-resources.test.ts +148 -232
  13. package/__tests__/protocol-v2.test.ts +1236 -516
  14. package/dist/api/CheckAllFirmwareRelease.d.ts +2 -3
  15. package/dist/api/CheckAllFirmwareRelease.d.ts.map +1 -1
  16. package/dist/api/CheckBLEFirmwareRelease.d.ts +3 -2
  17. package/dist/api/CheckBLEFirmwareRelease.d.ts.map +1 -1
  18. package/dist/api/CheckBootloaderRelease.d.ts +2 -1
  19. package/dist/api/CheckBootloaderRelease.d.ts.map +1 -1
  20. package/dist/api/CheckFirmwareRelease.d.ts +3 -2
  21. package/dist/api/CheckFirmwareRelease.d.ts.map +1 -1
  22. package/dist/api/FirmwareUpdateV2.d.ts +1 -0
  23. package/dist/api/FirmwareUpdateV2.d.ts.map +1 -1
  24. package/dist/api/FirmwareUpdateV3.d.ts.map +1 -1
  25. package/dist/api/FirmwareUpdateV4.d.ts +8 -8
  26. package/dist/api/FirmwareUpdateV4.d.ts.map +1 -1
  27. package/dist/api/UploadPortfolio.d.ts.map +1 -1
  28. package/dist/api/device/DeviceUpdateBootloader.d.ts.map +1 -1
  29. package/dist/api/firmware/FirmwareHostBinding.d.ts +1 -1
  30. package/dist/api/firmware/FirmwareHostBinding.d.ts.map +1 -1
  31. package/dist/api/firmware/FirmwareMemoryHost.d.ts +22 -0
  32. package/dist/api/firmware/FirmwareMemoryHost.d.ts.map +1 -0
  33. package/dist/api/firmware/FirmwarePreparedResourceArchive.d.ts +11 -0
  34. package/dist/api/firmware/FirmwarePreparedResourceArchive.d.ts.map +1 -0
  35. package/dist/api/firmware/FirmwareUpdatePlan.d.ts +48 -1
  36. package/dist/api/firmware/FirmwareUpdatePlan.d.ts.map +1 -1
  37. package/dist/api/firmware/FirmwareUpdatePreparedPlan.d.ts +6 -1
  38. package/dist/api/firmware/FirmwareUpdatePreparedPlan.d.ts.map +1 -1
  39. package/dist/api/firmware/getBinary.d.ts +0 -1
  40. package/dist/api/firmware/getBinary.d.ts.map +1 -1
  41. package/dist/api/firmware/protocolV2Release.d.ts +33 -0
  42. package/dist/api/firmware/protocolV2Release.d.ts.map +1 -0
  43. package/dist/data-manager/DataManager.d.ts +4 -3
  44. package/dist/data-manager/DataManager.d.ts.map +1 -1
  45. package/dist/index.d.ts +108 -114
  46. package/dist/index.d.ts.map +1 -1
  47. package/dist/index.js +1633 -967
  48. package/dist/protocols/protocol-v2/resources.d.ts +10 -28
  49. package/dist/protocols/protocol-v2/resources.d.ts.map +1 -1
  50. package/dist/types/api/checkAllFirmwareRelease.d.ts +20 -8
  51. package/dist/types/api/checkAllFirmwareRelease.d.ts.map +1 -1
  52. package/dist/types/api/checkBLEFirmwareRelease.d.ts +2 -13
  53. package/dist/types/api/checkBLEFirmwareRelease.d.ts.map +1 -1
  54. package/dist/types/api/checkBootloaderRelease.d.ts +2 -6
  55. package/dist/types/api/checkBootloaderRelease.d.ts.map +1 -1
  56. package/dist/types/api/checkFirmwareRelease.d.ts +2 -13
  57. package/dist/types/api/checkFirmwareRelease.d.ts.map +1 -1
  58. package/dist/types/api/checkFirmwareTypeAvailable.d.ts +2 -2
  59. package/dist/types/api/checkFirmwareTypeAvailable.d.ts.map +1 -1
  60. package/dist/types/api/deviceUpdateBootloader.d.ts.map +1 -1
  61. package/dist/types/api/export.d.ts +1 -1
  62. package/dist/types/api/export.d.ts.map +1 -1
  63. package/dist/types/api/firmwareUpdate.d.ts +4 -12
  64. package/dist/types/api/firmwareUpdate.d.ts.map +1 -1
  65. package/dist/types/api/firmwareUpdatePlan.d.ts +2 -2
  66. package/dist/types/api/firmwareUpdatePlan.d.ts.map +1 -1
  67. package/dist/types/settings.d.ts +35 -36
  68. package/dist/types/settings.d.ts.map +1 -1
  69. package/package.json +4 -4
  70. package/src/api/CheckAllFirmwareRelease.ts +108 -80
  71. package/src/api/CheckBLEFirmwareRelease.ts +37 -5
  72. package/src/api/CheckBootloaderRelease.ts +35 -3
  73. package/src/api/CheckFirmwareRelease.ts +35 -7
  74. package/src/api/FirmwareUpdateV2.ts +108 -73
  75. package/src/api/FirmwareUpdateV3.ts +101 -56
  76. package/src/api/FirmwareUpdateV4.ts +885 -403
  77. package/src/api/UploadPortfolio.ts +18 -0
  78. package/src/api/device/DeviceUpdateBootloader.ts +37 -11
  79. package/src/api/firmware/FirmwareHostBinding.ts +16 -3
  80. package/src/api/firmware/FirmwareMemoryHost.ts +142 -0
  81. package/src/api/firmware/FirmwarePreparedResourceArchive.ts +207 -0
  82. package/src/api/firmware/FirmwareUpdatePlan.ts +282 -122
  83. package/src/api/firmware/FirmwareUpdatePreparedPlan.ts +27 -3
  84. package/src/api/firmware/protocolV2Release.ts +168 -0
  85. package/src/data-manager/DataManager.ts +35 -19
  86. package/src/index.ts +10 -0
  87. package/src/protocols/protocol-v2/resources.ts +157 -341
  88. package/src/types/api/checkAllFirmwareRelease.ts +27 -12
  89. package/src/types/api/checkBLEFirmwareRelease.ts +4 -13
  90. package/src/types/api/checkBootloaderRelease.ts +2 -6
  91. package/src/types/api/checkFirmwareRelease.ts +2 -13
  92. package/src/types/api/checkFirmwareTypeAvailable.ts +2 -2
  93. package/src/types/api/deviceUpdateBootloader.ts +1 -0
  94. package/src/types/api/export.ts +6 -1
  95. package/src/types/api/firmwareUpdate.ts +12 -17
  96. package/src/types/api/firmwareUpdatePlan.ts +2 -2
  97. package/src/types/settings.ts +35 -61
  98. package/src/utils/deviceFeaturesUtils.ts +2 -2
@@ -0,0 +1,150 @@
1
+ import { sha256 } from '@noble/hashes/sha256';
2
+ import { bytesToHex } from '@noble/hashes/utils';
3
+ import { EFirmwareType } from '@onekeyfe/hd-shared';
4
+ import JSZip from 'jszip';
5
+
6
+ import { readVerifiedPreparedResourceArchive } from '../../src/api/firmware/FirmwarePreparedResourceArchive';
7
+ import { prepareFirmwareUpdatePlan } from '../../src/api/firmware/FirmwareUpdatePreparedPlan';
8
+ import { digestFirmwareUpdateContract } from '../../src/api/firmware/FirmwareUpdatePlan';
9
+
10
+ import type {
11
+ FirmwareArtifactReader,
12
+ FirmwareArtifactReference,
13
+ } from '../../src/types/api/firmwareUpdate';
14
+ import type { FirmwareUpdatePlan } from '../../src/types/api/firmwareUpdatePlan';
15
+
16
+ jest.mock('../../src/data/config', () => ({
17
+ getSDKVersion: jest.fn(() => '1.0.0'),
18
+ DEFAULT_DOMAIN: 'https://jssdk.onekey.so/1.0.0/',
19
+ }));
20
+
21
+ const createReference = (artifactRef: string, binary: ArrayBuffer): FirmwareArtifactReference => ({
22
+ artifactRef,
23
+ size: binary.byteLength,
24
+ sha256: bytesToHex(sha256(new Uint8Array(binary))),
25
+ });
26
+
27
+ const createReader = (artifacts: Map<string, ArrayBuffer>): FirmwareArtifactReader => {
28
+ const opened = new Map<string, ArrayBuffer>();
29
+ let sequence = 0;
30
+ return {
31
+ open({ artifactRef }) {
32
+ const binary = artifacts.get(artifactRef);
33
+ if (!binary) throw new Error(`missing artifact: ${artifactRef}`);
34
+ sequence += 1;
35
+ const readerId = `reader-${sequence}`;
36
+ opened.set(readerId, binary);
37
+ return Promise.resolve({ readerId, size: binary.byteLength });
38
+ },
39
+ read({ readerId, offset, length }) {
40
+ const binary = opened.get(readerId);
41
+ if (!binary) throw new Error(`missing reader: ${readerId}`);
42
+ const data = binary.slice(offset, offset + length);
43
+ return Promise.resolve({
44
+ data,
45
+ bytesRead: data.byteLength,
46
+ eof: offset + length === binary.byteLength,
47
+ });
48
+ },
49
+ close({ readerId }) {
50
+ opened.delete(readerId);
51
+ return Promise.resolve();
52
+ },
53
+ };
54
+ };
55
+
56
+ const createPreparedResourcePlan = ({
57
+ executor,
58
+ archiveBinary,
59
+ entryBinary,
60
+ }: {
61
+ executor: 'v2' | 'v3';
62
+ archiveBinary: ArrayBuffer;
63
+ entryBinary: ArrayBuffer;
64
+ }) => {
65
+ const archiveArtifact = createReference('resource-archive', archiveBinary);
66
+ const entryArtifact = createReference('resource-entry', entryBinary);
67
+ const planWithoutDigest: Omit<FirmwareUpdatePlan, 'planDigest'> = {
68
+ schemaVersion: 2,
69
+ executor,
70
+ deviceIdentity: `${executor}-device`,
71
+ deviceModel: executor === 'v2' ? 'TOUCH' : 'PRO',
72
+ firmwareType: EFirmwareType.Universal,
73
+ platform: 'desktop',
74
+ artifacts: [
75
+ {
76
+ artifactId: 'resource',
77
+ role: 'resource',
78
+ target: 'resource',
79
+ url: 'https://firmware.onekey.so/resource.zip',
80
+ container: 'zip',
81
+ expectedSize: archiveArtifact.size,
82
+ expectedSha256: archiveArtifact.sha256,
83
+ },
84
+ ],
85
+ targetsToUpdate: ['resource'],
86
+ };
87
+ const plan: FirmwareUpdatePlan = {
88
+ ...planWithoutDigest,
89
+ planDigest: digestFirmwareUpdateContract(planWithoutDigest),
90
+ };
91
+ return {
92
+ archiveArtifact,
93
+ preparedPlan: prepareFirmwareUpdatePlan({
94
+ plan,
95
+ leaseRef: `${executor}-resource-lease`,
96
+ artifacts: [
97
+ {
98
+ artifactId: 'resource',
99
+ artifact: archiveArtifact,
100
+ materializedEntries: [{ entryName: 'images/logo.bin', artifact: entryArtifact }],
101
+ },
102
+ ],
103
+ }),
104
+ };
105
+ };
106
+
107
+ describe.each(['v2', 'v3'] as const)('%s prepared resource archive binding', executor => {
108
+ test('uses canonical bytes extracted from the approved ZIP', async () => {
109
+ const entryBinary = Uint8Array.from([1, 2, 3, 4]).buffer;
110
+ const zip = new JSZip();
111
+ zip.file('images/logo.bin', entryBinary);
112
+ const archiveBinary = await zip.generateAsync({ type: 'arraybuffer' });
113
+ const { archiveArtifact, preparedPlan } = createPreparedResourcePlan({
114
+ executor,
115
+ archiveBinary,
116
+ entryBinary,
117
+ });
118
+
119
+ const result = await readVerifiedPreparedResourceArchive({
120
+ preparedPlan,
121
+ reader: createReader(new Map([[archiveArtifact.artifactRef, archiveBinary]])),
122
+ });
123
+
124
+ expect(result).toHaveLength(1);
125
+ expect(result[0].entryName).toBe('logo.bin');
126
+ expect(new Uint8Array(result[0].binary)).toEqual(new Uint8Array(entryBinary));
127
+ });
128
+
129
+ test('rejects entries that are not derived from the approved ZIP', async () => {
130
+ const approvedEntry = Uint8Array.from([1, 2, 3, 4]).buffer;
131
+ const substitutedEntry = Uint8Array.from([9, 9, 9, 9]).buffer;
132
+ const zip = new JSZip();
133
+ zip.file('images/logo.bin', approvedEntry);
134
+ const archiveBinary = await zip.generateAsync({ type: 'arraybuffer' });
135
+ const { archiveArtifact, preparedPlan } = createPreparedResourcePlan({
136
+ executor,
137
+ archiveBinary,
138
+ entryBinary: substitutedEntry,
139
+ });
140
+
141
+ await expect(
142
+ readVerifiedPreparedResourceArchive({
143
+ preparedPlan,
144
+ reader: createReader(new Map([[archiveArtifact.artifactRef, archiveBinary]])),
145
+ })
146
+ ).rejects.toMatchObject({
147
+ params: { firmwareUpdateCode: 'FirmwareArtifactReceiptMismatch' },
148
+ });
149
+ });
150
+ });
@@ -1,6 +1,10 @@
1
1
  import { EDeviceType, EFirmwareType } from '@onekeyfe/hd-shared';
2
2
 
3
3
  import { buildFirmwareUpdatePlan } from '../../src/api/firmware/FirmwareUpdatePlan';
4
+ import {
5
+ assertFirmwareUpdatePreparedPlanDeviceIdentity,
6
+ prepareFirmwareUpdatePlan,
7
+ } from '../../src/api/firmware/FirmwareUpdatePreparedPlan';
4
8
 
5
9
  import type { Features } from '../../src/types';
6
10
 
@@ -50,6 +54,8 @@ const createLegacyForceInput = (
50
54
  release: Record<string, unknown> = {
51
55
  url: 'https://firmware.onekey.so/pro/firmware.bin',
52
56
  version: [4, 21, 0],
57
+ expectedSize: 1024,
58
+ fingerprint: '1'.repeat(64),
53
59
  }
54
60
  ): BuildPlanInput => ({
55
61
  features: createFeatures({
@@ -168,6 +174,8 @@ describe('buildFirmwareUpdatePlan', () => {
168
174
  release: {
169
175
  url: 'https://firmware.onekey.so/pro/firmware.bin',
170
176
  version: [4, 0, 0],
177
+ expectedSize: 1024,
178
+ fingerprint: '1'.repeat(64),
171
179
  },
172
180
  },
173
181
  ble: noUpdate,
@@ -193,7 +201,11 @@ describe('buildFirmwareUpdatePlan', () => {
193
201
  release: {
194
202
  url: 'https://firmware.onekey.so/pro/firmware.bin',
195
203
  version: [4, 21, 0],
204
+ expectedSize: 1024,
205
+ fingerprint: '1'.repeat(64),
196
206
  resource: 'https://firmware.onekey.so/pro/resource.zip',
207
+ resourceExpectedSize: 4096,
208
+ resourceFingerprint: '1'.repeat(64),
197
209
  },
198
210
  },
199
211
  ble: {
@@ -201,6 +213,8 @@ describe('buildFirmwareUpdatePlan', () => {
201
213
  release: {
202
214
  webUpdate: 'https://firmware.onekey.so/pro/ble.bin',
203
215
  version: [2, 3, 7],
216
+ expectedSize: 512,
217
+ fingerprintWeb: '2'.repeat(64),
204
218
  },
205
219
  },
206
220
  bootloader: {
@@ -208,6 +222,8 @@ describe('buildFirmwareUpdatePlan', () => {
208
222
  release: {
209
223
  bootloaderResource: 'https://firmware.onekey.so/pro/bootloader.bin',
210
224
  bootloaderVersion: [2, 8, 4],
225
+ bootloaderExpectedSize: 768,
226
+ bootloaderFingerprint: '3'.repeat(64),
211
227
  },
212
228
  },
213
229
  forceUpdateTargets: ['firmware', 'resource', 'ble', 'bootloader'],
@@ -237,6 +253,8 @@ describe('buildFirmwareUpdatePlan', () => {
237
253
  release: {
238
254
  url: 'https://firmware.onekey.so/pro/firmware.bin',
239
255
  version: [4, 21, 0],
256
+ expectedSize: 1024,
257
+ fingerprint: '1'.repeat(64),
240
258
  resource: 'https://firmware.onekey.so/pro/resource.zip',
241
259
  },
242
260
  },
@@ -249,6 +267,32 @@ describe('buildFirmwareUpdatePlan', () => {
249
267
  expect(plan.targetsToUpdate).toEqual(['firmware']);
250
268
  });
251
269
 
270
+ test('does not approve an unexecutable V2 resource during bootloader recovery', () => {
271
+ const features = createFeatures({ deviceType: EDeviceType.Classic1s });
272
+ features.bootloaderMode = true;
273
+ const plan = buildFirmwareUpdatePlan({
274
+ features,
275
+ firmwareType: EFirmwareType.Universal,
276
+ platform: 'desktop',
277
+ firmware: {
278
+ status: 'none',
279
+ release: {
280
+ url: 'https://firmware.onekey.so/classic/firmware.bin',
281
+ expectedSize: 1024,
282
+ fingerprint: '1'.repeat(64),
283
+ resource: 'https://firmware.onekey.so/classic/resource.zip',
284
+ resourceExpectedSize: 4096,
285
+ resourceFingerprint: '2'.repeat(64),
286
+ },
287
+ },
288
+ ble: noUpdate,
289
+ bootloader: noUpdate,
290
+ });
291
+
292
+ expect(plan.artifacts.map(artifact => artifact.artifactId)).toEqual(['firmware']);
293
+ expect(plan.targetsToUpdate).toEqual(['firmware']);
294
+ });
295
+
252
296
  test.each([
253
297
  {
254
298
  label: 'a non-array value',
@@ -272,6 +316,65 @@ describe('buildFirmwareUpdatePlan', () => {
272
316
  expectFirmwarePlanInvalid(() => buildFirmwareUpdatePlan(createLegacyForceInput(['resource'])));
273
317
  });
274
318
 
319
+ test('rejects a legacy resource archive without complete integrity metadata', () => {
320
+ expectFirmwarePlanInvalid(
321
+ () =>
322
+ buildFirmwareUpdatePlan(
323
+ createLegacyForceInput(['resource'], {
324
+ url: 'https://firmware.onekey.so/pro/firmware.bin',
325
+ resource: 'https://firmware.onekey.so/pro/resource.zip',
326
+ resourceExpectedSize: 4096,
327
+ })
328
+ ),
329
+ 'integrity metadata is invalid'
330
+ );
331
+ });
332
+
333
+ test.each(['firmware', 'ble', 'bootloader'] as const)(
334
+ 'rejects a remote legacy %s artifact without complete integrity metadata',
335
+ target => {
336
+ expectFirmwarePlanInvalid(
337
+ () =>
338
+ buildFirmwareUpdatePlan({
339
+ features: createFeatures({ deviceType: EDeviceType.Classic1s }),
340
+ firmwareType: EFirmwareType.Universal,
341
+ platform: 'desktop',
342
+ firmware:
343
+ target === 'firmware'
344
+ ? {
345
+ status: 'outdated',
346
+ release: {
347
+ url: 'https://firmware.onekey.so/classic/firmware.bin',
348
+ expectedSize: 1024,
349
+ },
350
+ }
351
+ : noUpdate,
352
+ ble:
353
+ target === 'ble'
354
+ ? {
355
+ status: 'outdated',
356
+ release: {
357
+ webUpdate: 'https://firmware.onekey.so/classic/ble.bin',
358
+ expectedSize: 512,
359
+ },
360
+ }
361
+ : noUpdate,
362
+ bootloader:
363
+ target === 'bootloader'
364
+ ? {
365
+ status: 'outdated',
366
+ release: {
367
+ bootloaderResource: 'https://firmware.onekey.so/classic/bootloader.bin',
368
+ bootloaderExpectedSize: 768,
369
+ },
370
+ }
371
+ : noUpdate,
372
+ }),
373
+ 'integrity metadata is invalid'
374
+ );
375
+ }
376
+ );
377
+
275
378
  test.each(['ble', 'bootloader'] as const)(
276
379
  'rejects the unsupported Pro2 %s force target',
277
380
  forceTarget => {
@@ -297,12 +400,6 @@ describe('buildFirmwareUpdatePlan', () => {
297
400
  forceTarget: 'firmware' as const,
298
401
  release: {
299
402
  components: {},
300
- resourceBundles: [
301
- {
302
- name: 'images',
303
- url: 'https://firmware.onekey.so/pro2/images.okpkg',
304
- },
305
- ],
306
403
  },
307
404
  },
308
405
  {
@@ -312,6 +409,8 @@ describe('buildFirmwareUpdatePlan', () => {
312
409
  applicationP1: {
313
410
  target: 'APPLICATION_P1',
314
411
  url: 'https://firmware.onekey.so/pro2/application-p1.bin',
412
+ expectedSize: 1024,
413
+ fingerprint: '1111111111111111111111111111111111111111111111111111111111111111',
315
414
  },
316
415
  },
317
416
  },
@@ -325,41 +424,23 @@ describe('buildFirmwareUpdatePlan', () => {
325
424
  }
326
425
  );
327
426
 
328
- test.each([
329
- {
330
- forceTarget: 'firmware' as const,
331
- expectedArtifactIds: ['component:app_v1'],
332
- expectedTargets: ['app_v1'],
333
- },
334
- {
335
- forceTarget: 'resource' as const,
336
- expectedArtifactIds: ['resourceBundle:images'],
337
- expectedTargets: ['resource'],
338
- },
339
- ])(
340
- 'limits a Pro2 $forceTarget force to the selected artifact role',
341
- ({ forceTarget, expectedArtifactIds, expectedTargets }) => {
342
- const plan = buildFirmwareUpdatePlan(
343
- createProtocolV2ForceInput([forceTarget], {
344
- components: {
345
- applicationP1: {
346
- target: 'APPLICATION_P1',
347
- url: 'https://firmware.onekey.so/pro2/application-p1.bin',
348
- },
427
+ test('limits a Pro2 firmware force to component artifacts', () => {
428
+ const plan = buildFirmwareUpdatePlan(
429
+ createProtocolV2ForceInput(['firmware'], {
430
+ components: {
431
+ applicationP1: {
432
+ target: 'APPLICATION_P1',
433
+ url: 'https://firmware.onekey.so/pro2/application-p1.bin',
434
+ expectedSize: 1024,
435
+ fingerprint: '1111111111111111111111111111111111111111111111111111111111111111',
349
436
  },
350
- resourceBundles: [
351
- {
352
- name: 'images',
353
- url: 'https://firmware.onekey.so/pro2/images.okpkg',
354
- },
355
- ],
356
- })
357
- );
437
+ },
438
+ })
439
+ );
358
440
 
359
- expect(plan.artifacts.map(artifact => artifact.artifactId)).toEqual(expectedArtifactIds);
360
- expect(plan.targetsToUpdate).toEqual(expectedTargets);
361
- }
362
- );
441
+ expect(plan.artifacts.map(artifact => artifact.artifactId)).toEqual(['component:app_v1']);
442
+ expect(plan.targetsToUpdate).toEqual(['app_v1']);
443
+ });
363
444
 
364
445
  test('rejects a prepared native plan without a stable device identity', () => {
365
446
  const features = createFeatures({
@@ -376,6 +457,8 @@ describe('buildFirmwareUpdatePlan', () => {
376
457
  status: 'outdated',
377
458
  release: {
378
459
  url: 'https://firmware.onekey.so/classic/firmware.bin',
460
+ expectedSize: 1024,
461
+ fingerprint: '1'.repeat(64),
379
462
  },
380
463
  },
381
464
  ble: noUpdate,
@@ -384,7 +467,72 @@ describe('buildFirmwareUpdatePlan', () => {
384
467
  ).toThrow('requires a stable device identity');
385
468
  });
386
469
 
387
- test('maps the Pro2 component order and resource bundles without downloading them', () => {
470
+ test('allows an identity-less Protocol V2 device while retaining the model binding', () => {
471
+ const features = createFeatures({
472
+ deviceType: EDeviceType.Pro2,
473
+ });
474
+ delete (features as Partial<Features>).serialNo;
475
+
476
+ const plan = buildFirmwareUpdatePlan({
477
+ features,
478
+ firmwareType: EFirmwareType.Universal,
479
+ platform: 'desktop',
480
+ firmware: {
481
+ status: 'outdated',
482
+ release: {
483
+ components: {
484
+ applicationP1: {
485
+ target: 'APPLICATION_P1',
486
+ url: 'https://firmware.onekey.so/pro2/application-p1.bin',
487
+ expectedSize: 1024,
488
+ fingerprint: '1111111111111111111111111111111111111111111111111111111111111111',
489
+ },
490
+ },
491
+ },
492
+ },
493
+ ble: noUpdate,
494
+ bootloader: noUpdate,
495
+ });
496
+
497
+ expect(plan).toMatchObject({
498
+ executor: 'v4',
499
+ deviceIdentity: 'unavailable',
500
+ deviceModel: String(EDeviceType.Pro2),
501
+ });
502
+
503
+ const preparedPlan = prepareFirmwareUpdatePlan({
504
+ plan,
505
+ leaseRef: 'fwlease:00000000-0000-4000-8000-000000000004',
506
+ artifacts: [
507
+ {
508
+ artifactId: 'component:app_v1',
509
+ artifact: {
510
+ artifactRef: `fw:${'1'.repeat(64)}`,
511
+ size: 1024,
512
+ sha256: '1'.repeat(64),
513
+ },
514
+ },
515
+ ],
516
+ });
517
+ expect(() =>
518
+ assertFirmwareUpdatePreparedPlanDeviceIdentity({
519
+ preparedPlan,
520
+ deviceIdentity: undefined,
521
+ bootloaderMode: false,
522
+ deviceModel: String(EDeviceType.Pro2),
523
+ })
524
+ ).not.toThrow();
525
+ expect(() =>
526
+ assertFirmwareUpdatePreparedPlanDeviceIdentity({
527
+ preparedPlan,
528
+ deviceIdentity: undefined,
529
+ bootloaderMode: false,
530
+ deviceModel: String(EDeviceType.Neo),
531
+ })
532
+ ).toThrow();
533
+ });
534
+
535
+ test('maps the Pro2 component order without downloading artifacts', () => {
388
536
  const plan = buildFirmwareUpdatePlan({
389
537
  features: createFeatures({ deviceType: EDeviceType.Pro2 }),
390
538
  firmwareType: EFirmwareType.Universal,
@@ -403,18 +551,16 @@ describe('buildFirmwareUpdatePlan', () => {
403
551
  bootloader: {
404
552
  target: 'BOOTLOADER',
405
553
  url: 'https://firmware.onekey.so/pro2/bootloader.bin',
554
+ expectedSize: 2048,
555
+ fingerprint: '2222222222222222222222222222222222222222222222222222222222222222',
406
556
  },
407
557
  se01: {
408
558
  target: 'SE01',
409
559
  url: 'https://firmware.onekey.so/pro2/se01.bin',
560
+ expectedSize: 512,
561
+ fingerprint: '3333333333333333333333333333333333333333333333333333333333333333',
410
562
  },
411
563
  },
412
- resourceBundles: [
413
- {
414
- name: 'images',
415
- url: 'https://firmware.onekey.so/pro2/images.okpkg',
416
- },
417
- ],
418
564
  },
419
565
  },
420
566
  ble: noUpdate,
@@ -426,9 +572,8 @@ describe('buildFirmwareUpdatePlan', () => {
426
572
  'component:boot',
427
573
  'component:app_v1',
428
574
  'component:se01',
429
- 'resourceBundle:images',
430
575
  ]);
431
- expect(plan.targetsToUpdate).toEqual(['boot', 'app_v1', 'se01', 'resource']);
576
+ expect(plan.targetsToUpdate).toEqual(['boot', 'app_v1', 'se01']);
432
577
  expect(plan.artifacts[1]).toEqual(
433
578
  expect.objectContaining({
434
579
  expectedSize: 1024,
@@ -437,6 +582,41 @@ describe('buildFirmwareUpdatePlan', () => {
437
582
  );
438
583
  });
439
584
 
585
+ test.each([
586
+ {
587
+ expectedSize: undefined,
588
+ fingerprint: '1'.repeat(64),
589
+ },
590
+ {
591
+ expectedSize: 1024,
592
+ fingerprint: undefined,
593
+ },
594
+ ])('rejects a remote Pro2 component without complete integrity metadata', integrity => {
595
+ expectFirmwarePlanInvalid(
596
+ () =>
597
+ buildFirmwareUpdatePlan({
598
+ features: createFeatures({ deviceType: EDeviceType.Pro2 }),
599
+ firmwareType: EFirmwareType.Universal,
600
+ platform: 'desktop',
601
+ firmware: {
602
+ status: 'required',
603
+ release: {
604
+ components: {
605
+ applicationP1: {
606
+ target: 'APPLICATION_P1',
607
+ url: 'https://firmware.onekey.so/pro2/application-p1.bin',
608
+ ...integrity,
609
+ },
610
+ },
611
+ },
612
+ },
613
+ ble: noUpdate,
614
+ bootloader: noUpdate,
615
+ }),
616
+ 'integrity metadata is invalid'
617
+ );
618
+ });
619
+
440
620
  test('rejects Pro2 ROMLOADER because it requires a separate loader flow', () => {
441
621
  expect(() =>
442
622
  buildFirmwareUpdatePlan({
@@ -468,37 +648,19 @@ describe('buildFirmwareUpdatePlan', () => {
468
648
  application: {
469
649
  target: 'APPLICATION_P1',
470
650
  url: 'https://firmware.onekey.so/pro2/application.bin',
651
+ expectedSize: 1024,
652
+ fingerprint: '1111111111111111111111111111111111111111111111111111111111111111',
471
653
  },
472
654
  applicationDuplicate: {
473
655
  target: 'APPLICATION_P1',
474
656
  url: 'https://firmware.onekey.so/pro2/application-duplicate.bin',
657
+ expectedSize: 1024,
658
+ fingerprint: '2222222222222222222222222222222222222222222222222222222222222222',
475
659
  },
476
660
  },
477
661
  },
478
662
  expectedError: 'duplicates target app_v1',
479
663
  },
480
- {
481
- label: 'resource bundle name',
482
- release: {
483
- components: {
484
- application: {
485
- target: 'APPLICATION_P1',
486
- url: 'https://firmware.onekey.so/pro2/application.bin',
487
- },
488
- },
489
- resourceBundles: [
490
- {
491
- name: 'images',
492
- url: 'https://firmware.onekey.so/pro2/images.okpkg',
493
- },
494
- {
495
- name: 'images',
496
- url: 'https://firmware.onekey.so/pro2/images-duplicate.okpkg',
497
- },
498
- ],
499
- },
500
- expectedError: 'duplicates name images',
501
- },
502
664
  ])('rejects a duplicate Pro2 $label', ({ release, expectedError }) => {
503
665
  expect(() =>
504
666
  buildFirmwareUpdatePlan({
@@ -1,6 +1,9 @@
1
1
  import { EDeviceType, EFirmwareType } from '@onekeyfe/hd-shared';
2
2
 
3
- import { buildFirmwareUpdatePlan } from '../../src/api/firmware/FirmwareUpdatePlan';
3
+ import {
4
+ buildFirmwareUpdatePlan,
5
+ digestFirmwareUpdateContract,
6
+ } from '../../src/api/firmware/FirmwareUpdatePlan';
4
7
  import {
5
8
  assertFirmwareUpdatePreparedPlanBinding,
6
9
  assertFirmwareUpdatePreparedPlanDeviceIdentity,
@@ -209,6 +212,55 @@ describe('FirmwareUpdatePreparedPlan', () => {
209
212
  ).toThrow('entry name is invalid');
210
213
  });
211
214
 
215
+ test('rejects duplicate materialized entry names during standalone validation', () => {
216
+ const plan = buildPlan();
217
+ const prepared = prepareFirmwareUpdatePlan({
218
+ plan,
219
+ leaseRef: 'fwlease:00000000-0000-4000-8000-000000000001',
220
+ artifacts: [
221
+ {
222
+ artifactId: 'firmware',
223
+ artifact: artifact('a'.repeat(64), 4),
224
+ },
225
+ {
226
+ artifactId: 'resource',
227
+ artifact: artifact('b'.repeat(64), 8),
228
+ materializedEntries: [
229
+ {
230
+ entryName: 'resource/index.bin',
231
+ artifact: artifact('c'.repeat(64), 2),
232
+ },
233
+ ],
234
+ },
235
+ ],
236
+ });
237
+ const resourceArtifact = prepared.artifacts.find(item => item.target === 'resource')!;
238
+ const tamperedWithoutDigest = {
239
+ ...prepared,
240
+ artifacts: prepared.artifacts.map(item =>
241
+ item === resourceArtifact
242
+ ? {
243
+ ...item,
244
+ materializedEntries: [
245
+ ...resourceArtifact.materializedEntries!,
246
+ {
247
+ entryName: 'other/index.bin',
248
+ artifact: artifact('d'.repeat(64), 2),
249
+ },
250
+ ],
251
+ }
252
+ : item
253
+ ),
254
+ };
255
+ const { preparedPlanDigest: _preparedPlanDigest, ...digestInput } = tamperedWithoutDigest;
256
+ const tampered = {
257
+ ...tamperedWithoutDigest,
258
+ preparedPlanDigest: digestFirmwareUpdateContract(digestInput),
259
+ };
260
+
261
+ expect(() => validateFirmwareUpdatePreparedPlan(tampered)).toThrow('duplicate entry names');
262
+ });
263
+
212
264
  test('rejects unknown fields even when a caller presents a plan-shaped object', () => {
213
265
  const plan = buildPlan();
214
266
  expect(() =>