Compare commits
2 Commits
3fd992d582
...
09e6e6de71
Author | SHA1 | Date |
---|---|---|
Ryan Heywood | 09e6e6de71 | |
Ryan Heywood | 536e6da5ad |
|
@ -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;
|
||||||
//!
|
//!
|
||||||
|
//! #[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";
|
//! # 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'")?;
|
||||||
//! let derivation_path = DerivationPath::from_str("m/44'/0'").unwrap();
|
//! let mut client = Client::discover_socket()?;
|
||||||
//! let mut client = Client::discover_socket().unwrap();
|
//! let xprv = client.request_xprv::<PrivateKey>(&derivation_path)?;
|
||||||
//! let xprv = client.request_xprv::<PrivateKey>(&derivation_path).unwrap();
|
//! # Ok::<_, Error>(())
|
||||||
//! # keyforkd::test_util::Infallible::Ok(())
|
//! # })?;
|
||||||
//! # }).unwrap();
|
//! 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)?;
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
|
@ -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");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,36 +3,34 @@
|
||||||
use std::io::{stdin, stdout};
|
use std::io::{stdin, stdout};
|
||||||
|
|
||||||
use keyfork_prompt::{
|
use keyfork_prompt::{
|
||||||
validators::{mnemonic, Validator},
|
MaybeIdentifier, PromptHandler, Terminal,
|
||||||
Terminal, PromptHandler,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
use keyfork_mnemonic_util::English;
|
#[derive(PartialEq, Eq, Debug)]
|
||||||
|
pub enum Example {
|
||||||
|
RetryQR,
|
||||||
|
UseMnemonic,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MaybeIdentifier for Example {}
|
||||||
|
|
||||||
|
impl std::fmt::Display for Example {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
Example::RetryQR => f.write_str("Retry QR Code"),
|
||||||
|
Example::UseMnemonic => f.write_str("Use Mnemonic"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let mut mgr = Terminal::new(stdin(), stdout())?;
|
let mut mgr = Terminal::new(stdin(), stdout())?;
|
||||||
let transport_validator = mnemonic::MnemonicSetValidator {
|
|
||||||
word_lengths: [9, 24],
|
|
||||||
};
|
|
||||||
let combine_validator = mnemonic::MnemonicSetValidator {
|
|
||||||
word_lengths: [24, 48],
|
|
||||||
};
|
|
||||||
|
|
||||||
let mnemonics = mgr.prompt_validated_wordlist::<English, _>(
|
let choice = mgr.prompt_choice(
|
||||||
"Enter a 9-word and 24-word mnemonic: ",
|
"Unable to detect QR code.",
|
||||||
3,
|
&[Example::RetryQR, Example::UseMnemonic],
|
||||||
transport_validator.to_fn(),
|
|
||||||
)?;
|
)?;
|
||||||
assert_eq!(mnemonics[0].as_bytes().len(), 12);
|
dbg!(choice);
|
||||||
assert_eq!(mnemonics[1].as_bytes().len(), 32);
|
|
||||||
|
|
||||||
let mnemonics = mgr.prompt_validated_wordlist::<English, _>(
|
|
||||||
"Enter a 24 and 48-word mnemonic: ",
|
|
||||||
3,
|
|
||||||
combine_validator.to_fn(),
|
|
||||||
)?;
|
|
||||||
assert_eq!(mnemonics[0].as_bytes().len(), 32);
|
|
||||||
assert_eq!(mnemonics[1].as_bytes().len(), 64);
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,7 @@ use keyfork_mnemonic_util::Wordlist;
|
||||||
///
|
///
|
||||||
pub mod terminal;
|
pub mod terminal;
|
||||||
pub mod validators;
|
pub mod validators;
|
||||||
pub use terminal::{Terminal, DefaultTerminal, default_terminal};
|
pub use terminal::{default_terminal, DefaultTerminal, Terminal};
|
||||||
|
|
||||||
/// An error occurred while displaying a prompt.
|
/// An error occurred while displaying a prompt.
|
||||||
#[derive(thiserror::Error, Debug)]
|
#[derive(thiserror::Error, Debug)]
|
||||||
|
@ -42,6 +42,12 @@ pub enum Message {
|
||||||
Data(String),
|
Data(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub trait MaybeIdentifier {
|
||||||
|
fn identifier(&self) -> Option<char> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// A trait to allow displaying prompts and accepting input.
|
/// A trait to allow displaying prompts and accepting input.
|
||||||
pub trait PromptHandler {
|
pub trait PromptHandler {
|
||||||
/// Prompt the user for input.
|
/// Prompt the user for input.
|
||||||
|
@ -58,7 +64,9 @@ pub trait PromptHandler {
|
||||||
/// The method may return an error if the message was not able to be displayed or if the input
|
/// The method may return an error if the message was not able to be displayed or if the input
|
||||||
/// could not be read.
|
/// could not be read.
|
||||||
#[cfg(feature = "mnemonic")]
|
#[cfg(feature = "mnemonic")]
|
||||||
fn prompt_wordlist<X>(&mut self, prompt: &str) -> Result<String> where X: Wordlist;
|
fn prompt_wordlist<X>(&mut self, prompt: &str) -> Result<String>
|
||||||
|
where
|
||||||
|
X: Wordlist;
|
||||||
|
|
||||||
/// Prompt the user for input based on a wordlist, while validating the wordlist using a
|
/// Prompt the user for input based on a wordlist, while validating the wordlist using a
|
||||||
/// provided parser function, returning the type from the parser. A language must be specified
|
/// provided parser function, returning the type from the parser. A language must be specified
|
||||||
|
@ -97,6 +105,19 @@ pub trait PromptHandler {
|
||||||
validator_fn: impl Fn(String) -> Result<V, Box<dyn std::error::Error>>,
|
validator_fn: impl Fn(String) -> Result<V, Box<dyn std::error::Error>>,
|
||||||
) -> Result<V, Error>;
|
) -> Result<V, Error>;
|
||||||
|
|
||||||
|
/// Prompt the user to select a choice between multiple options.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// The method may return an error if the message was not able to be displayed or if a choice
|
||||||
|
/// could not be received.
|
||||||
|
fn prompt_choice<'a, T>(
|
||||||
|
&mut self,
|
||||||
|
prompt: &str,
|
||||||
|
choices: &'a [T],
|
||||||
|
) -> Result<&'a T, Error>
|
||||||
|
where
|
||||||
|
T: std::fmt::Display + PartialEq + MaybeIdentifier;
|
||||||
|
|
||||||
/// Prompt the user with a [`Message`].
|
/// Prompt the user with a [`Message`].
|
||||||
///
|
///
|
||||||
/// # Errors
|
/// # Errors
|
||||||
|
|
|
@ -15,7 +15,7 @@ use keyfork_crossterm::{
|
||||||
|
|
||||||
use keyfork_bug::bug;
|
use keyfork_bug::bug;
|
||||||
|
|
||||||
use crate::{Error, Message, PromptHandler, Wordlist};
|
use crate::{Error, MaybeIdentifier, Message, PromptHandler, Wordlist};
|
||||||
|
|
||||||
#[allow(missing_docs)]
|
#[allow(missing_docs)]
|
||||||
pub type Result<T, E = Error> = std::result::Result<T, E>;
|
pub type Result<T, E = Error> = std::result::Result<T, E>;
|
||||||
|
@ -122,6 +122,9 @@ where
|
||||||
W: Write + AsRawFd,
|
W: Write + AsRawFd,
|
||||||
{
|
{
|
||||||
fn drop(&mut self) {
|
fn drop(&mut self) {
|
||||||
|
self.write
|
||||||
|
.execute(cursor::Show)
|
||||||
|
.expect(bug!("can't enable cursor blinking"));
|
||||||
self.write
|
self.write
|
||||||
.execute(DisableBracketedPaste)
|
.execute(DisableBracketedPaste)
|
||||||
.expect(bug!("can't restore bracketed paste"));
|
.expect(bug!("can't restore bracketed paste"));
|
||||||
|
@ -455,6 +458,77 @@ where
|
||||||
Ok(passphrase)
|
Ok(passphrase)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn prompt_choice<'a, T>(&mut self, prompt: &str, choices: &'a [T]) -> Result<&'a T, Error>
|
||||||
|
where
|
||||||
|
T: std::fmt::Display + PartialEq + MaybeIdentifier,
|
||||||
|
{
|
||||||
|
let mut terminal = self.lock().alternate_screen()?.raw_mode()?;
|
||||||
|
|
||||||
|
terminal
|
||||||
|
.queue(terminal::Clear(terminal::ClearType::All))?
|
||||||
|
.queue(cursor::MoveTo(0, 0))?
|
||||||
|
.queue(cursor::Hide)?;
|
||||||
|
|
||||||
|
for line in prompt.lines() {
|
||||||
|
terminal.queue(Print(line))?;
|
||||||
|
terminal
|
||||||
|
.queue(cursor::MoveDown(1))?
|
||||||
|
.queue(cursor::MoveToColumn(0))?;
|
||||||
|
|
||||||
|
}
|
||||||
|
terminal.flush()?;
|
||||||
|
|
||||||
|
let mut active_choice = 0;
|
||||||
|
|
||||||
|
let mut redraw = |active_choice| {
|
||||||
|
terminal.queue(cursor::MoveToColumn(0))?;
|
||||||
|
|
||||||
|
let mut iter = choices.iter().enumerate().peekable();
|
||||||
|
while let Some((i, choice)) = iter.next() {
|
||||||
|
// if active choice, flip foreground and background
|
||||||
|
// if active choice, wrap in []
|
||||||
|
// if not, wrap in spaces, to preserve spacing
|
||||||
|
if i == active_choice {
|
||||||
|
terminal.queue(PrintStyledContent(format!("[{choice}]").to_string().reverse()))?;
|
||||||
|
} else {
|
||||||
|
terminal.queue(Print(format!(" {choice} ").to_string()))?;
|
||||||
|
}
|
||||||
|
if iter.peek().is_some() {
|
||||||
|
terminal.queue(Print(" "))?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
terminal.flush()?;
|
||||||
|
Ok::<_, Error>(())
|
||||||
|
};
|
||||||
|
|
||||||
|
redraw(active_choice)?;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
if let Event::Key(k) = read()? {
|
||||||
|
match k.code {
|
||||||
|
KeyCode::Char('c') if k.modifiers.contains(KeyModifiers::CONTROL) => {
|
||||||
|
return Err(Error::CtrlC);
|
||||||
|
}
|
||||||
|
KeyCode::Left => {
|
||||||
|
// prevent underflow
|
||||||
|
// if 0, max is 1, -1 is 0, no underflow
|
||||||
|
// if 1, max is 1, -1 is 0
|
||||||
|
// if 2 or higher, max is 2 or higher, -1 is fine
|
||||||
|
active_choice = std::cmp::max(1, active_choice) - 1;
|
||||||
|
}
|
||||||
|
KeyCode::Right => {
|
||||||
|
active_choice = std::cmp::min(choices.len() - 1, active_choice + 1);
|
||||||
|
}
|
||||||
|
KeyCode::Enter => {
|
||||||
|
return Ok(&choices[active_choice]);
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
redraw(active_choice)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn prompt_message(&mut self, prompt: impl Borrow<Message>) -> Result<()> {
|
fn prompt_message(&mut self, prompt: impl Borrow<Message>) -> Result<()> {
|
||||||
let mut terminal = self.lock().alternate_screen()?.raw_mode()?;
|
let mut terminal = self.lock().alternate_screen()?.raw_mode()?;
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue