Clarify documentation and add more examples

Note: The type signature of smex::encode and smex::decode has changed,
but will still accept values that were previously passed in.
This commit is contained in:
Ryan Heywood 2024-02-18 17:57:24 -05:00
parent 33405ee4fc
commit ece9f435d2
Signed by: ryan
GPG Key ID: 8E401478A3FBEF72
8 changed files with 161 additions and 16 deletions

View File

@ -40,6 +40,25 @@
//! # keyforkd::test_util::Infallible::Ok(()) //! # keyforkd::test_util::Infallible::Ok(())
//! # }).unwrap(); //! # }).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::<PrivateKey>(&derivation_path).unwrap();
//! keyforkd::test_util::Infallible::Ok(())
//! }).unwrap();
//! ```
pub use std::os::unix::net::UnixStream; pub use std::os::unix::net::UnixStream;
use std::{collections::HashMap, path::PathBuf}; use std::{collections::HashMap, path::PathBuf};

View File

@ -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 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 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. 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.

View File

@ -25,14 +25,25 @@ pub struct InfallibleError {
/// ``` /// ```
pub type Infallible<T> = std::result::Result<T, InfallibleError>; pub type Infallible<T> = std::result::Result<T, InfallibleError>;
/// Run a test making use of a Keyforkd server. The path to the socket of the Keyforkd server is /// Run a test making use of a Keyforkd server. The test may use a seed (the first argument) from a
/// provided as the only argument to the closure. The closure is expected to return a Result; the /// test suite, or (as shown in the example below) a simple seed may be used solely to ensure
/// Error field of the Result may be an error returned by a test. /// 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 /// # 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 /// # Examples
/// loose" wih the usage of [`Result::expect`]. In normal usage, these should never be an issue. /// ```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)] #[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

View File

@ -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::{ use std::{
str::FromStr, str::FromStr,

View File

@ -57,10 +57,21 @@ fn ensure_offline() {
/// # std::env::set_var("SHOOT_SELF_IN_FOOT", "1"); /// # std::env::set_var("SHOOT_SELF_IN_FOOT", "1");
/// keyfork_entropy::ensure_safe(); /// 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() { pub fn ensure_safe() {
if !std::env::vars() if !std::env::vars().any(|(name, value)| {
.any(|(name, _)| name == "SHOOT_SELF_IN_FOOT" || name == "INSECURE_HARDWARE_ALLOWED") (name == "SHOOT_SELF_IN_FOOT" || name == "INSECURE_HARDWARE_ALLOWED")
{ && value != "test-must-fail"
}) {
ensure_safe_kernel_version(); ensure_safe_kernel_version();
ensure_offline(); ensure_offline();
} }

View File

@ -66,6 +66,11 @@ pub(crate) fn hash(data: &[u8]) -> Vec<u8> {
/// # Errors /// # Errors
/// An error may be returned if the given `data` is more than [`u32::MAX`] bytes. This is a /// An error may be returned if the given `data` is more than [`u32::MAX`] bytes. This is a
/// constraint on a protocol level. /// 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<Vec<u8>, EncodeError> { pub fn try_encode(data: &[u8]) -> Result<Vec<u8>, EncodeError> {
let mut output = vec![]; let mut output = vec![];
try_encode_to(data, &mut output)?; try_encode_to(data, &mut output)?;
@ -77,6 +82,12 @@ pub fn try_encode(data: &[u8]) -> Result<Vec<u8>, EncodeError> {
/// # Errors /// # Errors
/// An error may be returned if the givenu `data` is more than [`u32::MAX`] bytes, or if the writer /// An error may be returned if the givenu `data` is more than [`u32::MAX`] bytes, or if the writer
/// is unable to write data. /// 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> { pub fn try_encode_to(data: &[u8], writable: &mut impl Write) -> Result<(), EncodeError> {
let hash = hash(data); let hash = hash(data);
let len = hash.len() + data.len(); 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 enough data to parse a length,
/// * The given `data` does not contain the given length's worth of data, /// * 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 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<Vec<u8>, DecodeError> { pub fn try_decode(data: &[u8]) -> Result<Vec<u8>, DecodeError> {
try_decode_from(&mut &data[..]) try_decode_from(&mut &data[..])
} }
/// Read and decode a framed message into a `Vec<u8>`. /// Read and decode a framed message into a `Vec<u8>`.
/// ///
/// 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 /// # Errors
/// An error may be returned if: /// An error may be returned if:
/// * The given `data` does not contain enough data to parse a length, /// * 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` 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 given `data` has a checksum that does not match what we build locally.
/// * The source for the data returned an error. /// * 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<Vec<u8>, DecodeError> { pub fn try_decode_from(readable: &mut impl Read) -> Result<Vec<u8>, DecodeError> {
let mut bytes = 0u32.to_be_bytes(); let mut bytes = 0u32.to_be_bytes();
readable.read_exact(&mut bytes)?; readable.read_exact(&mut bytes)?;

View File

@ -231,6 +231,13 @@ impl Mnemonic {
/// ///
/// # Errors /// # Errors
/// An error may be returned if the entropy is not within the acceptable lengths. /// 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( pub fn from_entropy(
bytes: &[u8], bytes: &[u8],
wordlist: Arc<Wordlist>, wordlist: Arc<Wordlist>,
@ -248,11 +255,36 @@ impl Mnemonic {
Ok(unsafe { Self::from_raw_entropy(bytes, wordlist) }) 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 /// # Safety
/// ///
/// This function can potentially produce mnemonics that are not BIP-0039 compliant or can't /// 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 /// 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<Wordlist>) -> Mnemonic { pub unsafe fn from_raw_entropy(bytes: &[u8], wordlist: Arc<Wordlist>) -> Mnemonic {
Mnemonic { Mnemonic {
entropy: bytes.to_vec(), 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] { pub fn as_bytes(&self) -> &[u8] {
&self.entropy &self.entropy
} }
/// The internal representation of the decoded data, as a [`Vec<u8>`]. /// A clone of the internal representation of the decoded data.
pub fn to_bytes(&self) -> Vec<u8> { pub fn to_bytes(&self) -> Vec<u8> {
self.entropy.to_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<u8> { pub fn into_bytes(self) -> Vec<u8> {
self.entropy self.entropy
} }
/// Clone the existing entropy. /// Clone the existing data.
#[deprecated = "Use as_bytes(), to_bytes(), or into_bytes() instead"] #[deprecated = "Use as_bytes(), to_bytes(), or into_bytes() instead"]
pub fn entropy(&self) -> Vec<u8> { pub fn entropy(&self) -> Vec<u8> {
self.entropy.clone() self.entropy.clone()

View File

@ -28,7 +28,15 @@ impl std::fmt::Display for DecodeError {
impl std::error::Error for DecodeError {} impl std::error::Error for DecodeError {}
/// Encode a given input as a hex string. /// 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(); let mut s = String::new();
for byte in input { for byte in input {
write!(s, "{byte:02x}").unwrap(); write!(s, "{byte:02x}").unwrap();
@ -50,7 +58,26 @@ fn val(c: u8) -> Result<u8, DecodeError> {
/// # Errors /// # Errors
/// The function may error if a non-hex character is encountered or if the character count is not /// The function may error if a non-hex character is encountered or if the character count is not
/// evenly divisible by two. /// evenly divisible by two.
pub fn decode(input: &str) -> Result<Vec<u8>, 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<str>) -> Result<Vec<u8>, DecodeError> {
let input = input.as_ref();
let len = input.len(); let len = input.len();
if len % 2 != 0 { if len % 2 != 0 {
return Err(DecodeError::InvalidCharacterCount(len)); return Err(DecodeError::InvalidCharacterCount(len));