all crates: add documentation

This commit is contained in:
Ryan Heywood 2024-01-15 21:44:48 -05:00
parent c8f255f0aa
commit 701f5ca4e9
Signed by: ryan
GPG Key ID: 8E401478A3FBEF72
53 changed files with 518 additions and 14 deletions

View File

@ -59,7 +59,7 @@ Note: The following features are proposed, and may not yet be implemented.
* Unpredictable * Unpredictable
* Generate a BIP39 phrase from OS or physicalized entropy * Generate a BIP39 phrase from OS or physicalized entropy
* Provide and use BIP39 passphrase from user supplied entropy * Provide and use BIP39 passphrase from user supplied entropy
* Read up on [https://milksad.info](milksad) to understand why this matters! * Read up on [milksad](https://milksad.info) to understand why this matters!
* Deterministic * Deterministic
* Given the same seed, repeated derivation requests will be reproducible * Given the same seed, repeated derivation requests will be reproducible
* Any secret data can be derived again at any point in the future * Any secret data can be derived again at any point in the future

View File

@ -1,3 +1,5 @@
//! A client for Keyforkd.
use std::{collections::HashMap, os::unix::net::UnixStream, path::PathBuf}; use std::{collections::HashMap, os::unix::net::UnixStream, path::PathBuf};
use keyfork_frame::{try_decode_from, try_encode_to, DecodeError, EncodeError}; use keyfork_frame::{try_decode_from, try_encode_to, DecodeError, EncodeError};
@ -6,32 +8,46 @@ use keyforkd_models::{Request, Response, Error as KeyforkdError};
#[cfg(test)] #[cfg(test)]
mod tests; mod tests;
/// An error occurred while interacting with Keyforkd.
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]
pub enum Error { pub enum Error {
/// The environment variables used for determining a Keyforkd socket path were not set.
#[error("Neither KEYFORK_SOCKET_PATH nor XDG_RUNTIME_DIR were set")] #[error("Neither KEYFORK_SOCKET_PATH nor XDG_RUNTIME_DIR were set")]
EnvVarsNotFound, 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)")] #[error("Socket was unable to connect to {1}: {0} (make sure keyforkd is running)")]
Connect(std::io::Error, PathBuf), 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}")] #[error("Could not write to or from the socket: {0}")]
Io(#[from] std::io::Error), 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}")] #[error("Could not perform bincode transformation: {0}")]
Bincode(#[from] Box<bincode::ErrorKind>), Bincode(#[from] Box<bincode::ErrorKind>),
/// A frame could not be encoded from the given data.
#[error("Could not perform frame transformation: {0}")] #[error("Could not perform frame transformation: {0}")]
FrameEnc(#[from] EncodeError), FrameEnc(#[from] EncodeError),
/// A frame could not be decoded from the given data.
#[error("Could not perform frame transformation: {0}")] #[error("Could not perform frame transformation: {0}")]
FrameDec(#[from] DecodeError), FrameDec(#[from] DecodeError),
/// An error encountered in Keyforkd.
#[error("Error in Keyforkd: {0}")] #[error("Error in Keyforkd: {0}")]
Keyforkd(#[from] KeyforkdError) Keyforkd(#[from] KeyforkdError)
} }
#[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>;
/// 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> { pub fn get_socket() -> Result<UnixStream, Error> {
let socket_vars = std::env::vars() let socket_vars = std::env::vars()
.filter(|(key, _)| ["XDG_RUNTIME_DIR", "KEYFORKD_SOCKET_PATH"].contains(&key.as_str())) .filter(|(key, _)| ["XDG_RUNTIME_DIR", "KEYFORKD_SOCKET_PATH"].contains(&key.as_str()))
@ -71,11 +87,21 @@ impl Client {
} }
/// Create a new client using well-known socket locations. /// 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.
pub fn discover_socket() -> Result<Self> { pub fn discover_socket() -> Result<Self> {
get_socket().map(|socket| Self { socket }) get_socket().map(|socket| Self { socket })
} }
/// Serialize and send a [`Request`] to the server, awaiting a [`Result<Response>`]. /// Serialize and send a [`Request`] to the server, awaiting a [`Result<Response>`].
///
/// # 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> { 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

@ -28,23 +28,30 @@ impl From<(DerivationRequest, String)> for Request {
} }
} }
/// Any error that could occur while deriving a key with Keyforkd.
#[derive(thiserror::Error, Clone, Debug, Serialize, Deserialize)] #[derive(thiserror::Error, Clone, Debug, Serialize, Deserialize)]
pub enum DerivationError { pub enum DerivationError {
/// The TTY used for pinentry or passphrase entry was invalid.
#[error("The provided TTY was not valid")] #[error("The provided TTY was not valid")]
InvalidTTY, InvalidTTY,
/// No TTY was required for pinentry, but was not provided.
#[error("A TTY was required by the pinentry program but was not provided")] #[error("A TTY was required by the pinentry program but was not provided")]
NoTTY, NoTTY,
#[error("Invalid derivation length: Expected 2, actual: {0}")] /// The derivation length was invalid, must be at least 2 indexes long.
#[error("Invalid derivation length: Expected at least 2, actual: {0}")]
InvalidDerivationLength(usize), InvalidDerivationLength(usize),
/// An error occurred while deriving data.
#[error("Derivation error: {0}")] #[error("Derivation error: {0}")]
Derivation(String), Derivation(String),
} }
/// An error that could occur while interacting with Keyforkd.
#[derive(thiserror::Error, Clone, Debug, Serialize, Deserialize)] #[derive(thiserror::Error, Clone, Debug, Serialize, Deserialize)]
pub enum Error { pub enum Error {
/// An error occurred while processing a derivation request.
#[error(transparent)] #[error(transparent)]
Derivation(#[from] DerivationError), Derivation(#[from] DerivationError),
} }
@ -64,6 +71,7 @@ pub enum Response {
Derivation(DerivationResponse), Derivation(DerivationResponse),
} }
/// Attempting to convert from a [`DerivationResponse`] to a [`Response`]
#[derive(thiserror::Error, Debug)] #[derive(thiserror::Error, Debug)]
#[error("Unable to downcast to {0}")] #[error("Unable to downcast to {0}")]
pub struct Downcast(&'static str); pub struct Downcast(&'static str);

View File

@ -1,7 +1,9 @@
use thiserror::Error; use thiserror::Error;
/// An error occurred while starting the Keyfork server.
#[derive(Debug, Clone, Error)] #[derive(Debug, Clone, Error)]
pub enum Keyforkd { pub enum Keyforkd {
/// The required environment variables were not set and a socket could not be mounted.
#[error("Neither KEYFORKD_SOCKET_PATH nor XDG_RUNTIME_DIR were set, nowhere to mount socket")] #[error("Neither KEYFORKD_SOCKET_PATH nor XDG_RUNTIME_DIR were set, nowhere to mount socket")]
NoSocketPath, NoSocketPath,
} }

View File

