bhwi 0.1.0

Sans-IO Bitcoin hardware wallet interpreters and their common command model
Documentation
use crate::common::{
    Command, DeviceCode, DeviceContext, DisplayAddress, Error, ErrorKind, Info,
    MultisigAddressType, MultisigDisplayAddress, Response,
};
use crate::trezor::ManagementContext;
use crate::trezor::error::TrezorError;
use crate::trezor::interpreter::{
    TrezorCommand, TrezorDeviceInfo, TrezorMultisigAddress, TrezorMultisigAddressType,
    TrezorResponse, address_n, script_type,
};

impl TryFrom<Command> for TrezorCommand {
    type Error = TrezorError;

    fn try_from(command: Command) -> Result<Self, TrezorError> {
        Ok(match command {
            Command::Unlock { options } => TrezorCommand::Initialize(options.network),
            Command::GetVersion => TrezorCommand::GetFeatures,
            Command::GetMasterFingerprint => TrezorCommand::GetMasterFingerprint,
            Command::GetXpub { path, display } => TrezorCommand::GetXpub {
                address_n: address_n(&path),
                display,
            },
            Command::DisplayAddress(
                DisplayAddress::ByPath {
                    path,
                    display,
                    address_format,
                },
                _,
            ) => TrezorCommand::GetAddress {
                address_n: address_n(&path),
                display,
                script_type: script_type(address_format, &path),
            },
            Command::DisplayAddress(DisplayAddress::ByDescriptor { .. }, _) => {
                return Err(TrezorError::UnsupportedDisplayAddress(
                    "descriptor address display is not yet supported",
                ));
            }
            Command::DisplayAddress(DisplayAddress::ByMultisig(address), _) => {
                TrezorCommand::GetMultisigAddress(multisig_address(address))
            }
            Command::SignTx(psbt, context) => {
                if context.is_some() {
                    return Err(TrezorError::Unsupported(
                        "Trezor SignTx does not support device context",
                    ));
                }
                TrezorCommand::SignTx(Box::new(psbt))
            }
            Command::SignMessage { message, path } => TrezorCommand::SignMessage {
                address_n: address_n(&path),
                message,
            },
            Command::RegisterWallet { .. } => {
                return Err(TrezorError::Unsupported("register_wallet is not supported"));
            }
            Command::Backup => {
                return Err(TrezorError::Unsupported("backup is not yet supported"));
            }
            Command::Setup(options, context) => {
                let Some(DeviceContext::TrezorManagement(ManagementContext::Setup {
                    host_entropy,
                })) = context
                else {
                    return Err(TrezorError::MissingContext(
                        "Trezor setup requires host entropy in the device context",
                    ));
                };
                TrezorCommand::Setup {
                    label: (!options.label.is_empty()).then_some(options.label),
                    host_entropy,
                }
            }
            Command::Wipe => TrezorCommand::Wipe,
            Command::Restore(options, context) => {
                let Some(DeviceContext::TrezorManagement(ManagementContext::Restore {
                    u2f_counter,
                })) = context
                else {
                    return Err(TrezorError::MissingContext(
                        "Trezor restore requires a U2F counter in the device context",
                    ));
                };
                let word_count = u32::try_from(options.word_count).map_err(|_| {
                    TrezorError::InvalidInput("restore word count must be positive".into())
                })?;
                if !matches!(word_count, 12 | 18 | 24) {
                    return Err(TrezorError::InvalidInput(
                        "restore word count must be 12, 18, or 24".into(),
                    ));
                }
                TrezorCommand::Restore {
                    label: (!options.label.is_empty()).then_some(options.label),
                    word_count,
                    u2f_counter,
                }
            }
            Command::TogglePassphrase => TrezorCommand::TogglePassphrase,
            Command::PromptPin => TrezorCommand::PromptPin,
            Command::SendPin(context) => {
                let Some(DeviceContext::TrezorManagement(ManagementContext::Pin(pin))) = context
                else {
                    return Err(TrezorError::MissingContext(
                        "Trezor sendpin requires the PIN positions in the device context",
                    ));
                };
                TrezorCommand::SendPin(pin)
            }
        })
    }
}

