@onekeyfe/hd-core 1.2.0-alpha.21 → 1.2.0-alpha.22

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 (175) hide show
  1. package/__tests__/DeviceCommands.test.ts +87 -2
  2. package/__tests__/connectSettings.test.ts +47 -5
  3. package/__tests__/device-identity.test.ts +24 -2
  4. package/__tests__/device-settings.test.ts +1 -1
  5. package/__tests__/device-wallet-session-store.test.ts +111 -7
  6. package/__tests__/firmware-update/firmware-artifact-source.test.ts +119 -0
  7. package/__tests__/firmware-update/firmware-host-binding.test.ts +70 -0
  8. package/__tests__/firmware-update/firmware-update-capabilities.test.ts +13 -0
  9. package/__tests__/firmware-update/firmware-update-plan.test.ts +326 -0
  10. package/__tests__/firmware-update/firmware-update-prepared-plan.test.ts +200 -0
  11. package/__tests__/firmware-update/firmware-update-v2-download-before-boot.test.ts +39 -30
  12. package/__tests__/firmware-update/firmware-upload-source.test.ts +70 -0
  13. package/__tests__/open-wallet-session.test.ts +253 -13
  14. package/__tests__/pro2Wallpaper.test.ts +7 -18
  15. package/__tests__/protocol-v2-unlock-policy.test.ts +45 -0
  16. package/__tests__/protocol-v2.test.ts +415 -145
  17. package/__tests__/public-device-state-api.test.ts +69 -62
  18. package/__tests__/public-pro2-api-boundary.test.ts +8 -34
  19. package/dist/api/BaseMethod.d.ts +2 -1
  20. package/dist/api/BaseMethod.d.ts.map +1 -1
  21. package/dist/api/CheckAllFirmwareRelease.d.ts.map +1 -1
  22. package/dist/api/ClearSessionCache.d.ts.map +1 -1
  23. package/dist/api/FirmwareUpdate.d.ts.map +1 -1
  24. package/dist/api/FirmwareUpdateV2.d.ts +11 -0
  25. package/dist/api/FirmwareUpdateV2.d.ts.map +1 -1
  26. package/dist/api/FirmwareUpdateV3.d.ts +7 -3
  27. package/dist/api/FirmwareUpdateV3.d.ts.map +1 -1
  28. package/dist/api/FirmwareUpdateV4.d.ts +29 -10
  29. package/dist/api/FirmwareUpdateV4.d.ts.map +1 -1
  30. package/dist/api/OpenWalletSession.d.ts.map +1 -1
  31. package/dist/api/conflux/ConfluxGetAddress.d.ts +1 -0
  32. package/dist/api/conflux/ConfluxGetAddress.d.ts.map +1 -1
  33. package/dist/api/conflux/ConfluxSignMessage.d.ts +1 -0
  34. package/dist/api/conflux/ConfluxSignMessage.d.ts.map +1 -1
  35. package/dist/api/conflux/ConfluxSignMessageCIP23.d.ts +1 -0
  36. package/dist/api/conflux/ConfluxSignMessageCIP23.d.ts.map +1 -1
  37. package/dist/api/conflux/ConfluxSignTransaction.d.ts +1 -0
  38. package/dist/api/conflux/ConfluxSignTransaction.d.ts.map +1 -1
  39. package/dist/api/device/DeviceUnlock.d.ts +1 -1
  40. package/dist/api/device/DeviceUnlock.d.ts.map +1 -1
  41. package/dist/api/device/DeviceUpdateBootloader.d.ts +5 -1
  42. package/dist/api/device/DeviceUpdateBootloader.d.ts.map +1 -1
  43. package/dist/api/firmware/FirmwareArtifactSource.d.ts +26 -0
  44. package/dist/api/firmware/FirmwareArtifactSource.d.ts.map +1 -0
  45. package/dist/api/firmware/FirmwareHostBinding.d.ts +6 -0
  46. package/dist/api/firmware/FirmwareHostBinding.d.ts.map +1 -0
  47. package/dist/api/firmware/FirmwareUpdateBaseMethod.d.ts +7 -0
  48. package/dist/api/firmware/FirmwareUpdateBaseMethod.d.ts.map +1 -1
  49. package/dist/api/firmware/FirmwareUpdateCapabilities.d.ts +3 -0
  50. package/dist/api/firmware/FirmwareUpdateCapabilities.d.ts.map +1 -0
  51. package/dist/api/firmware/FirmwareUpdatePlan.d.ts +24 -0
  52. package/dist/api/firmware/FirmwareUpdatePlan.d.ts.map +1 -0
  53. package/dist/api/firmware/FirmwareUpdatePreparedPlan.d.ts +25 -0
  54. package/dist/api/firmware/FirmwareUpdatePreparedPlan.d.ts.map +1 -0
  55. package/dist/api/firmware/getBinary.d.ts +7 -0
  56. package/dist/api/firmware/getBinary.d.ts.map +1 -1
  57. package/dist/api/firmware/updateBootloader.d.ts +2 -0
  58. package/dist/api/firmware/updateBootloader.d.ts.map +1 -1
  59. package/dist/api/firmware/uploadFirmware.d.ts +9 -1
  60. package/dist/api/firmware/uploadFirmware.d.ts.map +1 -1
  61. package/dist/api/helpers/protocolV2FileWrite.d.ts.map +1 -1
  62. package/dist/api/index.d.ts +0 -17
  63. package/dist/api/index.d.ts.map +1 -1
  64. package/dist/api/nexa/NexaSignTransaction.d.ts +1 -0
  65. package/dist/api/nexa/NexaSignTransaction.d.ts.map +1 -1
  66. package/dist/api/stellar/StellarSignTransaction.d.ts +1 -1
  67. package/dist/data-manager/DataManager.d.ts +1 -0
  68. package/dist/data-manager/DataManager.d.ts.map +1 -1
  69. package/dist/data-manager/connectSettings.d.ts.map +1 -1
  70. package/dist/device/Device.d.ts.map +1 -1
  71. package/dist/device/DeviceCommands.d.ts.map +1 -1
  72. package/dist/device/DeviceStateMapper.d.ts.map +1 -1
  73. package/dist/device/DeviceStateProjector.d.ts.map +1 -1
  74. package/dist/events/logBlockEvent.d.ts.map +1 -1
  75. package/dist/index.d.ts +265 -226
  76. package/dist/index.d.ts.map +1 -1
  77. package/dist/index.js +3045 -1550
  78. package/dist/inject.d.ts.map +1 -1
  79. package/dist/lowLevelInject.d.ts.map +1 -1
  80. package/dist/protocols/protocol-v2/settingsUnlockPolicy.d.ts.map +1 -1
  81. package/dist/protocols/protocol-v2/unlockPolicy.d.ts +4 -0
  82. package/dist/protocols/protocol-v2/unlockPolicy.d.ts.map +1 -0
  83. package/dist/protocols/protocol-v2/unlockRetry.d.ts.map +1 -1
  84. package/dist/protocols/protocol-v2/walletSession.d.ts.map +1 -1
  85. package/dist/topLevelInject.d.ts.map +1 -1
  86. package/dist/types/api/checkAllFirmwareRelease.d.ts +3 -0
  87. package/dist/types/api/checkAllFirmwareRelease.d.ts.map +1 -1
  88. package/dist/types/api/deviceUnlock.d.ts +2 -2
  89. package/dist/types/api/deviceUnlock.d.ts.map +1 -1
  90. package/dist/types/api/deviceUpdateBootloader.d.ts +6 -0
  91. package/dist/types/api/deviceUpdateBootloader.d.ts.map +1 -1
  92. package/dist/types/api/export.d.ts +4 -1
  93. package/dist/types/api/export.d.ts.map +1 -1
  94. package/dist/types/api/firmwareUpdate.d.ts +69 -0
  95. package/dist/types/api/firmwareUpdate.d.ts.map +1 -1
  96. package/dist/types/api/firmwareUpdateCapabilities.d.ts +9 -0
  97. package/dist/types/api/firmwareUpdateCapabilities.d.ts.map +1 -0
  98. package/dist/types/api/firmwareUpdatePlan.d.ts +27 -0
  99. package/dist/types/api/firmwareUpdatePlan.d.ts.map +1 -0
  100. package/dist/types/api/firmwareUpdatePreparedPlan.d.ts +42 -0
  101. package/dist/types/api/firmwareUpdatePreparedPlan.d.ts.map +1 -0
  102. package/dist/types/api/index.d.ts +10 -19
  103. package/dist/types/api/index.d.ts.map +1 -1
  104. package/dist/types/api/openWalletSession.d.ts +1 -0
  105. package/dist/types/api/openWalletSession.d.ts.map +1 -1
  106. package/dist/types/api/protocolV2.d.ts +3 -76
  107. package/dist/types/api/protocolV2.d.ts.map +1 -1
  108. package/dist/types/api/sessionCache.d.ts +4 -1
  109. package/dist/types/api/sessionCache.d.ts.map +1 -1
  110. package/dist/types/settings.d.ts +13 -0
  111. package/dist/types/settings.d.ts.map +1 -1
  112. package/dist/utils/deviceFeaturesUtils.d.ts.map +1 -1
  113. package/dist/utils/patch.d.ts +1 -1
  114. package/dist/utils/patch.d.ts.map +1 -1
  115. package/package.json +4 -4
  116. package/src/api/BaseMethod.ts +8 -3
  117. package/src/api/CheckAllFirmwareRelease.ts +16 -3
  118. package/src/api/ClearSessionCache.ts +4 -0
  119. package/src/api/FirmwareUpdate.ts +8 -1
  120. package/src/api/FirmwareUpdateV2.ts +284 -119
  121. package/src/api/FirmwareUpdateV3.ts +248 -55
  122. package/src/api/FirmwareUpdateV4.ts +816 -328
  123. package/src/api/OpenWalletSession.ts +74 -25
  124. package/src/api/UploadPortfolio.ts +1 -1
  125. package/src/api/conflux/ConfluxGetAddress.ts +2 -0
  126. package/src/api/conflux/ConfluxSignMessage.ts +2 -0
  127. package/src/api/conflux/ConfluxSignMessageCIP23.ts +2 -0
  128. package/src/api/conflux/ConfluxSignTransaction.ts +2 -0
  129. package/src/api/device/DeviceChangePin.ts +1 -1
  130. package/src/api/device/DeviceUnlock.ts +1 -2
  131. package/src/api/device/DeviceUpdateBootloader.ts +114 -6
  132. package/src/api/device/DeviceWipe.ts +1 -1
  133. package/src/api/firmware/FirmwareArtifactSource.ts +278 -0
  134. package/src/api/firmware/FirmwareHostBinding.ts +100 -0
  135. package/src/api/firmware/FirmwareUpdateBaseMethod.ts +50 -0
  136. package/src/api/firmware/FirmwareUpdateCapabilities.ts +9 -0
  137. package/src/api/firmware/FirmwareUpdatePlan.ts +621 -0
  138. package/src/api/firmware/FirmwareUpdatePreparedPlan.ts +378 -0
  139. package/src/api/firmware/updateBootloader.ts +19 -4
  140. package/src/api/firmware/uploadFirmware.ts +140 -22
  141. package/src/api/helpers/protocolV2FileWrite.ts +6 -0
  142. package/src/api/index.ts +0 -18
  143. package/src/api/nexa/NexaSignTransaction.ts +2 -0
  144. package/src/api/protocol-v2/DeviceUploadWallpaper.ts +1 -1
  145. package/src/data/messages/messages-protocol-v2.json +77 -4
  146. package/src/data-manager/DataManager.ts +33 -33
  147. package/src/data-manager/connectSettings.ts +11 -0
  148. package/src/device/Device.ts +50 -60
  149. package/src/device/DeviceCommands.ts +58 -14
  150. package/src/device/DeviceStateMapper.ts +18 -1
  151. package/src/device/DeviceStateProjector.ts +7 -1
  152. package/src/events/logBlockEvent.ts +1 -0
  153. package/src/index.ts +5 -0
  154. package/src/inject.ts +22 -25
  155. package/src/lowLevelInject.ts +5 -1
  156. package/src/protocols/protocol-v2/deviceSession.ts +1 -1
  157. package/src/protocols/protocol-v2/settingsUnlockPolicy.ts +2 -2
  158. package/src/protocols/protocol-v2/unlockPolicy.ts +105 -0
  159. package/src/protocols/protocol-v2/unlockRetry.ts +1 -3
  160. package/src/protocols/protocol-v2/walletSession.ts +68 -12
  161. package/src/topLevelInject.ts +5 -1
  162. package/src/types/api/checkAllFirmwareRelease.ts +3 -0
  163. package/src/types/api/deviceUnlock.ts +2 -5
  164. package/src/types/api/deviceUpdateBootloader.ts +6 -0
  165. package/src/types/api/export.ts +18 -0
  166. package/src/types/api/firmwareUpdate.ts +76 -0
  167. package/src/types/api/firmwareUpdateCapabilities.ts +9 -0
  168. package/src/types/api/firmwareUpdatePlan.ts +36 -0
  169. package/src/types/api/firmwareUpdatePreparedPlan.ts +53 -0
  170. package/src/types/api/index.ts +14 -34
  171. package/src/types/api/openWalletSession.ts +2 -0
  172. package/src/types/api/protocolV2.ts +3 -156
  173. package/src/types/api/sessionCache.ts +9 -4
  174. package/src/types/settings.ts +13 -0
  175. package/src/utils/deviceFeaturesUtils.ts +5 -1
