Compare commits

...

5 Commits

15 changed files with 330 additions and 47 deletions

50
Cargo.lock generated
View File

@ -26,6 +26,12 @@ dependencies = [
"memchr",
]
[[package]]
name = "anstyle"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b84bf0a05bbb2a83e5eb6fa36bb6e87baa08193c35ff52bbf6b38d8af2890e46"
[[package]]
name = "backtrace"
version = "0.3.69"
@ -109,6 +115,31 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "clap"
version = "4.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a13b88d2c62ff462f88e4a121f17a82c1af05693a2f192b5c38d14de73c19f6"
dependencies = [
"clap_builder",
]
[[package]]
name = "clap_builder"
version = "4.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2bb9faaa7c2ef94b2743a21f5a29e6f0010dff4caa69ac8e9d6cf8b6fa74da08"
dependencies = [
"anstyle",
"clap_lex",
]
[[package]]
name = "clap_lex"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd7cc57abe963c6d3b9d8be5b06ba7c8957a930305ca90304f24ef040aa6f961"
[[package]]
name = "console"
version = "0.15.7"
@ -448,6 +479,17 @@ dependencies = [
"sha2",
]
[[package]]
name = "keyfork-seed"
version = "0.1.0"
dependencies = [
"bincode",
"clap",
"keyfork-derive-util",
"keyfork-frame",
"thiserror",
]
[[package]]
name = "keyforkd"
version = "0.1.0"
@ -855,18 +897,18 @@ dependencies = [
[[package]]
name = "thiserror"
version = "1.0.47"
version = "1.0.48"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97a802ec30afc17eee47b2855fc72e0c4cd62be9b4efe6591edde0ec5bd68d8f"
checksum = "9d6d7a740b8a666a7e828dd00da9c0dc290dff53154ea77ac109281de90589b7"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.47"
version = "1.0.48"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6bb623b56e39ab7dcd4b1b98bb6c8f8d907ed255b18de254088016b27a8ee19b"
checksum = "49922ecae66cc8a249b77e68d1d0623c1b2c514f0060c27cdc68bd62a1219d35"
dependencies = [
"proc-macro2",
"quote",

View File

@ -5,6 +5,7 @@ members = [
"keyfork-mnemonic-generate",
"keyfork-mnemonic-util",
"keyfork-derive-util",
"keyfork-seed",
"keyforkd",
"keyfork-frame"
]

View File

@ -26,6 +26,8 @@ thiserror = "1.0.47"
# Optional, not personally audited
k256 = { version = "0.13.1", default-features = false, features = ["std", "arithmetic"], optional = true }
ed25519-dalek = { version = "2.0.0", optional = true }
# Workspace
keyfork-mnemonic-util = { version = "0.1.0", path = "../keyfork-mnemonic-util" }
[dev-dependencies]

View File

@ -29,6 +29,10 @@ pub enum Error {
/// The algorithm used mandates hardened derivation only.
#[error("The algorithm used mandates hardened derivation only")]
HardenedDerivationRequired,
/// The given slice was of an inappropriate size to create a Private Key.
#[error("The given slice was of an inappropriate size to create a Private Key")]
InvalidSliceError(#[from] std::array::TryFromSliceError),
}
type Result<T, E = Error> = std::result::Result<T, E>;
@ -41,9 +45,9 @@ type HmacSha512 = Hmac<Sha512>;
#[derive(Clone, Serialize, Deserialize)]
pub struct ExtendedPrivateKey<K: PrivateKey + Clone> {
/// The internal private key data.
pub private_key: K,
private_key: K,
depth: u8,
pub(crate) chain_code: ChainCode,
chain_code: ChainCode,
}
impl<K: PrivateKey + Clone> std::fmt::Debug for ExtendedPrivateKey<K> {
@ -88,18 +92,42 @@ where
.into_bytes();
let (private_key, chain_code) = hash.split_at(KEY_SIZE / 8);
Self::new_from_parts(
private_key,
0,
// Checked: chain_code is always the same length, hash is static size
chain_code.try_into().expect("Invalid chain code length"),
)
}
pub fn new_from_parts(seed: &[u8], depth: u8, chain_code: [u8; 32]) -> Result<Self> {
Ok(Self {
private_key: K::from_bytes(private_key.try_into().expect("Invalid key length")),
depth: 0,
chain_code: chain_code.try_into().expect("Invalid chain code length"),
private_key: K::from_bytes(seed.try_into()?),
depth,
chain_code,
})
}
/// Returns a reference to the [`PrivateKey`].
pub fn private_key(&self) -> &K {
&self.private_key
}
/// Return a public key for the current [`PrivateKey`].
pub fn public_key(&self) -> K::PublicKey {
self.private_key.public_key()
}
/// Returns the current depth.
pub fn depth(&self) -> u8 {
self.depth
}
/// Returns a copy of the current chain code.
pub fn chain_code(&self) -> [u8; 32] {
self.chain_code
}
/// Derive a child using the given [`DerivationPath`].
///
/// # Errors

View File

@ -34,6 +34,14 @@ impl DerivationPath {
pub fn iter(&self) -> impl Iterator<Item = &DerivationIndex> {
self.path.iter()
}
pub fn len(&self) -> usize {
self.path.len()
}
pub fn is_empty(&self) -> bool {
self.path.is_empty()
}
}
impl std::str::FromStr for DerivationPath {

View File

@ -28,19 +28,19 @@ impl DerivationAlgorithm {
Self::Ed25519 => {
let key = ExtendedPrivateKey::<ed25519_dalek::SigningKey>::new(seed)?;
let derived_key = key.derive_path(path)?;
Ok(DerivationResponse {
algorithm: self.clone(),
data: PrivateKey::to_bytes(&derived_key.private_key).to_vec(),
})
Ok(DerivationResponse::with_algo_and_xprv(
self.clone(),
&derived_key,
))
}
#[cfg(feature = "secp256k1")]
Self::Secp256k1 => {
let key = ExtendedPrivateKey::<k256::SecretKey>::new(seed)?;
let derived_key = key.derive_path(path)?;
Ok(DerivationResponse {
algorithm: self.clone(),
data: PrivateKey::to_bytes(&derived_key.private_key).to_vec(),
})
Ok(DerivationResponse::with_algo_and_xprv(
self.clone(),
&derived_key,
))
}
#[allow(unreachable_patterns)]
_ => Err(DerivationError::Algorithm),
@ -48,6 +48,18 @@ impl DerivationAlgorithm {
}
}
impl std::str::FromStr for DerivationAlgorithm {
type Err = DerivationError;
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
Ok(match s {
"ed25519" => Self::Ed25519,
"secp256k1" => Self::Secp256k1,
_ => return Err(DerivationError::Algorithm),
})
}
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct DerivationRequest {
algorithm: DerivationAlgorithm,
@ -59,6 +71,10 @@ impl DerivationRequest {
Self { algorithm, path }
}
pub fn path(&self) -> &DerivationPath {
&self.path
}
pub fn derive_with_mnemonic(&self, mnemonic: &Mnemonic) -> Result<DerivationResponse> {
self.derive_with_master_seed(mnemonic.seed())
}
@ -72,4 +88,20 @@ impl DerivationRequest {
pub struct DerivationResponse {
pub algorithm: DerivationAlgorithm,
pub data: Vec<u8>,
pub chain_code: [u8; 32],
pub depth: u8,
}
impl DerivationResponse {
pub fn with_algo_and_xprv<T: PrivateKey + Clone>(
algorithm: DerivationAlgorithm,
xprv: &ExtendedPrivateKey<T>,
) -> Self {
Self {
algorithm,
data: PrivateKey::to_bytes(xprv.private_key()).to_vec(),
chain_code: xprv.chain_code(),
depth: xprv.depth(),
}
}
}

View File

@ -62,8 +62,8 @@ fn secp256k1() {
for (seed, chain, chain_code, private_key, public_key) in tests {
let xkey = ExtendedPrivateKey::<SecretKey>::new(seed).unwrap();
let derived_key = xkey.derive_path(&chain).unwrap();
assert_eq!(derived_key.chain_code, chain_code);
assert_eq!(derived_key.private_key.to_bytes().as_slice(), private_key);
assert_eq!(derived_key.chain_code(), chain_code);
assert_eq!(derived_key.private_key().to_bytes().as_slice(), private_key);
assert_eq!(derived_key.public_key().to_bytes(), public_key);
let request = DerivationRequest::new(DerivationAlgorithm::Secp256k1, chain);
let response = request.derive_with_master_seed(seed.to_vec()).unwrap();
@ -103,8 +103,8 @@ fn ed25519() {
for (seed, chain, chain_code, private_key, public_key) in tests {
let xkey = ExtendedPrivateKey::<SigningKey>::new(seed).unwrap();
let derived_key = xkey.derive_path(&chain).unwrap();
assert_eq!(derived_key.chain_code, chain_code);
assert_eq!(PrivateKey::to_bytes(&derived_key.private_key), private_key);
assert_eq!(derived_key.chain_code(), chain_code);
assert_eq!(PrivateKey::to_bytes(derived_key.private_key()), private_key);
assert_eq!(PublicKey::to_bytes(&derived_key.public_key()), public_key);
let request = DerivationRequest::new(DerivationAlgorithm::Ed25519, chain);
let response = request.derive_with_master_seed(seed.to_vec()).unwrap();

View File

@ -15,7 +15,8 @@ pub async fn try_decode_from(
) -> Result<Vec<u8>, DecodeError> {
let len = readable.read_u32().await?;
let mut data = Vec::with_capacity(len as usize);
// Note: Pre-filling the vec is *required* as read_exact uses len, not capacity.
let mut data = vec![0u8; len as usize];
readable.read_exact(&mut data[..]).await?;
let content = verify_checksum(&data[..])?;

View File

@ -12,6 +12,8 @@
//! | checksum: [u8; 32] sha256 hash of `raw_data` | raw_data: &[u8] |
//! ```
use std::io::{Read, Write};
#[cfg(feature = "async")]
pub mod asyncext;
@ -51,8 +53,6 @@ pub enum EncodeError {
Io(#[from] std::io::Error),
}
const LEN_SIZE: usize = std::mem::size_of::<u32>();
pub(crate) fn hash(data: &[u8]) -> Vec<u8> {
let mut hashobj = Sha256::new();
hashobj.update(data);
@ -65,14 +65,19 @@ pub(crate) fn hash(data: &[u8]) -> Vec<u8> {
/// An error may be returned if the given `data` is more than [`u32::MAX`] bytes. This is a
/// constraint on a protocol level.
pub fn try_encode(data: &[u8]) -> Result<Vec<u8>, EncodeError> {
let mut output = vec![];
try_encode_to(data, &mut output)?;
Ok(output)
}
pub fn try_encode_to(data: &[u8], writable: &mut impl Write) -> Result<(), EncodeError> {
let hash = hash(data);
let content = hash.iter().chain(data.iter()).copied().collect::<Vec<_>>();
let mut result = (u32::try_from(content.len())
.map_err(|_| EncodeError::InputTooLarge(content.len()))?)
.to_be_bytes()
.to_vec();
result.extend(content);
Ok(result)
let len = hash.len() + data.len();
let len = u32::try_from(len).map_err(|_| EncodeError::InputTooLarge(len))?;
writable.write_all(&len.to_be_bytes())?;
writable.write_all(&hash)?;
writable.write_all(data)?;
Ok(())
}
pub(crate) fn verify_checksum(data: &[u8]) -> Result<&[u8], DecodeError> {
@ -99,18 +104,16 @@ pub(crate) fn verify_checksum(data: &[u8]) -> Result<&[u8], DecodeError> {
/// * 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.
pub fn try_decode(data: &[u8]) -> Result<Vec<u8>, DecodeError> {
// check length and advance data pointer beyond length check
let len_bytes: [u8; LEN_SIZE] = data[..LEN_SIZE]
.try_into()
.map_err(DecodeError::InvalidLength)?;
let len = u32::from_be_bytes(len_bytes);
if len as usize + LEN_SIZE > data.len() {
return Err(DecodeError::IncorrectLength(data.len() - LEN_SIZE, len));
}
let data = &data[LEN_SIZE..];
let content = verify_checksum(data)?;
try_decode_from(&mut &data[..])
}
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)?;
let len = u32::from_be_bytes(bytes);
let mut data = vec![0u8; len as usize];
readable.read_exact(&mut data)?;
let content = verify_checksum(&data)?;
Ok(content.to_vec())
}

13
keyfork-seed/Cargo.toml Normal file
View File

@ -0,0 +1,13 @@
[package]
name = "keyfork-seed"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
bincode = { version = "1.3.3", default-features = false }
clap = { version = "4.4.2", default-features = false, features = ["std", "usage", "help"] }
keyfork-derive-util = { version = "0.1.0", path = "../keyfork-derive-util" }
keyfork-frame = { version = "0.1.0", path = "../keyfork-frame", default-features = false }
thiserror = "1.0.48"

19
keyfork-seed/src/cli.rs Normal file
View File

@ -0,0 +1,19 @@
use clap::{arg, value_parser, ArgMatches, Command};
use keyfork_derive_util::{request::*, DerivationPath};
pub fn get_args() -> ArgMatches {
Command::new("keyfork-seed")
.arg(
arg!(--path <PATH>)
.required(true)
.help("string value of a DerivationPath")
.value_parser(value_parser!(DerivationPath)),
)
.arg(
arg!(--algorithm <ALGO>)
.required(true)
.help("string value of a DerivationAlgorithm")
.value_parser(value_parser!(DerivationAlgorithm)),
)
.get_matches()
}

49
keyfork-seed/src/main.rs Normal file
View File

@ -0,0 +1,49 @@
use keyfork_derive_util::{request::*, DerivationPath};
use keyfork_frame::*;
use std::path::PathBuf;
mod cli;
mod socket;
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("The first argument to the command should be a derivation path")]
Args,
#[error("The given path was incorrectly formatted: {0}")]
ArgsFormat(#[from] keyfork_derive_util::path::Error),
#[error("Neither KEYFORK_SOCKET_PATH nor XDG_RUNTIME_DIR were set")]
EnvVarsNotFound,
#[error("Socket was unable to connect to {1}: {0}")]
Connect(std::io::Error, PathBuf),
#[error("Could not write to or from the socket: {0}")]
Io(#[from] std::io::Error),
#[error("Could not perform bincode transformation: {0}")]
Bincode(#[from] Box<bincode::ErrorKind>),
#[error("Could not perform frame transformation: {0}")]
FrameEnc(#[from] EncodeError),
#[error("Could not perform frame transformation: {0}")]
FrameDec(#[from] DecodeError),
}
fn main() -> Result<(), Error> {
let args = cli::get_args();
let mut socket = socket::get_socket()?;
let path = args.get_one::<DerivationPath>("path").expect("required");
let algo = args
.get_one::<DerivationAlgorithm>("algorithm")
.expect("required");
let req = DerivationRequest::new(algo.clone(), path.clone());
let ser_req = bincode::serialize(&req)?;
try_encode_to(&ser_req, &mut socket)?;
let ser_response = try_decode_from(&mut socket)?;
let response: DerivationResponse = bincode::deserialize(&ser_response)?;
dbg!(&response);
Ok(())
}

View File

@ -0,0 +1,24 @@
use super::Error;
use std::{collections::HashMap, os::unix::net::UnixStream, path::PathBuf};
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()))
.collect::<HashMap<String, String>>();
let mut socket_path: PathBuf;
#[allow(clippy::single_match_else)]
match socket_vars.get("KEYFORKD_SOCKET_PATH") {
Some(occupied) => {
socket_path = PathBuf::from(occupied);
}
None => {
socket_path = PathBuf::from(
socket_vars
.get("XDG_RUNTIME_DIR")
.ok_or(Error::EnvVarsNotFound)?,
);
socket_path.extend(["keyforkd", "keyforkd.sock"]);
}
}
UnixStream::connect(&socket_path).map_err(|e| Error::Connect(e, socket_path))
}

View File

@ -103,7 +103,7 @@ mod tests {
impl Test {
fn new() -> Self {
Self {
field: "hello world!".to_string()
field: "hello world!".to_string(),
}
}
}
@ -135,8 +135,7 @@ mod tests {
#[tokio::test]
async fn can_serde_responses() {
let content = serialize(&Test::new())
.unwrap();
let content = serialize(&Test::new()).unwrap();
let mut service = ServiceBuilder::new()
.layer(BincodeLayer::<Test>::new())
.service(App);

View File

@ -7,6 +7,15 @@ use tower::Service;
// 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.
#[derive(thiserror::Error, Debug)]
pub enum KeyforkdRequestError {
#[error("Invalid derivation length: Expected: 2, actual: {0}")]
InvalidDerivationLength(usize),
#[error("Derivation error: {0}")]
Derivation(#[from] DerivationError),
}
#[derive(Clone, Debug)]
pub struct Keyforkd {
mnemonic: Arc<Mnemonic>,
@ -23,7 +32,7 @@ impl Keyforkd {
impl Service<DerivationRequest> for Keyforkd {
type Response = DerivationResponse;
type Error = DerivationError;
type Error = KeyforkdRequestError;
type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send>>;
@ -37,7 +46,14 @@ impl Service<DerivationRequest> for Keyforkd {
#[cfg_attr(feature = "tracing", tracing::instrument(skip(self)))]
fn call(&mut self, req: DerivationRequest) -> Self::Future {
let mnemonic = self.mnemonic.clone();
Box::pin(async { req.derive_with_mnemonic(&mnemonic) })
Box::pin(async {
let len = req.path().len();
if len < 2 {
return Err(KeyforkdRequestError::InvalidDerivationLength(len));
}
req.derive_with_mnemonic(&mnemonic)
.map_err(KeyforkdRequestError::from)
})
}
}
@ -54,6 +70,9 @@ mod tests {
async fn properly_derives_data() {
// Pulled from keyfork-derive-util's tests, which is more extensively tested.
let tests = [
/*
* Note: Tests excluded because the derivation path is not deep enough
* for the API's preferences.
(
&hex!("000102030405060708090a0b0c0d0e0f")[..],
DerivationPath::from_str("m").unwrap(),
@ -68,6 +87,7 @@ mod tests {
hex!("68e0fe46dfb67e368c75379acec591dad19df3cde26e63b93a8e704f1dade7a3"),
hex!("008c8a13df77a28f3445213a0f432fde644acaa215fc72dcdf300d5efaa85d350c"),
),
*/
(
&hex!("000102030405060708090a0b0c0d0e0f")[..],
DerivationPath::from_str("m/0'/1'/2'/2'/1000000000'").unwrap(),
@ -86,4 +106,46 @@ mod tests {
assert_eq!(response.data, private_key)
}
}
#[should_panic]
#[tokio::test]
async fn errors_on_no_path() {
let tests = [(
&hex!("000102030405060708090a0b0c0d0e0f")[..],
DerivationPath::from_str("m").unwrap(),
hex!("90046a93de5380a72b5e45010748567d5ea02bbf6522f979e05c0d8d8ca9fffb"),
hex!("2b4be7f19ee27bbf30c667b642d5f4aa69fd169872f8fc3059c08ebae2eb19e7"),
hex!("00a4b2856bfec510abab89753fac1ac0e1112364e7d250545963f135f2a33188ed"),
)];
let wordlist = Wordlist::default().arc();
for (seed, path, _, private_key, _) in tests {
let mnemonic = Mnemonic::from_entropy(&seed[..], wordlist.clone()).unwrap();
assert_eq!(mnemonic.seed(), seed);
let req = DerivationRequest::new(DerivationAlgorithm::Ed25519, path);
let mut keyforkd = Keyforkd::new(mnemonic);
let response = keyforkd.ready().await.unwrap().call(req).await.unwrap();
assert_eq!(response.data, private_key)
}
}
#[should_panic]
#[tokio::test]
async fn errors_on_short_path() {
let tests = [(
&hex!("000102030405060708090a0b0c0d0e0f")[..],
DerivationPath::from_str("m/0'").unwrap(),
hex!("8b59aa11380b624e81507a27fedda59fea6d0b779a778918a2fd3590e16e9c69"),
hex!("68e0fe46dfb67e368c75379acec591dad19df3cde26e63b93a8e704f1dade7a3"),
hex!("008c8a13df77a28f3445213a0f432fde644acaa215fc72dcdf300d5efaa85d350c"),
)];
let wordlist = Wordlist::default().arc();
for (seed, path, _, private_key, _) in tests {
let mnemonic = Mnemonic::from_entropy(&seed[..], wordlist.clone()).unwrap();
assert_eq!(mnemonic.seed(), seed);
let req = DerivationRequest::new(DerivationAlgorithm::Ed25519, path);
let mut keyforkd = Keyforkd::new(mnemonic);
let response = keyforkd.ready().await.unwrap().call(req).await.unwrap();
assert_eq!(response.data, private_key)
}
}
}