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,
};
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>;
@ -32,7 +32,7 @@ fn run() -> Result<()> {
};
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));
Ok(())

View File

@ -33,7 +33,7 @@ fn run() -> Result<()> {
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(())
}

View File

@ -52,7 +52,7 @@ fn run() -> Result<()> {
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(())
}

View File

@ -1,9 +1,6 @@
#![doc = include_str!("../README.md")]
use std::{
io::{stdin, stdout, Read, Write},
path::Path,
};
use std::io::{stdin, stdout, Read, Write};
use aes_gcm::{
aead::{consts::U12, Aead, AeadCore, OsRng},
@ -25,6 +22,27 @@ const ENC_LEN: u8 = 4 * 16;
#[cfg(feature = "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.
pub trait Format {
/// The error type returned from any failed operations.
@ -42,17 +60,6 @@ pub trait Format {
/// A type representing the parsed, but encrypted, Shard data.
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
fn derive_signing_key(&self, seed: &[u8]) -> Self::SigningKey;
@ -83,17 +90,6 @@ pub trait Format {
signing_key: &mut Self::SigningKey,
) -> 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.
///
/// # Errors
@ -148,11 +144,11 @@ pub trait Format {
/// be combined into a secret.
fn decrypt_all_shards_to_secret(
&self,
private_key_data_path: Option<impl AsRef<Path>>,
private_key_discovery: Option<impl KeyDiscovery<Self>>,
reader: impl Read + Send + Sync,
) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
let private_keys = private_key_data_path
.map(|p| self.parse_private_key_data(p))
let private_keys = private_key_discovery
.map(|p| p.discover_private_keys())
.transpose()?;
let encrypted_messages = self.parse_shard_file(reader)?;
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.
fn decrypt_one_shard_for_transport(
&self,
private_key_data_path: Option<impl AsRef<Path>>,
private_key_discovery: Option<impl KeyDiscovery<Self>>,
reader: impl Read + Send + Sync,
) -> Result<(), Box<dyn std::error::Error>> {
let mut pm = Terminal::new(stdin(), stdout())?;
let wordlist = Wordlist::default();
// parse input
let private_keys = private_key_data_path
.map(|p| self.parse_private_key_data(p))
let private_keys = private_key_discovery
.map(|p| p.discover_private_keys())
.transpose()?;
let encrypted_messages = self.parse_shard_file(reader)?;
@ -321,7 +317,7 @@ pub trait Format {
threshold: u8,
max: u8,
secret: &[u8],
public_key_data_path: impl AsRef<Path>,
public_key_discovery: impl KeyDiscovery<Self>,
writer: impl Write + Send + Sync,
) -> Result<(), Box<dyn std::error::Error>> {
let mut signing_key = self.derive_signing_key(secret);
@ -329,7 +325,7 @@ pub trait Format {
let sharks = Sharks(threshold);
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!(
public_keys.len() < u8::MAX as usize,
"must have less than u8::MAX public keys"

View File

@ -58,7 +58,7 @@ const SHARD_METADATA_OFFSET: usize = 2;
use super::{
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
@ -282,13 +282,6 @@ impl Format for OpenPGP {
type SigningKey = Cert;
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.
fn derive_signing_key(&self, seed: &[u8]) -> Self::SigningKey {
let seed = VariableLengthSeed::new(seed);
@ -449,13 +442,6 @@ impl Format for OpenPGP {
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(
&self,
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
/// 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()
|| cert.keys().any(|k| &k.keyid() == pkesk.recipient())
}) {
#[allow(deprecated, clippy::map_flatten)]
let name = cert
.userids()
.next()
.map(|userid| userid.userid().name().transpose())
.flatten()
.and_then(|userid| userid.userid().name2().transpose())
.transpose()
.ok()
.flatten();

View File

@ -37,7 +37,7 @@ impl RecoverSubcommands {
let openpgp = keyfork_shard::openpgp::OpenPGP;
// TODO: remove .clone() by making handle() consume self
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)
} else {
panic!("unknown format of shard file");

View File

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

View File

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