diff --git a/crates/daemon/keyforkd-client/src/lib.rs b/crates/daemon/keyforkd-client/src/lib.rs index 81720e6..de31926 100644 --- a/crates/daemon/keyforkd-client/src/lib.rs +++ b/crates/daemon/keyforkd-client/src/lib.rs @@ -40,6 +40,25 @@ //! # keyforkd::test_util::Infallible::Ok(()) //! # }).unwrap(); //! ``` +//! +//! In tests, the Keyforkd test_util module and TestPrivateKeys can be used. +//! +//! ```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| { +//! std::env::set_var("KEYFORKD_SOCKET_PATH", socket_path); +//! let derivation_path = DerivationPath::from_str("m/44'/0'").unwrap(); +//! let mut client = Client::discover_socket().unwrap(); +//! let xprv = client.request_xprv::(&derivation_path).unwrap(); +//! keyforkd::test_util::Infallible::Ok(()) +//! }).unwrap(); +//! ``` pub use std::os::unix::net::UnixStream; use std::{collections::HashMap, path::PathBuf}; diff --git a/crates/daemon/keyforkd/README.md b/crates/daemon/keyforkd/README.md index 8d139fa..5177639 100644 --- a/crates/daemon/keyforkd/README.md +++ b/crates/daemon/keyforkd/README.md @@ -34,3 +34,15 @@ request, as well as its best-effort guess on what path is being derived (using the `keyfork-derive-path-data` crate), to inform the user of what keys are requested. Once the server sends the client the new extended private key, the client can then choose to use the key as-is, or derive further keys. + +## Testing + +A Keyfork server can be automatically started by using [`test_util::run_test`]. +The function accepts a closure, starting the server before the closure is run, +and closing the server after the closure has completed. This may be useful for +people writing software that interacts with the Keyfork server, such as a +deriver or a provisioner. A test seed must be provided, but can be any content. +The closure accepts one argument, the path of the UNIX socket from which the +server can be accessed. + +Examples of the test utility can be seen in the `keyforkd-client` crate. diff --git a/crates/daemon/keyforkd/src/test_util.rs b/crates/daemon/keyforkd/src/test_util.rs index 5e7fdb7..692c9b9 100644 --- a/crates/daemon/keyforkd/src/test_util.rs +++ b/crates/daemon/keyforkd/src/test_util.rs @@ -25,14 +25,25 @@ pub struct InfallibleError { /// ``` pub type Infallible = std::result::Result; -/// Run a test making use of a Keyforkd server. The path to the socket of the Keyforkd server is -/// provided as the only argument to the closure. The closure is expected to return a Result; the -/// Error field of the Result may be an error returned by a test. +/// 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 +/// the server is capable of being interacted with. The test is in the form of a closure, expected +/// to return a [`Result`] where success is a unit type (test passed) and the error is any error +/// that happened during the test (alternatively, a panic may be used, and will be returned as an +/// error). /// /// # Panics +/// The function may panic if any errors arise while configuring and using the Tokio multithreaded +/// runtime. /// -/// The function is not expected to run in production; therefore, the function plays "fast and -/// loose" wih the usage of [`Result::expect`]. In normal usage, these should never be an issue. +/// # Examples +/// ```rust +/// use std::os::unix::net::UnixStream; +/// let seed = b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"; +/// keyforkd::test_util::run_test(seed.as_slice(), |path| { +/// UnixStream::connect(&path).map(|_| ()) +/// }).unwrap(); +/// ``` #[allow(clippy::missing_errors_doc)] pub fn run_test(seed: &[u8], closure: F) -> Result<(), E> where diff --git a/crates/derive/keyfork-derive-openpgp/src/lib.rs b/crates/derive/keyfork-derive-openpgp/src/lib.rs index a5e0232..f77977b 100644 --- a/crates/derive/keyfork-derive-openpgp/src/lib.rs +++ b/crates/derive/keyfork-derive-openpgp/src/lib.rs @@ -1,4 +1,4 @@ -//! Creation of OpenPGP certificates from BIP-0032 derived data. +//! Creation of OpenPGP Transferable Secret Keys from BIP-0032 derived data. use std::{ str::FromStr, diff --git a/crates/util/keyfork-entropy/src/lib.rs b/crates/util/keyfork-entropy/src/lib.rs index 8f1580f..061e03a 100644 --- a/crates/util/keyfork-entropy/src/lib.rs +++ b/crates/util/keyfork-entropy/src/lib.rs @@ -57,10 +57,21 @@ fn ensure_offline() { /// # std::env::set_var("SHOOT_SELF_IN_FOOT", "1"); /// keyfork_entropy::ensure_safe(); /// ``` +/// +/// When running on a system that's online, or running an outdated kernel: +/// +/// ```rust,should_panic +/// # // NOTE: sometimes, the environment variable is set, for testing purposes. I'm not sure how +/// # // to un-set it. Set it to a sentinel value. +/// # std::env::set_var("SHOOT_SELF_IN_FOOT", "test-must-fail"); +/// # std::env::set_var("INSECURE_HARDWARE_ALLOWED", "test-must-fail"); +/// keyfork_entropy::ensure_safe(); +/// ``` pub fn ensure_safe() { - if !std::env::vars() - .any(|(name, _)| name == "SHOOT_SELF_IN_FOOT" || name == "INSECURE_HARDWARE_ALLOWED") - { + if !std::env::vars().any(|(name, value)| { + (name == "SHOOT_SELF_IN_FOOT" || name == "INSECURE_HARDWARE_ALLOWED") + && value != "test-must-fail" + }) { ensure_safe_kernel_version(); ensure_offline(); } diff --git a/crates/util/keyfork-frame/src/lib.rs b/crates/util/keyfork-frame/src/lib.rs index 4e39a45..ed54846 100644 --- a/crates/util/keyfork-frame/src/lib.rs +++ b/crates/util/keyfork-frame/src/lib.rs @@ -66,6 +66,11 @@ pub(crate) fn hash(data: &[u8]) -> Vec { /// # Errors /// An error may be returned if the given `data` is more than [`u32::MAX`] bytes. This is a /// constraint on a protocol level. +/// +/// # Examples +/// ```rust +/// let data = keyfork_frame::try_encode(b"hello world!".as_slice()).unwrap(); +/// ``` pub fn try_encode(data: &[u8]) -> Result, EncodeError> { let mut output = vec![]; try_encode_to(data, &mut output)?; @@ -77,6 +82,12 @@ pub fn try_encode(data: &[u8]) -> Result, EncodeError> { /// # Errors /// An error may be returned if the givenu `data` is more than [`u32::MAX`] bytes, or if the writer /// is unable to write data. +/// +/// # Examples +/// ```rust +/// let mut output = vec![]; +/// keyfork_frame::try_encode_to(b"hello world!".as_slice(), &mut output).unwrap(); +/// ``` pub fn try_encode_to(data: &[u8], writable: &mut impl Write) -> Result<(), EncodeError> { let hash = hash(data); let len = hash.len() + data.len(); @@ -107,18 +118,40 @@ pub(crate) fn verify_checksum(data: &[u8]) -> Result<&[u8], DecodeError> { /// * The given `data` does not contain enough data to parse a length, /// * The given `data` does not contain the given length's worth of data, /// * The given `data` has a checksum that does not match what we build locally. +/// +/// # Examples +/// ```rust +/// let input = b"hello world!"; +/// let encoded = keyfork_frame::try_encode(input.as_slice()).unwrap(); +/// let decoded = keyfork_frame::try_decode(&encoded).unwrap(); +/// assert_eq!(input.as_slice(), decoded.as_slice()); +/// ``` pub fn try_decode(data: &[u8]) -> Result, DecodeError> { try_decode_from(&mut &data[..]) } /// Read and decode a framed message into a `Vec`. /// +/// Note that unlike [`try_encode_to`], this method does not allow writing to an object +/// implementing Write. This is because the data must be stored entirely in memory to allow +/// verifying the data. The data is then returned using the same in-memory representation as is +/// used in memory, and a caller may then choose to use `writable.write_all()`. +/// /// # Errors /// An error may be returned if: /// * The given `data` does not contain enough data to parse a length, /// * The given `data` does not contain the given length's worth of data, /// * The given `data` has a checksum that does not match what we build locally. /// * The source for the data returned an error. +/// +/// # Examples +/// ```rust +/// let input = b"hello world!"; +/// let mut encoded = vec![]; +/// keyfork_frame::try_encode_to(input.as_slice(), &mut encoded).unwrap(); +/// let decoded = keyfork_frame::try_decode_from(&mut &encoded[..]).unwrap(); +/// assert_eq!(input.as_slice(), decoded.as_slice()); +/// ``` pub fn try_decode_from(readable: &mut impl Read) -> Result, DecodeError> { let mut bytes = 0u32.to_be_bytes(); readable.read_exact(&mut bytes)?; diff --git a/crates/util/keyfork-mnemonic-util/src/lib.rs b/crates/util/keyfork-mnemonic-util/src/lib.rs index b1eb04b..f5d22cb 100644 --- a/crates/util/keyfork-mnemonic-util/src/lib.rs +++ b/crates/util/keyfork-mnemonic-util/src/lib.rs @@ -231,6 +231,13 @@ impl Mnemonic { /// /// # Errors /// An error may be returned if the entropy is not within the acceptable lengths. + /// + /// # Examples + /// ```rust + /// use keyfork_mnemonic_util::Mnemonic; + /// let data = b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"; + /// let mnemonic = Mnemonic::from_entropy(data.as_slice(), Default::default()).unwrap(); + /// ``` pub fn from_entropy( bytes: &[u8], wordlist: Arc, @@ -248,11 +255,36 @@ impl Mnemonic { Ok(unsafe { Self::from_raw_entropy(bytes, wordlist) }) } + /// Create a Mnemonic using an arbitrary length of given entropy. The length does not need to + /// conform to BIP-0039 standards. + /// /// # Safety /// /// This function can potentially produce mnemonics that are not BIP-0039 compliant or can't /// properly be encoded as a mnemonic. It is assumed the caller asserts the byte count is `% 4 - /// == 0`. + /// == 0`. If the assumption is incorrect, code may panic. + /// + /// # Examples + /// ```rust + /// use keyfork_mnemonic_util::Mnemonic; + /// let data = b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"; + /// let mnemonic = unsafe { Mnemonic::from_raw_entropy(data.as_slice(), Default::default()) }; + /// let mnemonic_text = mnemonic.to_string(); + /// ``` + /// + /// If given an invalid length, undefined behavior may follow, or code may panic. + /// + /// ```rust,should_panic + /// use keyfork_mnemonic_util::Mnemonic; + /// use std::str::FromStr; + /// + /// // NOTE: Data is of invalid length, 31 + /// let data = b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"; + /// let mnemonic = unsafe { Mnemonic::from_raw_entropy(data.as_slice(), Default::default()) }; + /// let mnemonic_text = mnemonic.to_string(); + /// // NOTE: panic happens here + /// let new_mnemonic = Mnemonic::from_str(&mnemonic_text).unwrap(); + /// ``` pub unsafe fn from_raw_entropy(bytes: &[u8], wordlist: Arc) -> Mnemonic { Mnemonic { entropy: bytes.to_vec(), @@ -260,22 +292,22 @@ impl Mnemonic { } } - /// The internal representation of the decoded data. + /// A view to internal representation of the decoded data. pub fn as_bytes(&self) -> &[u8] { &self.entropy } - /// The internal representation of the decoded data, as a [`Vec`]. + /// A clone of the internal representation of the decoded data. pub fn to_bytes(&self) -> Vec { self.entropy.to_vec() } - /// Drop self, returning the decoded data. + /// Conver the Mnemonic into the internal representation of the decoded data. pub fn into_bytes(self) -> Vec { self.entropy } - /// Clone the existing entropy. + /// Clone the existing data. #[deprecated = "Use as_bytes(), to_bytes(), or into_bytes() instead"] pub fn entropy(&self) -> Vec { self.entropy.clone() diff --git a/crates/util/smex/src/lib.rs b/crates/util/smex/src/lib.rs index 88a6912..9eec990 100644 --- a/crates/util/smex/src/lib.rs +++ b/crates/util/smex/src/lib.rs @@ -28,7 +28,15 @@ impl std::fmt::Display for DecodeError { impl std::error::Error for DecodeError {} /// Encode a given input as a hex string. -pub fn encode(input: &[u8]) -> String { +/// +/// # Examples +/// ```rust +/// let data = b"hello world!"; +/// let result = smex::encode(&data); +/// assert_eq!(result, "68656c6c6f20776f726c6421"); +/// ``` +pub fn encode(input: impl AsRef<[u8]>) -> String { + let input = input.as_ref(); let mut s = String::new(); for byte in input { write!(s, "{byte:02x}").unwrap(); @@ -50,7 +58,26 @@ fn val(c: u8) -> Result { /// # Errors /// The function may error if a non-hex character is encountered or if the character count is not /// evenly divisible by two. -pub fn decode(input: &str) -> Result, DecodeError> { +/// +/// # Examples +/// ```rust +/// let data = b"hello world!"; +/// let encoded = smex::encode(&data); +/// let decoded = smex::decode(&encoded).unwrap(); +/// assert_eq!(data.as_slice(), decoded.as_slice()); +/// ``` +/// +/// The function may return an error if the given input is not valid hex. +/// +/// ```rust,should_panic +/// let data = b"hello world!"; +/// let mut encoded = smex::encode(&data); +/// encoded.push('G'); +/// let decoded = smex::decode(&encoded).unwrap(); +/// assert_eq!(data.as_slice(), decoded.as_slice()); +/// ``` +pub fn decode(input: impl AsRef) -> Result, DecodeError> { + let input = input.as_ref(); let len = input.len(); if len % 2 != 0 { return Err(DecodeError::InvalidCharacterCount(len));