From 7da9738d52c050c5090e922ec1f82eacf51fbff4 Mon Sep 17 00:00:00 2001 From: ryan Date: Thu, 19 Oct 2023 19:20:10 -0500 Subject: [PATCH] keyfork: add `keyfork shard` --- Cargo.lock | 1 + keyfork/Cargo.toml | 1 + keyfork/src/cli/mod.rs | 8 ++ keyfork/src/cli/shard.rs | 186 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 196 insertions(+) create mode 100644 keyfork/src/cli/shard.rs diff --git a/Cargo.lock b/Cargo.lock index bd215d6..83f9db2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -882,6 +882,7 @@ dependencies = [ "clap", "keyfork-mnemonic-util", "keyfork-plumbing", + "keyfork-shard", "smex", "thiserror", ] diff --git a/keyfork/Cargo.toml b/keyfork/Cargo.toml index fd4731f..b415787 100644 --- a/keyfork/Cargo.toml +++ b/keyfork/Cargo.toml @@ -11,3 +11,4 @@ clap = { version = "4.4.2", features = ["derive", "env"] } thiserror = "1.0.48" smex = { version = "0.1.0", path = "../smex" } keyfork-plumbing = { version = "0.1.0", path = "../keyfork-plumbing" } +keyfork-shard = { version = "0.1.0", path = "../keyfork-shard" } diff --git a/keyfork/src/cli/mod.rs b/keyfork/src/cli/mod.rs index bdbe8a5..1c3ea6f 100644 --- a/keyfork/src/cli/mod.rs +++ b/keyfork/src/cli/mod.rs @@ -1,6 +1,7 @@ use clap::{Parser, Subcommand}; mod mnemonic; +mod shard; /// The Kitchen Sink of Entropy. #[derive(Parser, Clone, Debug)] @@ -16,6 +17,9 @@ pub enum KeyforkCommands { /// Mnemonic generation and persistence utilities. Mnemonic(mnemonic::Mnemonic), + /// Secret sharing utilities. + Shard(shard::Shard), + /// Keyforkd background daemon to manage seed creation. Daemon, } @@ -27,6 +31,10 @@ impl KeyforkCommands { let response = m.command.handle(m, keyfork)?; println!("{response}"); } + KeyforkCommands::Shard(s) => { + // TODO: When actually fleshing out, this takes a `Read` and a `Write` + s.command.handle(s, keyfork)?; + } KeyforkCommands::Daemon => { todo!() } diff --git a/keyfork/src/cli/shard.rs b/keyfork/src/cli/shard.rs new file mode 100644 index 0000000..cea6e9c --- /dev/null +++ b/keyfork/src/cli/shard.rs @@ -0,0 +1,186 @@ +use super::Keyfork; +use clap::{builder::PossibleValue, Parser, Subcommand, ValueEnum}; +use std::{ + io::{stdin, stdout, BufRead, BufReader, Read, Write}, + path::{Path, PathBuf}, +}; + +#[derive(Debug, Clone)] +enum Format { + OpenPGP(OpenPGP), + P256(P256), +} + +impl ValueEnum for Format { + fn value_variants<'a>() -> &'a [Self] { + &[Self::OpenPGP(OpenPGP), Self::P256(P256)] + } + + fn to_possible_value(&self) -> Option { + Some(match self { + Format::OpenPGP(_) => PossibleValue::new("openpgp"), + Format::P256(_) => PossibleValue::new("p256"), + }) + } +} + +trait ShardExec { + fn split( + &self, + threshold: u8, + max: u8, + key_discovery: impl AsRef, + secret: &[u8], + output: &mut impl Write, + ) -> Result<(), Box>; + + fn combine( + &self, + threshold: u8, + key_discovery: Option, + input: impl Read + Send + Sync, + output: &mut impl Write, + ) -> Result<(), Box> + where + T: AsRef; +} + +#[derive(Clone, Debug)] +struct OpenPGP; + +impl ShardExec for OpenPGP { + fn split( + &self, + threshold: u8, + max: u8, + key_discovery: impl AsRef, + secret: &[u8], + output: &mut impl Write, + ) -> Result<(), Box> { + // Get certs and input + let certs = keyfork_shard::openpgp::discover_certs(key_discovery.as_ref())?; + assert_eq!( + certs.len(), + max.into(), + "cert count {} != max {max}", + certs.len() + ); + keyfork_shard::openpgp::split(threshold, certs, secret, output) + } + + fn combine( + &self, + threshold: u8, + key_discovery: Option, + input: impl Read + Send + Sync, + output: &mut impl Write, + ) -> Result<(), Box> + where + T: AsRef, + { + let certs = key_discovery + .map(|kd| keyfork_shard::openpgp::discover_certs(kd.as_ref())) + .transpose()? + .unwrap_or(vec![]); + + let mut encrypted_messages = keyfork_shard::openpgp::parse_messages(input)?; + let encrypted_metadata = encrypted_messages + .pop_front() + .expect("any pgp encrypted message"); + + keyfork_shard::openpgp::combine( + threshold, + certs, + encrypted_metadata, + encrypted_messages.into(), + output, + )?; + + Ok(()) + } +} + +#[derive(Clone, Debug)] +struct P256; + +#[derive(Subcommand, Clone, Debug)] +pub enum ShardSubcommands { + /// Split a secret into multiple shares, using Shamir's Secret Sharing. + Split { + /// The amount of shares required to recombine a secret. + #[arg(long)] + threshold: u8, + + /// The total amount of shares to generate. + #[arg(long)] + max: u8, + + /// The path to discover public keys from. + key_discovery: PathBuf, + }, + + /// Combine multiple shares into a secret + Combine { + /// The amount of sharesr equired to recombine a secret. + #[arg(long)] + threshold: u8, + + /// The path to discover private keys from. + key_discovery: Option, + }, +} + +impl ShardSubcommands { + pub fn handle( + &self, + shard: &Shard, + _keyfork: &Keyfork, + ) -> Result<(), Box> { + let stdin = stdin(); + let mut stdout = stdout(); + match self { + ShardSubcommands::Split { + threshold, + max, + key_discovery, + } => { + assert!(threshold <= max, "threshold {threshold} <= max {max}"); + let mut input = BufReader::new(stdin); + let mut hex_line = String::new(); + input.read_line(&mut hex_line)?; + let secret = smex::decode(hex_line.trim())?; + match &shard.format { + Some(Format::OpenPGP(o)) => { + o.split(*threshold, *max, key_discovery, &secret, &mut stdout) + } + Some(Format::P256(_p)) => { + todo!() + } + None => panic!("--format was not given"), + } + } + ShardSubcommands::Combine { + threshold, + key_discovery, + } => match &shard.format { + Some(Format::OpenPGP(o)) => { + o.combine(*threshold, key_discovery.as_ref(), stdin, &mut stdout) + } + Some(Format::P256(_p)) => { + todo!() + } + None => panic!("--format was not given"), + }, + } + } +} + +#[derive(Parser, Debug, Clone)] +pub struct Shard { + /// Which format to use for encoding/encrypting and decoding/decrypting shares. + #[arg(long, value_enum, global = true)] + format: Option, + + #[command(subcommand)] + pub command: ShardSubcommands, +}