@onekeyfe/hd-core 1.2.0-alpha.21 → 1.2.0-alpha.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/__tests__/DeviceCommands.test.ts +85 -0
- package/__tests__/connectSettings.test.ts +47 -5
- package/__tests__/device-identity.test.ts +24 -2
- package/__tests__/device-settings.test.ts +2 -2
- package/__tests__/device-wallet-session-store.test.ts +152 -7
- package/__tests__/firmware-update/check-all-firmware-release.test.ts +86 -0
- package/__tests__/firmware-update/firmware-artifact-source.test.ts +119 -0
- package/__tests__/firmware-update/firmware-host-binding.test.ts +70 -0
- package/__tests__/firmware-update/firmware-update-capabilities.test.ts +13 -0
- package/__tests__/firmware-update/firmware-update-plan.test.ts +326 -0
- package/__tests__/firmware-update/firmware-update-prepared-plan.test.ts +213 -0
- package/__tests__/firmware-update/firmware-update-v2-download-before-boot.test.ts +39 -30
- package/__tests__/firmware-update/firmware-upload-source.test.ts +70 -0
- package/__tests__/logBlockEvent.test.ts +17 -0
- package/__tests__/open-wallet-session.test.ts +311 -12
- package/__tests__/pro2Wallpaper.test.ts +7 -18
- package/__tests__/protocol-v2-unlock-policy.test.ts +45 -0
- package/__tests__/protocol-v2.test.ts +685 -177
- package/__tests__/public-device-state-api.test.ts +69 -62
- package/__tests__/public-pro2-api-boundary.test.ts +8 -34
- package/dist/api/BaseMethod.d.ts +2 -1
- package/dist/api/BaseMethod.d.ts.map +1 -1
- package/dist/api/CheckAllFirmwareRelease.d.ts.map +1 -1
- package/dist/api/ClearSessionCache.d.ts.map +1 -1
- package/dist/api/FirmwareUpdate.d.ts.map +1 -1
- package/dist/api/FirmwareUpdateV2.d.ts +11 -0
- package/dist/api/FirmwareUpdateV2.d.ts.map +1 -1
- package/dist/api/FirmwareUpdateV3.d.ts +7 -3
- package/dist/api/FirmwareUpdateV3.d.ts.map +1 -1
- package/dist/api/FirmwareUpdateV4.d.ts +29 -10
- package/dist/api/FirmwareUpdateV4.d.ts.map +1 -1
- package/dist/api/GetPassphraseState.d.ts.map +1 -1
- package/dist/api/OpenWalletSession.d.ts.map +1 -1
- package/dist/api/conflux/ConfluxGetAddress.d.ts +1 -0
- package/dist/api/conflux/ConfluxGetAddress.d.ts.map +1 -1
- package/dist/api/conflux/ConfluxSignMessage.d.ts +1 -0
- package/dist/api/conflux/ConfluxSignMessage.d.ts.map +1 -1
- package/dist/api/conflux/ConfluxSignMessageCIP23.d.ts +1 -0
- package/dist/api/conflux/ConfluxSignMessageCIP23.d.ts.map +1 -1
- package/dist/api/conflux/ConfluxSignTransaction.d.ts +1 -0
- package/dist/api/conflux/ConfluxSignTransaction.d.ts.map +1 -1
- package/dist/api/device/DeviceUnlock.d.ts +1 -1
- package/dist/api/device/DeviceUnlock.d.ts.map +1 -1
- package/dist/api/device/DeviceUpdateBootloader.d.ts +5 -1
- package/dist/api/device/DeviceUpdateBootloader.d.ts.map +1 -1
- package/dist/api/device/DeviceWipe.d.ts.map +1 -1
- package/dist/api/firmware/FirmwareArtifactSource.d.ts +25 -0
- package/dist/api/firmware/FirmwareArtifactSource.d.ts.map +1 -0
- package/dist/api/firmware/FirmwareHostBinding.d.ts +6 -0
- package/dist/api/firmware/FirmwareHostBinding.d.ts.map +1 -0
- package/dist/api/firmware/FirmwareUpdateBaseMethod.d.ts +7 -0
- package/dist/api/firmware/FirmwareUpdateBaseMethod.d.ts.map +1 -1
- package/dist/api/firmware/FirmwareUpdateCapabilities.d.ts +3 -0
- package/dist/api/firmware/FirmwareUpdateCapabilities.d.ts.map +1 -0
- package/dist/api/firmware/FirmwareUpdatePlan.d.ts +23 -0
- package/dist/api/firmware/FirmwareUpdatePlan.d.ts.map +1 -0
- package/dist/api/firmware/FirmwareUpdatePreparedPlan.d.ts +30 -0
- package/dist/api/firmware/FirmwareUpdatePreparedPlan.d.ts.map +1 -0
- package/dist/api/firmware/getBinary.d.ts +7 -0
- package/dist/api/firmware/getBinary.d.ts.map +1 -1
- package/dist/api/firmware/updateBootloader.d.ts +2 -0
- package/dist/api/firmware/updateBootloader.d.ts.map +1 -1
- package/dist/api/firmware/uploadFirmware.d.ts +9 -1
- package/dist/api/firmware/uploadFirmware.d.ts.map +1 -1
- package/dist/api/helpers/protocolV2FileWrite.d.ts.map +1 -1
- package/dist/api/index.d.ts +0 -17
- package/dist/api/index.d.ts.map +1 -1
- package/dist/api/nexa/NexaSignTransaction.d.ts +1 -0
- package/dist/api/nexa/NexaSignTransaction.d.ts.map +1 -1
- package/dist/api/stellar/StellarSignTransaction.d.ts +1 -1
- package/dist/core/index.d.ts.map +1 -1
- package/dist/data-manager/DataManager.d.ts +1 -0
- package/dist/data-manager/DataManager.d.ts.map +1 -1
- package/dist/data-manager/connectSettings.d.ts.map +1 -1
- package/dist/device/Device.d.ts +3 -2
- package/dist/device/Device.d.ts.map +1 -1
- package/dist/device/DeviceCommands.d.ts.map +1 -1
- package/dist/device/DeviceStateMapper.d.ts.map +1 -1
- package/dist/device/DeviceStateProjector.d.ts.map +1 -1
- package/dist/events/device.d.ts +1 -0
- package/dist/events/device.d.ts.map +1 -1
- package/dist/events/logBlockEvent.d.ts.map +1 -1
- package/dist/events/ui-request.d.ts +1 -0
- package/dist/events/ui-request.d.ts.map +1 -1
- package/dist/index.d.ts +273 -230
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3060 -1676
- package/dist/inject.d.ts.map +1 -1
- package/dist/lowLevelInject.d.ts.map +1 -1
- package/dist/protocols/protocol-v2/settingsUnlockPolicy.d.ts.map +1 -1
- package/dist/protocols/protocol-v2/unlockPolicy.d.ts +4 -0
- package/dist/protocols/protocol-v2/unlockPolicy.d.ts.map +1 -0
- package/dist/protocols/protocol-v2/unlockRetry.d.ts.map +1 -1
- package/dist/protocols/protocol-v2/walletSession.d.ts.map +1 -1
- package/dist/topLevelInject.d.ts.map +1 -1
- package/dist/types/api/checkAllFirmwareRelease.d.ts +3 -0
- package/dist/types/api/checkAllFirmwareRelease.d.ts.map +1 -1
- package/dist/types/api/deviceUnlock.d.ts +2 -2
- package/dist/types/api/deviceUnlock.d.ts.map +1 -1
- package/dist/types/api/deviceUpdateBootloader.d.ts +6 -0
- package/dist/types/api/deviceUpdateBootloader.d.ts.map +1 -1
- package/dist/types/api/export.d.ts +4 -1
- package/dist/types/api/export.d.ts.map +1 -1
- package/dist/types/api/firmwareUpdate.d.ts +69 -0
- package/dist/types/api/firmwareUpdate.d.ts.map +1 -1
- package/dist/types/api/firmwareUpdateCapabilities.d.ts +9 -0
- package/dist/types/api/firmwareUpdateCapabilities.d.ts.map +1 -0
- package/dist/types/api/firmwareUpdatePlan.d.ts +27 -0
- package/dist/types/api/firmwareUpdatePlan.d.ts.map +1 -0
- package/dist/types/api/firmwareUpdatePreparedPlan.d.ts +42 -0
- package/dist/types/api/firmwareUpdatePreparedPlan.d.ts.map +1 -0
- package/dist/types/api/getPassphraseState.d.ts.map +1 -1
- package/dist/types/api/index.d.ts +10 -19
- package/dist/types/api/index.d.ts.map +1 -1
- package/dist/types/api/openWalletSession.d.ts +1 -0
- package/dist/types/api/openWalletSession.d.ts.map +1 -1
- package/dist/types/api/protocolV2.d.ts +3 -76
- package/dist/types/api/protocolV2.d.ts.map +1 -1
- package/dist/types/api/sessionCache.d.ts +4 -1
- package/dist/types/api/sessionCache.d.ts.map +1 -1
- package/dist/types/settings.d.ts +13 -0
- package/dist/types/settings.d.ts.map +1 -1
- package/dist/utils/deviceFeaturesUtils.d.ts.map +1 -1
- package/dist/utils/deviceInfoUtils.d.ts.map +1 -1
- package/dist/utils/patch.d.ts +1 -1
- package/dist/utils/patch.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/api/BaseMethod.ts +8 -3
- package/src/api/CheckAllFirmwareRelease.ts +23 -3
- package/src/api/ClearSessionCache.ts +4 -0
- package/src/api/FirmwareUpdate.ts +8 -1
- package/src/api/FirmwareUpdateV2.ts +292 -120
- package/src/api/FirmwareUpdateV3.ts +256 -55
- package/src/api/FirmwareUpdateV4.ts +821 -331
- package/src/api/GetPassphraseState.ts +11 -5
- package/src/api/OpenWalletSession.ts +74 -25
- package/src/api/UploadPortfolio.ts +1 -1
- package/src/api/conflux/ConfluxGetAddress.ts +2 -0
- package/src/api/conflux/ConfluxSignMessage.ts +2 -0
- package/src/api/conflux/ConfluxSignMessageCIP23.ts +2 -0
- package/src/api/conflux/ConfluxSignTransaction.ts +2 -0
- package/src/api/device/DeviceChangePin.ts +1 -1
- package/src/api/device/DeviceUnlock.ts +1 -2
- package/src/api/device/DeviceUpdateBootloader.ts +122 -6
- package/src/api/device/DeviceWipe.ts +3 -1
- package/src/api/firmware/FirmwareArtifactSource.ts +278 -0
- package/src/api/firmware/FirmwareHostBinding.ts +100 -0
- package/src/api/firmware/FirmwareUpdateBaseMethod.ts +50 -0
- package/src/api/firmware/FirmwareUpdateCapabilities.ts +9 -0
- package/src/api/firmware/FirmwareUpdatePlan.ts +621 -0
- package/src/api/firmware/FirmwareUpdatePreparedPlan.ts +392 -0
- package/src/api/firmware/updateBootloader.ts +19 -4
- package/src/api/firmware/uploadFirmware.ts +140 -22
- package/src/api/helpers/protocolV2FileWrite.ts +6 -0
- package/src/api/index.ts +0 -18
- package/src/api/nexa/NexaSignTransaction.ts +2 -0
- package/src/api/protocol-v2/DeviceUploadWallpaper.ts +1 -1
- package/src/core/index.ts +1 -0
- package/src/data/messages/messages-protocol-v2.json +35 -1
- package/src/data-manager/DataManager.ts +33 -33
- package/src/data-manager/connectSettings.ts +11 -0
- package/src/device/Device.ts +80 -67
- package/src/device/DeviceCommands.ts +76 -159
- package/src/device/DeviceStateMapper.ts +18 -1
- package/src/device/DeviceStateProjector.ts +7 -1
- package/src/events/device.ts +1 -0
- package/src/events/logBlockEvent.ts +1 -14
- package/src/events/ui-request.ts +1 -0
- package/src/index.ts +5 -0
- package/src/inject.ts +22 -25
- package/src/lowLevelInject.ts +5 -1
- package/src/protocols/protocol-v2/settingsUnlockPolicy.ts +2 -3
- package/src/protocols/protocol-v2/unlockPolicy.ts +105 -0
- package/src/protocols/protocol-v2/unlockRetry.ts +1 -3
- package/src/protocols/protocol-v2/walletSession.ts +86 -9
- package/src/topLevelInject.ts +5 -1
- package/src/types/api/checkAllFirmwareRelease.ts +3 -0
- package/src/types/api/deviceUnlock.ts +2 -5
- package/src/types/api/deviceUpdateBootloader.ts +6 -0
- package/src/types/api/export.ts +18 -0
- package/src/types/api/firmwareUpdate.ts +76 -0
- package/src/types/api/firmwareUpdateCapabilities.ts +9 -0
- package/src/types/api/firmwareUpdatePlan.ts +36 -0
- package/src/types/api/firmwareUpdatePreparedPlan.ts +53 -0
- package/src/types/api/getPassphraseState.ts +4 -3
- package/src/types/api/index.ts +14 -34
- package/src/types/api/openWalletSession.ts +2 -0
- package/src/types/api/protocolV2.ts +3 -156
- package/src/types/api/sessionCache.ts +9 -4
- package/src/types/settings.ts +13 -0
- package/src/utils/deviceFeaturesUtils.ts +5 -1
- package/src/utils/deviceInfoUtils.ts +3 -16
|
@@ -0,0 +1,621 @@
|
|
|
1
|
+
import { bytesToHex } from '@noble/hashes/utils';
|
|
2
|
+
import { sha256 } from '@noble/hashes/sha256';
|
|
3
|
+
import { EDeviceType, EFirmwareType, ERRORS, HardwareErrorCode } from '@onekeyfe/hd-shared';
|
|
4
|
+
import semver from 'semver';
|
|
5
|
+
|
|
6
|
+
import { getDeviceType, getDeviceUUID } from '../../utils/deviceInfoUtils';
|
|
7
|
+
import {
|
|
8
|
+
getDeviceBootloaderVersion,
|
|
9
|
+
getDeviceFirmwareVersion,
|
|
10
|
+
} from '../../utils/deviceVersionUtils';
|
|
11
|
+
|
|
12
|
+
import type {
|
|
13
|
+
FirmwareUpdatePlan,
|
|
14
|
+
FirmwareUpdatePlanArtifact,
|
|
15
|
+
FirmwareUpdatePlanTarget,
|
|
16
|
+
} from '../../types/api/firmwareUpdatePlan';
|
|
17
|
+
import type { Features } from '../../types';
|
|
18
|
+
|
|
19
|
+
type FirmwareUpdatePlatform = 'native' | 'desktop' | 'ext' | 'web' | 'web-embed';
|
|
20
|
+
|
|
21
|
+
type ReleaseRecord = {
|
|
22
|
+
url?: unknown;
|
|
23
|
+
webUpdate?: unknown;
|
|
24
|
+
resource?: unknown;
|
|
25
|
+
fullResource?: unknown;
|
|
26
|
+
fullResourceRange?: unknown;
|
|
27
|
+
bootloaderResource?: unknown;
|
|
28
|
+
version?: unknown;
|
|
29
|
+
components?: unknown;
|
|
30
|
+
installOrder?: unknown;
|
|
31
|
+
resourceBundles?: unknown;
|
|
32
|
+
fingerprint?: unknown;
|
|
33
|
+
fingerprintWeb?: unknown;
|
|
34
|
+
expectedSize?: unknown;
|
|
35
|
+
resourceFingerprint?: unknown;
|
|
36
|
+
resourceExpectedSize?: unknown;
|
|
37
|
+
fullResourceFingerprint?: unknown;
|
|
38
|
+
fullResourceExpectedSize?: unknown;
|
|
39
|
+
bootloaderFingerprint?: unknown;
|
|
40
|
+
bootloaderExpectedSize?: unknown;
|
|
41
|
+
bootloaderVersion?: unknown;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
type ReleaseSelection = {
|
|
45
|
+
shouldUpdate?: boolean;
|
|
46
|
+
status?: unknown;
|
|
47
|
+
release?: unknown;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const PROTOCOL_V2_TARGETS: Readonly<
|
|
51
|
+
Record<string, Exclude<FirmwareUpdatePlanTarget, 'firmware' | 'ble' | 'bootloader' | 'resource'>>
|
|
52
|
+
> = {
|
|
53
|
+
BOOTLOADER: 'boot',
|
|
54
|
+
APPLICATION_P1: 'app_v1',
|
|
55
|
+
APPLICATION_P2: 'app_v2',
|
|
56
|
+
COPROCESSOR: 'coprocessor',
|
|
57
|
+
SE01: 'se01',
|
|
58
|
+
SE02: 'se02',
|
|
59
|
+
SE03: 'se03',
|
|
60
|
+
SE04: 'se04',
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const asRecord = (value: unknown): Record<string, unknown> | undefined =>
|
|
64
|
+
value && typeof value === 'object' && !Array.isArray(value)
|
|
65
|
+
? (value as Record<string, unknown>)
|
|
66
|
+
: undefined;
|
|
67
|
+
|
|
68
|
+
const asRelease = (release: ReleaseSelection | undefined): ReleaseRecord | undefined =>
|
|
69
|
+
asRecord(release?.release) as ReleaseRecord | undefined;
|
|
70
|
+
|
|
71
|
+
const asString = (value: unknown): string | undefined =>
|
|
72
|
+
typeof value === 'string' && value.length > 0 ? value : undefined;
|
|
73
|
+
|
|
74
|
+
const asVersion = (value: unknown): string | undefined => {
|
|
75
|
+
if (
|
|
76
|
+
!Array.isArray(value) ||
|
|
77
|
+
value.length !== 3 ||
|
|
78
|
+
value.some(part => typeof part !== 'number' || !Number.isSafeInteger(part) || part < 0)
|
|
79
|
+
) {
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
82
|
+
return value.join('.');
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const asIntegrity = ({
|
|
86
|
+
size,
|
|
87
|
+
sha256: value,
|
|
88
|
+
}: {
|
|
89
|
+
size: unknown;
|
|
90
|
+
sha256: unknown;
|
|
91
|
+
}): Pick<FirmwareUpdatePlanArtifact, 'expectedSize' | 'expectedSha256'> => {
|
|
92
|
+
const sha256Value =
|
|
93
|
+
typeof value === 'string' && /^[a-f0-9]{64}$/iu.test(value) ? value.toLowerCase() : undefined;
|
|
94
|
+
return {
|
|
95
|
+
...(Number.isSafeInteger(size) && (size as number) > 0 ? { expectedSize: size as number } : {}),
|
|
96
|
+
...(sha256Value ? { expectedSha256: sha256Value } : {}),
|
|
97
|
+
};
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const isUpgrade = (release: ReleaseSelection | undefined): boolean =>
|
|
101
|
+
release?.status === 'required' ||
|
|
102
|
+
release?.status === 'outdated' ||
|
|
103
|
+
release?.shouldUpdate === true;
|
|
104
|
+
|
|
105
|
+
const assertArtifactUrl = (value: unknown, label: string): string => {
|
|
106
|
+
const raw = asString(value);
|
|
107
|
+
if (!raw) {
|
|
108
|
+
throw ERRORS.TypedError(
|
|
109
|
+
HardwareErrorCode.RuntimeError,
|
|
110
|
+
`${label} has no downloadable artifact`,
|
|
111
|
+
{ firmwareUpdateCode: 'FirmwarePlanInvalid' }
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
const parsed = new URL(raw);
|
|
115
|
+
if (
|
|
116
|
+
parsed.protocol !== 'https:' ||
|
|
117
|
+
parsed.port !== '' ||
|
|
118
|
+
parsed.username ||
|
|
119
|
+
parsed.password ||
|
|
120
|
+
parsed.hash
|
|
121
|
+
) {
|
|
122
|
+
throw ERRORS.TypedError(HardwareErrorCode.RuntimeError, `${label} URL is not canonical HTTPS`, {
|
|
123
|
+
firmwareUpdateCode: 'FirmwarePlanInvalid',
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
return raw;
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const canonicalizeFirmwareUpdateContract = (value: unknown): unknown => {
|
|
130
|
+
if (Array.isArray(value)) {
|
|
131
|
+
return value.map(canonicalizeFirmwareUpdateContract);
|
|
132
|
+
}
|
|
133
|
+
if (value && typeof value === 'object') {
|
|
134
|
+
return Object.keys(value as Record<string, unknown>)
|
|
135
|
+
.sort()
|
|
136
|
+
.reduce<Record<string, unknown>>((result, key) => {
|
|
137
|
+
const item = (value as Record<string, unknown>)[key];
|
|
138
|
+
if (item !== undefined) {
|
|
139
|
+
result[key] = canonicalizeFirmwareUpdateContract(item);
|
|
140
|
+
}
|
|
141
|
+
return result;
|
|
142
|
+
}, {});
|
|
143
|
+
}
|
|
144
|
+
return value;
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
export const digestFirmwareUpdateContract = (value: unknown): string =>
|
|
148
|
+
bytesToHex(
|
|
149
|
+
sha256(new TextEncoder().encode(JSON.stringify(canonicalizeFirmwareUpdateContract(value))))
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
const digestFirmwareUpdatePlan = (plan: Omit<FirmwareUpdatePlan, 'planDigest'>): string =>
|
|
153
|
+
digestFirmwareUpdateContract(plan);
|
|
154
|
+
|
|
155
|
+
const planError = (message: string): never => {
|
|
156
|
+
throw ERRORS.TypedError(HardwareErrorCode.RuntimeError, message, {
|
|
157
|
+
firmwareUpdateCode: 'FirmwarePlanInvalid',
|
|
158
|
+
});
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
const assertExactKeys = (
|
|
162
|
+
value: Record<string, unknown>,
|
|
163
|
+
required: readonly string[],
|
|
164
|
+
optional: readonly string[] = []
|
|
165
|
+
) => {
|
|
166
|
+
const allowed = new Set([...required, ...optional]);
|
|
167
|
+
if (
|
|
168
|
+
required.some(key => !Object.prototype.hasOwnProperty.call(value, key)) ||
|
|
169
|
+
Object.keys(value).some(key => !allowed.has(key))
|
|
170
|
+
) {
|
|
171
|
+
planError('Firmware update plan contains an invalid field set');
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
const assertBoundedString = (value: unknown, label: string, maxLength = 256): string => {
|
|
176
|
+
if (typeof value !== 'string' || value.length === 0 || value.length > maxLength) {
|
|
177
|
+
return planError(`Firmware update plan ${label} is invalid`);
|
|
178
|
+
}
|
|
179
|
+
return value;
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
export const FIRMWARE_UPDATE_PLAN_TARGETS = new Set<FirmwareUpdatePlanTarget>([
|
|
183
|
+
'firmware',
|
|
184
|
+
'ble',
|
|
185
|
+
'bootloader',
|
|
186
|
+
'resource',
|
|
187
|
+
'boot',
|
|
188
|
+
'app_v1',
|
|
189
|
+
'app_v2',
|
|
190
|
+
'coprocessor',
|
|
191
|
+
'se01',
|
|
192
|
+
'se02',
|
|
193
|
+
'se03',
|
|
194
|
+
'se04',
|
|
195
|
+
]);
|
|
196
|
+
|
|
197
|
+
export const FIRMWARE_UPDATE_PLAN_ROLES = new Set([
|
|
198
|
+
'firmware',
|
|
199
|
+
'ble',
|
|
200
|
+
'bootloader',
|
|
201
|
+
'resource',
|
|
202
|
+
'component',
|
|
203
|
+
'resourceBundle',
|
|
204
|
+
]);
|
|
205
|
+
|
|
206
|
+
const PLAN_CONTAINERS = new Set(['raw', 'zip']);
|
|
207
|
+
|
|
208
|
+
const PLAN_PLATFORMS = new Set<FirmwareUpdatePlatform>([
|
|
209
|
+
'native',
|
|
210
|
+
'desktop',
|
|
211
|
+
'ext',
|
|
212
|
+
'web',
|
|
213
|
+
'web-embed',
|
|
214
|
+
]);
|
|
215
|
+
|
|
216
|
+
export const assertFirmwareUpdatePlan = (value: unknown): FirmwareUpdatePlan => {
|
|
217
|
+
const plan = asRecord(value);
|
|
218
|
+
if (!plan) {
|
|
219
|
+
return planError('Firmware update plan must be an object');
|
|
220
|
+
}
|
|
221
|
+
assertExactKeys(plan, [
|
|
222
|
+
'schemaVersion',
|
|
223
|
+
'planDigest',
|
|
224
|
+
'executor',
|
|
225
|
+
'deviceIdentity',
|
|
226
|
+
'deviceModel',
|
|
227
|
+
'firmwareType',
|
|
228
|
+
'platform',
|
|
229
|
+
'artifacts',
|
|
230
|
+
'targetsToUpdate',
|
|
231
|
+
]);
|
|
232
|
+
if (
|
|
233
|
+
plan.schemaVersion !== 2 ||
|
|
234
|
+
typeof plan.planDigest !== 'string' ||
|
|
235
|
+
!/^[a-f0-9]{64}$/u.test(plan.planDigest) ||
|
|
236
|
+
(plan.executor !== 'v2' && plan.executor !== 'v3' && plan.executor !== 'v4') ||
|
|
237
|
+
!PLAN_PLATFORMS.has(plan.platform as FirmwareUpdatePlatform) ||
|
|
238
|
+
(plan.firmwareType !== EFirmwareType.Universal &&
|
|
239
|
+
plan.firmwareType !== EFirmwareType.BitcoinOnly)
|
|
240
|
+
) {
|
|
241
|
+
return planError('Firmware update plan header is invalid');
|
|
242
|
+
}
|
|
243
|
+
assertBoundedString(plan.deviceIdentity, 'device identity');
|
|
244
|
+
assertBoundedString(plan.deviceModel, 'device model', 64);
|
|
245
|
+
if (!Array.isArray(plan.artifacts) || plan.artifacts.length > 128) {
|
|
246
|
+
return planError('Firmware update plan artifact set is invalid');
|
|
247
|
+
}
|
|
248
|
+
const artifactIds = new Set<string>();
|
|
249
|
+
const artifactTargets = new Set<FirmwareUpdatePlanTarget>();
|
|
250
|
+
for (const item of plan.artifacts) {
|
|
251
|
+
const artifact = asRecord(item);
|
|
252
|
+
if (!artifact) {
|
|
253
|
+
return planError('Firmware update plan artifact is invalid');
|
|
254
|
+
}
|
|
255
|
+
assertExactKeys(
|
|
256
|
+
artifact,
|
|
257
|
+
['artifactId', 'role', 'target', 'url', 'container'],
|
|
258
|
+
['logicalName', 'expectedSize', 'expectedSha256', 'targetVersion']
|
|
259
|
+
);
|
|
260
|
+
const artifactId = assertBoundedString(artifact.artifactId, 'artifact id', 160);
|
|
261
|
+
if (artifactIds.has(artifactId)) {
|
|
262
|
+
return planError('Firmware update plan contains duplicate artifact ids');
|
|
263
|
+
}
|
|
264
|
+
artifactIds.add(artifactId);
|
|
265
|
+
if (
|
|
266
|
+
!FIRMWARE_UPDATE_PLAN_ROLES.has(artifact.role as string) ||
|
|
267
|
+
!FIRMWARE_UPDATE_PLAN_TARGETS.has(artifact.target as FirmwareUpdatePlanTarget) ||
|
|
268
|
+
!PLAN_CONTAINERS.has(artifact.container as string)
|
|
269
|
+
) {
|
|
270
|
+
return planError('Firmware update plan artifact contract is invalid');
|
|
271
|
+
}
|
|
272
|
+
artifactTargets.add(artifact.target as FirmwareUpdatePlanTarget);
|
|
273
|
+
assertArtifactUrl(artifact.url, `Firmware artifact ${artifactId}`);
|
|
274
|
+
if (artifact.logicalName !== undefined) {
|
|
275
|
+
assertBoundedString(artifact.logicalName, 'logical name', 256);
|
|
276
|
+
}
|
|
277
|
+
if (
|
|
278
|
+
artifact.expectedSize !== undefined &&
|
|
279
|
+
(!Number.isSafeInteger(artifact.expectedSize) || (artifact.expectedSize as number) <= 0)
|
|
280
|
+
) {
|
|
281
|
+
return planError('Firmware update plan artifact size is invalid');
|
|
282
|
+
}
|
|
283
|
+
if (
|
|
284
|
+
artifact.expectedSha256 !== undefined &&
|
|
285
|
+
(typeof artifact.expectedSha256 !== 'string' ||
|
|
286
|
+
!/^[a-f0-9]{64}$/u.test(artifact.expectedSha256))
|
|
287
|
+
) {
|
|
288
|
+
return planError('Firmware update plan artifact digest is invalid');
|
|
289
|
+
}
|
|
290
|
+
if (artifact.targetVersion !== undefined) {
|
|
291
|
+
assertBoundedString(artifact.targetVersion, 'target version', 64);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
if (
|
|
295
|
+
!Array.isArray(plan.targetsToUpdate) ||
|
|
296
|
+
plan.targetsToUpdate.some(
|
|
297
|
+
target => !FIRMWARE_UPDATE_PLAN_TARGETS.has(target as FirmwareUpdatePlanTarget)
|
|
298
|
+
) ||
|
|
299
|
+
new Set(plan.targetsToUpdate).size !== plan.targetsToUpdate.length ||
|
|
300
|
+
plan.targetsToUpdate.length !== artifactTargets.size ||
|
|
301
|
+
plan.targetsToUpdate.some(target => !artifactTargets.has(target as FirmwareUpdatePlanTarget))
|
|
302
|
+
) {
|
|
303
|
+
return planError('Firmware update plan targets are invalid');
|
|
304
|
+
}
|
|
305
|
+
const legacyTargets = new Set<FirmwareUpdatePlanTarget>([
|
|
306
|
+
'firmware',
|
|
307
|
+
'ble',
|
|
308
|
+
'bootloader',
|
|
309
|
+
'resource',
|
|
310
|
+
]);
|
|
311
|
+
const legacyOnlyTargets = new Set<FirmwareUpdatePlanTarget>(['firmware', 'ble', 'bootloader']);
|
|
312
|
+
if (
|
|
313
|
+
((plan.executor === 'v2' || plan.executor === 'v3') &&
|
|
314
|
+
plan.targetsToUpdate.some(
|
|
315
|
+
target => !legacyTargets.has(target as FirmwareUpdatePlanTarget)
|
|
316
|
+
)) ||
|
|
317
|
+
(plan.executor === 'v4' &&
|
|
318
|
+
plan.targetsToUpdate.some(target =>
|
|
319
|
+
legacyOnlyTargets.has(target as FirmwareUpdatePlanTarget)
|
|
320
|
+
)) ||
|
|
321
|
+
(plan.artifacts.length > 0 &&
|
|
322
|
+
(plan.platform === 'native' || plan.platform === 'desktop') &&
|
|
323
|
+
plan.deviceIdentity === 'unavailable')
|
|
324
|
+
) {
|
|
325
|
+
return planError('Firmware update plan executor contract is invalid');
|
|
326
|
+
}
|
|
327
|
+
const { planDigest, ...withoutDigest } = plan as unknown as FirmwareUpdatePlan;
|
|
328
|
+
if (digestFirmwareUpdatePlan(withoutDigest) !== planDigest) {
|
|
329
|
+
return planError('Firmware update plan digest is invalid');
|
|
330
|
+
}
|
|
331
|
+
return plan as unknown as FirmwareUpdatePlan;
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
const selectResourceUrl = ({
|
|
335
|
+
release,
|
|
336
|
+
features,
|
|
337
|
+
platform,
|
|
338
|
+
}: {
|
|
339
|
+
release: ReleaseRecord;
|
|
340
|
+
features: Features;
|
|
341
|
+
platform: FirmwareUpdatePlatform;
|
|
342
|
+
}): string | undefined => {
|
|
343
|
+
const resource = asString(release.resource);
|
|
344
|
+
const fullResource = asString(release.fullResource);
|
|
345
|
+
const range = release.fullResourceRange;
|
|
346
|
+
const currentVersion = getDeviceFirmwareVersion(features).join('.');
|
|
347
|
+
const targetVersion = asVersion(release.version);
|
|
348
|
+
if (
|
|
349
|
+
platform === 'desktop' &&
|
|
350
|
+
fullResource &&
|
|
351
|
+
targetVersion &&
|
|
352
|
+
Array.isArray(range) &&
|
|
353
|
+
range.length === 2 &&
|
|
354
|
+
typeof range[0] === 'string' &&
|
|
355
|
+
typeof range[1] === 'string' &&
|
|
356
|
+
semver.valid(currentVersion) &&
|
|
357
|
+
semver.valid(targetVersion) &&
|
|
358
|
+
semver.valid(range[0]) &&
|
|
359
|
+
semver.valid(range[1]) &&
|
|
360
|
+
semver.lt(currentVersion, range[0]) &&
|
|
361
|
+
semver.gte(targetVersion, range[1])
|
|
362
|
+
) {
|
|
363
|
+
return fullResource;
|
|
364
|
+
}
|
|
365
|
+
return resource;
|
|
366
|
+
};
|
|
367
|
+
|
|
368
|
+
const getExecutor = (features: Features): FirmwareUpdatePlan['executor'] => {
|
|
369
|
+
const deviceType = getDeviceType(features);
|
|
370
|
+
if (deviceType === EDeviceType.Pro2) {
|
|
371
|
+
return 'v4';
|
|
372
|
+
}
|
|
373
|
+
if (
|
|
374
|
+
deviceType === EDeviceType.Pro &&
|
|
375
|
+
semver.gte(getDeviceBootloaderVersion(features).join('.'), '2.8.0')
|
|
376
|
+
) {
|
|
377
|
+
return 'v3';
|
|
378
|
+
}
|
|
379
|
+
return 'v2';
|
|
380
|
+
};
|
|
381
|
+
|
|
382
|
+
const buildProtocolV2Artifacts = (
|
|
383
|
+
release: ReleaseRecord
|
|
384
|
+
): {
|
|
385
|
+
artifacts: FirmwareUpdatePlanArtifact[];
|
|
386
|
+
targets: FirmwareUpdatePlanTarget[];
|
|
387
|
+
} => {
|
|
388
|
+
const components = asRecord(release.components);
|
|
389
|
+
if (!components) {
|
|
390
|
+
throw ERRORS.TypedError(
|
|
391
|
+
HardwareErrorCode.RuntimeError,
|
|
392
|
+
'Protocol V2 release has no component set',
|
|
393
|
+
{ firmwareUpdateCode: 'FirmwarePlanInvalid' }
|
|
394
|
+
);
|
|
395
|
+
}
|
|
396
|
+
const installOrder = Array.isArray(release.installOrder)
|
|
397
|
+
? release.installOrder.filter((key): key is string => typeof key === 'string')
|
|
398
|
+
: [];
|
|
399
|
+
const componentKeys = [
|
|
400
|
+
...installOrder,
|
|
401
|
+
...Object.keys(components).filter(key => !installOrder.includes(key)),
|
|
402
|
+
];
|
|
403
|
+
const artifacts: FirmwareUpdatePlanArtifact[] = [];
|
|
404
|
+
const targets: FirmwareUpdatePlanTarget[] = [];
|
|
405
|
+
const componentTargets = new Set<FirmwareUpdatePlanTarget>();
|
|
406
|
+
for (const key of componentKeys) {
|
|
407
|
+
const component = asRecord(components[key]);
|
|
408
|
+
const targetName = asString(component?.target)?.toUpperCase();
|
|
409
|
+
if (targetName === 'ROMLOADER') {
|
|
410
|
+
throw ERRORS.TypedError(
|
|
411
|
+
HardwareErrorCode.RuntimeError,
|
|
412
|
+
'Protocol V2 ROMLOADER requires its dedicated loader flow',
|
|
413
|
+
{ firmwareUpdateCode: 'FirmwarePlanInvalid' }
|
|
414
|
+
);
|
|
415
|
+
}
|
|
416
|
+
const target = targetName ? PROTOCOL_V2_TARGETS[targetName] : undefined;
|
|
417
|
+
if (!component || !target) {
|
|
418
|
+
throw ERRORS.TypedError(
|
|
419
|
+
HardwareErrorCode.RuntimeError,
|
|
420
|
+
`Protocol V2 component ${key} has an unsupported target`,
|
|
421
|
+
{ firmwareUpdateCode: 'FirmwarePlanInvalid' }
|
|
422
|
+
);
|
|
423
|
+
}
|
|
424
|
+
if (componentTargets.has(target)) {
|
|
425
|
+
throw ERRORS.TypedError(
|
|
426
|
+
HardwareErrorCode.RuntimeError,
|
|
427
|
+
`Protocol V2 component ${key} duplicates target ${target}`,
|
|
428
|
+
{ firmwareUpdateCode: 'FirmwarePlanInvalid' }
|
|
429
|
+
);
|
|
430
|
+
}
|
|
431
|
+
componentTargets.add(target);
|
|
432
|
+
artifacts.push({
|
|
433
|
+
artifactId: `component:${target}`,
|
|
434
|
+
role: 'component',
|
|
435
|
+
target,
|
|
436
|
+
url: assertArtifactUrl(component.url, `Protocol V2 component ${key}`),
|
|
437
|
+
container: 'raw',
|
|
438
|
+
logicalName: key,
|
|
439
|
+
...asIntegrity({
|
|
440
|
+
size: component.expectedSize,
|
|
441
|
+
sha256: component.fingerprint,
|
|
442
|
+
}),
|
|
443
|
+
...(asVersion(component.version) ? { targetVersion: asVersion(component.version) } : {}),
|
|
444
|
+
});
|
|
445
|
+
targets.push(target);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
const bundles = Array.isArray(release.resourceBundles) ? release.resourceBundles : [];
|
|
449
|
+
const resourceBundleNames = new Set<string>();
|
|
450
|
+
for (const value of bundles) {
|
|
451
|
+
const bundle = asRecord(value);
|
|
452
|
+
const name = asString(bundle?.name);
|
|
453
|
+
if (!bundle || !name) {
|
|
454
|
+
throw ERRORS.TypedError(
|
|
455
|
+
HardwareErrorCode.RuntimeError,
|
|
456
|
+
'Protocol V2 resource bundle is invalid',
|
|
457
|
+
{ firmwareUpdateCode: 'FirmwarePlanInvalid' }
|
|
458
|
+
);
|
|
459
|
+
}
|
|
460
|
+
if (resourceBundleNames.has(name)) {
|
|
461
|
+
throw ERRORS.TypedError(
|
|
462
|
+
HardwareErrorCode.RuntimeError,
|
|
463
|
+
`Protocol V2 resource bundle duplicates name ${name}`,
|
|
464
|
+
{ firmwareUpdateCode: 'FirmwarePlanInvalid' }
|
|
465
|
+
);
|
|
466
|
+
}
|
|
467
|
+
resourceBundleNames.add(name);
|
|
468
|
+
artifacts.push({
|
|
469
|
+
artifactId: `resourceBundle:${name}`,
|
|
470
|
+
role: 'resourceBundle',
|
|
471
|
+
target: 'resource',
|
|
472
|
+
url: assertArtifactUrl(bundle.url, `Protocol V2 resource bundle ${name}`),
|
|
473
|
+
container: 'raw',
|
|
474
|
+
logicalName: name,
|
|
475
|
+
...asIntegrity({
|
|
476
|
+
size: bundle.expectedSize,
|
|
477
|
+
sha256: bundle.fingerprint,
|
|
478
|
+
}),
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
if (bundles.length > 0) {
|
|
482
|
+
targets.push('resource');
|
|
483
|
+
}
|
|
484
|
+
return { artifacts, targets: [...new Set(targets)] };
|
|
485
|
+
};
|
|
486
|
+
|
|
487
|
+
export const buildFirmwareUpdatePlan = ({
|
|
488
|
+
features,
|
|
489
|
+
firmwareType,
|
|
490
|
+
platform,
|
|
491
|
+
firmware,
|
|
492
|
+
ble,
|
|
493
|
+
bootloader,
|
|
494
|
+
}: {
|
|
495
|
+
features: Features;
|
|
496
|
+
firmwareType: EFirmwareType;
|
|
497
|
+
platform: FirmwareUpdatePlatform;
|
|
498
|
+
firmware: ReleaseSelection;
|
|
499
|
+
ble: ReleaseSelection;
|
|
500
|
+
bootloader?: ReleaseSelection;
|
|
501
|
+
}): FirmwareUpdatePlan => {
|
|
502
|
+
const executor = getExecutor(features);
|
|
503
|
+
let artifacts: FirmwareUpdatePlanArtifact[] = [];
|
|
504
|
+
let targetsToUpdate: FirmwareUpdatePlanTarget[] = [];
|
|
505
|
+
const firmwareRelease = asRelease(firmware);
|
|
506
|
+
|
|
507
|
+
if (executor === 'v4' && isUpgrade(firmware)) {
|
|
508
|
+
const protocolV2 = buildProtocolV2Artifacts(firmwareRelease ?? {});
|
|
509
|
+
artifacts = protocolV2.artifacts;
|
|
510
|
+
targetsToUpdate = protocolV2.targets;
|
|
511
|
+
} else {
|
|
512
|
+
if (isUpgrade(bootloader)) {
|
|
513
|
+
artifacts.push({
|
|
514
|
+
artifactId: 'bootloader',
|
|
515
|
+
role: 'bootloader',
|
|
516
|
+
target: 'bootloader',
|
|
517
|
+
url: assertArtifactUrl(asRelease(bootloader)?.bootloaderResource, 'Bootloader release'),
|
|
518
|
+
container: 'raw',
|
|
519
|
+
...asIntegrity({
|
|
520
|
+
size: asRelease(bootloader)?.bootloaderExpectedSize,
|
|
521
|
+
sha256: asRelease(bootloader)?.bootloaderFingerprint,
|
|
522
|
+
}),
|
|
523
|
+
...(asVersion(asRelease(bootloader)?.bootloaderVersion)
|
|
524
|
+
? {
|
|
525
|
+
targetVersion: asVersion(asRelease(bootloader)?.bootloaderVersion),
|
|
526
|
+
}
|
|
527
|
+
: {}),
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
if (isUpgrade(firmware)) {
|
|
531
|
+
if (!firmwareRelease) {
|
|
532
|
+
throw ERRORS.TypedError(HardwareErrorCode.RuntimeError, 'Firmware release is invalid', {
|
|
533
|
+
firmwareUpdateCode: 'FirmwarePlanInvalid',
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
artifacts.push({
|
|
537
|
+
artifactId: 'firmware',
|
|
538
|
+
role: 'firmware',
|
|
539
|
+
target: 'firmware',
|
|
540
|
+
url: assertArtifactUrl(firmwareRelease.url, 'Firmware release'),
|
|
541
|
+
container: 'raw',
|
|
542
|
+
...asIntegrity({
|
|
543
|
+
size: firmwareRelease.expectedSize,
|
|
544
|
+
sha256: firmwareRelease.fingerprint,
|
|
545
|
+
}),
|
|
546
|
+
...(asVersion(firmwareRelease.version)
|
|
547
|
+
? { targetVersion: asVersion(firmwareRelease.version) }
|
|
548
|
+
: {}),
|
|
549
|
+
});
|
|
550
|
+
const resourceUrl = selectResourceUrl({
|
|
551
|
+
release: firmwareRelease,
|
|
552
|
+
features,
|
|
553
|
+
platform,
|
|
554
|
+
});
|
|
555
|
+
if (resourceUrl) {
|
|
556
|
+
artifacts.push({
|
|
557
|
+
artifactId: 'resource',
|
|
558
|
+
role: 'resource',
|
|
559
|
+
target: 'resource',
|
|
560
|
+
url: assertArtifactUrl(resourceUrl, 'Firmware resource release'),
|
|
561
|
+
container: 'zip',
|
|
562
|
+
...asIntegrity({
|
|
563
|
+
size:
|
|
564
|
+
resourceUrl === asString(firmwareRelease.fullResource)
|
|
565
|
+
? firmwareRelease.fullResourceExpectedSize
|
|
566
|
+
: firmwareRelease.resourceExpectedSize,
|
|
567
|
+
sha256:
|
|
568
|
+
resourceUrl === asString(firmwareRelease.fullResource)
|
|
569
|
+
? firmwareRelease.fullResourceFingerprint
|
|
570
|
+
: firmwareRelease.resourceFingerprint,
|
|
571
|
+
}),
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
if (isUpgrade(ble)) {
|
|
576
|
+
const bleRelease = asRelease(ble);
|
|
577
|
+
artifacts.push({
|
|
578
|
+
artifactId: 'ble',
|
|
579
|
+
role: 'ble',
|
|
580
|
+
target: 'ble',
|
|
581
|
+
url: assertArtifactUrl(bleRelease?.webUpdate ?? bleRelease?.url, 'BLE release'),
|
|
582
|
+
container: 'raw',
|
|
583
|
+
...asIntegrity({
|
|
584
|
+
size: bleRelease?.expectedSize,
|
|
585
|
+
sha256: bleRelease?.fingerprintWeb ?? bleRelease?.fingerprint,
|
|
586
|
+
}),
|
|
587
|
+
...(asVersion(bleRelease?.version)
|
|
588
|
+
? { targetVersion: asVersion(bleRelease?.version) }
|
|
589
|
+
: {}),
|
|
590
|
+
});
|
|
591
|
+
}
|
|
592
|
+
targetsToUpdate = [...new Set(artifacts.map(artifact => artifact.target))];
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
const deviceIdentity = getDeviceUUID(features);
|
|
596
|
+
if (
|
|
597
|
+
artifacts.length > 0 &&
|
|
598
|
+
(platform === 'native' || platform === 'desktop') &&
|
|
599
|
+
!deviceIdentity
|
|
600
|
+
) {
|
|
601
|
+
throw ERRORS.TypedError(
|
|
602
|
+
HardwareErrorCode.RuntimeError,
|
|
603
|
+
'Prepared firmware update requires a stable device identity',
|
|
604
|
+
{ firmwareUpdateCode: 'FirmwarePlanInvalid' }
|
|
605
|
+
);
|
|
606
|
+
}
|
|
607
|
+
const planWithoutDigest: Omit<FirmwareUpdatePlan, 'planDigest'> = {
|
|
608
|
+
schemaVersion: 2,
|
|
609
|
+
executor,
|
|
610
|
+
deviceIdentity: deviceIdentity || 'unavailable',
|
|
611
|
+
deviceModel: String(getDeviceType(features)),
|
|
612
|
+
firmwareType,
|
|
613
|
+
platform,
|
|
614
|
+
artifacts,
|
|
615
|
+
targetsToUpdate,
|
|
616
|
+
};
|
|
617
|
+
return {
|
|
618
|
+
...planWithoutDigest,
|
|
619
|
+
planDigest: digestFirmwareUpdatePlan(planWithoutDigest),
|
|
620
|
+
};
|
|
621
|
+
};
|