@onekeyfe/hd-core 1.2.0-alpha.136 → 1.2.0-alpha.140
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 +1 -1
- package/__tests__/check-all-firmware-release-protocol-v2.test.ts +48 -0
- package/__tests__/device-lifecycle-events.test.ts +21 -17
- package/__tests__/device-settings.test.ts +44 -8
- package/__tests__/device-state-mapper.test.ts +38 -0
- package/__tests__/device-wallet-session-store.test.ts +21 -79
- package/__tests__/firmware-update/firmware-update-v4-install-poll.test.ts +37 -1
- package/__tests__/protocol-v2-unlock-policy.test.ts +97 -1
- package/__tests__/protocol-v2.test.ts +102 -21
- package/dist/api/FirmwareUpdateV4.d.ts +1 -1
- package/dist/api/FirmwareUpdateV4.d.ts.map +1 -1
- package/dist/api/device/DeviceChangePin.d.ts.map +1 -1
- package/dist/api/device/DeviceSettings.d.ts.map +1 -1
- package/dist/api/device/DeviceVerify.d.ts +1 -0
- package/dist/api/device/DeviceVerify.d.ts.map +1 -1
- package/dist/api/device/DeviceWipe.d.ts.map +1 -1
- package/dist/api/firmware/protocolV2Release.d.ts.map +1 -1
- package/dist/device/Device.d.ts.map +1 -1
- package/dist/device/DeviceStateMapper.d.ts.map +1 -1
- package/dist/index.js +93 -82
- package/package.json +4 -4
- package/src/api/FirmwareUpdateV4.ts +52 -78
- package/src/api/device/DeviceChangePin.ts +4 -1
- package/src/api/device/DeviceSettings.ts +13 -0
- package/src/api/device/DeviceVerify.ts +25 -0
- package/src/api/device/DeviceWipe.ts +4 -1
- package/src/api/firmware/protocolV2Release.ts +7 -2
- package/src/device/Device.ts +17 -42
- package/src/device/DeviceCommands.ts +1 -1
- package/src/device/DeviceStateMapper.ts +11 -7
|
@@ -485,7 +485,7 @@ describe('DeviceCommands failure mapping', () => {
|
|
|
485
485
|
});
|
|
486
486
|
});
|
|
487
487
|
|
|
488
|
-
it.each(['Cancelled on device', 'Confirm dismissed'])(
|
|
488
|
+
it.each(['Cancelled on device', 'Confirm dismissed', 'Update cancelled'])(
|
|
489
489
|
'maps legacy Protocol V2 cancellation message "%s" without a subcode',
|
|
490
490
|
async message => {
|
|
491
491
|
const commands = createCommands();
|
|
@@ -620,6 +620,54 @@ describe('checkAllFirmwareRelease Protocol V2 support', () => {
|
|
|
620
620
|
});
|
|
621
621
|
});
|
|
622
622
|
|
|
623
|
+
test('selects the Protocol V2 resource archive using the DeviceState identity', async () => {
|
|
624
|
+
const releaseWithResources: IFirmwareReleaseInfo = {
|
|
625
|
+
...release,
|
|
626
|
+
resources: { source: resourceSource },
|
|
627
|
+
};
|
|
628
|
+
const method = new CheckAllFirmwareRelease({
|
|
629
|
+
id: 1,
|
|
630
|
+
payload: {
|
|
631
|
+
method: 'checkAllFirmwareRelease',
|
|
632
|
+
platform: 'desktop',
|
|
633
|
+
protocolV2ForceUpdateTargets: ['resource'],
|
|
634
|
+
},
|
|
635
|
+
});
|
|
636
|
+
method.init();
|
|
637
|
+
method.device = {
|
|
638
|
+
isProtocolV2: () => true,
|
|
639
|
+
features: {
|
|
640
|
+
firmwareVersion: '1.0.0',
|
|
641
|
+
serialNo: 'device-without-model-prefix',
|
|
642
|
+
},
|
|
643
|
+
getDeviceState: jest.fn().mockResolvedValue({
|
|
644
|
+
identity: { deviceType: 'pro2', firmwareType: EFirmwareType.Universal },
|
|
645
|
+
status: { mode: 'normal' },
|
|
646
|
+
versions: currentVersions,
|
|
647
|
+
}),
|
|
648
|
+
} as unknown as CheckAllFirmwareRelease['device'];
|
|
649
|
+
const getLatestReleaseSpy = jest
|
|
650
|
+
.spyOn(DataManager, 'getFirmwareLatestRelease')
|
|
651
|
+
.mockImplementation(features =>
|
|
652
|
+
features.deviceType === 'pro2' ? releaseWithResources : undefined
|
|
653
|
+
);
|
|
654
|
+
|
|
655
|
+
await expect(method.run()).resolves.toMatchObject({
|
|
656
|
+
deviceType: 'pro2',
|
|
657
|
+
resourceArchive: resourceSource,
|
|
658
|
+
targetsToUpdate: expect.arrayContaining(['resource']),
|
|
659
|
+
firmwareUpdatePlan: {
|
|
660
|
+
executor: 'v4',
|
|
661
|
+
deviceModel: 'pro2',
|
|
662
|
+
targetsToUpdate: expect.arrayContaining(['resource']),
|
|
663
|
+
},
|
|
664
|
+
});
|
|
665
|
+
expect(getLatestReleaseSpy).toHaveBeenCalledWith(
|
|
666
|
+
expect.objectContaining({ deviceType: 'pro2' }),
|
|
667
|
+
EFirmwareType.Universal
|
|
668
|
+
);
|
|
669
|
+
});
|
|
670
|
+
|
|
623
671
|
test.each(['resource', 'boot_resources'] as const)(
|
|
624
672
|
'allows an explicitly forced Protocol V2 resource-only target through %s',
|
|
625
673
|
async forceTarget => {
|
|
@@ -343,25 +343,29 @@ describe('public device lifecycle events', () => {
|
|
|
343
343
|
}
|
|
344
344
|
);
|
|
345
345
|
|
|
346
|
-
test('
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
346
|
+
test.each(['react-native', 'webusb', 'desktop-webusb'] as const)(
|
|
347
|
+
'sends a fallback Cancel for an acquired Protocol V2 %s call without a prompt callback',
|
|
348
|
+
async env => {
|
|
349
|
+
jest.spyOn(DataManager, 'getSettings').mockReturnValue(env as never);
|
|
350
|
+
const device = createInitializedDevice('V2');
|
|
351
|
+
const post = jest.fn().mockResolvedValue(undefined);
|
|
352
|
+
const cancelDevice = jest.fn(() => cancelDeviceInPrompt(device, false));
|
|
353
|
+
const cancel = jest.fn().mockResolvedValue(undefined);
|
|
354
|
+
device.originalDescriptor.session = device.mainId;
|
|
355
|
+
(device as unknown as { deviceAcquired: boolean }).deviceAcquired = true;
|
|
356
|
+
device.commands = {
|
|
357
|
+
transport: { post },
|
|
358
|
+
cancelDevice,
|
|
359
|
+
cancel,
|
|
360
|
+
} as never;
|
|
358
361
|
|
|
359
|
-
|
|
362
|
+
await device.interruptionFromUser();
|
|
360
363
|
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
364
|
+
expect(cancelDevice).toHaveBeenCalledTimes(1);
|
|
365
|
+
expect(post).toHaveBeenCalledWith(device.mainId, 'Cancel', {});
|
|
366
|
+
expect(cancel).toHaveBeenCalledTimes(1);
|
|
367
|
+
}
|
|
368
|
+
);
|
|
365
369
|
|
|
366
370
|
test('waits for the canceled run to finish releasing before cancellation completes', async () => {
|
|
367
371
|
jest.spyOn(DataManager, 'getSettings').mockReturnValue('react-native' as never);
|
|
@@ -361,7 +361,7 @@ describe('DeviceSettings protocol routing', () => {
|
|
|
361
361
|
expect(getDeviceState).toHaveBeenCalledWith({ refreshSections: ['settings'] });
|
|
362
362
|
});
|
|
363
363
|
|
|
364
|
-
it('
|
|
364
|
+
it('persists a confirmed Pro2 passphrase enable when the refreshed state is unavailable', async () => {
|
|
365
365
|
const { device, getDeviceState, updateState } = createDevice({ protocol: 'V2' });
|
|
366
366
|
getDeviceState.mockResolvedValue({
|
|
367
367
|
status: { passphraseProtection: false },
|
|
@@ -377,10 +377,13 @@ describe('DeviceSettings protocol routing', () => {
|
|
|
377
377
|
(method as any).device = device;
|
|
378
378
|
|
|
379
379
|
await expect(method.run()).resolves.toEqual({ message: 'Success' });
|
|
380
|
-
expect(updateState).
|
|
380
|
+
expect(updateState).toHaveBeenCalledWith(
|
|
381
|
+
{ status: { passphraseProtection: true } },
|
|
382
|
+
'settings-write'
|
|
383
|
+
);
|
|
381
384
|
});
|
|
382
385
|
|
|
383
|
-
it('
|
|
386
|
+
it('persists a confirmed Pro2 passphrase disable when refreshed state is unavailable after locking', async () => {
|
|
384
387
|
const { device, getDeviceState, updateState } = createDevice({ protocol: 'V2' });
|
|
385
388
|
getDeviceState
|
|
386
389
|
.mockResolvedValueOnce({
|
|
@@ -400,10 +403,18 @@ describe('DeviceSettings protocol routing', () => {
|
|
|
400
403
|
(method as any).device = device;
|
|
401
404
|
|
|
402
405
|
await expect(method.run()).resolves.toEqual({ message: 'Success' });
|
|
403
|
-
expect(updateState).
|
|
406
|
+
expect(updateState).toHaveBeenCalledWith(
|
|
407
|
+
{
|
|
408
|
+
status: {
|
|
409
|
+
passphraseProtection: false,
|
|
410
|
+
unlockedAttachPin: false,
|
|
411
|
+
},
|
|
412
|
+
},
|
|
413
|
+
'settings-write'
|
|
414
|
+
);
|
|
404
415
|
});
|
|
405
416
|
|
|
406
|
-
it('
|
|
417
|
+
it('persists a confirmed Pro2 passphrase disable when the Attach PIN session locks', async () => {
|
|
407
418
|
let statusReadCount = 0;
|
|
408
419
|
const typedCall = jest.fn().mockImplementation((requestType: string) => {
|
|
409
420
|
if (requestType === 'DeviceStatusGet') {
|
|
@@ -411,8 +422,13 @@ describe('DeviceSettings protocol routing', () => {
|
|
|
411
422
|
return {
|
|
412
423
|
message:
|
|
413
424
|
statusReadCount === 1
|
|
414
|
-
? {
|
|
415
|
-
|
|
425
|
+
? {
|
|
426
|
+
init_states: true,
|
|
427
|
+
unlocked: true,
|
|
428
|
+
passphrase_enabled: true,
|
|
429
|
+
unlocked_by_attach_to_pin: true,
|
|
430
|
+
}
|
|
431
|
+
: { init_states: true, unlocked: false },
|
|
416
432
|
};
|
|
417
433
|
}
|
|
418
434
|
if (requestType === 'DeviceSettingsPageShow') {
|
|
@@ -437,6 +453,7 @@ describe('DeviceSettings protocol routing', () => {
|
|
|
437
453
|
mode: 'normal',
|
|
438
454
|
unlocked: true,
|
|
439
455
|
passphraseProtection: true,
|
|
456
|
+
unlockedAttachPin: true,
|
|
440
457
|
},
|
|
441
458
|
raw: {
|
|
442
459
|
protocolV2ProtocolInfo: {
|
|
@@ -456,12 +473,31 @@ describe('DeviceSettings protocol routing', () => {
|
|
|
456
473
|
});
|
|
457
474
|
method.init();
|
|
458
475
|
(method as any).device = device;
|
|
476
|
+
const onState = jest.fn();
|
|
477
|
+
device.on(DEVICE.STATE, onState);
|
|
459
478
|
|
|
460
479
|
await expect(method.run()).resolves.toEqual({ message: 'Success' });
|
|
461
480
|
expect(device.state?.status).toMatchObject({
|
|
462
481
|
unlocked: false,
|
|
463
|
-
passphraseProtection:
|
|
482
|
+
passphraseProtection: false,
|
|
483
|
+
unlockedAttachPin: false,
|
|
464
484
|
});
|
|
485
|
+
expect(onState).toHaveBeenCalledWith(
|
|
486
|
+
device,
|
|
487
|
+
expect.objectContaining({
|
|
488
|
+
source: 'settings-write',
|
|
489
|
+
changedKeys: expect.arrayContaining([
|
|
490
|
+
'status.passphraseProtection',
|
|
491
|
+
'status.unlockedAttachPin',
|
|
492
|
+
]),
|
|
493
|
+
state: expect.objectContaining({
|
|
494
|
+
status: expect.objectContaining({
|
|
495
|
+
passphraseProtection: false,
|
|
496
|
+
unlockedAttachPin: false,
|
|
497
|
+
}),
|
|
498
|
+
}),
|
|
499
|
+
})
|
|
500
|
+
);
|
|
465
501
|
});
|
|
466
502
|
|
|
467
503
|
it('keeps Protocol V1 passphrase settings on ApplySettings', async () => {
|
|
@@ -250,6 +250,8 @@ describe('DeviceStateMapper', () => {
|
|
|
250
250
|
init_states: true,
|
|
251
251
|
unlocked: false,
|
|
252
252
|
passphrase_enabled: true,
|
|
253
|
+
attach_to_pin_enabled: true,
|
|
254
|
+
unlocked_by_attach_to_pin: true,
|
|
253
255
|
backup_required: true,
|
|
254
256
|
} as DeviceStatus);
|
|
255
257
|
|
|
@@ -261,6 +263,26 @@ describe('DeviceStateMapper', () => {
|
|
|
261
263
|
backupRequired: true,
|
|
262
264
|
});
|
|
263
265
|
expect(locked.status).not.toHaveProperty('passphraseProtection');
|
|
266
|
+
expect(locked.status).not.toHaveProperty('attachToPinEnabled');
|
|
267
|
+
expect(locked.status).not.toHaveProperty('unlockedAttachPin');
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
test('omits an unavailable attach-to-PIN status after an unlocked query failure', () => {
|
|
271
|
+
const status = mapProtocolV2DeviceStatusToState({
|
|
272
|
+
init_states: true,
|
|
273
|
+
unlocked: true,
|
|
274
|
+
passphrase_enabled: true,
|
|
275
|
+
attach_to_pin_enabled: null,
|
|
276
|
+
unlocked_by_attach_to_pin: false,
|
|
277
|
+
backup_required: false,
|
|
278
|
+
} as DeviceStatus);
|
|
279
|
+
|
|
280
|
+
expect(status.status).toMatchObject({
|
|
281
|
+
unlocked: true,
|
|
282
|
+
passphraseProtection: true,
|
|
283
|
+
unlockedAttachPin: false,
|
|
284
|
+
});
|
|
285
|
+
expect(status.status).not.toHaveProperty('attachToPinEnabled');
|
|
264
286
|
});
|
|
265
287
|
|
|
266
288
|
test('maps common settings into identity, status and settings sections', () => {
|
|
@@ -288,10 +310,26 @@ describe('DeviceStateMapper', () => {
|
|
|
288
310
|
language: 'en-Latn-US',
|
|
289
311
|
brightness: 80,
|
|
290
312
|
autolock_delay_ms: 30_000,
|
|
313
|
+
passphrase_enable: null,
|
|
291
314
|
} as DeviceSettings)
|
|
292
315
|
).toEqual({
|
|
293
316
|
identity: { label: 'Pro2' },
|
|
294
317
|
settings: { language: 'en', brightness: 80, autoLockDelayMs: 30_000 },
|
|
295
318
|
});
|
|
296
319
|
});
|
|
320
|
+
|
|
321
|
+
test('omits private Protocol V2 settings that are unavailable while locked', () => {
|
|
322
|
+
expect(
|
|
323
|
+
mapDeviceSettingsToState({
|
|
324
|
+
brightness: 80,
|
|
325
|
+
passphrase_enable: null,
|
|
326
|
+
fido_enabled: null,
|
|
327
|
+
autolock_delay_ms: null,
|
|
328
|
+
autoshutdown_delay_ms: null,
|
|
329
|
+
label: null,
|
|
330
|
+
} as DeviceSettings)
|
|
331
|
+
).toEqual({
|
|
332
|
+
settings: { brightness: 80 },
|
|
333
|
+
});
|
|
334
|
+
});
|
|
297
335
|
});
|
|
@@ -352,7 +352,7 @@ describe('Protocol V1 wallet identity initialization', () => {
|
|
|
352
352
|
jest.restoreAllMocks();
|
|
353
353
|
});
|
|
354
354
|
|
|
355
|
-
test('
|
|
355
|
+
test('verifies the live device id without resetting the cached wallet session', async () => {
|
|
356
356
|
const device = Device.fromDescriptor({ id: 'connect-b', path: 'connect-b' } as never);
|
|
357
357
|
device.features = {
|
|
358
358
|
protocol: 'V1',
|
|
@@ -363,7 +363,7 @@ describe('Protocol V1 wallet identity initialization', () => {
|
|
|
363
363
|
deviceWalletSessionStore.set('cached-device-a', 'hidden-a', 'session-a');
|
|
364
364
|
const typedCall = jest.fn().mockResolvedValue({
|
|
365
365
|
type: 'Features',
|
|
366
|
-
message: { device_id: 'live-device-b'
|
|
366
|
+
message: { device_id: 'live-device-b' },
|
|
367
367
|
});
|
|
368
368
|
device.commands = { typedCall } as never;
|
|
369
369
|
jest.spyOn(TransportManager, 'reconfigure').mockResolvedValue(undefined);
|
|
@@ -371,76 +371,11 @@ describe('Protocol V1 wallet identity initialization', () => {
|
|
|
371
371
|
await expect(
|
|
372
372
|
device.initialize({ deviceId: 'cached-device-a', passphraseState: 'hidden-a' })
|
|
373
373
|
).rejects.toMatchObject({ errorCode: HardwareErrorCode.DeviceCheckDeviceIdError });
|
|
374
|
-
// Identity is validated from the single Initialize round trip; no separate
|
|
375
|
-
// GetFeatures preflight.
|
|
376
374
|
expect(typedCall).toHaveBeenCalledTimes(1);
|
|
377
|
-
expect(typedCall).toHaveBeenCalledWith(
|
|
378
|
-
'Initialize',
|
|
379
|
-
'Features',
|
|
380
|
-
expect.objectContaining({ passphrase_state: 'hidden-a' }),
|
|
381
|
-
expect.any(Object)
|
|
382
|
-
);
|
|
383
|
-
// Identity change purges the previous device's cached sessions (pre-existing
|
|
384
|
-
// reconcileDeviceIdentity behavior — never carry sessions across devices)…
|
|
385
|
-
expect(deviceWalletSessionStore.get('cached-device-a', 'hidden-a')).toBeUndefined();
|
|
386
|
-
// …and the session the mismatched Initialize cached under the wrong
|
|
387
|
-
// device's identity is dropped as well.
|
|
388
|
-
expect(deviceWalletSessionStore.get('live-device-b', 'hidden-a')).toBeUndefined();
|
|
375
|
+
expect(typedCall).toHaveBeenCalledWith('GetFeatures', 'Features', {});
|
|
389
376
|
});
|
|
390
377
|
|
|
391
|
-
test('
|
|
392
|
-
const device = Device.fromDescriptor({ id: 'connect-b', path: 'connect-b' } as never);
|
|
393
|
-
device.features = {
|
|
394
|
-
protocol: 'V1',
|
|
395
|
-
deviceId: 'cached-device-a',
|
|
396
|
-
unlocked: true,
|
|
397
|
-
passphraseProtection: true,
|
|
398
|
-
} as never;
|
|
399
|
-
deviceWalletSessionStore.set('cached-device-a', 'hidden-a', 'session-a');
|
|
400
|
-
const typedCall = jest.fn().mockResolvedValue({
|
|
401
|
-
type: 'Features',
|
|
402
|
-
message: { device_id: 'live-device-b', session_id: 'wrong-device-session' },
|
|
403
|
-
});
|
|
404
|
-
device.commands = { typedCall } as never;
|
|
405
|
-
// Pre-encode resync succeeds; the post-response reconfigure inside
|
|
406
|
-
// callInitialize rejects — the session write has already happened by then.
|
|
407
|
-
jest
|
|
408
|
-
.spyOn(TransportManager, 'reconfigure')
|
|
409
|
-
.mockResolvedValueOnce(undefined)
|
|
410
|
-
.mockRejectedValueOnce(new Error('configure failed'));
|
|
411
|
-
|
|
412
|
-
await expect(
|
|
413
|
-
device.initialize({ deviceId: 'cached-device-a', passphraseState: 'hidden-a' })
|
|
414
|
-
).rejects.toMatchObject({ errorCode: HardwareErrorCode.DeviceCheckDeviceIdError });
|
|
415
|
-
// The wrong-device session must not survive the error path either.
|
|
416
|
-
expect(deviceWalletSessionStore.get('live-device-b', 'hidden-a')).toBeUndefined();
|
|
417
|
-
});
|
|
418
|
-
|
|
419
|
-
test('re-syncs the V1 message schema for this device before encoding Initialize', async () => {
|
|
420
|
-
const device = Device.fromDescriptor({ id: 'connect-a', path: 'connect-a' } as never);
|
|
421
|
-
device.features = {
|
|
422
|
-
protocol: 'V1',
|
|
423
|
-
deviceId: 'device-a',
|
|
424
|
-
unlocked: true,
|
|
425
|
-
passphraseProtection: true,
|
|
426
|
-
} as never;
|
|
427
|
-
const typedCall = jest.fn().mockResolvedValue({
|
|
428
|
-
type: 'Features',
|
|
429
|
-
message: { device_id: 'device-a' },
|
|
430
|
-
});
|
|
431
|
-
device.commands = { typedCall } as never;
|
|
432
|
-
const reconfigure = jest.spyOn(TransportManager, 'reconfigure').mockResolvedValue(undefined);
|
|
433
|
-
|
|
434
|
-
await device.initialize({ deviceId: 'device-a', passphraseState: 'hidden-a' });
|
|
435
|
-
|
|
436
|
-
// A stale process-global schema (from another device) would silently strip
|
|
437
|
-
// passphrase_state from the wire message, so the resync must come first.
|
|
438
|
-
expect(reconfigure.mock.invocationCallOrder[0]).toBeLessThan(
|
|
439
|
-
typedCall.mock.invocationCallOrder[0]
|
|
440
|
-
);
|
|
441
|
-
});
|
|
442
|
-
|
|
443
|
-
test('resumes a cached V1 wallet with a single identity-validated Initialize', async () => {
|
|
378
|
+
test('resumes a cached V1 wallet after a non-destructive live identity read', async () => {
|
|
444
379
|
const device = Device.fromDescriptor({ id: 'connect-a', path: 'connect-a' } as never);
|
|
445
380
|
device.features = {
|
|
446
381
|
protocol: 'V1',
|
|
@@ -449,17 +384,22 @@ describe('Protocol V1 wallet identity initialization', () => {
|
|
|
449
384
|
passphraseProtection: true,
|
|
450
385
|
} as never;
|
|
451
386
|
deviceWalletSessionStore.set('device-a', 'hidden-a', 'session-a');
|
|
452
|
-
const typedCall = jest
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
387
|
+
const typedCall = jest
|
|
388
|
+
.fn()
|
|
389
|
+
.mockResolvedValueOnce({ type: 'Features', message: { device_id: 'device-a' } })
|
|
390
|
+
.mockResolvedValueOnce({
|
|
391
|
+
type: 'Features',
|
|
392
|
+
message: { device_id: 'device-a', session_id: 'session-a' },
|
|
393
|
+
});
|
|
456
394
|
device.commands = { typedCall } as never;
|
|
457
395
|
jest.spyOn(TransportManager, 'reconfigure').mockResolvedValue(undefined);
|
|
458
396
|
|
|
459
397
|
await device.initialize({ deviceId: 'device-a', passphraseState: 'hidden-a' });
|
|
460
398
|
|
|
461
|
-
expect(typedCall).toHaveBeenCalledTimes(
|
|
462
|
-
expect(typedCall).
|
|
399
|
+
expect(typedCall).toHaveBeenCalledTimes(2);
|
|
400
|
+
expect(typedCall).toHaveBeenNthCalledWith(1, 'GetFeatures', 'Features', {});
|
|
401
|
+
expect(typedCall).toHaveBeenNthCalledWith(
|
|
402
|
+
2,
|
|
463
403
|
'Initialize',
|
|
464
404
|
'Features',
|
|
465
405
|
expect.objectContaining({
|
|
@@ -468,10 +408,9 @@ describe('Protocol V1 wallet identity initialization', () => {
|
|
|
468
408
|
}),
|
|
469
409
|
expect.any(Object)
|
|
470
410
|
);
|
|
471
|
-
expect(deviceWalletSessionStore.get('device-a', 'hidden-a')).toBe('session-a');
|
|
472
411
|
});
|
|
473
412
|
|
|
474
|
-
test('selects the standard V1 wallet
|
|
413
|
+
test('selects the standard V1 wallet after a non-destructive live identity read', async () => {
|
|
475
414
|
const device = Device.fromDescriptor({ id: 'connect-a', path: 'connect-a' } as never);
|
|
476
415
|
device.features = {
|
|
477
416
|
protocol: 'V1',
|
|
@@ -481,14 +420,17 @@ describe('Protocol V1 wallet identity initialization', () => {
|
|
|
481
420
|
} as never;
|
|
482
421
|
const typedCall = jest
|
|
483
422
|
.fn()
|
|
423
|
+
.mockResolvedValueOnce({ type: 'Features', message: { device_id: 'device-a' } })
|
|
484
424
|
.mockResolvedValueOnce({ type: 'Features', message: { device_id: 'device-a' } });
|
|
485
425
|
device.commands = { typedCall } as never;
|
|
486
426
|
jest.spyOn(TransportManager, 'reconfigure').mockResolvedValue(undefined);
|
|
487
427
|
|
|
488
428
|
await device.initialize({ deviceId: 'device-a' });
|
|
489
429
|
|
|
490
|
-
expect(typedCall).toHaveBeenCalledTimes(
|
|
491
|
-
expect(typedCall).
|
|
430
|
+
expect(typedCall).toHaveBeenCalledTimes(2);
|
|
431
|
+
expect(typedCall).toHaveBeenNthCalledWith(1, 'GetFeatures', 'Features', {});
|
|
432
|
+
expect(typedCall).toHaveBeenNthCalledWith(
|
|
433
|
+
2,
|
|
492
434
|
'Initialize',
|
|
493
435
|
'Features',
|
|
494
436
|
{
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { HardwareErrorCode } from '@onekeyfe/hd-shared';
|
|
1
|
+
import { ERRORS, HardwareErrorCode } from '@onekeyfe/hd-shared';
|
|
2
2
|
|
|
3
3
|
import FirmwareUpdateV4 from '../../src/api/FirmwareUpdateV4';
|
|
4
4
|
|
|
@@ -127,4 +127,40 @@ describe('FirmwareUpdateV4 install polling', () => {
|
|
|
127
127
|
errorCode: HardwareErrorCode.CallQueueActionCancelled,
|
|
128
128
|
});
|
|
129
129
|
});
|
|
130
|
+
|
|
131
|
+
test('stops polling when the device cancels firmware installation', async () => {
|
|
132
|
+
const method = new FirmwareUpdateV4({
|
|
133
|
+
id: 1,
|
|
134
|
+
payload: {
|
|
135
|
+
method: 'firmwareUpdateV4',
|
|
136
|
+
connectId: 'pro2-ble',
|
|
137
|
+
},
|
|
138
|
+
});
|
|
139
|
+
const typedCall = jest
|
|
140
|
+
.fn()
|
|
141
|
+
.mockRejectedValue(ERRORS.TypedError(HardwareErrorCode.ActionCancelled));
|
|
142
|
+
const reconnectProtocolV2Device = jest.fn().mockResolvedValue(undefined);
|
|
143
|
+
|
|
144
|
+
method.device = {
|
|
145
|
+
getCommands: () => ({ typedCall }),
|
|
146
|
+
} as unknown as Device;
|
|
147
|
+
const firmwareUpdate = method as unknown as {
|
|
148
|
+
waitForProtocolV2FirmwareUpdateComplete: (
|
|
149
|
+
targets: Array<{ target_id: number; path: string }>
|
|
150
|
+
) => Promise<void>;
|
|
151
|
+
reconnectProtocolV2Device: () => Promise<void>;
|
|
152
|
+
};
|
|
153
|
+
firmwareUpdate.reconnectProtocolV2Device = reconnectProtocolV2Device;
|
|
154
|
+
|
|
155
|
+
await expect(
|
|
156
|
+
firmwareUpdate.waitForProtocolV2FirmwareUpdateComplete([
|
|
157
|
+
{ target_id: 4, path: 'vol0:/application_p1.bin' },
|
|
158
|
+
])
|
|
159
|
+
).rejects.toMatchObject({
|
|
160
|
+
errorCode: HardwareErrorCode.ActionCancelled,
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
expect(typedCall).toHaveBeenCalledTimes(1);
|
|
164
|
+
expect(reconnectProtocolV2Device).not.toHaveBeenCalled();
|
|
165
|
+
});
|
|
130
166
|
});
|
|
@@ -1,9 +1,14 @@
|
|
|
1
|
-
import { DeviceSessionPinType } from '@onekeyfe/hd-transport';
|
|
1
|
+
import TransportUtils, { DeviceSessionPinType } from '@onekeyfe/hd-transport';
|
|
2
2
|
|
|
3
3
|
import ConfluxSignMessageCIP23 from '../src/api/conflux/ConfluxSignMessageCIP23';
|
|
4
|
+
import DeviceChangePin from '../src/api/device/DeviceChangePin';
|
|
4
5
|
import DeviceLock from '../src/api/device/DeviceLock';
|
|
5
6
|
import DeviceSettings from '../src/api/device/DeviceSettings';
|
|
7
|
+
import DeviceVerify from '../src/api/device/DeviceVerify';
|
|
8
|
+
import DeviceWipe from '../src/api/device/DeviceWipe';
|
|
9
|
+
import FirmwareUpdateV4 from '../src/api/FirmwareUpdateV4';
|
|
6
10
|
import OpenWalletSession from '../src/api/OpenWalletSession';
|
|
11
|
+
import DataManager from '../src/data-manager/DataManager';
|
|
7
12
|
import { runMethodWithUnlockPolicy } from '../src/protocols/protocol-v2/unlockPolicyRunner';
|
|
8
13
|
|
|
9
14
|
jest.mock('../src/data/config', () => ({
|
|
@@ -12,6 +17,42 @@ jest.mock('../src/data/config', () => ({
|
|
|
12
17
|
}));
|
|
13
18
|
|
|
14
19
|
describe('Protocol V2 unlock semantics', () => {
|
|
20
|
+
test('serializes genuine-device verification with Protocol V2 certificate messages', async () => {
|
|
21
|
+
const method = new DeviceVerify({
|
|
22
|
+
id: 1,
|
|
23
|
+
payload: { method: 'deviceVerify', dataHex: 'aabb' },
|
|
24
|
+
});
|
|
25
|
+
method.init();
|
|
26
|
+
const protocolV2Messages = TransportUtils.parseConfigure(
|
|
27
|
+
DataManager.getProtobufMessages('v2Schema')
|
|
28
|
+
);
|
|
29
|
+
const typedCall = jest.fn(
|
|
30
|
+
(requestType: string, responseType: string, payload: Record<string, unknown> = {}) => {
|
|
31
|
+
const request = TransportUtils.createMessageFromName(protocolV2Messages, requestType);
|
|
32
|
+
request.Message.encode(request.Message.create(payload)).finish();
|
|
33
|
+
TransportUtils.createMessageFromName(protocolV2Messages, responseType);
|
|
34
|
+
if (requestType === 'DeviceCertificateSign') {
|
|
35
|
+
return Promise.resolve({ message: { data: 'signature' } });
|
|
36
|
+
}
|
|
37
|
+
return Promise.resolve({ message: { cert_and_pubkey: 'certificate' } });
|
|
38
|
+
}
|
|
39
|
+
);
|
|
40
|
+
method.device = {
|
|
41
|
+
getCurrentDeviceType: () => 'pro2',
|
|
42
|
+
isProtocolV2: () => true,
|
|
43
|
+
commands: { typedCall },
|
|
44
|
+
} as any;
|
|
45
|
+
|
|
46
|
+
await expect(method.run()).resolves.toEqual({
|
|
47
|
+
cert: 'certificate',
|
|
48
|
+
signature: 'signature',
|
|
49
|
+
});
|
|
50
|
+
expect(typedCall.mock.calls.map(call => call.slice(0, 2))).toEqual([
|
|
51
|
+
['DeviceCertificateSign', 'DeviceCertificateSignature'],
|
|
52
|
+
['DeviceCertificateRead', 'DeviceCertificate'],
|
|
53
|
+
]);
|
|
54
|
+
});
|
|
55
|
+
|
|
15
56
|
test('wallet business methods inherit the wallet-session unlock requirement', () => {
|
|
16
57
|
const method = new ConfluxSignMessageCIP23({
|
|
17
58
|
id: 1,
|
|
@@ -191,6 +232,61 @@ describe('Protocol V2 unlock semantics', () => {
|
|
|
191
232
|
expect(device.unlockDevice).toHaveBeenCalledWith(DeviceSessionPinType.Any, expect.any(Object));
|
|
192
233
|
});
|
|
193
234
|
|
|
235
|
+
test.each([
|
|
236
|
+
[
|
|
237
|
+
'PIN changes',
|
|
238
|
+
() =>
|
|
239
|
+
new DeviceChangePin({
|
|
240
|
+
id: 1,
|
|
241
|
+
payload: { method: 'deviceChangePin', remove: false },
|
|
242
|
+
}),
|
|
243
|
+
],
|
|
244
|
+
['device wipe', () => new DeviceWipe({ id: 1, payload: { method: 'deviceWipe' } })],
|
|
245
|
+
[
|
|
246
|
+
'firmware updates',
|
|
247
|
+
() =>
|
|
248
|
+
new FirmwareUpdateV4({
|
|
249
|
+
id: 1,
|
|
250
|
+
payload: { method: 'firmwareUpdateV4', platform: 'desktop' } as any,
|
|
251
|
+
}),
|
|
252
|
+
],
|
|
253
|
+
[
|
|
254
|
+
'genuine-device verification',
|
|
255
|
+
() =>
|
|
256
|
+
new DeviceVerify({
|
|
257
|
+
id: 1,
|
|
258
|
+
payload: { method: 'deviceVerify', dataHex: '00' },
|
|
259
|
+
}),
|
|
260
|
+
],
|
|
261
|
+
])('allows either PIN type when pre-unlocking %s', async (_name, createMethod) => {
|
|
262
|
+
const method = createMethod();
|
|
263
|
+
method.init();
|
|
264
|
+
const features = { unlocked: false };
|
|
265
|
+
const device = {
|
|
266
|
+
features,
|
|
267
|
+
commands: {
|
|
268
|
+
typedCall: jest.fn().mockResolvedValue({ message: { unlocked: false } }),
|
|
269
|
+
},
|
|
270
|
+
isProtocolV2: () => true,
|
|
271
|
+
isBootloader: () => false,
|
|
272
|
+
isRomloader: () => false,
|
|
273
|
+
updateProtocolV2Status: jest.fn(() => features),
|
|
274
|
+
unlockDevice: jest.fn().mockImplementation(() => {
|
|
275
|
+
features.unlocked = true;
|
|
276
|
+
return Promise.resolve(features);
|
|
277
|
+
}),
|
|
278
|
+
};
|
|
279
|
+
method.run = jest.fn().mockResolvedValue({ message: 'ok' });
|
|
280
|
+
|
|
281
|
+
await expect(runMethodWithUnlockPolicy(method, device as any)).resolves.toEqual({
|
|
282
|
+
message: 'ok',
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
expect(method.unlockPolicy).toBe('unlock-before-run');
|
|
286
|
+
expect(method.getSupportedProtocols()).toContain('V2');
|
|
287
|
+
expect(device.unlockDevice).toHaveBeenCalledWith(DeviceSessionPinType.Any, expect.any(Object));
|
|
288
|
+
});
|
|
289
|
+
|
|
194
290
|
test('pre-unlocks a locked standard wallet before wallet-session preparation', async () => {
|
|
195
291
|
const calls: string[] = [];
|
|
196
292
|
const features = {
|