@onekeyfe/hd-core 1.2.0-alpha.96 → 1.2.0-alpha.98
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/__tests__/firmware-update/device-update-bootloader.test.ts +8 -1
- package/__tests__/firmware-update/firmware-prepared-host-digest.test.ts +106 -2
- package/__tests__/firmware-update/firmware-prepared-resource-archive.test.ts +150 -0
- package/__tests__/firmware-update/firmware-update-bootloader-poll.test.ts +112 -0
- package/__tests__/firmware-update/firmware-update-plan.test.ts +101 -0
- package/__tests__/firmware-update/firmware-update-prepared-resource-executors.test.ts +326 -0
- package/__tests__/firmware-update/firmware-update-v2-download-before-boot.test.ts +2 -2
- package/__tests__/protocol-v2.test.ts +131 -0
- package/dist/api/FirmwareUpdateV2.d.ts.map +1 -1
- package/dist/api/FirmwareUpdateV3.d.ts.map +1 -1
- package/dist/api/FirmwareUpdateV4.d.ts +2 -0
- package/dist/api/FirmwareUpdateV4.d.ts.map +1 -1
- package/dist/api/device/DeviceUpdateBootloader.d.ts.map +1 -1
- package/dist/api/firmware/FirmwarePreparedResourceArchive.d.ts +11 -0
- package/dist/api/firmware/FirmwarePreparedResourceArchive.d.ts.map +1 -0
- package/dist/api/firmware/FirmwareUpdateBaseMethod.d.ts.map +1 -1
- package/dist/api/firmware/FirmwareUpdatePlan.d.ts.map +1 -1
- package/dist/api/firmware/FirmwareUpdatePreparedPlan.d.ts +6 -1
- package/dist/api/firmware/FirmwareUpdatePreparedPlan.d.ts.map +1 -1
- package/dist/index.d.ts +5 -3
- package/dist/index.js +359 -157
- package/dist/types/api/deviceUpdateBootloader.d.ts.map +1 -1
- package/dist/types/api/firmwareUpdate.d.ts +1 -1
- package/dist/types/api/firmwareUpdate.d.ts.map +1 -1
- package/dist/types/api/firmwareUpdatePlan.d.ts +2 -2
- package/dist/types/api/firmwareUpdatePlan.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/api/FirmwareUpdateV2.ts +95 -72
- package/src/api/FirmwareUpdateV3.ts +74 -51
- package/src/api/FirmwareUpdateV4.ts +72 -7
- package/src/api/device/DeviceUpdateBootloader.ts +17 -12
- package/src/api/firmware/FirmwarePreparedResourceArchive.ts +207 -0
- package/src/api/firmware/FirmwareUpdateBaseMethod.ts +2 -0
- package/src/api/firmware/FirmwareUpdatePlan.ts +70 -48
- package/src/api/firmware/FirmwareUpdatePreparedPlan.ts +17 -0
- package/src/types/api/deviceUpdateBootloader.ts +1 -0
- package/src/types/api/firmwareUpdate.ts +2 -1
- package/src/types/api/firmwareUpdatePlan.ts +2 -2
|
@@ -9,6 +9,9 @@ import {
|
|
|
9
9
|
jest.mock('../../src/api/firmware/FirmwareUpdatePreparedPlan', () => ({
|
|
10
10
|
assertFirmwareUpdatePreparedPlanBinding: jest.fn(),
|
|
11
11
|
assertFirmwareUpdatePreparedPlanDeviceIdentity: jest.fn(),
|
|
12
|
+
getFirmwareUpdatePreparedRawArtifact: jest.fn(
|
|
13
|
+
({ preparedPlan }: { preparedPlan: { artifacts: unknown[] } }) => preparedPlan.artifacts[0]
|
|
14
|
+
),
|
|
12
15
|
validateFirmwareUpdatePreparedPlan: jest.fn((preparedPlan: unknown) => preparedPlan),
|
|
13
16
|
}));
|
|
14
17
|
|
|
@@ -47,7 +50,11 @@ const createMethod = () => {
|
|
|
47
50
|
method: 'deviceUpdateBootloader',
|
|
48
51
|
artifact,
|
|
49
52
|
hostBindingGeneration,
|
|
50
|
-
preparedPlan: {
|
|
53
|
+
preparedPlan: {
|
|
54
|
+
preparedPlanDigest: 'a'.repeat(64),
|
|
55
|
+
firmwareType: EFirmwareType.Universal,
|
|
56
|
+
artifacts: [{ artifact }],
|
|
57
|
+
},
|
|
51
58
|
},
|
|
52
59
|
});
|
|
53
60
|
(method as any).device = {
|
|
@@ -207,7 +207,40 @@ describe('prepared firmware host digest binding', () => {
|
|
|
207
207
|
},
|
|
208
208
|
});
|
|
209
209
|
|
|
210
|
-
expect(() => method.init()).toThrow(
|
|
210
|
+
expect(() => method.init()).toThrow(
|
|
211
|
+
'Prepared firmware plans cannot be combined with legacy firmware inputs'
|
|
212
|
+
);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
test('FirmwareUpdateV2 derives its component artifact from the digest-bound plan', () => {
|
|
216
|
+
const { artifact, preparedPlan } = createPreparedPlan({
|
|
217
|
+
executor: 'v2',
|
|
218
|
+
target: 'firmware',
|
|
219
|
+
});
|
|
220
|
+
const artifactReader = {
|
|
221
|
+
open: jest.fn(),
|
|
222
|
+
read: jest.fn(),
|
|
223
|
+
close: jest.fn(),
|
|
224
|
+
};
|
|
225
|
+
const hostBindingGeneration = registerFirmwareUpdateHostBinding({
|
|
226
|
+
preparedPlanDigest: preparedPlan.preparedPlanDigest,
|
|
227
|
+
artifactReader,
|
|
228
|
+
});
|
|
229
|
+
const method = new FirmwareUpdateV2({
|
|
230
|
+
id: 1,
|
|
231
|
+
payload: {
|
|
232
|
+
method: 'firmwareUpdateV2',
|
|
233
|
+
platform: 'desktop',
|
|
234
|
+
updateType: 'firmware',
|
|
235
|
+
preparedPlan,
|
|
236
|
+
hostBindingGeneration,
|
|
237
|
+
},
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
expect(() => method.init()).not.toThrow();
|
|
241
|
+
expect((method as unknown as { params: { artifact: unknown } }).params.artifact).toEqual(
|
|
242
|
+
artifact
|
|
243
|
+
);
|
|
211
244
|
});
|
|
212
245
|
|
|
213
246
|
test('FirmwareUpdateV3 rejects a host registered for another prepared plan', () => {
|
|
@@ -277,6 +310,37 @@ describe('prepared firmware host digest binding', () => {
|
|
|
277
310
|
);
|
|
278
311
|
});
|
|
279
312
|
|
|
313
|
+
test('FirmwareUpdateV3 derives its component artifacts from the digest-bound plan', () => {
|
|
314
|
+
const { artifact, preparedPlan } = createPreparedPlan({
|
|
315
|
+
executor: 'v3',
|
|
316
|
+
target: 'firmware',
|
|
317
|
+
});
|
|
318
|
+
const artifactReader = {
|
|
319
|
+
open: jest.fn(),
|
|
320
|
+
read: jest.fn(),
|
|
321
|
+
close: jest.fn(),
|
|
322
|
+
};
|
|
323
|
+
const hostBindingGeneration = registerFirmwareUpdateHostBinding({
|
|
324
|
+
preparedPlanDigest: preparedPlan.preparedPlanDigest,
|
|
325
|
+
artifactReader,
|
|
326
|
+
});
|
|
327
|
+
const method = new FirmwareUpdateV3({
|
|
328
|
+
id: 1,
|
|
329
|
+
payload: {
|
|
330
|
+
method: 'firmwareUpdateV3',
|
|
331
|
+
platform: 'desktop',
|
|
332
|
+
preparedPlan,
|
|
333
|
+
hostBindingGeneration,
|
|
334
|
+
},
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
expect(() => method.init()).not.toThrow();
|
|
338
|
+
expect(
|
|
339
|
+
(method as unknown as { params: { artifacts: { firmware: unknown } } }).params.artifacts
|
|
340
|
+
.firmware
|
|
341
|
+
).toEqual(artifact);
|
|
342
|
+
});
|
|
343
|
+
|
|
280
344
|
test('DeviceUpdateBootloader rejects a host registered for another prepared plan', async () => {
|
|
281
345
|
const { artifact, preparedPlan } = createPreparedPlan({
|
|
282
346
|
executor: 'v2',
|
|
@@ -349,7 +413,47 @@ describe('prepared firmware host digest binding', () => {
|
|
|
349
413
|
method.device = { features: undefined } as any;
|
|
350
414
|
|
|
351
415
|
await expect(method.run()).rejects.toThrow(
|
|
352
|
-
'Prepared bootloader plans
|
|
416
|
+
'Prepared bootloader plans cannot be combined with a legacy binary'
|
|
417
|
+
);
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
test('DeviceUpdateBootloader derives its artifact from the digest-bound plan', async () => {
|
|
421
|
+
const { artifact, preparedPlan } = createPreparedPlan({
|
|
422
|
+
executor: 'v2',
|
|
423
|
+
target: 'bootloader',
|
|
424
|
+
});
|
|
425
|
+
const artifactReader = {
|
|
426
|
+
open: jest.fn().mockResolvedValue({ readerId: 'bootloader-reader', size: artifact.size }),
|
|
427
|
+
read: jest.fn(),
|
|
428
|
+
close: jest.fn().mockResolvedValue(undefined),
|
|
429
|
+
};
|
|
430
|
+
const hostBindingGeneration = registerFirmwareUpdateHostBinding({
|
|
431
|
+
preparedPlanDigest: preparedPlan.preparedPlanDigest,
|
|
432
|
+
artifactReader,
|
|
433
|
+
});
|
|
434
|
+
const method = new DeviceUpdateBootloader({
|
|
435
|
+
id: 1,
|
|
436
|
+
payload: {
|
|
437
|
+
method: 'deviceUpdateBootloader',
|
|
438
|
+
preparedPlan,
|
|
439
|
+
hostBindingGeneration,
|
|
440
|
+
},
|
|
441
|
+
});
|
|
442
|
+
method.device = {
|
|
443
|
+
features: {
|
|
444
|
+
deviceType: EDeviceType.Classic1s,
|
|
445
|
+
serialNo: 'classic-device-id',
|
|
446
|
+
},
|
|
447
|
+
getCurrentDeviceType: () => EDeviceType.Touch,
|
|
448
|
+
} as any;
|
|
449
|
+
const updateTouchBootloader = jest
|
|
450
|
+
.spyOn(method, 'updateTouchBootloader')
|
|
451
|
+
.mockResolvedValue(true);
|
|
452
|
+
|
|
453
|
+
await expect(method.run()).resolves.toBe(true);
|
|
454
|
+
expect(artifactReader.open).toHaveBeenCalledWith({ artifactRef: artifact.artifactRef });
|
|
455
|
+
expect(updateTouchBootloader).toHaveBeenCalledWith(
|
|
456
|
+
expect.objectContaining({ firmwareType: preparedPlan.firmwareType })
|
|
353
457
|
);
|
|
354
458
|
});
|
|
355
459
|
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { sha256 } from '@noble/hashes/sha256';
|
|
2
|
+
import { bytesToHex } from '@noble/hashes/utils';
|
|
3
|
+
import { EFirmwareType } from '@onekeyfe/hd-shared';
|
|
4
|
+
import JSZip from 'jszip';
|
|
5
|
+
|
|
6
|
+
import { readVerifiedPreparedResourceArchive } from '../../src/api/firmware/FirmwarePreparedResourceArchive';
|
|
7
|
+
import { prepareFirmwareUpdatePlan } from '../../src/api/firmware/FirmwareUpdatePreparedPlan';
|
|
8
|
+
import { digestFirmwareUpdateContract } from '../../src/api/firmware/FirmwareUpdatePlan';
|
|
9
|
+
|
|
10
|
+
import type {
|
|
11
|
+
FirmwareArtifactReader,
|
|
12
|
+
FirmwareArtifactReference,
|
|
13
|
+
} from '../../src/types/api/firmwareUpdate';
|
|
14
|
+
import type { FirmwareUpdatePlan } from '../../src/types/api/firmwareUpdatePlan';
|
|
15
|
+
|
|
16
|
+
jest.mock('../../src/data/config', () => ({
|
|
17
|
+
getSDKVersion: jest.fn(() => '1.0.0'),
|
|
18
|
+
DEFAULT_DOMAIN: 'https://jssdk.onekey.so/1.0.0/',
|
|
19
|
+
}));
|
|
20
|
+
|
|
21
|
+
const createReference = (artifactRef: string, binary: ArrayBuffer): FirmwareArtifactReference => ({
|
|
22
|
+
artifactRef,
|
|
23
|
+
size: binary.byteLength,
|
|
24
|
+
sha256: bytesToHex(sha256(new Uint8Array(binary))),
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const createReader = (artifacts: Map<string, ArrayBuffer>): FirmwareArtifactReader => {
|
|
28
|
+
const opened = new Map<string, ArrayBuffer>();
|
|
29
|
+
let sequence = 0;
|
|
30
|
+
return {
|
|
31
|
+
open({ artifactRef }) {
|
|
32
|
+
const binary = artifacts.get(artifactRef);
|
|
33
|
+
if (!binary) throw new Error(`missing artifact: ${artifactRef}`);
|
|
34
|
+
sequence += 1;
|
|
35
|
+
const readerId = `reader-${sequence}`;
|
|
36
|
+
opened.set(readerId, binary);
|
|
37
|
+
return Promise.resolve({ readerId, size: binary.byteLength });
|
|
38
|
+
},
|
|
39
|
+
read({ readerId, offset, length }) {
|
|
40
|
+
const binary = opened.get(readerId);
|
|
41
|
+
if (!binary) throw new Error(`missing reader: ${readerId}`);
|
|
42
|
+
const data = binary.slice(offset, offset + length);
|
|
43
|
+
return Promise.resolve({
|
|
44
|
+
data,
|
|
45
|
+
bytesRead: data.byteLength,
|
|
46
|
+
eof: offset + length === binary.byteLength,
|
|
47
|
+
});
|
|
48
|
+
},
|
|
49
|
+
close({ readerId }) {
|
|
50
|
+
opened.delete(readerId);
|
|
51
|
+
return Promise.resolve();
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const createPreparedResourcePlan = ({
|
|
57
|
+
executor,
|
|
58
|
+
archiveBinary,
|
|
59
|
+
entryBinary,
|
|
60
|
+
}: {
|
|
61
|
+
executor: 'v2' | 'v3';
|
|
62
|
+
archiveBinary: ArrayBuffer;
|
|
63
|
+
entryBinary: ArrayBuffer;
|
|
64
|
+
}) => {
|
|
65
|
+
const archiveArtifact = createReference('resource-archive', archiveBinary);
|
|
66
|
+
const entryArtifact = createReference('resource-entry', entryBinary);
|
|
67
|
+
const planWithoutDigest: Omit<FirmwareUpdatePlan, 'planDigest'> = {
|
|
68
|
+
schemaVersion: 2,
|
|
69
|
+
executor,
|
|
70
|
+
deviceIdentity: `${executor}-device`,
|
|
71
|
+
deviceModel: executor === 'v2' ? 'TOUCH' : 'PRO',
|
|
72
|
+
firmwareType: EFirmwareType.Universal,
|
|
73
|
+
platform: 'desktop',
|
|
74
|
+
artifacts: [
|
|
75
|
+
{
|
|
76
|
+
artifactId: 'resource',
|
|
77
|
+
role: 'resource',
|
|
78
|
+
target: 'resource',
|
|
79
|
+
url: 'https://firmware.onekey.so/resource.zip',
|
|
80
|
+
container: 'zip',
|
|
81
|
+
expectedSize: archiveArtifact.size,
|
|
82
|
+
expectedSha256: archiveArtifact.sha256,
|
|
83
|
+
},
|
|
84
|
+
],
|
|
85
|
+
targetsToUpdate: ['resource'],
|
|
86
|
+
};
|
|
87
|
+
const plan: FirmwareUpdatePlan = {
|
|
88
|
+
...planWithoutDigest,
|
|
89
|
+
planDigest: digestFirmwareUpdateContract(planWithoutDigest),
|
|
90
|
+
};
|
|
91
|
+
return {
|
|
92
|
+
archiveArtifact,
|
|
93
|
+
preparedPlan: prepareFirmwareUpdatePlan({
|
|
94
|
+
plan,
|
|
95
|
+
leaseRef: `${executor}-resource-lease`,
|
|
96
|
+
artifacts: [
|
|
97
|
+
{
|
|
98
|
+
artifactId: 'resource',
|
|
99
|
+
artifact: archiveArtifact,
|
|
100
|
+
materializedEntries: [{ entryName: 'images/logo.bin', artifact: entryArtifact }],
|
|
101
|
+
},
|
|
102
|
+
],
|
|
103
|
+
}),
|
|
104
|
+
};
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
describe.each(['v2', 'v3'] as const)('%s prepared resource archive binding', executor => {
|
|
108
|
+
test('uses canonical bytes extracted from the approved ZIP', async () => {
|
|
109
|
+
const entryBinary = Uint8Array.from([1, 2, 3, 4]).buffer;
|
|
110
|
+
const zip = new JSZip();
|
|
111
|
+
zip.file('images/logo.bin', entryBinary);
|
|
112
|
+
const archiveBinary = await zip.generateAsync({ type: 'arraybuffer' });
|
|
113
|
+
const { archiveArtifact, preparedPlan } = createPreparedResourcePlan({
|
|
114
|
+
executor,
|
|
115
|
+
archiveBinary,
|
|
116
|
+
entryBinary,
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
const result = await readVerifiedPreparedResourceArchive({
|
|
120
|
+
preparedPlan,
|
|
121
|
+
reader: createReader(new Map([[archiveArtifact.artifactRef, archiveBinary]])),
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
expect(result).toHaveLength(1);
|
|
125
|
+
expect(result[0].entryName).toBe('logo.bin');
|
|
126
|
+
expect(new Uint8Array(result[0].binary)).toEqual(new Uint8Array(entryBinary));
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test('rejects entries that are not derived from the approved ZIP', async () => {
|
|
130
|
+
const approvedEntry = Uint8Array.from([1, 2, 3, 4]).buffer;
|
|
131
|
+
const substitutedEntry = Uint8Array.from([9, 9, 9, 9]).buffer;
|
|
132
|
+
const zip = new JSZip();
|
|
133
|
+
zip.file('images/logo.bin', approvedEntry);
|
|
134
|
+
const archiveBinary = await zip.generateAsync({ type: 'arraybuffer' });
|
|
135
|
+
const { archiveArtifact, preparedPlan } = createPreparedResourcePlan({
|
|
136
|
+
executor,
|
|
137
|
+
archiveBinary,
|
|
138
|
+
entryBinary: substitutedEntry,
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
await expect(
|
|
142
|
+
readVerifiedPreparedResourceArchive({
|
|
143
|
+
preparedPlan,
|
|
144
|
+
reader: createReader(new Map([[archiveArtifact.artifactRef, archiveBinary]])),
|
|
145
|
+
})
|
|
146
|
+
).rejects.toMatchObject({
|
|
147
|
+
params: { firmwareUpdateCode: 'FirmwareArtifactReceiptMismatch' },
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
});
|
|
@@ -1,8 +1,11 @@
|
|
|
1
|
+
import { EDeviceType } from '@onekeyfe/hd-shared';
|
|
2
|
+
|
|
1
3
|
import FirmwareUpdate from '../../src/api/FirmwareUpdate';
|
|
2
4
|
import FirmwareUpdateV2 from '../../src/api/FirmwareUpdateV2';
|
|
3
5
|
import FirmwareUpdateV3 from '../../src/api/FirmwareUpdateV3';
|
|
4
6
|
import { BOOTLOADER_POLL_INITIALIZE_TIMEOUT_MS } from '../../src/api/firmware/FirmwareUpdateBaseMethod';
|
|
5
7
|
import { DataManager } from '../../src/data-manager';
|
|
8
|
+
import { DevicePool } from '../../src/device/DevicePool';
|
|
6
9
|
|
|
7
10
|
import type { Device } from '../../src/device/Device';
|
|
8
11
|
|
|
@@ -12,6 +15,7 @@ jest.mock('../../src/data/config', () => ({
|
|
|
12
15
|
}));
|
|
13
16
|
|
|
14
17
|
const BLE_ID = 'ble-device';
|
|
18
|
+
const WEBUSB_ID = 'webusb-device';
|
|
15
19
|
|
|
16
20
|
const flush = () =>
|
|
17
21
|
new Promise(resolve => {
|
|
@@ -198,3 +202,111 @@ describe.each([
|
|
|
198
202
|
expect(maxActiveProbes).toBe(1);
|
|
199
203
|
});
|
|
200
204
|
});
|
|
205
|
+
|
|
206
|
+
describe.each([
|
|
207
|
+
[
|
|
208
|
+
'FirmwareUpdateV2',
|
|
209
|
+
() =>
|
|
210
|
+
new FirmwareUpdateV2({
|
|
211
|
+
id: 1,
|
|
212
|
+
payload: { method: 'firmwareUpdateV2', connectId: WEBUSB_ID },
|
|
213
|
+
}),
|
|
214
|
+
],
|
|
215
|
+
[
|
|
216
|
+
'FirmwareUpdateV3 (base method)',
|
|
217
|
+
() =>
|
|
218
|
+
new FirmwareUpdateV3({
|
|
219
|
+
id: 1,
|
|
220
|
+
payload: { method: 'firmwareUpdateV3', connectId: WEBUSB_ID },
|
|
221
|
+
}),
|
|
222
|
+
],
|
|
223
|
+
])('%s WebUSB bootloader authorization', (_name, buildMethod) => {
|
|
224
|
+
beforeEach(() => {
|
|
225
|
+
jest.useFakeTimers({ doNotFake: ['setImmediate', 'performance'] });
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
afterEach(() => {
|
|
229
|
+
jest.clearAllTimers();
|
|
230
|
+
jest.useRealTimers();
|
|
231
|
+
jest.restoreAllMocks();
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
test('pauses the reboot deadline while waiting for browser authorization', async () => {
|
|
235
|
+
jest.spyOn(DataManager, 'getSettings').mockReturnValue('webusb' as never);
|
|
236
|
+
jest.spyOn(DataManager, 'isBleConnect').mockReturnValue(false);
|
|
237
|
+
jest.spyOn(DataManager, 'isBrowserWebUsb').mockReturnValue(true);
|
|
238
|
+
|
|
239
|
+
const method = buildMethod();
|
|
240
|
+
let permissionGranted = false;
|
|
241
|
+
const bootloaderDevice = {
|
|
242
|
+
isBootloader: jest.fn(() => true),
|
|
243
|
+
} as unknown as Device;
|
|
244
|
+
jest.spyOn(DevicePool, 'getDevices').mockImplementation(() =>
|
|
245
|
+
Promise.resolve({
|
|
246
|
+
devices: {},
|
|
247
|
+
deviceList: permissionGranted ? [bootloaderDevice] : [],
|
|
248
|
+
})
|
|
249
|
+
);
|
|
250
|
+
|
|
251
|
+
const commands = { disposed: true };
|
|
252
|
+
const updateFromCache = jest.fn();
|
|
253
|
+
method.device = {
|
|
254
|
+
originalDescriptor: {
|
|
255
|
+
id: WEBUSB_ID,
|
|
256
|
+
path: WEBUSB_ID,
|
|
257
|
+
protocolType: 'V1',
|
|
258
|
+
},
|
|
259
|
+
deviceConnector: {
|
|
260
|
+
enumerate: jest.fn(() =>
|
|
261
|
+
Promise.resolve({
|
|
262
|
+
descriptors: permissionGranted ? [{ path: 'bootloader-device' }] : [],
|
|
263
|
+
})
|
|
264
|
+
),
|
|
265
|
+
},
|
|
266
|
+
commands,
|
|
267
|
+
updateFromCache,
|
|
268
|
+
getCurrentDeviceType: jest.fn(() => EDeviceType.Pro),
|
|
269
|
+
} as unknown as Device;
|
|
270
|
+
method.postTipMessage = jest.fn();
|
|
271
|
+
|
|
272
|
+
let resolvePrompt: (deviceId: string) => void = () => undefined;
|
|
273
|
+
const promptPromise = new Promise<string>(resolve => {
|
|
274
|
+
resolvePrompt = resolve;
|
|
275
|
+
});
|
|
276
|
+
const prompt = jest
|
|
277
|
+
.spyOn(method as any, '_promptDeviceInBootloaderForWebDevice')
|
|
278
|
+
.mockReturnValue(promptPromise);
|
|
279
|
+
|
|
280
|
+
method.checkDeviceToBootloader(WEBUSB_ID);
|
|
281
|
+
const resolved = jest.fn();
|
|
282
|
+
const rejected = jest.fn();
|
|
283
|
+
method.checkPromise?.promise.then(resolved, rejected);
|
|
284
|
+
|
|
285
|
+
for (let elapsed = 0; elapsed < 15000; elapsed += 500) {
|
|
286
|
+
jest.advanceTimersByTime(500);
|
|
287
|
+
// eslint-disable-next-line no-await-in-loop
|
|
288
|
+
await flush();
|
|
289
|
+
if (prompt.mock.calls.length) break;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
expect(prompt).toHaveBeenCalledTimes(1);
|
|
293
|
+
|
|
294
|
+
// Browser permission depends on a user gesture and must not consume the
|
|
295
|
+
// automatic reboot detection deadline.
|
|
296
|
+
for (let elapsed = 0; elapsed < 35000; elapsed += 500) {
|
|
297
|
+
jest.advanceTimersByTime(500);
|
|
298
|
+
// eslint-disable-next-line no-await-in-loop
|
|
299
|
+
await flush();
|
|
300
|
+
}
|
|
301
|
+
expect(rejected).not.toHaveBeenCalled();
|
|
302
|
+
|
|
303
|
+
permissionGranted = true;
|
|
304
|
+
resolvePrompt('bootloader-device');
|
|
305
|
+
await flush();
|
|
306
|
+
await flush();
|
|
307
|
+
|
|
308
|
+
expect(resolved).toHaveBeenCalledWith(true);
|
|
309
|
+
expect(updateFromCache).toHaveBeenCalledWith(bootloaderDevice);
|
|
310
|
+
expect(commands.disposed).toBe(false);
|
|
311
|
+
});
|
|
312
|
+
});
|
|
@@ -54,6 +54,8 @@ const createLegacyForceInput = (
|
|
|
54
54
|
release: Record<string, unknown> = {
|
|
55
55
|
url: 'https://firmware.onekey.so/pro/firmware.bin',
|
|
56
56
|
version: [4, 21, 0],
|
|
57
|
+
expectedSize: 1024,
|
|
58
|
+
fingerprint: '1'.repeat(64),
|
|
57
59
|
}
|
|
58
60
|
): BuildPlanInput => ({
|
|
59
61
|
features: createFeatures({
|
|
@@ -172,6 +174,8 @@ describe('buildFirmwareUpdatePlan', () => {
|
|
|
172
174
|
release: {
|
|
173
175
|
url: 'https://firmware.onekey.so/pro/firmware.bin',
|
|
174
176
|
version: [4, 0, 0],
|
|
177
|
+
expectedSize: 1024,
|
|
178
|
+
fingerprint: '1'.repeat(64),
|
|
175
179
|
},
|
|
176
180
|
},
|
|
177
181
|
ble: noUpdate,
|
|
@@ -197,7 +201,11 @@ describe('buildFirmwareUpdatePlan', () => {
|
|
|
197
201
|
release: {
|
|
198
202
|
url: 'https://firmware.onekey.so/pro/firmware.bin',
|
|
199
203
|
version: [4, 21, 0],
|
|
204
|
+
expectedSize: 1024,
|
|
205
|
+
fingerprint: '1'.repeat(64),
|
|
200
206
|
resource: 'https://firmware.onekey.so/pro/resource.zip',
|
|
207
|
+
resourceExpectedSize: 4096,
|
|
208
|
+
resourceFingerprint: '1'.repeat(64),
|
|
201
209
|
},
|
|
202
210
|
},
|
|
203
211
|
ble: {
|
|
@@ -205,6 +213,8 @@ describe('buildFirmwareUpdatePlan', () => {
|
|
|
205
213
|
release: {
|
|
206
214
|
webUpdate: 'https://firmware.onekey.so/pro/ble.bin',
|
|
207
215
|
version: [2, 3, 7],
|
|
216
|
+
expectedSize: 512,
|
|
217
|
+
fingerprintWeb: '2'.repeat(64),
|
|
208
218
|
},
|
|
209
219
|
},
|
|
210
220
|
bootloader: {
|
|
@@ -212,6 +222,8 @@ describe('buildFirmwareUpdatePlan', () => {
|
|
|
212
222
|
release: {
|
|
213
223
|
bootloaderResource: 'https://firmware.onekey.so/pro/bootloader.bin',
|
|
214
224
|
bootloaderVersion: [2, 8, 4],
|
|
225
|
+
bootloaderExpectedSize: 768,
|
|
226
|
+
bootloaderFingerprint: '3'.repeat(64),
|
|
215
227
|
},
|
|
216
228
|
},
|
|
217
229
|
forceUpdateTargets: ['firmware', 'resource', 'ble', 'bootloader'],
|
|
@@ -241,6 +253,8 @@ describe('buildFirmwareUpdatePlan', () => {
|
|
|
241
253
|
release: {
|
|
242
254
|
url: 'https://firmware.onekey.so/pro/firmware.bin',
|
|
243
255
|
version: [4, 21, 0],
|
|
256
|
+
expectedSize: 1024,
|
|
257
|
+
fingerprint: '1'.repeat(64),
|
|
244
258
|
resource: 'https://firmware.onekey.so/pro/resource.zip',
|
|
245
259
|
},
|
|
246
260
|
},
|
|
@@ -253,6 +267,32 @@ describe('buildFirmwareUpdatePlan', () => {
|
|
|
253
267
|
expect(plan.targetsToUpdate).toEqual(['firmware']);
|
|
254
268
|
});
|
|
255
269
|
|
|
270
|
+
test('does not approve an unexecutable V2 resource during bootloader recovery', () => {
|
|
271
|
+
const features = createFeatures({ deviceType: EDeviceType.Classic1s });
|
|
272
|
+
features.bootloaderMode = true;
|
|
273
|
+
const plan = buildFirmwareUpdatePlan({
|
|
274
|
+
features,
|
|
275
|
+
firmwareType: EFirmwareType.Universal,
|
|
276
|
+
platform: 'desktop',
|
|
277
|
+
firmware: {
|
|
278
|
+
status: 'none',
|
|
279
|
+
release: {
|
|
280
|
+
url: 'https://firmware.onekey.so/classic/firmware.bin',
|
|
281
|
+
expectedSize: 1024,
|
|
282
|
+
fingerprint: '1'.repeat(64),
|
|
283
|
+
resource: 'https://firmware.onekey.so/classic/resource.zip',
|
|
284
|
+
resourceExpectedSize: 4096,
|
|
285
|
+
resourceFingerprint: '2'.repeat(64),
|
|
286
|
+
},
|
|
287
|
+
},
|
|
288
|
+
ble: noUpdate,
|
|
289
|
+
bootloader: noUpdate,
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
expect(plan.artifacts.map(artifact => artifact.artifactId)).toEqual(['firmware']);
|
|
293
|
+
expect(plan.targetsToUpdate).toEqual(['firmware']);
|
|
294
|
+
});
|
|
295
|
+
|
|
256
296
|
test.each([
|
|
257
297
|
{
|
|
258
298
|
label: 'a non-array value',
|
|
@@ -276,6 +316,65 @@ describe('buildFirmwareUpdatePlan', () => {
|
|
|
276
316
|
expectFirmwarePlanInvalid(() => buildFirmwareUpdatePlan(createLegacyForceInput(['resource'])));
|
|
277
317
|
});
|
|
278
318
|
|
|
319
|
+
test('rejects a legacy resource archive without complete integrity metadata', () => {
|
|
320
|
+
expectFirmwarePlanInvalid(
|
|
321
|
+
() =>
|
|
322
|
+
buildFirmwareUpdatePlan(
|
|
323
|
+
createLegacyForceInput(['resource'], {
|
|
324
|
+
url: 'https://firmware.onekey.so/pro/firmware.bin',
|
|
325
|
+
resource: 'https://firmware.onekey.so/pro/resource.zip',
|
|
326
|
+
resourceExpectedSize: 4096,
|
|
327
|
+
})
|
|
328
|
+
),
|
|
329
|
+
'integrity metadata is invalid'
|
|
330
|
+
);
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
test.each(['firmware', 'ble', 'bootloader'] as const)(
|
|
334
|
+
'rejects a remote legacy %s artifact without complete integrity metadata',
|
|
335
|
+
target => {
|
|
336
|
+
expectFirmwarePlanInvalid(
|
|
337
|
+
() =>
|
|
338
|
+
buildFirmwareUpdatePlan({
|
|
339
|
+
features: createFeatures({ deviceType: EDeviceType.Classic1s }),
|
|
340
|
+
firmwareType: EFirmwareType.Universal,
|
|
341
|
+
platform: 'desktop',
|
|
342
|
+
firmware:
|
|
343
|
+
target === 'firmware'
|
|
344
|
+
? {
|
|
345
|
+
status: 'outdated',
|
|
346
|
+
release: {
|
|
347
|
+
url: 'https://firmware.onekey.so/classic/firmware.bin',
|
|
348
|
+
expectedSize: 1024,
|
|
349
|
+
},
|
|
350
|
+
}
|
|
351
|
+
: noUpdate,
|
|
352
|
+
ble:
|
|
353
|
+
target === 'ble'
|
|
354
|
+
? {
|
|
355
|
+
status: 'outdated',
|
|
356
|
+
release: {
|
|
357
|
+
webUpdate: 'https://firmware.onekey.so/classic/ble.bin',
|
|
358
|
+
expectedSize: 512,
|
|
359
|
+
},
|
|
360
|
+
}
|
|
361
|
+
: noUpdate,
|
|
362
|
+
bootloader:
|
|
363
|
+
target === 'bootloader'
|
|
364
|
+
? {
|
|
365
|
+
status: 'outdated',
|
|
366
|
+
release: {
|
|
367
|
+
bootloaderResource: 'https://firmware.onekey.so/classic/bootloader.bin',
|
|
368
|
+
bootloaderExpectedSize: 768,
|
|
369
|
+
},
|
|
370
|
+
}
|
|
371
|
+
: noUpdate,
|
|
372
|
+
}),
|
|
373
|
+
'integrity metadata is invalid'
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
);
|
|
377
|
+
|
|
279
378
|
test.each(['ble', 'bootloader'] as const)(
|
|
280
379
|
'rejects the unsupported Pro2 %s force target',
|
|
281
380
|
forceTarget => {
|
|
@@ -358,6 +457,8 @@ describe('buildFirmwareUpdatePlan', () => {
|
|
|
358
457
|
status: 'outdated',
|
|
359
458
|
release: {
|
|
360
459
|
url: 'https://firmware.onekey.so/classic/firmware.bin',
|
|
460
|
+
expectedSize: 1024,
|
|
461
|
+
fingerprint: '1'.repeat(64),
|
|
361
462
|
},
|
|
362
463
|
},
|
|
363
464
|
ble: noUpdate,
|