diff --git a/Cargo.lock b/Cargo.lock index 333e533..ccc79df 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1742,6 +1742,7 @@ dependencies = [ name = "keyfork" version = "0.2.4" dependencies = [ + "base64", "card-backend-pcsc", "clap", "clap_complete", @@ -1756,6 +1757,7 @@ dependencies = [ "keyfork-shard", "keyforkd", "keyforkd-client", + "keyforkd-models", "openpgp-card", "openpgp-card-sequoia", "sequoia-openpgp", diff --git a/Cargo.toml b/Cargo.toml index 3964c4c..e7d08be 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -74,6 +74,7 @@ image = { version = "0.25.2", default-features = false } thiserror = "1.0.56" tokio = "1.35.1" v4l = "0.14.0" +base64 = "0.22.1" [profile.dev.package.keyfork-qrcode] opt-level = 3 diff --git a/crates/keyfork/Cargo.toml b/crates/keyfork/Cargo.toml index cfcebe1..bd756fe 100644 --- a/crates/keyfork/Cargo.toml +++ b/crates/keyfork/Cargo.toml @@ -45,3 +45,5 @@ openpgp-card-sequoia = { workspace = true } openpgp-card = { workspace = true } clap_complete = { version = "4.4.6", optional = true } sequoia-openpgp = { workspace = true } +keyforkd-models.workspace = true +base64.workspace = true diff --git a/crates/keyfork/src/cli/derive.rs b/crates/keyfork/src/cli/derive.rs index d444173..3f28856 100644 --- a/crates/keyfork/src/cli/derive.rs +++ b/crates/keyfork/src/cli/derive.rs @@ -1,5 +1,5 @@ use super::Keyfork; -use clap::{Args, Parser, Subcommand}; +use clap::{Args, Parser, Subcommand, ValueEnum}; use keyfork_derive_openpgp::{ openpgp::{ @@ -10,9 +10,13 @@ use keyfork_derive_openpgp::{ }, XPrvKey, }; -use keyfork_derive_util::DerivationIndex; use keyfork_derive_path_data::paths; +use keyfork_derive_util::{ + request::{DerivationAlgorithm, DerivationRequest, DerivationResponse}, + DerivationIndex, DerivationPath, IndexError, +}; use keyforkd_client::Client; +use keyforkd_models::Request; type Result> = std::result::Result; @@ -28,7 +32,10 @@ pub enum DeriveSubcommands { /// It is recommended to use the default expiration of one day and to change the expiration /// using an external utility, to ensure the Certify key is usable. #[command(name = "openpgp")] - OpenPGP(OpenPGP) + OpenPGP(OpenPGP), + + /// Derive a bare key for a specific algorithm, in a given format. + Key(Key), } #[derive(Args, Clone, Debug)] @@ -37,10 +44,64 @@ pub struct OpenPGP { user_id: String, } +/// A format for exporting a key. +#[derive(ValueEnum, Clone, Debug)] +pub enum KeyFormat { + Hex, + Base64, +} + +/// An invalid slug was provided. +#[derive(thiserror::Error, Debug)] +pub enum InvalidSlug { + /// The value provided was longer than four bytes. + #[error("The value provided was longer than four bytes: {0}")] + InvalidSize(usize), + + /// The value provided was higher than the maximum derivation index. + #[error("The value provided was higher than the maximum derivation index: {0}")] + InvalidValue(#[from] IndexError), +} + +#[derive(Clone, Debug)] +pub struct Slug(DerivationIndex); + +impl std::str::FromStr for Slug { + type Err = InvalidSlug; + + fn from_str(s: &str) -> std::result::Result { + let bytes = s.as_bytes(); + let mut parseable_bytes = [0u8; 4]; + if bytes.len() <= 4 && !bytes.is_empty() { + parseable_bytes[(4 - bytes.len())..4].copy_from_slice(bytes); + } else { + return Err(InvalidSlug::InvalidSize(bytes.len())); + } + let slug = u32::from_be_bytes(parseable_bytes); + let index = DerivationIndex::new(slug, true)?; + Ok(Slug(index)) + } +} + +#[derive(Args, Clone, Debug)] +pub struct Key { + /// The derivation algorithm to derive a key for. + derivation_algorithm: DerivationAlgorithm, + + /// The output format. + #[arg(value_enum)] + format: KeyFormat, + + /// A maximum of four bytes, used for creating the derivation path. + #[arg(value_parser = clap::value_parser!(Slug))] + slug: Slug, +} + impl DeriveSubcommands { fn handle(&self, account: DerivationIndex) -> Result<()> { match self { DeriveSubcommands::OpenPGP(opgp) => opgp.handle(account), + DeriveSubcommands::Key(key) => key.handle(account), } } } @@ -72,6 +133,29 @@ impl OpenPGP { } } +impl Key { + pub fn handle(&self, account: DerivationIndex) -> Result<()> { + let mut client = keyforkd_client::Client::discover_socket()?; + let path = DerivationPath::default() + .chain_push(self.slug.0.clone()) + .chain_push(account); + let request = DerivationRequest::new(self.derivation_algorithm.clone(), &path); + let request = Request::Derivation(request); + let derived_key: DerivationResponse = client.request(&request)?.try_into()?; + + let formatted = match self.format { + KeyFormat::Hex => smex::encode(derived_key.data), + KeyFormat::Base64 => { + use base64::prelude::*; + BASE64_STANDARD.encode(derived_key.data) + } + }; + + eprintln!("{formatted}"); + Ok(()) + } +} + #[derive(Parser, Debug, Clone)] pub struct Derive { #[command(subcommand)]