@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.
Files changed (103) hide show
  1. package/__tests__/check-all-firmware-release-protocol-v2.test.ts +411 -86
  2. package/__tests__/check-firmware-release-protocol-v2.test.ts +199 -0
  3. package/__tests__/firmware-memory-host.test.ts +126 -0
  4. package/__tests__/firmware-update/device-update-bootloader.test.ts +14 -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-bootloader-poll.test.ts +65 -2
  9. package/__tests__/firmware-update/firmware-update-plan-bootloader-identity.test.ts +80 -0
  10. package/__tests__/firmware-update/firmware-update-plan.test.ts +293 -71
  11. package/__tests__/firmware-update/firmware-update-prepared-plan.test.ts +53 -1
  12. package/__tests__/firmware-update/firmware-update-prepared-resource-executors.test.ts +326 -0
  13. package/__tests__/firmware-update/firmware-update-v2-download-before-boot.test.ts +2 -2
  14. package/__tests__/protocol-v2-firmware-targets.test.ts +16 -0
  15. package/__tests__/protocol-v2-resources.test.ts +148 -232
  16. package/__tests__/protocol-v2.test.ts +1326 -510
  17. package/dist/api/CheckAllFirmwareRelease.d.ts +2 -3
  18. package/dist/api/CheckAllFirmwareRelease.d.ts.map +1 -1
  19. package/dist/api/CheckBLEFirmwareRelease.d.ts +3 -2
  20. package/dist/api/CheckBLEFirmwareRelease.d.ts.map +1 -1
  21. package/dist/api/CheckBootloaderRelease.d.ts +2 -1
  22. package/dist/api/CheckBootloaderRelease.d.ts.map +1 -1
  23. package/dist/api/CheckFirmwareRelease.d.ts +3 -2
  24. package/dist/api/CheckFirmwareRelease.d.ts.map +1 -1
  25. package/dist/api/FirmwareUpdateV2.d.ts +1 -0
  26. package/dist/api/FirmwareUpdateV2.d.ts.map +1 -1
  27. package/dist/api/FirmwareUpdateV3.d.ts.map +1 -1
  28. package/dist/api/FirmwareUpdateV4.d.ts +11 -9
  29. package/dist/api/FirmwareUpdateV4.d.ts.map +1 -1
  30. package/dist/api/UploadPortfolio.d.ts.map +1 -1
  31. package/dist/api/device/DeviceUpdateBootloader.d.ts.map +1 -1
  32. package/dist/api/firmware/FirmwareHostBinding.d.ts +1 -1
  33. package/dist/api/firmware/FirmwareHostBinding.d.ts.map +1 -1
  34. package/dist/api/firmware/FirmwareMemoryHost.d.ts +22 -0
  35. package/dist/api/firmware/FirmwareMemoryHost.d.ts.map +1 -0
  36. package/dist/api/firmware/FirmwarePreparedResourceArchive.d.ts +11 -0
  37. package/dist/api/firmware/FirmwarePreparedResourceArchive.d.ts.map +1 -0
  38. package/dist/api/firmware/FirmwareUpdateBaseMethod.d.ts.map +1 -1
  39. package/dist/api/firmware/FirmwareUpdatePlan.d.ts +48 -1
  40. package/dist/api/firmware/FirmwareUpdatePlan.d.ts.map +1 -1
  41. package/dist/api/firmware/FirmwareUpdatePreparedPlan.d.ts +7 -1
  42. package/dist/api/firmware/FirmwareUpdatePreparedPlan.d.ts.map +1 -1
  43. package/dist/api/firmware/getBinary.d.ts +0 -1
  44. package/dist/api/firmware/getBinary.d.ts.map +1 -1
  45. package/dist/api/firmware/protocolV2Release.d.ts +33 -0
  46. package/dist/api/firmware/protocolV2Release.d.ts.map +1 -0
  47. package/dist/data-manager/DataManager.d.ts +4 -3
  48. package/dist/data-manager/DataManager.d.ts.map +1 -1
  49. package/dist/index.d.ts +108 -114
  50. package/dist/index.d.ts.map +1 -1
  51. package/dist/index.js +1825 -1006
  52. package/dist/protocols/protocol-v2/resources.d.ts +10 -28
  53. package/dist/protocols/protocol-v2/resources.d.ts.map +1 -1
  54. package/dist/types/api/checkAllFirmwareRelease.d.ts +20 -8
  55. package/dist/types/api/checkAllFirmwareRelease.d.ts.map +1 -1
  56. package/dist/types/api/checkBLEFirmwareRelease.d.ts +2 -13
  57. package/dist/types/api/checkBLEFirmwareRelease.d.ts.map +1 -1
  58. package/dist/types/api/checkBootloaderRelease.d.ts +2 -6
  59. package/dist/types/api/checkBootloaderRelease.d.ts.map +1 -1
  60. package/dist/types/api/checkFirmwareRelease.d.ts +2 -13
  61. package/dist/types/api/checkFirmwareRelease.d.ts.map +1 -1
  62. package/dist/types/api/checkFirmwareTypeAvailable.d.ts +2 -2
  63. package/dist/types/api/checkFirmwareTypeAvailable.d.ts.map +1 -1
  64. package/dist/types/api/deviceUpdateBootloader.d.ts.map +1 -1
  65. package/dist/types/api/export.d.ts +1 -1
  66. package/dist/types/api/export.d.ts.map +1 -1
  67. package/dist/types/api/firmwareUpdate.d.ts +4 -12
  68. package/dist/types/api/firmwareUpdate.d.ts.map +1 -1
  69. package/dist/types/api/firmwareUpdatePlan.d.ts +2 -2
  70. package/dist/types/api/firmwareUpdatePlan.d.ts.map +1 -1
  71. package/dist/types/settings.d.ts +35 -36
  72. package/dist/types/settings.d.ts.map +1 -1
  73. package/package.json +4 -4
  74. package/src/api/CheckAllFirmwareRelease.ts +113 -80
  75. package/src/api/CheckBLEFirmwareRelease.ts +37 -5
  76. package/src/api/CheckBootloaderRelease.ts +35 -3
  77. package/src/api/CheckFirmwareRelease.ts +35 -7
  78. package/src/api/FirmwareUpdateV2.ts +231 -138
  79. package/src/api/FirmwareUpdateV3.ts +101 -56
  80. package/src/api/FirmwareUpdateV4.ts +936 -408
  81. package/src/api/UploadPortfolio.ts +18 -0
  82. package/src/api/device/DeviceUpdateBootloader.ts +37 -11
  83. package/src/api/firmware/FirmwareHostBinding.ts +26 -3
  84. package/src/api/firmware/FirmwareMemoryHost.ts +143 -0
  85. package/src/api/firmware/FirmwarePreparedResourceArchive.ts +207 -0
  86. package/src/api/firmware/FirmwareUpdateBaseMethod.ts +122 -73
  87. package/src/api/firmware/FirmwareUpdatePlan.ts +295 -122
  88. package/src/api/firmware/FirmwareUpdatePreparedPlan.ts +49 -12
  89. package/src/api/firmware/protocolV2Release.ts +168 -0
  90. package/src/data-manager/DataManager.ts +35 -19
  91. package/src/index.ts +10 -0
  92. package/src/protocols/protocol-v2/resources.ts +157 -341
  93. package/src/types/api/checkAllFirmwareRelease.ts +27 -12
  94. package/src/types/api/checkBLEFirmwareRelease.ts +4 -13
  95. package/src/types/api/checkBootloaderRelease.ts +2 -6
  96. package/src/types/api/checkFirmwareRelease.ts +2 -13
  97. package/src/types/api/checkFirmwareTypeAvailable.ts +2 -2
  98. package/src/types/api/deviceUpdateBootloader.ts +1 -0
  99. package/src/types/api/export.ts +6 -1
  100. package/src/types/api/firmwareUpdate.ts +12 -17
  101. package/src/types/api/firmwareUpdatePlan.ts +2 -2
  102. package/src/types/settings.ts +35 -61
  103. 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 hostBinding = payload.artifact
161
- ? resolveFirmwareUpdateHostBinding(payload.hostBindingGeneration)
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: hostBinding?.artifactReader ?? payload.artifactReader,
193
+ artifactReader,
166
194
  };
167
- if (payload.artifact) {
195
+ if (preparedPlan && plannedArtifact) {
168
196
  assertFirmwareUpdatePreparedPlanDeviceIdentity({
169
- preparedPlan: payload.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: payload.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: payload.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
- ): FirmwareUpdateHostBinding => firmwareHostBindingRegistry.resolve(generation ?? Number.NaN);
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
+ };