@@ -0,0 +1,378 @@
1
+ import { ERRORS, HardwareErrorCode } from '@onekeyfe/hd-shared';
2
+
3
+ import { assertFirmwareArtifactReference } from './FirmwareArtifactSource';
4
+ import {
5
+ FIRMWARE_UPDATE_PLAN_ROLES,
6
+ FIRMWARE_UPDATE_PLAN_TARGETS,
7
+ assertFirmwareUpdatePlan,
8
+ digestFirmwareUpdateContract,
9
+ } from './FirmwareUpdatePlan';
10
+
11
+ import type {
12
+ FirmwareUpdatePreparedArtifact,
13
+ FirmwareUpdatePreparedArtifactInput,
14
+ FirmwareUpdatePreparedEntry,
15
+ FirmwareUpdatePreparedPlan,
16
+ } from '../../types/api/firmwareUpdatePreparedPlan';
17
+ import type { FirmwareArtifactReference } from '../../types/api/firmwareUpdate';
18
+ import type { FirmwareUpdatePlan } from '../../types/api/firmwareUpdatePlan';
19
+
20
+ const preparedPlanError = (message: string): never => {
21
+ throw ERRORS.TypedError(HardwareErrorCode.RuntimeError, message, {
22
+ firmwareUpdateCode: 'FirmwarePreparedPlanInvalid',
23
+ });
24
+ };
25
+
26
+ const digestPreparedPlan = (plan: Omit<FirmwareUpdatePreparedPlan, 'preparedPlanDigest'>): string =>
27
+ digestFirmwareUpdateContract(plan);
28
+
29
+ const assertOpaqueLeaseRef = (value: unknown): string => {
30
+ if (
31
+ typeof value !== 'string' ||
32
+ value.length === 0 ||
33
+ value.length > 160 ||
34
+ value.includes('/') ||
35
+ value.includes('\\')
36
+ ) {
37
+ return preparedPlanError('Firmware prepared plan leaseRef must be opaque');
38
+ }
39
+ return value;
40
+ };
41
+
42
+ export const getFirmwareUpdateResourceName = (value: unknown): string => {
43
+ const normalized = typeof value === 'string' ? value.normalize('NFC') : undefined;
44
+ const parts = normalized?.split('/');
45
+ const resourceName = parts?.at(-1);
46
+ if (
47
+ typeof value !== 'string' ||
48
+ value.length === 0 ||
49
+ value.length > 512 ||
50
+ value !== normalized ||
51
+ value.startsWith('/') ||
52
+ value.includes('\\') ||
53
+ value.includes(':') ||
54
+ [...value].some(char => {
55
+ const code = char.charCodeAt(0);
56
+ return code <= 0x1f || code === 0x7f;
57
+ }) ||
58
+ !parts?.every(
59
+ part =>
60
+ part.length > 0 &&
61
+ part !== '.' &&
62
+ part !== '..' &&
63
+ !part.endsWith('.') &&
64
+ !part.endsWith(' ')
65
+ ) ||
66
+ !resourceName ||
67
+ resourceName.length > 128
68
+ ) {
69
+ return preparedPlanError('Firmware prepared plan entry name is invalid');
70
+ }
71
+ return resourceName;
72
+ };
73
+
74
+ const assertPreparedEntry = (value: unknown): FirmwareUpdatePreparedEntry => {
75
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
76
+ return preparedPlanError('Firmware prepared plan entry must be an object');
77
+ }
78
+ const entry = value as Record<string, unknown>;
79
+ if (
80
+ Object.keys(entry).length !== 2 ||
81
+ !Object.prototype.hasOwnProperty.call(entry, 'entryName') ||
82
+ !Object.prototype.hasOwnProperty.call(entry, 'artifact')
83
+ ) {
84
+ return preparedPlanError('Firmware prepared plan entry fields are invalid');
85
+ }
86
+ getFirmwareUpdateResourceName(entry.entryName);
87
+ assertFirmwareArtifactReference(entry.artifact as FirmwareUpdatePreparedEntry['artifact']);
88
+ return value as FirmwareUpdatePreparedEntry;
89
+ };
90
+
91
+ const assertPreparedArtifacts = ({
92
+ plan,
93
+ artifacts,
94
+ }: {
95
+ plan: FirmwareUpdatePlan;
96
+ artifacts: readonly FirmwareUpdatePreparedArtifactInput[];
97
+ }): FirmwareUpdatePreparedArtifact[] => {
98
+ if (artifacts.length !== plan.artifacts.length) {
99
+ return preparedPlanError('Firmware prepared plan does not cover every artifact');
100
+ }
101
+ const inputs = new Map<string, FirmwareUpdatePreparedArtifactInput>();
102
+ for (const input of artifacts) {
103
+ if (
104
+ !input ||
105
+ typeof input !== 'object' ||
106
+ typeof input.artifactId !== 'string' ||
107
+ inputs.has(input.artifactId)
108
+ ) {
109
+ return preparedPlanError('Firmware prepared plan contains a duplicate artifact receipt');
110
+ }
111
+ inputs.set(input.artifactId, input);
112
+ }
113
+ return plan.artifacts.map(planArtifact => {
114
+ const input = inputs.get(planArtifact.artifactId);
115
+ if (!input) {
116
+ return preparedPlanError('Firmware prepared plan artifact receipt is missing');
117
+ }
118
+ const artifact = assertFirmwareArtifactReference(input.artifact);
119
+ if (
120
+ (planArtifact.expectedSize !== undefined && artifact.size !== planArtifact.expectedSize) ||
121
+ (planArtifact.expectedSha256 !== undefined &&
122
+ artifact.sha256.toLowerCase() !== planArtifact.expectedSha256.toLowerCase())
123
+ ) {
124
+ return preparedPlanError('Firmware prepared plan receipt does not match the update plan');
125
+ }
126
+ const materializedEntries = input.materializedEntries?.map(value => {
127
+ const entry = assertPreparedEntry(value);
128
+ return {
129
+ entryName: entry.entryName,
130
+ artifact: {
131
+ ...entry.artifact,
132
+ sha256: entry.artifact.sha256.toLowerCase(),
133
+ },
134
+ };
135
+ });
136
+ if (
137
+ (planArtifact.container === 'raw' && materializedEntries?.length) ||
138
+ (planArtifact.container === 'zip' && !materializedEntries?.length)
139
+ ) {
140
+ return preparedPlanError('Firmware prepared plan materialization is incomplete');
141
+ }
142
+ if (
143
+ materializedEntries &&
144
+ new Set(
145
+ materializedEntries.map(entry =>
146
+ getFirmwareUpdateResourceName(entry.entryName).toLowerCase()
147
+ )
148
+ ).size !== materializedEntries.length
149
+ ) {
150
+ return preparedPlanError('Firmware prepared plan contains duplicate entry names');
151
+ }
152
+ return {
153
+ artifactId: planArtifact.artifactId,
154
+ role: planArtifact.role,
155
+ target: planArtifact.target,
156
+ container: planArtifact.container,
157
+ ...(planArtifact.logicalName ? { logicalName: planArtifact.logicalName } : {}),
158
+ ...(planArtifact.targetVersion ? { targetVersion: planArtifact.targetVersion } : {}),
159
+ artifact: {
160
+ ...artifact,
161
+ sha256: artifact.sha256.toLowerCase(),
162
+ },
163
+ ...(materializedEntries?.length ? { materializedEntries } : {}),
164
+ };
165
+ });
166
+ };
167
+
168
+ export const prepareFirmwareUpdatePlan = ({
169
+ plan: value,
170
+ leaseRef,
171
+ artifacts,
172
+ }: {
173
+ plan: FirmwareUpdatePlan;
174
+ leaseRef: string;
175
+ artifacts: FirmwareUpdatePreparedArtifactInput[];
176
+ }): FirmwareUpdatePreparedPlan => {
177
+ const plan = assertFirmwareUpdatePlan(value);
178
+ const planWithoutDigest: Omit<FirmwareUpdatePreparedPlan, 'preparedPlanDigest'> = {
179
+ schemaVersion: 2,
180
+ planDigest: plan.planDigest,
181
+ networkPolicy: 'forbid',
182
+ executor: plan.executor,
183
+ deviceIdentity: plan.deviceIdentity,
184
+ deviceModel: plan.deviceModel,
185
+ firmwareType: plan.firmwareType,
186
+ platform: plan.platform,
187
+ leaseRef: assertOpaqueLeaseRef(leaseRef),
188
+ artifacts: assertPreparedArtifacts({ plan, artifacts }),
189
+ targetsToUpdate: plan.targetsToUpdate,
190
+ };
191
+ return {
192
+ ...planWithoutDigest,
193
+ preparedPlanDigest: digestPreparedPlan(planWithoutDigest),
194
+ };
195
+ };
196
+
197
+ export const validateFirmwareUpdatePreparedPlan = (value: unknown): FirmwareUpdatePreparedPlan => {
198
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
199
+ return preparedPlanError('Firmware prepared plan must be an object');
200
+ }
201
+ const preparedPlan = value as FirmwareUpdatePreparedPlan;
202
+ const exactKeys = [
203
+ 'schemaVersion',
204
+ 'preparedPlanDigest',
205
+ 'planDigest',
206
+ 'networkPolicy',
207
+ 'executor',
208
+ 'deviceIdentity',
209
+ 'deviceModel',
210
+ 'firmwareType',
211
+ 'platform',
212
+ 'leaseRef',
213
+ 'artifacts',
214
+ 'targetsToUpdate',
215
+ ];
216
+ if (
217
+ Object.keys(value).length !== exactKeys.length ||
218
+ exactKeys.some(key => !Object.prototype.hasOwnProperty.call(value, key)) ||
219
+ preparedPlan.schemaVersion !== 2 ||
220
+ preparedPlan.networkPolicy !== 'forbid' ||
221
+ !/^[a-f0-9]{64}$/u.test(preparedPlan.planDigest) ||
222
+ !/^[a-f0-9]{64}$/u.test(preparedPlan.preparedPlanDigest) ||
223
+ !Array.isArray(preparedPlan.artifacts) ||
224
+ !Array.isArray(preparedPlan.targetsToUpdate) ||
225
+ (preparedPlan.executor !== 'v2' &&
226
+ preparedPlan.executor !== 'v3' &&
227
+ preparedPlan.executor !== 'v4') ||
228
+ (preparedPlan.platform !== 'native' &&
229
+ preparedPlan.platform !== 'desktop' &&
230
+ preparedPlan.platform !== 'ext' &&
231
+ preparedPlan.platform !== 'web' &&
232
+ preparedPlan.platform !== 'web-embed') ||
233
+ (preparedPlan.firmwareType !== 'universal' && preparedPlan.firmwareType !== 'bitcoinonly') ||
234
+ typeof preparedPlan.deviceIdentity !== 'string' ||
235
+ preparedPlan.deviceIdentity.length === 0 ||
236
+ typeof preparedPlan.deviceModel !== 'string' ||
237
+ preparedPlan.deviceModel.length === 0
238
+ ) {
239
+ return preparedPlanError('Firmware prepared plan header is invalid');
240
+ }
241
+ assertOpaqueLeaseRef(preparedPlan.leaseRef);
242
+ const artifactIds = new Set<string>();
243
+ const artifactTargets = new Set<string>();
244
+ for (const artifact of preparedPlan.artifacts) {
245
+ if (!artifact || typeof artifact !== 'object') {
246
+ return preparedPlanError('Firmware prepared plan artifact is invalid');
247
+ }
248
+ const artifactKeys = [
249
+ 'artifactId',
250
+ 'role',
251
+ 'target',
252
+ 'container',
253
+ 'artifact',
254
+ ...(artifact.logicalName !== undefined ? ['logicalName'] : []),
255
+ ...(artifact.targetVersion !== undefined ? ['targetVersion'] : []),
256
+ ...(artifact.materializedEntries !== undefined ? ['materializedEntries'] : []),
257
+ ];
258
+ if (
259
+ typeof artifact.artifactId !== 'string' ||
260
+ artifact.artifactId.length === 0 ||
261
+ artifactIds.has(artifact.artifactId) ||
262
+ Object.keys(artifact).length !== artifactKeys.length ||
263
+ artifactKeys.some(key => !Object.prototype.hasOwnProperty.call(artifact, key)) ||
264
+ !FIRMWARE_UPDATE_PLAN_ROLES.has(artifact.role) ||
265
+ !FIRMWARE_UPDATE_PLAN_TARGETS.has(artifact.target) ||
266
+ (artifact.container !== 'raw' && artifact.container !== 'zip') ||
267
+ (artifact.logicalName !== undefined &&
268
+ (typeof artifact.logicalName !== 'string' || artifact.logicalName.length === 0)) ||
269
+ (artifact.targetVersion !== undefined &&
270
+ (typeof artifact.targetVersion !== 'string' || artifact.targetVersion.length === 0)) ||
271
+ (artifact.materializedEntries !== undefined &&
272
+ !Array.isArray(artifact.materializedEntries)) ||
273
+ (artifact.container === 'raw' && Boolean(artifact.materializedEntries?.length)) ||
274
+ (artifact.container === 'zip' && !artifact.materializedEntries?.length)
275
+ ) {
276
+ return preparedPlanError('Firmware prepared plan artifact is invalid');
277
+ }
278
+ artifactIds.add(artifact.artifactId);
279
+ artifactTargets.add(artifact.target);
280
+ assertFirmwareArtifactReference(artifact.artifact);
281
+ artifact.materializedEntries?.forEach(assertPreparedEntry);
282
+ }
283
+ if (
284
+ preparedPlan.targetsToUpdate.some(target => !FIRMWARE_UPDATE_PLAN_TARGETS.has(target)) ||
285
+ new Set(preparedPlan.targetsToUpdate).size !== preparedPlan.targetsToUpdate.length ||
286
+ preparedPlan.targetsToUpdate.length !== artifactTargets.size ||
287
+ preparedPlan.targetsToUpdate.some(target => !artifactTargets.has(target))
288
+ ) {
289
+ return preparedPlanError('Firmware prepared plan targets are invalid');
290
+ }
291
+ const { preparedPlanDigest, ...withoutDigest } = preparedPlan;
292
+ if (digestPreparedPlan(withoutDigest) !== preparedPlanDigest) {
293
+ return preparedPlanError('Firmware prepared plan digest is invalid');
294
+ }
295
+ return preparedPlan;
296
+ };
297
+
298
+ export type FirmwareUpdatePreparedBinding = {
299
+ target: FirmwareUpdatePlan['targetsToUpdate'][number];
300
+ artifact: FirmwareArtifactReference;
301
+ logicalName?: string;
302
+ entryName?: string;
303
+ };
304
+
305
+ const sameArtifactReference = (
306
+ first: FirmwareArtifactReference,
307
+ second: FirmwareArtifactReference
308
+ ) =>
309
+ first.artifactRef === second.artifactRef &&
310
+ first.size === second.size &&
311
+ first.sha256.toLowerCase() === second.sha256.toLowerCase();
312
+
313
+ export const assertFirmwareUpdatePreparedPlanBinding = ({
314
+ preparedPlan: value,
315
+ executor,
316
+ platform,
317
+ deviceIdentity,
318
+ scopeTargets,
319
+ bindings,
320
+ }: {
321
+ preparedPlan: unknown;
322
+ executor: FirmwareUpdatePreparedPlan['executor'];
323
+ platform?: FirmwareUpdatePreparedPlan['platform'];
324
+ deviceIdentity?: string;
325
+ scopeTargets: FirmwareUpdatePreparedPlan['targetsToUpdate'];
326
+ bindings: FirmwareUpdatePreparedBinding[];
327
+ }): FirmwareUpdatePreparedPlan => {
328
+ const preparedPlan = validateFirmwareUpdatePreparedPlan(value);
329
+ if (
330
+ preparedPlan.executor !== executor ||
331
+ (platform !== undefined && preparedPlan.platform !== platform) ||
332
+ (deviceIdentity !== undefined && preparedPlan.deviceIdentity !== deviceIdentity)
333
+ ) {
334
+ return preparedPlanError('Firmware prepared plan executor binding is invalid');
335
+ }
336
+ const scope = new Set(scopeTargets);
337
+ const expectedBindings: FirmwareUpdatePreparedBinding[] = [];
338
+ for (const artifact of preparedPlan.artifacts) {
339
+ if (scope.has(artifact.target)) {
340
+ if (artifact.container === 'zip') {
341
+ for (const entry of artifact.materializedEntries ?? []) {
342
+ expectedBindings.push({
343
+ target: artifact.target,
344
+ ...(artifact.target === 'resource' && artifact.logicalName
345
+ ? { logicalName: artifact.logicalName }
346
+ : {}),
347
+ entryName: entry.entryName,
348
+ artifact: entry.artifact,
349
+ });
350
+ }
351
+ } else {
352
+ expectedBindings.push({
353
+ target: artifact.target,
354
+ ...(artifact.target === 'resource' && artifact.logicalName
355
+ ? { logicalName: artifact.logicalName }
356
+ : {}),
357
+ artifact: artifact.artifact,
358
+ });
359
+ }
360
+ }
361
+ }
362
+ if (
363
+ expectedBindings.length !== bindings.length ||
364
+ expectedBindings.some(expected => {
365
+ const matching = bindings.filter(
366
+ binding =>
367
+ binding.target === expected.target &&
368
+ binding.logicalName === expected.logicalName &&
369
+ binding.entryName === expected.entryName &&
370
+ sameArtifactReference(binding.artifact, expected.artifact)
371
+ );
372
+ return matching.length !== 1;
373
+ })
374
+ ) {
375
+ return preparedPlanError('Firmware prepared plan artifact binding is invalid');
376
+ }
377
+ return preparedPlan;
378
+ };
@@ -8,6 +8,7 @@ import { shouldUpdateBootloaderForClassicAndMini } from './bootloaderHelper';
8
8
 
