diff --git a/docs/device/wallet-session-and-security.md b/docs/device/wallet-session-and-security.md index 5e4d2fbd2..cad21eda7 100644 --- a/docs/device/wallet-session-and-security.md +++ b/docs/device/wallet-session-and-security.md @@ -60,9 +60,19 @@ 校验原始 `DeviceStatus`,然后直接用空参数 `DeviceSessionGet()` 读取当前隐藏钱包,不重新发起 Passphrase 选择、`DeviceSessionAskPassphrase` 或 `DeviceSessionAskPin`。复核状态不一致时失败关闭, 不回退到钱包重选。 -- Core 把现有 `deriveCardano` 意图映射为 `DeviceSessionGet.seed_domains`:普通业务请求 - `[Standard]`,Cardano 业务请求 `[Standard, Cardano]`。调用链没有提供派生意图时省略该字段, - 保持固件“派生全部支持域”的兼容行为。 +- Protocol V2 maps `deriveCardano` to `DeviceSessionAskPassphrase.seed_domains`: ordinary requests + use `[Standard]`, and explicit Cardano requests use `[Standard, Cardano]`. `DeviceSessionGet` + only reads or resumes a session; `DeviceSession.seed_domains` reports the generated domains. + This replaces the firmware contract used by SDK 1.2.1, which sent the field on `DeviceSessionGet`. +- When a reused session lacks Cardano, Core requests the missing derivation and verifies that the + returned wallet identity is unchanged. Standard and Attach PIN wallets use an empty host + passphrase; other hidden wallets request their passphrase again. With passphrase protection + disabled, firmware generates Cardano through `DeviceSessionGet`. Missing Cardano or a changed + identity fails closed. Attach PIN context is retained after the empty host request. +- Before skipping Main PIN selection, Core refreshes device status. An unlocked standard wallet + with passphrase protection disabled does not request Main PIN again. Locked devices, Attach PIN + contexts, and mismatched cached standard wallets still require the existing authentication flow. + Existing PIN, passphrase, and Attach PIN UI events and Protocol V1 behavior remain unchanged. - `DeviceSessionAskPin` 的类型按业务意图选择:标准钱包和安全操作使用 `Main`;普通业务调用已携带目标 `passphraseState` 时,预解锁使用 `Any`,允许主 PIN 或 Attach PIN 进入,随后仍以返回的 `btc_test_address` 校验目标隐藏钱包;用户明确选择 Attach PIN 打开隐藏钱包时使用 `AttachToPin`。 diff --git a/packages/connect-examples/electron-example/package.json b/packages/connect-examples/electron-example/package.json index 0139dda0f..2768aaa8f 100644 --- a/packages/connect-examples/electron-example/package.json +++ b/packages/connect-examples/electron-example/package.json @@ -2,7 +2,7 @@ "name": "hardware-example", "productName": "HardwareExample", "executableName": "onekey-hardware-example", - "version": "1.2.1", + "version": "1.2.2", "author": "OneKey", "description": "OneKey Hardware SDK Electron BLE example", "main": "dist/index.js", @@ -21,7 +21,7 @@ "ts:check": "yarn tsc --noEmit" }, "dependencies": { - "@onekeyfe/hd-transport-electron": "1.2.1", + "@onekeyfe/hd-transport-electron": "1.2.2", "@stoprocent/noble": "2.3.16", "debug": "4.3.4", "electron-is-dev": "^3.0.1", diff --git a/packages/connect-examples/expo-example/package.json b/packages/connect-examples/expo-example/package.json index c0e635e9a..4e70f24e8 100644 --- a/packages/connect-examples/expo-example/package.json +++ b/packages/connect-examples/expo-example/package.json @@ -1,6 +1,6 @@ { "name": "expo-example", - "version": "1.2.1", + "version": "1.2.2", "scripts": { "start": "yarn expo start --dev-client", "android": "yarn expo run:android", @@ -19,11 +19,11 @@ "@noble/ed25519": "^2.1.0", "@noble/hashes": "^1.3.3", "@noble/secp256k1": "^1.7.1", - "@onekeyfe/hd-ble-sdk": "1.2.1", - "@onekeyfe/hd-common-connect-sdk": "1.2.1", - "@onekeyfe/hd-core": "1.2.1", - "@onekeyfe/hd-shared": "1.2.1", - "@onekeyfe/hd-transport": "1.2.1", + "@onekeyfe/hd-ble-sdk": "1.2.2", + "@onekeyfe/hd-common-connect-sdk": "1.2.2", + "@onekeyfe/hd-core": "1.2.2", + "@onekeyfe/hd-shared": "1.2.2", + "@onekeyfe/hd-transport": "1.2.2", "@onekeyfe/react-native-ble-utils": "^0.1.3", "@polkadot/util-crypto": "13.1.1", "@react-native-async-storage/async-storage": "1.21.0", diff --git a/packages/connect-examples/expo-playground/package.json b/packages/connect-examples/expo-playground/package.json index 565469ace..b37a024e4 100644 --- a/packages/connect-examples/expo-playground/package.json +++ b/packages/connect-examples/expo-playground/package.json @@ -1,6 +1,6 @@ { "name": "onekey-hardware-playground", - "version": "1.2.1", + "version": "1.2.2", "private": true, "sideEffects": [ "app/utils/shim.js", @@ -19,10 +19,10 @@ }, "dependencies": { "@noble/hashes": "^1.8.0", - "@onekeyfe/hd-common-connect-sdk": "1.2.1", - "@onekeyfe/hd-core": "1.2.1", - "@onekeyfe/hd-shared": "1.2.1", - "@onekeyfe/hd-transport": "1.2.1", + "@onekeyfe/hd-common-connect-sdk": "1.2.2", + "@onekeyfe/hd-core": "1.2.2", + "@onekeyfe/hd-shared": "1.2.2", + "@onekeyfe/hd-transport": "1.2.2", "@radix-ui/react-checkbox": "^1.3.2", "@radix-ui/react-dialog": "^1.1.14", "@radix-ui/react-dropdown-menu": "^2.1.15", diff --git a/packages/connect-examples/hwk-demo/package.json b/packages/connect-examples/hwk-demo/package.json index 737c4287f..60654830e 100644 --- a/packages/connect-examples/hwk-demo/package.json +++ b/packages/connect-examples/hwk-demo/package.json @@ -1,6 +1,6 @@ { "name": "hwk-demo", - "version": "1.2.1", + "version": "1.2.2", "private": true, "main": "index.js", "scripts": { diff --git a/packages/core/__tests__/open-wallet-session.test.ts b/packages/core/__tests__/open-wallet-session.test.ts index ce8789c35..fbfe41f8a 100644 --- a/packages/core/__tests__/open-wallet-session.test.ts +++ b/packages/core/__tests__/open-wallet-session.test.ts @@ -11,6 +11,12 @@ import { getProtocolV2WalletSession, } from '../src/protocols/protocol-v2/walletSession'; +const STANDARD_SEED_DOMAINS = [DeviceSessionSeedDomain.SeedDomain_Standard]; +const CARDANO_SEED_DOMAINS = [ + DeviceSessionSeedDomain.SeedDomain_Standard, + DeviceSessionSeedDomain.SeedDomain_Cardano, +]; + jest.mock('../src/data/config', () => ({ getSDKVersion: jest.fn(() => '1.0.0'), DEFAULT_DOMAIN: 'https://jssdk.onekey.so/1.0.0/', @@ -19,12 +25,16 @@ jest.mock('../src/data/config', () => ({ const createDevice = ({ passphraseProtection = true, unlockedAttachPin = false, + refreshedUnlocked = true, + refreshedPassphraseProtection = passphraseProtection, refreshedUnlockedAttachPin = unlockedAttachPin, typedCall = jest.fn(), promptPassphrase = jest.fn(), }: { passphraseProtection?: boolean; unlockedAttachPin?: boolean; + refreshedUnlocked?: boolean; + refreshedPassphraseProtection?: boolean; refreshedUnlockedAttachPin?: boolean; typedCall?: jest.Mock; promptPassphrase?: jest.Mock; @@ -43,11 +53,11 @@ const createDevice = ({ return { message: { device_id: 'device-1', - unlocked: true, + unlocked: refreshedUnlocked, attach_to_pin_enabled: unlockedAttachPin, unlocked_attach_pin: refreshedUnlockedAttachPin, unlocked_by_attach_to_pin: refreshedUnlockedAttachPin, - passphrase_enabled: passphraseProtection, + passphrase_enabled: refreshedPassphraseProtection, }, }; } @@ -88,6 +98,8 @@ const createDevice = ({ unlockDevice: jest.fn(), updateProtocolV2Status: jest.fn((status: Record) => { device.features.unlocked = status.unlocked ?? device.features.unlocked; + device.features.passphraseProtection = + status.passphrase_enabled ?? device.features.passphraseProtection; device.features.attachToPinEnabled = status.attach_to_pin_enabled ?? device.features.attachToPinEnabled; device.features.unlockedAttachPin = @@ -142,13 +154,12 @@ describe('openWalletSession', () => { expect(typedCall).toHaveBeenCalledWith('DeviceSessionAskPassphrase', 'Success', { passphrase: '', on_device: false, + seed_domains: [DeviceSessionSeedDomain.SeedDomain_Standard], }); - expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', { - seed_domains: [], - }); + expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {}); }); - test('selects the Main PIN before opening the standard wallet when passphrase is disabled', async () => { + test('opens the already-unlocked standard wallet without repeating Main PIN when passphrase is disabled', async () => { const typedCall = jest.fn((request: string) => { if (request === 'ProtocolInfoRequest') { return { message: { version: 2 } }; @@ -167,21 +178,115 @@ describe('openWalletSession', () => { await getProtocolV2WalletSession(device as any, { onlyMainPin: true }); - expect(device.unlockDevice).toHaveBeenCalledWith(DeviceSessionPinType.Main, { - source: 'wallet-session-coordinator', - reason: 'open-wallet', - deviceOnly: true, - }); + expect(device.unlockDevice).not.toHaveBeenCalled(); expect(typedCall).not.toHaveBeenCalledWith( 'DeviceSessionAskPassphrase', 'Success', expect.anything() ); - expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', { - seed_domains: [], + expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {}); + }); + + test('refreshes stale wallet status before accepting an only-Main-PIN session', async () => { + let attachPinSelected = true; + const typedCall = jest.fn((request: string) => { + if (request === 'ProtocolInfoRequest') { + return { message: { version: 2 } }; + } + if (request === 'DeviceSessionGet') { + return { + message: { + btc_test_address: 'standard-state', + session_id: 'standard-session', + }, + }; + } + if (request === 'DeviceSessionAskPassphrase') { + return { message: {} }; + } + throw new Error(`Unexpected request: ${request}`); + }); + const device = createDevice({ + passphraseProtection: false, + unlockedAttachPin: false, + typedCall, + }); + device.commands.typedCall.mockImplementation((request: string, ...args: unknown[]) => { + if (request === 'DeviceStatusGet') { + return { + message: { + device_id: 'device-1', + unlocked: true, + attach_to_pin_enabled: true, + unlocked_attach_pin: attachPinSelected, + unlocked_by_attach_to_pin: attachPinSelected, + passphrase_enabled: true, + }, + }; + } + return typedCall(request, ...args); + }); + device.unlockDevice.mockImplementation(() => { + attachPinSelected = false; + device.features.unlockedAttachPin = false; + return Promise.resolve(device.features); + }); + + await expect( + getProtocolV2WalletSession(device as any, { onlyMainPin: true }) + ).resolves.toMatchObject({ + unlockedAttachPin: false, + }); + + expect(device.commands.typedCall).toHaveBeenCalledWith('DeviceStatusGet', 'DeviceStatus', {}); + expect(device.unlockDevice).toHaveBeenCalledWith(DeviceSessionPinType.Main, { + source: 'wallet-session-coordinator', + reason: 'open-wallet', + deviceOnly: true, }); }); + test('still requires Main PIN when a cached standard session resolves to another wallet', async () => { + let sessionGetCount = 0; + const typedCall = jest.fn((request: string) => { + if (request === 'ProtocolInfoRequest') { + return { message: { version: 2 } }; + } + if (request === 'DeviceSessionGet') { + sessionGetCount += 1; + return { + message: { + btc_test_address: + sessionGetCount === 1 ? 'unexpected-wallet-state' : 'cached-standard-state', + session_id: + sessionGetCount === 1 ? 'unexpected-wallet-session' : 'cached-standard-session', + }, + }; + } + throw new Error(`Unexpected request: ${request}`); + }); + const device = createDevice({ passphraseProtection: false, typedCall }); + device.getStandardInternalState = jest.fn(() => ({ + passphraseState: 'cached-standard-state', + sessionId: 'cached-standard-session', + })); + device.clearStandardInternalState = jest.fn(); + + await expect( + getProtocolV2WalletSession(device as any, { onlyMainPin: true }) + ).resolves.toMatchObject({ + passphraseState: 'cached-standard-state', + newSession: 'cached-standard-session', + }); + + expect(device.unlockDevice).toHaveBeenCalledWith(DeviceSessionPinType.Main, { + source: 'wallet-session-coordinator', + reason: 'session-recovery', + deviceOnly: true, + }); + expect(sessionGetCount).toBe(2); + }); + test('reuses a Main PIN selected by the current preflight when passphrase is disabled', async () => { const typedCall = jest.fn((request: string) => { if (request === 'ProtocolInfoRequest') { @@ -205,9 +310,7 @@ describe('openWalletSession', () => { }); expect(device.unlockDevice).not.toHaveBeenCalled(); - expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', { - seed_domains: [], - }); + expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {}); }); test('does not treat an Attach PIN DeviceStatus as the Main wallet', async () => { @@ -228,8 +331,7 @@ describe('openWalletSession', () => { } throw new Error(`Unexpected request: ${request}`); }); - const device = createDevice({ typedCall }); - device.features.unlockedAttachPin = true; + const device = createDevice({ refreshedUnlockedAttachPin: true, typedCall }); await getProtocolV2WalletSession(device as any, { onlyMainPin: true }); @@ -334,10 +436,9 @@ describe('openWalletSession', () => { expect(typedCall).toHaveBeenNthCalledWith(2, 'DeviceSessionAskPassphrase', 'Success', { passphrase: 'host hidden wallet', on_device: false, + seed_domains: [DeviceSessionSeedDomain.SeedDomain_Standard], }); - expect(typedCall).toHaveBeenNthCalledWith(3, 'DeviceSessionGet', 'DeviceSession', { - seed_domains: [], - }); + expect(typedCall).toHaveBeenNthCalledWith(3, 'DeviceSessionGet', 'DeviceSession', {}); }); test('keeps Legacy Protocol V1 getPassphraseState parameterless', async () => { @@ -454,10 +555,9 @@ describe('openWalletSession', () => { expect(typedCall).toHaveBeenNthCalledWith(2, 'DeviceSessionAskPassphrase', 'Success', { passphrase: 'host hidden wallet', on_device: false, + seed_domains: [DeviceSessionSeedDomain.SeedDomain_Standard], }); - expect(typedCall).toHaveBeenNthCalledWith(3, 'DeviceSessionGet', 'DeviceSession', { - seed_domains: [], - }); + expect(typedCall).toHaveBeenNthCalledWith(3, 'DeviceSessionGet', 'DeviceSession', {}); }); test('refreshes Pro2 status and unlocks before selecting a hidden wallet when locked', async () => { @@ -731,9 +831,7 @@ describe('openWalletSession', () => { }); expect(promptPassphrase).not.toHaveBeenCalled(); expect(device.unlockDevice).not.toHaveBeenCalled(); - expect(device.commands.typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', { - seed_domains: [], - }); + expect(device.commands.typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {}); expect(device.commands.typedCall).not.toHaveBeenCalledWith( 'DeviceSessionAskPassphrase', 'Success', @@ -829,10 +927,9 @@ describe('openWalletSession', () => { expect(typedCall).toHaveBeenCalledWith('DeviceSessionAskPassphrase', 'Success', { passphrase: 'host hidden wallet', on_device: false, + seed_domains: [DeviceSessionSeedDomain.SeedDomain_Standard], }); - expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', { - seed_domains: [], - }); + expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {}); expect(promptPassphrase).toHaveBeenCalled(); expect(deviceWalletSessionStore.get('device-1', 'new-hidden-state')).toBe('new-hidden-session'); }); @@ -904,7 +1001,6 @@ describe('openWalletSession', () => { expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', { session_id: 'known-session', btc_test_address: 'hidden-state', - seed_domains: [], }); expect(promptPassphrase).not.toHaveBeenCalled(); }); @@ -1087,10 +1183,9 @@ describe('openWalletSession', () => { expect(typedCall).toHaveBeenCalledWith('DeviceSessionAskPassphrase', 'Success', { passphrase: '', on_device: false, + seed_domains: [DeviceSessionSeedDomain.SeedDomain_Standard], }); - expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', { - seed_domains: [], - }); + expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {}); expect(promptPassphrase).not.toHaveBeenCalled(); expect(device.passphraseState).toBeUndefined(); }); @@ -1110,7 +1205,7 @@ describe('openWalletSession', () => { payload: { method: 'openWalletSession', connectId: 'connect-id', mode: 'standard' }, }); method.init(); - const device = createDevice({ typedCall }); + const device = createDevice({ refreshedUnlocked: false, typedCall }); device.features.unlocked = false; device.getDeviceState = jest .fn() @@ -1210,6 +1305,7 @@ describe('openWalletSession', () => { }); test('switches from Attach PIN to Main PIN before opening the standard wallet', async () => { + let attachPinSelected = true; const typedCall = jest .fn() .mockResolvedValueOnce({ message: { version: 2 } }) @@ -1226,6 +1322,21 @@ describe('openWalletSession', () => { method.init(); const device = createDevice({ typedCall }); device.features.unlockedAttachPin = true; + device.commands.typedCall.mockImplementation((request: string, ...args: unknown[]) => { + if (request === 'DeviceStatusGet') { + return { + message: { + device_id: 'device-1', + unlocked: true, + attach_to_pin_enabled: true, + unlocked_attach_pin: attachPinSelected, + unlocked_by_attach_to_pin: attachPinSelected, + passphrase_enabled: true, + }, + }; + } + return typedCall(request, ...args); + }); device.getDeviceState = jest .fn() .mockResolvedValueOnce({ @@ -1245,6 +1356,7 @@ describe('openWalletSession', () => { }, }); device.unlockDevice = jest.fn().mockImplementation(() => { + attachPinSelected = false; device.features.unlockedAttachPin = false; return Promise.resolve(device.features); }); @@ -1259,9 +1371,7 @@ describe('openWalletSession', () => { reason: 'open-wallet', deviceOnly: true, }); - expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', { - seed_domains: [], - }); + expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {}); }); test('selects a hidden wallet without exposing the internal device session', async () => { @@ -1294,10 +1404,9 @@ describe('openWalletSession', () => { expect(typedCall).toHaveBeenCalledWith('DeviceSessionAskPassphrase', 'Success', { passphrase: 'host hidden wallet', on_device: false, + seed_domains: [DeviceSessionSeedDomain.SeedDomain_Standard], }); - expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', { - seed_domains: [], - }); + expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {}); expect(promptPassphrase).toHaveBeenCalled(); expect(device.passphraseState).toBeUndefined(); expect(deviceWalletSessionStore.get('device-1', 'hidden-state')).toBe('hidden-session'); @@ -1356,10 +1465,9 @@ describe('openWalletSession', () => { expect(typedCall).toHaveBeenCalledWith('DeviceSessionAskPassphrase', 'Success', { passphrase: 'host hidden wallet', on_device: false, + seed_domains: [DeviceSessionSeedDomain.SeedDomain_Standard], }); - expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', { - seed_domains: [], - }); + expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {}); }); test('selects an on-device passphrase wallet with an explicit on_device request', async () => { @@ -1404,10 +1512,9 @@ describe('openWalletSession', () => { }); expect(typedCall).toHaveBeenNthCalledWith(2, 'DeviceSessionAskPassphrase', 'Success', { on_device: true, + seed_domains: [DeviceSessionSeedDomain.SeedDomain_Standard], }); - expect(typedCall).toHaveBeenNthCalledWith(3, 'DeviceSessionGet', 'DeviceSession', { - seed_domains: [], - }); + expect(typedCall).toHaveBeenNthCalledWith(3, 'DeviceSessionGet', 'DeviceSession', {}); expect(device.createProtocolV2UiPhaseMetadata).toHaveBeenNthCalledWith( 2, 'passphrase-on-device', @@ -1458,10 +1565,9 @@ describe('openWalletSession', () => { expect(typedCall).toHaveBeenNthCalledWith(2, 'DeviceSessionAskPassphrase', 'Success', { passphrase: 'host hidden wallet', on_device: false, + seed_domains: [DeviceSessionSeedDomain.SeedDomain_Standard], }); - expect(typedCall).toHaveBeenNthCalledWith(3, 'DeviceSessionGet', 'DeviceSession', { - seed_domains: [], - }); + expect(typedCall).toHaveBeenNthCalledWith(3, 'DeviceSessionGet', 'DeviceSession', {}); }); test('normalizes a Unicode Host passphrase before sending it to Pro2 firmware', async () => { @@ -1483,6 +1589,7 @@ describe('openWalletSession', () => { expect(typedCall).toHaveBeenNthCalledWith(2, 'DeviceSessionAskPassphrase', 'Success', { passphrase: 'cafe\u0301', on_device: false, + seed_domains: [DeviceSessionSeedDomain.SeedDomain_Standard], }); }); @@ -1506,6 +1613,7 @@ describe('openWalletSession', () => { expect(typedCall).toHaveBeenNthCalledWith(2, 'DeviceSessionAskPassphrase', 'Success', { passphrase, on_device: false, + seed_domains: [DeviceSessionSeedDomain.SeedDomain_Standard], }); }); @@ -1541,9 +1649,7 @@ describe('openWalletSession', () => { expect(device.unlockDevice).toHaveBeenCalledWith(DeviceSessionPinType.AttachToPin, { emitUiEvent: false, }); - expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', { - seed_domains: [], - }); + expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {}); }); test('uses the complete Attach PIN wire flow without a main PIN unlock', async () => { @@ -1572,7 +1678,7 @@ describe('openWalletSession', () => { ['ProtocolInfoRequest', 'ProtocolInfo', { eventless_wallet_session: true }], ['DeviceSessionAskPin', 'Success', { type: DeviceSessionPinType.AttachToPin }], ['DeviceStatusGet', 'DeviceStatus', {}], - ['DeviceSessionGet', 'DeviceSession', { seed_domains: [] }], + ['DeviceSessionGet', 'DeviceSession', {}], ]); expect(device.commands.typedCall).not.toHaveBeenCalledWith('DeviceSessionAskPin', 'Success', { type: DeviceSessionPinType.Main, @@ -1784,7 +1890,6 @@ describe('openWalletSession', () => { expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', { session_id: 'known-session', btc_test_address: 'hidden-state', - seed_domains: [], }); expect(promptPassphrase).not.toHaveBeenCalled(); }); @@ -1844,7 +1949,6 @@ describe('openWalletSession', () => { expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', { session_id: 'known-session', btc_test_address: 'hidden-state', - seed_domains: [], }); const sessionGetCall = typedCall.mock.calls.findIndex( ([requestName]) => requestName === 'DeviceSessionGet' @@ -1942,7 +2046,6 @@ describe('openWalletSession', () => { expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', { session_id: 'expired-session', btc_test_address: 'hidden-state', - seed_domains: [], }); expect(promptPassphrase).toHaveBeenCalledTimes(1); expect(deviceWalletSessionStore.get('device-1', 'hidden-state')).toBe('renewed-session'); @@ -1980,7 +2083,6 @@ describe('openWalletSession', () => { }); expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', { btc_test_address: 'hidden-state', - seed_domains: [], }); expect(promptPassphrase).not.toHaveBeenCalled(); expect(deviceWalletSessionStore.get('device-1', 'hidden-state')).toBe('new-session'); @@ -1995,6 +2097,7 @@ describe('openWalletSession', () => { message: { btc_test_address: 'hidden-state', session_id: 'new-session', + seed_domains: deriveCardano ? CARDANO_SEED_DOMAINS : STANDARD_SEED_DOMAINS, }, }); const method = new OpenWalletSession({ @@ -2015,11 +2118,405 @@ describe('openWalletSession', () => { walletType: 'hidden', passphraseState: 'hidden-state', }); + expect(typedCall).toHaveBeenCalledWith('DeviceSessionAskPassphrase', 'Success', { + passphrase: 'host hidden wallet', + on_device: false, + seed_domains: deriveCardano ? CARDANO_SEED_DOMAINS : STANDARD_SEED_DOMAINS, + }); + expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {}); + }); + + test('requests Cardano seed domains when a V2 session rebuild has Cardano intent', async () => { + const typedCall = jest.fn((request: string) => { + if (request === 'ProtocolInfoRequest') { + return { message: { version: 2 } }; + } + if (request === 'DeviceSessionAskPassphrase') { + return { message: {} }; + } + if (request === 'DeviceSessionGet') { + return { + message: { + btc_test_address: 'hidden-state', + session_id: 'new-session', + seed_domains: [ + DeviceSessionSeedDomain.SeedDomain_Standard, + DeviceSessionSeedDomain.SeedDomain_Cardano, + ], + }, + }; + } + throw new Error(`Unexpected request: ${request}`); + }); + const device = createDevice({ + typedCall, + promptPassphrase: jest.fn().mockResolvedValue({ passphrase: 'host hidden wallet' }), + }); + + await expect( + getProtocolV2WalletSession(device as any, { + forceWalletSelection: true, + deriveCardano: true, + }) + ).resolves.toMatchObject({ + passphraseState: 'hidden-state', + newSession: 'new-session', + }); + expect(typedCall).toHaveBeenCalledWith('DeviceSessionAskPassphrase', 'Success', { + passphrase: 'host hidden wallet', + on_device: false, + seed_domains: CARDANO_SEED_DOMAINS, + }); + expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {}); + }); + + test('asks Cardano seed domains after Get reports a Standard-only session', async () => { + const typedCall = jest.fn((request: string) => { + if (request === 'ProtocolInfoRequest') { + return { message: { version: 2 } }; + } + if (request === 'DeviceSessionAskPassphrase') { + return { message: {} }; + } + if (request === 'DeviceSessionGet') { + const hasCardanoAsk = typedCall.mock.calls.some( + call => + call[0] === 'DeviceSessionAskPassphrase' && + Array.isArray(call[2]?.seed_domains) && + call[2].seed_domains.includes(DeviceSessionSeedDomain.SeedDomain_Cardano) + ); + return { + message: { + btc_test_address: 'hidden-state', + session_id: 'new-session', + seed_domains: hasCardanoAsk + ? [ + DeviceSessionSeedDomain.SeedDomain_Standard, + DeviceSessionSeedDomain.SeedDomain_Cardano, + ] + : [DeviceSessionSeedDomain.SeedDomain_Standard], + }, + }; + } + throw new Error(`Unexpected request: ${request}`); + }); + const device = createDevice({ + typedCall, + promptPassphrase: jest.fn().mockResolvedValue({ passphrase: 'host hidden wallet' }), + }); + device.passphraseState = 'hidden-state'; + deviceWalletSessionStore.set('device-1', 'hidden-state', 'cached-session'); + + await getProtocolV2WalletSession(device as any, { + expectedPassphraseState: 'hidden-state', + deriveCardano: true, + }); + expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', { + session_id: 'cached-session', + btc_test_address: 'hidden-state', + }); + expect(typedCall).toHaveBeenCalledWith('DeviceSessionAskPassphrase', 'Success', { + passphrase: 'host hidden wallet', + on_device: false, seed_domains: [ DeviceSessionSeedDomain.SeedDomain_Standard, - ...(deriveCardano ? [DeviceSessionSeedDomain.SeedDomain_Cardano] : []), + DeviceSessionSeedDomain.SeedDomain_Cardano, ], }); }); + + test('uses empty AskPassphrase to add Cardano on an Attach PIN session', async () => { + const typedCall = jest.fn((request: string) => { + if (request === 'ProtocolInfoRequest') { + return { message: { version: 2 } }; + } + if (request === 'DeviceSessionAskPassphrase') { + return { message: {} }; + } + if (request === 'DeviceSessionGet') { + const askedCardano = typedCall.mock.calls.some( + call => + call[0] === 'DeviceSessionAskPassphrase' && + Array.isArray(call[2]?.seed_domains) && + call[2].seed_domains.includes(DeviceSessionSeedDomain.SeedDomain_Cardano) + ); + return { + message: { + btc_test_address: 'attach-state', + session_id: 'attach-session', + seed_domains: askedCardano ? CARDANO_SEED_DOMAINS : STANDARD_SEED_DOMAINS, + }, + }; + } + throw new Error(`Unexpected request: ${request}`); + }); + const promptPassphrase = jest.fn().mockResolvedValue({ passphrase: 'should-not-ask' }); + const device = createDevice({ + typedCall, + promptPassphrase, + unlockedAttachPin: true, + }); + + await expect( + getProtocolV2WalletSession(device as any, { + readCurrentAttachPinSession: true, + deriveCardano: true, + }) + ).resolves.toMatchObject({ + passphraseState: 'attach-state', + newSession: 'attach-session', + unlockedAttachPin: true, + }); + expect(promptPassphrase).not.toHaveBeenCalled(); + expect(typedCall).toHaveBeenCalledWith('DeviceSessionAskPassphrase', 'Success', { + passphrase: '', + on_device: false, + seed_domains: CARDANO_SEED_DOMAINS, + }); + expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {}); + expect( + typedCall.mock.calls + .filter(call => call[0] === 'DeviceSessionGet') + .every(call => !('seed_domains' in (call[2] ?? {}))) + ).toBe(true); + }); + + test('uses empty AskPassphrase after selecting Attach PIN for Cardano intent', async () => { + const typedCall = jest.fn((request: string) => { + if (request === 'ProtocolInfoRequest') { + return { message: { version: 2 } }; + } + if (request === 'DeviceSessionAskPassphrase') { + return { message: {} }; + } + if (request === 'DeviceSessionGet') { + return { + message: { + btc_test_address: 'attach-state', + session_id: 'attach-session', + seed_domains: CARDANO_SEED_DOMAINS, + }, + }; + } + throw new Error(`Unexpected request: ${request}`); + }); + const promptPassphrase = jest.fn().mockResolvedValue({ attachPinOnDevice: true }); + const device = createDevice({ typedCall, promptPassphrase }); + device.features.attachToPinEnabled = true; + + await expect( + getProtocolV2WalletSession(device as any, { + forceWalletSelection: true, + deriveCardano: true, + }) + ).resolves.toMatchObject({ + passphraseState: 'attach-state', + newSession: 'attach-session', + unlockedAttachPin: true, + }); + expect(promptPassphrase).toHaveBeenCalledTimes(1); + expect(device.unlockDevice).toHaveBeenCalledWith( + DeviceSessionPinType.AttachToPin, + expect.objectContaining({ emitUiEvent: false }) + ); + expect(typedCall).toHaveBeenCalledWith('DeviceSessionAskPassphrase', 'Success', { + passphrase: '', + on_device: false, + seed_domains: CARDANO_SEED_DOMAINS, + }); + expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {}); + expect( + typedCall.mock.calls + .filter(call => call[0] === 'DeviceSessionGet') + .every(call => !('seed_domains' in (call[2] ?? {}))) + ).toBe(true); + }); + + test('still locks before a passphrase picker after Attach PIN Cardano empty Ask', async () => { + const typedCall = jest.fn((request: string) => { + if (request === 'ProtocolInfoRequest') { + return { message: { version: 2 } }; + } + if (request === 'DeviceSessionAskPassphrase') { + return { message: {} }; + } + if (request === 'DeviceSessionGet') { + return { + message: { + btc_test_address: 'attach-state', + session_id: 'attach-session', + seed_domains: CARDANO_SEED_DOMAINS, + }, + }; + } + throw new Error(`Unexpected request: ${request}`); + }); + const promptPassphrase = jest.fn().mockResolvedValue({ passphrase: 'should-not-ask' }); + const device = createDevice({ + typedCall, + promptPassphrase, + unlockedAttachPin: true, + }); + + await getProtocolV2WalletSession(device as any, { + readCurrentAttachPinSession: true, + deriveCardano: true, + }); + await expect( + getProtocolV2WalletSession(device as any, { forceWalletSelection: true }) + ).rejects.toMatchObject({ + errorCode: HardwareErrorCode.DeviceCheckUnlockTypeError, + }); + expect(device.lockDevice).toHaveBeenCalled(); + expect(promptPassphrase).not.toHaveBeenCalled(); + }); + + test('uses a second Get for Cardano when passphrase protection is off', async () => { + const typedCall = jest.fn((request: string) => { + if (request === 'ProtocolInfoRequest') { + return { message: { version: 2 } }; + } + if (request === 'DeviceSessionGet') { + const getCount = typedCall.mock.calls.filter(call => call[0] === 'DeviceSessionGet').length; + return { + message: { + btc_test_address: 'standard-state', + session_id: 'standard-session', + seed_domains: getCount > 1 ? CARDANO_SEED_DOMAINS : STANDARD_SEED_DOMAINS, + }, + }; + } + throw new Error(`Unexpected request: ${request}`); + }); + const device = createDevice({ + typedCall, + passphraseProtection: false, + }); + + await expect( + getProtocolV2WalletSession(device as any, { + onlyMainPin: true, + deriveCardano: true, + }) + ).resolves.toMatchObject({ + passphraseState: 'standard-state', + newSession: 'standard-session', + }); + expect(typedCall).not.toHaveBeenCalledWith( + 'DeviceSessionAskPassphrase', + 'Success', + expect.anything() + ); + expect(typedCall.mock.calls.filter(call => call[0] === 'DeviceSessionGet')).toHaveLength(2); + expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {}); + }); + + test('fails closed when Cardano fallback still lacks Cardano', async () => { + const typedCall = jest.fn((request: string) => { + if (request === 'ProtocolInfoRequest') { + return { message: { version: 2 } }; + } + if (request === 'DeviceSessionAskPassphrase') { + return { message: {} }; + } + if (request === 'DeviceSessionGet') { + return { + message: { + btc_test_address: 'hidden-state', + session_id: 'hidden-session', + seed_domains: STANDARD_SEED_DOMAINS, + }, + }; + } + throw new Error(`Unexpected request: ${request}`); + }); + const device = createDevice({ + typedCall, + promptPassphrase: jest.fn().mockResolvedValue({ passphrase: 'host hidden wallet' }), + }); + deviceWalletSessionStore.set('device-1', 'hidden-state', 'cached-session'); + + await expect( + getProtocolV2WalletSession(device as any, { + expectedPassphraseState: 'hidden-state', + deriveCardano: true, + }) + ).rejects.toMatchObject({ + errorCode: HardwareErrorCode.WalletSessionInvalid, + }); + expect(device.clearInternalState).toHaveBeenCalled(); + }); + + test('locks before selecting a passphrase wallet from an Attach PIN session', async () => { + const typedCall = jest.fn((request: string) => { + if (request === 'ProtocolInfoRequest') { + return { message: { version: 2 } }; + } + if (request === 'DeviceStatusGet') { + return { + message: { + device_id: 'device-1', + unlocked: true, + passphrase_enabled: true, + unlocked_by_attach_to_pin: true, + }, + }; + } + throw new Error(`Unexpected request: ${request}`); + }); + const promptPassphrase = jest.fn().mockResolvedValue({ passphrase: 'should-not-ask' }); + const device = createDevice({ + typedCall, + promptPassphrase, + unlockedAttachPin: true, + }); + + await expect( + getProtocolV2WalletSession(device as any, { forceWalletSelection: true }) + ).rejects.toMatchObject({ + errorCode: HardwareErrorCode.DeviceCheckUnlockTypeError, + }); + expect(device.lockDevice).toHaveBeenCalled(); + expect(promptPassphrase).not.toHaveBeenCalled(); + expect(typedCall).not.toHaveBeenCalledWith( + 'DeviceSessionAskPassphrase', + 'Success', + expect.anything() + ); + }); + + test('asks Standard-only seed domains when deriveCardano is false', async () => { + const typedCall = jest.fn((request: string) => { + if (request === 'ProtocolInfoRequest') { + return { message: { version: 2 } }; + } + if (request === 'DeviceSessionAskPassphrase') { + return { message: {} }; + } + if (request === 'DeviceSessionGet') { + return { + message: { + btc_test_address: 'hidden-state', + session_id: 'new-session', + }, + }; + } + throw new Error(`Unexpected request: ${request}`); + }); + const device = createDevice({ + typedCall, + promptPassphrase: jest.fn().mockResolvedValue({ passphrase: 'host hidden wallet' }), + }); + + await getProtocolV2WalletSession(device as any, { + forceWalletSelection: true, + deriveCardano: false, + }); + expect(typedCall).toHaveBeenCalledWith('DeviceSessionAskPassphrase', 'Success', { + passphrase: 'host hidden wallet', + on_device: false, + seed_domains: [DeviceSessionSeedDomain.SeedDomain_Standard], + }); + }); }); diff --git a/packages/core/__tests__/protocol-v2.test.ts b/packages/core/__tests__/protocol-v2.test.ts index a734f65ad..d96df1915 100644 --- a/packages/core/__tests__/protocol-v2.test.ts +++ b/packages/core/__tests__/protocol-v2.test.ts @@ -7,6 +7,7 @@ import { encode as encodeJpeg } from 'jpeg-js'; import { DeviceRebootType, DeviceSessionPinType, + DeviceSessionSeedDomain, DeviceSettingsPage, DeviceType, } from '@onekeyfe/hd-transport'; @@ -1033,10 +1034,9 @@ describe('Protocol V2 feature adapter', () => { expect(typedCall).toHaveBeenCalledWith('DeviceSessionAskPassphrase', 'Success', { passphrase: '', on_device: false, + seed_domains: [DeviceSessionSeedDomain.SeedDomain_Standard], }); - expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', { - seed_domains: [], - }); + expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {}); device.passphraseState = 'state-1'; expect(device.getInternalState()).toBe('session-1'); }); @@ -1089,13 +1089,10 @@ describe('Protocol V2 feature adapter', () => { expect( typedCall.mock.calls.filter(call => call[0] === 'DeviceSessionAskPassphrase') ).toHaveLength(1); - expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', { - seed_domains: [], - }); + expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {}); expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', { session_id: 'standard-session', btc_test_address: 'standard-state', - seed_domains: [], }); expect(deviceWalletSessionStore.getStandard(deviceId)).toEqual({ passphraseState: 'standard-state', @@ -1460,10 +1457,9 @@ describe('Protocol V2 feature adapter', () => { expect(typedCall).toHaveBeenCalledWith('DeviceSessionAskPassphrase', 'Success', { passphrase: 'host hidden wallet', on_device: false, + seed_domains: [DeviceSessionSeedDomain.SeedDomain_Standard], }); - expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', { - seed_domains: [], - }); + expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {}); }); test('deviceStatusGet returns raw DeviceStatus and updates dynamic features', async () => { @@ -1572,7 +1568,6 @@ describe('Protocol V2 feature adapter', () => { expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', { session_id: 'session-a', btc_test_address: 'state-a', - seed_domains: [], }); expect(device.getInternalState()).toBe('session-b'); }); @@ -1646,7 +1641,6 @@ describe('Protocol V2 feature adapter', () => { { session_id: 'session-a', btc_test_address: 'state-a', - seed_domains: [], }, ], ]); @@ -1693,10 +1687,14 @@ describe('Protocol V2 feature adapter', () => { [ 'DeviceSessionAskPassphrase', 'Success', - { passphrase: 'host hidden wallet', on_device: false }, + { + passphrase: 'host hidden wallet', + on_device: false, + seed_domains: [DeviceSessionSeedDomain.SeedDomain_Standard], + }, ], ['DeviceStatusGet', 'DeviceStatus', {}], - ['DeviceSessionGet', 'DeviceSession', { seed_domains: [] }], + ['DeviceSessionGet', 'DeviceSession', {}], ]); }); @@ -1858,7 +1856,6 @@ describe('Protocol V2 feature adapter', () => { expect(promptPassphrase).not.toHaveBeenCalled(); expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', { btc_test_address: 'expected-state', - seed_domains: [], }); expect(typedCall).toHaveBeenCalledWith('LockDevice', 'Success', {}); expect(typedCall.mock.calls.filter(call => call[0] === 'DeviceSessionGet')).toHaveLength(1); @@ -2156,7 +2153,7 @@ describe('Protocol V2 feature adapter', () => { firmwareVersion: '4.15.0', passphraseProtection: true, sessionId: 'feature-session', - unlockedAttachPin: true, + unlockedAttachPin: false, }; const typedCall = jest .fn() @@ -2249,7 +2246,6 @@ describe('Protocol V2 feature adapter', () => { ).resolves.toMatchObject({ passphraseState: 'expected-state' }); expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', { btc_test_address: 'expected-state', - seed_domains: [], }); expect(promptPassphrase).not.toHaveBeenCalled(); }); @@ -2323,7 +2319,6 @@ describe('Protocol V2 feature adapter', () => { { session_id: 'session-pro2-app', btc_test_address: 'state-pro2-app', - seed_domains: [], }, ], ]); @@ -2513,10 +2508,9 @@ describe('Protocol V2 feature adapter', () => { expect(typedCall).toHaveBeenNthCalledWith(2, 'DeviceSessionAskPassphrase', 'Success', { passphrase: 'host hidden wallet', on_device: false, + seed_domains: [DeviceSessionSeedDomain.SeedDomain_Standard], }); - expect(typedCall).toHaveBeenLastCalledWith('DeviceSessionGet', 'DeviceSession', { - seed_domains: [], - }); + expect(typedCall).toHaveBeenLastCalledWith('DeviceSessionGet', 'DeviceSession', {}); }); test('does not mark Pro2 passphrase enabled from a main PIN session alone', async () => { @@ -2596,16 +2590,14 @@ describe('Protocol V2 feature adapter', () => { expect(device.getInternalState()).toBeUndefined(); expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', { btc_test_address: 'expected-state', - seed_domains: [], }); expect(typedCall).toHaveBeenCalledWith('DeviceSessionAskPassphrase', 'Success', { passphrase: 'host hidden wallet', on_device: false, + seed_domains: [DeviceSessionSeedDomain.SeedDomain_Standard], }); expect(typedCall).toHaveBeenCalledWith('DeviceStatusGet', 'DeviceStatus', {}); - expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', { - seed_domains: [], - }); + expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {}); }); test('fails closed instead of switching to Main PIN during a standard-wallet safety check', async () => { @@ -2703,20 +2695,19 @@ describe('Protocol V2 feature adapter', () => { await expect( device.checkPassphraseStateSafety('stale-hidden-state', true, false) ).resolves.toBe(true); - expect(typedCall).toHaveBeenCalledTimes(5); + expect(typedCall).toHaveBeenCalledTimes(6); expect(typedCall).toHaveBeenCalledWith('ProtocolInfoRequest', 'ProtocolInfo', { eventless_wallet_session: true, }); - expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', { - seed_domains: [], - }); + expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {}); expect(typedCall).toHaveBeenCalledWith('DeviceSessionAskPassphrase', 'Success', { passphrase: '', on_device: false, + seed_domains: [DeviceSessionSeedDomain.SeedDomain_Standard], }); expect(typedCall).toHaveBeenCalledWith('DeviceStatusGet', 'DeviceStatus', {}); expect(typedCall.mock.calls.filter(([request]) => request === 'DeviceStatusGet')).toHaveLength( - 2 + 3 ); expect(typedCall).not.toHaveBeenCalledWith('DeviceSessionAskPin', 'Success', expect.anything()); }); diff --git a/packages/core/package.json b/packages/core/package.json index afe50f023..78ca5f818 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/hd-core", - "version": "1.2.1", + "version": "1.2.2", "description": "Core processes and APIs for communicating with OneKey hardware devices.", "author": "OneKey", "homepage": "https://github.com/OneKeyHQ/hardware-js-sdk#readme", @@ -25,8 +25,8 @@ "url": "https://github.com/OneKeyHQ/hardware-js-sdk/issues" }, "dependencies": { - "@onekeyfe/hd-shared": "1.2.1", - "@onekeyfe/hd-transport": "1.2.1", + "@onekeyfe/hd-shared": "1.2.2", + "@onekeyfe/hd-transport": "1.2.2", "axios": "1.15.2", "bignumber.js": "^9.0.2", "buffer": "^6.0.3", diff --git a/packages/core/src/data/messages/messages-protocol-v2.json b/packages/core/src/data/messages/messages-protocol-v2.json index fbe2a142e..5647eafa1 100644 --- a/packages/core/src/data/messages/messages-protocol-v2.json +++ b/packages/core/src/data/messages/messages-protocol-v2.json @@ -12322,11 +12322,6 @@ "btc_test_address": { "type": "string", "id": 2 - }, - "seed_domains": { - "rule": "repeated", - "type": "DeviceSessionSeedDomain", - "id": 3 } } }, @@ -12339,6 +12334,11 @@ "btc_test_address": { "type": "string", "id": 2 + }, + "seed_domains": { + "rule": "repeated", + "type": "DeviceSessionSeedDomain", + "id": 3 } } }, @@ -12367,6 +12367,11 @@ "rule": "required", "type": "bool", "id": 2 + }, + "seed_domains": { + "rule": "repeated", + "type": "DeviceSessionSeedDomain", + "id": 3 } } }, diff --git a/packages/core/src/protocols/protocol-v2/walletSession.ts b/packages/core/src/protocols/protocol-v2/walletSession.ts index 0a414e1e2..a41a4a7e4 100644 --- a/packages/core/src/protocols/protocol-v2/walletSession.ts +++ b/packages/core/src/protocols/protocol-v2/walletSession.ts @@ -63,32 +63,41 @@ const negotiateEventlessWalletSession = async (device: Device) => { const getDeviceSession = async (device: Device, request: DeviceSessionGet) => device.commands.typedCall('DeviceSessionGet', 'DeviceSession', request); +const STANDARD_SEED_DOMAINS = [DeviceSessionSeedDomain.SeedDomain_Standard]; +const CARDANO_SEED_DOMAINS = [ + DeviceSessionSeedDomain.SeedDomain_Standard, + DeviceSessionSeedDomain.SeedDomain_Cardano, +]; + +const buildDeviceSessionSeedDomains = (deriveCardano?: boolean): DeviceSessionSeedDomain[] => + deriveCardano === true ? CARDANO_SEED_DOMAINS : STANDARD_SEED_DOMAINS; + +const deviceSessionHasCardano = (message: { seed_domains?: DeviceSessionSeedDomain[] }) => + Array.isArray(message.seed_domains) && + message.seed_domains.includes(DeviceSessionSeedDomain.SeedDomain_Cardano); + +// DeviceSessionGet only reads or resumes a session; AskPassphrase generates seed domains. const buildDeviceSessionGetRequest = ({ sessionId, expectedPassphraseState, - deriveCardano, }: { sessionId?: string; expectedPassphraseState?: string; - deriveCardano?: boolean; } = {}): DeviceSessionGet => ({ ...(sessionId ? { session_id: sessionId } : {}), ...(expectedPassphraseState ? { btc_test_address: expectedPassphraseState } : {}), - seed_domains: - deriveCardano === undefined - ? [] - : [ - DeviceSessionSeedDomain.SeedDomain_Standard, - ...(deriveCardano ? [DeviceSessionSeedDomain.SeedDomain_Cardano] : []), - ], }); const askDevicePassphrase = async ( device: Device, - requestPayload: DeviceSessionAskPassphrase, + requestPayload: Omit, + deriveCardano?: boolean, onStatusRefreshed?: () => void ) => { - await device.commands.typedCall('DeviceSessionAskPassphrase', 'Success', requestPayload); + await device.commands.typedCall('DeviceSessionAskPassphrase', 'Success', { + ...requestPayload, + seed_domains: buildDeviceSessionSeedDomains(deriveCardano), + }); await refreshProtocolV2DeviceStatus(device); onStatusRefreshed?.(); }; @@ -160,7 +169,22 @@ const selectDeviceSession = async ( interaction: attachPinInteraction, }); onStatusRefreshed?.(); - return getDeviceSession(device, buildDeviceSessionGetRequest({ deriveCardano })); + if (deriveCardano === true) { + await askDevicePassphrase( + device, + { passphrase: '', on_device: false }, + true, + onStatusRefreshed + ); + // Firmware AskPassphrase Success clears unlocked_by_attach_to_pin. + // Identity is still the Attach PIN wallet; keep the SDK flag so a later + // passphrase picker still locks instead of prompting. + if (device.features) { + device.features.unlockedAttachPin = true; + } + } + const attachPinSession = await getDeviceSession(device, buildDeviceSessionGetRequest()); + return Object.assign(attachPinSession, { viaAttachPin: true as const }); } if (hasHostPassphrase) { @@ -170,9 +194,10 @@ const selectDeviceSession = async ( passphrase: hostPassphrase, on_device: false, }, + deriveCardano, onStatusRefreshed ); - return getDeviceSession(device, buildDeviceSessionGetRequest({ deriveCardano })); + return getDeviceSession(device, buildDeviceSessionGetRequest()); } const passphraseOnDeviceInteraction = device.createProtocolV2UiPhaseMetadata?.( @@ -183,8 +208,8 @@ const selectDeviceSession = async ( ...metadata, ...(passphraseOnDeviceInteraction ? { interaction: passphraseOnDeviceInteraction } : {}), }); - await askDevicePassphrase(device, { on_device: true }, onStatusRefreshed); - return getDeviceSession(device, buildDeviceSessionGetRequest({ deriveCardano })); + await askDevicePassphrase(device, { on_device: true }, deriveCardano, onStatusRefreshed); + return getDeviceSession(device, buildDeviceSessionGetRequest()); }; export async function getProtocolV2WalletSession( @@ -209,6 +234,10 @@ export async function getProtocolV2WalletSession( const forceWalletSelection = options?.forceWalletSelection === true || options?.initSession === true; const readCurrentAttachPinSession = options?.readCurrentAttachPinSession === true; + const sessionIsAttachPinWallet = (session?: { viaAttachPin?: boolean }) => + readCurrentAttachPinSession || + session?.viaAttachPin === true || + device.features?.unlockedAttachPin === true; if (forceWalletSelection) { if (options.onlyMainPin) { @@ -244,6 +273,10 @@ export async function getProtocolV2WalletSession( const markWalletStatusRefreshed = () => { walletStatusRefreshed = true; }; + if (options?.onlyMainPin && options.mainPinSelected !== true) { + await refreshProtocolV2DeviceStatus(device); + markWalletStatusRefreshed(); + } let mainPinAuthenticated = options?.mainPinSelected === true || (options?.onlyMainPin === true && @@ -259,6 +292,26 @@ export async function getProtocolV2WalletSession( } }; + const askEmptyPassphraseAndGet = async ({ + deriveCardano, + keepAttachPin, + }: { + deriveCardano?: boolean; + keepAttachPin?: boolean; + } = {}) => { + await askDevicePassphrase( + device, + { passphrase: '', on_device: false }, + deriveCardano, + markWalletStatusRefreshed + ); + if (keepAttachPin && device.features) { + device.features.unlockedAttachPin = true; + } + const session = await getDeviceSession(device, buildDeviceSessionGetRequest()); + return keepAttachPin ? Object.assign(session, { viaAttachPin: true as const }) : session; + }; + const rejectMismatchedAttachPinWallet = async () => { const features = await refreshProtocolV2DeviceStatus(device); markWalletStatusRefreshed(); @@ -280,6 +333,19 @@ export async function getProtocolV2WalletSession( throw ERRORS.TypedError(HardwareErrorCode.DeviceCheckUnlockTypeError); }; + // The passphrase picker would prompt. Empty host AskPassphrase does not; + // that path is Attach PIN / standard Cardano. Switching to a different + // passphrase wallet still locks first. + const lockAttachPinBeforePassphraseSelection = async () => { + if (readCurrentAttachPinSession || options?.onlyMainPin) { + return; + } + if (device.features?.unlockedAttachPin !== true) { + return; + } + await rejectMismatchedAttachPinWallet(); + }; + if (options?.onlyMainPin && options.rejectAttachPinForMainWallet) { await rejectMismatchedAttachPinWallet(); } @@ -297,7 +363,7 @@ export async function getProtocolV2WalletSession( } }; - const selectStandardWallet = async () => { + const selectStandardWallet = async (forceMainPin = false) => { if (device.features?.passphraseProtection === true) { // Main PIN authenticates the device; an empty host passphrase selects the standard derivation. await selectMainPin(); @@ -307,13 +373,16 @@ export async function getProtocolV2WalletSession( passphrase: '', on_device: false, }, + options?.deriveCardano, markWalletStatusRefreshed ); standardWalletSelected = true; } else if (!standardWalletSelected) { // Without passphrase protection there is no empty-passphrase selector. - // Main PIN selection is the only authoritative switch back to the standard wallet. - await selectMainPin(true); + // An unlocked non-Attach-PIN device is already in the only available wallet context. + // Force Main PIN only when recovering from a mismatched cached standard session. + await selectMainPin(forceMainPin); + standardWalletSelected = true; } }; @@ -329,10 +398,7 @@ export async function getProtocolV2WalletSession( device.clearInternalState(); throw ERRORS.TypedError(HardwareErrorCode.DeviceCheckUnlockTypeError); } - response = await getDeviceSession( - device, - buildDeviceSessionGetRequest({ deriveCardano: options?.deriveCardano }) - ); + response = await getDeviceSession(device, buildDeviceSessionGetRequest()); } else if (options?.onlyMainPin) { expectedPassphraseState = cachedStandardSession?.passphraseState; if (cachedStandardSession) { @@ -345,7 +411,6 @@ export async function getProtocolV2WalletSession( buildDeviceSessionGetRequest({ sessionId: cachedStandardSession.sessionId, expectedPassphraseState, - deriveCardano: options?.deriveCardano, }) ); resumed = true; @@ -360,10 +425,7 @@ export async function getProtocolV2WalletSession( if (!response) { await selectStandardWallet(); - response = await getDeviceSession( - device, - buildDeviceSessionGetRequest({ deriveCardano: options?.deriveCardano }) - ); + response = await getDeviceSession(device, buildDeviceSessionGetRequest()); } } else if (cachedSessionId && expectedPassphraseState) { try { @@ -372,7 +434,6 @@ export async function getProtocolV2WalletSession( buildDeviceSessionGetRequest({ sessionId: cachedSessionId, expectedPassphraseState, - deriveCardano: options?.deriveCardano, }) ); resumed = true; @@ -389,7 +450,6 @@ export async function getProtocolV2WalletSession( device, buildDeviceSessionGetRequest({ expectedPassphraseState, - deriveCardano: options?.deriveCardano, }) ); } catch (error) { @@ -404,6 +464,7 @@ export async function getProtocolV2WalletSession( device.clearInternalState(); throw ERRORS.TypedError(HardwareErrorCode.WalletSessionInvalid); } + await lockAttachPinBeforePassphraseSelection(); response = await selectDeviceSession( device, expectedPassphraseState, @@ -432,13 +493,11 @@ export async function getProtocolV2WalletSession( } if (options?.onlyMainPin) { device.clearStandardInternalState?.(); - await selectStandardWallet(); - response = await getDeviceSession( - device, - buildDeviceSessionGetRequest({ deriveCardano: options?.deriveCardano }) - ); + await selectStandardWallet(true); + response = await getDeviceSession(device, buildDeviceSessionGetRequest()); } else { device.clearInternalState(); + await lockAttachPinBeforePassphraseSelection(); response = await selectDeviceSession( device, expectedPassphraseState, @@ -464,6 +523,54 @@ export async function getProtocolV2WalletSession( } } + // Firmware generates Cardano on AskPassphrase, not Get. Empty host + // passphrase is the Attach PIN / standard-wallet secret. Hidden wallets + // still need a real passphrase Ask. Passphrase-off Get auto-requests Cardano. + if (options?.deriveCardano === true && !deviceSessionHasCardano(message)) { + if (options?.resumeOnly) { + device.clearInternalState(); + throw ERRORS.TypedError(HardwareErrorCode.WalletSessionInvalid); + } + resumed = false; + const previousAddress = message.btc_test_address; + if (sessionIsAttachPinWallet(response)) { + response = await askEmptyPassphraseAndGet({ deriveCardano: true, keepAttachPin: true }); + } else if (device.features?.passphraseProtection === false) { + response = await getDeviceSession(device, buildDeviceSessionGetRequest()); + } else if (options?.onlyMainPin) { + await selectMainPin(); + response = await askEmptyPassphraseAndGet({ deriveCardano: true }); + } else { + await lockAttachPinBeforePassphraseSelection(); + response = await selectDeviceSession( + device, + expectedPassphraseState, + true, + markWalletStatusRefreshed + ); + } + message = response.message; + try { + assertCompleteDeviceSession(message); + } catch (error) { + if (options?.onlyMainPin) { + device.clearStandardInternalState?.(); + } else { + device.clearInternalState(); + } + throw error; + } + if (previousAddress && previousAddress !== message.btc_test_address) { + await rejectMismatchedAttachPinWallet(); + clearCurrentWalletSession(); + throw ERRORS.TypedError(HardwareErrorCode.DeviceCheckPassphraseStateError); + } + if (!deviceSessionHasCardano(message)) { + clearCurrentWalletSession(); + throw ERRORS.TypedError(HardwareErrorCode.WalletSessionInvalid); + } + } + const internalStateArgs = [ true, message.btc_test_address, @@ -478,7 +585,7 @@ export async function getProtocolV2WalletSession( } let unlockedAttachPin: boolean | undefined; - if (readCurrentAttachPinSession) { + if (readCurrentAttachPinSession || sessionIsAttachPinWallet(response)) { unlockedAttachPin = true; } else if (mainPinAuthenticated) { unlockedAttachPin = false; diff --git a/packages/hd-ble-sdk/package.json b/packages/hd-ble-sdk/package.json index aa5cae4b5..069c25fa8 100644 --- a/packages/hd-ble-sdk/package.json +++ b/packages/hd-ble-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/hd-ble-sdk", - "version": "1.2.1", + "version": "1.2.2", "author": "OneKey", "homepage": "https://github.com/OneKeyHQ/hardware-js-sdk#readme", "license": "ISC", @@ -20,8 +20,8 @@ "lint:fix": "eslint . --fix" }, "dependencies": { - "@onekeyfe/hd-core": "1.2.1", - "@onekeyfe/hd-shared": "1.2.1", - "@onekeyfe/hd-transport-react-native": "1.2.1" + "@onekeyfe/hd-core": "1.2.2", + "@onekeyfe/hd-shared": "1.2.2", + "@onekeyfe/hd-transport-react-native": "1.2.2" } } diff --git a/packages/hd-cli/package.json b/packages/hd-cli/package.json index 8345c6b87..e1db3dd48 100644 --- a/packages/hd-cli/package.json +++ b/packages/hd-cli/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/hardware-cli", - "version": "1.2.1", + "version": "1.2.2", "description": "OneKey hardware wallet CLI for testing device communication", "author": "OneKey", "license": "Apache-2.0", @@ -31,10 +31,10 @@ "test": "jest" }, "dependencies": { - "@onekeyfe/hd-common-connect-sdk": "1.2.1", - "@onekeyfe/hd-core": "1.2.1", - "@onekeyfe/hd-shared": "1.2.1", - "@onekeyfe/hd-transport-usb": "1.2.1", + "@onekeyfe/hd-common-connect-sdk": "1.2.2", + "@onekeyfe/hd-core": "1.2.2", + "@onekeyfe/hd-shared": "1.2.2", + "@onekeyfe/hd-transport-usb": "1.2.2", "@stoprocent/noble": "2.3.16", "commander": "^12.0.0" } diff --git a/packages/hd-common-connect-sdk/package.json b/packages/hd-common-connect-sdk/package.json index be928c7dd..8cbc5a465 100644 --- a/packages/hd-common-connect-sdk/package.json +++ b/packages/hd-common-connect-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/hd-common-connect-sdk", - "version": "1.2.1", + "version": "1.2.2", "author": "OneKey", "homepage": "https://github.com/OneKeyHQ/hardware-js-sdk#readme", "license": "ISC", @@ -20,12 +20,12 @@ "lint:fix": "eslint . --fix" }, "dependencies": { - "@onekeyfe/hd-core": "1.2.1", - "@onekeyfe/hd-shared": "1.2.1", - "@onekeyfe/hd-transport-emulator": "1.2.1", - "@onekeyfe/hd-transport-http": "1.2.1", - "@onekeyfe/hd-transport-lowlevel": "1.2.1", - "@onekeyfe/hd-transport-usb": "1.2.1", - "@onekeyfe/hd-transport-web-device": "1.2.1" + "@onekeyfe/hd-core": "1.2.2", + "@onekeyfe/hd-shared": "1.2.2", + "@onekeyfe/hd-transport-emulator": "1.2.2", + "@onekeyfe/hd-transport-http": "1.2.2", + "@onekeyfe/hd-transport-lowlevel": "1.2.2", + "@onekeyfe/hd-transport-usb": "1.2.2", + "@onekeyfe/hd-transport-web-device": "1.2.2" } } diff --git a/packages/hd-transport-electron/package.json b/packages/hd-transport-electron/package.json index acce46c65..aa5cbe804 100644 --- a/packages/hd-transport-electron/package.json +++ b/packages/hd-transport-electron/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/hd-transport-electron", - "version": "1.2.1", + "version": "1.2.2", "author": "OneKey", "homepage": "https://github.com/OneKeyHQ/hardware-js-sdk#readme", "license": "MIT", @@ -25,9 +25,9 @@ "electron-log": ">=4.0.0" }, "dependencies": { - "@onekeyfe/hd-core": "1.2.1", - "@onekeyfe/hd-shared": "1.2.1", - "@onekeyfe/hd-transport": "1.2.1", + "@onekeyfe/hd-core": "1.2.2", + "@onekeyfe/hd-shared": "1.2.2", + "@onekeyfe/hd-transport": "1.2.2", "@stoprocent/noble": "2.3.16", "p-retry": "^4.6.2" }, diff --git a/packages/hd-transport-emulator/package.json b/packages/hd-transport-emulator/package.json index 6a2da5a6d..8c51118ec 100644 --- a/packages/hd-transport-emulator/package.json +++ b/packages/hd-transport-emulator/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/hd-transport-emulator", - "version": "1.2.1", + "version": "1.2.2", "description": "hardware emulator transport", "author": "OneKey", "homepage": "https://github.com/OneKeyHQ/hardware-js-sdk#readme", @@ -24,8 +24,8 @@ "url": "https://github.com/OneKeyHQ/hardware-js-sdk/issues" }, "dependencies": { - "@onekeyfe/hd-shared": "1.2.1", - "@onekeyfe/hd-transport": "1.2.1", + "@onekeyfe/hd-shared": "1.2.2", + "@onekeyfe/hd-transport": "1.2.2", "axios": "1.15.2", "secure-json-parse": "^4.0.0" } diff --git a/packages/hd-transport-http/package.json b/packages/hd-transport-http/package.json index 2a510d203..dc120688b 100644 --- a/packages/hd-transport-http/package.json +++ b/packages/hd-transport-http/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/hd-transport-http", - "version": "1.2.1", + "version": "1.2.2", "description": "hardware http transport", "author": "OneKey", "homepage": "https://github.com/OneKeyHQ/hardware-js-sdk#readme", @@ -24,8 +24,8 @@ "url": "https://github.com/OneKeyHQ/hardware-js-sdk/issues" }, "dependencies": { - "@onekeyfe/hd-shared": "1.2.1", - "@onekeyfe/hd-transport": "1.2.1", + "@onekeyfe/hd-shared": "1.2.2", + "@onekeyfe/hd-transport": "1.2.2", "axios": "1.15.2", "secure-json-parse": "^4.0.0" } diff --git a/packages/hd-transport-lowlevel/package.json b/packages/hd-transport-lowlevel/package.json index 88a63b1e8..d28ae0c74 100644 --- a/packages/hd-transport-lowlevel/package.json +++ b/packages/hd-transport-lowlevel/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/hd-transport-lowlevel", - "version": "1.2.1", + "version": "1.2.2", "homepage": "https://github.com/OneKeyHQ/hardware-js-sdk#readme", "license": "MIT", "main": "dist/index.js", @@ -20,7 +20,7 @@ "lint:fix": "eslint . --fix" }, "dependencies": { - "@onekeyfe/hd-shared": "1.2.1", - "@onekeyfe/hd-transport": "1.2.1" + "@onekeyfe/hd-shared": "1.2.2", + "@onekeyfe/hd-transport": "1.2.2" } } diff --git a/packages/hd-transport-react-native/package.json b/packages/hd-transport-react-native/package.json index 021d9372c..5381cbbf0 100644 --- a/packages/hd-transport-react-native/package.json +++ b/packages/hd-transport-react-native/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/hd-transport-react-native", - "version": "1.2.1", + "version": "1.2.2", "homepage": "https://github.com/OneKeyHQ/hardware-js-sdk#readme", "license": "MIT", "main": "dist/index.js", @@ -20,9 +20,9 @@ "lint:fix": "eslint . --fix" }, "dependencies": { - "@onekeyfe/hd-core": "1.2.1", - "@onekeyfe/hd-shared": "1.2.1", - "@onekeyfe/hd-transport": "1.2.1", + "@onekeyfe/hd-core": "1.2.2", + "@onekeyfe/hd-shared": "1.2.2", + "@onekeyfe/hd-transport": "1.2.2", "@onekeyfe/react-native-ble-utils": "^0.1.6", "react-native-ble-plx": "3.5.1" } diff --git a/packages/hd-transport-usb/package.json b/packages/hd-transport-usb/package.json index ab34a51de..63efcc5c0 100644 --- a/packages/hd-transport-usb/package.json +++ b/packages/hd-transport-usb/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/hd-transport-usb", - "version": "1.2.1", + "version": "1.2.2", "description": "OneKey hardware wallet direct USB transport plugin (libusb)", "homepage": "https://github.com/OneKeyHQ/hardware-js-sdk#readme", "license": "MIT", @@ -21,8 +21,8 @@ "lint:fix": "eslint . --fix" }, "dependencies": { - "@onekeyfe/hd-shared": "1.2.1", - "@onekeyfe/hd-transport": "1.2.1", + "@onekeyfe/hd-shared": "1.2.2", + "@onekeyfe/hd-transport": "1.2.2", "bytebuffer": "^5.0.1", "usb": "^2.14.0" } diff --git a/packages/hd-transport-web-device/package.json b/packages/hd-transport-web-device/package.json index 4ef1f3c95..dd2dd56ac 100644 --- a/packages/hd-transport-web-device/package.json +++ b/packages/hd-transport-web-device/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/hd-transport-web-device", - "version": "1.2.1", + "version": "1.2.2", "author": "OneKey", "homepage": "https://github.com/OneKeyHQ/hardware-js-sdk#readme", "license": "MIT", @@ -21,11 +21,11 @@ "lint:fix": "eslint . --fix" }, "dependencies": { - "@onekeyfe/hd-shared": "1.2.1", - "@onekeyfe/hd-transport": "1.2.1" + "@onekeyfe/hd-shared": "1.2.2", + "@onekeyfe/hd-transport": "1.2.2" }, "devDependencies": { - "@onekeyfe/hd-transport-electron": "1.2.1", + "@onekeyfe/hd-transport-electron": "1.2.2", "@types/w3c-web-usb": "^1.0.6", "@types/web-bluetooth": "^0.0.17" } diff --git a/packages/hd-transport/__tests__/messages.test.js b/packages/hd-transport/__tests__/messages.test.js index a6d9b218f..4de827a92 100644 --- a/packages/hd-transport/__tests__/messages.test.js +++ b/packages/hd-transport/__tests__/messages.test.js @@ -150,11 +150,6 @@ describe('messages', () => { expect(v2Messages.nested.DeviceSessionGet.fields).toEqual({ session_id: { id: 1, type: 'bytes' }, btc_test_address: { id: 2, type: 'string' }, - seed_domains: { - id: 3, - rule: 'repeated', - type: 'DeviceSessionSeedDomain', - }, }); expect(v2Messages.nested.DeviceSessionSeedDomain.values).toEqual({ SeedDomain_Standard: 1, @@ -176,9 +171,14 @@ describe('messages', () => { expect(v2Messages.nested).not.toHaveProperty('DeviceWalletSelect'); expect(v2Messages.nested).not.toHaveProperty('DeviceWalletType'); expect(v2Messages.nested).not.toHaveProperty('DeviceHiddenWalletSelect'); - expect(v2Messages.nested.DeviceSession.fields).toMatchObject({ + expect(v2Messages.nested.DeviceSession.fields).toEqual({ session_id: { id: 1, type: 'bytes' }, btc_test_address: { id: 2, type: 'string' }, + seed_domains: { + rule: 'repeated', + type: 'DeviceSessionSeedDomain', + id: 3, + }, }); expect(v2Messages.nested.DeviceSessionAskPin.fields.type).toMatchObject({ id: 1, @@ -195,6 +195,11 @@ describe('messages', () => { type: 'bool', id: 2, }, + seed_domains: { + rule: 'repeated', + type: 'DeviceSessionSeedDomain', + id: 3, + }, }, }); expect(v2Messages.nested.DeviceSessionAskPin_FailureSubCodes.values).toEqual({ @@ -212,38 +217,77 @@ describe('messages', () => { const messages = parseConfigure(v2Messages); const { Message } = createMessageFromName(messages, 'DeviceSessionAskPassphrase'); - const standardWallet = encode(Message, { passphrase: '', on_device: false }); + const standardWallet = encode(Message, { + passphrase: '', + on_device: false, + seed_domains: [], + }); const onHost = Message.encode( - Message.create({ passphrase: 'host hidden wallet', on_device: false }) + Message.create({ + passphrase: 'host hidden wallet', + on_device: false, + seed_domains: [ + generatedTypes.DeviceSessionSeedDomain.SeedDomain_Standard, + generatedTypes.DeviceSessionSeedDomain.SeedDomain_Cardano, + ], + }) + ).finish(); + const onDevice = Message.encode( + Message.create({ + on_device: true, + seed_domains: [generatedTypes.DeviceSessionSeedDomain.SeedDomain_Standard], + }) ).finish(); - const onDevice = Message.encode(Message.create({ on_device: true })).finish(); expect(standardWallet.toString('hex')).toBe('0a001000'); expect(Buffer.from(onHost).toString('hex')).toBe( - '0a12686f73742068696464656e2077616c6c65741000' + '0a12686f73742068696464656e2077616c6c657410001a020102' ); - expect(Buffer.from(onDevice).toString('hex')).toBe('1001'); + expect(Buffer.from(onDevice).toString('hex')).toBe('10011a0101'); expect(Message.decode(onHost)).toMatchObject({ passphrase: 'host hidden wallet', on_device: false, + seed_domains: [ + generatedTypes.DeviceSessionSeedDomain.SeedDomain_Standard, + generatedTypes.DeviceSessionSeedDomain.SeedDomain_Cardano, + ], + }); + expect(Message.decode(onDevice)).toMatchObject({ + on_device: true, + seed_domains: [generatedTypes.DeviceSessionSeedDomain.SeedDomain_Standard], }); - expect(Message.decode(onDevice)).toMatchObject({ on_device: true }); }); - test('Protocol V2 wallet recovery carries the expected wallet and seed domains on wire', () => { + test('Protocol V2 wallet recovery carries the expected wallet on wire', () => { const messages = parseConfigure(v2Messages); const { Message } = createMessageFromName(messages, 'DeviceSessionGet'); const payload = encode(Message, { btc_test_address: 'tb1qwallet', - seed_domains: [ - generatedTypes.DeviceSessionSeedDomain.SeedDomain_Standard, - generatedTypes.DeviceSessionSeedDomain.SeedDomain_Cardano, - ], }); - expect(payload.toString('hex')).toBe('120a7462317177616c6c65741a020102'); + expect(payload.toString('hex')).toBe('120a7462317177616c6c6574'); expect(Message.decode(payload.toBuffer())).toMatchObject({ btc_test_address: 'tb1qwallet', + }); + expect(Message.decode(payload.toBuffer())).not.toHaveProperty('seed_domains'); + }); + + test('Protocol V2 DeviceSession reports generated seed domains on wire', () => { + const messages = parseConfigure(v2Messages); + const { Message } = createMessageFromName(messages, 'DeviceSession'); + const encoded = Message.encode( + Message.create({ + btc_test_address: 'tb1qwallet', + seed_domains: [ + generatedTypes.DeviceSessionSeedDomain.SeedDomain_Standard, + generatedTypes.DeviceSessionSeedDomain.SeedDomain_Cardano, + ], + }) + ).finish(); + + expect(Buffer.from(encoded).toString('hex')).toBe('120a7462317177616c6c65741a020102'); + expect(Message.decode(encoded)).toMatchObject({ + btc_test_address: 'tb1qwallet', seed_domains: [ generatedTypes.DeviceSessionSeedDomain.SeedDomain_Standard, generatedTypes.DeviceSessionSeedDomain.SeedDomain_Cardano, diff --git a/packages/hd-transport/messages-protocol-v2.json b/packages/hd-transport/messages-protocol-v2.json index fbe2a142e..5647eafa1 100644 --- a/packages/hd-transport/messages-protocol-v2.json +++ b/packages/hd-transport/messages-protocol-v2.json @@ -12322,11 +12322,6 @@ "btc_test_address": { "type": "string", "id": 2 - }, - "seed_domains": { - "rule": "repeated", - "type": "DeviceSessionSeedDomain", - "id": 3 } } }, @@ -12339,6 +12334,11 @@ "btc_test_address": { "type": "string", "id": 2 + }, + "seed_domains": { + "rule": "repeated", + "type": "DeviceSessionSeedDomain", + "id": 3 } } }, @@ -12367,6 +12367,11 @@ "rule": "required", "type": "bool", "id": 2 + }, + "seed_domains": { + "rule": "repeated", + "type": "DeviceSessionSeedDomain", + "id": 3 } } }, diff --git a/packages/hd-transport/package.json b/packages/hd-transport/package.json index 90bb2c8be..8ec8655ce 100644 --- a/packages/hd-transport/package.json +++ b/packages/hd-transport/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/hd-transport", - "version": "1.2.1", + "version": "1.2.2", "description": "Transport layer abstractions and utilities for OneKey hardware SDK.", "author": "OneKey", "homepage": "https://github.com/OneKeyHQ/hardware-js-sdk#readme", diff --git a/packages/hd-transport/src/types/messages.ts b/packages/hd-transport/src/types/messages.ts index 176331077..5feb8e49f 100644 --- a/packages/hd-transport/src/types/messages.ts +++ b/packages/hd-transport/src/types/messages.ts @@ -5032,13 +5032,13 @@ export enum DeviceSessionSeedDomain { export type DeviceSessionGet = { session_id?: string; btc_test_address?: string; - seed_domains: DeviceSessionSeedDomain[]; }; // DeviceSession export type DeviceSession = { session_id?: string; btc_test_address?: string; + seed_domains: DeviceSessionSeedDomain[]; }; export enum DeviceSessionPinType { @@ -5056,6 +5056,7 @@ export type DeviceSessionAskPin = { export type DeviceSessionAskPassphrase = { passphrase?: string; on_device: boolean; + seed_domains: DeviceSessionSeedDomain[]; }; export enum DeviceSessionAskPin_FailureSubCodes { diff --git a/packages/hd-web-sdk/package.json b/packages/hd-web-sdk/package.json index 7fc0ecb15..257a295ef 100644 --- a/packages/hd-web-sdk/package.json +++ b/packages/hd-web-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/hd-web-sdk", - "version": "1.2.1", + "version": "1.2.2", "author": "OneKey", "homepage": "https://github.com/OneKeyHQ/hardware-js-sdk#readme", "license": "ISC", @@ -23,10 +23,10 @@ }, "dependencies": { "@onekeyfe/cross-inpage-provider-core": "2.2.67", - "@onekeyfe/hd-core": "1.2.1", - "@onekeyfe/hd-shared": "1.2.1", - "@onekeyfe/hd-transport-http": "1.2.1", - "@onekeyfe/hd-transport-web-device": "1.2.1" + "@onekeyfe/hd-core": "1.2.2", + "@onekeyfe/hd-shared": "1.2.2", + "@onekeyfe/hd-transport-http": "1.2.2", + "@onekeyfe/hd-transport-web-device": "1.2.2" }, "devDependencies": { "@babel/plugin-proposal-optional-chaining": "^7.17.12", diff --git a/packages/hwk-adapter-core/package.json b/packages/hwk-adapter-core/package.json index 11c7e4269..53fbc8fbd 100644 --- a/packages/hwk-adapter-core/package.json +++ b/packages/hwk-adapter-core/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/hwk-adapter-core", - "version": "1.2.1", + "version": "1.2.2", "description": "Shared types and utilities for OneKey hardware wallet kit", "author": "OneKey", "license": "MIT", diff --git a/packages/hwk-ledger-adapter/package.json b/packages/hwk-ledger-adapter/package.json index 6355496e7..1bfa06c80 100644 --- a/packages/hwk-ledger-adapter/package.json +++ b/packages/hwk-ledger-adapter/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/hwk-ledger-adapter", - "version": "1.2.1", + "version": "1.2.2", "description": "Ledger hardware wallet adapter for OneKey", "author": "OneKey", "license": "MIT", @@ -50,7 +50,7 @@ "@ledgerhq/device-signer-kit-solana": "^1.7.0", "@ledgerhq/hw-app-trx": "^6.34.1", "@ledgerhq/hw-transport": "^6.34.1", - "@onekeyfe/hwk-adapter-core": "1.2.1", + "@onekeyfe/hwk-adapter-core": "1.2.2", "bitcoinjs-lib": "npm:@onekeyfe/bitcoinjs-lib@7.0.1" }, "devDependencies": { diff --git a/packages/hwk-ledger-connector-ble/package.json b/packages/hwk-ledger-connector-ble/package.json index d3acd8e1a..880fdcdea 100644 --- a/packages/hwk-ledger-connector-ble/package.json +++ b/packages/hwk-ledger-connector-ble/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/hwk-ledger-connector-ble", - "version": "1.2.1", + "version": "1.2.2", "description": "IConnector implementation for Ledger hardware wallets via React Native BLE", "author": "OneKey", "license": "MIT", @@ -51,8 +51,8 @@ "@ledgerhq/device-signer-kit-ethereum": "^1.9.0", "@ledgerhq/device-signer-kit-solana": "^1.7.0", "@ledgerhq/device-transport-kit-react-native-ble": "^1.0.0", - "@onekeyfe/hwk-adapter-core": "1.2.1", - "@onekeyfe/hwk-ledger-adapter": "1.2.1" + "@onekeyfe/hwk-adapter-core": "1.2.2", + "@onekeyfe/hwk-ledger-adapter": "1.2.2" }, "peerDependencies": { "react-native": "*" diff --git a/packages/hwk-ledger-connector-webhid/package.json b/packages/hwk-ledger-connector-webhid/package.json index e5e9b1392..181a44b5f 100644 --- a/packages/hwk-ledger-connector-webhid/package.json +++ b/packages/hwk-ledger-connector-webhid/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/hwk-ledger-connector-webhid", - "version": "1.2.1", + "version": "1.2.2", "description": "IConnector implementation for Ledger hardware wallets via WebHID (DMK)", "author": "OneKey", "license": "MIT", @@ -49,8 +49,8 @@ "@ledgerhq/device-signer-kit-ethereum": "^1.9.0", "@ledgerhq/device-signer-kit-solana": "^1.7.0", "@ledgerhq/device-transport-kit-web-hid": "^1.0.0", - "@onekeyfe/hwk-adapter-core": "1.2.1", - "@onekeyfe/hwk-ledger-adapter": "1.2.1" + "@onekeyfe/hwk-adapter-core": "1.2.2", + "@onekeyfe/hwk-ledger-adapter": "1.2.2" }, "devDependencies": { "rimraf": "^5.0.0", diff --git a/packages/hwk-trezor-adapter/package.json b/packages/hwk-trezor-adapter/package.json index c3865d5a6..95d6a5e2e 100644 --- a/packages/hwk-trezor-adapter/package.json +++ b/packages/hwk-trezor-adapter/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/hwk-trezor-adapter", - "version": "1.2.1", + "version": "1.2.2", "description": "Trezor hardware wallet adapter primitives for OneKey", "author": "OneKey", "license": "MIT", @@ -43,8 +43,8 @@ "crypto" ], "dependencies": { - "@onekeyfe/hwk-adapter-core": "1.2.1", - "@onekeyfe/hwk-trezor-core": "1.2.1", + "@onekeyfe/hwk-adapter-core": "1.2.2", + "@onekeyfe/hwk-trezor-core": "1.2.2", "tslib": "^2.6.2" }, "devDependencies": { diff --git a/packages/hwk-trezor-connector-electron-ble/package.json b/packages/hwk-trezor-connector-electron-ble/package.json index 0c6fda67b..502b8bec9 100644 --- a/packages/hwk-trezor-connector-electron-ble/package.json +++ b/packages/hwk-trezor-connector-electron-ble/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/hwk-trezor-connector-electron-ble", - "version": "1.2.1", + "version": "1.2.2", "description": "IConnector implementation for Trezor hardware wallets via Electron BLE (noble in main process)", "author": "OneKey", "license": "MIT", @@ -58,9 +58,9 @@ "connector" ], "dependencies": { - "@onekeyfe/hwk-adapter-core": "1.2.1", - "@onekeyfe/hwk-trezor-adapter": "1.2.1", - "@onekeyfe/hwk-trezor-connector": "1.2.1", + "@onekeyfe/hwk-adapter-core": "1.2.2", + "@onekeyfe/hwk-trezor-adapter": "1.2.2", + "@onekeyfe/hwk-trezor-connector": "1.2.2", "buffer": "^6.0.3" }, "peerDependencies": { diff --git a/packages/hwk-trezor-connector-rn-ble/package.json b/packages/hwk-trezor-connector-rn-ble/package.json index bd158e31f..55d64435d 100644 --- a/packages/hwk-trezor-connector-rn-ble/package.json +++ b/packages/hwk-trezor-connector-rn-ble/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/hwk-trezor-connector-rn-ble", - "version": "1.2.1", + "version": "1.2.2", "description": "IConnector implementation for Trezor hardware wallets via React Native BLE", "author": "OneKey", "license": "MIT", @@ -46,9 +46,9 @@ "connector" ], "dependencies": { - "@onekeyfe/hwk-adapter-core": "1.2.1", - "@onekeyfe/hwk-trezor-adapter": "1.2.1", - "@onekeyfe/hwk-trezor-connector": "1.2.1", + "@onekeyfe/hwk-adapter-core": "1.2.2", + "@onekeyfe/hwk-trezor-adapter": "1.2.2", + "@onekeyfe/hwk-trezor-connector": "1.2.2", "buffer": "^6.0.3", "react-native-ble-plx": "^3.5.1" }, diff --git a/packages/hwk-trezor-connector-webusb/package.json b/packages/hwk-trezor-connector-webusb/package.json index 055e72213..43eb4f64d 100644 --- a/packages/hwk-trezor-connector-webusb/package.json +++ b/packages/hwk-trezor-connector-webusb/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/hwk-trezor-connector-webusb", - "version": "1.2.1", + "version": "1.2.2", "description": "IConnector implementation for Trezor hardware wallets via WebUSB", "author": "OneKey", "license": "MIT", @@ -55,8 +55,8 @@ "connector" ], "dependencies": { - "@onekeyfe/hwk-adapter-core": "1.2.1", - "@onekeyfe/hwk-trezor-connector": "1.2.1", + "@onekeyfe/hwk-adapter-core": "1.2.2", + "@onekeyfe/hwk-trezor-connector": "1.2.2", "buffer": "^6.0.3" }, "devDependencies": { diff --git a/packages/hwk-trezor-connector/package.json b/packages/hwk-trezor-connector/package.json index 3d3314ae1..a7e14c6b9 100644 --- a/packages/hwk-trezor-connector/package.json +++ b/packages/hwk-trezor-connector/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/hwk-trezor-connector", - "version": "1.2.1", + "version": "1.2.2", "description": "Shared Trezor connector base for Hardware Wallet Kit", "author": "OneKey", "license": "MIT", @@ -38,13 +38,13 @@ }, "sideEffects": false, "dependencies": { - "@onekeyfe/hwk-adapter-core": "1.2.1", - "@onekeyfe/hwk-trezor-core": "1.2.1" + "@onekeyfe/hwk-adapter-core": "1.2.2", + "@onekeyfe/hwk-trezor-core": "1.2.2" }, "devDependencies": { - "@onekeyfe/hwk-trezor-protobuf": "1.2.1", - "@onekeyfe/hwk-trezor-protocol": "1.2.1", - "@onekeyfe/hwk-trezor-transport": "1.2.1", + "@onekeyfe/hwk-trezor-protobuf": "1.2.2", + "@onekeyfe/hwk-trezor-protocol": "1.2.2", + "@onekeyfe/hwk-trezor-transport": "1.2.2", "rimraf": "^5.0.0", "tsup": "^8.0.0", "typescript": "5.1.6" diff --git a/packages/hwk-trezor-core/package.json b/packages/hwk-trezor-core/package.json index fe3a9ade3..fb11cc543 100644 --- a/packages/hwk-trezor-core/package.json +++ b/packages/hwk-trezor-core/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/hwk-trezor-core", - "version": "1.2.1", + "version": "1.2.2", "description": "Minimal Trezor core call layer for Hardware Wallet Kit", "author": "OneKey", "license": "MIT", @@ -41,9 +41,9 @@ "devDependencies": { "@noble/ciphers": "^1.3.0", "@noble/hashes": "^1.8.0", - "@onekeyfe/hwk-trezor-protobuf": "1.2.1", - "@onekeyfe/hwk-trezor-protocol": "1.2.1", - "@onekeyfe/hwk-trezor-transport": "1.2.1", + "@onekeyfe/hwk-trezor-protobuf": "1.2.2", + "@onekeyfe/hwk-trezor-protocol": "1.2.2", + "@onekeyfe/hwk-trezor-transport": "1.2.2", "buffer": "^6.0.3", "rimraf": "^5.0.0", "tsup": "^8.0.0", diff --git a/packages/hwk-trezor-protobuf/package.json b/packages/hwk-trezor-protobuf/package.json index 26b940bf8..a65e1d7f7 100644 --- a/packages/hwk-trezor-protobuf/package.json +++ b/packages/hwk-trezor-protobuf/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/hwk-trezor-protobuf", - "version": "1.2.1", + "version": "1.2.2", "private": true, "description": "Trezor protobuf helpers vendored from Trezor Suite MIT sources", "author": "OneKey", @@ -50,7 +50,7 @@ "sideEffects": false, "dependencies": { "@bufbuild/protobuf": "^2.11.0", - "@onekeyfe/hwk-trezor-schema-utils": "1.2.1" + "@onekeyfe/hwk-trezor-schema-utils": "1.2.2" }, "devDependencies": { "rimraf": "^5.0.0", diff --git a/packages/hwk-trezor-protocol/package.json b/packages/hwk-trezor-protocol/package.json index 160613cf8..891c081a4 100644 --- a/packages/hwk-trezor-protocol/package.json +++ b/packages/hwk-trezor-protocol/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/hwk-trezor-protocol", - "version": "1.2.1", + "version": "1.2.2", "private": true, "description": "Trezor transport protocol helpers vendored from Trezor Suite MIT sources", "author": "OneKey", diff --git a/packages/hwk-trezor-schema-utils/package.json b/packages/hwk-trezor-schema-utils/package.json index bc843a159..74264c9c9 100644 --- a/packages/hwk-trezor-schema-utils/package.json +++ b/packages/hwk-trezor-schema-utils/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/hwk-trezor-schema-utils", - "version": "1.2.1", + "version": "1.2.2", "private": true, "description": "Schema helpers vendored from Trezor Suite MIT sources for HWK Trezor support", "author": "OneKey", @@ -44,7 +44,7 @@ }, "sideEffects": false, "dependencies": { - "@onekeyfe/hwk-trezor-type-utils": "1.2.1", + "@onekeyfe/hwk-trezor-type-utils": "1.2.2", "@sinclair/typebox": "^0.34.49", "ts-mixer": "^6.0.4" }, diff --git a/packages/hwk-trezor-transport/package.json b/packages/hwk-trezor-transport/package.json index 59f8d8028..0dc3eefa8 100644 --- a/packages/hwk-trezor-transport/package.json +++ b/packages/hwk-trezor-transport/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/hwk-trezor-transport", - "version": "1.2.1", + "version": "1.2.2", "private": true, "description": "Low-level Trezor transport helpers vendored from Trezor Suite MIT sources", "author": "OneKey", @@ -49,10 +49,10 @@ }, "sideEffects": false, "dependencies": { - "@onekeyfe/hwk-trezor-protobuf": "1.2.1", - "@onekeyfe/hwk-trezor-protocol": "1.2.1", - "@onekeyfe/hwk-trezor-type-utils": "1.2.1", - "@onekeyfe/hwk-trezor-utils": "1.2.1", + "@onekeyfe/hwk-trezor-protobuf": "1.2.2", + "@onekeyfe/hwk-trezor-protocol": "1.2.2", + "@onekeyfe/hwk-trezor-type-utils": "1.2.2", + "@onekeyfe/hwk-trezor-utils": "1.2.2", "cross-fetch": "^4.1.0", "usb": "^2.17.0" }, diff --git a/packages/hwk-trezor-type-utils/package.json b/packages/hwk-trezor-type-utils/package.json index 7f8c61d51..5d459efd6 100644 --- a/packages/hwk-trezor-type-utils/package.json +++ b/packages/hwk-trezor-type-utils/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/hwk-trezor-type-utils", - "version": "1.2.1", + "version": "1.2.2", "private": true, "description": "Type helpers vendored from Trezor Suite MIT sources for HWK Trezor support", "author": "OneKey", diff --git a/packages/hwk-trezor-utils/package.json b/packages/hwk-trezor-utils/package.json index b19e8e741..794e869a7 100644 --- a/packages/hwk-trezor-utils/package.json +++ b/packages/hwk-trezor-utils/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/hwk-trezor-utils", - "version": "1.2.1", + "version": "1.2.2", "private": true, "description": "Utility helpers vendored from Trezor Suite MIT sources for HWK Trezor support", "author": "OneKey", @@ -39,7 +39,7 @@ }, "sideEffects": false, "dependencies": { - "@onekeyfe/hwk-trezor-type-utils": "1.2.1", + "@onekeyfe/hwk-trezor-type-utils": "1.2.2", "bignumber.js": "^9.3.1", "events": "^3.3.0" }, diff --git a/packages/shared/package.json b/packages/shared/package.json index f2e5e97db..8493c5da2 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/hd-shared", - "version": "1.2.1", + "version": "1.2.2", "description": "Hardware SDK's shared tool library", "keywords": [ "Hardware-SDK",