Compare commits

...

5 Commits

9 changed files with 801 additions and 188 deletions

View File

@ -7,51 +7,32 @@ use std::{
process::ExitCode,
};
use keyfork_shard::openpgp::{combine, discover_certs, openpgp::Cert, parse_messages};
use keyfork_shard::{openpgp::OpenPGP, Format};
type Result<T, E = Box<dyn std::error::Error>> = std::result::Result<T, E>;
fn validate(
shard: impl AsRef<Path>,
key_discovery: Option<&str>,
) -> Result<(File, Vec<Cert>)> {
) -> Result<(File, Option<PathBuf>)> {
let key_discovery = key_discovery.map(PathBuf::from);
key_discovery.as_ref().map(std::fs::metadata).transpose()?;
// Load certs from path
let certs = key_discovery
.map(discover_certs)
.transpose()?
.unwrap_or(vec![]);
Ok((File::open(shard)?, certs))
Ok((File::open(shard)?, key_discovery))
}
fn run() -> Result<()> {
let mut args = env::args();
let program_name = args.next().expect("program name");
let args = args.collect::<Vec<_>>();
let (messages_file, cert_list) = match args.as_slice() {
let (messages_file, key_discovery) = match args.as_slice() {
[shard, key_discovery] => validate(shard, Some(key_discovery))?,
[shard] => validate(shard, None)?,
_ => panic!("Usage: {program_name} <shard> [key_discovery]"),
};
let mut encrypted_messages = parse_messages(messages_file)?;
let encrypted_metadata = encrypted_messages
.pop_front()
.expect("any pgp encrypted message");
let mut bytes = vec![];
combine(
cert_list,
&encrypted_metadata,
encrypted_messages.into(),
&mut bytes,
)?;
let openpgp = OpenPGP;
let bytes = openpgp.decrypt_all_shards_to_secret(key_discovery.as_deref(), messages_file)?;
print!("{}", smex::encode(&bytes));
Ok(())

View File

@ -7,47 +7,33 @@ use std::{
process::ExitCode,
};
use keyfork_shard::openpgp::{decrypt, discover_certs, openpgp::Cert, parse_messages};
use keyfork_shard::{Format, openpgp::OpenPGP};
type Result<T, E = Box<dyn std::error::Error>> = std::result::Result<T, E>;
fn validate<'a>(
messages_file: impl AsRef<Path>,
key_discovery: impl Into<Option<&'a str>>,
) -> Result<(File, Vec<Cert>)> {
let key_discovery = key_discovery.into().map(PathBuf::from);
fn validate(
shard: impl AsRef<Path>,
key_discovery: Option<&str>,
) -> Result<(File, Option<PathBuf>)> {
let key_discovery = key_discovery.map(PathBuf::from);
key_discovery.as_ref().map(std::fs::metadata).transpose()?;
// Load certs from path
let certs = key_discovery
.map(discover_certs)
.transpose()?
.unwrap_or(vec![]);
Ok((File::open(messages_file)?, certs))
Ok((File::open(shard)?, key_discovery))
}
fn run() -> Result<()> {
let mut args = env::args();
let program_name = args.next().expect("program name");
let args = args.collect::<Vec<_>>();
let (messages_file, cert_list) = match args.as_slice() {
[messages_file, key_discovery] => validate(messages_file, key_discovery.as_str())?,
[messages_file] => validate(messages_file, None)?,
_ => panic!("Usage: {program_name} messages_file [key_discovery]"),
let (messages_file, key_discovery) = match args.as_slice() {
[shard, key_discovery] => validate(shard, Some(key_discovery))?,
[shard] => validate(shard, None)?,
_ => panic!("Usage: {program_name} <shard> [key_discovery]"),
};
let mut encrypted_messages = parse_messages(messages_file)?;
let openpgp = OpenPGP;
let encrypted_metadata = encrypted_messages
.pop_front()
.expect("any pgp encrypted message");
decrypt(
&cert_list,
&encrypted_metadata,
encrypted_messages.make_contiguous(),
)?;
openpgp.decrypt_one_shard_for_transport(key_discovery.as_deref(), messages_file)?;
Ok(())
}

View File

@ -2,14 +2,12 @@
use std::{env, path::PathBuf, process::ExitCode, str::FromStr};
use keyfork_shard::openpgp::{discover_certs, openpgp::Cert, split};
use keyfork_shard::{Format, openpgp::OpenPGP};
#[derive(Clone, Debug)]
enum Error {
Usage(String),
Input,
Threshold(u8, u8),
InvalidCertCount(usize, u8),
}
impl std::fmt::Display for Error {
@ -19,15 +17,6 @@ impl std::fmt::Display for Error {
write!(f, "Usage: {program_name} threshold max key_discovery")
}
Error::Input => f.write_str("Expected hex encoded input"),
Error::Threshold(threshold, max) => {
write!(
f,
"Invalid threshold: 0 < threshold {threshold} <= max {max} < 256"
)
}
Error::InvalidCertCount(count, max) => {
write!(f, "Invalid cert count: count {count} != max {max}")
}
}
}
}
@ -36,31 +25,20 @@ impl std::error::Error for Error {}
type Result<T, E = Box<dyn std::error::Error>> = std::result::Result<T, E>;
fn validate(threshold: &str, max: &str, key_discovery: &str) -> Result<(u8, Vec<Cert>)> {
fn validate(threshold: &str, max: &str, key_discovery: &str) -> Result<(u8, u8, PathBuf)> {
let threshold = u8::from_str(threshold)?;
let max = u8::from_str(max)?;
let key_discovery = PathBuf::from(key_discovery);
if threshold > max {
return Err(Error::Threshold(threshold, max).into());
}
// Verify path exists
std::fs::metadata(&key_discovery)?;
// Load certs from path
let certs = discover_certs(key_discovery)?;
if certs.len() != max.into() {
return Err(Error::InvalidCertCount(certs.len(), max).into());
}
Ok((threshold, certs))
Ok((threshold, max, key_discovery))
}
fn run() -> Result<()> {
let mut args = env::args();
let program_name = args.next().expect("program name");
let args = args.collect::<Vec<_>>();
let (threshold, cert_list) = match args.as_slice() {
let (threshold, max, key_discovery) = match args.as_slice() {
[threshold, max, key_discovery] => validate(threshold, max, key_discovery)?,
_ => return Err(Error::Usage(program_name).into()),
};
@ -72,8 +50,9 @@ fn run() -> Result<()> {
smex::decode(&line?)?
};
split(threshold, cert_list, &input, std::io::stdout())?;
let openpgp = OpenPGP;
openpgp.shard_and_encrypt(threshold, max, &input, key_discovery.as_path(), std::io::stdout())?;
Ok(())
}

View File

@ -1,10 +1,10 @@
#![doc = include_str!("../README.md")]
use std::io::{stdin, stdout, Write};
use std::io::{stdin, stdout, Read, Write};
use aes_gcm::{
aead::{Aead, AeadCore, OsRng},
Aes256Gcm, KeyInit,
aead::{consts::U12, Aead, AeadCore, OsRng},
Aes256Gcm, KeyInit, Nonce,
};
use hkdf::Hkdf;
use keyfork_mnemonic_util::{Mnemonic, Wordlist};
@ -16,9 +16,341 @@ use sha2::Sha256;
use sharks::{Share, Sharks};
use x25519_dalek::{EphemeralSecret, PublicKey};
// 256 bit share encrypted is 49 bytes, couple more bytes before we reach max size
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.
type Error: std::error::Error + 'static;
/// A type encapsulating a single public key recipient.
type PublicKey;
/// A type encapsulating the private key recipients of shards.
type PrivateKeyData;
/// A type representing a Signer derived from the secret.
type SigningKey;
/// A type representing the parsed, but encrypted, Shard data.
type EncryptedData;
/// Derive a signer
fn derive_signing_key(&self, seed: &[u8]) -> Self::SigningKey;
/// Format a header containing necessary metadata. Such metadata contains a version byte, a
/// threshold byte, a public version of the [`Format::SigningKey`], and the public keys used to
/// encrypt shards. The public keys must be kept _in order_ to the encrypted shards. Keyfork
/// will use the same key_data for both, ensuring an iteration of this method will match with
/// iterations in methods called later.
///
/// # Errors
/// The method may return an error if encryption to any of the public keys fails.
fn format_encrypted_header(
&self,
signing_key: &Self::SigningKey,
key_data: &[Self::PublicKey],
threshold: u8,
) -> Result<Self::EncryptedData, Self::Error>;
/// Format a shard encrypted to the given public key, signing with the private key.
///
/// # Errors
/// The method may return an error if the public key used to encrypt the shard is unsuitable
/// for encryption, or if an error occurs while encrypting.
fn encrypt_shard(
&self,
shard: &[u8],
public_key: &Self::PublicKey,
signing_key: &mut Self::SigningKey,
) -> Result<Self::EncryptedData, Self::Error>;
/// Parse the Shard file into a processable type.
///
/// # Errors
/// The method may return an error if the Shard file could not be read from or if the Shard
/// file could not be properly parsed.
fn parse_shard_file(
&self,
shard_file: impl Read + Send + Sync,
) -> Result<Vec<Self::EncryptedData>, Self::Error>;
/// Write the Shard data to a Shard file.
///
/// # Errors
/// The method may return an error if the Shard data could not be properly serialized or if the
/// Shard file could not be written to.
fn format_shard_file(
&self,
encrypted_data: &[Self::EncryptedData],
shard_file: impl Write + Send + Sync,
) -> Result<(), Self::Error>;
/// Decrypt shares and associated metadata from a readable input. For the current version of
/// Keyfork, the only associated metadata is a u8 representing the threshold to combine
/// secrets.
///
/// # Errors
/// The method may return an error if the shardfile couldn't be read from, if all shards
/// could not be decrypted, or if a shard could not be parsed from the decrypted data.
fn decrypt_all_shards(
&self,
private_keys: Option<Self::PrivateKeyData>,
encrypted_messages: &[Self::EncryptedData],
) -> Result<(Vec<Share>, u8), Self::Error>;
/// Decrypt a single share and associated metadata from a reaable input. For the current
/// version of Keyfork, the only associated metadata is a u8 representing the threshold to
/// combine secrets.
///
/// # Errors
/// The method may return an error if the shardfile couldn't be read from, if a shard could not
/// be decrypted, or if a shard could not be parsed from the decrypted data.
fn decrypt_one_shard(
&self,
private_keys: Option<Self::PrivateKeyData>,
encrypted_data: &[Self::EncryptedData],
) -> Result<(Share, u8), Self::Error>;
/// Decrypt multiple shares and combine them to recreate a secret.
///
/// # Errors
/// The method may return an error if the shares can't be decrypted or if the shares can't
/// be combined into a secret.
fn decrypt_all_shards_to_secret(
&self,
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_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)?;
let secret = Sharks(threshold)
.recover(&shares)
.map_err(|e| SharksError::CombineShare(e.to_string()))?;
Ok(secret)
}
/// Establish an AES-256-GCM transport key using ECDH, decrypt a single shard, and encrypt the
/// shard to the AES key.
///
/// # Errors
/// The method may return an error if a share can't be decrypted. The method will not return an
/// error if the camera is inaccessible or if a hardware error is encountered while scanning a
/// QR code; instead, a mnemonic prompt will be used.
fn decrypt_one_shard_for_transport(
&self,
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_discovery
.map(|p| p.discover_private_keys())
.transpose()?;
let encrypted_messages = self.parse_shard_file(reader)?;
// establish AES-256-GCM key via ECDH
let mut nonce_data: Option<[u8; 12]> = None;
let mut pubkey_data: Option<[u8; 32]> = None;
// receive remote data via scanning QR code from camera
#[cfg(feature = "qrcode")]
{
pm.prompt_message(PromptMessage::Text(QRCODE_PROMPT.to_string()))?;
if let Ok(Some(hex)) =
keyfork_qrcode::scan_camera(std::time::Duration::from_secs(30), 0)
{
let decoded_data = smex::decode(&hex)?;
nonce_data = Some(decoded_data[..12].try_into().map_err(|_| InvalidData)?);
pubkey_data = Some(decoded_data[12..].try_into().map_err(|_| InvalidData)?)
} else {
pm.prompt_message(PromptMessage::Text(QRCODE_ERROR.to_string()))?;
};
}
// if QR code scanning failed or was unavailable, read from a set of mnemonics
let (nonce, their_pubkey) = match (nonce_data, pubkey_data) {
(Some(nonce), Some(pubkey)) => (nonce, pubkey),
_ => {
let validator = MnemonicSetValidator {
word_lengths: [9, 24],
};
let [nonce_mnemonic, pubkey_mnemonic] = pm.prompt_validated_wordlist(
QRCODE_COULDNT_READ,
&wordlist,
3,
validator.to_fn(),
)?;
let nonce = nonce_mnemonic
.as_bytes()
.try_into()
.map_err(|_| InvalidData)?;
let pubkey = pubkey_mnemonic
.as_bytes()
.try_into()
.map_err(|_| InvalidData)?;
(nonce, pubkey)
}
};
// create our shared key
let our_key = EphemeralSecret::random();
let our_pubkey_mnemonic =
Mnemonic::from_entropy(PublicKey::from(&our_key).as_bytes(), Default::default())?;
let shared_secret = our_key
.diffie_hellman(&PublicKey::from(their_pubkey))
.to_bytes();
let hkdf = Hkdf::<Sha256>::new(None, &shared_secret);
let mut hkdf_output = [0u8; 256 / 8];
hkdf.expand(&[], &mut hkdf_output)?;
let shared_key = Aes256Gcm::new_from_slice(&hkdf_output)?;
// decrypt a single shard and create the payload
let (share, threshold) = self.decrypt_one_shard(private_keys, &encrypted_messages)?;
let mut payload = Vec::from(&share);
payload.insert(0, HUNK_VERSION);
payload.insert(1, threshold);
assert!(
payload.len() <= ENC_LEN as usize,
"invalid share length (too long, max {ENC_LEN} bytes)"
);
// encrypt data
let nonce = Nonce::<U12>::from_slice(&nonce);
let payload_bytes = shared_key.encrypt(nonce, payload.as_slice())?;
// convert data to a static-size payload
// NOTE: Padding length is less than u8::MAX because ENC_LEN < u8::MAX
#[allow(clippy::assertions_on_constants)]
{
assert!(ENC_LEN < u8::MAX, "padding byte can be u8");
}
#[allow(clippy::cast_possible_truncation)]
let mut out_bytes = [payload_bytes.len() as u8; ENC_LEN as usize];
assert!(
payload_bytes.len() < out_bytes.len(),
"encrypted payload larger than acceptable limit"
);
out_bytes[..payload_bytes.len()].clone_from_slice(&payload_bytes);
// NOTE: This previously used a single repeated value as the padding byte, but resulted in
// difficulty when entering in prompts manually, as one's place could be lost due to
// repeated keywords. This is resolved below by having sequentially increasing numbers up to
// but not including the last byte.
#[allow(clippy::cast_possible_truncation)]
for (i, byte) in (out_bytes[payload_bytes.len()..(ENC_LEN as usize - 1)])
.iter_mut()
.enumerate()
{
*byte = (i % u8::MAX as usize) as u8;
}
// safety: size of out_bytes is constant and always % 4 == 0
let payload_mnemonic =
unsafe { Mnemonic::from_raw_entropy(&out_bytes, Default::default()) };
#[cfg(feature = "qrcode")]
{
use keyfork_qrcode::{qrencode, ErrorCorrection};
let mut qrcode_data = our_pubkey_mnemonic.to_bytes();
qrcode_data.extend(payload_mnemonic.as_bytes());
if let Ok(qrcode) = qrencode(&smex::encode(&qrcode_data), ErrorCorrection::Highest) {
pm.prompt_message(PromptMessage::Text(
concat!(
"A QR code will be displayed after this prompt. ",
"Send the QR code back to the operator combining the shards. ",
"Nobody else should scan this QR code."
)
.to_string(),
))?;
pm.prompt_message(PromptMessage::Data(qrcode))?;
}
}
pm.prompt_message(PromptMessage::Text(format!(
"Upon request, these words should be sent: {our_pubkey_mnemonic} {payload_mnemonic}"
)))?;
Ok(())
}
/// Split a secret into a shard for every shard in keys, with the given Shamir's Secret Sharing
/// threshold.
///
/// # Errors
/// The method may return an error if the shares can't be encrypted.
fn shard_and_encrypt(
&self,
threshold: u8,
max: u8,
secret: &[u8],
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);
let sharks = Sharks(threshold);
let dealer = sharks.dealer(secret);
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"
);
assert_eq!(
max,
public_keys.len() as u8,
"max must be equal to amount of public keys"
);
let max = public_keys.len() as u8;
assert!(max >= threshold, "threshold must not exceed max keys");
let header = self.format_encrypted_header(&signing_key, &public_keys, threshold)?;
let mut messages = vec![header];
for (pk, share) in public_keys.iter().zip(dealer) {
let shard = Vec::from(&share);
messages.push(self.encrypt_shard(&shard, pk, &mut signing_key)?);
}
self.format_shard_file(&messages, writer)?;
Ok(())
}
}
/// Errors encountered while creating or combining shares using Shamir's Secret Sharing.
#[derive(thiserror::Error, Debug)]
pub enum SharksError {

View File

@ -57,8 +57,8 @@ const SHARD_METADATA_VERSION: u8 = 1;
const SHARD_METADATA_OFFSET: usize = 2;
use super::{
InvalidData, SharksError, HUNK_VERSION, QRCODE_COULDNT_READ, QRCODE_ERROR, QRCODE_PROMPT,
QRCODE_TIMEOUT,
Format, InvalidData, SharksError, HUNK_VERSION, QRCODE_COULDNT_READ, QRCODE_ERROR,
QRCODE_PROMPT, QRCODE_TIMEOUT, KeyDiscovery
};
// 256 bit share is 49 bytes + some amount of hunk bytes, gives us reasonable padding
@ -163,6 +163,55 @@ impl EncryptedMessage {
}
}
/// Parse OpenPGP packets for encrypted messages.
pub fn from_reader(input: impl Read + Send + Sync) -> openpgp::Result<Vec<Self>> {
let mut pkesks = Vec::new();
let mut encrypted_messages = vec![];
for packet in PacketPile::from_reader(input)
.map_err(Error::Sequoia)?
.into_children()
{
match packet {
Packet::PKESK(p) => pkesks.push(p),
Packet::SEIP(s) => {
encrypted_messages.push(EncryptedMessage::new(&mut pkesks, s));
}
s => {
panic!("Invalid variant found: {}", s.tag());
}
}
}
Ok(encrypted_messages)
}
/// Serialize all contents of the message to a writer.
///
/// # Errors
/// The function may error for any condition in Sequoia's Serialize trait.
fn serialize(&self, mut o: impl std::io::Write + Send + Sync) -> openpgp::Result<()> {
for pkesk in &self.pkesks {
let mut packet = vec![];
pkesk.serialize(&mut packet).map_err(Error::Sequoia)?;
let message = Message::new(&mut o);
let mut message = ArbitraryWriter::new(message, Tag::PKESK).map_err(Error::Sequoia)?;
message.write_all(&packet).map_err(Error::SequoiaIo)?;
message.finalize().map_err(Error::Sequoia)?;
}
let mut packet = vec![];
self.message
.serialize(&mut packet)
.map_err(Error::Sequoia)?;
let message = Message::new(&mut o);
let mut message = ArbitraryWriter::new(message, Tag::SEIP).map_err(Error::Sequoia)?;
message.write_all(&packet).map_err(Error::SequoiaIo)?;
message.finalize().map_err(Error::Sequoia)?;
Ok(())
}
/// Decrypt the message with a Sequoia policy and decryptor.
///
/// This method creates a container containing the packets and passes the serialized container
@ -176,23 +225,8 @@ impl EncryptedMessage {
H: VerificationHelper + DecryptionHelper,
{
let mut packets = vec![];
for pkesk in &self.pkesks {
let mut packet = vec![];
pkesk.serialize(&mut packet).map_err(Error::Sequoia)?;
let message = Message::new(&mut packets);
let mut message = ArbitraryWriter::new(message, Tag::PKESK).map_err(Error::Sequoia)?;
message.write_all(&packet).map_err(Error::SequoiaIo)?;
message.finalize().map_err(Error::Sequoia)?;
}
let mut packet = vec![];
self.message
.serialize(&mut packet)
self.serialize(&mut packets)
.map_err(Error::Sequoia)?;
let message = Message::new(&mut packets);
let mut message = ArbitraryWriter::new(message, Tag::SEIP).map_err(Error::Sequoia)?;
message.write_all(&packet).map_err(Error::SequoiaIo)?;
message.finalize().map_err(Error::Sequoia)?;
let mut decryptor = DecryptorBuilder::from_bytes(&packets)
.map_err(Error::Sequoia)?
@ -207,12 +241,357 @@ impl EncryptedMessage {
}
}
///
pub struct OpenPGP;
impl OpenPGP {
/// 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.
///
/// # Errors
/// The function may return an error if it is unable to read the directory or if Sequoia is unable
/// to load certificates from the file.
pub fn discover_certs(path: impl AsRef<Path>) -> Result<Vec<Cert>> {
let path = path.as_ref();
if path.is_file() {
let mut vec = vec![];
for cert in CertParser::from_file(path).map_err(Error::Sequoia)? {
vec.push(cert.map_err(Error::Sequoia)?);
}
Ok(vec)
} else {
let mut vec = vec![];
for entry in path
.read_dir()
.map_err(Error::Io)?
.filter_map(Result::ok)
.filter(|p| p.path().is_file())
{
vec.push(Cert::from_file(entry.path()).map_err(Error::Sequoia)?);
}
Ok(vec)
}
}
}
impl Format for OpenPGP {
type Error = Error;
type PublicKey = Cert;
type PrivateKeyData = Vec<Cert>;
type SigningKey = Cert;
type EncryptedData = EncryptedMessage;
/// Derive an OpenPGP Shard certificate from the given seed.
fn derive_signing_key(&self, seed: &[u8]) -> Self::SigningKey {
let seed = VariableLengthSeed::new(seed);
// build cert to sign encrypted shares
let userid = UserID::from("keyfork-sss");
let path = DerivationPath::from_str("m/7366512'/0'").expect("valid derivation path");
let xprv = XPrv::new(seed)
.derive_path(&path)
.expect("valid derivation");
keyfork_derive_openpgp::derive(
xprv,
&[KeyFlags::empty().set_certification().set_signing()],
&userid,
)
.expect("valid cert creation")
}
fn format_encrypted_header(
&self,
signing_key: &Self::SigningKey,
key_data: &[Self::PublicKey],
threshold: u8,
) -> Result<Self::EncryptedData, Self::Error> {
let policy = StandardPolicy::new();
let mut pp = vec![SHARD_METADATA_VERSION, threshold];
// Note: Sequoia does not export private keys on a Cert, only on a TSK
signing_key
.serialize(&mut pp)
.expect("serialize cert into bytes");
for cert in key_data {
cert.serialize(&mut pp)
.expect("serialize pubkey into bytes");
}
// verify packet pile
let mut iter = openpgp::cert::CertParser::from_bytes(&pp[SHARD_METADATA_OFFSET..])
.expect("should have certs");
let first_cert = iter.next().transpose().ok().flatten().expect("first cert");
assert_eq!(signing_key, &first_cert);
for (packet_cert, cert) in iter.zip(key_data) {
assert_eq!(
&packet_cert.expect("parsed packet cert"),
cert,
"packet pile could not recreate cert: {}",
cert.fingerprint(),
);
}
let valid_certs = key_data
.iter()
.map(|c| c.with_policy(&policy, None))
.collect::<openpgp::Result<Vec<_>>>()
.map_err(Error::Sequoia)?;
let recipients = valid_certs.iter().flat_map(|vc| {
get_encryption_keys(vc).map(|key| Recipient::new(KeyID::wildcard(), key.key()))
});
// Process is as follows:
// * Any OpenPGP message
// * An encrypted message
// * A literal message
// * The packet pile
//
// When decrypting, OpenPGP will see:
// * A message, and parse it
// * An encrypted message, and decrypt it
// * A literal message, and extract it
// * The packet pile
let mut output = vec![];
let message = Message::new(&mut output);
let encrypted_message = Encryptor2::for_recipients(message, recipients)
.build()
.map_err(Error::Sequoia)?;
let mut literal_message = LiteralWriter::new(encrypted_message)
.build()
.map_err(Error::Sequoia)?;
literal_message.write_all(&pp).map_err(Error::SequoiaIo)?;
literal_message.finalize().map_err(Error::Sequoia)?;
// Parse it into an EncryptedMessage. Yes, this takes a serialized message
// and deserializes it. Don't think about it too hard. It's easier this way.
let mut pkesks = vec![];
for packet in PacketPile::from_reader(output.as_slice())
.map_err(Error::Sequoia)?
.into_children()
{
match packet {
Packet::PKESK(p) => pkesks.push(p),
Packet::SEIP(s) => return Ok(EncryptedMessage::new(&mut pkesks, s)),
s => panic!("Invalid variant found: {}", s.tag()),
}
}
panic!("Unable to build EncryptedMessage from PacketPile");
}
fn encrypt_shard(
&self,
shard: &[u8],
public_key: &Cert,
signing_key: &mut Self::SigningKey,
) -> Result<EncryptedMessage> {
let policy = StandardPolicy::new();
let valid_cert = public_key
.with_policy(&policy, None)
.map_err(Error::Sequoia)?;
let encryption_keys = get_encryption_keys(&valid_cert).collect::<Vec<_>>();
let signing_key = signing_key
.primary_key()
.parts_into_secret()
.map_err(Error::Sequoia)?
.key()
.clone()
.into_keypair()
.map_err(Error::Sequoia)?;
// Process is as follows:
// * Any OpenPGP message
// * An encrypted message
// * A signed message
// * A literal message
// * The shard itself
//
// When decrypting, OpenPGP will see:
// * A message, and parse it
// * An encrypted message, and decrypt it
// * A signed message, and verify it
// * A literal message, and extract it
// * The shard itself
let mut message_output = vec![];
let message = Message::new(&mut message_output);
let encrypted_message = Encryptor2::for_recipients(
message,
encryption_keys
.iter()
.map(|k| Recipient::new(KeyID::wildcard(), k.key())),
)
.build()
.map_err(Error::Sequoia)?;
let signed_message = Signer::new(encrypted_message, signing_key)
.build()
.map_err(Error::Sequoia)?;
let mut message = LiteralWriter::new(signed_message)
.build()
.map_err(Error::Sequoia)?;
message.write_all(shard).map_err(Error::SequoiaIo)?;
message.finalize().map_err(Error::Sequoia)?;
let message = EncryptedMessage::from_reader(message_output.as_slice())
.map_err(Error::Sequoia)?
.into_iter()
.next()
.expect("serialized message should be parseable");
Ok(message)
}
fn parse_shard_file(
&self,
shard_file: impl Read + Send + Sync,
) -> Result<Vec<Self::EncryptedData>, Self::Error> {
EncryptedMessage::from_reader(shard_file).map_err(Error::Sequoia)
}
fn format_shard_file(
&self,
encrypted_data: &[Self::EncryptedData],
shard_file: impl Write + Send + Sync,
) -> Result<(), Self::Error> {
let mut writer = Writer::new(shard_file, Kind::Message).map_err(Error::SequoiaIo)?;
for message in encrypted_data {
message.serialize(&mut writer).map_err(Error::Sequoia)?;
}
writer.finalize().map_err(Error::SequoiaIo)?;
Ok(())
}
fn decrypt_all_shards(
&self,
private_keys: Option<Self::PrivateKeyData>,
encrypted_data: &[Self::EncryptedData],
) -> std::result::Result<(Vec<Share>, u8), Self::Error> {
// Be as liberal as possible when decrypting.
// We don't want to invalidate someone's keys just because the old sig expired.
let policy = NullPolicy::new();
let mut keyring = Keyring::new(private_keys.unwrap_or_default())?;
let mut manager = SmartcardManager::new()?;
let mut encrypted_messages = encrypted_data.iter();
let metadata = encrypted_messages.next().expect("metdata");
let metadata_content = decrypt_metadata(metadata, &policy, &mut keyring, &mut manager)?;
let (threshold, root_cert, certs) = decode_metadata_v1(&metadata_content)?;
keyring.set_root_cert(root_cert.clone());
manager.set_root_cert(root_cert.clone());
// Generate a controlled binding from certificates to encrypted messages. This is stable
// because we control the order packets are encrypted and certificates are stored.
// TODO: remove alloc, convert EncryptedMessage to &EncryptedMessage
let mut messages: HashMap<KeyID, EncryptedMessage> = certs
.iter()
.map(Cert::keyid)
.zip(encrypted_messages.cloned())
.collect();
let mut decrypted_messages =
decrypt_with_keyring(&mut messages, &certs, &policy, &mut keyring)?;
// clean decrypted messages from encrypted messages
messages.retain(|k, _v| !decrypted_messages.contains_key(k));
let left_from_threshold = threshold as usize - decrypted_messages.len();
if left_from_threshold > 0 {
#[allow(clippy::cast_possible_truncation)]
let new_messages = decrypt_with_manager(
left_from_threshold as u8,
&mut messages,
&certs,
&policy,
&mut manager,
)?;
decrypted_messages.extend(new_messages);
}
let shares = decrypted_messages
.values()
.map(|message| Share::try_from(message.as_slice()))
.collect::<Result<Vec<_>, &str>>()
.map_err(|e| SharksError::Share(e.to_string()))?;
Ok((shares, threshold))
}
fn decrypt_one_shard(
&self,
private_keys: Option<Self::PrivateKeyData>,
encrypted_data: &[Self::EncryptedData],
) -> std::result::Result<(Share, u8), Self::Error> {
let policy = NullPolicy::new();
let mut keyring = Keyring::new(private_keys.unwrap_or_default())?;
let mut manager = SmartcardManager::new()?;
let mut encrypted_messages = encrypted_data.iter();
let metadata = encrypted_messages.next().expect("metadata");
let metadata_content = decrypt_metadata(metadata, &policy, &mut keyring, &mut manager)?;
let (threshold, root_cert, certs) = decode_metadata_v1(&metadata_content)?;
keyring.set_root_cert(root_cert.clone());
manager.set_root_cert(root_cert.clone());
let mut messages: HashMap<KeyID, EncryptedMessage> = certs
.iter()
.map(Cert::keyid)
.zip(encrypted_messages.cloned())
.collect();
let decrypted_messages =
decrypt_with_keyring(&mut messages, &certs, &policy, &mut keyring)?;
if let Some(message) = decrypted_messages.into_values().next() {
let share = Share::try_from(message.as_slice())
.map_err(|e| SharksError::Share(e.to_string()))?;
return Ok((share, threshold));
}
let decrypted_messages =
decrypt_with_manager(1, &mut messages, &certs, &policy, &mut manager)?;
if let Some(message) = decrypted_messages.into_values().next() {
let share = Share::try_from(message.as_slice())
.map_err(|e| SharksError::Share(e.to_string()))?;
return Ok((share, threshold));
}
panic!("unable to decrypt shard");
}
}
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.
///
/// # Errors
/// The function may return an error if it is unable to read the directory or if Sequoia is unable
/// to load certificates from the file.
#[deprecated]
pub fn discover_certs(path: impl AsRef<Path>) -> Result<Vec<Cert>> {
let path = path.as_ref();
@ -245,6 +624,7 @@ pub fn discover_certs(path: impl AsRef<Path>) -> Result<Vec<Cert>> {
/// # Panics
/// When given packets that are not a list of PKESK packets and SEIP packets, the function panics.
/// The `split` utility should never give packets that are not in this format.
#[deprecated]
pub fn parse_messages(reader: impl Read + Send + Sync) -> Result<VecDeque<EncryptedMessage>> {
let mut pkesks = Vec::new();
let mut encrypted_messages = VecDeque::new();
@ -416,6 +796,7 @@ fn decrypt_metadata(
})
}
#[deprecated]
fn decrypt_one(
messages: Vec<EncryptedMessage>,
certs: &[Cert],
@ -465,6 +846,8 @@ fn decrypt_one(
/// The function may panic if a share is decrypted but has a length larger than 256 bits. This is
/// atypical usage and should not be encountered in normal usage, unless something that is not a
/// Keyfork seed has been fed into [`split`].
#[deprecated]
#[allow(deprecated)]
pub fn decrypt(
certs: &[Cert],
metadata: &EncryptedMessage,
@ -592,6 +975,7 @@ pub fn decrypt(
/// # Errors
/// The function may return an error if an error occurs while decrypting shards, parsing shards, or
/// combining the shards into a secret.
#[deprecated]
pub fn combine(
certs: Vec<Cert>,
metadata: &EncryptedMessage,
@ -679,6 +1063,7 @@ pub fn combine(
///
/// The function may panic if the metadata can't properly store the certificates used to generate
/// the encrypted shares.
#[deprecated]
pub fn split(threshold: u8, certs: Vec<Cert>, secret: &[u8], output: impl Write) -> Result<()> {
let seed = VariableLengthSeed::new(secret);
// build cert to sign encrypted shares

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

@ -3,10 +3,7 @@ use clap::{Parser, Subcommand};
use std::path::PathBuf;
use keyfork_mnemonic_util::Mnemonic;
use keyfork_shard::{
openpgp::{combine, discover_certs, parse_messages},
remote_decrypt,
};
use keyfork_shard::{remote_decrypt, Format};
type Result<T, E = Box<dyn std::error::Error>> = std::result::Result<T, E>;
@ -37,15 +34,10 @@ impl RecoverSubcommands {
} => {
let content = std::fs::read_to_string(shard_file)?;
if content.contains("BEGIN PGP MESSAGE") {
let certs = key_discovery
.as_ref()
.map(discover_certs)
.transpose()?
.unwrap_or(vec![]);
let mut messages = parse_messages(content.as_bytes())?;
let metadata = messages.pop_front().expect("any pgp encrypted message");
let mut seed = vec![];
combine(certs, &metadata, messages.into(), &mut seed)?;
let openpgp = keyfork_shard::openpgp::OpenPGP;
// TODO: remove .clone() by making handle() consume self
let seed = openpgp
.decrypt_all_shards_to_secret(key_discovery.as_deref(), content.as_bytes())?;
Ok(seed)
} else {
panic!("unknown format of shard file");

View File

@ -1,5 +1,6 @@
use super::Keyfork;
use clap::{builder::PossibleValue, Parser, Subcommand, ValueEnum};
use keyfork_shard::Format as _;
use std::{
io::{stdin, stdout, Read, Write},
path::{Path, PathBuf},
@ -31,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(
&self,
key_discovery: Option<&Path>,
input: impl Read + Send + Sync,
output: &mut impl Write,
) -> Result<(), Box<dyn std::error::Error>>;
fn combine<T>(
fn decrypt(
&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>;
fn decrypt<T>(
&self,
key_discovery: Option<T>,
input: impl Read + Send + Sync,
) -> Result<(), Box<dyn std::error::Error>>
where
T: AsRef<Path>;
) -> Result<(), Box<dyn std::error::Error>>;
}
#[derive(Clone, Debug)]
@ -62,77 +59,36 @@ impl ShardExec for OpenPGP {
&self,
threshold: u8,
max: u8,
key_discovery: impl AsRef<Path>,
key_discovery: &Path,
secret: &[u8],
output: &mut impl Write,
output: &mut (impl Write + Send + Sync),
) -> 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).map_err(Into::into)
let opgp = keyfork_shard::openpgp::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 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");
let mut bytes = vec![];
keyfork_shard::openpgp::combine(
certs,
&encrypted_metadata,
encrypted_messages.into(),
&mut bytes,
)?;
let openpgp = keyfork_shard::openpgp::OpenPGP;
let bytes = openpgp.decrypt_all_shards_to_secret(key_discovery, input)?;
write!(output, "{}", smex::encode(&bytes))?;
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 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::decrypt(
&certs,
&encrypted_metadata,
encrypted_messages.make_contiguous(),
)?;
let openpgp = keyfork_shard::openpgp::OpenPGP;
openpgp.decrypt_one_shard_for_transport(key_discovery, input)?;
Ok(())
}
}
@ -225,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}"),
@ -242,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,11 +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)?;
keyfork_shard::openpgp::split(threshold, certs, &seed, output)?;
opgp.shard_and_encrypt(threshold, certs.len() as u8, &seed, &certs[..], output)?;
} else {
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(())
}