Compare commits

..

2 Commits

5 changed files with 171 additions and 45 deletions

View File

@ -1,8 +1,10 @@
//! # The Keyforkd Client //! # The Keyforkd Client
//! //!
//! Keyfork allows securing the master key and highest-level derivation keys by having derivation //! Keyfork allows securing the master key and highest-level derivation keys by having derivation
//! requests performed against a server, "Keyforkd" or the "Keyfork Server". The server is operated //! requests performed against a server, "Keyforkd" or the "Keyfork Server". This allows
//! on a UNIX socket with messages sent using the Keyfork Frame format. //! enforcement of policies, such as requiring at least two leves of a derivation path (for
//! instance, `m/0'` would not be allowed, but `m/0'/0'` would). The server is operated on a UNIX
//! socket with messages sent using the Keyfork Frame format.
//! //!
//! Programs using the Keyfork Client should ensure they are built against a compatible version of //! Programs using the Keyfork Client should ensure they are built against a compatible version of
//! the Keyfork Server. For versions prior to `1.0.0`, all versions within a "minor" version (i.e., //! the Keyfork Server. For versions prior to `1.0.0`, all versions within a "minor" version (i.e.,
@ -10,18 +12,23 @@
//! after `1.0.0`, all versions within a "major" version (i.e., `1.0.0`) will be compatible, but //! after `1.0.0`, all versions within a "major" version (i.e., `1.0.0`) will be compatible, but
//! `1.x.y` will not be compatible with `2.0.0`. //! `1.x.y` will not be compatible with `2.0.0`.
//! //!
//! Presently, the Keyfork server only supports the following requests: //! The Keyfork Client documentation makes extensive use of the `keyforkd::test_util` module.
//! This provides testing infrastructure to set up a temporary Keyfork Daemon. In
//! your code, you should assume the daemon has already been initialized, whether by another
//! process, on another terminal, or some other instance. At no point should a program deriving an
//! "endpoint" key have control over a mnemonic or a seed.
//! //!
//! * Derive Key //! ## Server Requests
//! //!
//! ## Extended Private Keys //! Keyfork is designed as a client-request/server-response model. The client sends a request, such
//! as a derivation request, and the server sends its response. Presently, the Keyfork server
//! supports the following requests:
//! //!
//! Keyfork doesn't need to be continuously called once a key has been derived. Once an Extended //! ### Request: Derive Key
//! Private Key (often shortened to "XPrv") has been created, further derivations can be performed. //!
//! The tests for this library ensure that all levels of Keyfork derivation beyond the required two //! The client creates a derivation path of at least two indices and requests a derived XPrv
//! will be derived similarly between the server and the client. //! (Extended Private Key) from the server.
//! //!
//! # Examples
//! ```rust //! ```rust
//! use std::str::FromStr; //! use std::str::FromStr;
//! //!
@ -31,17 +38,121 @@
//! // use k256::SecretKey as PrivateKey; //! // use k256::SecretKey as PrivateKey;
//! // use ed25519_dalek::SigningKey as PrivateKey; //! // use ed25519_dalek::SigningKey as PrivateKey;
//! //!
//! # let seed = b"funky accordion noises"; //! #[derive(Debug, thiserror::Error)]
//! # keyforkd::test_util::run_test(seed, |socket_path| { //! enum Error {
//! # std::env::set_var("KEYFORKD_SOCKET_PATH", socket_path); //! #[error(transparent)]
//! let derivation_path = DerivationPath::from_str("m/44'/0'").unwrap(); //! Path(#[from] keyfork_derive_util::PathError),
//! let mut client = Client::discover_socket().unwrap(); //!
//! let xprv = client.request_xprv::<PrivateKey>(&derivation_path).unwrap(); //! #[error(transparent)]
//! # keyforkd::test_util::Infallible::Ok(()) //! Keyforkd(#[from] keyforkd_client::Error),
//! # }).unwrap(); //! }
//!
//! fn main() -> Result<(), Error> {
//! # let seed = b"funky accordion noises";
//! # keyforkd::test_util::run_test(seed, |socket_path| {
//! let derivation_path = DerivationPath::from_str("m/44'/0'")?;
//! let mut client = Client::discover_socket()?;
//! let xprv = client.request_xprv::<PrivateKey>(&derivation_path)?;
//! # Ok::<_, Error>(())
//! # })?;
//! Ok(())
//! }
//! ``` //! ```
//! //!
//! In tests, the Keyforkd test_util module and TestPrivateKeys can be used. //! ---
//!
//! Request objects are typically handled by the Keyfork Client library (such as with
//! [`Client::request_xprv`]). While unadvised, clients can also attempt to handle their own
//! requests, using [`Client::request`].
//!
//! ## Extended Private Keys
//!
//! Keyfork doesn't need to be continuously called once a key has been derived. Once an Extended
//! Private Key (often shortened to "XPrv") has been created, further derivations can be performed.
//! The tests for this library ensure that all levels of Keyfork derivation beyond the required two
//! will be derived similarly between the server and the client.
//!
//! ```rust
//! use std::str::FromStr;
//!
//! use keyforkd_client::Client;
//! use keyfork_derive_util::{DerivationIndex, DerivationPath};
//! # use keyfork_derive_util::private_key::TestPrivateKey as PrivateKey;
//! // use k256::SecretKey as PrivateKey;
//! // use ed25519_dalek::SigningKey as PrivateKey;
//! # fn check_wallet<T>(_: T) {}
//!
//! #[derive(Debug, thiserror::Error)]
//! enum Error {
//! #[error(transparent)]
//! Index(#[from] keyfork_derive_util::IndexError),
//!
//! #[error(transparent)]
//! Path(#[from] keyfork_derive_util::PathError),
//!
//! #[error(transparent)]
//! PrivateKey(#[from] keyfork_derive_util::PrivateKeyError),
//!
//! #[error(transparent)]
//! Keyforkd(#[from] keyforkd_client::Error),
//! }
//!
//! fn main() -> Result<(), Error> {
//! # let seed = b"funky accordion noises";
//! # keyforkd::test_util::run_test(seed, |socket_path| {
//! let derivation_path = DerivationPath::from_str("m/44'/0'/0'/0")?;
//! let mut client = Client::discover_socket()?;
//! let xprv = client.request_xprv::<PrivateKey>(&derivation_path)?;
//! // scan first 20 wallets
//! for index in 0..20 {
//! // use non-hardened derivation
//! let new_xprv = xprv.derive_child(&DerivationIndex::new(index, false)?);
//! check_wallet(new_xprv)
//! }
//! # Ok::<_, Error>(())
//! # })?;
//! Ok(())
//! }
//! ```
//!
//! ## Testing Infrastructure
//!
//! In tests, the `keyforkd::test_util` module and TestPrivateKeys can be used. These provide
//! useful utilities for writing tests that interact with the Keyfork Server without needing to
//! manually create the server for the purpose of the test. The `run_test` method can be used to
//! run a test, which can handle both returning errors and correctly translating panics (though,
//! the panics definitely won't look tidy).
//!
//! ```rust
//! use std::str::FromStr;
//!
//! use keyforkd_client::Client;
//! use keyfork_derive_util::DerivationPath;
//! use keyfork_derive_util::private_key::TestPrivateKey as PrivateKey;
//!
//! #[derive(Debug, thiserror::Error)]
//! enum Error {
//! #[error(transparent)]
//! Path(#[from] keyfork_derive_util::PathError),
//!
//! #[error(transparent)]
//! Keyforkd(#[from] keyforkd_client::Error),
//! }
//!
//! fn main() -> Result<(), Error> {
//! let seed = b"funky accordion noises";
//! keyforkd::test_util::run_test(seed, |socket_path| {
//! let derivation_path = DerivationPath::from_str("m/44'/0'")?;
//! let mut client = Client::discover_socket()?;
//! let xprv = client.request_xprv::<PrivateKey>(&derivation_path)?;
//! Ok::<_, Error>(())
//! })?;
//! Ok(())
//! }
//! ```
//!
//! If you would rather write tests to panic rather than error, or would rather not deal with error
//! types, the Panicable type should be used, which will handle the Error type for the closure.
//! //!
//! ```rust //! ```rust
//! use std::str::FromStr; //! use std::str::FromStr;
@ -52,11 +163,10 @@
//! //!
//! let seed = b"funky accordion noises"; //! let seed = b"funky accordion noises";
//! keyforkd::test_util::run_test(seed, |socket_path| { //! keyforkd::test_util::run_test(seed, |socket_path| {
//! std::env::set_var("KEYFORKD_SOCKET_PATH", socket_path);
//! let derivation_path = DerivationPath::from_str("m/44'/0'").unwrap(); //! let derivation_path = DerivationPath::from_str("m/44'/0'").unwrap();
//! let mut client = Client::discover_socket().unwrap(); //! let mut client = Client::discover_socket().unwrap();
//! let xprv = client.request_xprv::<PrivateKey>(&derivation_path).unwrap(); //! let xprv = client.request_xprv::<PrivateKey>(&derivation_path).unwrap();
//! keyforkd::test_util::Infallible::Ok(()) //! keyforkd::test_util::Panicable::Ok(())
//! }).unwrap(); //! }).unwrap();
//! ``` //! ```
@ -165,10 +275,9 @@ impl Client {
/// ///
/// # let seed = b"funky accordion noises"; /// # let seed = b"funky accordion noises";
/// # keyforkd::test_util::run_test(seed, |socket_path| { /// # keyforkd::test_util::run_test(seed, |socket_path| {
/// # std::env::set_var("KEYFORKD_SOCKET_PATH", socket_path); /// let mut socket = get_socket()?;
/// let mut socket = get_socket().unwrap();
/// let mut client = Client::new(socket); /// let mut client = Client::new(socket);
/// # keyforkd::test_util::Infallible::Ok(()) /// # Ok::<_, keyforkd_client::Error>(())
/// # }).unwrap(); /// # }).unwrap();
/// ``` /// ```
pub fn new(socket: UnixStream) -> Self { pub fn new(socket: UnixStream) -> Self {
@ -187,9 +296,8 @@ impl Client {
/// ///
/// # let seed = b"funky accordion noises"; /// # let seed = b"funky accordion noises";
/// # keyforkd::test_util::run_test(seed, |socket_path| { /// # keyforkd::test_util::run_test(seed, |socket_path| {
/// # std::env::set_var("KEYFORKD_SOCKET_PATH", socket_path); /// let mut client = Client::discover_socket()?;
/// let mut client = Client::discover_socket().unwrap(); /// # Ok::<_, keyforkd_client::Error>(())
/// # keyforkd::test_util::Infallible::Ok(())
/// # }).unwrap(); /// # }).unwrap();
/// ``` /// ```
pub fn discover_socket() -> Result<Self> { pub fn discover_socket() -> Result<Self> {
@ -217,11 +325,10 @@ impl Client {
/// ///
/// # let seed = b"funky accordion noises"; /// # let seed = b"funky accordion noises";
/// # keyforkd::test_util::run_test(seed, |socket_path| { /// # keyforkd::test_util::run_test(seed, |socket_path| {
/// # std::env::set_var("KEYFORKD_SOCKET_PATH", socket_path);
/// let derivation_path = DerivationPath::from_str("m/44'/0'").unwrap(); /// let derivation_path = DerivationPath::from_str("m/44'/0'").unwrap();
/// let mut client = Client::discover_socket().unwrap(); /// let mut client = Client::discover_socket().unwrap();
/// let xprv = client.request_xprv::<PrivateKey>(&derivation_path).unwrap(); /// let xprv = client.request_xprv::<PrivateKey>(&derivation_path).unwrap();
/// # keyforkd::test_util::Infallible::Ok(()) /// # keyforkd::test_util::Panicable::Ok(())
/// # }).unwrap(); /// # }).unwrap();
/// ``` /// ```
pub fn request_xprv<K>(&mut self, path: &DerivationPath) -> Result<ExtendedPrivateKey<K>> pub fn request_xprv<K>(&mut self, path: &DerivationPath) -> Result<ExtendedPrivateKey<K>>
@ -244,15 +351,20 @@ impl Client {
_ => Err(Error::InvalidResponse), _ => Err(Error::InvalidResponse),
} }
} }
}
impl Client {
/// Serialize and send a [`Request`] to the server, awaiting a [`Result<Response>`]. /// Serialize and send a [`Request`] to the server, awaiting a [`Result<Response>`].
/// ///
/// This function does not properly assert the association between a request type and a
/// response type, and does not perform any serialization of native objects into Request or
/// Response types, and should only be used when absolutely necessary.
///
/// # Errors /// # Errors
/// An error may be returned if: /// An error may be returned if:
/// * Reading or writing from or to the socket encountered an error. /// * Reading or writing from or to the socket encountered an error.
/// * Bincode could not serialize the request or deserialize the response. /// * Bincode could not serialize the request or deserialize the response.
/// * An error occurred in Keyforkd. /// * An error occurred in Keyforkd.
#[doc(hidden)]
pub fn request(&mut self, req: &Request) -> Result<Response> { pub fn request(&mut self, req: &Request) -> Result<Response> {
try_encode_to(&bincode::serialize(&req)?, &mut self.socket)?; try_encode_to(&bincode::serialize(&req)?, &mut self.socket)?;
let resp = try_decode_from(&mut self.socket)?; let resp = try_decode_from(&mut self.socket)?;

View File

@ -1,7 +1,7 @@
use crate::Client; use crate::Client;
use keyfork_derive_util::{request::*, DerivationPath}; use keyfork_derive_util::{request::*, DerivationPath};
use keyfork_slip10_test_data::test_data; use keyfork_slip10_test_data::test_data;
use keyforkd::test_util::{run_test, Infallible}; use keyforkd::test_util::{run_test, Panicable};
use std::{os::unix::net::UnixStream, str::FromStr}; use std::{os::unix::net::UnixStream, str::FromStr};
#[test] #[test]
@ -109,7 +109,7 @@ fn ed25519_test_suite() {
DerivationResponse::try_from(client.request(&req.into()).unwrap()).unwrap(); DerivationResponse::try_from(client.request(&req.into()).unwrap()).unwrap();
assert_eq!(&response.data, test.private_key.as_slice()); assert_eq!(&response.data, test.private_key.as_slice());
} }
Infallible::Ok(()) Panicable::Ok(())
}) })
.unwrap(); .unwrap();
} }

