feat(connect): implement THP passphrase flow

This commit is contained in:
Szymon Lesisz
2024-08-28 12:07:15 +02:00
committed by Szymon Lesisz
parent de01a0302e
commit dd0312cf0b
5 changed files with 125 additions and 9 deletions

View File

@@ -112,7 +112,9 @@ export class DeviceCurrentSession implements TypedCallProvider {
expectedType: Messages.MessageKey | Messages.MessageKey[],
msg: Messages.MessagePayload = {},
) {
if (!allowedCallsBeforeInitialize.includes(type) && !this.device?.features?.session_id) {
const deviceSessionId =
this.device.getThpState()?.sessionId || this.device?.features?.session_id;
if (!allowedCallsBeforeInitialize.includes(type) && !deviceSessionId) {
console.error(
'Runtime',
`typedCall: Device not initialized when calling ${type}. call Initialize first`,

View File

@@ -4,6 +4,7 @@ import { thpPairing } from './pairing';
export { abortThpWorkflow } from './thpCall';
export { getThpCredentials } from './pairing';
export { createThpSession } from './session';
export const getThpChannel = async (device: Device, withInteraction?: boolean) => {
const thpState = device.getThpState();

View File

@@ -0,0 +1,29 @@
import { thp as protocolThp } from '@trezor/protocol';
import type { Device } from '../Device';
import { thpCall } from './thpCall';
export const createThpSession = async (device: Device, deriveCardano: boolean) => {
let passphrase: protocolThp.ThpCreateNewSession;
if (!device.features.passphrase_protection) {
passphrase = { passphrase: '' };
} else {
// same flow as DeviceCurrentSession PassphraseRequest
passphrase = await device.prompt('passphrase', {}).then(promptRes => {
if (!promptRes.success) {
return { passphrase: '' };
}
return promptRes.payload.passphraseOnDevice
? { on_device: true }
: { passphrase: promptRes.payload.value };
});
}
await thpCall(device, 'ThpCreateNewSession', {
...passphrase,
derive_cardano: deriveCardano,
});
return 0;
};

View File

@@ -4,6 +4,20 @@ import { DEVICE, UI, createDeviceMessage, createUiMessage } from '../../events';
import { StaticSessionId } from '../../types';
import { WorkflowContext } from '../../types/workflow';
import { toHardened } from '../../utils/pathUtils';
import { createThpSession } from '../thp';
const getStaticSessionId = (device: WorkflowContext['device']) =>
device
.getCurrentSession()
.typedCall('GetAddress', 'Address', {
address_n: [toHardened(44), toHardened(1), toHardened(0), 0, 0],
coin_name: 'Testnet',
script_type: 'SPENDADDRESS',
})
.then(
({ message }) =>
`${message.address}@${device.features.device_id}:${device.getInstance()}` as StaticSessionId,
);
const getState = async ({ device, method }: WorkflowContext) => {
if (!device.features) return;
@@ -21,13 +35,8 @@ const getState = async ({ device, method }: WorkflowContext) => {
const expectedState = device.getState()?.staticSessionId;
// add-abort-signal
const { message } = await device.getCurrentSession().typedCall('GetAddress', 'Address', {
address_n: [toHardened(44), toHardened(1), toHardened(0), 0, 0],
coin_name: 'Testnet',
script_type: 'SPENDADDRESS',
});
const uniqueState: StaticSessionId = `${message.address}@${device.features.device_id}:${device.getInstance()}`;
const uniqueState = await getStaticSessionId(device);
if (device.features.session_id) {
device.setState({ sessionId: device.features.session_id });
}
@@ -73,17 +82,66 @@ const getInvalidDeviceState = async (
});
};
const getInvalidThpDeviceState = async (context: WorkflowContext) => {
const { device, method } = context;
const currentState = device.getState();
const expectedState = currentState?.staticSessionId;
const expectedSessionId = currentState?.sessionId
? Buffer.from(currentState.sessionId, 'hex')
: undefined;
let uniqueState;
const thpState = device.getThpState()!;
if (expectedSessionId) {
// validate that expected ThpSession still exists
thpState.setSessionId(expectedSessionId);
uniqueState = await getStaticSessionId(device).catch(e => {
if (e.code === 'Failure_InvalidSession') {
// requested sessionId is not valid, reset setSessionId
device.setState({
sessionId: undefined,
deriveCardano: undefined,
});
thpState?.setSessionId(Buffer.alloc(1));
return undefined;
}
});
}
if (!uniqueState || (!currentState?.deriveCardano && method.useCardanoDerivation)) {
const newSessionId = thpState.createNewSessionId();
await createThpSession(device, method.useCardanoDerivation);
uniqueState = await getStaticSessionId(device);
device.setState({
sessionId: newSessionId.toString('hex'),
deriveCardano: method.useCardanoDerivation,
});
}
if (expectedState && expectedState !== uniqueState) {
return uniqueState;
}
if (!expectedState) {
device.setState({ staticSessionId: uniqueState });
}
};
export const validateState = async (context: WorkflowContext) => {
const { device, method } = context;
if (!method.useDeviceState) {
return;
}
const validate =
device.protocol.name === 'v2' ? getInvalidThpDeviceState : getInvalidDeviceState;
// Make sure that device will display pin/passphrase
const isDeviceUnlocked = device.features.unlocked;
const isUsingPopup = DataManager.getSettings('popup');
try {
let invalidDeviceState = await getInvalidDeviceState(context);
let invalidDeviceState = await validate(context);
if (isUsingPopup) {
while (invalidDeviceState) {
const uiPromise = method.createUiPromise(UI.INVALID_PASSPHRASE_ACTION, device);
@@ -101,7 +159,7 @@ export const validateState = async (context: WorkflowContext) => {
device.setState({ sessionId: undefined });
await device.initialize(method.useCardanoDerivation);
invalidDeviceState = await getInvalidDeviceState(context);
invalidDeviceState = await validate(context);
} else {
// set new state as requested
device.setState({ staticSessionId: invalidDeviceState });

View File

@@ -35,6 +35,8 @@ export class ThpState {
private _expectedResponses: number[] = [];
private _selectedMethod?: ThpPairingMethod;
private _nfcSecret?: Buffer;
private _sessionId: Buffer = Buffer.alloc(1);
private _sessionIdCounter: number = 0;
get pairingTagPromise() {
return this._pairingTagPromise;
@@ -207,6 +209,28 @@ export class ThpState {
};
}
get sessionId() {
return this._sessionId;
}
createNewSessionId() {
this._sessionIdCounter++;
if (this._sessionIdCounter > 255) {
this._sessionIdCounter = 1;
}
const sessionId = Buffer.alloc(1);
sessionId.writeUint8(this._sessionIdCounter, 0);
this.setSessionId(sessionId);
return sessionId;
}
setSessionId(sessionId: Buffer) {
this._sessionId = sessionId;
}
serialize(): ThpStateSerialized {
return {
properties: this._properties,
@@ -276,6 +300,8 @@ export class ThpState {
this._pairingCredentials = [];
this._selectedMethod = undefined;
this._nfcSecret = undefined;
this._sessionId = Buffer.alloc(1);
this._sessionIdCounter = 0;
}
toString() {