all crates: add documentation
This commit is contained in:
parent
c8f255f0aa
commit
701f5ca4e9
|
@ -59,7 +59,7 @@ Note: The following features are proposed, and may not yet be implemented.
|
|||
* Unpredictable
|
||||
* Generate a BIP39 phrase from OS or physicalized 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
|
||||
* Given the same seed, repeated derivation requests will be reproducible
|
||||
* Any secret data can be derived again at any point in the future
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
//! A client for Keyforkd.
|
||||
|
||||
use std::{collections::HashMap, os::unix::net::UnixStream, path::PathBuf};
|
||||
|
||||
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)]
|
||||
mod tests;
|
||||
|
||||
/// An error occurred while interacting with Keyforkd.
|
||||
#[derive(Debug, thiserror::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")]
|
||||
EnvVarsNotFound,
|
||||
|
||||
/// The Keyforkd client was unable to connect to the soocket.
|
||||
#[error("Socket was unable to connect to {1}: {0} (make sure keyforkd is running)")]
|
||||
Connect(std::io::Error, PathBuf),
|
||||
|
||||
/// Data could not be written to, or read from, the socket.
|
||||
#[error("Could not write to or from the socket: {0}")]
|
||||
Io(#[from] std::io::Error),
|
||||
|
||||
/// Attempting to serialize or deserialize a type to or from bincode encountered an error.
|
||||
#[error("Could not perform bincode transformation: {0}")]
|
||||
Bincode(#[from] Box<bincode::ErrorKind>),
|
||||
|
||||
/// A frame could not be encoded from the given data.
|
||||
#[error("Could not perform frame transformation: {0}")]
|
||||
FrameEnc(#[from] EncodeError),
|
||||
|
||||
/// A frame could not be decoded from the given data.
|
||||
#[error("Could not perform frame transformation: {0}")]
|
||||
FrameDec(#[from] DecodeError),
|
||||
|
||||
/// An error encountered in Keyforkd.
|
||||
#[error("Error in Keyforkd: {0}")]
|
||||
Keyforkd(#[from] KeyforkdError)
|
||||
}
|
||||
|
||||
#[allow(missing_docs)]
|
||||
pub type Result<T, E = Error> = std::result::Result<T, E>;
|
||||
|
||||
/// Create a [`UnixStream`] from the common Keyforkd socket paths.
|
||||
///
|
||||
/// # Errors
|
||||
/// An error may be returned if the required environment variables were not set or if the socket
|
||||
/// could not be connected to.
|
||||
pub fn get_socket() -> Result<UnixStream, Error> {
|
||||
let socket_vars = std::env::vars()
|
||||
.filter(|(key, _)| ["XDG_RUNTIME_DIR", "KEYFORKD_SOCKET_PATH"].contains(&key.as_str()))
|
||||
|
@ -71,11 +87,21 @@ impl Client {
|
|||
}
|
||||
|
||||
/// 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> {
|
||||
get_socket().map(|socket| Self { socket })
|
||||
}
|
||||
|
||||
/// 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> {
|
||||
try_encode_to(&bincode::serialize(&req)?, &mut self.socket)?;
|
||||
let resp = try_decode_from(&mut self.socket)?;
|
||||
|
|
|
@ -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)]
|
||||
pub enum DerivationError {
|
||||
/// The TTY used for pinentry or passphrase entry was invalid.
|
||||
#[error("The provided TTY was not valid")]
|
||||
InvalidTTY,
|
||||
|
||||
/// No TTY was required for pinentry, but was not provided.
|
||||
#[error("A TTY was required by the pinentry program but was not provided")]
|
||||
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),
|
||||
|
||||
/// An error occurred while deriving data.
|
||||
#[error("Derivation error: {0}")]
|
||||
Derivation(String),
|
||||
}
|
||||
|
||||
/// An error that could occur while interacting with Keyforkd.
|
||||
#[derive(thiserror::Error, Clone, Debug, Serialize, Deserialize)]
|
||||
pub enum Error {
|
||||
/// An error occurred while processing a derivation request.
|
||||
#[error(transparent)]
|
||||
Derivation(#[from] DerivationError),
|
||||
}
|
||||
|
@ -64,6 +71,7 @@ pub enum Response {
|
|||
Derivation(DerivationResponse),
|
||||
}
|
||||
|
||||
/// Attempting to convert from a [`DerivationResponse`] to a [`Response`]
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
#[error("Unable to downcast to {0}")]
|
||||
pub struct Downcast(&'static str);
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
use thiserror::Error;
|
||||
|
||||
/// An error occurred while starting the Keyfork server.
|
||||
#[derive(Debug, Clone, Error)]
|
||||
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")]
|
||||
NoSocketPath,
|
||||
}
|
||||
|
|
|
@ -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::{
|
||||
collections::HashMap,
|
||||
path::{Path, PathBuf},
|
||||
|
@ -17,7 +32,10 @@ use tracing_subscriber::{
|
|||
registry,
|
||||
};
|
||||
|
||||
/// Errors occurring while starting Keyforkd.
|
||||
pub mod error;
|
||||
|
||||
/// Middleware used by Keyforkd.
|
||||
pub mod middleware;
|
||||
pub mod server;
|
||||
pub mod service;
|
||||
|
@ -25,6 +43,7 @@ pub use error::Keyforkd as KeyforkdError;
|
|||
pub use server::UnixServer;
|
||||
pub use service::Keyforkd;
|
||||
|
||||
/// Set up a Tracing subscriber, defaulting to debug mode.
|
||||
#[cfg(feature = "tracing")]
|
||||
pub fn setup_registry() {
|
||||
let envfilter = EnvFilter::builder()
|
||||
|
@ -37,6 +56,11 @@ pub fn setup_registry() {
|
|||
.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(
|
||||
mnemonic: Mnemonic,
|
||||
socket_path: &Path,
|
||||
|
@ -66,6 +90,12 @@ pub async fn start_and_run_server_on(
|
|||
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>> {
|
||||
let runtime_vars = std::env::vars()
|
||||
.filter(|(key, _)| ["XDG_RUNTIME_DIR", "KEYFORKD_SOCKET_PATH"].contains(&key.as_str()))
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
//!
|
||||
|
||||
use keyfork_mnemonic_util::Mnemonic;
|
||||
|
||||
use tokio::io::{self, AsyncBufReadExt, BufReader};
|
||||
|
|
|
@ -5,12 +5,14 @@ use serde::{de::DeserializeOwned, Serialize};
|
|||
use thiserror::Error;
|
||||
use tower::{Layer, Service};
|
||||
|
||||
/// Layer a [`BincodeService`] upon another Service.
|
||||
pub struct BincodeLayer<'a, Request> {
|
||||
phantom: PhantomData<&'a ()>,
|
||||
phantom_request: PhantomData<&'a Request>,
|
||||
}
|
||||
|
||||
impl<'a, Request> BincodeLayer<'a, Request> {
|
||||
/// Create a new [`BincodeLayer`].
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
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)]
|
||||
pub struct BincodeService<S, Request> {
|
||||
service: S,
|
||||
phantom_request: PhantomData<Request>,
|
||||
}
|
||||
|
||||
/// An error encountered either while transforming data or against the interior Service.
|
||||
#[derive(Debug, Error)]
|
||||
pub enum BincodeServiceError {
|
||||
/// An error occurred while polling the internal service.
|
||||
#[error("Error while polling: {0}")]
|
||||
Poll(String),
|
||||
|
||||
/// An error occurred while calling the internal service.
|
||||
#[error("Error while calling: {0}")]
|
||||
Call(String),
|
||||
|
||||
/// An error occurred while converting to or from bincode.
|
||||
#[error("Error while converting: {0}")]
|
||||
Convert(String),
|
||||
}
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
//! A UNIX socket server to run a Tower Service.
|
||||
|
||||
use keyfork_frame::asyncext::{try_decode_from, try_encode_to};
|
||||
use std::{
|
||||
io::Error,
|
||||
|
@ -9,12 +11,17 @@ use tower::{Service, ServiceExt};
|
|||
#[cfg(feature = "tracing")]
|
||||
use tracing::debug;
|
||||
|
||||
/// A UNIX Socket Server.
|
||||
#[allow(clippy::module_name_repetitions)]
|
||||
pub struct UnixServer {
|
||||
listener: UnixListener,
|
||||
}
|
||||
|
||||
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> {
|
||||
let mut path = PathBuf::new();
|
||||
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>>
|
||||
where
|
||||
S: Service<R> + Clone + Send + 'static,
|
||||
|
|
|
@ -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)]
|
||||
|
||||
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
|
||||
// using an Arc or by having a small signature. This is because Service<T> takes &mut self.
|
||||
//
|
||||
|
||||
/// The Keyfork Service.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Keyforkd {
|
||||
seed: Arc<Vec<u8>>,
|
||||
}
|
||||
|
||||
impl Keyforkd {
|
||||
/// Create a new instance of Keyfork from a given seed.
|
||||
pub fn new(seed: Vec<u8>) -> Self {
|
||||
Self {
|
||||
seed: Arc::new(seed),
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
//!
|
||||
|
||||
use std::{env, process::ExitCode, str::FromStr};
|
||||
|
||||
use keyfork_derive_util::{
|
||||
|
@ -6,18 +8,23 @@ use keyfork_derive_util::{
|
|||
};
|
||||
use keyforkd_client::Client;
|
||||
|
||||
/// Any error that can occur while deriving a key.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum Error {
|
||||
/// The given algorithm could not be parsed.
|
||||
#[error("Could not parse the given algorithm {0:?}: {1}")]
|
||||
AlgoFormat(String, DerivationError),
|
||||
|
||||
/// The given path could not be parsed.
|
||||
#[error("Could not parse the given path: {0}")]
|
||||
PathFormat(#[from] keyfork_derive_util::path::Error),
|
||||
|
||||
/// The request to derive data failed.
|
||||
#[error("Unable to perform key derivation request: {0}")]
|
||||
KeyforkdClient(#[from] keyforkd_client::Error),
|
||||
}
|
||||
|
||||
#[allow(missing_docs)]
|
||||
pub type Result<T, E = Error> = std::result::Result<T, E>;
|
||||
|
||||
fn validate(algo: &str, path: &str) -> Result<(DerivationAlgorithm, DerivationPath)> {
|
||||
|
|
|
@ -5,6 +5,9 @@ edition = "2021"
|
|||
license = "AGPL-3.0-only"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
[features]
|
||||
default = []
|
||||
bin = ["sequoia-openpgp/crypto-nettle"]
|
||||
|
||||
[dependencies]
|
||||
keyfork-derive-util = { version = "0.1.0", path = "../keyfork-derive-util", default-features = false, features = ["ed25519"] }
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
//! Creation of OpenPGP certificates from BIP-0032 derived data.
|
||||
|
||||
use std::time::{Duration, SystemTime, SystemTimeError};
|
||||
|
||||
use derive_util::{
|
||||
|
@ -17,35 +19,52 @@ use sequoia_openpgp::{
|
|||
};
|
||||
pub use sequoia_openpgp as openpgp;
|
||||
|
||||
/// An error occurred while creating an OpenPGP key.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum Error {
|
||||
/// An error occurred with the internal OpenPGP library.
|
||||
#[error("{0}")]
|
||||
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:?}")]
|
||||
InvalidKeyFlags(KeyFlags),
|
||||
|
||||
/// The derivation response contained incorrect data.
|
||||
#[error("Incorrect derived data: {0}")]
|
||||
IncorrectDerivedData(#[from] TryFromDerivationResponseError),
|
||||
|
||||
/// A derivation index could not be created from the given index.
|
||||
#[error("Could not create derivation index: {0}")]
|
||||
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}")]
|
||||
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}")]
|
||||
SystemTime(#[from] SystemTimeError),
|
||||
|
||||
/// The first certificate in an OpenPGP keychain must have the Certify capability.
|
||||
#[error("First key in certificate must have certify capability")]
|
||||
NotCert,
|
||||
|
||||
/// The given index was out of bounds.
|
||||
#[error("Index out of bounds: {0}")]
|
||||
IndexOutOfBounds(#[from] std::num::TryFromIntError),
|
||||
}
|
||||
|
||||
#[allow(missing_docs)]
|
||||
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> {
|
||||
let primary_key_flags = match keys.get(0) {
|
||||
Some(kf) if kf.for_certification() => kf,
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
//!
|
||||
|
||||
use std::{env, process::ExitCode, str::FromStr};
|
||||
|
||||
use keyfork_derive_util::{
|
||||
|
|
|
@ -1,10 +1,15 @@
|
|||
//! ## Path data guesswork for BIP-0032 derivation paths.
|
||||
|
||||
#![allow(clippy::unreadable_literal)]
|
||||
|
||||
use keyfork_derive_util::{DerivationIndex, DerivationPath};
|
||||
|
||||
/// The default derivation path for OpenPGP.
|
||||
pub static OPENPGP: DerivationIndex = DerivationIndex::new_unchecked(7366512, true);
|
||||
|
||||
/// A derivation target.
|
||||
pub enum Target {
|
||||
/// An OpenPGP key, whose account is the given index.
|
||||
OpenPGP(DerivationIndex),
|
||||
}
|
||||
|
||||
|
|
|
@ -1,2 +1,4 @@
|
|||
///
|
||||
pub mod private_key;
|
||||
///
|
||||
pub mod public_key;
|
||||
|
|
|
@ -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> {
|
||||
Ok(Self {
|
||||
private_key: K::from_bytes(seed.try_into()?),
|
||||
|
@ -113,6 +117,7 @@ where
|
|||
&self.private_key
|
||||
}
|
||||
|
||||
/// Create an [`ExtendedPublicKey`] for the current [`PrivateKey`].
|
||||
pub fn extended_public_key(&self) -> ExtendedPublicKey<K::PublicKey> {
|
||||
ExtendedPublicKey::new(self.public_key(), self.chain_code)
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
self.0
|
||||
}
|
||||
|
|
|
@ -2,11 +2,17 @@
|
|||
|
||||
//! BIP-0032 derivation utilities.
|
||||
|
||||
///
|
||||
pub mod extended_key;
|
||||
///
|
||||
pub mod index;
|
||||
///
|
||||
pub mod path;
|
||||
///
|
||||
pub mod private_key;
|
||||
///
|
||||
pub mod public_key;
|
||||
///
|
||||
pub mod request;
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
|
@ -35,18 +35,23 @@ impl DerivationPath {
|
|||
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 {
|
||||
self.path.len()
|
||||
}
|
||||
|
||||
/// Returns true if there are no path segments.
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.path.is_empty()
|
||||
}
|
||||
|
||||
/// Append an index to the path.
|
||||
pub fn push(&mut self, index: DerivationIndex) {
|
||||
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 {
|
||||
self.path.push(index);
|
||||
self
|
||||
|
|
|
@ -7,27 +7,41 @@ use crate::{
|
|||
use keyfork_mnemonic_util::{Mnemonic, MnemonicGenerationError};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// An error encountered while deriving a key.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
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,
|
||||
|
||||
/// A seed was unable to be created from the mnemonic.
|
||||
#[error("Unable to create seed from mnemonic: {0}")]
|
||||
Mnemonic(#[from] MnemonicGenerationError),
|
||||
|
||||
/// Generating an [`ExtendedPrivateKey`] resulted in an error.
|
||||
#[error("{0}")]
|
||||
ExtendedPrivateKey(#[from] XPrvError),
|
||||
}
|
||||
|
||||
#[allow(missing_docs)]
|
||||
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)]
|
||||
pub enum DerivationAlgorithm {
|
||||
#[allow(missing_docs)]
|
||||
Ed25519,
|
||||
#[allow(missing_docs)]
|
||||
Secp256k1,
|
||||
}
|
||||
|
||||
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> {
|
||||
match self {
|
||||
#[cfg(feature = "ed25519")]
|
||||
|
@ -66,6 +80,7 @@ impl std::str::FromStr for DerivationAlgorithm {
|
|||
}
|
||||
}
|
||||
|
||||
/// A derivation request.
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct DerivationRequest {
|
||||
algorithm: DerivationAlgorithm,
|
||||
|
@ -73,6 +88,7 @@ pub struct DerivationRequest {
|
|||
}
|
||||
|
||||
impl DerivationRequest {
|
||||
/// Create a new derivation request.
|
||||
pub fn new(algorithm: DerivationAlgorithm, path: &DerivationPath) -> Self {
|
||||
Self {
|
||||
algorithm,
|
||||
|
@ -80,29 +96,47 @@ impl DerivationRequest {
|
|||
}
|
||||
}
|
||||
|
||||
/// Return the path of the derivation request.
|
||||
pub fn path(&self) -> &DerivationPath {
|
||||
&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> {
|
||||
// TODO: passphrase support and/or store passphrase within mnemonic
|
||||
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> {
|
||||
self.algorithm.derive(seed, &self.path)
|
||||
}
|
||||
}
|
||||
|
||||
/// A response to a [`DerivationRequest`]
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct DerivationResponse {
|
||||
/// The algorithm used to derive the data.
|
||||
pub algorithm: DerivationAlgorithm,
|
||||
|
||||
/// The derived private key.
|
||||
pub data: Vec<u8>,
|
||||
|
||||
/// The chain code, used for further derivation.
|
||||
pub chain_code: [u8; 32],
|
||||
|
||||
/// The depth, used for further derivation.
|
||||
pub depth: u8,
|
||||
}
|
||||
|
||||
impl DerivationResponse {
|
||||
/// Create a [`DerivationResponse`] with the given values.
|
||||
pub fn with_algo_and_xprv<T: PrivateKey + Clone>(
|
||||
algorithm: DerivationAlgorithm,
|
||||
xprv: &ExtendedPrivateKey<T>,
|
||||
|
@ -116,11 +150,15 @@ impl DerivationResponse {
|
|||
}
|
||||
}
|
||||
|
||||
/// An error when creating a [`DerivationResponse`].
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum TryFromDerivationResponseError {
|
||||
/// The algorithm used to derive the data does not match the algorithm of the
|
||||
/// [`ExtendedPrivateKey`] being created.
|
||||
#[error("incorrect algorithm provided")]
|
||||
Algorithm,
|
||||
|
||||
/// An error occurred while creating an [`ExtendedPrivateKey`] from the given response.
|
||||
#[error("{0}")]
|
||||
ExtendedPrivateKey(#[from] XPrvError),
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@ default = ["openpgp", "openpgp-card", "qrcode"]
|
|||
openpgp = ["sequoia-openpgp", "anyhow"]
|
||||
openpgp-card = ["openpgp-card-sequoia", "card-backend-pcsc", "card-backend", "dep:openpgp-card"]
|
||||
qrcode = ["keyfork-qrcode"]
|
||||
bin = ["sequoia-openpgp/crypto-nettle", "keyfork-qrcode/decode-backend-rqrr"]
|
||||
|
||||
[dependencies]
|
||||
keyfork-prompt = { version = "0.1.0", path = "../util/keyfork-prompt", default-features = false, features = ["mnemonic"] }
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
//!
|
||||
|
||||
use std::{
|
||||
env,
|
||||
fs::File,
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
//!
|
||||
|
||||
use std::{
|
||||
env,
|
||||
fs::File,
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
//!
|
||||
|
||||
use std::{
|
||||
env,
|
||||
process::ExitCode,
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
//!
|
||||
|
||||
use std::{env, path::PathBuf, process::ExitCode, str::FromStr};
|
||||
|
||||
use keyfork_shard::openpgp::{discover_certs, openpgp::Cert, split};
|
||||
|
|
|
@ -1,3 +1,7 @@
|
|||
//! ## Keyfork Shard
|
||||
//!
|
||||
//! Utilities for securing secrets using Shamir's Secret Sharing.
|
||||
|
||||
use std::io::{stdin, stdout, Write};
|
||||
|
||||
use aes_gcm::{
|
||||
|
@ -17,15 +21,20 @@ use x25519_dalek::{EphemeralSecret, PublicKey};
|
|||
#[cfg(feature = "openpgp")]
|
||||
pub mod openpgp;
|
||||
|
||||
/// Errors encountered while creating or combining shares using Shamir's Secret Sharing.
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum SharksError {
|
||||
/// A Shamir Share could not be created.
|
||||
#[error("Error creating share: {0}")]
|
||||
Share(String),
|
||||
|
||||
/// The Shamir shares could not be combined.
|
||||
#[error("Error combining shares: {0}")]
|
||||
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)]
|
||||
#[error("Mnemonic or QR code did not store enough data")]
|
||||
pub struct InvalidData;
|
||||
|
@ -37,8 +46,16 @@ pub struct InvalidData;
|
|||
pub(crate) const HUNK_VERSION: u8 = 1;
|
||||
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
|
||||
/// incompatible with the currently running version.
|
||||
pub fn remote_decrypt(w: &mut impl Write) -> Result<(), Box<dyn std::error::Error>> {
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
//! OpenPGP Shard functionality.
|
||||
|
||||
use std::{
|
||||
collections::{HashMap, VecDeque},
|
||||
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
|
||||
const ENC_LEN: u8 = 4 * 16;
|
||||
|
||||
/// Errors encountered while performing operations using OpenPGP.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum Error {
|
||||
/// Errors encountered while creating or combining shares.
|
||||
#[error("{0}")]
|
||||
Sharks(#[from] SharksError),
|
||||
|
||||
/// Unable to decrypt a share.
|
||||
#[error("Error decrypting share: {0}")]
|
||||
SymDecryptShare(#[from] AesError),
|
||||
|
||||
/// The generated AES key is of an invalid length.
|
||||
#[error("Invalid length of AES key: {0}")]
|
||||
AesLength(#[from] InvalidLength),
|
||||
|
||||
/// The HKDF function was given an input of an invalid length.
|
||||
#[error("Invalid KDF length: {0}")]
|
||||
HkdfLength(#[from] HkdfInvalidLength),
|
||||
|
||||
/// The secret did not match the previously-known secret fingerprint.
|
||||
#[error("Derived secret hash {0} != expected {1}")]
|
||||
InvalidSecret(Fingerprint, Fingerprint),
|
||||
|
||||
/// An error occurred while performing an OpenPGP operation.
|
||||
#[error("OpenPGP error: {0}")]
|
||||
Sequoia(#[source] anyhow::Error),
|
||||
|
||||
/// An IO error occurred while performing an OpenPGP operation.
|
||||
#[error("OpenPGP IO error: {0}")]
|
||||
SequoiaIo(#[source] std::io::Error),
|
||||
|
||||
/// An error occurred while using a keyring.
|
||||
#[error("Keyring error: {0}")]
|
||||
Keyring(#[from] keyring::Error),
|
||||
|
||||
/// An error occurred while using a smartcard.
|
||||
#[error("Smartcard error: {0}")]
|
||||
Smartcard(#[from] smartcard::Error),
|
||||
|
||||
/// An error occurred while displaying a prompt.
|
||||
#[error("Prompt error: {0}")]
|
||||
Prompt(#[from] PromptError),
|
||||
|
||||
/// An error occurred while generating a mnemonic.
|
||||
#[error("Mnemonic generation error: {0}")]
|
||||
MnemonicGeneration(#[from] MnemonicGenerationError),
|
||||
|
||||
/// An error occurred while parsing a mnemonic.
|
||||
#[error("Mnemonic parse error: {0}")]
|
||||
MnemonicFromStr(#[from] MnemonicFromStrError),
|
||||
|
||||
/// An error occurred while converting mnemonic data.
|
||||
#[error("{0}")]
|
||||
InvalidMnemonicData(#[from] InvalidData),
|
||||
|
||||
/// An IO error occurred.
|
||||
#[error("IO error: {0}")]
|
||||
Io(#[source] std::io::Error),
|
||||
|
||||
/// An error occurred while parsing a derivation path.
|
||||
#[error("Derivation path: {0}")]
|
||||
DerivationPath(#[from] keyfork_derive_openpgp::derive_util::path::Error),
|
||||
|
||||
/// An error occurred while requesting derivation.
|
||||
#[error("Derivation request: {0}")]
|
||||
DerivationRequest(#[from] keyfork_derive_openpgp::derive_util::request::DerivationError),
|
||||
|
||||
/// An error occurred while decoding hex.
|
||||
#[error("Unable to decode hex: {0}")]
|
||||
HexDecode(#[from] smex::DecodeError),
|
||||
|
||||
/// An error occurred while creating an OpenPGP cert.
|
||||
#[error("Keyfork OpenPGP: {0}")]
|
||||
KeyforkOpenPGP(#[from] keyfork_derive_openpgp::Error),
|
||||
}
|
||||
|
||||
#[allow(missing_docs)]
|
||||
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)]
|
||||
pub struct EncryptedMessage {
|
||||
pkesks: Vec<PKESK>,
|
||||
|
@ -125,6 +148,7 @@ pub struct EncryptedMessage {
|
|||
}
|
||||
|
||||
impl EncryptedMessage {
|
||||
/// Create a new EncryptedMessage from known parts.
|
||||
pub fn new(pkesks: &mut Vec<PKESK>, seip: SEIP) -> Self {
|
||||
Self {
|
||||
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>>
|
||||
where
|
||||
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>> {
|
||||
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.
|
||||
/// 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>> {
|
||||
|
@ -401,6 +444,15 @@ fn decrypt_one(
|
|||
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
|
||||
///
|
||||
/// 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(())
|
||||
}
|
||||
|
||||
/// 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(
|
||||
certs: Vec<Cert>,
|
||||
metadata: &EncryptedMessage,
|
||||
|
@ -600,6 +657,13 @@ pub fn combine(
|
|||
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
|
||||
///
|
||||
/// The function may panic if the metadata can't properly store the certificates used to generate
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
#![doc = include_str!("../../../README.md")]
|
||||
|
||||
#![allow(clippy::module_name_repetitions)]
|
||||
|
||||
use std::process::ExitCode;
|
||||
|
||||
use clap::Parser;
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
//!
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
use keyfork_qrcode::scan_camera;
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
//! Encoding and decoding QR codes.
|
||||
|
||||
use image::io::Reader as ImageReader;
|
||||
use std::{
|
||||
io::{Cursor, Write},
|
||||
|
@ -10,40 +12,61 @@ use v4l::{
|
|||
Device,
|
||||
};
|
||||
|
||||
/// A QR code could not be generated.
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum QRGenerationError {
|
||||
/// The resulting QR coode could not be read from the generator program.
|
||||
#[error("{0}")]
|
||||
Io(#[from] std::io::Error),
|
||||
|
||||
/// The generator program produced invalid data.
|
||||
#[error("Could not decode output of qrencode (this is a bug!): {0}")]
|
||||
StringParse(#[from] std::string::FromUtf8Error),
|
||||
}
|
||||
|
||||
/// An error occurred while scanning for a QR code.
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum QRCodeScanError {
|
||||
/// The camera could not load the requested format.
|
||||
#[error("Camera could not use {expected} format, instead used {actual}")]
|
||||
CameraGaveBadFormat {
|
||||
/// The expected format, in FourCC format.
|
||||
expected: String,
|
||||
|
||||
/// The actual format, in FourCC format.
|
||||
actual: String,
|
||||
},
|
||||
|
||||
/// Interfacing with the camera resulted in an error.
|
||||
#[error("Unable to interface with camera: {0}")]
|
||||
CameraIO(#[from] std::io::Error),
|
||||
|
||||
/// Decoding an image from the camera resulted in an error.
|
||||
#[error("Could not decode image: {0}")]
|
||||
ImageDecode(#[from] image::ImageError),
|
||||
}
|
||||
|
||||
/// The level of error correction when generating a QR code.
|
||||
#[derive(Default)]
|
||||
pub enum ErrorCorrection {
|
||||
/// 7% of the QR code can be recovered.
|
||||
#[default]
|
||||
Lowest,
|
||||
|
||||
/// 15% of the QR code can be recovered.
|
||||
Medium,
|
||||
|
||||
/// 25% of the QR code can be recovered.
|
||||
Quartile,
|
||||
|
||||
/// 30% of the QR code can be recovered.
|
||||
Highest,
|
||||
}
|
||||
|
||||
/// 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(
|
||||
text: &str,
|
||||
error_correction: impl Into<Option<ErrorCorrection>>,
|
||||
|
@ -73,6 +96,7 @@ pub fn qrencode(
|
|||
Ok(result)
|
||||
}
|
||||
|
||||
/// Continuously scan the `index`-th camera for a QR code.
|
||||
#[cfg(feature = "decode-backend-rqrr")]
|
||||
pub fn scan_camera(timeout: Duration, index: usize) -> Result<Option<String>, QRCodeScanError> {
|
||||
let device = Device::new(index)?;
|
||||
|
@ -100,6 +124,7 @@ pub fn scan_camera(timeout: Duration, index: usize) -> Result<Option<String>, QR
|
|||
Ok(None)
|
||||
}
|
||||
|
||||
/// Continuously scan the `index`-th camera for a QR code.
|
||||
#[cfg(feature = "decode-backend-zbar")]
|
||||
pub fn scan_camera(timeout: Duration, index: usize) -> Result<Option<String>, QRCodeScanError> {
|
||||
let device = Device::new(index)?;
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
#![allow(missing_docs, clippy::missing_errors_doc)]
|
||||
|
||||
use std::{env::VarError, path::Path};
|
||||
|
||||
use pkg_config::Config;
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
#![allow(non_upper_case_globals, non_camel_case_types, non_snake_case)]
|
||||
#![allow(missing_docs)]
|
||||
|
||||
include!(concat!(env!("OUT_DIR"), "/bindings.rs"));
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
//! Scan for a barcode or QR code from the default camera.
|
||||
|
||||
use std::{
|
||||
io::Cursor,
|
||||
time::{Duration, SystemTime},
|
||||
|
@ -31,7 +33,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||
.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()));
|
||||
return Ok(());
|
||||
}
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
//! Conversions for the internal Image type used by zbar.
|
||||
|
||||
use super::sys;
|
||||
|
||||
/// The internal image type used by zbar.
|
||||
pub struct Image {
|
||||
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.
|
||||
|
|
|
@ -1,20 +1,29 @@
|
|||
//! ## Image scanning
|
||||
|
||||
use super::{
|
||||
image::Image,
|
||||
symbol::{Symbol, SymbolType},
|
||||
sys, Config,
|
||||
};
|
||||
|
||||
/// Errors encountered while creating or using an [`ImageScanner`].
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum ImageScannerError {
|
||||
/// The provided configuration resulted in an error.
|
||||
#[error("Unable to set Image Scanner configuration")]
|
||||
UnableToSetConfig,
|
||||
}
|
||||
|
||||
/// An [`ImageScanner`].
|
||||
///
|
||||
/// Link: [`sys::zbar_image_scanner_t`]
|
||||
pub struct ImageScanner {
|
||||
inner: *mut sys::zbar_image_scanner_t,
|
||||
}
|
||||
|
||||
impl ImageScanner {
|
||||
/// create a new ImageScanner.
|
||||
///
|
||||
/// Link: [`sys::zbar_image_scanner_create`]
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
|
@ -22,7 +31,12 @@ impl ImageScanner {
|
|||
}
|
||||
}
|
||||
|
||||
/// Set a configuration option for the ImageScanner.
|
||||
///
|
||||
/// Link: [`sys::zbar_image_scanner_set_config`]
|
||||
///
|
||||
/// # Errors
|
||||
/// The function may error if the provided configuration was invalid.
|
||||
pub fn set_config(
|
||||
&mut self,
|
||||
symbol: SymbolType,
|
||||
|
@ -39,10 +53,9 @@ impl ImageScanner {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Link: [`sys::zbar_scan_image`]
|
||||
/// Scan an [`Image`] for QR codes.
|
||||
///
|
||||
/// TODO: move `image` to newtype, offering conversions
|
||||
/// to and from image::Image
|
||||
/// Link: [`sys::zbar_scan_image`]
|
||||
///
|
||||
/// TODO: return an iterator over scanned values
|
||||
pub fn scan_image(
|
||||
|
|
|
@ -1,8 +1,13 @@
|
|||
//!
|
||||
|
||||
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;
|
||||
|
||||
// TODO: config, modifiers
|
||||
|
||||
/// A Symbol detected by zbar.
|
||||
#[derive(Debug)]
|
||||
pub struct Symbol {
|
||||
_type: SymbolType,
|
||||
|
@ -17,14 +22,17 @@ impl Symbol {
|
|||
}
|
||||
}
|
||||
|
||||
/// The type of symbol
|
||||
pub fn _type(&self) -> SymbolType {
|
||||
self._type
|
||||
}
|
||||
|
||||
/// The internal data of the image.
|
||||
pub fn data(&self) -> &[u8] {
|
||||
self.data.as_slice()
|
||||
}
|
||||
|
||||
/// Consume self, returning the internal data.
|
||||
pub fn into_data(self) -> Vec<u8> {
|
||||
self.data
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ use std::io;
|
|||
|
||||
use keyfork_crossterm::event::{self, Event, KeyCode, KeyEvent};
|
||||
|
||||
/// Read a character from input.
|
||||
pub fn read_char() -> io::Result<char> {
|
||||
loop {
|
||||
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> {
|
||||
let mut line = String::new();
|
||||
while let Event::Key(KeyEvent { code, .. }) = event::read()? {
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
//!
|
||||
|
||||
use keyfork_crossterm::{
|
||||
execute,
|
||||
terminal::{size, SetSize},
|
||||
|
@ -5,7 +7,7 @@ use keyfork_crossterm::{
|
|||
};
|
||||
use std::io::{stdin, stdout};
|
||||
|
||||
pub fn main() {
|
||||
fn main() {
|
||||
println!("size: {:?}", size().unwrap());
|
||||
execute!(stdout(), SetSize(10, 10)).unwrap();
|
||||
println!("resized: {:?}", size().unwrap());
|
||||
|
|
|
@ -72,6 +72,7 @@ where
|
|||
Ok(user_char)
|
||||
}
|
||||
|
||||
/// Read a character from input.
|
||||
pub fn read_char() -> io::Result<char> {
|
||||
loop {
|
||||
if let Event::Key(KeyEvent {
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
#![allow(missing_docs, clippy::missing_errors_doc, clippy::missing_panics_doc)]
|
||||
#![deny(unused_imports, unused_must_use)]
|
||||
|
||||
//! # Cross-platform Terminal Manipulation Library
|
||||
|
|
|
@ -149,7 +149,7 @@ mod tests {
|
|||
|
||||
// Helper for execute tests to confirm flush
|
||||
#[derive(Default, Debug, Clone)]
|
||||
pub(self) struct FakeWrite {
|
||||
struct FakeWrite {
|
||||
buffer: String,
|
||||
flushed: bool,
|
||||
}
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
//! Utilities for reading entropy from secure sources.
|
||||
|
||||
use std::{fs::{read_dir, read_to_string, File}, io::Read};
|
||||
|
||||
static WARNING_LINKS: [&str; 1] =
|
||||
|
@ -45,6 +47,7 @@ fn ensure_offline() {
|
|||
}
|
||||
}
|
||||
|
||||
/// Ensure the system is safe.
|
||||
pub fn ensure_safe() {
|
||||
if !std::env::vars()
|
||||
.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> {
|
||||
ensure_safe();
|
||||
let mut vec = vec![0u8; byte_count];
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
//! Functions for decoding from and encoding to types that implement [`AsyncRead`] and
|
||||
//! [`AsyncWrite`].
|
||||
|
||||
use std::marker::Unpin;
|
||||
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 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 async fn try_decode_from(
|
||||
readable: &mut (impl AsyncRead + Unpin),
|
||||
) -> Result<Vec<u8>, DecodeError> {
|
||||
|
@ -31,7 +35,7 @@ pub async fn try_decode_from(
|
|||
/// # Errors
|
||||
/// An error may be returned if:
|
||||
/// * 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(
|
||||
data: &[u8],
|
||||
writable: &mut (impl AsyncWrite + Unpin),
|
||||
|
|
|
@ -19,6 +19,7 @@ pub mod asyncext;
|
|||
|
||||
use sha2::{Digest, Sha256};
|
||||
|
||||
/// An error encountered while decoding a frame.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum DecodeError {
|
||||
/// 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),
|
||||
}
|
||||
|
||||
/// An error encountered while encoding a frame.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum EncodeError {
|
||||
/// 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)
|
||||
}
|
||||
|
||||
/// 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> {
|
||||
let hash = hash(data);
|
||||
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[..])
|
||||
}
|
||||
|
||||
/// 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> {
|
||||
let mut bytes = 0u32.to_be_bytes();
|
||||
readable.read_exact(&mut bytes)?;
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
//! Zero-dependency Mnemonic encoding and decoding.
|
||||
|
||||
use std::{error::Error, fmt::Display, str::FromStr, sync::Arc};
|
||||
|
||||
use hmac::Hmac;
|
||||
|
@ -258,18 +260,22 @@ impl Mnemonic {
|
|||
}
|
||||
}
|
||||
|
||||
/// The internal representation of the decoded data.
|
||||
pub fn as_bytes(&self) -> &[u8] {
|
||||
&self.entropy
|
||||
}
|
||||
|
||||
/// Drop self, returning the decoded data.
|
||||
pub fn to_bytes(self) -> Vec<u8> {
|
||||
self.entropy
|
||||
}
|
||||
|
||||
/// Clone the existing entropy.
|
||||
pub fn entropy(&self) -> Vec<u8> {
|
||||
self.entropy.clone()
|
||||
}
|
||||
|
||||
/// Create a BIP-0032 seed from the provided data and an optional passphrase.
|
||||
pub fn seed<'a>(
|
||||
&self,
|
||||
passphrase: impl Into<Option<&'a str>>,
|
||||
|
@ -284,6 +290,7 @@ impl Mnemonic {
|
|||
Ok(seed.to_vec())
|
||||
}
|
||||
|
||||
/// Encode the mnemonic into a list of wordlist indexes.
|
||||
pub fn words(self) -> (Vec<usize>, Arc<Wordlist>) {
|
||||
let bit_count = self.entropy.len() * 8;
|
||||
let mut bits = vec![false; bit_count + bit_count / 32];
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
//!
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let bit_size: usize = std::env::args()
|
||||
.nth(1)
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
//!
|
||||
|
||||
use keyfork_mnemonic_util::Mnemonic;
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
//!
|
||||
|
||||
use std::io::{stdin, stdout};
|
||||
|
||||
use keyfork_prompt::{
|
|
@ -1,36 +1,66 @@
|
|||
//! Prompt display and interaction management.
|
||||
|
||||
use std::borrow::Borrow;
|
||||
|
||||
#[cfg(feature = "mnemonic")]
|
||||
use keyfork_mnemonic_util::Wordlist;
|
||||
|
||||
///
|
||||
pub mod terminal;
|
||||
pub mod validators;
|
||||
pub use terminal::{Terminal, DefaultTerminal, default_terminal};
|
||||
|
||||
/// An error occurred while displaying a prompt.
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
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")]
|
||||
NotATTY,
|
||||
|
||||
/// Validating user input failed.
|
||||
#[error("Validation of the input failed after {0} retries (last error: {1})")]
|
||||
Validation(u8, String),
|
||||
|
||||
/// An error occurred while interacting with a terminal.
|
||||
#[error("IO Error: {0}")]
|
||||
IO(#[from] std::io::Error),
|
||||
}
|
||||
|
||||
#[allow(missing_docs)]
|
||||
pub type Result<T, E = Error> = std::result::Result<T, E>;
|
||||
|
||||
/// A message displayed by [`PromptHandler::prompt_message`].
|
||||
pub enum Message {
|
||||
/// A textual message, wrapping at space boundaries when reaching the end of the terminal.
|
||||
Text(String),
|
||||
/// A data message, with no word wrapping, and automatic hiding of the message when a terminal
|
||||
/// is too small.
|
||||
Data(String),
|
||||
}
|
||||
|
||||
/// A trait to allow displaying prompts and accepting input.
|
||||
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>;
|
||||
|
||||
/// 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>;
|
||||
|
||||
/// 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")]
|
||||
fn prompt_validated_wordlist<V, F, E>(
|
||||
&mut self,
|
||||
|
@ -43,8 +73,19 @@ pub trait PromptHandler {
|
|||
F: Fn(String) -> Result<V, E>,
|
||||
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>;
|
||||
|
||||
/// 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>(
|
||||
&mut self,
|
||||
prompt: &str,
|
||||
|
@ -55,5 +96,10 @@ pub trait PromptHandler {
|
|||
F: Fn(String) -> Result<V, E>,
|
||||
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<()>;
|
||||
}
|
||||
|
|
|
@ -14,6 +14,7 @@ use keyfork_crossterm::{
|
|||
|
||||
use crate::{PromptHandler, Message, Wordlist, Error};
|
||||
|
||||
#[allow(missing_docs)]
|
||||
pub type Result<T, E = Error> = std::result::Result<T, E>;
|
||||
|
||||
struct TerminalGuard<'a, R, W>
|
||||
|
@ -124,6 +125,7 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
/// A handler for a terminal.
|
||||
pub struct Terminal<R, W> {
|
||||
read: BufReader<R>,
|
||||
write: W,
|
||||
|
@ -135,6 +137,10 @@ where
|
|||
R: Read + 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> {
|
||||
if !write_handle.is_tty() {
|
||||
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>;
|
||||
|
||||
/// 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> {
|
||||
Terminal::new(stdin(), stderr())
|
||||
}
|
||||
|
|
|
@ -1,21 +1,32 @@
|
|||
//! Validator and parser types.
|
||||
|
||||
#![allow(clippy::type_complexity)]
|
||||
use std::ops::RangeInclusive;
|
||||
|
||||
/// A trait to create validator functions.
|
||||
pub trait Validator {
|
||||
/// The output of the validator function.
|
||||
type Output;
|
||||
|
||||
/// The error type returned from the validator function.
|
||||
type Error;
|
||||
|
||||
/// Create a validator function from the given parameters.
|
||||
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)]
|
||||
pub enum PinError {
|
||||
/// The provided PIN was too short.
|
||||
#[error("PIN too short: {0} < {1}")]
|
||||
TooShort(usize, usize),
|
||||
|
||||
/// The provided PIN was too long.
|
||||
#[error("PIN too long: {0} > {1}")]
|
||||
TooLong(usize, usize),
|
||||
|
||||
/// The PIN contained invalid characters.
|
||||
#[error("PIN contained invalid characters (found {0} at position {1})")]
|
||||
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.
|
||||
#[derive(Default, Clone)]
|
||||
pub struct PinValidator {
|
||||
/// The minimum length of provided PINs.
|
||||
pub min_length: Option<usize>,
|
||||
|
||||
/// The maximum length of provided PINs.
|
||||
pub max_length: Option<usize>,
|
||||
|
||||
/// The characters allowed by the PIN parser.
|
||||
pub range: Option<RangeInclusive<char>>,
|
||||
}
|
||||
|
||||
|
@ -57,24 +73,33 @@ impl Validator for PinValidator {
|
|||
|
||||
#[cfg(feature = "mnemonic")]
|
||||
pub mod mnemonic {
|
||||
//! Validators for mnemonics.
|
||||
|
||||
use std::{ops::Range, str::FromStr};
|
||||
|
||||
use super::Validator;
|
||||
|
||||
use keyfork_mnemonic_util::{Mnemonic, MnemonicFromStrError};
|
||||
|
||||
/// A mnemonic could not be validated from the given input.
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum MnemonicValidationError {
|
||||
/// The provided mnemonic had an unexpected word length.
|
||||
#[error("Invalid word length: {0} does not match {1:?}")]
|
||||
InvalidLength(usize, WordLength),
|
||||
|
||||
/// A mnemonic could not be parsed from the given mnemonic.
|
||||
#[error("{0}")]
|
||||
MnemonicFromStrError(#[from] MnemonicFromStrError),
|
||||
}
|
||||
|
||||
/// The mnemonic had an unexpected word length.
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum WordLength {
|
||||
/// The bounds of a mnemonic.
|
||||
Range(Range<usize>),
|
||||
|
||||
/// The exact count of words.
|
||||
Count(usize),
|
||||
}
|
||||
|
||||
|
@ -90,6 +115,7 @@ pub mod mnemonic {
|
|||
/// Validate a mnemonic of a range of word lengths or a specific length.
|
||||
#[derive(Default, Clone)]
|
||||
pub struct MnemonicValidator {
|
||||
/// The allowed word length of provided mnemonics.
|
||||
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)]
|
||||
pub enum MnemonicSetValidationError {
|
||||
/// The provided mnemonic did not have the correct amount of words.
|
||||
#[error("Invalid word length in set {0}: {1} != expected {2}")]
|
||||
InvalidSetLength(usize, usize, usize),
|
||||
|
||||
/// A mnemonic could not be parsed from the provided mnemonics.
|
||||
#[error("Error parsing mnemonic set {0}: {1}")]
|
||||
MnemonicFromStrError(usize, MnemonicFromStrError),
|
||||
}
|
||||
|
@ -128,6 +157,8 @@ pub mod mnemonic {
|
|||
/// Validate a set of mnemonics of a specific word length.
|
||||
#[derive(Clone)]
|
||||
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],
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
||||
/// Decoded hex, as a [`Vec<u8>`]
|
||||
pub type DecodedHex = Vec<u8>;
|
||||
|
||||
/// A test and its results.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Test {
|
||||
/// The derivation path for the test.
|
||||
pub chain: &'static str,
|
||||
|
||||
/// The expected fingerprint.
|
||||
pub fingerprint: DecodedHex,
|
||||
|
||||
/// The expected chain code.
|
||||
pub chain_code: DecodedHex,
|
||||
|
||||
/// The expected private key.
|
||||
pub private_key: DecodedHex,
|
||||
|
||||
/// The expected public key.
|
||||
pub public_key: DecodedHex,
|
||||
}
|
||||
|
||||
/// A set of tests for a given seed.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct TestData {
|
||||
/// The seed to run the tests on.
|
||||
pub seed: DecodedHex,
|
||||
|
||||
/// The tests to run against the seed.
|
||||
pub tests: Vec<Test>,
|
||||
}
|
||||
|
||||
|
@ -24,7 +41,10 @@ const SECP256K1_512: &str = "fffcf9f6f3f0edeae7e4e1dedbd8d5d2cfccc9c6c3c0bdbab7b
|
|||
const ED25519_512: &str = "fffcf9f6f3f0edeae7e4e1dedbd8d5d2cfccc9c6c3c0bdbab7b4b1aeaba8a5a2\
|
||||
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)]
|
||||
pub fn test_data() -> Result<HashMap<String, Vec<TestData>>, Box<dyn std::error::Error>> {
|
||||
// Format:
|
||||
|
|
|
@ -1,8 +1,14 @@
|
|||
//! Zero-dependency hex encoding and decoding.
|
||||
|
||||
use std::fmt::Write;
|
||||
|
||||
/// The type could not be decoded.
|
||||
#[derive(Debug)]
|
||||
pub enum DecodeError {
|
||||
/// An invalid character was encountered.
|
||||
InvalidCharacter(u8),
|
||||
|
||||
/// The amount of characters was invalid. Hex strings must be in pairs of two.
|
||||
InvalidCharacterCount(usize),
|
||||
}
|
||||
|
||||
|
@ -21,6 +27,7 @@ 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 {
|
||||
let mut s = String::new();
|
||||
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> {
|
||||
let len = input.len();
|
||||
if len % 2 != 0 {
|
||||
|
|
Loading…
Reference in New Issue