View File

@ -12,20 +12,21 @@ use keyfork_bug::bug;
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]
#[error("This error can never be instantiated")] #[error("This error can never be instantiated")]
#[doc(hidden)] #[doc(hidden)]
pub struct InfallibleError { pub enum UninstantiableError {}
protected: (),
}
/// An infallible result. This type can be used to represent a function that should never error. /// A panicable result. This type can be used when a closure chooses to panic instead of
/// returning an error. This doesn't necessarily mean a closure _has_ to panic, and its absence
/// doesn't imply a closure _can't_ panic, but this is a useful utility function for writing tests,
/// to avoid the necessity of making custom error types.
/// ///
/// ```rust /// ```rust
/// use keyforkd::test_util::Infallible; /// use keyforkd::test_util::Panicable;
/// let closure = || { /// let closure = || {
/// Infallible::Ok(()) /// Panicable::Ok(())
/// }; /// };
/// assert!(closure().is_ok()); /// assert!(closure().is_ok());
/// ``` /// ```
pub type Infallible<T> = std::result::Result<T, InfallibleError>; pub type Panicable<T> = std::result::Result<T, UninstantiableError>;
/// Run a test making use of a Keyforkd server. The test may use a seed (the first argument) from a /// Run a test making use of a Keyforkd server. The test may use a seed (the first argument) from a
/// test suite, or (as shown in the example below) a simple seed may be used solely to ensure /// test suite, or (as shown in the example below) a simple seed may be used solely to ensure
@ -39,6 +40,8 @@ pub type Infallible<T> = std::result::Result<T, InfallibleError>;
/// runtime. /// runtime.
/// ///
/// # Examples /// # Examples
/// The test utility provides a socket that can be connected to for deriving keys.
///
/// ```rust /// ```rust
/// use std::os::unix::net::UnixStream; /// use std::os::unix::net::UnixStream;
/// let seed = b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"; /// let seed = b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
@ -46,6 +49,18 @@ pub type Infallible<T> = std::result::Result<T, InfallibleError>;
/// UnixStream::connect(&path).map(|_| ()) /// UnixStream::connect(&path).map(|_| ())
/// }).unwrap(); /// }).unwrap();
/// ``` /// ```
///
/// The `keyforkd-client` crate uses the `KEYFORKD_SOCKET_PATH` variable to determine the default
/// socket path. The test will export the environment variable so it may be used by default.
///
/// ```rust
/// use std::os::unix::net::UnixStream;
/// let seed = b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
/// keyforkd::test_util::run_test(seed.as_slice(), |path| {
/// assert_eq!(std::env::var_os("KEYFORKD_SOCKET_PATH").unwrap(), path.as_os_str());
/// UnixStream::connect(&path).map(|_| ())
/// }).unwrap();
/// ```
#[allow(clippy::missing_errors_doc)] #[allow(clippy::missing_errors_doc)]
pub fn run_test<F, E>(seed: &[u8], closure: F) -> Result<(), E> pub fn run_test<F, E>(seed: &[u8], closure: F) -> Result<(), E>
where where
@ -82,6 +97,7 @@ where
rx.recv() rx.recv()
.await .await
.expect(bug!("can't receive server start signal from channel")); .expect(bug!("can't receive server start signal from channel"));
std::env::set_var("KEYFORKD_SOCKET_PATH", &socket_path);
let test_handle = tokio::task::spawn_blocking(move || closure(&socket_path)); let test_handle = tokio::task::spawn_blocking(move || closure(&socket_path));
let result = test_handle.await; let result = test_handle.await;
@ -89,8 +105,8 @@ where
result result
}); });
if let Err(e) = result { if let Err(e) = result {
if e.is_panic() { if let Ok(reason) = e.try_into_panic() {
std::panic::resume_unwind(e.into_panic()); std::panic::resume_unwind(reason);
} }
} }
Ok(()) Ok(())
@ -103,6 +119,6 @@ mod tests {
#[test] #[test]
fn test_run_test() { fn test_run_test() {
let seed = b"beefbeef"; let seed = b"beefbeef";
run_test(seed, |_path| Infallible::Ok(())).expect("infallible"); run_test(seed, |_path| Panicable::Ok(())).expect("infallible");
} }
} }

