Compare commits

...

4 Commits

Author SHA1 Message Date
Ryan Heywood 31e51f65a5
keyfork-mnemonic-util: optimize Default::default() for Wordlist 2024-02-18 18:01:51 -05:00
Ryan Heywood 883e0cdf65
keyfork-mnemonic-util: deprecate seed() in favor of generate_seed() 2024-02-18 18:01:18 -05:00
Ryan Heywood 9cb953414f
tests, examples: make clippy happy 2024-02-18 17:59:23 -05:00
Ryan Heywood ece9f435d2
Clarify documentation and add more examples
Note: The type signature of smex::encode and smex::decode has changed,
but will still accept values that were previously passed in.
2024-02-18 17:57:24 -05:00
15 changed files with 230 additions and 48 deletions

View File

@ -40,6 +40,25 @@
//! # keyforkd::test_util::Infallible::Ok(())
//! # }).unwrap();
//! ```
//!
//! In tests, the Keyforkd test_util module and TestPrivateKeys can be used.
//!
//! ```rust
//! use std::str::FromStr;
//!
//! use keyforkd_client::Client;
//! use keyfork_derive_util::DerivationPath;
//! use keyfork_derive_util::private_key::TestPrivateKey as PrivateKey;
//!
//! let seed = b"funky accordion noises";
//! keyforkd::test_util::run_test(seed, |socket_path| {
//! std::env::set_var("KEYFORKD_SOCKET_PATH", socket_path);
//! let derivation_path = DerivationPath::from_str("m/44'/0'").unwrap();
//! let mut client = Client::discover_socket().unwrap();
//! let xprv = client.request_xprv::<PrivateKey>(&derivation_path).unwrap();
//! keyforkd::test_util::Infallible::Ok(())
//! }).unwrap();
//! ```
pub use std::os::unix::net::UnixStream;
use std::{collections::HashMap, path::PathBuf};

View File

