Skip to content
Merged
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
485 changes: 479 additions & 6 deletions Cargo.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,5 @@ reqwest = "0.13.2"
serde = "1.0.228"
serde_json = "1.0.149"
serde_cbor = "0.11.2"
thiserror = "1"
tokio = "1.0"
1 change: 1 addition & 0 deletions bhwi-async/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,6 @@ hex = { workspace = true, optional = true}
serde = { workspace = true, optional = true}
serde_cbor = { workspace = true, optional = true}
serde_json = { workspace = true, optional = true}
thiserror.workspace = true

byteorder = "1.5"
91 changes: 79 additions & 12 deletions bhwi-async/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ pub mod jade;
pub mod ledger;
pub mod transport;

use std::fmt::Debug;
use std::{error::Error as StdError, fmt::Debug};

use async_trait::async_trait;
use bhwi::{
Expand Down Expand Up @@ -47,17 +47,45 @@ pub trait HWI {
) -> Result<(u8, Signature), Self::Error>;
}

#[derive(Debug)]
// TODO: this will become a pain to maintain, but we can have a proc-macro
// generate this trait by putting it over HWI's definition and then also
// generate the blanket impl which will map the errors to HWIDeviceError
#[async_trait(?Send)]
pub trait HWIDevice {
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Trying to come up with a better solution to this before attempting to write a proc macro. Duplicating all the methods isn't so awful, but not very nice IMO

async fn unlock(&mut self, network: Network) -> Result<(), HWIDeviceError>;
async fn get_master_fingerprint(&mut self) -> Result<Fingerprint, HWIDeviceError>;
async fn get_extended_pubkey(
&mut self,
path: DerivationPath,
display: bool,
) -> Result<Xpub, HWIDeviceError>;
async fn sign_message(
&mut self,
message: &[u8],
path: DerivationPath,
) -> Result<(u8, Signature), HWIDeviceError>;
}

#[derive(Debug, thiserror::Error)]
#[error("hwi device error: {0}")]
pub struct HWIDeviceError(#[from] Box<dyn StdError + Send + Sync + 'static>);

impl HWIDeviceError {
pub fn new(error: impl StdError + Send + Sync + 'static) -> Self {
Self(Box::new(error))
}
}

#[derive(Debug, thiserror::Error)]
pub enum Error<E, F> {
#[error("transport error: {0}")]
Transport(E),

#[error("http client error: {0}")]
HttpClient(F),
Interpreter(common::Error),
}

impl<E, F> From<common::Error> for Error<E, F> {
fn from(value: common::Error) -> Self {
Self::Interpreter(value)
}
#[error("interpreter error: {0}")]
Interpreter(#[from] common::Error),
}

#[async_trait(?Send)]
Expand Down Expand Up @@ -126,6 +154,45 @@ where
}
}

#[async_trait(?Send)]
impl<T> HWIDevice for T
where
T: HWI,
T::Error: StdError + Send + Sync + 'static,
{
async fn unlock(&mut self, network: Network) -> Result<(), HWIDeviceError> {
HWI::unlock(self, network)
.await
.map_err(HWIDeviceError::new)
}

async fn get_master_fingerprint(&mut self) -> Result<Fingerprint, HWIDeviceError> {
HWI::get_master_fingerprint(self)
.await
.map_err(HWIDeviceError::new)
}

async fn get_extended_pubkey(
&mut self,
path: DerivationPath,
display: bool,
) -> Result<Xpub, HWIDeviceError> {
HWI::get_extended_pubkey(self, path, display)
.await
.map_err(HWIDeviceError::new)
}

async fn sign_message(
&mut self,
message: &[u8],
path: DerivationPath,
) -> Result<(u8, Signature), HWIDeviceError> {
HWI::sign_message(self, message, path)
.await
.map_err(HWIDeviceError::new)
}
}

pub trait OnUnlock {
fn on_unlock(&mut self, _response: common::Response) -> Result<(), common::Error>;
}
Expand All @@ -144,13 +211,13 @@ pub trait CommonInterface<C, T, R, E> {
);
}

