Project refactoring

* keyfork-seed has become keyfork-derive-key
* Create keyfork-entropy as a way to pull entropy from system
* Fix tests in keyfork-derive-util and keyfork-frame
* Remove keyfork-mnemonic-generate
* Add keyfork-mnemonic-from-seed
* Refactor keyfork to only include highest level utilities
* Add smex (small hex)
This commit is contained in:
Ryan Heywood 2023-09-21 17:30:48 -05:00
parent 7e8702a150
commit d059c21b7d
Signed by: ryan
GPG Key ID: 8E401478A3FBEF72
29 changed files with 394 additions and 237 deletions

52
Cargo.lock generated
View File

@ -515,11 +515,29 @@ name = "keyfork"
version = "0.1.0"
dependencies = [
"clap",
"keyfork-mnemonic-generate",
"keyfork-seed",
"keyfork-derive-key",
"keyfork-entropy",
"keyfork-mnemonic-from-seed",
"smex",
"thiserror",
]
[[package]]
name = "keyfork-derive-key"
version = "0.1.0"
dependencies = [
"bincode",
"clap",
"ed25519-dalek",
"hex-literal",
"keyfork-derive-util",
"keyfork-frame",
"keyforkd",
"tempdir",
"thiserror",
"tokio",
]
[[package]]
name = "keyfork-derive-util"
version = "0.1.0"
@ -536,6 +554,13 @@ dependencies = [
"thiserror",
]
[[package]]
name = "keyfork-entropy"
version = "0.1.0"
dependencies = [
"smex",
]
[[package]]
name = "keyfork-frame"
version = "0.1.0"
@ -548,10 +573,11 @@ dependencies = [
]
[[package]]
name = "keyfork-mnemonic-generate"
name = "keyfork-mnemonic-from-seed"
version = "0.2.0"
dependencies = [
"keyfork-mnemonic-util",
"smex",
]
[[package]]
@ -564,22 +590,6 @@ dependencies = [
"sha2",
]
[[package]]
name = "keyfork-seed"
version = "0.1.0"
dependencies = [
"bincode",
"clap",
"ed25519-dalek",
"hex-literal",
"keyfork-derive-util",
"keyfork-frame",
"keyforkd",
"tempdir",
"thiserror",
"tokio",
]
[[package]]
name = "keyforkd"
version = "0.1.0"
@ -994,6 +1004,10 @@ version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62bb4feee49fdd9f707ef802e22365a35de4b7b299de4763d44bfea899442ff9"
[[package]]
name = "smex"
version = "0.1.0"
[[package]]
name = "socket2"
version = "0.5.3"

View File

@ -3,10 +3,12 @@
resolver = "2"
members = [
"keyfork",
"keyfork-mnemonic-generate",
"keyfork-mnemonic-util",
"keyfork-derive-util",
"keyfork-seed",
"keyfork-derive-key",
"keyfork-entropy",
"keyfork-frame",
"keyfork-mnemonic-from-seed",
"keyfork-mnemonic-util",
"keyforkd",
"keyfork-frame"
"smex",
]

View File

@ -8,6 +8,21 @@ This toolchain uses a bip32 seed loaded into an agent to generate deterministic
and unique keypairs. This ensures only the agent has control over the mnemonic
itself, and other components can simply request deterministic data.
## Dependency Policy
Dependencies must not be added to core utilities such as seed generation and
path derivation without a _really_ good reason we can't implement it ourselves,
such as cryptography libraries. For instance, `keyfork-derive-util` _only_
utilizes cryptography libraries, `serde`, and `thiserror`, with the latter two
being audited dependencies. Utilities such as forklets (applications that
use derived data, such as an OpenPGP keychain generator) and the kitchen-sink
`keyfork` utility may pull in additional dependencies _as needed_, but should
strive to use the standard library as much as possible. To avoid code reuse,
additional crates (such as the `smex` crate) may be used to share functionality
across several crates.
---
Note: The following document is all proposed, and not yet implemented.
## Features

View File

@ -1,5 +1,5 @@
[package]
name = "keyfork-seed"
name = "keyfork-derive-key"
version = "0.1.0"
edition = "2021"

View File