@ -18,7 +18,7 @@ fn secp256k1_test_suite() {
let seed = seed_test.seed;
run_test(&seed, move |socket_path| -> Result<(), Box<dyn std::error::Error + Send>> {
for test in seed_test.tests {
let socket = UnixStream::connect(&socket_path).unwrap();
let socket = UnixStream::connect(socket_path).unwrap();
let mut client = Client::new(socket);
let chain = DerivationPath::from_str(test.chain).unwrap();
let chain_len = chain.len();
@ -29,7 +29,7 @@ fn secp256k1_test_suite() {
// key using an XPrv, for all but the last XPrv, which is verified after this
for i in 2..chain_len {
// FIXME: Keyfork will only allow one request per session
let socket = UnixStream::connect(&socket_path).unwrap();
let socket = UnixStream::connect(socket_path).unwrap();
let mut client = Client::new(socket);
let path = DerivationPath::from_str(test.chain).unwrap();
let left_path = path.inner()[..i]
@ -40,7 +40,7 @@ fn secp256k1_test_suite() {
.fold(DerivationPath::default(), |p, i| p.chain_push(i.clone()));
let xprv = dbg!(client.request_xprv::<SecretKey>(&left_path)).unwrap();
let derived_xprv = xprv.derive_path(&right_path).unwrap();
let socket = UnixStream::connect(&socket_path).unwrap();
let socket = UnixStream::connect(socket_path).unwrap();
let mut client = Client::new(socket);
let keyforkd_xprv = client.request_xprv::<SecretKey>(&path).unwrap();
assert_eq!(
@ -73,7 +73,7 @@ fn ed25519_test_suite() {
let seed = seed_test.seed;
run_test(&seed, move |socket_path| {
for test in seed_test.tests {
let socket = UnixStream::connect(&socket_path).unwrap();
let socket = UnixStream::connect(socket_path).unwrap();
let mut client = Client::new(socket);
let chain = DerivationPath::from_str(test.chain).unwrap();
let chain_len = chain.len();

View File

@ -34,3 +34,15 @@ request, as well as its best-effort guess on what path is being derived (using
the `keyfork-derive-path-data` crate), to inform the user of what keys are
requested. Once the server sends the client the new extended private key, the
client can then choose to use the key as-is, or derive further keys.
## Testing
A Keyfork server can be automatically started by using [`test_util::run_test`].
The function accepts a closure, starting the server before the closure is run,
and closing the server after the closure has completed. This may be useful for
people writing software that interacts with the Keyfork server, such as a
deriver or a provisioner. A test seed must be provided, but can be any content.
The closure accepts one argument, the path of the UNIX socket from which the
server can be accessed.
Examples of the test utility can be seen in the `keyforkd-client` crate.

View File

@ -57,7 +57,7 @@ pub async fn start_and_run_server_on(
let service = ServiceBuilder::new()
.layer(middleware::BincodeLayer::new())
// TODO: passphrase support and/or store passphrase with mnemonic
.service(Keyforkd::new(mnemonic.seed(None)?));
.service(Keyforkd::new(mnemonic.generate_seed(None)));
let mut server = match UnixServer::bind(socket_path) {
Ok(s) => s,

View File

@ -25,14 +25,25 @@ pub struct InfallibleError {
/// ```
pub type Infallible<T> = std::result::Result<T, InfallibleError>;
/// Run a test making use of a Keyforkd server. The path to the socket of the Keyforkd server is
/// provided as the only argument to the closure. The closure is expected to return a Result; the
/// Error field of the Result may be an error returned by a test.
/// Run a test making use of a Keyforkd server. The test may use a seed (the first argument) from a
/// test suite, or (as shown in the example below) a simple seed may be used solely to ensure
/// the server is capable of being interacted with. The test is in the form of a closure, expected
/// to return a [`Result`] where success is a unit type (test passed) and the error is any error
/// that happened during the test (alternatively, a panic may be used, and will be returned as an
/// error).
///
/// # Panics
/// The function may panic if any errors arise while configuring and using the Tokio multithreaded
/// runtime.
///
/// The function is not expected to run in production; therefore, the function plays "fast and
/// loose" wih the usage of [`Result::expect`]. In normal usage, these should never be an issue.
/// # Examples
/// ```rust
/// use std::os::unix::net::UnixStream;
/// let seed = b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
/// keyforkd::test_util::run_test(seed.as_slice(), |path| {
/// UnixStream::connect(&path).map(|_| ())
/// }).unwrap();
/// ```
#[allow(clippy::missing_errors_doc)]
pub fn run_test<F, E>(seed: &[u8], closure: F) -> Result<(), E>
where

View File

@ -46,7 +46,7 @@ fn run() -> Result<(), Box<dyn std::error::Error>> {
let mut client = Client::discover_socket()?;
let request = DerivationRequest::new(algo, &path);
let response = client.request(&request.into())?;
println!("{}", smex::encode(&DerivationResponse::try_from(response)?.data));
println!("{}", smex::encode(DerivationResponse::try_from(response)?.data));
Ok(())
}

View File

@ -1,4 +1,4 @@
//! Creation of OpenPGP certificates from BIP-0032 derived data.
//! Creation of OpenPGP Transferable Secret Keys from BIP-0032 derived data.
use std::{
str::FromStr,
@ -74,7 +74,7 @@ pub fn derive(xprv: XPrv, keys: &[KeyFlags], userid: &UserID) -> Result<Cert> {
let expiration_date = match std::env::var("KEYFORK_OPENPGP_EXPIRE").as_mut() {
Ok(var) => {
let ch = var.pop();
match (ch, u64::from_str(&var)) {
match (ch, u64::from_str(var)) {
(Some(ch @ ('d' | 'm' | 'y')), Ok(expire)) => {
let multiplier = match ch {
'd' => 1,

View File

@ -209,7 +209,7 @@ impl DerivationRequest {
/// # }
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)?)
self.derive_with_master_seed(&mnemonic.generate_seed(None))
}
/// Derive an [`ExtendedPrivateKey`] using the given seed.

View File

@ -30,7 +30,7 @@ fn secp256k1() {
} = test;
// Tests for ExtendedPrivateKey
let varlen_seed = VariableLengthSeed::new(&seed);
let varlen_seed = VariableLengthSeed::new(seed);
let xkey = ExtendedPrivateKey::<SecretKey>::new(varlen_seed);
let derived_key = xkey.derive_path(&chain).unwrap();
assert_eq!(
@ -51,7 +51,7 @@ fn secp256k1() {
// Tests for DerivationRequest
let request = DerivationRequest::new(DerivationAlgorithm::Secp256k1, &chain);
let response = request.derive_with_master_seed(&seed).unwrap();
let response = request.derive_with_master_seed(seed).unwrap();
assert_eq!(&response.data, private_key.as_slice(), "test: {chain}");
}
}
@ -76,7 +76,7 @@ fn ed25519() {
} = test;
// Tests for ExtendedPrivateKey
let varlen_seed = VariableLengthSeed::new(&seed);
let varlen_seed = VariableLengthSeed::new(seed);
let xkey = ExtendedPrivateKey::<SigningKey>::new(varlen_seed);
let derived_key = xkey.derive_path(&chain).unwrap();
assert_eq!(
@ -97,7 +97,7 @@ fn ed25519() {
// Tests for DerivationRequest
let request = DerivationRequest::new(DerivationAlgorithm::Ed25519, &chain);
let response = request.derive_with_master_seed(&seed).unwrap();
let response = request.derive_with_master_seed(seed).unwrap();
assert_eq!(&response.data, private_key.as_slice(), "test: {chain}");
}
}

View File

@ -69,7 +69,7 @@ fn run() -> Result<()> {
let Some(line) = stdin().lines().next() else {
return Err(Error::Input.into());
};
smex::decode(&line?)?
smex::decode(line?)?
};
split(threshold, cert_list, &input, std::io::stdout())?;

View File

@ -57,10 +57,21 @@ fn ensure_offline() {
/// # std::env::set_var("SHOOT_SELF_IN_FOOT", "1");
/// keyfork_entropy::ensure_safe();
/// ```
///
/// When running on a system that's online, or running an outdated kernel:
///
/// ```rust,should_panic
/// # // NOTE: sometimes, the environment variable is set, for testing purposes. I'm not sure how
/// # // to un-set it. Set it to a sentinel value.
/// # std::env::set_var("SHOOT_SELF_IN_FOOT", "test-must-fail");
/// # std::env::set_var("INSECURE_HARDWARE_ALLOWED", "test-must-fail");
/// keyfork_entropy::ensure_safe();
/// ```
pub fn ensure_safe() {
if !std::env::vars()
.any(|(name, _)| name == "SHOOT_SELF_IN_FOOT" || name == "INSECURE_HARDWARE_ALLOWED")
{
if !std::env::vars().any(|(name, value)| {
(name == "SHOOT_SELF_IN_FOOT" || name == "INSECURE_HARDWARE_ALLOWED")
&& value != "test-must-fail"
}) {
ensure_safe_kernel_version();
ensure_offline();
}

View File

@ -16,7 +16,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
);
let entropy = keyfork_entropy::generate_entropy_of_size(bit_size / 8)?;
println!("{}", smex::encode(&entropy));
println!("{}", smex::encode(entropy));
Ok(())
}

View File

@ -66,6 +66,11 @@ pub(crate) fn hash(data: &[u8]) -> Vec<u8> {
/// # Errors
/// An error may be returned if the given `data` is more than [`u32::MAX`] bytes. This is a
/// constraint on a protocol level.
///
/// # Examples
/// ```rust
/// let data = keyfork_frame::try_encode(b"hello world!".as_slice()).unwrap();
/// ```
pub fn try_encode(data: &[u8]) -> Result<Vec<u8>, EncodeError> {
let mut output = vec![];
try_encode_to(data, &mut output)?;
@ -77,6 +82,12 @@ pub fn try_encode(data: &[u8]) -> Result<Vec<u8>, EncodeError> {
/// # Errors
/// An error may be returned if the givenu `data` is more than [`u32::MAX`] bytes, or if the writer
/// is unable to write data.
///
/// # Examples
/// ```rust
/// let mut output = vec![];
/// keyfork_frame::try_encode_to(b"hello world!".as_slice(), &mut output).unwrap();
/// ```
pub fn try_encode_to(data: &[u8], writable: &mut impl Write) -> Result<(), EncodeError> {
let hash = hash(data);
let len = hash.len() + data.len();
@ -107,18 +118,40 @@ pub(crate) fn verify_checksum(data: &[u8]) -> Result<&[u8], DecodeError> {
/// * The given `data` does not contain enough data to parse a length,
/// * The given `data` does not contain the given length's worth of data,
/// * The given `data` has a checksum that does not match what we build locally.
///
/// # Examples
/// ```rust
/// let input = b"hello world!";
/// let encoded = keyfork_frame::try_encode(input.as_slice()).unwrap();
/// let decoded = keyfork_frame::try_decode(&encoded).unwrap();
/// assert_eq!(input.as_slice(), decoded.as_slice());
/// ```
pub fn try_decode(data: &[u8]) -> Result<Vec<u8>, DecodeError> {
try_decode_from(&mut &data[..])
}
/// Read and decode a framed message into a `Vec<u8>`.
///
/// Note that unlike [`try_encode_to`], this method does not allow writing to an object
/// implementing Write. This is because the data must be stored entirely in memory to allow
/// verifying the data. The data is then returned using the same in-memory representation as is
/// used in memory, and a caller may then choose to use `writable.write_all()`.
///
/// # Errors
/// An error may be returned if:
/// * The given `data` does not contain enough data to parse a length,
/// * The given `data` does not contain the given length's worth of data,
/// * The given `data` has a checksum that does not match what we build locally.
/// * The source for the data returned an error.
///
/// # Examples
/// ```rust
/// let input = b"hello world!";
/// let mut encoded = vec![];
/// keyfork_frame::try_encode_to(input.as_slice(), &mut encoded).unwrap();
/// let decoded = keyfork_frame::try_decode_from(&mut &encoded[..]).unwrap();
/// assert_eq!(input.as_slice(), decoded.as_slice());
/// ```
pub fn try_decode_from(readable: &mut impl Read) -> Result<Vec<u8>, DecodeError> {
let mut bytes = 0u32.to_be_bytes();
readable.read_exact(&mut bytes)?;

View File

@ -1,6 +1,11 @@
//! 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, OnceLock},
};
use hmac::Hmac;
use pbkdf2::pbkdf2;
@ -43,10 +48,14 @@ impl Error for MnemonicGenerationError {}
#[derive(Debug, Clone)]
pub struct Wordlist(Vec<String>);
static ENGLISH: OnceLock<Wordlist> = OnceLock::new();
impl Default for Wordlist {
/// Returns the English wordlist in the Bitcoin BIP-0039 specification.
fn default() -> Self {
// TODO: English is the only supported language.
ENGLISH
.get_or_init(|| {
let wordlist_file = include_str!("data/wordlist.txt");
Wordlist(
wordlist_file
@ -56,6 +65,9 @@ impl Default for Wordlist {
.map(|x| x.trim().to_string())
.collect(),
)
.shrank()
})
.clone()
}
}
@ -66,6 +78,12 @@ impl Wordlist {
Arc::new(self)
}
/// Return a shrank version of the Wordlist
pub fn shrank(mut self) -> Self {
self.0.shrink_to_fit();
self
}
/// Determine whether the Wordlist contains a given word.
pub fn contains(&self, word: &str) -> bool {
self.0.iter().any(|w| w.as_str() == word)
@ -96,6 +114,14 @@ pub struct Mnemonic {
wordlist: Arc<Wordlist>,
}
impl PartialEq for Mnemonic {
fn eq(&self, other: &Self) -> bool {
self.entropy.eq(&other.entropy)
}
}
impl Eq for Mnemonic {}
impl Display for Mnemonic {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let bit_count = self.entropy.len() * 8;
@ -231,6 +257,13 @@ impl Mnemonic {
///
/// # Errors
/// An error may be returned if the entropy is not within the acceptable lengths.
///
/// # Examples
/// ```rust
/// use keyfork_mnemonic_util::Mnemonic;
/// let data = b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
/// let mnemonic = Mnemonic::from_entropy(data.as_slice(), Default::default()).unwrap();
/// ```
pub fn from_entropy(
bytes: &[u8],
wordlist: Arc<Wordlist>,
@ -248,11 +281,36 @@ impl Mnemonic {
Ok(unsafe { Self::from_raw_entropy(bytes, wordlist) })
}
/// Create a Mnemonic using an arbitrary length of given entropy. The length does not need to
/// conform to BIP-0039 standards.
///
/// # Safety
///
/// This function can potentially produce mnemonics that are not BIP-0039 compliant or can't
/// properly be encoded as a mnemonic. It is assumed the caller asserts the byte count is `% 4
/// == 0`.
/// == 0`. If the assumption is incorrect, code may panic.
///
/// # Examples
/// ```rust
/// use keyfork_mnemonic_util::Mnemonic;
/// let data = b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
/// let mnemonic = unsafe { Mnemonic::from_raw_entropy(data.as_slice(), Default::default()) };
/// let mnemonic_text = mnemonic.to_string();
/// ```
///
/// If given an invalid length, undefined behavior may follow, or code may panic.
///
/// ```rust,should_panic
/// use keyfork_mnemonic_util::Mnemonic;
/// use std::str::FromStr;
///
/// // NOTE: Data is of invalid length, 31
/// let data = b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
/// let mnemonic = unsafe { Mnemonic::from_raw_entropy(data.as_slice(), Default::default()) };
/// let mnemonic_text = mnemonic.to_string();
/// // NOTE: panic happens here
/// let new_mnemonic = Mnemonic::from_str(&mnemonic_text).unwrap();
/// ```
pub unsafe fn from_raw_entropy(bytes: &[u8], wordlist: Arc<Wordlist>) -> Mnemonic {
Mnemonic {
entropy: bytes.to_vec(),
@ -260,22 +318,22 @@ impl Mnemonic {
}
}
/// The internal representation of the decoded data.
/// A view to internal representation of the decoded data.
pub fn as_bytes(&self) -> &[u8] {
&self.entropy
}
/// The internal representation of the decoded data, as a [`Vec<u8>`].
/// A clone of the internal representation of the decoded data.
pub fn to_bytes(&self) -> Vec<u8> {
self.entropy.to_vec()
}
/// Drop self, returning the decoded data.
/// Conver the Mnemonic into the internal representation of the decoded data.
pub fn into_bytes(self) -> Vec<u8> {
self.entropy
}
/// Clone the existing entropy.
/// Clone the existing data.
#[deprecated = "Use as_bytes(), to_bytes(), or into_bytes() instead"]
pub fn entropy(&self) -> Vec<u8> {
self.entropy.clone()
@ -284,23 +342,34 @@ impl Mnemonic {
/// Create a BIP-0032 seed from the provided data and an optional passphrase.
///
/// # Errors
/// The method may return an error if the pbkdf2 function returns an invalid length, but this
/// case should not be reached.
/// The method should not return an error.
#[deprecated = "Use generate_seed() instead"]
pub fn seed<'a>(
&self,
passphrase: impl Into<Option<&'a str>>,
) -> Result<Vec<u8>, MnemonicGenerationError> {
Ok(self.generate_seed(passphrase))
}
/// Create a BIP-0032 seed from the provided data and an optional passphrase.
///
/// # Panics
/// The function may panic if the HmacSha512 function returns an error. The only error the
/// HmacSha512 function should return is an invalid length, which should not be possible.
///
pub fn generate_seed<'a>(&self, passphrase: impl Into<Option<&'a str>>) -> Vec<u8> {
let passphrase = passphrase.into();
let mut seed = [0u8; 64];
let mnemonic = self.to_string();
let salt = ["mnemonic", passphrase.unwrap_or("")].join("");
pbkdf2::<Hmac<Sha512>>(mnemonic.as_bytes(), salt.as_bytes(), 2048, &mut seed)
.map_err(|_| MnemonicGenerationError::InvalidPbkdf2Length)?;
Ok(seed.to_vec())
.expect("HmacSha512 InvalidLength should be infallible");
seed.to_vec()
}
/// Encode the mnemonic into a list of wordlist indexes.
/// Encode the mnemonic into a list of integers 11 bits in length, matching the length of a
/// BIP-0039 wordlist.
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];
@ -384,13 +453,13 @@ mod tests {
let my_mnemonic = super::Mnemonic::from_entropy(&entropy[..256 / 8], wordlist).unwrap();
let their_mnemonic = bip39::Mnemonic::from_entropy(&entropy[..256 / 8]).unwrap();
assert_eq!(my_mnemonic.to_string(), their_mnemonic.to_string());
assert_eq!(my_mnemonic.seed(None).unwrap(), their_mnemonic.to_seed(""));
assert_eq!(my_mnemonic.generate_seed(None), their_mnemonic.to_seed(""));
assert_eq!(
my_mnemonic.seed("testing").unwrap(),
my_mnemonic.generate_seed("testing"),
their_mnemonic.to_seed("testing")
);
assert_ne!(
my_mnemonic.seed("test1").unwrap(),
my_mnemonic.generate_seed("test1"),
their_mnemonic.to_seed("test2")
);
}

View File

@ -28,7 +28,15 @@ impl std::fmt::Display for DecodeError {
impl std::error::Error for DecodeError {}
/// Encode a given input as a hex string.
pub fn encode(input: &[u8]) -> String {
///
/// # Examples
/// ```rust
/// let data = b"hello world!";
/// let result = smex::encode(&data);
/// assert_eq!(result, "68656c6c6f20776f726c6421");
/// ```
pub fn encode(input: impl AsRef<[u8]>) -> String {
let input = input.as_ref();
let mut s = String::new();
for byte in input {
write!(s, "{byte:02x}").unwrap();
@ -50,7 +58,26 @@ fn val(c: u8) -> Result<u8, DecodeError> {
/// # 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> {
///
/// # Examples
/// ```rust
/// let data = b"hello world!";
/// let encoded = smex::encode(&data);
/// let decoded = smex::decode(&encoded).unwrap();
/// assert_eq!(data.as_slice(), decoded.as_slice());
/// ```
///
/// The function may return an error if the given input is not valid hex.
///
/// ```rust,should_panic
/// let data = b"hello world!";
/// let mut encoded = smex::encode(&data);
/// encoded.push('G');
/// let decoded = smex::decode(&encoded).unwrap();
/// assert_eq!(data.as_slice(), decoded.as_slice());
/// ```
pub fn decode(input: impl AsRef<str>) -> Result<Vec<u8>, DecodeError> {
let input = input.as_ref();
let len = input.len();
if len % 2 != 0 {
return Err(DecodeError::InvalidCharacterCount(len));