keyfork-shard: refactor key discovery mechanisms

This commit is contained in:
Ryan Heywood 2024-02-18 20:19:29 -05:00
parent 2541d49fb8
commit b75d45876a
Signed by: ryan
GPG Key ID: 8E401478A3FBEF72
9 changed files with 77 additions and 83 deletions

View File

@ -7,7 +7,7 @@ use std::{
process::ExitCode, process::ExitCode,
}; };
use keyfork_shard::{Format, openpgp::OpenPGP}; use keyfork_shard::{openpgp::OpenPGP, Format};
type Result<T, E = Box<dyn std::error::Error>> = std::result::Result<T, E>; type Result<T, E = Box<dyn std::error::Error>> = std::result::Result<T, E>;
@ -32,7 +32,7 @@ fn run() -> Result<()> {
}; };
let openpgp = OpenPGP; let openpgp = OpenPGP;
let bytes = openpgp.decrypt_all_shards_to_secret(key_discovery, messages_file)?; let bytes = openpgp.decrypt_all_shards_to_secret(key_discovery.as_deref(), messages_file)?;
print!("{}", smex::encode(&bytes)); print!("{}", smex::encode(&bytes));
Ok(()) Ok(())

View File

@ -33,7 +33,7 @@ fn run() -> Result<()> {
let openpgp = OpenPGP; let openpgp = OpenPGP;
openpgp.decrypt_one_shard_for_transport(key_discovery, messages_file)?; openpgp.decrypt_one_shard_for_transport(key_discovery.as_deref(), messages_file)?;
Ok(()) Ok(())
} }

View File

@ -52,7 +52,7 @@ fn run() -> Result<()> {
let openpgp = OpenPGP; let openpgp = OpenPGP;
openpgp.shard_and_encrypt(threshold, max, &input, key_discovery, std::io::stdout())?; openpgp.shard_and_encrypt(threshold, max, &input, key_discovery.as_path(), std::io::stdout())?;
Ok(()) Ok(())
} }

View File