@ -1,6 +1,6 @@
use crate::client::Client;
use clap::Parser;
use keyfork_derive_util::{request::*, DerivationPath};
use crate::client::Client;
#[derive(Parser, Clone, Debug)]
pub struct Command {

View File

@ -1,7 +1,7 @@
use keyfork_frame::*;
use crate::Result;
use std::os::unix::net::UnixStream;
use keyfork_derive_util::request::*;
use keyfork_frame::*;
use std::os::unix::net::UnixStream;
#[derive(Debug)]
pub struct Client {

View File

@ -1,9 +1,9 @@
use keyfork_frame::{DecodeError, EncodeError};
use std::path::PathBuf;
use keyfork_frame::{EncodeError, DecodeError};
pub mod cli;
pub mod socket;
pub mod client;
pub mod socket;
pub use client::Client;

View File

@ -3,11 +3,10 @@ use clap::Parser;
#[cfg(test)]
mod tests;
use keyfork_seed::*;
use keyfork_derive_key::*;
fn main() -> Result<()> {
let args = cli::Command::parse();
let response = args.handle()?;
dbg!(&response);
args.handle()?;
Ok(())
}

View File

@ -1,6 +1,6 @@
use crate::client::Client;
use hex_literal::hex;
use keyfork_derive_util::{request::*, DerivationPath, DerivationIndex, ExtendedPrivateKey};
use keyfork_derive_util::{request::*, DerivationIndex, DerivationPath, ExtendedPrivateKey};
use std::sync::mpsc::channel;
use std::{os::unix::net::UnixStream, str::FromStr};
use tempdir::TempDir;
@ -82,10 +82,12 @@ fn misc_multi_requests() {
&response.data,
response.depth,
response.chain_code,
).unwrap();
)
.unwrap();
for i in 0..255 {
key.derive_child(&DerivationIndex::new(i, true).unwrap()).unwrap();
key.derive_child(&DerivationIndex::new(i, true).unwrap())
.unwrap();
}
handle.abort();
}

View File

@ -121,6 +121,8 @@ fn panics_at_depth() {
let seed = hex!("000102030405060708090a0b0c0d0e0f");
let mut xkey = ExtendedPrivateKey::<SigningKey>::new(seed).unwrap();
for i in 0..u32::from(u8::MAX) + 1 {
xkey = xkey.derive_child(&DerivationIndex::new(i, true).unwrap()).unwrap();
xkey = xkey
.derive_child(&DerivationIndex::new(i, true).unwrap())
.unwrap();
}
}

View File

@ -0,0 +1,9 @@
[package]
name = "keyfork-entropy"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
smex = { version = "0.1.0", path = "../smex" }

View File

@ -0,0 +1,65 @@
use std::{
fs::{read_dir, read_to_string},
io::Read,
};
static WARNING_LINKS: [&str; 1] =
["https://lore.kernel.org/lkml/20211223141113.1240679-2-Jason@zx2c4.com/"];
fn ensure_safe_kernel_version() {
let kernel_version = read_to_string("/proc/version").expect("/proc/version");
let v = kernel_version
.split(' ')
.nth(2)
.expect("Unable to parse kernel version")
.split('.')
.take(2)
.map(str::parse)
.map(|x| x.expect("Unable to parse kernel version number"))
.collect::<Vec<u32>>();
let [major, minor, ..] = v.as_slice() else {
panic!("Unable to determine major and minor: {kernel_version}");
};
assert!(
[major, minor] > [&5, &4],
"kernel can't generate clean entropy: {}",
WARNING_LINKS[0]
);
}
fn ensure_offline() {
let paths = read_dir("/sys/class/net").expect("Unable to read network interfaces");
for entry in paths {
let mut path = entry.expect("Unable to read directory entry").path();
if path
.as_os_str()
.to_str()
.expect("Unable to decode UTF-8 filepath")
.split('/')
.last()
.unwrap()
== "lo"
{
continue;
}
path.push("operstate");
let isup = read_to_string(&path).expect("Unable to read operstate of network interfaces");
assert_ne!(isup.trim(), "up", "No network interfaces should be up");
}
}
pub fn ensure_safe() {
if !std::env::vars()
.any(|(name, _)| name == "SHOOT_SELF_IN_FOOT" || name == "INSECURE_HARDWARE_ALLOWED")
{
ensure_safe_kernel_version();
ensure_offline();
}
}
pub fn generate_entropy_of_size(byte_count: usize) -> Result<Vec<u8>, std::io::Error> {
let mut vec = vec![0u8; byte_count];
let mut entropy_file = std::fs::File::open("/dev/urandom")?;
entropy_file.read_exact(&mut vec[..])?;
Ok(vec)
}