impl From<TrezorResponse> for Response {
    fn from(response: TrezorResponse) -> Self {
        match response {
            TrezorResponse::Info(info) => Response::Info(device_info(info)),
            TrezorResponse::MasterFingerprint(fingerprint) => {
                Response::MasterFingerprint(fingerprint)
            }
            TrezorResponse::Xpub(xpub) => Response::Xpub(xpub),
            TrezorResponse::Address(address) => Response::Address(address),
            TrezorResponse::Signature(header, signature) => Response::Signature(header, signature),
            TrezorResponse::SignedPsbt(psbt) => Response::SignedPsbt(*psbt),
            TrezorResponse::DeviceAction(success) => Response::DeviceAction(success),
        }
    }
}

impl From<TrezorError> for Error {
    fn from(e: TrezorError) -> Self {
        engine_error(e, DeviceCode::Trezor)
    }
}

pub(super) fn engine_error(e: TrezorError, device_code: fn(i32) -> DeviceCode) -> Error {
    match e {
        TrezorError::Decode(err) => Error::new(ErrorKind::Serialization, err.to_string()),
        TrezorError::MalformedFrame => {
            Error::new(ErrorKind::Serialization, "malformed device message frame")
        }
        TrezorError::UnexpectedMessage(t, ctx) => Error::new(
            ErrorKind::UnexpectedResponse,
            format!("unexpected response to {ctx}: message type {t}"),
        ),
        TrezorError::Failure(Some(code), msg) => {
            Error::new(failure_kind(code), msg).with_device_code(device_code(code))
        }
        TrezorError::Failure(None, msg) => Error::new(ErrorKind::Other, msg),
        TrezorError::Locked(ctx) => Error::new(ErrorKind::Locked, ctx),
        TrezorError::NetworkMismatch => Error::new(
            ErrorKind::WrongNetwork,
            "device returned a key for the wrong network",
        ),
        TrezorError::ActionCancelled(code, text) => {
            Error::new(ErrorKind::UserCancelled, text.unwrap_or_default())
                .with_device_code(device_code(code))
        }
        TrezorError::AlreadyInitialized => Error::new(
            ErrorKind::AlreadyInitialized,
            "Device is already initialized. Use wipe first and try again",
        ),
        TrezorError::Unsupported(s) => Error::new(ErrorKind::Unsupported, s),
        TrezorError::MissingContext(s) => Error::new(ErrorKind::MissingContext, s),
        TrezorError::UnsupportedDisplayAddress(s) => {
            Error::new(ErrorKind::UnsupportedDisplayAddress, s)
        }
        TrezorError::PassphraseTooLong => {
            Error::new(ErrorKind::InvalidInput, "Passphrase too long")
        }
        TrezorError::NonNumericPin => {
            Error::new(ErrorKind::InvalidInput, "Non-numeric PIN provided")
        }
        TrezorError::AlreadyUnlocked(s) => Error::new(ErrorKind::AlreadyUnlocked, s),
        TrezorError::InvalidInput(s) => Error::new(ErrorKind::InvalidInput, s),
    }
}

fn failure_kind(code: i32) -> ErrorKind {
    use crate::trezor::proto::common::failure::FailureType;
    match FailureType::try_from(code) {
        Ok(FailureType::FailurePinExpected) => ErrorKind::Locked,
        Ok(FailureType::FailurePinInvalid) => ErrorKind::WrongPin,
        Ok(FailureType::FailureNotInitialized) => ErrorKind::NotInitialized,
        Ok(FailureType::FailureFirmwareError) => ErrorKind::DeviceFailure,
        _ => ErrorKind::Other,
    }
}

fn device_info(info: TrezorDeviceInfo) -> Info {
    let needs_host_passphrase = !info.on_device_passphrase_entry && info.passphrase_protection;
    Info {
        version: info.version,
        networks: vec![info.network],
        firmware: info.model,
        initialized: info.initialized,
        label: info.label,
        on_device_passphrase_entry: Some(info.on_device_passphrase_entry),
        needs_pin_sent: Some(info.needs_pin_sent),
        needs_passphrase_sent: Some(needs_host_passphrase),
    }
}

fn multisig_address(address: MultisigDisplayAddress) -> TrezorMultisigAddress {
    TrezorMultisigAddress {
        threshold: address.threshold,
        address_type: match address.address_type {
            MultisigAddressType::Legacy => TrezorMultisigAddressType::Legacy,
            MultisigAddressType::ShWit => TrezorMultisigAddressType::ShWit,
            MultisigAddressType::Wit => TrezorMultisigAddressType::Wit,
        },
        sorted: address.sorted,
        keys: address.keys,
    }
}