@ -1,3 +1,18 @@
//! ## The Keyfork server.
//!
//! The server uses a [`keyfork_frame`]'d [`bincode`]'d request+response format and can be
//! interacted with by using the `keyforkd_client` crate.
//!
//! All requests made to Keyfork are required to list at least two derivation paths. This helps
//! prevent cases where the master seed or the general protocol seed are leaked by a client. An
//! example is BIP-0044, where the path used is `m/44'/0'` for Bitcoin, and often `m/44'/60'` is
//! used for Ethereum. To prevent an Ethereum wallet from deriving the Bitcoin coin seed, and to
//! prevent leaking the master seed in general, all requests must contain at least two paths.
//!
//! Additionally, this ensures that keys are not reused across separate purposes. Because keys are
//! required to have at least two indexes, they are drawn to the pattern of using the first index
//! as the key's purpose, such as `m/ pgp'` for OpenPGP.
use std::{ use std::{
collections::HashMap, collections::HashMap,
path::{Path, PathBuf}, path::{Path, PathBuf},
@ -17,7 +32,10 @@ use tracing_subscriber::{
registry, registry,
}; };
/// Errors occurring while starting Keyforkd.
pub mod error; pub mod error;
/// Middleware used by Keyforkd.
pub mod middleware; pub mod middleware;
pub mod server; pub mod server;
pub mod service; pub mod service;
@ -25,6 +43,7 @@ pub use error::Keyforkd as KeyforkdError;
pub use server::UnixServer; pub use server::UnixServer;
pub use service::Keyforkd; pub use service::Keyforkd;
/// Set up a Tracing subscriber, defaulting to debug mode.
#[cfg(feature = "tracing")] #[cfg(feature = "tracing")]
pub fn setup_registry() { pub fn setup_registry() {
let envfilter = EnvFilter::builder() let envfilter = EnvFilter::builder()
@ -37,6 +56,11 @@ pub fn setup_registry() {
.init(); .init();
} }
/// Start and run a server on a given socket path.
///
/// # Errors
/// The function may return an error if a socket can't be bound, if the service can't be created,
/// or if the server encounters an unrecoverable error while running.
pub async fn start_and_run_server_on( pub async fn start_and_run_server_on(
mnemonic: Mnemonic, mnemonic: Mnemonic,
socket_path: &Path, socket_path: &Path,
@ -66,6 +90,12 @@ pub async fn start_and_run_server_on(
Ok(()) Ok(())
} }
/// Start and run the server using a discovered socket location.
///
/// # Errors
/// The function may return an error if the socket location could not be guessed, if a socket can't
/// be bound, if the service can't be created, or if the server encounters an unrecoverable error
/// while running.
pub async fn start_and_run_server(mnemonic: Mnemonic) -> Result<(), Box<dyn std::error::Error>> { pub async fn start_and_run_server(mnemonic: Mnemonic) -> Result<(), Box<dyn std::error::Error>> {
let runtime_vars = std::env::vars() let runtime_vars = std::env::vars()
.filter(|(key, _)| ["XDG_RUNTIME_DIR", "KEYFORKD_SOCKET_PATH"].contains(&key.as_str())) .filter(|(key, _)| ["XDG_RUNTIME_DIR", "KEYFORKD_SOCKET_PATH"].contains(&key.as_str()))

View File

@ -1,3 +1,5 @@
//!
use keyfork_mnemonic_util::Mnemonic; use keyfork_mnemonic_util::Mnemonic;
use tokio::io::{self, AsyncBufReadExt, BufReader}; use tokio::io::{self, AsyncBufReadExt, BufReader};

View File

@ -5,12 +5,14 @@ use serde::{de::DeserializeOwned, Serialize};
use thiserror::Error; use thiserror::Error;
use tower::{Layer, Service}; use tower::{Layer, Service};
/// Layer a [`BincodeService`] upon another Service.
pub struct BincodeLayer<'a, Request> { pub struct BincodeLayer<'a, Request> {
phantom: PhantomData<&'a ()>, phantom: PhantomData<&'a ()>,
phantom_request: PhantomData<&'a Request>, phantom_request: PhantomData<&'a Request>,
} }
impl<'a, Request> BincodeLayer<'a, Request> { impl<'a, Request> BincodeLayer<'a, Request> {
/// Create a new [`BincodeLayer`].
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
phantom: PhantomData, phantom: PhantomData,
@ -36,20 +38,25 @@ impl<'a, S: 'a, Request> Layer<S> for BincodeLayer<'a, Request> {
} }
} }
/// Transform a Bincode-serialized type to a Rust type.
#[derive(Clone)] #[derive(Clone)]
pub struct BincodeService<S, Request> { pub struct BincodeService<S, Request> {
service: S, service: S,
phantom_request: PhantomData<Request>, phantom_request: PhantomData<Request>,
} }
/// An error encountered either while transforming data or against the interior Service.
#[derive(Debug, Error)] #[derive(Debug, Error)]
pub enum BincodeServiceError { pub enum BincodeServiceError {
/// An error occurred while polling the internal service.
#[error("Error while polling: {0}")] #[error("Error while polling: {0}")]
Poll(String), Poll(String),
/// An error occurred while calling the internal service.
#[error("Error while calling: {0}")] #[error("Error while calling: {0}")]
Call(String), Call(String),
/// An error occurred while converting to or from bincode.
#[error("Error while converting: {0}")] #[error("Error while converting: {0}")]
Convert(String), Convert(String),
} }

View File

@ -1,3 +1,5 @@
//! A UNIX socket server to run a Tower Service.
use keyfork_frame::asyncext::{try_decode_from, try_encode_to}; use keyfork_frame::asyncext::{try_decode_from, try_encode_to};
use std::{ use std::{
io::Error, io::Error,
@ -9,12 +11,17 @@ use tower::{Service, ServiceExt};
#[cfg(feature = "tracing")] #[cfg(feature = "tracing")]
use tracing::debug; use tracing::debug;
/// A UNIX Socket Server.
#[allow(clippy::module_name_repetitions)] #[allow(clippy::module_name_repetitions)]
pub struct UnixServer { pub struct UnixServer {
listener: UnixListener, listener: UnixListener,
} }
impl UnixServer { impl UnixServer {
/// Bind a socket to the given `address` and create a [`UnixServer`]. This function also creates a ctrl_c handler to automatically clean up the socket file.
///
/// # Errors
/// This function may return an error if the socket can't be bound.
pub fn bind(address: impl AsRef<Path>) -> Result<Self, Error> { pub fn bind(address: impl AsRef<Path>) -> Result<Self, Error> {
let mut path = PathBuf::new(); let mut path = PathBuf::new();
path.extend(address.as_ref().components()); path.extend(address.as_ref().components());
@ -40,6 +47,11 @@ impl UnixServer {
}) })
} }
/// Given a Service, accept clients and use their input to call the Service.
///
/// # Errors
/// The method may return an error if the server becomes unable to accept new connections.
/// Errors while the server is running are logged using the `tracing` crate.
pub async fn run<S, R>(&mut self, app: S) -> Result<(), Box<dyn std::error::Error>> pub async fn run<S, R>(&mut self, app: S) -> Result<(), Box<dyn std::error::Error>>
where where
S: Service<R> + Clone + Send + 'static, S: Service<R> + Clone + Send + 'static,

View File

@ -1,3 +1,9 @@
//! ## The Keyfork Service.
//!
//! The Keyfork service performs the following operations:
//!
//! * Derivation of data from a preconfigured seed, with a derivation path of at least two indexes.
#![allow(clippy::implicit_clone)] #![allow(clippy::implicit_clone)]
use std::{future::Future, pin::Pin, sync::Arc, task::Poll}; use std::{future::Future, pin::Pin, sync::Arc, task::Poll};
@ -11,12 +17,15 @@ use tracing::info;
// NOTE: All values implemented in Keyforkd must implement Clone with low overhead, either by // NOTE: All values implemented in Keyforkd must implement Clone with low overhead, either by
// using an Arc or by having a small signature. This is because Service<T> takes &mut self. // using an Arc or by having a small signature. This is because Service<T> takes &mut self.
// //
/// The Keyfork Service.
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct Keyforkd { pub struct Keyforkd {
seed: Arc<Vec<u8>>, seed: Arc<Vec<u8>>,
} }
impl Keyforkd { impl Keyforkd {
/// Create a new instance of Keyfork from a given seed.
pub fn new(seed: Vec<u8>) -> Self { pub fn new(seed: Vec<u8>) -> Self {
Self { Self {
seed: Arc::new(seed), seed: Arc::new(seed),

View File

@ -1,3 +1,5 @@
//!
use std::{env, process::ExitCode, str::FromStr}; use std::{env, process::ExitCode, str::FromStr};
use keyfork_derive_util::{ use keyfork_derive_util::{
@ -6,18 +8,23 @@ use keyfork_derive_util::{
}; };
use keyforkd_client::Client; use keyforkd_client::Client;
/// Any error that can occur while deriving a key.
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]
pub enum Error { pub enum Error {
/// The given algorithm could not be parsed.
#[error("Could not parse the given algorithm {0:?}: {1}")] #[error("Could not parse the given algorithm {0:?}: {1}")]
AlgoFormat(String, DerivationError), AlgoFormat(String, DerivationError),
/// The given path could not be parsed.
#[error("Could not parse the given path: {0}")] #[error("Could not parse the given path: {0}")]
PathFormat(#[from] keyfork_derive_util::path::Error), PathFormat(#[from] keyfork_derive_util::path::Error),
/// The request to derive data failed.
#[error("Unable to perform key derivation request: {0}")] #[error("Unable to perform key derivation request: {0}")]
KeyforkdClient(#[from] keyforkd_client::Error), KeyforkdClient(#[from] keyforkd_client::Error),
} }
#[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>;
fn validate(algo: &str, path: &str) -> Result<(DerivationAlgorithm, DerivationPath)> { fn validate(algo: &str, path: &str) -> Result<(DerivationAlgorithm, DerivationPath)> {

View File

@ -5,6 +5,9 @@ edition = "2021"
license = "AGPL-3.0-only" license = "AGPL-3.0-only"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[features]
default = []
bin = ["sequoia-openpgp/crypto-nettle"]
[dependencies] [dependencies]
keyfork-derive-util = { version = "0.1.0", path = "../keyfork-derive-util", default-features = false, features = ["ed25519"] } keyfork-derive-util = { version = "0.1.0", path = "../keyfork-derive-util", default-features = false, features = ["ed25519"] }

View File

@ -1,3 +1,5 @@
//! Creation of OpenPGP certificates from BIP-0032 derived data.
use std::time::{Duration, SystemTime, SystemTimeError}; use std::time::{Duration, SystemTime, SystemTimeError};
use derive_util::{ use derive_util::{
@ -17,35 +19,52 @@ use sequoia_openpgp::{
}; };
pub use sequoia_openpgp as openpgp; pub use sequoia_openpgp as openpgp;
/// An error occurred while creating an OpenPGP key.
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]
pub enum Error { pub enum Error {
/// An error occurred with the internal OpenPGP library.
#[error("{0}")] #[error("{0}")]
Anyhow(#[from] anyhow::Error), Anyhow(#[from] anyhow::Error),
/// The key was configured with both encryption and non-encryption key flags. Keys can either
/// support Ed25519 signatures or Curve25519 ECDH.
#[error("Key configured with both encryption and non-encryption key flags: {0:?}")] #[error("Key configured with both encryption and non-encryption key flags: {0:?}")]
InvalidKeyFlags(KeyFlags), InvalidKeyFlags(KeyFlags),
/// The derivation response contained incorrect data.
#[error("Incorrect derived data: {0}")] #[error("Incorrect derived data: {0}")]
IncorrectDerivedData(#[from] TryFromDerivationResponseError), IncorrectDerivedData(#[from] TryFromDerivationResponseError),
/// A derivation index could not be created from the given index.
#[error("Could not create derivation index: {0}")] #[error("Could not create derivation index: {0}")]
Index(#[from] keyfork_derive_util::index::Error), Index(#[from] keyfork_derive_util::index::Error),
/// A derivation operation could not be performed against the private key.
#[error("Could not perform operation against private key: {0}")] #[error("Could not perform operation against private key: {0}")]
PrivateKey(#[from] keyfork_derive_util::extended_key::private_key::Error), PrivateKey(#[from] keyfork_derive_util::extended_key::private_key::Error),
/// The operation involving system time was invalid. This means the system clock moved a
/// significant amount of time during the operation.
#[error("Invalid system time: {0}")] #[error("Invalid system time: {0}")]
SystemTime(#[from] SystemTimeError), SystemTime(#[from] SystemTimeError),
/// The first certificate in an OpenPGP keychain must have the Certify capability.
#[error("First key in certificate must have certify capability")] #[error("First key in certificate must have certify capability")]
NotCert, NotCert,
/// The given index was out of bounds.
#[error("Index out of bounds: {0}")] #[error("Index out of bounds: {0}")]
IndexOutOfBounds(#[from] std::num::TryFromIntError), IndexOutOfBounds(#[from] std::num::TryFromIntError),
} }
#[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>;
/// Create an OpenPGP Cert with derived keys from the given derivation response, keys, and User
/// ID.
///
/// # Errors
/// The function may error for any condition mentioned in [`Error`].
pub fn derive(data: DerivationResponse, keys: &[KeyFlags], userid: &UserID) -> Result<Cert> { pub fn derive(data: DerivationResponse, keys: &[KeyFlags], userid: &UserID) -> Result<Cert> {
let primary_key_flags = match keys.get(0) { let primary_key_flags = match keys.get(0) {
Some(kf) if kf.for_certification() => kf, Some(kf) if kf.for_certification() => kf,

View File

@ -1,3 +1,5 @@
//!
use std::{env, process::ExitCode, str::FromStr}; use std::{env, process::ExitCode, str::FromStr};
use keyfork_derive_util::{ use keyfork_derive_util::{

View File

@ -1,10 +1,15 @@
//! ## Path data guesswork for BIP-0032 derivation paths.
#![allow(clippy::unreadable_literal)] #![allow(clippy::unreadable_literal)]
use keyfork_derive_util::{DerivationIndex, DerivationPath}; use keyfork_derive_util::{DerivationIndex, DerivationPath};
/// The default derivation path for OpenPGP.
pub static OPENPGP: DerivationIndex = DerivationIndex::new_unchecked(7366512, true); pub static OPENPGP: DerivationIndex = DerivationIndex::new_unchecked(7366512, true);
/// A derivation target.
pub enum Target { pub enum Target {
/// An OpenPGP key, whose account is the given index.
OpenPGP(DerivationIndex), OpenPGP(DerivationIndex),
} }

View File

@ -1,2 +1,4 @@
///
pub mod private_key; pub mod private_key;
///
pub mod public_key; pub mod public_key;

View File

@ -100,6 +100,10 @@ where
) )
} }
/// Create an [`ExtendedPrivateKey`] from a given `seed`, `depth`, and `chain_code`.
///
/// # Errors
/// The function may error if a private key can't be created from the seed.
pub fn new_from_parts(seed: &[u8], depth: u8, chain_code: [u8; 32]) -> Result<Self> { pub fn new_from_parts(seed: &[u8], depth: u8, chain_code: [u8; 32]) -> Result<Self> {
Ok(Self { Ok(Self {
private_key: K::from_bytes(seed.try_into()?), private_key: K::from_bytes(seed.try_into()?),
@ -113,6 +117,7 @@ where
&self.private_key &self.private_key
} }
/// Create an [`ExtendedPublicKey`] for the current [`PrivateKey`].
pub fn extended_public_key(&self) -> ExtendedPublicKey<K::PublicKey> { pub fn extended_public_key(&self) -> ExtendedPublicKey<K::PublicKey> {
ExtendedPublicKey::new(self.public_key(), self.chain_code) ExtendedPublicKey::new(self.public_key(), self.chain_code)
} }

View File

@ -44,6 +44,8 @@ impl DerivationIndex {
} }
*/ */
/// Return the internal derivation index. Note that if the derivation index is hardened, the
/// highest bit will be set, and the value can't be used to create a new derivation index.
pub fn inner(&self) -> u32 { pub fn inner(&self) -> u32 {
self.0 self.0
} }

View File

@ -2,11 +2,17 @@
//! BIP-0032 derivation utilities. //! BIP-0032 derivation utilities.
///
pub mod extended_key; pub mod extended_key;
///
pub mod index; pub mod index;
///
pub mod path; pub mod path;
///
pub mod private_key; pub mod private_key;
///
pub mod public_key; pub mod public_key;
///
pub mod request; pub mod request;
#[cfg(test)] #[cfg(test)]

View File

@ -35,18 +35,23 @@ impl DerivationPath {
self.path.iter() self.path.iter()
} }
/// The amount of segments in the DerivationPath. For consistency, a [`usize`] is returned, but
/// BIP-0032 dictates that the depth should be no larger than `255`, [`u8::MAX`].
pub fn len(&self) -> usize { pub fn len(&self) -> usize {
self.path.len() self.path.len()
} }
/// Returns true if there are no path segments.
pub fn is_empty(&self) -> bool { pub fn is_empty(&self) -> bool {
self.path.is_empty() self.path.is_empty()
} }
/// Append an index to the path.
pub fn push(&mut self, index: DerivationIndex) { pub fn push(&mut self, index: DerivationIndex) {
self.path.push(index); self.path.push(index);
} }
/// Append an index to the path, returning self to allow chaining method calls.
pub fn chain_push(mut self, index: DerivationIndex) -> Self { pub fn chain_push(mut self, index: DerivationIndex) -> Self {
self.path.push(index); self.path.push(index);
self self

View File

@ -7,27 +7,41 @@ use crate::{
use keyfork_mnemonic_util::{Mnemonic, MnemonicGenerationError}; use keyfork_mnemonic_util::{Mnemonic, MnemonicGenerationError};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
/// An error encountered while deriving a key.
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]
pub enum DerivationError { pub enum DerivationError {
#[error("algorithm not supported")] /// The algorithm requested was not supported. This may occur when a feature adding support for
/// an algorithm has not been enabled.
#[error("Algorithm not supported")]
Algorithm, Algorithm,
/// A seed was unable to be created from the mnemonic.
#[error("Unable to create seed from mnemonic: {0}")] #[error("Unable to create seed from mnemonic: {0}")]
Mnemonic(#[from] MnemonicGenerationError), Mnemonic(#[from] MnemonicGenerationError),
/// Generating an [`ExtendedPrivateKey`] resulted in an error.
#[error("{0}")] #[error("{0}")]
ExtendedPrivateKey(#[from] XPrvError), ExtendedPrivateKey(#[from] XPrvError),
} }
#[allow(missing_docs)]
pub type Result<T, E = DerivationError> = std::result::Result<T, E>; pub type Result<T, E = DerivationError> = std::result::Result<T, E>;
/// The algorithm to derive a key for. The choice of algorithm will result in a different resulting
/// derivation.
#[derive(Serialize, Deserialize, Clone, Debug)] #[derive(Serialize, Deserialize, Clone, Debug)]
pub enum DerivationAlgorithm { pub enum DerivationAlgorithm {
#[allow(missing_docs)]
Ed25519, Ed25519,
#[allow(missing_docs)]
Secp256k1, Secp256k1,
} }
impl DerivationAlgorithm { impl DerivationAlgorithm {
/// Given a mnemonic seed and a derivation path, derive an [`ExtendedPrivateKey`].
///
/// # Errors
/// The method may error if the derivation fails or if the algorithm is not supported.
pub fn derive(&self, seed: Vec<u8>, path: &DerivationPath) -> Result<DerivationResponse> { pub fn derive(&self, seed: Vec<u8>, path: &DerivationPath) -> Result<DerivationResponse> {
match self { match self {
#[cfg(feature = "ed25519")] #[cfg(feature = "ed25519")]
@ -66,6 +80,7 @@ impl std::str::FromStr for DerivationAlgorithm {
} }
} }
/// A derivation request.
#[derive(Serialize, Deserialize, Clone, Debug)] #[derive(Serialize, Deserialize, Clone, Debug)]
pub struct DerivationRequest { pub struct DerivationRequest {
algorithm: DerivationAlgorithm, algorithm: DerivationAlgorithm,
@ -73,6 +88,7 @@ pub struct DerivationRequest {
} }
impl DerivationRequest { impl DerivationRequest {
/// Create a new derivation request.
pub fn new(algorithm: DerivationAlgorithm, path: &DerivationPath) -> Self { pub fn new(algorithm: DerivationAlgorithm, path: &DerivationPath) -> Self {
Self { Self {
algorithm, algorithm,
@ -80,29 +96,47 @@ impl DerivationRequest {
} }
} }
/// Return the path of the derivation request.
pub fn path(&self) -> &DerivationPath { pub fn path(&self) -> &DerivationPath {
&self.path &self.path
} }
/// Derive an [`ExtendedPrivateKey`] using the seed from the given mnemonic.
///
/// # Errors
/// The method may error if the derivation fails or if the algorithm is not supported.
pub fn derive_with_mnemonic(&self, mnemonic: &Mnemonic) -> Result<DerivationResponse> { pub fn derive_with_mnemonic(&self, mnemonic: &Mnemonic) -> Result<DerivationResponse> {
// TODO: passphrase support and/or store passphrase within mnemonic // TODO: passphrase support and/or store passphrase within mnemonic
self.derive_with_master_seed(mnemonic.seed(None)?) self.derive_with_master_seed(mnemonic.seed(None)?)
} }
/// Derive an [`ExtendedPrivateKey`] using the given seed.
///
/// # Errors
/// The method may error if the derivation fails or if the algorithm is not supported.
pub fn derive_with_master_seed(&self, seed: Vec<u8>) -> Result<DerivationResponse> { pub fn derive_with_master_seed(&self, seed: Vec<u8>) -> Result<DerivationResponse> {
self.algorithm.derive(seed, &self.path) self.algorithm.derive(seed, &self.path)
} }
} }
/// A response to a [`DerivationRequest`]
#[derive(Serialize, Deserialize, Clone, Debug)] #[derive(Serialize, Deserialize, Clone, Debug)]
pub struct DerivationResponse { pub struct DerivationResponse {
/// The algorithm used to derive the data.
pub algorithm: DerivationAlgorithm, pub algorithm: DerivationAlgorithm,
/// The derived private key.
pub data: Vec<u8>, pub data: Vec<u8>,
/// The chain code, used for further derivation.
pub chain_code: [u8; 32], pub chain_code: [u8; 32],
/// The depth, used for further derivation.
pub depth: u8, pub depth: u8,
} }
impl DerivationResponse { impl DerivationResponse {
/// Create a [`DerivationResponse`] with the given values.
pub fn with_algo_and_xprv<T: PrivateKey + Clone>( pub fn with_algo_and_xprv<T: PrivateKey + Clone>(
algorithm: DerivationAlgorithm, algorithm: DerivationAlgorithm,
xprv: &ExtendedPrivateKey<T>, xprv: &ExtendedPrivateKey<T>,
@ -116,11 +150,15 @@ impl DerivationResponse {
} }
} }
/// An error when creating a [`DerivationResponse`].
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]
pub enum TryFromDerivationResponseError { pub enum TryFromDerivationResponseError {
/// The algorithm used to derive the data does not match the algorithm of the
/// [`ExtendedPrivateKey`] being created.
#[error("incorrect algorithm provided")] #[error("incorrect algorithm provided")]
Algorithm, Algorithm,
/// An error occurred while creating an [`ExtendedPrivateKey`] from the given response.
#[error("{0}")] #[error("{0}")]
ExtendedPrivateKey(#[from] XPrvError), ExtendedPrivateKey(#[from] XPrvError),
} }

View File

@ -11,6 +11,7 @@ default = ["openpgp", "openpgp-card", "qrcode"]
openpgp = ["sequoia-openpgp", "anyhow"] openpgp = ["sequoia-openpgp", "anyhow"]
openpgp-card = ["openpgp-card-sequoia", "card-backend-pcsc", "card-backend", "dep:openpgp-card"] openpgp-card = ["openpgp-card-sequoia", "card-backend-pcsc", "card-backend", "dep:openpgp-card"]
qrcode = ["keyfork-qrcode"] qrcode = ["keyfork-qrcode"]
bin = ["sequoia-openpgp/crypto-nettle", "keyfork-qrcode/decode-backend-rqrr"]
[dependencies] [dependencies]
keyfork-prompt = { version = "0.1.0", path = "../util/keyfork-prompt", default-features = false, features = ["mnemonic"] } keyfork-prompt = { version = "0.1.0", path = "../util/keyfork-prompt", default-features = false, features = ["mnemonic"] }

View File

@ -1,3 +1,5 @@
//!
use std::{ use std::{
env, env,
fs::File, fs::File,

View File

@ -1,3 +1,5 @@
//!
use std::{ use std::{
env, env,
fs::File, fs::File,

View File

@ -1,3 +1,5 @@
//!
use std::{ use std::{
env, env,
process::ExitCode, process::ExitCode,

View File

@ -1,3 +1,5 @@
//!
use std::{env, path::PathBuf, process::ExitCode, str::FromStr}; use std::{env, path::PathBuf, process::ExitCode, str::FromStr};
use keyfork_shard::openpgp::{discover_certs, openpgp::Cert, split}; use keyfork_shard::openpgp::{discover_certs, openpgp::Cert, split};

View File

@ -1,3 +1,7 @@
//! ## Keyfork Shard
//!
//! Utilities for securing secrets using Shamir's Secret Sharing.
use std::io::{stdin, stdout, Write}; use std::io::{stdin, stdout, Write};
use aes_gcm::{ use aes_gcm::{
@ -17,15 +21,20 @@ use x25519_dalek::{EphemeralSecret, PublicKey};
#[cfg(feature = "openpgp")] #[cfg(feature = "openpgp")]
pub mod openpgp; pub mod openpgp;
/// Errors encountered while creating or combining shares using Shamir's Secret Sharing.
#[derive(thiserror::Error, Debug)] #[derive(thiserror::Error, Debug)]
pub enum SharksError { pub enum SharksError {
/// A Shamir Share could not be created.
#[error("Error creating share: {0}")] #[error("Error creating share: {0}")]
Share(String), Share(String),
/// The Shamir shares could not be combined.
#[error("Error combining shares: {0}")] #[error("Error combining shares: {0}")]
CombineShare(String), CombineShare(String),
} }
/// The mnemonic or QR code used to transport an encrypted shard did not store the correct amount
/// of data.
#[derive(thiserror::Error, Debug)] #[derive(thiserror::Error, Debug)]
#[error("Mnemonic or QR code did not store enough data")] #[error("Mnemonic or QR code did not store enough data")]
pub struct InvalidData; pub struct InvalidData;
@ -37,8 +46,16 @@ pub struct InvalidData;
pub(crate) const HUNK_VERSION: u8 = 1; pub(crate) const HUNK_VERSION: u8 = 1;
pub(crate) const HUNK_OFFSET: usize = 2; pub(crate) const HUNK_OFFSET: usize = 2;
/// # Panics /// Establish ECDH transport for remote operators, receive transport-encrypted shares, decrypt the
/// shares, and combine them.
/// ///
/// # Errors
/// The function may error if:
/// * Prompting for transport-encrypted shards fails.
/// * Decrypting shards fails.
/// * Combining shards fails.
///
/// # Panics
/// The function may panic if it is given payloads generated using a version of Keyfork that is /// The function may panic if it is given payloads generated using a version of Keyfork that is
/// incompatible with the currently running version. /// incompatible with the currently running version.
pub fn remote_decrypt(w: &mut impl Write) -> Result<(), Box<dyn std::error::Error>> { pub fn remote_decrypt(w: &mut impl Write) -> Result<(), Box<dyn std::error::Error>> {

View File

@ -1,3 +1,5 @@
//! OpenPGP Shard functionality.
use std::{ use std::{
collections::{HashMap, VecDeque}, collections::{HashMap, VecDeque},
io::{stdin, stdout, Read, Write}, io::{stdin, stdout, Read, Write},
@ -59,65 +61,86 @@ use super::{InvalidData, SharksError, HUNK_VERSION};
// 256 bit share is 49 bytes + some amount of hunk bytes, gives us reasonable padding // 256 bit share is 49 bytes + some amount of hunk bytes, gives us reasonable padding
const ENC_LEN: u8 = 4 * 16; const ENC_LEN: u8 = 4 * 16;
/// Errors encountered while performing operations using OpenPGP.
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]
pub enum Error { pub enum Error {
/// Errors encountered while creating or combining shares.
#[error("{0}")] #[error("{0}")]
Sharks(#[from] SharksError), Sharks(#[from] SharksError),
/// Unable to decrypt a share.
#[error("Error decrypting share: {0}")] #[error("Error decrypting share: {0}")]
SymDecryptShare(#[from] AesError), SymDecryptShare(#[from] AesError),
/// The generated AES key is of an invalid length.
#[error("Invalid length of AES key: {0}")] #[error("Invalid length of AES key: {0}")]
AesLength(#[from] InvalidLength), AesLength(#[from] InvalidLength),
/// The HKDF function was given an input of an invalid length.
#[error("Invalid KDF length: {0}")] #[error("Invalid KDF length: {0}")]
HkdfLength(#[from] HkdfInvalidLength), HkdfLength(#[from] HkdfInvalidLength),
/// The secret did not match the previously-known secret fingerprint.
#[error("Derived secret hash {0} != expected {1}")] #[error("Derived secret hash {0} != expected {1}")]
InvalidSecret(Fingerprint, Fingerprint), InvalidSecret(Fingerprint, Fingerprint),
/// An error occurred while performing an OpenPGP operation.
#[error("OpenPGP error: {0}")] #[error("OpenPGP error: {0}")]
Sequoia(#[source] anyhow::Error), Sequoia(#[source] anyhow::Error),
/// An IO error occurred while performing an OpenPGP operation.
#[error("OpenPGP IO error: {0}")] #[error("OpenPGP IO error: {0}")]
SequoiaIo(#[source] std::io::Error), SequoiaIo(#[source] std::io::Error),
/// An error occurred while using a keyring.
#[error("Keyring error: {0}")] #[error("Keyring error: {0}")]
Keyring(#[from] keyring::Error), Keyring(#[from] keyring::Error),
/// An error occurred while using a smartcard.
#[error("Smartcard error: {0}")] #[error("Smartcard error: {0}")]
Smartcard(#[from] smartcard::Error), Smartcard(#[from] smartcard::Error),
/// An error occurred while displaying a prompt.
#[error("Prompt error: {0}")] #[error("Prompt error: {0}")]
Prompt(#[from] PromptError), Prompt(#[from] PromptError),
/// An error occurred while generating a mnemonic.
#[error("Mnemonic generation error: {0}")] #[error("Mnemonic generation error: {0}")]
MnemonicGeneration(#[from] MnemonicGenerationError), MnemonicGeneration(#[from] MnemonicGenerationError),
/// An error occurred while parsing a mnemonic.
#[error("Mnemonic parse error: {0}")] #[error("Mnemonic parse error: {0}")]
MnemonicFromStr(#[from] MnemonicFromStrError), MnemonicFromStr(#[from] MnemonicFromStrError),
/// An error occurred while converting mnemonic data.
#[error("{0}")] #[error("{0}")]
InvalidMnemonicData(#[from] InvalidData), InvalidMnemonicData(#[from] InvalidData),
/// An IO error occurred.
#[error("IO error: {0}")] #[error("IO error: {0}")]
Io(#[source] std::io::Error), Io(#[source] std::io::Error),
/// An error occurred while parsing a derivation path.
#[error("Derivation path: {0}")] #[error("Derivation path: {0}")]
DerivationPath(#[from] keyfork_derive_openpgp::derive_util::path::Error), DerivationPath(#[from] keyfork_derive_openpgp::derive_util::path::Error),
/// An error occurred while requesting derivation.
#[error("Derivation request: {0}")] #[error("Derivation request: {0}")]
DerivationRequest(#[from] keyfork_derive_openpgp::derive_util::request::DerivationError), DerivationRequest(#[from] keyfork_derive_openpgp::derive_util::request::DerivationError),
/// An error occurred while decoding hex.
#[error("Unable to decode hex: {0}")] #[error("Unable to decode hex: {0}")]
HexDecode(#[from] smex::DecodeError), HexDecode(#[from] smex::DecodeError),
/// An error occurred while creating an OpenPGP cert.
#[error("Keyfork OpenPGP: {0}")] #[error("Keyfork OpenPGP: {0}")]
KeyforkOpenPGP(#[from] keyfork_derive_openpgp::Error), KeyforkOpenPGP(#[from] keyfork_derive_openpgp::Error),
} }
#[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>;
/// An OpenPGP encrypted message and public-key-encrypted-secret-key packets.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct EncryptedMessage { pub struct EncryptedMessage {
pkesks: Vec<PKESK>, pkesks: Vec<PKESK>,
@ -125,6 +148,7 @@ pub struct EncryptedMessage {
} }
impl EncryptedMessage { impl EncryptedMessage {
/// Create a new EncryptedMessage from known parts.
pub fn new(pkesks: &mut Vec<PKESK>, seip: SEIP) -> Self { pub fn new(pkesks: &mut Vec<PKESK>, seip: SEIP) -> Self {
Self { Self {
pkesks: std::mem::take(pkesks), pkesks: std::mem::take(pkesks),
@ -132,6 +156,14 @@ impl EncryptedMessage {
} }
} }
/// Decrypt the message with a Sequoia policy and decryptor.
///
/// This method creates a container containing the packets and passes the serialized container
/// to a DecryptorBuilder, which is used to decrypt the message.
///
/// # Errors
/// The method may return an error if it is unable to rebuild the message to decrypt or if it
/// is unable to decrypt the message.
pub fn decrypt_with<H>(&self, policy: &'_ dyn Policy, decryptor: H) -> Result<Vec<u8>> pub fn decrypt_with<H>(&self, policy: &'_ dyn Policy, decryptor: H) -> Result<Vec<u8>>
where where
H: VerificationHelper + DecryptionHelper, H: VerificationHelper + DecryptionHelper,
@ -168,6 +200,12 @@ impl EncryptedMessage {
} }
} }
/// Read all OpenPGP certificates in a path and return a [`Vec`] of them. Certificates are read
/// from a file, or from files one level deep in a directory.
///
/// # Errors
/// The function may return an error if it is unable to read the directory or if Sequoia is unable
/// to load certificates from the file.
pub fn discover_certs(path: impl AsRef<Path>) -> Result<Vec<Cert>> { pub fn discover_certs(path: impl AsRef<Path>) -> Result<Vec<Cert>> {
let path = path.as_ref(); let path = path.as_ref();
@ -191,8 +229,13 @@ pub fn discover_certs(path: impl AsRef<Path>) -> Result<Vec<Cert>> {
} }
} }
/// # Panics /// Parse messages from a type implementing [`Read`] and store them as [`EncryptedMessage`].
/// ///
/// # Errors
/// The function may return an error if the reader has run out of data or if the data is not
/// properly formatted OpenPGP messages.
///
/// # Panics
/// When given packets that are not a list of PKESK packets and SEIP packets, the function panics. /// When given packets that are not a list of PKESK packets and SEIP packets, the function panics.
/// The `split` utility should never give packets that are not in this format. /// The `split` utility should never give packets that are not in this format.
pub fn parse_messages(reader: impl Read + Send + Sync) -> Result<VecDeque<EncryptedMessage>> { pub fn parse_messages(reader: impl Read + Send + Sync) -> Result<VecDeque<EncryptedMessage>> {
@ -401,6 +444,15 @@ fn decrypt_one(
unreachable!("smartcard manager should always decrypt") unreachable!("smartcard manager should always decrypt")
} }
/// Decrypt a single shard, encrypt to a remote operator, and present the transport shard as a QR
/// code and mnemonic to be sent to the remote operator.
///
/// # Errors
///
/// The function may error if an error occurs while displaying a prompt or while decrypting the
/// shard. An error will not be returned if the camera has a hardware error while scanning a QR
/// code; instead, a mnemonic prompt will be used.
///
/// # Panics /// # Panics
/// ///
/// The function may panic if a share is decrypted but has a length larger than 256 bits. This is /// The function may panic if a share is decrypted but has a length larger than 256 bits. This is
@ -522,6 +574,11 @@ pub fn decrypt(
Ok(()) Ok(())
} }
/// Combine mulitple shards into a secret.
///
/// # Errors
/// The function may return an error if an error occurs while decrypting shards, parsing shards, or
/// combining the shards into a secret.
pub fn combine( pub fn combine(
certs: Vec<Cert>, certs: Vec<Cert>,
metadata: &EncryptedMessage, metadata: &EncryptedMessage,
@ -600,6 +657,13 @@ pub fn combine(
Ok(()) Ok(())
} }
/// Split a secret into an OpenPGP formatted Shard file.
///
/// # Errors
///
/// The function may return an error if the shards can't be encrypted to the provided OpenPGP
/// certs or if an error happens while writing the Shard file.
///
/// # Panics /// # Panics
/// ///
/// The function may panic if the metadata can't properly store the certificates used to generate /// The function may panic if the metadata can't properly store the certificates used to generate

View File

@ -1,4 +1,7 @@
#![doc = include_str!("../../../README.md")]
#![allow(clippy::module_name_repetitions)] #![allow(clippy::module_name_repetitions)]
use std::process::ExitCode; use std::process::ExitCode;
use clap::Parser; use clap::Parser;

View File

@ -1,3 +1,5 @@
//!
use std::time::Duration; use std::time::Duration;
use keyfork_qrcode::scan_camera; use keyfork_qrcode::scan_camera;

View File

@ -1,3 +1,5 @@
//! Encoding and decoding QR codes.
use image::io::Reader as ImageReader; use image::io::Reader as ImageReader;
use std::{ use std::{
io::{Cursor, Write}, io::{Cursor, Write},
@ -10,40 +12,61 @@ use v4l::{
Device, Device,
}; };
/// A QR code could not be generated.
#[derive(thiserror::Error, Debug)] #[derive(thiserror::Error, Debug)]
pub enum QRGenerationError { pub enum QRGenerationError {
/// The resulting QR coode could not be read from the generator program.
#[error("{0}")] #[error("{0}")]
Io(#[from] std::io::Error), Io(#[from] std::io::Error),
/// The generator program produced invalid data.
#[error("Could not decode output of qrencode (this is a bug!): {0}")] #[error("Could not decode output of qrencode (this is a bug!): {0}")]
StringParse(#[from] std::string::FromUtf8Error), StringParse(#[from] std::string::FromUtf8Error),
} }
/// An error occurred while scanning for a QR code.
#[derive(thiserror::Error, Debug)] #[derive(thiserror::Error, Debug)]
pub enum QRCodeScanError { pub enum QRCodeScanError {
/// The camera could not load the requested format.
#[error("Camera could not use {expected} format, instead used {actual}")] #[error("Camera could not use {expected} format, instead used {actual}")]
CameraGaveBadFormat { CameraGaveBadFormat {
/// The expected format, in FourCC format.
expected: String, expected: String,
/// The actual format, in FourCC format.
actual: String, actual: String,
}, },
/// Interfacing with the camera resulted in an error.
#[error("Unable to interface with camera: {0}")] #[error("Unable to interface with camera: {0}")]
CameraIO(#[from] std::io::Error), CameraIO(#[from] std::io::Error),
/// Decoding an image from the camera resulted in an error.
#[error("Could not decode image: {0}")] #[error("Could not decode image: {0}")]
ImageDecode(#[from] image::ImageError), ImageDecode(#[from] image::ImageError),
} }
/// The level of error correction when generating a QR code.
#[derive(Default)] #[derive(Default)]
pub enum ErrorCorrection { pub enum ErrorCorrection {
/// 7% of the QR code can be recovered.
#[default] #[default]
Lowest, Lowest,
/// 15% of the QR code can be recovered.
Medium, Medium,
/// 25% of the QR code can be recovered.
Quartile, Quartile,
/// 30% of the QR code can be recovered.
Highest, Highest,
} }
/// Generate a terminal-printable QR code for a given string. Uses the `qrencode` CLI utility. /// Generate a terminal-printable QR code for a given string. Uses the `qrencode` CLI utility.
///
/// # Errors
/// The function may return an error if interacting with the QR code generation program fails.
pub fn qrencode( pub fn qrencode(
text: &str, text: &str,
error_correction: impl Into<Option<ErrorCorrection>>, error_correction: impl Into<Option<ErrorCorrection>>,
@ -73,6 +96,7 @@ pub fn qrencode(
Ok(result) Ok(result)
} }
/// Continuously scan the `index`-th camera for a QR code.
#[cfg(feature = "decode-backend-rqrr")] #[cfg(feature = "decode-backend-rqrr")]
pub fn scan_camera(timeout: Duration, index: usize) -> Result<Option<String>, QRCodeScanError> { pub fn scan_camera(timeout: Duration, index: usize) -> Result<Option<String>, QRCodeScanError> {
let device = Device::new(index)?; let device = Device::new(index)?;
@ -100,6 +124,7 @@ pub fn scan_camera(timeout: Duration, index: usize) -> Result<Option<String>, QR
Ok(None) Ok(None)
} }
/// Continuously scan the `index`-th camera for a QR code.
#[cfg(feature = "decode-backend-zbar")] #[cfg(feature = "decode-backend-zbar")]
pub fn scan_camera(timeout: Duration, index: usize) -> Result<Option<String>, QRCodeScanError> { pub fn scan_camera(timeout: Duration, index: usize) -> Result<Option<String>, QRCodeScanError> {
let device = Device::new(index)?; let device = Device::new(index)?;

View File

@ -1,3 +1,5 @@
#![allow(missing_docs, clippy::missing_errors_doc)]
use std::{env::VarError, path::Path}; use std::{env::VarError, path::Path};
use pkg_config::Config; use pkg_config::Config;

View File

@ -1,3 +1,4 @@
#![allow(non_upper_case_globals, non_camel_case_types, non_snake_case)] #![allow(non_upper_case_globals, non_camel_case_types, non_snake_case)]
#![allow(missing_docs)]
include!(concat!(env!("OUT_DIR"), "/bindings.rs")); include!(concat!(env!("OUT_DIR"), "/bindings.rs"));

View File

@ -1,3 +1,5 @@
//! Scan for a barcode or QR code from the default camera.
use std::{ use std::{
io::Cursor, io::Cursor,
time::{Duration, SystemTime}, time::{Duration, SystemTime},
@ -31,7 +33,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
.decode()?, .decode()?,
); );
for symbol in scanner.scan_image(&image) { if let Some(symbol) = scanner.scan_image(&image).get(0) {
println!("{}", String::from_utf8_lossy(symbol.data())); println!("{}", String::from_utf8_lossy(symbol.data()));
return Ok(()); return Ok(());
} }

View File

@ -1,5 +1,8 @@
//! Conversions for the internal Image type used by zbar.
use super::sys; use super::sys;
/// The internal image type used by zbar.
pub struct Image { pub struct Image {
pub(crate) inner: *mut sys::zbar_image_s, pub(crate) inner: *mut sys::zbar_image_s,
/// Set to store the data of inner, as it will otherwise be freed when the data is dropped. /// Set to store the data of inner, as it will otherwise be freed when the data is dropped.

View File

@ -1,20 +1,29 @@
//! ## Image scanning
use super::{ use super::{
image::Image, image::Image,
symbol::{Symbol, SymbolType}, symbol::{Symbol, SymbolType},
sys, Config, sys, Config,
}; };
/// Errors encountered while creating or using an [`ImageScanner`].
#[derive(thiserror::Error, Debug)] #[derive(thiserror::Error, Debug)]
pub enum ImageScannerError { pub enum ImageScannerError {
/// The provided configuration resulted in an error.
#[error("Unable to set Image Scanner configuration")] #[error("Unable to set Image Scanner configuration")]
UnableToSetConfig, UnableToSetConfig,
} }
/// An [`ImageScanner`].
///
/// Link: [`sys::zbar_image_scanner_t`]
pub struct ImageScanner { pub struct ImageScanner {
inner: *mut sys::zbar_image_scanner_t, inner: *mut sys::zbar_image_scanner_t,
} }
impl ImageScanner { impl ImageScanner {
/// create a new ImageScanner.
///
/// Link: [`sys::zbar_image_scanner_create`] /// Link: [`sys::zbar_image_scanner_create`]
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
@ -22,7 +31,12 @@ impl ImageScanner {
} }
} }
/// Set a configuration option for the ImageScanner.
///
/// Link: [`sys::zbar_image_scanner_set_config`] /// Link: [`sys::zbar_image_scanner_set_config`]
///
/// # Errors
/// The function may error if the provided configuration was invalid.
pub fn set_config( pub fn set_config(
&mut self, &mut self,
symbol: SymbolType, symbol: SymbolType,
@ -39,10 +53,9 @@ impl ImageScanner {
Ok(()) Ok(())
} }
/// Link: [`sys::zbar_scan_image`] /// Scan an [`Image`] for QR codes.
/// ///
/// TODO: move `image` to newtype, offering conversions /// Link: [`sys::zbar_scan_image`]
/// to and from image::Image
/// ///
/// TODO: return an iterator over scanned values /// TODO: return an iterator over scanned values
pub fn scan_image( pub fn scan_image(

View File

@ -1,8 +1,13 @@
//!
use super::sys; use super::sys;
/// The type of symbol (i.e. what type of barcode or QR code).
pub use sys::zbar_symbol_type_e as SymbolType; pub use sys::zbar_symbol_type_e as SymbolType;
// TODO: config, modifiers // TODO: config, modifiers
/// A Symbol detected by zbar.
#[derive(Debug)] #[derive(Debug)]
pub struct Symbol { pub struct Symbol {
_type: SymbolType, _type: SymbolType,
@ -17,14 +22,17 @@ impl Symbol {
} }
} }
/// The type of symbol
pub fn _type(&self) -> SymbolType { pub fn _type(&self) -> SymbolType {
self._type self._type
} }
/// The internal data of the image.
pub fn data(&self) -> &[u8] { pub fn data(&self) -> &[u8] {
self.data.as_slice() self.data.as_slice()
} }
/// Consume self, returning the internal data.
pub fn into_data(self) -> Vec<u8> { pub fn into_data(self) -> Vec<u8> {
self.data self.data
} }

View File

@ -7,6 +7,7 @@ use std::io;
use keyfork_crossterm::event::{self, Event, KeyCode, KeyEvent}; use keyfork_crossterm::event::{self, Event, KeyCode, KeyEvent};
/// Read a character from input.
pub fn read_char() -> io::Result<char> { pub fn read_char() -> io::Result<char> {
loop { loop {
if let Event::Key(KeyEvent { if let Event::Key(KeyEvent {
@ -19,6 +20,7 @@ pub fn read_char() -> io::Result<char> {
} }
} }
/// Read a line from input.
pub fn read_line() -> io::Result<String> { pub fn read_line() -> io::Result<String> {
let mut line = String::new(); let mut line = String::new();
while let Event::Key(KeyEvent { code, .. }) = event::read()? { while let Event::Key(KeyEvent { code, .. }) = event::read()? {

View File

@ -1,3 +1,5 @@
//!
use keyfork_crossterm::{ use keyfork_crossterm::{
execute, execute,
terminal::{size, SetSize}, terminal::{size, SetSize},
@ -5,7 +7,7 @@ use keyfork_crossterm::{
}; };
use std::io::{stdin, stdout}; use std::io::{stdin, stdout};
pub fn main() { fn main() {
println!("size: {:?}", size().unwrap()); println!("size: {:?}", size().unwrap());
execute!(stdout(), SetSize(10, 10)).unwrap(); execute!(stdout(), SetSize(10, 10)).unwrap();
println!("resized: {:?}", size().unwrap()); println!("resized: {:?}", size().unwrap());

View File

@ -72,6 +72,7 @@ where
Ok(user_char) Ok(user_char)
} }
/// Read a character from input.
pub fn read_char() -> io::Result<char> { pub fn read_char() -> io::Result<char> {
loop { loop {
if let Event::Key(KeyEvent { if let Event::Key(KeyEvent {

View File

@ -1,3 +1,4 @@
#![allow(missing_docs, clippy::missing_errors_doc, clippy::missing_panics_doc)]
#![deny(unused_imports, unused_must_use)] #![deny(unused_imports, unused_must_use)]
//! # Cross-platform Terminal Manipulation Library //! # Cross-platform Terminal Manipulation Library

View File

@ -149,7 +149,7 @@ mod tests {
// Helper for execute tests to confirm flush // Helper for execute tests to confirm flush
#[derive(Default, Debug, Clone)] #[derive(Default, Debug, Clone)]
pub(self) struct FakeWrite { struct FakeWrite {
buffer: String, buffer: String,
flushed: bool, flushed: bool,
} }

View File

@ -1,3 +1,5 @@
//! Utilities for reading entropy from secure sources.
use std::{fs::{read_dir, read_to_string, File}, io::Read}; use std::{fs::{read_dir, read_to_string, File}, io::Read};
static WARNING_LINKS: [&str; 1] = static WARNING_LINKS: [&str; 1] =
@ -45,6 +47,7 @@ fn ensure_offline() {
} }
} }
/// Ensure the system is safe.
pub fn ensure_safe() { pub fn ensure_safe() {
if !std::env::vars() if !std::env::vars()
.any(|(name, _)| name == "SHOOT_SELF_IN_FOOT" || name == "INSECURE_HARDWARE_ALLOWED") .any(|(name, _)| name == "SHOOT_SELF_IN_FOOT" || name == "INSECURE_HARDWARE_ALLOWED")
@ -54,6 +57,10 @@ pub fn ensure_safe() {
} }
} }
/// Read system entropy of a given size.
///
/// # Errors
/// An error may be returned if an error occurred while reading from the random source.
pub fn generate_entropy_of_size(byte_count: usize) -> Result<Vec<u8>, std::io::Error> { pub fn generate_entropy_of_size(byte_count: usize) -> Result<Vec<u8>, std::io::Error> {
ensure_safe(); ensure_safe();
let mut vec = vec![0u8; byte_count]; let mut vec = vec![0u8; byte_count];

View File

@ -1,3 +1,6 @@
//! Functions for decoding from and encoding to types that implement [`AsyncRead`] and
//! [`AsyncWrite`].
use std::marker::Unpin; use std::marker::Unpin;
use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}; use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
@ -10,6 +13,7 @@ use super::{hash, verify_checksum, DecodeError, EncodeError};
/// * 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.
pub async fn try_decode_from( pub async fn try_decode_from(
readable: &mut (impl AsyncRead + Unpin), readable: &mut (impl AsyncRead + Unpin),
) -> Result<Vec<u8>, DecodeError> { ) -> Result<Vec<u8>, DecodeError> {
@ -31,7 +35,7 @@ pub async fn try_decode_from(
/// # Errors /// # Errors
/// An error may be returned if: /// An error may be returned if:
/// * The given `data` is more than [`u32::MAX`] bytes. This is a constraint on a protocol level. /// * The given `data` is more than [`u32::MAX`] bytes. This is a constraint on a protocol level.
/// * The resulting data was unable to be written to the given `writable`. /// * The resulting data was unable to be written.
pub async fn try_encode_to( pub async fn try_encode_to(
data: &[u8], data: &[u8],
writable: &mut (impl AsyncWrite + Unpin), writable: &mut (impl AsyncWrite + Unpin),

View File

@ -19,6 +19,7 @@ pub mod asyncext;
use sha2::{Digest, Sha256}; use sha2::{Digest, Sha256};
/// An error encountered while decoding a frame.
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]
pub enum DecodeError { pub enum DecodeError {
/// There were not enough bytes to determine the length of the data slice. /// There were not enough bytes to determine the length of the data slice.
@ -42,6 +43,7 @@ pub enum DecodeError {
Io(#[from] std::io::Error), Io(#[from] std::io::Error),
} }
/// An error encountered while encoding a frame.
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]
pub enum EncodeError { pub enum EncodeError {
/// The given input was larger than could be encoded by this protocol. /// The given input was larger than could be encoded by this protocol.
@ -70,6 +72,11 @@ pub fn try_encode(data: &[u8]) -> Result<Vec<u8>, EncodeError> {
Ok(output) Ok(output)
} }
/// Encode data to a type implementing [`Write`].
///
/// # 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.
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();
@ -104,6 +111,14 @@ 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>`.
///
/// # 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.
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

@ -1,3 +1,5 @@
//! Zero-dependency Mnemonic encoding and decoding.
use std::{error::Error, fmt::Display, str::FromStr, sync::Arc}; use std::{error::Error, fmt::Display, str::FromStr, sync::Arc};
use hmac::Hmac; use hmac::Hmac;
@ -258,18 +260,22 @@ impl Mnemonic {
} }
} }
/// The internal representation of the decoded data.
pub fn as_bytes(&self) -> &[u8] { pub fn as_bytes(&self) -> &[u8] {
&self.entropy &self.entropy
} }
/// Drop self, returning the decoded data.
pub fn to_bytes(self) -> Vec<u8> { pub fn to_bytes(self) -> Vec<u8> {
self.entropy self.entropy
} }
/// Clone the existing entropy.
pub fn entropy(&self) -> Vec<u8> { pub fn entropy(&self) -> Vec<u8> {
self.entropy.clone() self.entropy.clone()
} }
/// Create a BIP-0032 seed from the provided data and an optional passphrase.
pub fn seed<'a>( pub fn seed<'a>(
&self, &self,
passphrase: impl Into<Option<&'a str>>, passphrase: impl Into<Option<&'a str>>,
@ -284,6 +290,7 @@ impl Mnemonic {
Ok(seed.to_vec()) Ok(seed.to_vec())
} }
/// Encode the mnemonic into a list of wordlist indexes.
pub fn words(self) -> (Vec<usize>, Arc<Wordlist>) { pub fn words(self) -> (Vec<usize>, Arc<Wordlist>) {
let bit_count = self.entropy.len() * 8; let bit_count = self.entropy.len() * 8;
let mut bits = vec![false; bit_count + bit_count / 32]; let mut bits = vec![false; bit_count + bit_count / 32];

View File

@ -1,3 +1,5 @@
//!
fn main() -> Result<(), Box<dyn std::error::Error>> { fn main() -> Result<(), Box<dyn std::error::Error>> {
let bit_size: usize = std::env::args() let bit_size: usize = std::env::args()
.nth(1) .nth(1)

View File

@ -1,3 +1,5 @@
//!
use keyfork_mnemonic_util::Mnemonic; use keyfork_mnemonic_util::Mnemonic;
fn main() -> Result<(), Box<dyn std::error::Error>> { fn main() -> Result<(), Box<dyn std::error::Error>> {

View File

@ -1,3 +1,5 @@
//!
use std::io::{stdin, stdout}; use std::io::{stdin, stdout};
use keyfork_prompt::{ use keyfork_prompt::{

View File

@ -1,36 +1,66 @@
//! Prompt display and interaction management.
use std::borrow::Borrow; use std::borrow::Borrow;
#[cfg(feature = "mnemonic")] #[cfg(feature = "mnemonic")]
use keyfork_mnemonic_util::Wordlist; 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::{Terminal, DefaultTerminal, default_terminal};
/// An error occurred while displaying a prompt.
#[derive(thiserror::Error, Debug)] #[derive(thiserror::Error, Debug)]
pub enum Error { pub enum Error {
/// The given handler is not a TTY and can't be used to display prompts.
#[error("The given handler is not a TTY")] #[error("The given handler is not a TTY")]
NotATTY, NotATTY,
/// Validating user input failed.
#[error("Validation of the input failed after {0} retries (last error: {1})")] #[error("Validation of the input failed after {0} retries (last error: {1})")]
Validation(u8, String), Validation(u8, String),
/// An error occurred while interacting with a terminal.
#[error("IO Error: {0}")] #[error("IO Error: {0}")]
IO(#[from] std::io::Error), IO(#[from] std::io::Error),
} }
#[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>;
/// A message displayed by [`PromptHandler::prompt_message`].
pub enum Message { pub enum Message {
/// A textual message, wrapping at space boundaries when reaching the end of the terminal.
Text(String), Text(String),
/// A data message, with no word wrapping, and automatic hiding of the message when a terminal
/// is too small.
Data(String), Data(String),
} }
/// A trait to allow displaying prompts and accepting input.
pub trait PromptHandler { pub trait PromptHandler {
/// Prompt the user for input.
///
/// # Errors
/// The method may return an error if the message was not able to be displayed or if the input
/// could not be read.
fn prompt_input(&mut self, prompt: &str) -> Result<String>; fn prompt_input(&mut self, prompt: &str) -> Result<String>;
/// Prompt the user for input based on a wordlist.
///
/// # Errors
/// The method may return an error if the message was not able to be displayed or if the input
/// could not be read.
#[cfg(feature = "mnemonic")]
fn prompt_wordlist(&mut self, prompt: &str, wordlist: &Wordlist) -> Result<String>; fn prompt_wordlist(&mut self, prompt: &str, wordlist: &Wordlist) -> Result<String>;
/// Prompt the user for input based on a wordlist, while validating the wordlist using a
/// provided parser function, returning the type from the parser.
///
/// # Errors
/// The method may return an error if the message was not able to be displayed, if the input
/// could not be read, or if the parser returned an error.
#[cfg(feature = "mnemonic")] #[cfg(feature = "mnemonic")]
fn prompt_validated_wordlist<V, F, E>( fn prompt_validated_wordlist<V, F, E>(
&mut self, &mut self,
@ -43,8 +73,19 @@ pub trait PromptHandler {
F: Fn(String) -> Result<V, E>, F: Fn(String) -> Result<V, E>,
E: std::error::Error; E: std::error::Error;
/// Prompt the user for a passphrase, which is hidden while typing.
///
/// # Errors
/// The method may return an error if the message was not able to be displayed or if the input
/// could not be read.
fn prompt_passphrase(&mut self, prompt: &str) -> Result<String>; fn prompt_passphrase(&mut self, prompt: &str) -> Result<String>;
/// Prompt the user for a passphrase, which is hidden while typing, and validate the passphrase
/// using a provided parser function, returning the type from the parser.
///
/// # Errors
/// The method may return an error if the message was not able to be displayed, if the input
/// could not be read, or if the parser returned an error.
fn prompt_validated_passphrase<V, F, E>( fn prompt_validated_passphrase<V, F, E>(
&mut self, &mut self,
prompt: &str, prompt: &str,
@ -55,5 +96,10 @@ pub trait PromptHandler {
F: Fn(String) -> Result<V, E>, F: Fn(String) -> Result<V, E>,
E: std::error::Error; E: std::error::Error;
/// Prompt the user with a [`Message`].
///
/// # Errors
/// The method may return an error if the message was not able to be displayed or if an error
/// occurred while waiting for the user to dismiss the message.
fn prompt_message(&mut self, prompt: impl Borrow<Message>) -> Result<()>; fn prompt_message(&mut self, prompt: impl Borrow<Message>) -> Result<()>;
} }

View File

@ -14,6 +14,7 @@ use keyfork_crossterm::{
use crate::{PromptHandler, Message, Wordlist, Error}; use crate::{PromptHandler, Message, Wordlist, Error};
#[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>;
struct TerminalGuard<'a, R, W> struct TerminalGuard<'a, R, W>
@ -124,6 +125,7 @@ where
} }
} }
/// A handler for a terminal.
pub struct Terminal<R, W> { pub struct Terminal<R, W> {
read: BufReader<R>, read: BufReader<R>,
write: W, write: W,
@ -135,6 +137,10 @@ where
R: Read + Sized, R: Read + Sized,
W: Write + AsRawFd + Sized, W: Write + AsRawFd + Sized,
{ {
/// Create a new [`Terminal`] from values implementing [`Read`] and [`Write`].
///
/// # Errors
/// The function may error if the write handle is not a terminal.
pub fn new(read_handle: R, write_handle: W) -> Result<Self> { pub fn new(read_handle: R, write_handle: W) -> Result<Self> {
if !write_handle.is_tty() { if !write_handle.is_tty() {
return Err(Error::NotATTY); return Err(Error::NotATTY);
@ -490,8 +496,13 @@ impl<R, W> PromptHandler for Terminal<R, W> where R: Read + Sized, W: Write + As
} }
} }
/// A default terminal, using [`Stdin`] and [`Stderr`].
pub type DefaultTerminal = Terminal<Stdin, Stderr>; pub type DefaultTerminal = Terminal<Stdin, Stderr>;
/// Create a [`Terminal`] using the default [`Stdin`] and [`Stderr`] handles.
///
/// # Errors
/// The function may error if [`Stderr`] is not a terminal.
pub fn default_terminal() -> Result<DefaultTerminal> { pub fn default_terminal() -> Result<DefaultTerminal> {
Terminal::new(stdin(), stderr()) Terminal::new(stdin(), stderr())
} }

View File

@ -1,21 +1,32 @@
//! Validator and parser types.
#![allow(clippy::type_complexity)] #![allow(clippy::type_complexity)]
use std::ops::RangeInclusive; use std::ops::RangeInclusive;
/// A trait to create validator functions.
pub trait Validator { pub trait Validator {
/// The output of the validator function.
type Output; type Output;
/// The error type returned from the validator function.
type Error; type Error;
/// Create a validator function from the given parameters.
fn to_fn(&self) -> Box<dyn Fn(String) -> Result<Self::Output, Self::Error>>; fn to_fn(&self) -> Box<dyn Fn(String) -> Result<Self::Output, Self::Error>>;
} }
/// A PIN could not be validated from the given input.
#[derive(thiserror::Error, Debug)] #[derive(thiserror::Error, Debug)]
pub enum PinError { pub enum PinError {
/// The provided PIN was too short.
#[error("PIN too short: {0} < {1}")] #[error("PIN too short: {0} < {1}")]
TooShort(usize, usize), TooShort(usize, usize),
/// The provided PIN was too long.
#[error("PIN too long: {0} > {1}")] #[error("PIN too long: {0} > {1}")]
TooLong(usize, usize), TooLong(usize, usize),
/// The PIN contained invalid characters.
#[error("PIN contained invalid characters (found {0} at position {1})")] #[error("PIN contained invalid characters (found {0} at position {1})")]
InvalidCharacters(char, usize), InvalidCharacters(char, usize),
} }
@ -23,8 +34,13 @@ pub enum PinError {
/// Validate that a PIN is of a certain length and matches a range of characters. /// Validate that a PIN is of a certain length and matches a range of characters.
#[derive(Default, Clone)] #[derive(Default, Clone)]
pub struct PinValidator { pub struct PinValidator {
/// The minimum length of provided PINs.
pub min_length: Option<usize>, pub min_length: Option<usize>,
/// The maximum length of provided PINs.
pub max_length: Option<usize>, pub max_length: Option<usize>,
/// The characters allowed by the PIN parser.
pub range: Option<RangeInclusive<char>>, pub range: Option<RangeInclusive<char>>,
} }
@ -57,24 +73,33 @@ impl Validator for PinValidator {
#[cfg(feature = "mnemonic")] #[cfg(feature = "mnemonic")]
pub mod mnemonic { pub mod mnemonic {
//! Validators for mnemonics.
use std::{ops::Range, str::FromStr}; use std::{ops::Range, str::FromStr};
use super::Validator; use super::Validator;
use keyfork_mnemonic_util::{Mnemonic, MnemonicFromStrError}; use keyfork_mnemonic_util::{Mnemonic, MnemonicFromStrError};
/// A mnemonic could not be validated from the given input.
#[derive(thiserror::Error, Debug)] #[derive(thiserror::Error, Debug)]
pub enum MnemonicValidationError { pub enum MnemonicValidationError {
/// The provided mnemonic had an unexpected word length.
#[error("Invalid word length: {0} does not match {1:?}")] #[error("Invalid word length: {0} does not match {1:?}")]
InvalidLength(usize, WordLength), InvalidLength(usize, WordLength),
/// A mnemonic could not be parsed from the given mnemonic.
#[error("{0}")] #[error("{0}")]
MnemonicFromStrError(#[from] MnemonicFromStrError), MnemonicFromStrError(#[from] MnemonicFromStrError),
} }
/// The mnemonic had an unexpected word length.
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub enum WordLength { pub enum WordLength {
/// The bounds of a mnemonic.
Range(Range<usize>), Range(Range<usize>),
/// The exact count of words.
Count(usize), Count(usize),
} }
@ -90,6 +115,7 @@ pub mod mnemonic {
/// Validate a mnemonic of a range of word lengths or a specific length. /// Validate a mnemonic of a range of word lengths or a specific length.
#[derive(Default, Clone)] #[derive(Default, Clone)]
pub struct MnemonicValidator { pub struct MnemonicValidator {
/// The allowed word length of provided mnemonics.
pub word_length: Option<WordLength>, pub word_length: Option<WordLength>,
} }
@ -116,11 +142,14 @@ pub mod mnemonic {
} }
} }
/// A mnemonic in the set of mnemonics could not be validated from the given inputs.
#[derive(thiserror::Error, Debug)] #[derive(thiserror::Error, Debug)]
pub enum MnemonicSetValidationError { pub enum MnemonicSetValidationError {
/// The provided mnemonic did not have the correct amount of words.
#[error("Invalid word length in set {0}: {1} != expected {2}")] #[error("Invalid word length in set {0}: {1} != expected {2}")]
InvalidSetLength(usize, usize, usize), InvalidSetLength(usize, usize, usize),
/// A mnemonic could not be parsed from the provided mnemonics.
#[error("Error parsing mnemonic set {0}: {1}")] #[error("Error parsing mnemonic set {0}: {1}")]
MnemonicFromStrError(usize, MnemonicFromStrError), MnemonicFromStrError(usize, MnemonicFromStrError),
} }
@ -128,6 +157,8 @@ pub mod mnemonic {
/// Validate a set of mnemonics of a specific word length. /// Validate a set of mnemonics of a specific word length.
#[derive(Clone)] #[derive(Clone)]
pub struct MnemonicSetValidator<const N: usize> { pub struct MnemonicSetValidator<const N: usize> {
/// The exact word lengths of all mnemonics. Unlike [`MnemonicValidator`], ranges of words
/// are not allowed.
pub word_lengths: [usize; N], pub word_lengths: [usize; N],
} }

View File

@ -1,20 +1,37 @@
// Source: https://github.com/satoshilabs/slips/blob/master/slip-0010.md#test-vectors //! SLIP-0010 test data for use by derivation tests.
//! Source: https://github.com/satoshilabs/slips/blob/master/slip-0010.md#test-vectors
use std::collections::HashMap; use std::collections::HashMap;
/// Decoded hex, as a [`Vec<u8>`]
pub type DecodedHex = Vec<u8>; pub type DecodedHex = Vec<u8>;
/// A test and its results.
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct Test { pub struct Test {
/// The derivation path for the test.
pub chain: &'static str, pub chain: &'static str,
/// The expected fingerprint.
pub fingerprint: DecodedHex, pub fingerprint: DecodedHex,
/// The expected chain code.
pub chain_code: DecodedHex, pub chain_code: DecodedHex,
/// The expected private key.
pub private_key: DecodedHex, pub private_key: DecodedHex,
/// The expected public key.
pub public_key: DecodedHex, pub public_key: DecodedHex,
} }
/// A set of tests for a given seed.
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct TestData { pub struct TestData {
/// The seed to run the tests on.
pub seed: DecodedHex, pub seed: DecodedHex,
/// The tests to run against the seed.
pub tests: Vec<Test>, pub tests: Vec<Test>,
} }
@ -24,7 +41,10 @@ const SECP256K1_512: &str = "fffcf9f6f3f0edeae7e4e1dedbd8d5d2cfccc9c6c3c0bdbab7b
const ED25519_512: &str = "fffcf9f6f3f0edeae7e4e1dedbd8d5d2cfccc9c6c3c0bdbab7b4b1aeaba8a5a2\ const ED25519_512: &str = "fffcf9f6f3f0edeae7e4e1dedbd8d5d2cfccc9c6c3c0bdbab7b4b1aeaba8a5a2\
9f9c999693908d8a8784817e7b7875726f6c696663605d5a5754514e4b484542"; 9f9c999693908d8a8784817e7b7875726f6c696663605d5a5754514e4b484542";
// Note: This should never error. /// Return the SLIP-0010 test data.
///
/// # Errors
/// This function should not error. If it errors, it is due to malformed hex in the test data.
#[allow(clippy::too_many_lines)] #[allow(clippy::too_many_lines)]
pub fn test_data() -> Result<HashMap<String, Vec<TestData>>, Box<dyn std::error::Error>> { pub fn test_data() -> Result<HashMap<String, Vec<TestData>>, Box<dyn std::error::Error>> {
// Format: // Format:

View File

@ -1,8 +1,14 @@
//! Zero-dependency hex encoding and decoding.
use std::fmt::Write; use std::fmt::Write;
/// The type could not be decoded.
#[derive(Debug)] #[derive(Debug)]
pub enum DecodeError { pub enum DecodeError {
/// An invalid character was encountered.
InvalidCharacter(u8), InvalidCharacter(u8),
/// The amount of characters was invalid. Hex strings must be in pairs of two.
InvalidCharacterCount(usize), InvalidCharacterCount(usize),
} }
@ -21,6 +27,7 @@ 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.
pub fn encode(input: &[u8]) -> String { pub fn encode(input: &[u8]) -> String {
let mut s = String::new(); let mut s = String::new();
for byte in input { for byte in input {
@ -38,6 +45,11 @@ fn val(c: u8) -> Result<u8, DecodeError> {
} }
} }
/// Attempt to decode a string as hex.
///
/// # 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<Vec<u8>, DecodeError> { pub fn decode(input: &str) -> Result<Vec<u8>, DecodeError> {
let len = input.len(); let len = input.len();
if len % 2 != 0 { if len % 2 != 0 {