keyfork/crates/daemon/keyforkd-client/src/lib.rs

375 lines
14 KiB
Rust

//! # The Keyforkd Client
//!
//! Keyfork allows securing the master key and highest-level derivation keys by having derivation
//! requests performed against a server, "Keyforkd" or the "Keyfork Server". This allows
//! 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
//! the Keyfork Server. For versions prior to `1.0.0`, all versions within a "minor" version (i.e.,
//! `0.5.x`) will be compatible, but `0.5.x` will not be compatible with `0.6.x`. For versions
//! 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`.
//!
//! 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.
//!
//! ## Server Requests
//!
//! 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:
//!
//! ### Request: Derive Key
//!
//! The client creates a derivation path of at least two indices and requests a derived XPrv
//! (Extended Private Key) from the server.
//!
//! ```rust
//! use std::str::FromStr;
//!
//! use keyforkd_client::Client;
//! use keyfork_derive_util::DerivationPath;
//! # use keyfork_derive_util::private_key::TestPrivateKey as PrivateKey;
//! // use k256::SecretKey as PrivateKey;
//! // use ed25519_dalek::SigningKey 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(())
//! }
//! ```
//!
//! ---
//!
//! 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
//! use std::str::FromStr;
//!
//! use keyforkd_client::Client;
//! use keyfork_derive_util::DerivationPath;
//! use keyfork_derive_util::private_key::TestPrivateKey as PrivateKey;
//!
//! let seed = b"funky accordion noises";
//! keyforkd::test_util::run_test(seed, |socket_path| {
//! let derivation_path = DerivationPath::from_str("m/44'/0'").unwrap();
//! let mut client = Client::discover_socket().unwrap();
//! let xprv = client.request_xprv::<PrivateKey>(&derivation_path).unwrap();
//! keyforkd::test_util::Panicable::Ok(())
//! }).unwrap();
//! ```
pub use std::os::unix::net::UnixStream;
use std::{collections::HashMap, path::PathBuf};
use keyfork_derive_util::{
request::{AsAlgorithm, DerivationRequest},
DerivationPath, ExtendedPrivateKey, PrivateKey,
};
use keyfork_frame::{try_decode_from, try_encode_to, DecodeError, EncodeError};
use keyforkd_models::{Error as KeyforkdError, Request, Response};
#[cfg(test)]
mod tests;
/// An error occurred while interacting with Keyforkd.
#[derive(Debug, thiserror::Error)]
pub enum Error {
/// The response from the server did not match the request.
#[error("The response from the server did not match the request")]
InvalidResponse,
/// The environment variables used for determining a Keyforkd socket path were not set.
#[error("Neither KEYFORK_SOCKET_PATH nor XDG_RUNTIME_DIR were set")]
EnvVarsNotFound,
/// The Keyforkd client was unable to connect to the soocket.
#[error("Socket was unable to connect to {1}: {0} (make sure keyforkd is running)")]
Connect(std::io::Error, PathBuf),
/// Data could not be written to, or read from, the socket.
#[error("Could not write to or from the socket: {0}")]
Io(#[from] std::io::Error),
/// Attempting to serialize or deserialize a type to or from bincode encountered an error.
#[error("Could not perform bincode transformation: {0}")]
Bincode(#[from] Box<bincode::ErrorKind>),
/// A frame could not be encoded from the given data.
#[error("Could not perform frame transformation: {0}")]
FrameEnc(#[from] EncodeError),
/// A frame could not be decoded from the given data.
#[error("Could not perform frame transformation: {0}")]
FrameDec(#[from] DecodeError),
/// An error encountered in Keyforkd.
#[error("Error in Keyforkd: {0}")]
Keyforkd(#[from] KeyforkdError),
/// An invalid key was returned.
#[error("Invalid key returned")]
InvalidKey,
}
#[allow(missing_docs)]
pub type Result<T, E = Error> = std::result::Result<T, E>;
/// Create a [`UnixStream`] from the common Keyforkd socket paths.
///
/// # Errors
/// An error may be returned if the required environment variables were not set or if the socket
/// could not be connected to.
pub fn get_socket() -> Result<UnixStream, Error> {
let socket_vars = std::env::vars()
.filter(|(key, _)| ["XDG_RUNTIME_DIR", "KEYFORKD_SOCKET_PATH"].contains(&key.as_str()))
.collect::<HashMap<String, String>>();
let mut socket_path: PathBuf;
#[allow(clippy::single_match_else)]
match socket_vars.get("KEYFORKD_SOCKET_PATH") {
Some(occupied) => {
socket_path = PathBuf::from(occupied);
}
None => {
socket_path = PathBuf::from(
socket_vars
.get("XDG_RUNTIME_DIR")
.ok_or(Error::EnvVarsNotFound)?,
);
socket_path.extend(["keyforkd", "keyforkd.sock"]);
}
}
UnixStream::connect(&socket_path).map_err(|e| Error::Connect(e, socket_path))
}
/// A client to interact with Keyforkd.
///
/// Upon creation, a socket is opened, and is kept open for the duration of the object's lifetime.
/// Currently, Keyforkd does not support the reuse of sockets. Attempting to reuse the socket after
/// previously using it will likely result in an error.
#[derive(Debug)]
pub struct Client {
socket: UnixStream,
}
impl Client {
/// Create a new client from a given already-connected [`UnixStream`]. This function is
/// provided in case a specific UnixStream has to be used; otherwise,
/// [`Client::discover_socket`] should be preferred.
///
/// # Examples
/// ```rust
/// use keyforkd_client::{Client, get_socket};
///
/// # let seed = b"funky accordion noises";
/// # keyforkd::test_util::run_test(seed, |socket_path| {
/// let mut socket = get_socket()?;
/// let mut client = Client::new(socket);
/// # Ok::<_, keyforkd_client::Error>(())
/// # }).unwrap();
/// ```
pub fn new(socket: UnixStream) -> Self {
Self { socket }
}
/// Create a new client using well-known socket locations.
///
/// # Errors
/// An error may be returned if the required environment variables were not set or if the
/// socket could not be connected to.
///
/// # Examples
/// ```rust
/// use keyforkd_client::Client;
///
/// # let seed = b"funky accordion noises";
/// # keyforkd::test_util::run_test(seed, |socket_path| {
/// let mut client = Client::discover_socket()?;
/// # Ok::<_, keyforkd_client::Error>(())
/// # }).unwrap();
/// ```
pub fn discover_socket() -> Result<Self> {
get_socket().map(|socket| Self { socket })
}
/// Request an [`ExtendedPrivateKey`] for a given [`DerivationPath`].
///
/// # Errors
/// An error may be returned if:
/// * Reading or writing from or to the socket encountered an error.
/// * Bincode could not serialize the request or deserialize the response.
/// * An error occurred in Keyforkd.
/// * Keyforkd returned invalid data.
///
/// # Examples
/// ```rust
/// use std::str::FromStr;
///
/// use keyforkd_client::Client;
/// use keyfork_derive_util::DerivationPath;
/// # use keyfork_derive_util::private_key::TestPrivateKey as PrivateKey;
/// // use k256::SecretKey as PrivateKey;
/// // use ed25519_dalek::SigningKey as PrivateKey;
///
/// # let seed = b"funky accordion noises";
/// # keyforkd::test_util::run_test(seed, |socket_path| {
/// let derivation_path = DerivationPath::from_str("m/44'/0'").unwrap();
/// let mut client = Client::discover_socket().unwrap();
/// let xprv = client.request_xprv::<PrivateKey>(&derivation_path).unwrap();
/// # keyforkd::test_util::Panicable::Ok(())
/// # }).unwrap();
/// ```
pub fn request_xprv<K>(&mut self, path: &DerivationPath) -> Result<ExtendedPrivateKey<K>>
where
K: PrivateKey + Clone + AsAlgorithm,
{
let algo = K::as_algorithm();
let request = Request::Derivation(DerivationRequest::new(algo.clone(), path));
let response = self.request(&request)?;
match response {
Response::Derivation(d) => {
if d.algorithm != algo {
return Err(Error::InvalidResponse);
}
let depth = path.len() as u8;
ExtendedPrivateKey::from_parts(&d.data, depth, d.chain_code)
.map_err(|_| Error::InvalidKey)
}
_ => Err(Error::InvalidResponse),
}
}
}
impl Client {
/// 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
/// An error may be returned if:
/// * Reading or writing from or to the socket encountered an error.
/// * Bincode could not serialize the request or deserialize the response.
/// * An error occurred in Keyforkd.
pub fn request(&mut self, req: &Request) -> Result<Response> {
try_encode_to(&bincode::serialize(&req)?, &mut self.socket)?;
let resp = try_decode_from(&mut self.socket)?;
let resp: Result<Response, KeyforkdError> = bincode::deserialize(&resp)?;
resp.map_err(From::from)
}
}