Compare commits
4 Commits
33405ee4fc
...
31e51f65a5
Author | SHA1 | Date |
---|---|---|
Ryan Heywood | 31e51f65a5 | |
Ryan Heywood | 883e0cdf65 | |
Ryan Heywood | 9cb953414f | |
Ryan Heywood | ece9f435d2 |
|
@ -40,6 +40,25 @@
|
||||||
//! # keyforkd::test_util::Infallible::Ok(())
|
//! # keyforkd::test_util::Infallible::Ok(())
|
||||||
//! # }).unwrap();
|
//! # }).unwrap();
|
||||||
//! ```
|
//! ```
|
||||||
|
//!
|
||||||
|
//! In tests, the Keyforkd test_util module and TestPrivateKeys can be used.
|
||||||
|
//!
|
||||||
|
//! ```rust
|
||||||
|
//! use std::str::FromStr;
|
||||||
|
//!
|
||||||
|
//! use keyforkd_client::Client;
|
||||||
|
//! use keyfork_derive_util::DerivationPath;
|
||||||
|
//! use keyfork_derive_util::private_key::TestPrivateKey as PrivateKey;
|
||||||
|
//!
|
||||||
|
//! let seed = b"funky accordion noises";
|
||||||
|
//! keyforkd::test_util::run_test(seed, |socket_path| {
|
||||||
|
//! std::env::set_var("KEYFORKD_SOCKET_PATH", socket_path);
|
||||||
|
//! let derivation_path = DerivationPath::from_str("m/44'/0'").unwrap();
|
||||||
|
//! let mut client = Client::discover_socket().unwrap();
|
||||||
|
//! let xprv = client.request_xprv::<PrivateKey>(&derivation_path).unwrap();
|
||||||
|
//! keyforkd::test_util::Infallible::Ok(())
|
||||||
|
//! }).unwrap();
|
||||||
|
//! ```
|
||||||
|
|
||||||
pub use std::os::unix::net::UnixStream;
|
pub use std::os::unix::net::UnixStream;
|
||||||
use std::{collections::HashMap, path::PathBuf};
|
use std::{collections::HashMap, path::PathBuf};
|
||||||
|
|
|
@ -18,7 +18,7 @@ fn secp256k1_test_suite() {
|
||||||
let seed = seed_test.seed;
|
let seed = seed_test.seed;
|
||||||
run_test(&seed, move |socket_path| -> Result<(), Box<dyn std::error::Error + Send>> {
|
run_test(&seed, move |socket_path| -> Result<(), Box<dyn std::error::Error + Send>> {
|
||||||
for test in seed_test.tests {
|
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 mut client = Client::new(socket);
|
||||||
let chain = DerivationPath::from_str(test.chain).unwrap();
|
let chain = DerivationPath::from_str(test.chain).unwrap();
|
||||||
let chain_len = chain.len();
|
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
|
// key using an XPrv, for all but the last XPrv, which is verified after this
|
||||||
for i in 2..chain_len {
|
for i in 2..chain_len {
|
||||||
// FIXME: Keyfork will only allow one request per session
|
// 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 mut client = Client::new(socket);
|
||||||
let path = DerivationPath::from_str(test.chain).unwrap();
|
let path = DerivationPath::from_str(test.chain).unwrap();
|
||||||
let left_path = path.inner()[..i]
|
let left_path = path.inner()[..i]
|
||||||
|
@ -40,7 +40,7 @@ fn secp256k1_test_suite() {
|
||||||
.fold(DerivationPath::default(), |p, i| p.chain_push(i.clone()));
|
.fold(DerivationPath::default(), |p, i| p.chain_push(i.clone()));
|
||||||
let xprv = dbg!(client.request_xprv::<SecretKey>(&left_path)).unwrap();
|
let xprv = dbg!(client.request_xprv::<SecretKey>(&left_path)).unwrap();
|
||||||
let derived_xprv = xprv.derive_path(&right_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 mut client = Client::new(socket);
|
||||||
let keyforkd_xprv = client.request_xprv::<SecretKey>(&path).unwrap();
|
let keyforkd_xprv = client.request_xprv::<SecretKey>(&path).unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
@ -73,7 +73,7 @@ fn ed25519_test_suite() {
|
||||||
let seed = seed_test.seed;
|
let seed = seed_test.seed;
|
||||||
run_test(&seed, move |socket_path| {
|
run_test(&seed, move |socket_path| {
|
||||||
for test in seed_test.tests {
|
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 mut client = Client::new(socket);
|
||||||
let chain = DerivationPath::from_str(test.chain).unwrap();
|
let chain = DerivationPath::from_str(test.chain).unwrap();
|
||||||
let chain_len = chain.len();
|
let chain_len = chain.len();
|
||||||
|
|
|
@ -34,3 +34,15 @@ request, as well as its best-effort guess on what path is being derived (using
|
||||||
the `keyfork-derive-path-data` crate), to inform the user of what keys are
|
the `keyfork-derive-path-data` crate), to inform the user of what keys are
|
||||||
requested. Once the server sends the client the new extended private key, the
|
requested. Once the server sends the client the new extended private key, the
|
||||||
client can then choose to use the key as-is, or derive further keys.
|
client can then choose to use the key as-is, or derive further keys.
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
A Keyfork server can be automatically started by using [`test_util::run_test`].
|
||||||
|
The function accepts a closure, starting the server before the closure is run,
|
||||||
|
and closing the server after the closure has completed. This may be useful for
|
||||||
|
people writing software that interacts with the Keyfork server, such as a
|
||||||
|
deriver or a provisioner. A test seed must be provided, but can be any content.
|
||||||
|
The closure accepts one argument, the path of the UNIX socket from which the
|
||||||
|
server can be accessed.
|
||||||
|
|
||||||
|
Examples of the test utility can be seen in the `keyforkd-client` crate.
|
||||||
|
|
|
@ -57,7 +57,7 @@ pub async fn start_and_run_server_on(
|
||||||
let service = ServiceBuilder::new()
|
let service = ServiceBuilder::new()
|
||||||
.layer(middleware::BincodeLayer::new())
|
.layer(middleware::BincodeLayer::new())
|
||||||
// TODO: passphrase support and/or store passphrase with mnemonic
|
// 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) {
|
let mut server = match UnixServer::bind(socket_path) {
|
||||||
Ok(s) => s,
|
Ok(s) => s,
|
||||||
|
|
|
@ -25,14 +25,25 @@ pub struct InfallibleError {
|
||||||
/// ```
|
/// ```
|
||||||
pub type Infallible<T> = std::result::Result<T, InfallibleError>;
|
pub type Infallible<T> = std::result::Result<T, InfallibleError>;
|
||||||
|
|
||||||
/// Run a test making use of a Keyforkd server. The path to the socket of the Keyforkd server is
|
/// Run a test making use of a Keyforkd server. The test may use a seed (the first argument) from a
|
||||||
/// provided as the only argument to the closure. The closure is expected to return a Result; the
|
/// test suite, or (as shown in the example below) a simple seed may be used solely to ensure
|
||||||
/// Error field of the Result may be an error returned by a test.
|
/// the server is capable of being interacted with. The test is in the form of a closure, expected
|
||||||
|
/// to return a [`Result`] where success is a unit type (test passed) and the error is any error
|
||||||
|
/// that happened during the test (alternatively, a panic may be used, and will be returned as an
|
||||||
|
/// error).
|
||||||
///
|
///
|
||||||
/// # Panics
|
/// # Panics
|
||||||
|
/// The function may panic if any errors arise while configuring and using the Tokio multithreaded
|
||||||
|
/// runtime.
|
||||||
///
|
///
|
||||||
/// The function is not expected to run in production; therefore, the function plays "fast and
|
/// # Examples
|
||||||
/// loose" wih the usage of [`Result::expect`]. In normal usage, these should never be an issue.
|
/// ```rust
|
||||||
|
/// use std::os::unix::net::UnixStream;
|
||||||
|
/// let seed = b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
|
||||||
|
/// keyforkd::test_util::run_test(seed.as_slice(), |path| {
|
||||||
|
/// UnixStream::connect(&path).map(|_| ())
|
||||||
|
/// }).unwrap();
|
||||||
|
/// ```
|
||||||
#[allow(clippy::missing_errors_doc)]
|
#[allow(clippy::missing_errors_doc)]
|
||||||
pub fn run_test<F, E>(seed: &[u8], closure: F) -> Result<(), E>
|
pub fn run_test<F, E>(seed: &[u8], closure: F) -> Result<(), E>
|
||||||
where
|
where
|
||||||
|
|
|
@ -46,7 +46,7 @@ fn run() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let mut client = Client::discover_socket()?;
|
let mut client = Client::discover_socket()?;
|
||||||
let request = DerivationRequest::new(algo, &path);
|
let request = DerivationRequest::new(algo, &path);
|
||||||
let response = client.request(&request.into())?;
|
let response = client.request(&request.into())?;
|
||||||
println!("{}", smex::encode(&DerivationResponse::try_from(response)?.data));
|
println!("{}", smex::encode(DerivationResponse::try_from(response)?.data));
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
//! Creation of OpenPGP certificates from BIP-0032 derived data.
|
//! Creation of OpenPGP Transferable Secret Keys from BIP-0032 derived data.
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
str::FromStr,
|
str::FromStr,
|
||||||
|
@ -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() {
|
let expiration_date = match std::env::var("KEYFORK_OPENPGP_EXPIRE").as_mut() {
|
||||||
Ok(var) => {
|
Ok(var) => {
|
||||||
let ch = var.pop();
|
let ch = var.pop();
|
||||||
match (ch, u64::from_str(&var)) {
|
match (ch, u64::from_str(var)) {
|
||||||
(Some(ch @ ('d' | 'm' | 'y')), Ok(expire)) => {
|
(Some(ch @ ('d' | 'm' | 'y')), Ok(expire)) => {
|
||||||
let multiplier = match ch {
|
let multiplier = match ch {
|
||||||
'd' => 1,
|
'd' => 1,
|
||||||
|
|
|
@ -209,7 +209,7 @@ impl DerivationRequest {
|
||||||
/// # }
|
/// # }
|
||||||
pub fn derive_with_mnemonic(&self, mnemonic: &Mnemonic) -> Result<DerivationResponse> {
|
pub fn derive_with_mnemonic(&self, mnemonic: &Mnemonic) -> Result<DerivationResponse> {
|
||||||
// TODO: passphrase support and/or store passphrase within mnemonic
|
// TODO: passphrase support and/or store passphrase within mnemonic
|
||||||
self.derive_with_master_seed(&mnemonic.seed(None)?)
|
self.derive_with_master_seed(&mnemonic.generate_seed(None))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Derive an [`ExtendedPrivateKey`] using the given seed.
|
/// Derive an [`ExtendedPrivateKey`] using the given seed.
|
||||||
|
|
|
@ -30,7 +30,7 @@ fn secp256k1() {
|
||||||
} = test;
|
} = test;
|
||||||
|
|
||||||
// Tests for ExtendedPrivateKey
|
// Tests for ExtendedPrivateKey
|
||||||
let varlen_seed = VariableLengthSeed::new(&seed);
|
let varlen_seed = VariableLengthSeed::new(seed);
|
||||||
let xkey = ExtendedPrivateKey::<SecretKey>::new(varlen_seed);
|
let xkey = ExtendedPrivateKey::<SecretKey>::new(varlen_seed);
|
||||||
let derived_key = xkey.derive_path(&chain).unwrap();
|
let derived_key = xkey.derive_path(&chain).unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
@ -51,7 +51,7 @@ fn secp256k1() {
|
||||||
|
|
||||||
// Tests for DerivationRequest
|
// Tests for DerivationRequest
|
||||||
let request = DerivationRequest::new(DerivationAlgorithm::Secp256k1, &chain);
|
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}");
|
assert_eq!(&response.data, private_key.as_slice(), "test: {chain}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -76,7 +76,7 @@ fn ed25519() {
|
||||||
} = test;
|
} = test;
|
||||||
|
|
||||||
// Tests for ExtendedPrivateKey
|
// Tests for ExtendedPrivateKey
|
||||||
let varlen_seed = VariableLengthSeed::new(&seed);
|
let varlen_seed = VariableLengthSeed::new(seed);
|
||||||
let xkey = ExtendedPrivateKey::<SigningKey>::new(varlen_seed);
|
let xkey = ExtendedPrivateKey::<SigningKey>::new(varlen_seed);
|
||||||
let derived_key = xkey.derive_path(&chain).unwrap();
|
let derived_key = xkey.derive_path(&chain).unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
@ -97,7 +97,7 @@ fn ed25519() {
|
||||||
|
|
||||||
// Tests for DerivationRequest
|
// Tests for DerivationRequest
|
||||||
let request = DerivationRequest::new(DerivationAlgorithm::Ed25519, &chain);
|
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}");
|
assert_eq!(&response.data, private_key.as_slice(), "test: {chain}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -69,7 +69,7 @@ fn run() -> Result<()> {
|
||||||
let Some(line) = stdin().lines().next() else {
|
let Some(line) = stdin().lines().next() else {
|
||||||
return Err(Error::Input.into());
|
return Err(Error::Input.into());
|
||||||
};
|
};
|
||||||
smex::decode(&line?)?
|
smex::decode(line?)?
|
||||||
};
|
};
|
||||||
|
|
||||||
split(threshold, cert_list, &input, std::io::stdout())?;
|
split(threshold, cert_list, &input, std::io::stdout())?;
|
||||||
|
|
|
@ -57,10 +57,21 @@ fn ensure_offline() {
|
||||||
/// # std::env::set_var("SHOOT_SELF_IN_FOOT", "1");
|
/// # std::env::set_var("SHOOT_SELF_IN_FOOT", "1");
|
||||||
/// keyfork_entropy::ensure_safe();
|
/// keyfork_entropy::ensure_safe();
|
||||||
/// ```
|
/// ```
|
||||||
|
///
|
||||||
|
/// When running on a system that's online, or running an outdated kernel:
|
||||||
|
///
|
||||||
|
/// ```rust,should_panic
|
||||||
|
/// # // NOTE: sometimes, the environment variable is set, for testing purposes. I'm not sure how
|
||||||
|
/// # // to un-set it. Set it to a sentinel value.
|
||||||
|
/// # std::env::set_var("SHOOT_SELF_IN_FOOT", "test-must-fail");
|
||||||
|
/// # std::env::set_var("INSECURE_HARDWARE_ALLOWED", "test-must-fail");
|
||||||
|
/// keyfork_entropy::ensure_safe();
|
||||||
|
/// ```
|
||||||
pub fn ensure_safe() {
|
pub fn ensure_safe() {
|
||||||
if !std::env::vars()
|
if !std::env::vars().any(|(name, value)| {
|
||||||
.any(|(name, _)| name == "SHOOT_SELF_IN_FOOT" || name == "INSECURE_HARDWARE_ALLOWED")
|
(name == "SHOOT_SELF_IN_FOOT" || name == "INSECURE_HARDWARE_ALLOWED")
|
||||||
{
|
&& value != "test-must-fail"
|
||||||
|
}) {
|
||||||
ensure_safe_kernel_version();
|
ensure_safe_kernel_version();
|
||||||
ensure_offline();
|
ensure_offline();
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,7 +16,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
);
|
);
|
||||||
|
|
||||||
let entropy = keyfork_entropy::generate_entropy_of_size(bit_size / 8)?;
|
let entropy = keyfork_entropy::generate_entropy_of_size(bit_size / 8)?;
|
||||||
println!("{}", smex::encode(&entropy));
|
println!("{}", smex::encode(entropy));
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -66,6 +66,11 @@ pub(crate) fn hash(data: &[u8]) -> Vec<u8> {
|
||||||
/// # Errors
|
/// # Errors
|
||||||
/// An error may be returned if the given `data` is more than [`u32::MAX`] bytes. This is a
|
/// An error may be returned if the given `data` is more than [`u32::MAX`] bytes. This is a
|
||||||
/// constraint on a protocol level.
|
/// constraint on a protocol level.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
/// ```rust
|
||||||
|
/// let data = keyfork_frame::try_encode(b"hello world!".as_slice()).unwrap();
|
||||||
|
/// ```
|
||||||
pub fn try_encode(data: &[u8]) -> Result<Vec<u8>, EncodeError> {
|
pub fn try_encode(data: &[u8]) -> Result<Vec<u8>, EncodeError> {
|
||||||
let mut output = vec![];
|
let mut output = vec![];
|
||||||
try_encode_to(data, &mut output)?;
|
try_encode_to(data, &mut output)?;
|
||||||
|
@ -77,6 +82,12 @@ pub fn try_encode(data: &[u8]) -> Result<Vec<u8>, EncodeError> {
|
||||||
/// # Errors
|
/// # Errors
|
||||||
/// An error may be returned if the givenu `data` is more than [`u32::MAX`] bytes, or if the writer
|
/// An error may be returned if the givenu `data` is more than [`u32::MAX`] bytes, or if the writer
|
||||||
/// is unable to write data.
|
/// is unable to write data.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
/// ```rust
|
||||||
|
/// let mut output = vec![];
|
||||||
|
/// keyfork_frame::try_encode_to(b"hello world!".as_slice(), &mut output).unwrap();
|
||||||
|
/// ```
|
||||||
pub fn try_encode_to(data: &[u8], writable: &mut impl Write) -> Result<(), EncodeError> {
|
pub fn try_encode_to(data: &[u8], writable: &mut impl Write) -> Result<(), EncodeError> {
|
||||||
let hash = hash(data);
|
let hash = hash(data);
|
||||||
let len = hash.len() + data.len();
|
let len = hash.len() + data.len();
|
||||||
|
@ -107,18 +118,40 @@ pub(crate) fn verify_checksum(data: &[u8]) -> Result<&[u8], DecodeError> {
|
||||||
/// * The given `data` does not contain enough data to parse a length,
|
/// * The given `data` does not contain enough data to parse a length,
|
||||||
/// * The given `data` does not contain the given length's worth of data,
|
/// * The given `data` does not contain the given length's worth of data,
|
||||||
/// * The given `data` has a checksum that does not match what we build locally.
|
/// * The given `data` has a checksum that does not match what we build locally.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
/// ```rust
|
||||||
|
/// let input = b"hello world!";
|
||||||
|
/// let encoded = keyfork_frame::try_encode(input.as_slice()).unwrap();
|
||||||
|
/// let decoded = keyfork_frame::try_decode(&encoded).unwrap();
|
||||||
|
/// assert_eq!(input.as_slice(), decoded.as_slice());
|
||||||
|
/// ```
|
||||||
pub fn try_decode(data: &[u8]) -> Result<Vec<u8>, DecodeError> {
|
pub fn try_decode(data: &[u8]) -> Result<Vec<u8>, DecodeError> {
|
||||||
try_decode_from(&mut &data[..])
|
try_decode_from(&mut &data[..])
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Read and decode a framed message into a `Vec<u8>`.
|
/// Read and decode a framed message into a `Vec<u8>`.
|
||||||
///
|
///
|
||||||
|
/// Note that unlike [`try_encode_to`], this method does not allow writing to an object
|
||||||
|
/// implementing Write. This is because the data must be stored entirely in memory to allow
|
||||||
|
/// verifying the data. The data is then returned using the same in-memory representation as is
|
||||||
|
/// used in memory, and a caller may then choose to use `writable.write_all()`.
|
||||||
|
///
|
||||||
/// # Errors
|
/// # Errors
|
||||||
/// An error may be returned if:
|
/// An error may be returned if:
|
||||||
/// * The given `data` does not contain enough data to parse a length,
|
/// * The given `data` does not contain enough data to parse a length,
|
||||||
/// * The given `data` does not contain the given length's worth of data,
|
/// * The given `data` does not contain the given length's worth of data,
|
||||||
/// * The given `data` has a checksum that does not match what we build locally.
|
/// * The given `data` has a checksum that does not match what we build locally.
|
||||||
/// * The source for the data returned an error.
|
/// * The source for the data returned an error.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
/// ```rust
|
||||||
|
/// let input = b"hello world!";
|
||||||
|
/// let mut encoded = vec![];
|
||||||
|
/// keyfork_frame::try_encode_to(input.as_slice(), &mut encoded).unwrap();
|
||||||
|
/// let decoded = keyfork_frame::try_decode_from(&mut &encoded[..]).unwrap();
|
||||||
|
/// assert_eq!(input.as_slice(), decoded.as_slice());
|
||||||
|
/// ```
|
||||||
pub fn try_decode_from(readable: &mut impl Read) -> Result<Vec<u8>, DecodeError> {
|
pub fn try_decode_from(readable: &mut impl Read) -> Result<Vec<u8>, DecodeError> {
|
||||||
let mut bytes = 0u32.to_be_bytes();
|
let mut bytes = 0u32.to_be_bytes();
|
||||||
readable.read_exact(&mut bytes)?;
|
readable.read_exact(&mut bytes)?;
|
||||||
|
|
|
@ -1,6 +1,11 @@
|
||||||
//! Zero-dependency Mnemonic encoding and decoding.
|
//! 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 hmac::Hmac;
|
||||||
use pbkdf2::pbkdf2;
|
use pbkdf2::pbkdf2;
|
||||||
|
@ -43,10 +48,14 @@ impl Error for MnemonicGenerationError {}
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct Wordlist(Vec<String>);
|
pub struct Wordlist(Vec<String>);
|
||||||
|
|
||||||
|
static ENGLISH: OnceLock<Wordlist> = OnceLock::new();
|
||||||
|
|
||||||
impl Default for Wordlist {
|
impl Default for Wordlist {
|
||||||
/// Returns the English wordlist in the Bitcoin BIP-0039 specification.
|
/// Returns the English wordlist in the Bitcoin BIP-0039 specification.
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
// TODO: English is the only supported language.
|
// TODO: English is the only supported language.
|
||||||
|
ENGLISH
|
||||||
|
.get_or_init(|| {
|
||||||
let wordlist_file = include_str!("data/wordlist.txt");
|
let wordlist_file = include_str!("data/wordlist.txt");
|
||||||
Wordlist(
|
Wordlist(
|
||||||
wordlist_file
|
wordlist_file
|
||||||
|
@ -56,6 +65,9 @@ impl Default for Wordlist {
|
||||||
.map(|x| x.trim().to_string())
|
.map(|x| x.trim().to_string())
|
||||||
.collect(),
|
.collect(),
|
||||||
)
|
)
|
||||||
|
.shrank()
|
||||||
|
})
|
||||||
|
.clone()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -66,6 +78,12 @@ impl Wordlist {
|
||||||
Arc::new(self)
|
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.
|
/// Determine whether the Wordlist contains a given word.
|
||||||
pub fn contains(&self, word: &str) -> bool {
|
pub fn contains(&self, word: &str) -> bool {
|
||||||
self.0.iter().any(|w| w.as_str() == word)
|
self.0.iter().any(|w| w.as_str() == word)
|
||||||
|
@ -96,6 +114,14 @@ pub struct Mnemonic {
|
||||||
wordlist: Arc<Wordlist>,
|
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 {
|
impl Display for Mnemonic {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
let bit_count = self.entropy.len() * 8;
|
let bit_count = self.entropy.len() * 8;
|
||||||
|
@ -231,6 +257,13 @@ impl Mnemonic {
|
||||||
///
|
///
|
||||||
/// # Errors
|
/// # Errors
|
||||||
/// An error may be returned if the entropy is not within the acceptable lengths.
|
/// An error may be returned if the entropy is not within the acceptable lengths.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
/// ```rust
|
||||||
|
/// use keyfork_mnemonic_util::Mnemonic;
|
||||||
|
/// let data = b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
|
||||||
|
/// let mnemonic = Mnemonic::from_entropy(data.as_slice(), Default::default()).unwrap();
|
||||||
|
/// ```
|
||||||
pub fn from_entropy(
|
pub fn from_entropy(
|
||||||
bytes: &[u8],
|
bytes: &[u8],
|
||||||
wordlist: Arc<Wordlist>,
|
wordlist: Arc<Wordlist>,
|
||||||
|
@ -248,11 +281,36 @@ impl Mnemonic {
|
||||||
Ok(unsafe { Self::from_raw_entropy(bytes, wordlist) })
|
Ok(unsafe { Self::from_raw_entropy(bytes, wordlist) })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Create a Mnemonic using an arbitrary length of given entropy. The length does not need to
|
||||||
|
/// conform to BIP-0039 standards.
|
||||||
|
///
|
||||||
/// # Safety
|
/// # Safety
|
||||||
///
|
///
|
||||||
/// This function can potentially produce mnemonics that are not BIP-0039 compliant or can't
|
/// This function can potentially produce mnemonics that are not BIP-0039 compliant or can't
|
||||||
/// properly be encoded as a mnemonic. It is assumed the caller asserts the byte count is `% 4
|
/// properly be encoded as a mnemonic. It is assumed the caller asserts the byte count is `% 4
|
||||||
/// == 0`.
|
/// == 0`. If the assumption is incorrect, code may panic.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
/// ```rust
|
||||||
|
/// use keyfork_mnemonic_util::Mnemonic;
|
||||||
|
/// let data = b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
|
||||||
|
/// let mnemonic = unsafe { Mnemonic::from_raw_entropy(data.as_slice(), Default::default()) };
|
||||||
|
/// let mnemonic_text = mnemonic.to_string();
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// If given an invalid length, undefined behavior may follow, or code may panic.
|
||||||
|
///
|
||||||
|
/// ```rust,should_panic
|
||||||
|
/// use keyfork_mnemonic_util::Mnemonic;
|
||||||
|
/// use std::str::FromStr;
|
||||||
|
///
|
||||||
|
/// // NOTE: Data is of invalid length, 31
|
||||||
|
/// let data = b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
|
||||||
|
/// let mnemonic = unsafe { Mnemonic::from_raw_entropy(data.as_slice(), Default::default()) };
|
||||||
|
/// let mnemonic_text = mnemonic.to_string();
|
||||||
|
/// // NOTE: panic happens here
|
||||||
|
/// let new_mnemonic = Mnemonic::from_str(&mnemonic_text).unwrap();
|
||||||
|
/// ```
|
||||||
pub unsafe fn from_raw_entropy(bytes: &[u8], wordlist: Arc<Wordlist>) -> Mnemonic {
|
pub unsafe fn from_raw_entropy(bytes: &[u8], wordlist: Arc<Wordlist>) -> Mnemonic {
|
||||||
Mnemonic {
|
Mnemonic {
|
||||||
entropy: bytes.to_vec(),
|
entropy: bytes.to_vec(),
|
||||||
|
@ -260,22 +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] {
|
pub fn as_bytes(&self) -> &[u8] {
|
||||||
&self.entropy
|
&self.entropy
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The internal representation of the decoded data, as a [`Vec<u8>`].
|
/// A clone of the internal representation of the decoded data.
|
||||||
pub fn to_bytes(&self) -> Vec<u8> {
|
pub fn to_bytes(&self) -> Vec<u8> {
|
||||||
self.entropy.to_vec()
|
self.entropy.to_vec()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Drop self, returning the decoded data.
|
/// Conver the Mnemonic into the internal representation of the decoded data.
|
||||||
pub fn into_bytes(self) -> Vec<u8> {
|
pub fn into_bytes(self) -> Vec<u8> {
|
||||||
self.entropy
|
self.entropy
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Clone the existing entropy.
|
/// Clone the existing data.
|
||||||
#[deprecated = "Use as_bytes(), to_bytes(), or into_bytes() instead"]
|
#[deprecated = "Use as_bytes(), to_bytes(), or into_bytes() instead"]
|
||||||
pub fn entropy(&self) -> Vec<u8> {
|
pub fn entropy(&self) -> Vec<u8> {
|
||||||
self.entropy.clone()
|
self.entropy.clone()
|
||||||
|
@ -284,23 +342,34 @@ impl Mnemonic {
|
||||||
/// Create a BIP-0032 seed from the provided data and an optional passphrase.
|
/// Create a BIP-0032 seed from the provided data and an optional passphrase.
|
||||||
///
|
///
|
||||||
/// # Errors
|
/// # Errors
|
||||||
/// The method may return an error if the pbkdf2 function returns an invalid length, but this
|
/// The method should not return an error.
|
||||||
/// case should not be reached.
|
#[deprecated = "Use generate_seed() instead"]
|
||||||
pub fn seed<'a>(
|
pub fn seed<'a>(
|
||||||
&self,
|
&self,
|
||||||
passphrase: impl Into<Option<&'a str>>,
|
passphrase: impl Into<Option<&'a str>>,
|
||||||
) -> Result<Vec<u8>, MnemonicGenerationError> {
|
) -> 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 passphrase = passphrase.into();
|
||||||
|
|
||||||
let mut seed = [0u8; 64];
|
let mut seed = [0u8; 64];
|
||||||
let mnemonic = self.to_string();
|
let mnemonic = self.to_string();
|
||||||
let salt = ["mnemonic", passphrase.unwrap_or("")].join("");
|
let salt = ["mnemonic", passphrase.unwrap_or("")].join("");
|
||||||
pbkdf2::<Hmac<Sha512>>(mnemonic.as_bytes(), salt.as_bytes(), 2048, &mut seed)
|
pbkdf2::<Hmac<Sha512>>(mnemonic.as_bytes(), salt.as_bytes(), 2048, &mut seed)
|
||||||
.map_err(|_| MnemonicGenerationError::InvalidPbkdf2Length)?;
|
.expect("HmacSha512 InvalidLength should be infallible");
|
||||||
Ok(seed.to_vec())
|
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>) {
|
pub fn words(self) -> (Vec<usize>, Arc<Wordlist>) {
|
||||||
let bit_count = self.entropy.len() * 8;
|
let bit_count = self.entropy.len() * 8;
|
||||||
let mut bits = vec![false; bit_count + bit_count / 32];
|
let mut bits = vec![false; bit_count + bit_count / 32];
|
||||||
|
@ -384,13 +453,13 @@ mod tests {
|
||||||
let my_mnemonic = super::Mnemonic::from_entropy(&entropy[..256 / 8], wordlist).unwrap();
|
let my_mnemonic = super::Mnemonic::from_entropy(&entropy[..256 / 8], wordlist).unwrap();
|
||||||
let their_mnemonic = bip39::Mnemonic::from_entropy(&entropy[..256 / 8]).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.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!(
|
assert_eq!(
|
||||||
my_mnemonic.seed("testing").unwrap(),
|
my_mnemonic.generate_seed("testing"),
|
||||||
their_mnemonic.to_seed("testing")
|
their_mnemonic.to_seed("testing")
|
||||||
);
|
);
|
||||||
assert_ne!(
|
assert_ne!(
|
||||||
my_mnemonic.seed("test1").unwrap(),
|
my_mnemonic.generate_seed("test1"),
|
||||||
their_mnemonic.to_seed("test2")
|
their_mnemonic.to_seed("test2")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,7 +28,15 @@ impl std::fmt::Display for DecodeError {
|
||||||
impl std::error::Error for DecodeError {}
|
impl std::error::Error for DecodeError {}
|
||||||
|
|
||||||
/// Encode a given input as a hex string.
|
/// Encode a given input as a hex string.
|
||||||
pub fn encode(input: &[u8]) -> String {
|
///
|
||||||
|
/// # Examples
|
||||||
|
/// ```rust
|
||||||
|
/// let data = b"hello world!";
|
||||||
|
/// let result = smex::encode(&data);
|
||||||
|
/// assert_eq!(result, "68656c6c6f20776f726c6421");
|
||||||
|
/// ```
|
||||||
|
pub fn encode(input: impl AsRef<[u8]>) -> String {
|
||||||
|
let input = input.as_ref();
|
||||||
let mut s = String::new();
|
let mut s = String::new();
|
||||||
for byte in input {
|
for byte in input {
|
||||||
write!(s, "{byte:02x}").unwrap();
|
write!(s, "{byte:02x}").unwrap();
|
||||||
|
@ -50,7 +58,26 @@ fn val(c: u8) -> Result<u8, DecodeError> {
|
||||||
/// # Errors
|
/// # Errors
|
||||||
/// The function may error if a non-hex character is encountered or if the character count is not
|
/// The function may error if a non-hex character is encountered or if the character count is not
|
||||||
/// evenly divisible by two.
|
/// evenly divisible by two.
|
||||||
pub fn decode(input: &str) -> Result<Vec<u8>, DecodeError> {
|
///
|
||||||
|
/// # Examples
|
||||||
|
/// ```rust
|
||||||
|
/// let data = b"hello world!";
|
||||||
|
/// let encoded = smex::encode(&data);
|
||||||
|
/// let decoded = smex::decode(&encoded).unwrap();
|
||||||
|
/// assert_eq!(data.as_slice(), decoded.as_slice());
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// The function may return an error if the given input is not valid hex.
|
||||||
|
///
|
||||||
|
/// ```rust,should_panic
|
||||||
|
/// let data = b"hello world!";
|
||||||
|
/// let mut encoded = smex::encode(&data);
|
||||||
|
/// encoded.push('G');
|
||||||
|
/// let decoded = smex::decode(&encoded).unwrap();
|
||||||
|
/// assert_eq!(data.as_slice(), decoded.as_slice());
|
||||||
|
/// ```
|
||||||
|
pub fn decode(input: impl AsRef<str>) -> Result<Vec<u8>, DecodeError> {
|
||||||
|
let input = input.as_ref();
|
||||||
let len = input.len();
|
let len = input.len();
|
||||||
if len % 2 != 0 {
|
if len % 2 != 0 {
|
||||||
return Err(DecodeError::InvalidCharacterCount(len));
|
return Err(DecodeError::InvalidCharacterCount(len));
|
||||||
|
|
Loading…
Reference in New Issue