keyfork: add `keyfork shard`

This commit is contained in:
Ryan Heywood 2023-10-19 19:20:10 -05:00
parent a72bfaecec
commit 7da9738d52
Signed by: ryan
GPG Key ID: 8E401478A3FBEF72
4 changed files with 196 additions and 0 deletions

1
Cargo.lock generated
View File

@ -882,6 +882,7 @@ dependencies = [
"clap",
"keyfork-mnemonic-util",
"keyfork-plumbing",
"keyfork-shard",
"smex",
"thiserror",
]

View File

@ -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" }

View File

@ -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!()
}

186
keyfork/src/cli/shard.rs Normal file
View File

@ -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<PossibleValue> {
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<Path>,
secret: &[u8],
output: &mut impl Write,
) -> Result<(), Box<dyn std::error::Error>>;
fn combine<T>(
&self,
threshold: u8,
key_discovery: Option<T>,
input: impl Read + Send + Sync,
output: &mut impl Write,
) -> Result<(), Box<dyn std::error::Error>>
where
T: AsRef<Path>;
}
#[derive(Clone, Debug)]
struct OpenPGP;
impl ShardExec for OpenPGP {
fn split(
&self,
threshold: u8,
max: u8,
key_discovery: impl AsRef<Path>,
secret: &[u8],
output: &mut impl Write,
) -> Result<(), Box<dyn std::error::Error>> {
// 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<T>(
&self,
threshold: u8,
key_discovery: Option<T>,
input: impl Read + Send + Sync,
output: &mut impl Write,
) -> Result<(), Box<dyn std::error::Error>>
where
T: AsRef<Path>,
{
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<PathBuf>,
},
}
impl ShardSubcommands {
pub fn handle(
&self,
shard: &Shard,
_keyfork: &Keyfork,
) -> Result<(), Box<dyn std::error::Error>> {
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<Format>,
#[command(subcommand)]
pub command: ShardSubcommands,
}