View File

@ -108,7 +108,7 @@ pub trait PromptHandler {
/// Prompt the user to select a choice between multiple options. /// Prompt the user to select a choice between multiple options.
/// ///
/// # Errors /// # Errors
/// The metho may return an error if the message was not able to be displayed or if a choice /// The method may return an error if the message was not able to be displayed or if a choice
/// could not be received. /// could not be received.
fn prompt_choice<'a, T>( fn prompt_choice<'a, T>(
&mut self, &mut self,

View File

@ -510,7 +510,6 @@ where
return Err(Error::CtrlC); return Err(Error::CtrlC);
} }
KeyCode::Left => { KeyCode::Left => {
eprintln!("{active_choice}");
// prevent underflow // prevent underflow
// if 0, max is 1, -1 is 0, no underflow // if 0, max is 1, -1 is 0, no underflow
// if 1, max is 1, -1 is 0 // if 1, max is 1, -1 is 0
@ -518,7 +517,6 @@ where
active_choice = std::cmp::max(1, active_choice) - 1; active_choice = std::cmp::max(1, active_choice) - 1;
} }
KeyCode::Right => { KeyCode::Right => {
eprintln!("{active_choice}");
active_choice = std::cmp::min(choices.len() - 1, active_choice + 1); active_choice = std::cmp::min(choices.len() - 1, active_choice + 1);
} }
KeyCode::Enter => { KeyCode::Enter => {