9
9
  import type { Features } from '../../types';
10
10
  import type { EFirmwareType } from '@onekeyfe/hd-shared';
11
+ import type { FirmwareByteSource } from './FirmwareArtifactSource';
11
12
 
12
13
  export function checkNeedUpdateBootForTouch(features: Features, firmwareType: EFirmwareType) {
13
14
  const deviceType = getDeviceType(features);
@@ -62,8 +63,7 @@ export function checkNeedUpdateBootForClassicAndMini({
62
63
  }
63
64
 
64
65
  const INIT_DATA_CHUNK_SIZE = 16 * 1024;
65
- export function checkBootloaderLength(data: ArrayBuffer) {
66
- const chunk = new Uint8Array(data.slice(0, Math.min(INIT_DATA_CHUNK_SIZE, data.byteLength)));
66
+ const readBootloaderLength = (chunk: Uint8Array) => {
67
67
  const buffer = ByteBuffer.wrap(chunk, undefined, undefined, true);
68
68
  buffer.LE();
69
69
  // byte 'O', 'K', 'T', 'B'
@@ -77,6 +77,21 @@ export function checkBootloaderLength(data: ArrayBuffer) {
77
77
  buffer.readUint32();
78
78
  // codelen
79
79
  const codelen = buffer.readUint32();
80
- const bootloaderLength = hdrlen + codelen;
81
- return bootloaderLength === data.byteLength;
80
+ return hdrlen + codelen;
81
+ };
82
+
83
+ export function checkBootloaderLength(data: ArrayBuffer) {
84
+ if (data.byteLength < 16) {
85
+ return false;
86
+ }
87
+ const chunk = new Uint8Array(data.slice(0, Math.min(INIT_DATA_CHUNK_SIZE, data.byteLength)));
88
+ return readBootloaderLength(chunk) === data.byteLength;
82
89
  }
90
+
91
+ export const checkBootloaderSourceLength = async (source: FirmwareByteSource) => {
92
+ if (source.size < 16) {
93
+ return false;
94
+ }
95
+ const chunk = new Uint8Array(await source.readAt(0, Math.min(INIT_DATA_CHUNK_SIZE, source.size)));
96
+ return readBootloaderLength(chunk) === source.size;
97
+ };
@@ -10,6 +10,7 @@ import { bytesToHex } from '../helpers/hexUtils';
10
10
  import { DataManager } from '../../data-manager';
11
11
  import { DevicePool } from '../../device/DevicePool';
12
12
  import { buildProtocolV1FeaturesPayload } from '../../deviceProfile';
13
+ import { openFirmwareByteSource, readFirmwareByteSourceFully } from './FirmwareArtifactSource';
13
14
 
14
15
  import type { KnownDevice } from '../../types';
15
16
  import type { TypedCall, TypedResponseMessage } from '../../device/DeviceCommands';
@@ -17,6 +18,7 @@ import type { PROTO } from '../../constants';
17
18
  import type { CoreMessage, IFirmwareUpdateProgressType } from '../../events';
18
19
  import type { Success } from '@onekeyfe/hd-transport';
19
20
  import type { Device } from '../../device/Device';
21
+ import type { FirmwareByteSource } from './FirmwareArtifactSource';
20
22
 
21
23
  const NEW_BOOT_UPRATE_FIRMWARE_VERSION = '2.4.5';
22
24
  const SESSION_ERROR = 'session not found';
@@ -88,17 +90,20 @@ export const waitBleInstall = async (updateType: string) => {
88
90
  }
89
91
  };
90
92
 
91
- export const uploadFirmware = async (
93
+ const toArrayBuffer = (payload: ArrayBuffer | Buffer): ArrayBuffer => {
94
+ if (payload instanceof ArrayBuffer) {
95
+ return payload;
96
+ }
97
+ return new Uint8Array(payload.buffer, payload.byteOffset, payload.byteLength).slice().buffer;
98
+ };
99
+
100
+ export const uploadFirmwareFromSource = async (
92
101
  updateType: 'firmware' | 'ble',
93
102
  typedCall: TypedCall,
94
103
  postMessage: (message: CoreMessage) => void,
95
104
  device: Device,
96
- {
97
- payload,
98
- rebootOnSuccess,
99
- }: PROTO.FirmwareUpload & {
100
- rebootOnSuccess?: boolean;
101
- },
105
+ source: FirmwareByteSource,
106
+ rebootOnSuccess?: boolean,
102
107
  isUpdateBootloader?: boolean
103
108
  ) => {
104
109
  const deviceType = device.getCurrentDeviceType();
@@ -119,20 +124,20 @@ export const uploadFirmware = async (
119
124
  if (supportUpgradeFileHeader) {
120
125
  // Extract and validate firmware header (first 1KB)
121
126
  const HEADER_SIZE = 1024;
122
- if (payload.byteLength < HEADER_SIZE) {
127
+ if (source.size < HEADER_SIZE) {
123
128
  throw ERRORS.TypedError(
124
129
  HardwareErrorCode.RuntimeError,
125
- `firmware payload too small: ${payload.byteLength} bytes, expected at least ${HEADER_SIZE} bytes`
130
+ `firmware payload too small: ${source.size} bytes, expected at least ${HEADER_SIZE} bytes`
126
131
  );
127
132
  }
128
133
 
129
134
  Log.debug('Uploading firmware header:', {
130
135
  size: HEADER_SIZE,
131
- totalSize: payload.byteLength,
136
+ totalSize: source.size,
132
137
  });
133
138
  postProgressTip(device, 'UploadingFirmwareHeader', postMessage);
134
139
 
135
- const header = new Uint8Array(payload.slice(0, HEADER_SIZE));
140
+ const header = new Uint8Array(await source.readAt(0, HEADER_SIZE));
136
141
 
137
142
  try {
138
143
  const headerRes = await typedCall('UpgradeFileHeader', 'Success', {
@@ -169,6 +174,9 @@ export const uploadFirmware = async (
169
174
  postProgressMessage(device, 0, 'installingFirmware', postMessage);
170
175
  let updateResponse: TypedResponseMessage<'Success'>;
171
176
  try {
177
+ // Classic/Mini bootloaders accept one protobuf payload and do not expose
178
+ // a request-range protocol. Other device families remain fully streamed.
179
+ const payload = await readFirmwareByteSourceFully(source);
172
180
  updateResponse = await typedCall('FirmwareUpload', 'Success', {
173
181
  payload,
174
182
  });
@@ -200,9 +208,7 @@ export const uploadFirmware = async (
200
208
  updateType,
201
209
  postMessage,
202
210
  device,
203
- {
204
- payload,
205
- },
211
+ source,
206
212
  rebootOnSuccess
207
213
  );
208
214
  return response.message;
@@ -211,7 +217,7 @@ export const uploadFirmware = async (
211
217
 
212
218
  postConfirmationMessage(device);
213
219
  postProgressTip(device, 'ConfirmOnDevice', postMessage);
214
- const length = payload.byteLength;
220
+ const length = source.size;
215
221
 
216
222
  let response = await typedCall('FirmwareErase', ['FirmwareRequest', 'Success'], { length });
217
223
  postProgressTip(device, 'FirmwareEraseSuccess', postMessage);
@@ -220,7 +226,7 @@ export const uploadFirmware = async (
220
226
  const start = response.message.offset!;
221
227
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
222
228
  const end = response.message.offset! + response.message.length!;
223
- const chunk = payload.slice(start, end);
229
+ const chunk = await source.readAt(start, end - start);
224
230
 
225
231
  if (start > 0) {
226
232
  postProgressMessage(
@@ -249,11 +255,45 @@ export const uploadFirmware = async (
249
255
  throw ERRORS.TypedError(HardwareErrorCode.RuntimeError, 'uploadFirmware: unknown device model');
250
256
  };
251
257
 
258
+ export const uploadFirmware = async (
259
+ updateType: 'firmware' | 'ble',
260
+ typedCall: TypedCall,
261
+ postMessage: (message: CoreMessage) => void,
262
+ device: Device,
263
+ {
264
+ payload,
265
+ rebootOnSuccess,
266
+ }: PROTO.FirmwareUpload & {
267
+ rebootOnSuccess?: boolean;
268
+ },
269
+ isUpdateBootloader?: boolean
270
+ ) => {
271
+ const source = await openFirmwareByteSource({
272
+ binary: toArrayBuffer(payload),
273
+ });
274
+ if (!source) {
275
+ throw ERRORS.TypedError(HardwareErrorCode.RuntimeError, 'firmware payload is empty');
276
+ }
277
+ try {
278
+ return await uploadFirmwareFromSource(
279
+ updateType,
280
+ typedCall,
281
+ postMessage,
282
+ device,
283
+ source,
284
+ rebootOnSuccess,
285
+ isUpdateBootloader
286
+ );
287
+ } finally {
288
+ await source.close();
289
+ }
290
+ };
291
+
252
292
  const newTouchUpdateProcess = async (
253
293
  updateType: 'firmware' | 'ble',
254
294
  postMessage: (message: CoreMessage) => void,
255
295
  device: Device,
256
- { payload }: PROTO.FirmwareUpload,
296
+ source: FirmwareByteSource,
257
297
  rebootOnSuccess = true
258
298
  ) => {
259
299
  let typedCall = device.getCommands().typedCall.bind(device.getCommands());
@@ -263,13 +303,13 @@ const newTouchUpdateProcess = async (
263
303
  const env = DataManager.getSettings('env');
264
304
  const perPackageSize = DataManager.isBleConnect(env) ? 16 : 128;
265
305
  const chunkSize = 1024 * perPackageSize;
266
- const totalChunks = Math.ceil(payload.byteLength / chunkSize);
306
+ const totalChunks = Math.ceil(source.size / chunkSize);
267
307
  let offset = 0;
268
308
  for (let i = 0; i < totalChunks; i++) {
269
309
  const chunkStart = i * chunkSize;
270
- const chunkEnd = Math.min(chunkStart + chunkSize, payload.byteLength);
310
+ const chunkEnd = Math.min(chunkStart + chunkSize, source.size);
271
311
  const chunkLength = chunkEnd - chunkStart;
272
- const chunk = payload.slice(chunkStart, chunkEnd);
312
+ const chunk = await source.readAt(chunkStart, chunkLength);
273
313
  const overwrite = i === 0;
274
314
  const progress = Math.round(((i + 1) / totalChunks) * 100);
275
315
  const writeRes = await emmcFileWriteWithRetry(
@@ -490,6 +530,67 @@ export const updateResource = async (
490
530
  return processResourceRequest(typedCall, res, data);
491
531
  };
492
532
 
533
+ export const updateResourceFromSource = async (
534
+ typedCall: TypedCall,
535
+ fileName: string,
536
+ source: FirmwareByteSource,
537
+ onConfirmAfter?: () => void
538
+ ) => {
539
+ const initialLength = Math.min(INIT_DATA_CHUNK_SIZE, source.size);
540
+ const initialChunk = new Uint8Array(await source.readAt(0, initialLength));
541
+ let response = await typedCall('ResourceUpdate', ['ResourceRequest', 'Success'], {
542
+ file_name: fileName,
543
+ data_length: source.size,
544
+ initial_data_chunk: bytesToHex(initialChunk),
545
+ hash: bytesToHex(blake2s(initialChunk)),
546
+ });
547
+ onConfirmAfter?.();
548
+
549
+ while (response.type !== 'Success') {
550
+ const { offset, data_length: dataLength } = response.message;
551
+ if (
552
+ !Number.isSafeInteger(offset) ||
553
+ offset === undefined ||
554
+ offset < 0 ||
555
+ !Number.isSafeInteger(dataLength) ||
556
+ dataLength === undefined ||
557
+ dataLength <= 0 ||
558
+ offset + dataLength > source.size
559
+ ) {
560
+ throw ERRORS.TypedError(
561
+ HardwareErrorCode.RuntimeError,
562
+ 'Device requested an invalid firmware resource range'
563
+ );
564
+ }
565
+ const chunk = new Uint8Array(await source.readAt(offset, dataLength));
566
+ response = await typedCall('ResourceAck', ['ResourceRequest', 'Success'], {
567
+ data_chunk: bytesToHex(chunk),
568
+ hash: bytesToHex(blake2s(chunk)),
569
+ });
570
+ }
571
+ return response.message;
572
+ };
573
+
574
+ export const updateResourcesFromSources = async (
575
+ typedCall: TypedCall,
576
+ postMessage: (message: CoreMessage) => void,
577
+ device: Device,
578
+ entries: ReadonlyArray<{ entryName: string; source: FirmwareByteSource }>
579
+ ) => {
580
+ postProgressTip(device, 'UpdateSysResource', postMessage);
581
+ for (const [index, entry] of entries.entries()) {
582
+ await updateResourceFromSource(typedCall, entry.entryName, entry.source);
583
+ postProgressMessage(
584
+ device,
585
+ Math.floor(((index + 1) / entries.length) * 100),
586
+ 'installingFirmware',
587
+ postMessage
588
+ );
589
+ }
590
+ postProgressTip(device, 'UpdateSysResourceSuccess', postMessage);
591
+ return true;
592
+ };
593
+
493
594
  export const updateResources = async (
494
595
  typedCall: TypedCall,
495
596
  postMessage: (message: CoreMessage) => void,
@@ -524,11 +625,28 @@ export const updateBootloader = async (
524
625
  typedCall: TypedCall,
525
626
  postMessage: (message: CoreMessage) => void,
526
627
  device: Device,
527
- source: ArrayBuffer
628
+ data: ArrayBuffer
629
+ ) => {
630
+ const source = await openFirmwareByteSource({ binary: data });
631
+ if (!source) {
632
+ throw ERRORS.TypedError(HardwareErrorCode.RuntimeError, 'bootloader payload is empty');
633
+ }
634
+ try {
635
+ return await updateBootloaderFromSource(typedCall, postMessage, device, source);
636
+ } finally {
637
+ await source.close();
638
+ }
639
+ };
640
+
641
+ export const updateBootloaderFromSource = async (
642
+ typedCall: TypedCall,
643
+ postMessage: (message: CoreMessage) => void,
644
+ device: Device,
645
+ source: FirmwareByteSource
528
646
  ) => {
529
647
  postProgressTip(device, 'UpdateBootloader', postMessage);
530
648
  postProgressMessage(device, Math.floor(0), 'installingFirmware', postMessage);
531
- await updateResource(typedCall, 'bootloader.bin', source, () => {
649
+ await updateResourceFromSource(typedCall, 'bootloader.bin', source, () => {
532
650
  postProcessingMessage('resource', postMessage);
533
651
  });
534
652
  postProgressMessage(device, Math.floor(100), 'installingFirmware', postMessage);