View File

@ -0,0 +1,21 @@
fn main() -> Result<(), Box<dyn std::error::Error>> {
let bit_size: usize = std::env::args()
.nth(1)
.unwrap_or(String::from("256"))
.parse()
.expect("Expected integer bit size");
assert!(
bit_size % 8 == 0,
"Bit size must be divisible by 8, got: {bit_size}"
);
assert!(
bit_size <= 256,
"Maximum supported bit size is 256, got: {bit_size}"
);
keyfork_entropy::ensure_safe();
let entropy = keyfork_entropy::generate_entropy_of_size(bit_size / 8)?;
println!("{}", smex::encode(&entropy));
Ok(())
}

View File

@ -121,6 +121,8 @@ pub fn try_decode_from(readable: &mut impl Read) -> Result<Vec<u8>, DecodeError>
mod tests {
use super::{try_decode, try_encode, DecodeError};
const LEN_SIZE: usize = (u32::BITS / 8) as usize;
#[test]
fn stable_interface() {
let data = (0..255).collect::<Vec<u8>>();
@ -153,7 +155,7 @@ mod tests {
assert!(error.is_err());
// Data includes length and checksum
let error = try_decode(&encoded[..super::LEN_SIZE + 256 / 8]);
let error = try_decode(&encoded[..LEN_SIZE + 256 / 8]);
assert!(error.is_err());
// Data only includes length
@ -166,8 +168,8 @@ mod tests {
fn error_on_invalid_checksum() {
let data = (0..255).collect::<Vec<u8>>();
let mut encoded = try_encode(&data[..]).unwrap();
assert_ne!(encoded[super::LEN_SIZE + 1], 0);
encoded[super::LEN_SIZE + 1] = 0;
assert_ne!(encoded[LEN_SIZE + 1], 0);
encoded[LEN_SIZE + 1] = 0;
let error = try_decode(&data[..]);
assert!(error.is_err());

View File

@ -1,7 +1,7 @@
[package]
name = "keyfork-mnemonic-generate"
version = "0.2.0"
description = "A tool to generate BIP-0039 mnemonics."
name = "keyfork-mnemonic-from-seed"
version = "0.1.0"
description = "A tool to format BIP-0039 mnemonics from hex data."
license = "GPL-3.0"
repository = "https://git.distrust.co/public/keyfork"
edition = "2021"
@ -10,3 +10,4 @@ edition = "2021"
[dependencies]
keyfork-mnemonic-util = { version = "0.1.0", path = "../keyfork-mnemonic-util", registry = "distrust" }
smex = { version = "0.1.0", path = "../smex" }

View File

@ -0,0 +1,49 @@
use keyfork_mnemonic_util::{Mnemonic, MnemonicGenerationError, Wordlist};
pub fn generate_mnemonic(entropy: &[u8]) -> Result<Mnemonic, MnemonicGenerationError> {
let wordlist = Wordlist::default().arc();
Mnemonic::from_entropy(entropy, wordlist)
}
#[cfg(test)]
mod tests {
use keyfork_mnemonic_util::{Mnemonic, Wordlist};
use std::{collections::HashSet, io::Read};
#[test]
fn count_to_get_duplicate_words() {
let tests = 100_000;
let mut count = 0.;
let entropy = &mut [0u8; 256 / 8];
let wordlist = Wordlist::default().arc();
let mut random = std::fs::File::open("/dev/urandom").unwrap();
let mut hs = HashSet::<usize>::with_capacity(24);
for _ in 0..tests {
random.read_exact(&mut entropy[..]).unwrap();
let mnemonic = Mnemonic::from_entropy(&entropy[..256 / 8], wordlist.clone()).unwrap();
let (words, _) = mnemonic.into_inner();
hs.clear();
hs.extend(words);
if hs.len() != 24 {
count += 1.;
}
}
// NOTE: Birthday problem math is: 0.126532
// Set values to (about) 1 below, 1 above
// Source: https://en.wikipedia.org/wiki/Birthday_problem
let min = 11.5;
let max = 13.5;
assert!(
count > f64::from(tests) * min / 100.,
"{count} probability should be more than {min}%: {}",
count / f64::from(tests)
);
assert!(
count < f64::from(tests) * max / 100.,
"{count} probability should be more than {max}%: {}",
count / f64::from(tests)
);
}
}

View File

@ -0,0 +1,14 @@
use keyfork_mnemonic_from_seed::*;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let input = std::io::stdin();
let mut line = String::new();
input.read_line(&mut line)?;
let decoded = smex::decode(line.trim())?;
let mnemonic = generate_mnemonic(&decoded)?;
println!("{mnemonic}");
Ok(())
}

View File

@ -1,140 +0,0 @@
use std::{
env::vars,
error::Error,
fs::{read_dir, read_to_string, File},
io::Read,
};
use keyfork_mnemonic_util::{Mnemonic, Wordlist};
pub type Result<T, E = Box<dyn Error>> = std::result::Result<T, E>;
/// Usage: keyfork-mnemonic-generate [bitsize: 128 or 256]
/// CHECKS:
/// * If the system is online
/// * If a kernel is running post-BLAKE2
///
/// TODO:
/// * --features kitchen-sink: load username; system time's most random, precise bits; hostname;
/// kernel version; other env specific shit into a CSPRNG
pub struct Entropy(File);
/// An entropy source
impl Entropy {
pub fn new() -> Result<Self> {
let file = File::open("/dev/random")?;
Ok(Self(file))
}
pub fn read_into(&mut self, bytes: &mut [u8]) -> Result<()> {
self.0.read_exact(bytes)?;
Ok(())
}
}
static WARNING_LINKS: [&str; 1] =
["https://lore.kernel.org/lkml/20211223141113.1240679-2-Jason@zx2c4.com/"];
fn ensure_safe_kernel_version() {
let kernel_version = read_to_string("/proc/version").expect("/proc/version");
let v = kernel_version
.split(' ')
.nth(2)
.expect("Unable to parse kernel version")
.split('.')
.take(2)
.map(str::parse)
.map(|x| x.expect("Unable to parse kernel version number"))
.collect::<Vec<u32>>();
let [major, minor, ..] = v.as_slice() else {
panic!("Unable to determine major and minor: {kernel_version}");
};
assert!(
[major, minor] > [&5, &4],
"kernel can't generate clean entropy: {}",
WARNING_LINKS[0]
);
}
fn ensure_offline() {
let paths = read_dir("/sys/class/net").expect("Unable to read network interfaces");
for entry in paths {
let mut path = entry.expect("Unable to read directory entry").path();
if path
.as_os_str()
.to_str()
.expect("Unable to decode UTF-8 filepath")
.split('/')
.last()
.unwrap()
== "lo"
{
continue;
}
path.push("operstate");
let isup = read_to_string(&path).expect("Unable to read operstate of network interfaces");
assert_ne!(isup.trim(), "up", "No network interfaces should be up");
}
}
pub fn generate_mnemonic(bit_size: usize) -> Result<Mnemonic> {
if !vars()
.any(|(name, _)| name == "SHOOT_SELF_IN_FOOT" || name == "INSECURE_HARDWARE_ALLOWED")
{
ensure_safe_kernel_version();
ensure_offline();
}
let mut rng = Entropy::new()?;
let entropy = &mut [0u8; 256 / 8];
rng.read_into(&mut entropy[..])?;
let wordlist = Wordlist::default().arc();
Mnemonic::from_entropy(&entropy[..bit_size / 8], wordlist).map_err(From::from)
}
#[cfg(test)]
mod tests {
use keyfork_mnemonic_util::{Mnemonic, Wordlist};
use std::collections::HashSet;
use super::*;
#[test]
fn count_to_get_duplicate_words() {
let tests = 100_000;
let mut count = 0.;
let entropy = &mut [0u8; 256 / 8];
let wordlist = Wordlist::default().arc();
let mut rng = Entropy::new().unwrap();
let mut hs = HashSet::<usize>::with_capacity(24);
for _ in 0..tests {
rng.read_into(&mut entropy[..]).unwrap();
let mnemonic = Mnemonic::from_entropy(&entropy[..256 / 8], wordlist.clone()).unwrap();
let (words, _) = mnemonic.into_inner();
hs.clear();
hs.extend(words);
if hs.len() != 24 {
count += 1.;
}
}
// NOTE: Birthday problem math is: 0.126532
// Set values to (about) 1 below, 1 above
// Source: https://en.wikipedia.org/wiki/Birthday_problem
let min = 11.5;
let max = 13.5;
assert!(
count > f64::from(tests) * min / 100.,
"{count} probability should be more than {min}%: {}",
count / f64::from(tests)
);
assert!(
count < f64::from(tests) * max / 100.,
"{count} probability should be more than {max}%: {}",
count / f64::from(tests)
);
}
}

View File

@ -1,20 +0,0 @@
use std::env::args;
use keyfork_mnemonic_generate::*;
fn main() -> Result<()> {
let bit_size: usize = args()
.nth(1)
.unwrap_or(String::from("256"))
.parse()
.expect("Expected integer bit size");
assert!(
bit_size == 128 || bit_size == 256,
"Only 12 or 24 word mnemonics are supported"
);
let mnemonic = generate_mnemonic(bit_size)?;
println!("{mnemonic}");
Ok(())
}

View File

@ -7,6 +7,8 @@ edition = "2021"
[dependencies]
clap = { version = "4.4.2", features = ["derive", "env"] }
keyfork-mnemonic-generate = { version = "0.2.0", path = "../keyfork-mnemonic-generate" }
keyfork-seed = { version = "0.1.0", path = "../keyfork-seed" }
keyfork-mnemonic-from-seed = { version = "0.2.0", path = "../keyfork-mnemonic-from-seed" }
keyfork-derive-key = { version = "0.1.0", path = "../keyfork-derive-key" }
thiserror = "1.0.48"
smex = { version = "0.1.0", path = "../smex" }
keyfork-entropy = { version = "0.1.0", path = "../keyfork-entropy" }

View File

@ -1,46 +1,117 @@
use super::{Keyfork, KeyforkCommands};
use clap::{Parser, Subcommand};
use keyfork_mnemonic_generate::generate_mnemonic;
use super::Keyfork;
use clap::{Parser, Subcommand, ValueEnum};
use std::fmt::Display;
#[derive(Clone, Debug)]
pub enum EntropySize {
#[derive(Clone, Debug, Default)]
pub enum SeedSize {
Bits128,
#[default]
Bits256,
}
#[derive(thiserror::Error, Debug, Clone)]
pub enum EntropySizeError {
pub enum SeedSizeError {
#[error("Expected one of 128, 256")]
InvalidChoice,
}
impl std::str::FromStr for EntropySize {
type Err = EntropySizeError;
impl Display for SeedSize {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
SeedSize::Bits128 => write!(f, "128"),
SeedSize::Bits256 => write!(f, "256"),
}
}
}
impl std::str::FromStr for SeedSize {
type Err = SeedSizeError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(match s {
"128" => EntropySize::Bits128,
"256" => EntropySize::Bits256,
_ => return Err(EntropySizeError::InvalidChoice),
"128" => SeedSize::Bits128,
"256" => SeedSize::Bits256,
_ => return Err(SeedSizeError::InvalidChoice),
})
}
}
impl From<&EntropySize> for usize {
fn from(value: &EntropySize) -> Self {
impl From<&SeedSize> for usize {
fn from(value: &SeedSize) -> Self {
match value {
EntropySize::Bits128 => 128,
EntropySize::Bits256 => 256,
SeedSize::Bits128 => 128,
SeedSize::Bits256 => 256,
}
}
}
#[derive(Clone, Debug, thiserror::Error)]
pub enum MnemonicSeedSourceParseError {
#[error("Expected one of system, playing, tarot, dice")]
InvalidChoice,
}
#[derive(Clone, Debug, Default, ValueEnum)]
pub enum MnemonicSeedSource {
/// System entropy
#[default]
System,
/// Playing cards
Playing,
/// Tarot cards
Tarot,
/// Dice
Dice,
}
impl std::str::FromStr for MnemonicSeedSource {
type Err = MnemonicSeedSourceParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(match s {
"system" => Self::System,
"playing" => Self::Playing,
"tarot" => Self::Tarot,
"dice" => Self::Dice,
_ => return Err(Self::Err::InvalidChoice),
})
}
}
impl MnemonicSeedSource {
pub fn handle(&self, size: &SeedSize) -> Result<String, Box<dyn std::error::Error>> {
let size = match size {
SeedSize::Bits128 => 128,
SeedSize::Bits256 => 256,
};
let seed = match self {
MnemonicSeedSource::System => {
keyfork_entropy::ensure_safe();
keyfork_entropy::generate_entropy_of_size(size / 8)?
}
MnemonicSeedSource::Playing => todo!(),
MnemonicSeedSource::Tarot => todo!(),
MnemonicSeedSource::Dice => todo!(),
};
let mnemonic = keyfork_mnemonic_from_seed::generate_mnemonic(&seed)?;
Ok(mnemonic.to_string())
}
}
#[derive(Subcommand, Clone, Debug)]
pub enum MnemonicSubcommands {
/// Generate a mnemonic using OS entropy.
/// Generate a mnemonic using a given entropy source.
Generate {
/// The size in bits to generate entropy for.
entropy: EntropySize,
/// The source from where a seed is created.
#[arg(long, value_enum, default_value_t = Default::default())]
source: MnemonicSeedSource,
/// The size of the mnemonic, in bits.
#[arg(long, default_value_t = Default::default())]
size: SeedSize,
},
}
@ -48,15 +119,10 @@ impl MnemonicSubcommands {
pub fn handle(
&self,
_m: &Mnemonic,
_p: &KeyforkCommands,
_keyfork: &Keyfork,
) -> Result<(), Box<dyn std::error::Error>> {
) -> Result<String, Box<dyn std::error::Error>> {
match self {
MnemonicSubcommands::Generate { entropy } => {
let mnemonic = generate_mnemonic(usize::from(entropy))?;
println!("{mnemonic}");
Ok(())
}
MnemonicSubcommands::Generate { source, size } => source.handle(size),
}
}
}

View File

@ -15,9 +15,6 @@ pub struct Keyfork {
pub enum KeyforkCommands {
/// Mnemonic generation and persistence utilities.
Mnemonic(mnemonic::Mnemonic),
/// Seeded data generation utility.
Seed(keyfork_seed::cli::Command),
/// Keyforkd background daemon to manage seed creation.
Daemon,
@ -27,11 +24,8 @@ impl KeyforkCommands {
pub fn handle(&self, keyfork: &Keyfork) -> Result<(), Box<dyn std::error::Error>> {
match self {
KeyforkCommands::Mnemonic(m) => {
m.command.handle(m, self, keyfork)?;
}
KeyforkCommands::Seed(s) => {
let response = s.handle()?;
println!("{response:?}");
let response = m.command.handle(m, keyfork)?;
println!("{response}");
}
KeyforkCommands::Daemon => {
todo!()

8
smex/Cargo.toml Normal file
View File

@ -0,0 +1,8 @@
[package]
name = "smex"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]

52
smex/src/lib.rs Normal file
View File

@ -0,0 +1,52 @@
use std::fmt::Write;
#[derive(Debug)]
pub enum DecodeError {
InvalidCharacter(u8),
InvalidCharacterCount(usize),
}
impl std::fmt::Display for DecodeError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::InvalidCharacter(c) => {
write!(f, "Invalid character: {c} not in [0123456789ABCDEF]")
}
Self::InvalidCharacterCount(n) => {
write!(f, "Invalid character count: {n} % 2 != 0")
}
}
}
}
impl std::error::Error for DecodeError {}
pub fn encode(input: &[u8]) -> String {
let mut s = String::new();
for byte in input {
write!(s, "{byte:02x}").unwrap();
}
s
}
fn val(c: u8) -> Result<u8, DecodeError> {
match c {
b'A'..=b'F' => Ok(c - b'A' + 10),
b'a'..=b'f' => Ok(c - b'a' + 10),
b'0'..=b'9' => Ok(c - b'0'),
_ => Err(DecodeError::InvalidCharacter(c)),
}
}
pub fn decode(input: &str) -> Result<Vec<u8>, DecodeError> {
let len = input.len();
if len % 2 != 0 {
return Err(DecodeError::InvalidCharacterCount(len));
}
input
.as_bytes()
.chunks_exact(2)
.map(|pair| Ok(val(pair[0])? << 4 | val(pair[1])?))
.collect()
}