Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: TrezorHostProtocol #13542

Draft
wants to merge 14 commits into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/template-connect-test-params.yml
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,8 @@ jobs:
run: sed -i "/\"node\"/d" package.json
- if: ${{ inputs.testEnv == 'node' }}
run: yarn workspaces focus @trezor/connect
run: yarn install
run: yarn workspace @trezor/transport-bridge build:js

- if: ${{ inputs.disable_cache_tx == 'true' }}
run: echo "ADDITIONAL_ARGS=-c" >> "$GITHUB_ENV"
Expand Down
4 changes: 3 additions & 1 deletion docker/docker-compose.connect-test.yml
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
version: "3.9"
services:
trezor-user-env-unix:
image: ghcr.io/trezor/trezor-user-env:dadc035aa0c121140dba0991ec6abdb786e18406
image: ghcr.io/trezor/trezor-user-env:d029907396738bfff46cc58990aa503140f95f9a@sha256:f83c29a9b882aaf530c7b6f07aafabd650299213d438a70e93bcc4f4d634901e
environment:
- SDL_VIDEODRIVER=dummy
- XDG_RUNTIME_DIR=/var/tmp
volumes:
- ../:/trezor-suite
network_mode: host
1 change: 1 addition & 0 deletions packages/connect-iframe/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,7 @@ const shouldUiEventBeSentToHost = (message: CoreEventMessage) => {
DEVICE.CONNECT,
DEVICE.CONNECT_UNACQUIRED,
DEVICE.CHANGED,
DEVICE.TRANSPORT_STATE_CHANGED,
DEVICE.DISCONNECT,
DEVICE.BUTTON,
];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ export default {
},
{
// https://github.com/trezor/trezor-firmware/pull/2703/
rules: ['<2.8.2'],
rules: ['<2.9.9'],
success: false,
},
],
Expand Down
23 changes: 19 additions & 4 deletions packages/connect/e2e/common.setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,10 +121,11 @@ export const setup = async (
TrezorUserEnvLink.state = options;

// after all is done, start bridge again
await TrezorUserEnvLink.startBridge(
// @ts-expect-error
process.env.TESTS_TRANSPORT,
);
// await TrezorUserEnvLink.startBridge(
// // @ts-expect-error
// process.env.TESTS_TRANSPORT,
// );
await TrezorUserEnvLink.startBridge('node-bridge');
};

type InitParams = Partial<Parameters<typeof TrezorConnect.init>[0]> & { autoConfirm?: boolean };
Expand Down Expand Up @@ -171,17 +172,31 @@ export const initTrezorConnect = async (
});
}

// TrezorConnect.on('ui-request_passphrase', () => {
// TrezorConnect.uiResponse({
// type: 'ui-receive_passphrase',
// payload: { value: '' },
// });
// });

await TrezorConnect.init({
manifest: {
appUrl: 'tests.connect.trezor.io',
email: 'tests@connect.trezor.io',
},
transports: ['BridgeTransport'],
// transports: ['UdpTransport'],
debug: false,
popup: false,
pendingTransportEvent: true,
transportReconnect: false,
connectSrc: process.env.TREZOR_CONNECT_SRC, // custom source for karma tests
thp: {
hostName: 'TrezorConnect',
staticKeys: '0007070707070707070707070707070707070707070707070707070707070747',
knownCredentials: [],
pairingMethods: ['NoMethod'] as any,
},
...options,
});
};
Expand Down
150 changes: 150 additions & 0 deletions packages/connect/e2e/tests/device/thpPairing.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
import TrezorConnect, { ConnectSettings, Device } from '../../../src';
import { getController, initTrezorConnect, setup } from '../../common.setup';