@ -1,9 +1,6 @@
#![doc = include_str!("../README.md")] #![doc = include_str!("../README.md")]
use std::{ use std::io::{stdin, stdout, Read, Write};
io::{stdin, stdout, Read, Write},
path::Path,
};
use aes_gcm::{ use aes_gcm::{
aead::{consts::U12, Aead, AeadCore, OsRng}, aead::{consts::U12, Aead, AeadCore, OsRng},
@ -25,6 +22,27 @@ const ENC_LEN: u8 = 4 * 16;
#[cfg(feature = "openpgp")] #[cfg(feature = "openpgp")]
pub mod openpgp; pub mod openpgp;
/// A trait to specify where keys can be discovered from, such as a Rust-native type or a path on
/// the filesystem that keys may be read from.
pub trait KeyDiscovery<F: Format + ?Sized> {
/// Discover public keys for the associated format.
///
/// # Errors
/// The method may return an error if public keys could not be loaded from the given discovery
/// mechanism. A discovery mechanism _must_ be able to detect public keys.
fn discover_public_keys(&self) -> Result<Vec<F::PublicKey>, F::Error>;
/// Discover private keys for the associated format.
///
/// # Errors
/// The method may return an error if private keys could not be loaded from the given
/// discovery mechanism. Keys may exist off-system (such as with smartcards), in which case the
/// PrivateKeyData type of the asssociated format should be either `()` (if the keys may never
/// exist on-system) or an empty container (such as an empty Vec); in either case, this method
/// _must not_ return an error if keys are accessible but can't be transferred into memory.
fn discover_private_keys(&self) -> Result<F::PrivateKeyData, F::Error>;
}
/// A format to use for splitting and combining secrets. /// A format to use for splitting and combining secrets.
pub trait Format { pub trait Format {
/// The error type returned from any failed operations. /// The error type returned from any failed operations.
@ -42,17 +60,6 @@ pub trait Format {
/// A type representing the parsed, but encrypted, Shard data. /// A type representing the parsed, but encrypted, Shard data.
type EncryptedData; type EncryptedData;
/// Parse the public key data from a readable type.
///
/// # Errors
/// The method may return an error if private key data could not be properly parsed from the
/// path.
/// occurred while parsing the public key data.
fn parse_public_key_data(
&self,
key_data_path: impl AsRef<Path>,
) -> Result<Vec<Self::PublicKey>, Self::Error>;
/// Derive a signer /// Derive a signer
fn derive_signing_key(&self, seed: &[u8]) -> Self::SigningKey; fn derive_signing_key(&self, seed: &[u8]) -> Self::SigningKey;
@ -83,17 +90,6 @@ pub trait Format {
signing_key: &mut Self::SigningKey, signing_key: &mut Self::SigningKey,
) -> Result<Self::EncryptedData, Self::Error>; ) -> Result<Self::EncryptedData, Self::Error>;
/// Parse the private key data from a readable type. The private key may not be accessible (it
/// may be hardware only, such as a smartcard), for which this method may return None.
///
/// # Errors
/// The method may return an error if private key data could not be properly parsed from the
/// path.
fn parse_private_key_data(
&self,
key_data_path: impl AsRef<Path>,
) -> Result<Self::PrivateKeyData, Self::Error>;
/// Parse the Shard file into a processable type. /// Parse the Shard file into a processable type.
/// ///
/// # Errors /// # Errors
@ -148,11 +144,11 @@ pub trait Format {
/// be combined into a secret. /// be combined into a secret.
fn decrypt_all_shards_to_secret( fn decrypt_all_shards_to_secret(
&self, &self,
private_key_data_path: Option<impl AsRef<Path>>, private_key_discovery: Option<impl KeyDiscovery<Self>>,
reader: impl Read + Send + Sync, reader: impl Read + Send + Sync,
) -> Result<Vec<u8>, Box<dyn std::error::Error>> { ) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
let private_keys = private_key_data_path let private_keys = private_key_discovery
.map(|p| self.parse_private_key_data(p)) .map(|p| p.discover_private_keys())
.transpose()?; .transpose()?;
let encrypted_messages = self.parse_shard_file(reader)?; let encrypted_messages = self.parse_shard_file(reader)?;
let (shares, threshold) = self.decrypt_all_shards(private_keys, &encrypted_messages)?; let (shares, threshold) = self.decrypt_all_shards(private_keys, &encrypted_messages)?;
@ -173,15 +169,15 @@ pub trait Format {
/// QR code; instead, a mnemonic prompt will be used. /// QR code; instead, a mnemonic prompt will be used.
fn decrypt_one_shard_for_transport( fn decrypt_one_shard_for_transport(
&self, &self,
private_key_data_path: Option<impl AsRef<Path>>, private_key_discovery: Option<impl KeyDiscovery<Self>>,
reader: impl Read + Send + Sync, reader: impl Read + Send + Sync,
) -> Result<(), Box<dyn std::error::Error>> { ) -> Result<(), Box<dyn std::error::Error>> {
let mut pm = Terminal::new(stdin(), stdout())?; let mut pm = Terminal::new(stdin(), stdout())?;
let wordlist = Wordlist::default(); let wordlist = Wordlist::default();
// parse input // parse input
let private_keys = private_key_data_path let private_keys = private_key_discovery
.map(|p| self.parse_private_key_data(p)) .map(|p| p.discover_private_keys())
.transpose()?; .transpose()?;
let encrypted_messages = self.parse_shard_file(reader)?; let encrypted_messages = self.parse_shard_file(reader)?;
@ -321,7 +317,7 @@ pub trait Format {
threshold: u8, threshold: u8,
max: u8, max: u8,
secret: &[u8], secret: &[u8],
public_key_data_path: impl AsRef<Path>, public_key_discovery: impl KeyDiscovery<Self>,
writer: impl Write + Send + Sync, writer: impl Write + Send + Sync,
) -> Result<(), Box<dyn std::error::Error>> { ) -> Result<(), Box<dyn std::error::Error>> {
let mut signing_key = self.derive_signing_key(secret); let mut signing_key = self.derive_signing_key(secret);
@ -329,7 +325,7 @@ pub trait Format {
let sharks = Sharks(threshold); let sharks = Sharks(threshold);
let dealer = sharks.dealer(secret); let dealer = sharks.dealer(secret);
let public_keys = self.parse_public_key_data(public_key_data_path)?; let public_keys = public_key_discovery.discover_public_keys()?;
assert!( assert!(
public_keys.len() < u8::MAX as usize, public_keys.len() < u8::MAX as usize,
"must have less than u8::MAX public keys" "must have less than u8::MAX public keys"

View File

@ -58,7 +58,7 @@ const SHARD_METADATA_OFFSET: usize = 2;
use super::{ use super::{
Format, InvalidData, SharksError, HUNK_VERSION, QRCODE_COULDNT_READ, QRCODE_ERROR, Format, InvalidData, SharksError, HUNK_VERSION, QRCODE_COULDNT_READ, QRCODE_ERROR,
QRCODE_PROMPT, QRCODE_TIMEOUT, QRCODE_PROMPT, QRCODE_TIMEOUT, KeyDiscovery
}; };
// 256 bit share is 49 bytes + some amount of hunk bytes, gives us reasonable padding // 256 bit share is 49 bytes + some amount of hunk bytes, gives us reasonable padding
@ -282,13 +282,6 @@ impl Format for OpenPGP {
type SigningKey = Cert; type SigningKey = Cert;
type EncryptedData = EncryptedMessage; type EncryptedData = EncryptedMessage;
fn parse_public_key_data(
&self,
key_data_path: impl AsRef<Path>,
) -> std::result::Result<Vec<Self::PublicKey>, Self::Error> {
Self::discover_certs(key_data_path)
}
/// Derive an OpenPGP Shard certificate from the given seed. /// Derive an OpenPGP Shard certificate from the given seed.
fn derive_signing_key(&self, seed: &[u8]) -> Self::SigningKey { fn derive_signing_key(&self, seed: &[u8]) -> Self::SigningKey {
let seed = VariableLengthSeed::new(seed); let seed = VariableLengthSeed::new(seed);
@ -449,13 +442,6 @@ impl Format for OpenPGP {
Ok(message) Ok(message)
} }
fn parse_private_key_data(
&self,
key_data_path: impl AsRef<Path>,
) -> std::result::Result<Self::PrivateKeyData, Self::Error> {
Self::discover_certs(key_data_path)
}
fn parse_shard_file( fn parse_shard_file(
&self, &self,
shard_file: impl Read + Send + Sync, shard_file: impl Read + Send + Sync,
@ -579,6 +565,26 @@ impl Format for OpenPGP {
} }
} }
impl KeyDiscovery<OpenPGP> for &Path {
fn discover_public_keys(&self) -> Result<Vec<<OpenPGP as Format>::PublicKey>> {
OpenPGP::discover_certs(self)
}
fn discover_private_keys(&self) -> Result<<OpenPGP as Format>::PrivateKeyData> {
todo!()
}
}
impl KeyDiscovery<OpenPGP> for &[Cert] {
fn discover_public_keys(&self) -> Result<Vec<<OpenPGP as Format>::PublicKey>> {
Ok(self.to_vec())
}
fn discover_private_keys(&self) -> Result<<OpenPGP as Format>::PrivateKeyData> {
Ok(self.to_vec())
}
}
/// Read all OpenPGP certificates in a path and return a [`Vec`] of them. Certificates are read /// Read all OpenPGP certificates in a path and return a [`Vec`] of them. Certificates are read
/// from a file, or from files one level deep in a directory. /// from a file, or from files one level deep in a directory.
/// ///

View File

@ -111,12 +111,10 @@ impl DecryptionHelper for &mut Keyring {
pkesk.recipient().is_wildcard() pkesk.recipient().is_wildcard()
|| cert.keys().any(|k| &k.keyid() == pkesk.recipient()) || cert.keys().any(|k| &k.keyid() == pkesk.recipient())
}) { }) {
#[allow(deprecated, clippy::map_flatten)]
let name = cert let name = cert
.userids() .userids()
.next() .next()
.map(|userid| userid.userid().name().transpose()) .and_then(|userid| userid.userid().name2().transpose())
.flatten()
.transpose() .transpose()
.ok() .ok()
.flatten(); .flatten();

View File

@ -37,7 +37,7 @@ impl RecoverSubcommands {
let openpgp = keyfork_shard::openpgp::OpenPGP; let openpgp = keyfork_shard::openpgp::OpenPGP;
// TODO: remove .clone() by making handle() consume self // TODO: remove .clone() by making handle() consume self
let seed = openpgp let seed = openpgp
.decrypt_all_shards_to_secret(key_discovery.clone(), content.as_bytes())?; .decrypt_all_shards_to_secret(key_discovery.as_deref(), content.as_bytes())?;
Ok(seed) Ok(seed)
} else { } else {
panic!("unknown format of shard file"); panic!("unknown format of shard file");

View File

@ -32,27 +32,23 @@ trait ShardExec {
&self, &self,
threshold: u8, threshold: u8,
max: u8, max: u8,
key_discovery: impl AsRef<Path>, key_discovery: &Path,
secret: &[u8], secret: &[u8],
output: &mut (impl Write + Send + Sync), output: &mut (impl Write + Send + Sync),
) -> Result<(), Box<dyn std::error::Error>>; ) -> Result<(), Box<dyn std::error::Error>>;
fn combine<T>( fn combine(
&self, &self,
key_discovery: Option<T>, key_discovery: Option<&Path>,
input: impl Read + Send + Sync, input: impl Read + Send + Sync,
output: &mut impl Write, output: &mut impl Write,
) -> Result<(), Box<dyn std::error::Error>> ) -> Result<(), Box<dyn std::error::Error>>;
where
T: AsRef<Path>;
fn decrypt<T>( fn decrypt(
&self, &self,
key_discovery: Option<T>, key_discovery: Option<&Path>,
input: impl Read + Send + Sync, input: impl Read + Send + Sync,
) -> Result<(), Box<dyn std::error::Error>> ) -> Result<(), Box<dyn std::error::Error>>;
where
T: AsRef<Path>;
} }
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
@ -63,7 +59,7 @@ impl ShardExec for OpenPGP {
&self, &self,
threshold: u8, threshold: u8,
max: u8, max: u8,
key_discovery: impl AsRef<Path>, key_discovery: &Path,
secret: &[u8], secret: &[u8],
output: &mut (impl Write + Send + Sync), output: &mut (impl Write + Send + Sync),
) -> Result<(), Box<dyn std::error::Error>> { ) -> Result<(), Box<dyn std::error::Error>> {
@ -71,14 +67,12 @@ impl ShardExec for OpenPGP {
opgp.shard_and_encrypt(threshold, max, secret, key_discovery, output) opgp.shard_and_encrypt(threshold, max, secret, key_discovery, output)
} }
fn combine<T>( fn combine(
&self, &self,
key_discovery: Option<T>, key_discovery: Option<&Path>,
input: impl Read + Send + Sync, input: impl Read + Send + Sync,
output: &mut impl Write, output: &mut impl Write,
) -> Result<(), Box<dyn std::error::Error>> ) -> Result<(), Box<dyn std::error::Error>>
where
T: AsRef<Path>,
{ {
let openpgp = keyfork_shard::openpgp::OpenPGP; let openpgp = keyfork_shard::openpgp::OpenPGP;
let bytes = openpgp.decrypt_all_shards_to_secret(key_discovery, input)?; let bytes = openpgp.decrypt_all_shards_to_secret(key_discovery, input)?;
@ -87,13 +81,11 @@ impl ShardExec for OpenPGP {
Ok(()) Ok(())
} }
fn decrypt<T>( fn decrypt(
&self, &self,
key_discovery: Option<T>, key_discovery: Option<&Path>,
input: impl Read + Send + Sync, input: impl Read + Send + Sync,
) -> Result<(), Box<dyn std::error::Error>> ) -> Result<(), Box<dyn std::error::Error>>
where
T: AsRef<Path>,
{ {
let openpgp = keyfork_shard::openpgp::OpenPGP; let openpgp = keyfork_shard::openpgp::OpenPGP;
openpgp.decrypt_one_shard_for_transport(key_discovery, input)?; openpgp.decrypt_one_shard_for_transport(key_discovery, input)?;
@ -189,7 +181,7 @@ impl ShardSubcommands {
match format { match format {
Some(Format::OpenPGP(o)) => { Some(Format::OpenPGP(o)) => {
o.decrypt(key_discovery.as_ref(), shard_content.as_bytes()) o.decrypt(key_discovery.as_deref(), shard_content.as_bytes())
} }
Some(Format::P256(_p)) => todo!(), Some(Format::P256(_p)) => todo!(),
None => panic!("{COULD_NOT_DETERMINE_FORMAT}"), None => panic!("{COULD_NOT_DETERMINE_FORMAT}"),
@ -206,7 +198,7 @@ impl ShardSubcommands {
match format { match format {
Some(Format::OpenPGP(o)) => o.combine( Some(Format::OpenPGP(o)) => o.combine(
key_discovery.as_ref(), key_discovery.as_deref(),
shard_content.as_bytes(), shard_content.as_bytes(),
&mut stdout, &mut stdout,
), ),

View File

@ -15,6 +15,8 @@ use keyfork_prompt::{
Message, PromptHandler, Terminal, Message, PromptHandler, Terminal,
}; };
use keyfork_shard::{Format, openpgp::OpenPGP};
#[derive(thiserror::Error, Debug)] #[derive(thiserror::Error, Debug)]
#[error("Invalid PIN length: {0}")] #[error("Invalid PIN length: {0}")]
pub struct PinLength(usize); pub struct PinLength(usize);
@ -163,13 +165,13 @@ fn generate_shard_secret(
certs.push(cert); certs.push(cert);
} }
let opgp = OpenPGP;
if let Some(output_file) = output_file { if let Some(output_file) = output_file {
let output = File::create(output_file)?; let output = File::create(output_file)?;
#[allow(deprecated)] opgp.shard_and_encrypt(threshold, certs.len() as u8, &seed, &certs[..], output)?;
keyfork_shard::openpgp::split(threshold, certs, &seed, output)?;
} else { } else {
#[allow(deprecated)] opgp.shard_and_encrypt(threshold, certs.len() as u8, &seed, &certs[..], std::io::stdout())?;
keyfork_shard::openpgp::split(threshold, certs, &seed, std::io::stdout())?;
} }
Ok(()) Ok(())
} }