async fn run_command<'a, D, C, E, F>(
device: &'a mut D,
async fn run_command<D, C, E, F>(
device: &mut D,
command: C,
) -> Result<common::Response, Error<E, F>>
where
E: std::fmt::Debug + 'a,
F: std::fmt::Debug + 'a,
E: Debug,
F: Debug,
D: CommonInterface<
common::Command,
common::Transmit,
Expand Down
14 changes: 6 additions & 8 deletions bhwi-async/src/transport/coldcard/hid.rs
Original file line number Diff line number Diff line change
@@ -1,20 +1,18 @@
use crate::{Transport, transport::Channel};
use async_trait::async_trait;

pub const COLDCARD_VID: u16 = 0xd13e;
pub use bhwi::coldcard::COLDCARD_DEVICE_ID;

const COLDCARD_PACKET_WRITE_SIZE: usize = 63;
const COLDCARD_PACKET_READ_SIZE: usize = 64;

#[derive(Debug)]
#[derive(Debug, thiserror::Error)]
pub enum ColdcardHIDError {
#[error("communication error: {0}")]
Comm(&'static str),
Hid(std::io::Error),
}

impl From<std::io::Error> for ColdcardHIDError {
fn from(value: std::io::Error) -> Self {
ColdcardHIDError::Hid(value)
}
#[error("HID IO error")]
Hid(#[from] std::io::Error),
}

pub struct ColdcardTransportHID<C> {
Expand Down
1 change: 1 addition & 0 deletions bhwi-async/src/transport/coldcard/mod.rs
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
pub use bhwi::coldcard::DEFAULT_CKCC_SOCKET;
pub mod hid;
2 changes: 2 additions & 0 deletions bhwi-async/src/transport/jade/mod.rs
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
pub use bhwi::jade::JADE_DEVICE_IDS;

#[cfg(feature = "emulators")]
pub mod tcp;
12 changes: 4 additions & 8 deletions bhwi-async/src/transport/jade/tcp.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
use std::fmt::Debug;

use async_trait::async_trait;
use serde_cbor::Value;

Expand All @@ -17,15 +15,13 @@ impl<C> TcpTransport<C> {

#[async_trait(?Send)]
pub trait TcpClient {
type Error: Debug + From<serde_cbor::Error>;

async fn write_all(&mut self, command: &[u8]) -> Result<(), Self::Error>;
async fn read(&mut self, buf: &mut [u8]) -> Result<usize, Self::Error>;
async fn write_all(&mut self, command: &[u8]) -> Result<(), std::io::Error>;
async fn read(&mut self, buf: &mut [u8]) -> Result<usize, std::io::Error>;
}

#[async_trait(?Send)]
impl<C: TcpClient> Transport for TcpTransport<C> {
type Error = <C as TcpClient>::Error;
type Error = std::io::Error;

async fn exchange(&mut self, command: &[u8], _encrypted: bool) -> Result<Vec<u8>, Self::Error> {
self.client.write_all(command).await?;
Expand All @@ -47,7 +43,7 @@ impl<C: TcpClient> Transport for TcpTransport<C> {
continue; // read more bytes
}
Err(e) => {
return Err(e.into());
return Err(std::io::Error::other(e));
}
}
}
Expand Down
18 changes: 7 additions & 11 deletions bhwi-async/src/transport/ledger/hid.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,36 +14,32 @@
* limitations under the License.
********************************************************************************/
use async_trait::async_trait;
pub use bhwi::ledger::LEDGER_DEVICE_ID;
use byteorder::{BigEndian, ReadBytesExt};
use std::io::Cursor;

use crate::{Transport, transport::Channel};

pub const LEDGER_VID: u16 = 0x2c97;
pub const LEDGER_USAGE_PAGE: u16 = 0xFFA0;
pub const LEDGER_CHANNEL: u16 = 0x0101;
// for Windows compatability, we prepend the buffer with a 0x00
// so the actual buffer is 64 bytes
const LEDGER_PACKET_WRITE_SIZE: u8 = 64;
const LEDGER_PACKET_READ_SIZE: u8 = 64;

#[derive(Debug)]
#[derive(Debug, thiserror::Error)]
pub enum LedgerHIDError {
#[error("communication error: {0}")]
Comm(&'static str),
Hid(std::io::Error),
}

impl From<std::io::Error> for LedgerHIDError {
fn from(value: std::io::Error) -> Self {
LedgerHIDError::Hid(value)
}
#[error("HID IO error")]
Hid(#[from] std::io::Error),
}

pub struct LedgerTransportHID<C> {
pub struct LedgerTransportHID<C: Channel> {
channel: C,
}

impl<C> LedgerTransportHID<C> {
impl<C: Channel> LedgerTransportHID<C> {
pub fn new(channel: C) -> Self {
Self { channel }
}
Expand Down
114 changes: 28 additions & 86 deletions bhwi-async/src/transport/ledger/speculos.rs
Original file line number Diff line number Diff line change
@@ -1,109 +1,51 @@
use std::fmt::{Debug, Display};
use std::fmt::Debug;

use async_trait::async_trait;
use hex::FromHexError;
use serde::{Deserialize, Serialize, de::DeserializeOwned};

use crate::Transport;
use crate::{Transport, transport::Channel};

pub struct SpeculosTransport<C> {
pub client: C,
pub struct LedgerTransportTcp<C: Channel> {
channel: C,
}

impl<C> SpeculosTransport<C> {
pub fn new(client: C) -> Self {
Self { client }
}
}
#[derive(Debug, thiserror::Error)]
pub enum LedgerTcpError {
#[error("ledger io error: {0}")]
Io(#[from] std::io::Error),

#[derive(Serialize, Deserialize)]
/// Apdu response from speculos emulator
pub struct Apdu {
/// hex encoded apdu data
data: String,
#[error("ledger invalid response")]
InvalidResponse,
}

#[async_trait(?Send)]
pub trait SpeculosClient {
type Error: Debug + From<FromHexError>;

/// The endpoint that the speculos emulator is listening on.
/// Ex. "localhost:5000"
fn url(&self) -> &str;

async fn post<Req: Serialize>(&self, endpoint: &str, json_req: Req) -> Result<(), Self::Error>;

async fn post_json<Req: Serialize, Res: DeserializeOwned>(
&self,
endpoint: &str,
json_req: Req,
) -> Result<Res, Self::Error>;

async fn button_press(&self, button: Button) -> Result<(), Self::Error> {
Self::post(
self,
&format!("{}/button/{button}", Self::url(self)),
&ButtonRequest {
action: "press-and-release".into(),
},
)
.await
}

async fn set_automation<T: Serialize>(&self, automation_json: &T) -> Result<(), Self::Error> {
Self::post(
self,
&format!("{}/automation", Self::url(self)),
automation_json,
)
.await
impl<C: Channel> LedgerTransportTcp<C> {
pub fn new(channel: C) -> Self {
Self { channel }
}
}

#[async_trait(?Send)]
impl<C: SpeculosClient> Transport for SpeculosTransport<C> {
type Error = <C as SpeculosClient>::Error;
impl<C: Channel> Transport for LedgerTransportTcp<C> {
type Error = LedgerTcpError;

async fn exchange(
&mut self,
apdu_command: &[u8],
_encrypted: bool,
) -> Result<Vec<u8>, Self::Error> {
let Apdu { data } = self
.client
.post_json(
&format!("{}/apdu", self.client.url()),
&Apdu {
data: hex::encode(apdu_command),
},
)
.await?;
Ok(hex::decode(data)?)
}
}
let mut tx = Vec::with_capacity(4 + apdu_command.len());
tx.extend_from_slice(&(apdu_command.len() as u32).to_be_bytes());
tx.extend_from_slice(apdu_command);

#[derive(Debug, Clone, Copy)]
pub enum Button {
Left,
Right,
Both,
}
self.channel.send(&tx).await?;

impl Display for Button {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}",
match self {
Button::Left => "left",
Button::Right => "right",
Button::Both => "both",
}
)
}
}
let mut len_buf = [0u8; 4];
self.channel.receive(&mut len_buf).await?;

let resp_len = u32::from_be_bytes(len_buf) as usize;

#[derive(Serialize)]
struct ButtonRequest {
action: String,
let mut resp = vec![0u8; resp_len + 2];
self.channel.receive(&mut resp).await?;

Ok(resp)
}
}
2 changes: 2 additions & 0 deletions bhwi-async/src/transport/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ pub mod ledger;

use async_trait::async_trait;

pub use bhwi::device::DeviceId;

#[async_trait(?Send)]
pub trait Channel {
async fn send(&self, data: &[u8]) -> Result<usize, std::io::Error>;
Expand Down
Loading
Loading