describe('THP pairing', () => {
const controller = getController();

beforeAll(async () => {
await setup(controller, { mnemonic: 'mnemonic_all' });
});

afterEach(() => {
TrezorConnect.dispose();
});

const waitForDevice = async (settings: Partial<ConnectSettings['thp']>) => {
await initTrezorConnect(controller, {
thp: {
hostName: 'TrezorConnect tests:e2e',
staticKeys: '0007070707070707070707070707070707070707070707070707070707070747',
knownCredentials: [],
pairingMethods: [],
...settings,
},
});

return new Promise<Device>((resolve, reject) => {
const onDeviceConnected = (device: Device) => {
TrezorConnect.removeAllListeners('device-connect');
TrezorConnect.removeAllListeners('device-connect_unacquired');
if (device.type === 'unreadable') {
reject(new Error('Device unreadable'));
}
resolve(device);
};
TrezorConnect.on('device-connect', onDeviceConnected);
TrezorConnect.on('device-connect_unacquired', onDeviceConnected);
});
};

it('ThpPairing SkipPairing', async () => {
const spy = jest.fn();
const device = await waitForDevice({ pairingMethods: ['SkipPairing'] });
TrezorConnect.on('ui-request_thp_pairing', spy);

const address = await TrezorConnect.getAddress({
device,
path: "m/44'/0'/0'/1/1",
showOnTrezor: true,
});
expect(address).toMatchObject({ success: true });
expect(spy).toHaveBeenCalledTimes(0);
});

it('ThpPairing CodeEntry', async () => {
const device = await waitForDevice({ pairingMethods: ['CodeEntry'] });

// eslint-disable-next-line @typescript-eslint/no-shadow
TrezorConnect.on('ui-request_thp_pairing', async ({ device }) => {
const state = await controller.getPairingInfo(device.protocolState.channel);
TrezorConnect.removeAllListeners('ui-request_thp_pairing');
TrezorConnect.uiResponse({
type: 'ui-receive_thp_pairing_tag',
payload: {
source: 'code-entry',
tag: state.code_entry_code,
},
});
});

const address = await TrezorConnect.getAddress({
device,
path: "m/44'/0'/0'/1/1",
// showOnTrezor: true,
});
expect(address).toMatchObject({ success: true });
});

it('ThpPairing QrCode', async () => {
const device = await waitForDevice({ pairingMethods: ['QrCode'] });

// eslint-disable-next-line @typescript-eslint/no-shadow
TrezorConnect.on('ui-request_thp_pairing', async ({ device }) => {
const state = await controller.getPairingInfo(device.protocolState.channel);
TrezorConnect.removeAllListeners('ui-request_thp_pairing');
TrezorConnect.uiResponse({
type: 'ui-receive_thp_pairing_tag',
payload: { source: 'qr-code', tag: state.code_qr_code },
});
});

const address = await TrezorConnect.getAddress({
device,
path: "m/44'/0'/0'/1/1",
// showOnTrezor: true,
});
expect(address).toMatchObject({ success: true });
});

it('ThpPairing NFC', async () => {
const device = await waitForDevice({ pairingMethods: ['NFC'] });

// eslint-disable-next-line @typescript-eslint/no-shadow
TrezorConnect.on('ui-request_thp_pairing', async ({ device, nfcData }) => {
// await new Promise(resolve => setTimeout(resolve, 1000));
const state = await controller.getPairingInfo(device.protocolState.channel, nfcData);
TrezorConnect.removeAllListeners('ui-request_thp_pairing');
TrezorConnect.uiResponse({
type: 'ui-receive_thp_pairing_tag',
payload: { source: 'nfc', tag: state.nfc_secret_trezor },
});
});

const address = await TrezorConnect.getAddress({
device,
path: "m/44'/0'/0'/1/1",
// showOnTrezor: true,
});
expect(address).toMatchObject({ success: true });
});

it('ThpPairing no matching method. device unreadable', async () => {
const device = await waitForDevice({
pairingMethods: ['FooBar', undefined, 1234, null, {}] as any,
});

// TODO: expect(device.type).toEqual('unreadable');
expect(device.type).toEqual('unacquired');
});

it('ThpPairing using known credentials', async () => {
const device = await waitForDevice({
pairingMethods: ['CodeEntry'],
knownCredentials: [
{
trezor_static_pubkey:
'38d6437ef1d67a4742265281de1e9a68df28774636f34b5e3e336d3ab90e671c',
credential:
'0a0f0a0d5472657a6f72436f6e6e6563741220f53793b13dffe2a4f01c2c7272aecc75b8596cf0fce4b09efd4fb353696a179b',
},
],
});

const address = await TrezorConnect.getAddress({
device,
path: "m/44'/0'/0'/1/1",
showOnTrezor: true,
});
expect(address).toMatchObject({ success: true });
});
});
54 changes: 41 additions & 13 deletions packages/connect/src/core/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -191,11 +191,11 @@ const MAX_PIN_TRIES = 3;
const getInvalidDeviceState = async (
{ sendCoreMessage }: CoreContext,
device: Device,
preauthorized?: boolean,
method: AbstractMethod<any>,
): Promise<StaticSessionId | undefined> => {
for (let i = 0; i < MAX_PIN_TRIES - 1; ++i) {
try {
return await device.validateState(preauthorized);
return await device.validateState(method.preauthorized, method.useCardanoDerivation);
} catch (error) {
if (error.message.includes('PIN invalid')) {
sendCoreMessage(
Expand All @@ -207,7 +207,7 @@ const getInvalidDeviceState = async (
}
}

return device.validateState(preauthorized);
return device.validateState(method.preauthorized, method.useCardanoDerivation);
};

/**
Expand Down Expand Up @@ -376,11 +376,7 @@ const inner = async (context: CoreContext, method: AbstractMethod<any>, device:
const isDeviceUnlocked = device.features.unlocked;
if (method.useDeviceState) {
try {
let invalidDeviceState = await getInvalidDeviceState(
context,
device,
method.preauthorized,
);
let invalidDeviceState = await getInvalidDeviceState(context, device, method);
if (isUsingPopup) {
while (invalidDeviceState) {
const uiPromise = uiPromises.create(UI.INVALID_PASSPHRASE_ACTION, device);
Expand All @@ -397,11 +393,7 @@ const inner = async (context: CoreContext, method: AbstractMethod<any>, device:
device.setState({ sessionId: undefined });
await device.initialize(method.useCardanoDerivation);

invalidDeviceState = await getInvalidDeviceState(
context,
device,
method.preauthorized,
);
invalidDeviceState = await getInvalidDeviceState(context, device, method);
} else {
// set new state as requested
device.setState({ staticSessionId: invalidDeviceState });
Expand Down Expand Up @@ -654,6 +646,11 @@ const onCallDevice = async (
device.on(DEVICE.FIRMWARE_VERSION_CHANGED, payload => {
sendCoreMessage(createDeviceMessage(DEVICE.FIRMWARE_VERSION_CHANGED, payload));
});
device.on(DEVICE.THP_PAIRING, onThpPairingHandler(context));
device.on(DEVICE.TRANSPORT_STATE_CHANGED, () => {
postMessage(createDeviceMessage(DEVICE.TRANSPORT_STATE_CHANGED, device.toMessageObject()));
});

if (useCoreInPopup && env === 'webextension' && origin) {
device.initStorage(new WebextensionStateStorage(origin));
}
Expand Down Expand Up @@ -899,6 +896,32 @@ const onEmptyPassphraseHandler =
callback({ value: '' });
};

const onThpPairingHandler =
(context: CoreContext): DeviceEvents['thp_pairing'] =>
async (...[device, callback]) => {
const { uiPromises, sendCoreMessage } = context;
// wait for popup handshake
await waitForPopup(context);
// create ui promise
const uiPromise = uiPromises.create(UI.RECEIVE_THP_PAIRING_TAG, device);
const thpState = device.protocolState;
sendCoreMessage(
createUiMessage(UI.REQUEST_THP_PAIRING, {
device: device.toMessageObject(),
availableMethods: thpState.handshakeCredentials?.pairingMethods || [],
selectedMethod: thpState.pairingMethod,
nfcData: thpState.nfcData?.toString('hex'),
}),
);
// wait for response
try {
const uiResp = await uiPromise.promise;
callback(uiResp.payload);
} catch (error) {
callback(null, error);
}
};

/**
* Handle popup closed by user.
* @returns {void}
Expand Down Expand Up @@ -1014,6 +1037,10 @@ const initDeviceList = (context: CoreContext) => {
sendCoreMessage(createTransportMessage(TRANSPORT.START, transport)),
);

deviceList.on(DEVICE.TRANSPORT_STATE_CHANGED, device => {
postMessage(createDeviceMessage(DEVICE.TRANSPORT_STATE_CHANGED, device.toMessageObject()));
});

deviceList.on(TRANSPORT.ERROR, error => {
_log.warn('TRANSPORT.ERROR', error.error);
sendCoreMessage(createTransportMessage(TRANSPORT.ERROR, error));
Expand Down Expand Up @@ -1139,6 +1166,7 @@ export class Core extends EventEmitter {
case UI.RECEIVE_PIN:
case UI.RECEIVE_PASSPHRASE:
case UI.INVALID_PASSPHRASE_ACTION:
case UI.RECEIVE_THP_PAIRING_TAG:
case UI.RECEIVE_ACCOUNT:
case UI.RECEIVE_FEE:
case UI.RECEIVE_WORD:
Expand Down
11 changes: 11 additions & 0 deletions packages/connect/src/data/connectSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,12 @@ const initialSettings: ConnectSettings = {
sharedLogger: true,
deeplinkUrl: `${DEFAULT_DOMAIN}deeplink/${DEEPLINK_VERSION}/`,
transportReconnect: true,
thp: {
hostName: 'TrezorConnect',
staticKeys: '0007070707070707070707070707070707070707070707070707070707070747',
knownCredentials: [],
pairingMethods: ['CodeEntry', 'QrCode', 'NFC_Unidirectional'] as any[],
},
};

const parseManifest = (manifest?: Manifest) => {
Expand Down Expand Up @@ -155,5 +161,10 @@ export const parseConnectSettings = (input: Partial<ConnectSettings> = {}) => {
settings.enableFirmwareHashCheck = Boolean(input.enableFirmwareHashCheck);
}

if (input.thp) {
// TODO: validate
settings.thp = input.thp;
}

return settings;
};
Loading
Loading