Compare commits
3 Commits
64c5c648a6
...
4e64c73f21
Author | SHA1 | Date |
---|---|---|
Ryan Heywood | 4e64c73f21 | |
Ryan Heywood | 7da9738d52 | |
Ryan Heywood | a72bfaecec |
|
@ -882,6 +882,7 @@ dependencies = [
|
||||||
"clap",
|
"clap",
|
||||||
"keyfork-mnemonic-util",
|
"keyfork-mnemonic-util",
|
||||||
"keyfork-plumbing",
|
"keyfork-plumbing",
|
||||||
|
"keyfork-shard",
|
||||||
"smex",
|
"smex",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
]
|
]
|
||||||
|
|
|
@ -5,11 +5,15 @@ edition = "2021"
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[features]
|
||||||
|
default = ["openpgp"]
|
||||||
|
openpgp = ["sequoia-openpgp"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = "1.0.75"
|
anyhow = "1.0.75"
|
||||||
bincode = "1.3.3"
|
bincode = "1.3.3"
|
||||||
keyfork-derive-openpgp = { version = "0.1.0", path = "../keyfork-derive-openpgp" }
|
keyfork-derive-openpgp = { version = "0.1.0", path = "../keyfork-derive-openpgp" }
|
||||||
sequoia-openpgp = "1.16.1"
|
sequoia-openpgp = { version = "1.16.1", optional = true }
|
||||||
serde = "1.0.188"
|
serde = "1.0.188"
|
||||||
sharks = "0.5.0"
|
sharks = "0.5.0"
|
||||||
smex = { version = "0.1.0", path = "../smex" }
|
smex = { version = "0.1.0", path = "../smex" }
|
||||||
|
|
|
@ -6,7 +6,7 @@ use std::{
|
||||||
str::FromStr,
|
str::FromStr,
|
||||||
};
|
};
|
||||||
|
|
||||||
use keyfork_shard::{combine, discover_certs, parse_messages, openpgp::Cert};
|
use keyfork_shard::openpgp::{combine, discover_certs, parse_messages, openpgp::Cert};
|
||||||
|
|
||||||
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>;
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
use std::{env, path::PathBuf, process::ExitCode, str::FromStr};
|
use std::{env, path::PathBuf, process::ExitCode, str::FromStr};
|
||||||
|
|
||||||
use keyfork_shard::{discover_certs, openpgp::Cert, split};
|
use keyfork_shard::openpgp::{discover_certs, openpgp::Cert, split};
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
enum Error {
|
enum Error {
|
||||||
|
|
|
@ -1,338 +1,2 @@
|
||||||
use std::{
|
#[cfg(feature = "openpgp")]
|
||||||
collections::{HashMap, VecDeque},
|
pub mod openpgp;
|
||||||
io::{Read, Write},
|
|
||||||
path::Path,
|
|
||||||
str::FromStr,
|
|
||||||
};
|
|
||||||
|
|
||||||
use keyfork_derive_openpgp::derive_util::{
|
|
||||||
request::{DerivationAlgorithm, DerivationRequest},
|
|
||||||
DerivationPath,
|
|
||||||
};
|
|
||||||
use openpgp::{
|
|
||||||
armor::{Kind, Writer},
|
|
||||||
cert::{Cert, CertParser, ValidCert},
|
|
||||||
packet::{Packet, Tag, UserID, PKESK, SEIP},
|
|
||||||
parse::{stream::DecryptorBuilder, Parse},
|
|
||||||
policy::{NullPolicy, Policy, StandardPolicy},
|
|
||||||
serialize::{
|
|
||||||
stream::{ArbitraryWriter, Encryptor, LiteralWriter, Message, Recipient, Signer},
|
|
||||||
Marshal,
|
|
||||||
},
|
|
||||||
types::KeyFlags,
|
|
||||||
KeyID, PacketPile
|
|
||||||
};
|
|
||||||
pub use sequoia_openpgp as openpgp;
|
|
||||||
use sharks::{Share, Sharks};
|
|
||||||
|
|
||||||
mod keyring;
|
|
||||||
use keyring::Keyring;
|
|
||||||
|
|
||||||
// TODO: better error handling
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct WrappedError(String);
|
|
||||||
|
|
||||||
impl std::fmt::Display for WrappedError {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
f.write_str(&self.0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::error::Error for WrappedError {}
|
|
||||||
|
|
||||||
pub type Result<T, E = Box<dyn std::error::Error>> = std::result::Result<T, E>;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct EncryptedMessage {
|
|
||||||
pkesks: Vec<PKESK>,
|
|
||||||
message: SEIP,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl EncryptedMessage {
|
|
||||||
pub fn with_swap(pkesks: &mut Vec<PKESK>, seip: SEIP) -> Self {
|
|
||||||
Self {
|
|
||||||
pkesks: std::mem::take(pkesks),
|
|
||||||
message: seip,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn decrypt_with(&self, policy: &'_ dyn Policy, keyring: &mut Keyring) -> Result<Vec<u8>> {
|
|
||||||
let mut packets = vec![];
|
|
||||||
|
|
||||||
for pkesk in &self.pkesks {
|
|
||||||
let mut packet = vec![];
|
|
||||||
pkesk.serialize(&mut packet)?;
|
|
||||||
let message = Message::new(&mut packets);
|
|
||||||
let mut message = ArbitraryWriter::new(message, Tag::PKESK)?;
|
|
||||||
message.write_all(&packet)?;
|
|
||||||
message.finalize()?;
|
|
||||||
}
|
|
||||||
let mut packet = vec![];
|
|
||||||
self.message.serialize(&mut packet)?;
|
|
||||||
let message = Message::new(&mut packets);
|
|
||||||
let mut message = ArbitraryWriter::new(message, Tag::SEIP)?;
|
|
||||||
message.write_all(&packet)?;
|
|
||||||
message.finalize()?;
|
|
||||||
|
|
||||||
let mut decryptor =
|
|
||||||
DecryptorBuilder::from_bytes(&packets)?.with_policy(policy, None, keyring)?;
|
|
||||||
|
|
||||||
let mut content = vec![];
|
|
||||||
decryptor.read_to_end(&mut content)?;
|
|
||||||
Ok(content)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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)? {
|
|
||||||
vec.push(cert?);
|
|
||||||
}
|
|
||||||
Ok(vec)
|
|
||||||
} else {
|
|
||||||
let mut vec = vec![];
|
|
||||||
for entry in path
|
|
||||||
.read_dir()?
|
|
||||||
.filter_map(Result::ok)
|
|
||||||
.filter(|p| p.path().is_file())
|
|
||||||
{
|
|
||||||
vec.push(Cert::from_file(entry.path())?);
|
|
||||||
}
|
|
||||||
Ok(vec)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn parse_messages(reader: impl Read + Send + Sync) -> Result<VecDeque<EncryptedMessage> >{
|
|
||||||
let mut pkesks = Vec::new();
|
|
||||||
let mut encrypted_messages = VecDeque::new();
|
|
||||||
|
|
||||||
for packet in PacketPile::from_reader(reader)?.into_children() {
|
|
||||||
match packet {
|
|
||||||
Packet::PKESK(p) => pkesks.push(p),
|
|
||||||
Packet::SEIP(s) => {
|
|
||||||
encrypted_messages.push_back(EncryptedMessage::with_swap(&mut pkesks, s));
|
|
||||||
}
|
|
||||||
s => {
|
|
||||||
panic!("Invalid variant found: {}", s.tag());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(encrypted_messages)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_encryption_keys<'a>(
|
|
||||||
cert: &'a ValidCert,
|
|
||||||
) -> openpgp::cert::prelude::ValidKeyAmalgamationIter<
|
|
||||||
'a,
|
|
||||||
openpgp::packet::key::PublicParts,
|
|
||||||
openpgp::packet::key::UnspecifiedRole,
|
|
||||||
> {
|
|
||||||
cert.keys()
|
|
||||||
.alive()
|
|
||||||
.revoked(false)
|
|
||||||
.supported()
|
|
||||||
.for_storage_encryption()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_decryption_keys<'a>(
|
|
||||||
cert: &'a ValidCert,
|
|
||||||
) -> openpgp::cert::prelude::ValidKeyAmalgamationIter<
|
|
||||||
'a,
|
|
||||||
openpgp::packet::key::SecretParts,
|
|
||||||
openpgp::packet::key::UnspecifiedRole,
|
|
||||||
> {
|
|
||||||
cert.keys()
|
|
||||||
/*
|
|
||||||
.alive()
|
|
||||||
.revoked(false)
|
|
||||||
.supported()
|
|
||||||
*/
|
|
||||||
.for_storage_encryption()
|
|
||||||
.secret()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn combine(
|
|
||||||
threshold: u8,
|
|
||||||
certs: Vec<Cert>,
|
|
||||||
metadata: EncryptedMessage,
|
|
||||||
messages: Vec<EncryptedMessage>,
|
|
||||||
mut output: impl Write,
|
|
||||||
) -> Result<()> {
|
|
||||||
// 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(certs);
|
|
||||||
let content = metadata.decrypt_with(&policy, &mut keyring)?;
|
|
||||||
|
|
||||||
let mut cert_parser = CertParser::from_bytes(&content)?;
|
|
||||||
let root_cert = match cert_parser.next() {
|
|
||||||
Some(Ok(c)) => c,
|
|
||||||
Some(Err(e)) => panic!("Could not find root (first) certificate: {e}"),
|
|
||||||
None => panic!("No certs found in cert parser"),
|
|
||||||
};
|
|
||||||
let certs = cert_parser.collect::<openpgp::Result<Vec<_>>>()?;
|
|
||||||
keyring.set_root_cert(root_cert);
|
|
||||||
let mut messages: HashMap<KeyID, EncryptedMessage> =
|
|
||||||
HashMap::from_iter(certs.iter().map(|c| c.keyid()).zip(messages));
|
|
||||||
let mut decrypted_messages: HashMap<KeyID, Vec<u8>> = HashMap::new();
|
|
||||||
|
|
||||||
// NOTE: This is ONLY stable because we control the generation of PKESK packets and
|
|
||||||
// encode the policy to ourselves.
|
|
||||||
for valid_cert in certs.iter().map(|cert| cert.with_policy(&policy, None)) {
|
|
||||||
let valid_cert = valid_cert?;
|
|
||||||
// get keys from keyring for cert
|
|
||||||
let Some(secret_cert) = keyring.get_cert_for_primary_keyid(&valid_cert.keyid()) else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
let secret_cert = secret_cert.with_policy(&policy, None)?;
|
|
||||||
let keys = get_decryption_keys(&secret_cert).collect::<Vec<_>>();
|
|
||||||
if !keys.is_empty() {
|
|
||||||
if let Some(message) = messages.get_mut(&valid_cert.keyid()) {
|
|
||||||
for (pkesk, key) in message.pkesks.iter_mut().zip(keys) {
|
|
||||||
pkesk.set_recipient(key.keyid());
|
|
||||||
}
|
|
||||||
// we have a pkesk, decrypt via keyring
|
|
||||||
let result = message.decrypt_with(&policy, &mut keyring);
|
|
||||||
match result {
|
|
||||||
Ok(message) => {
|
|
||||||
decrypted_messages.insert(valid_cert.keyid(), message);
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
eprintln!(
|
|
||||||
"Could not decrypt with fingerprint {}: {}",
|
|
||||||
valid_cert.keyid(),
|
|
||||||
e
|
|
||||||
);
|
|
||||||
// do nothing, key will be retained
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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 {
|
|
||||||
eprintln!("remaining keys: {left_from_threshold}, prompting yubikeys");
|
|
||||||
}
|
|
||||||
for _ in 0..left_from_threshold {
|
|
||||||
todo!("prompt for Yubikeys")
|
|
||||||
}
|
|
||||||
|
|
||||||
let shares = decrypted_messages
|
|
||||||
.values()
|
|
||||||
.map(|message| Share::try_from(message.as_slice()))
|
|
||||||
.collect::<Result<Vec<_>, &str>>()
|
|
||||||
.map_err(|e| WrappedError(e.to_string()))?;
|
|
||||||
let secret = Sharks(threshold).recover(&shares)?;
|
|
||||||
|
|
||||||
output.write_all(smex::encode(&secret).as_bytes())?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn split(threshold: u8, certs: Vec<Cert>, secret: &[u8], output: impl Write) -> Result<()> {
|
|
||||||
// build cert to sign encrypted shares
|
|
||||||
let userid = UserID::from("keyfork-sss");
|
|
||||||
let kdr = DerivationRequest::new(
|
|
||||||
DerivationAlgorithm::Ed25519,
|
|
||||||
&DerivationPath::from_str("m/7366512'/0'")?,
|
|
||||||
)
|
|
||||||
.derive_with_master_seed(secret.to_vec())?;
|
|
||||||
let derived_cert = keyfork_derive_openpgp::derive(
|
|
||||||
kdr,
|
|
||||||
&[KeyFlags::empty().set_certification().set_signing()],
|
|
||||||
userid,
|
|
||||||
)?;
|
|
||||||
let signing_key = derived_cert
|
|
||||||
.primary_key()
|
|
||||||
.parts_into_secret()?
|
|
||||||
.key()
|
|
||||||
.clone()
|
|
||||||
.into_keypair()?;
|
|
||||||
|
|
||||||
let sharks = Sharks(threshold);
|
|
||||||
let dealer = sharks.dealer(secret);
|
|
||||||
let shares = dealer.map(|s| Vec::from(&s)).collect::<Vec<_>>();
|
|
||||||
let policy = StandardPolicy::new();
|
|
||||||
let mut writer = Writer::new(output, Kind::Message)?;
|
|
||||||
|
|
||||||
let mut total_recipients = vec![];
|
|
||||||
let mut messages = vec![];
|
|
||||||
|
|
||||||
for (share, cert) in shares.iter().zip(certs) {
|
|
||||||
total_recipients.push(cert.clone());
|
|
||||||
let valid_cert = cert.with_policy(&policy, None)?;
|
|
||||||
let encryption_keys = get_encryption_keys(&valid_cert).collect::<Vec<_>>();
|
|
||||||
|
|
||||||
let mut message_output = vec![];
|
|
||||||
let message = Message::new(&mut message_output);
|
|
||||||
let message = Encryptor::for_recipients(
|
|
||||||
message,
|
|
||||||
encryption_keys
|
|
||||||
.iter()
|
|
||||||
.map(|k| Recipient::new(KeyID::wildcard(), k.key())),
|
|
||||||
)
|
|
||||||
.build()?;
|
|
||||||
let message = Signer::new(message, signing_key.clone()).build()?;
|
|
||||||
let mut message = LiteralWriter::new(message).build()?;
|
|
||||||
message.write_all(share)?;
|
|
||||||
message.finalize()?;
|
|
||||||
|
|
||||||
messages.push(message_output);
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut pp = vec![];
|
|
||||||
// store derived cert to verify provided shares
|
|
||||||
derived_cert.serialize(&mut pp)?;
|
|
||||||
for recipient in &total_recipients {
|
|
||||||
recipient.serialize(&mut pp)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
// verify packet pile
|
|
||||||
for (packet_cert, cert) in openpgp::cert::CertParser::from_bytes(&pp)?
|
|
||||||
.skip(1)
|
|
||||||
.zip(total_recipients.iter())
|
|
||||||
{
|
|
||||||
if packet_cert? != *cert {
|
|
||||||
panic!(
|
|
||||||
"packet pile could not recreate cert: {}",
|
|
||||||
cert.fingerprint()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let valid_certs = total_recipients
|
|
||||||
.iter()
|
|
||||||
.map(|c| c.with_policy(&policy, None))
|
|
||||||
.collect::<openpgp::Result<Vec<_>>>()?;
|
|
||||||
|
|
||||||
let total_recipients = valid_certs.iter().flat_map(|vc| {
|
|
||||||
get_encryption_keys(vc).map(|key| Recipient::new(KeyID::wildcard(), key.key()))
|
|
||||||
});
|
|
||||||
|
|
||||||
// metadata
|
|
||||||
let mut message_output = vec![];
|
|
||||||
let message = Message::new(&mut message_output);
|
|
||||||
let message = Encryptor::for_recipients(message, total_recipients).build()?;
|
|
||||||
let mut message = LiteralWriter::new(message).build()?;
|
|
||||||
message.write_all(&pp)?;
|
|
||||||
message.finalize()?;
|
|
||||||
writer.write_all(&message_output)?;
|
|
||||||
|
|
||||||
for message in messages {
|
|
||||||
writer.write_all(&message)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
writer.finalize()?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
|
@ -0,0 +1,338 @@
|
||||||
|
use std::{
|
||||||
|
collections::{HashMap, VecDeque},
|
||||||
|
io::{Read, Write},
|
||||||
|
path::Path,
|
||||||
|
str::FromStr,
|
||||||
|
};
|
||||||
|
|
||||||
|
use keyfork_derive_openpgp::derive_util::{
|
||||||
|
request::{DerivationAlgorithm, DerivationRequest},
|
||||||
|
DerivationPath,
|
||||||
|
};
|
||||||
|
use openpgp::{
|
||||||
|
armor::{Kind, Writer},
|
||||||
|
cert::{Cert, CertParser, ValidCert},
|
||||||
|
packet::{Packet, Tag, UserID, PKESK, SEIP},
|
||||||
|
parse::{stream::DecryptorBuilder, Parse},
|
||||||
|
policy::{NullPolicy, Policy, StandardPolicy},
|
||||||
|
serialize::{
|
||||||
|
stream::{ArbitraryWriter, Encryptor, LiteralWriter, Message, Recipient, Signer},
|
||||||
|
Marshal,
|
||||||
|
},
|
||||||
|
types::KeyFlags,
|
||||||
|
KeyID, PacketPile
|
||||||
|
};
|
||||||
|
pub use sequoia_openpgp as openpgp;
|
||||||
|
use sharks::{Share, Sharks};
|
||||||
|
|
||||||
|
mod keyring;
|
||||||
|
use keyring::Keyring;
|
||||||
|
|
||||||
|
// TODO: better error handling
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct WrappedError(String);
|
||||||
|
|
||||||
|
impl std::fmt::Display for WrappedError {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.write_str(&self.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::error::Error for WrappedError {}
|
||||||
|
|
||||||
|
pub type Result<T, E = Box<dyn std::error::Error>> = std::result::Result<T, E>;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct EncryptedMessage {
|
||||||
|
pkesks: Vec<PKESK>,
|
||||||
|
message: SEIP,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EncryptedMessage {
|
||||||
|
pub fn with_swap(pkesks: &mut Vec<PKESK>, seip: SEIP) -> Self {
|
||||||
|
Self {
|
||||||
|
pkesks: std::mem::take(pkesks),
|
||||||
|
message: seip,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn decrypt_with(&self, policy: &'_ dyn Policy, keyring: &mut Keyring) -> Result<Vec<u8>> {
|
||||||
|
let mut packets = vec![];
|
||||||
|
|
||||||
|
for pkesk in &self.pkesks {
|
||||||
|
let mut packet = vec![];
|
||||||
|
pkesk.serialize(&mut packet)?;
|
||||||
|
let message = Message::new(&mut packets);
|
||||||
|
let mut message = ArbitraryWriter::new(message, Tag::PKESK)?;
|
||||||
|
message.write_all(&packet)?;
|
||||||
|
message.finalize()?;
|
||||||
|
}
|
||||||
|
let mut packet = vec![];
|
||||||
|
self.message.serialize(&mut packet)?;
|
||||||
|
let message = Message::new(&mut packets);
|
||||||
|
let mut message = ArbitraryWriter::new(message, Tag::SEIP)?;
|
||||||
|
message.write_all(&packet)?;
|
||||||
|
message.finalize()?;
|
||||||
|
|
||||||
|
let mut decryptor =
|
||||||
|
DecryptorBuilder::from_bytes(&packets)?.with_policy(policy, None, keyring)?;
|
||||||
|
|
||||||
|
let mut content = vec![];
|
||||||
|
decryptor.read_to_end(&mut content)?;
|
||||||
|
Ok(content)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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)? {
|
||||||
|
vec.push(cert?);
|
||||||
|
}
|
||||||
|
Ok(vec)
|
||||||
|
} else {
|
||||||
|
let mut vec = vec![];
|
||||||
|
for entry in path
|
||||||
|
.read_dir()?
|
||||||
|
.filter_map(Result::ok)
|
||||||
|
.filter(|p| p.path().is_file())
|
||||||
|
{
|
||||||
|
vec.push(Cert::from_file(entry.path())?);
|
||||||
|
}
|
||||||
|
Ok(vec)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_messages(reader: impl Read + Send + Sync) -> Result<VecDeque<EncryptedMessage> >{
|
||||||
|
let mut pkesks = Vec::new();
|
||||||
|
let mut encrypted_messages = VecDeque::new();
|
||||||
|
|
||||||
|
for packet in PacketPile::from_reader(reader)?.into_children() {
|
||||||
|
match packet {
|
||||||
|
Packet::PKESK(p) => pkesks.push(p),
|
||||||
|
Packet::SEIP(s) => {
|
||||||
|
encrypted_messages.push_back(EncryptedMessage::with_swap(&mut pkesks, s));
|
||||||
|
}
|
||||||
|
s => {
|
||||||
|
panic!("Invalid variant found: {}", s.tag());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(encrypted_messages)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_encryption_keys<'a>(
|
||||||
|
cert: &'a ValidCert,
|
||||||
|
) -> openpgp::cert::prelude::ValidKeyAmalgamationIter<
|
||||||
|
'a,
|
||||||
|
openpgp::packet::key::PublicParts,
|
||||||
|
openpgp::packet::key::UnspecifiedRole,
|
||||||
|
> {
|
||||||
|
cert.keys()
|
||||||
|
.alive()
|
||||||
|
.revoked(false)
|
||||||
|
.supported()
|
||||||
|
.for_storage_encryption()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_decryption_keys<'a>(
|
||||||
|
cert: &'a ValidCert,
|
||||||
|
) -> openpgp::cert::prelude::ValidKeyAmalgamationIter<
|
||||||
|
'a,
|
||||||
|
openpgp::packet::key::SecretParts,
|
||||||
|
openpgp::packet::key::UnspecifiedRole,
|
||||||
|
> {
|
||||||
|
cert.keys()
|
||||||
|
/*
|
||||||
|
.alive()
|
||||||
|
.revoked(false)
|
||||||
|
.supported()
|
||||||
|
*/
|
||||||
|
.for_storage_encryption()
|
||||||
|
.secret()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn combine(
|
||||||
|
threshold: u8,
|
||||||
|
certs: Vec<Cert>,
|
||||||
|
metadata: EncryptedMessage,
|
||||||
|
messages: Vec<EncryptedMessage>,
|
||||||
|
mut output: impl Write,
|
||||||
|
) -> Result<()> {
|
||||||
|
// 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(certs);
|
||||||
|
let content = metadata.decrypt_with(&policy, &mut keyring)?;
|
||||||
|
|
||||||
|
let mut cert_parser = CertParser::from_bytes(&content)?;
|
||||||
|
let root_cert = match cert_parser.next() {
|
||||||
|
Some(Ok(c)) => c,
|
||||||
|
Some(Err(e)) => panic!("Could not find root (first) certificate: {e}"),
|
||||||
|
None => panic!("No certs found in cert parser"),
|
||||||
|
};
|
||||||
|
let certs = cert_parser.collect::<openpgp::Result<Vec<_>>>()?;
|
||||||
|
keyring.set_root_cert(root_cert);
|
||||||
|
let mut messages: HashMap<KeyID, EncryptedMessage> =
|
||||||
|
HashMap::from_iter(certs.iter().map(|c| c.keyid()).zip(messages));
|
||||||
|
let mut decrypted_messages: HashMap<KeyID, Vec<u8>> = HashMap::new();
|
||||||
|
|
||||||
|
// NOTE: This is ONLY stable because we control the generation of PKESK packets and
|
||||||
|
// encode the policy to ourselves.
|
||||||
|
for valid_cert in certs.iter().map(|cert| cert.with_policy(&policy, None)) {
|
||||||
|
let valid_cert = valid_cert?;
|
||||||
|
// get keys from keyring for cert
|
||||||
|
let Some(secret_cert) = keyring.get_cert_for_primary_keyid(&valid_cert.keyid()) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let secret_cert = secret_cert.with_policy(&policy, None)?;
|
||||||
|
let keys = get_decryption_keys(&secret_cert).collect::<Vec<_>>();
|
||||||
|
if !keys.is_empty() {
|
||||||
|
if let Some(message) = messages.get_mut(&valid_cert.keyid()) {
|
||||||
|
for (pkesk, key) in message.pkesks.iter_mut().zip(keys) {
|
||||||
|
pkesk.set_recipient(key.keyid());
|
||||||
|
}
|
||||||
|
// we have a pkesk, decrypt via keyring
|
||||||
|
let result = message.decrypt_with(&policy, &mut keyring);
|
||||||
|
match result {
|
||||||
|
Ok(message) => {
|
||||||
|
decrypted_messages.insert(valid_cert.keyid(), message);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!(
|
||||||
|
"Could not decrypt with fingerprint {}: {}",
|
||||||
|
valid_cert.keyid(),
|
||||||
|
e
|
||||||
|
);
|
||||||
|
// do nothing, key will be retained
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 {
|
||||||
|
eprintln!("remaining keys: {left_from_threshold}, prompting yubikeys");
|
||||||
|
}
|
||||||
|
for _ in 0..left_from_threshold {
|
||||||
|
todo!("prompt for Yubikeys")
|
||||||
|
}
|
||||||
|
|
||||||
|
let shares = decrypted_messages
|
||||||
|
.values()
|
||||||
|
.map(|message| Share::try_from(message.as_slice()))
|
||||||
|
.collect::<Result<Vec<_>, &str>>()
|
||||||
|
.map_err(|e| WrappedError(e.to_string()))?;
|
||||||
|
let secret = Sharks(threshold).recover(&shares)?;
|
||||||
|
|
||||||
|
output.write_all(smex::encode(&secret).as_bytes())?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn split(threshold: u8, certs: Vec<Cert>, secret: &[u8], output: impl Write) -> Result<()> {
|
||||||
|
// build cert to sign encrypted shares
|
||||||
|
let userid = UserID::from("keyfork-sss");
|
||||||
|
let kdr = DerivationRequest::new(
|
||||||
|
DerivationAlgorithm::Ed25519,
|
||||||
|
&DerivationPath::from_str("m/7366512'/0'")?,
|
||||||
|
)
|
||||||
|
.derive_with_master_seed(secret.to_vec())?;
|
||||||
|
let derived_cert = keyfork_derive_openpgp::derive(
|
||||||
|
kdr,
|
||||||
|
&[KeyFlags::empty().set_certification().set_signing()],
|
||||||
|
userid,
|
||||||
|
)?;
|
||||||
|
let signing_key = derived_cert
|
||||||
|
.primary_key()
|
||||||
|
.parts_into_secret()?
|
||||||
|
.key()
|
||||||
|
.clone()
|
||||||
|
.into_keypair()?;
|
||||||
|
|
||||||
|
let sharks = Sharks(threshold);
|
||||||
|
let dealer = sharks.dealer(secret);
|
||||||
|
let shares = dealer.map(|s| Vec::from(&s)).collect::<Vec<_>>();
|
||||||
|
let policy = StandardPolicy::new();
|
||||||
|
let mut writer = Writer::new(output, Kind::Message)?;
|
||||||
|
|
||||||
|
let mut total_recipients = vec![];
|
||||||
|
let mut messages = vec![];
|
||||||
|
|
||||||
|
for (share, cert) in shares.iter().zip(certs) {
|
||||||
|
total_recipients.push(cert.clone());
|
||||||
|
let valid_cert = cert.with_policy(&policy, None)?;
|
||||||
|
let encryption_keys = get_encryption_keys(&valid_cert).collect::<Vec<_>>();
|
||||||
|
|
||||||
|
let mut message_output = vec![];
|
||||||
|
let message = Message::new(&mut message_output);
|
||||||
|
let message = Encryptor::for_recipients(
|
||||||
|
message,
|
||||||
|
encryption_keys
|
||||||
|
.iter()
|
||||||
|
.map(|k| Recipient::new(KeyID::wildcard(), k.key())),
|
||||||
|
)
|
||||||
|
.build()?;
|
||||||
|
let message = Signer::new(message, signing_key.clone()).build()?;
|
||||||
|
let mut message = LiteralWriter::new(message).build()?;
|
||||||
|
message.write_all(share)?;
|
||||||
|
message.finalize()?;
|
||||||
|
|
||||||
|
messages.push(message_output);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut pp = vec![];
|
||||||
|
// store derived cert to verify provided shares
|
||||||
|
derived_cert.serialize(&mut pp)?;
|
||||||
|
for recipient in &total_recipients {
|
||||||
|
recipient.serialize(&mut pp)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// verify packet pile
|
||||||
|
for (packet_cert, cert) in openpgp::cert::CertParser::from_bytes(&pp)?
|
||||||
|
.skip(1)
|
||||||
|
.zip(total_recipients.iter())
|
||||||
|
{
|
||||||
|
if packet_cert? != *cert {
|
||||||
|
panic!(
|
||||||
|
"packet pile could not recreate cert: {}",
|
||||||
|
cert.fingerprint()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let valid_certs = total_recipients
|
||||||
|
.iter()
|
||||||
|
.map(|c| c.with_policy(&policy, None))
|
||||||
|
.collect::<openpgp::Result<Vec<_>>>()?;
|
||||||
|
|
||||||
|
let total_recipients = valid_certs.iter().flat_map(|vc| {
|
||||||
|
get_encryption_keys(vc).map(|key| Recipient::new(KeyID::wildcard(), key.key()))
|
||||||
|
});
|
||||||
|
|
||||||
|
// metadata
|
||||||
|
let mut message_output = vec![];
|
||||||
|
let message = Message::new(&mut message_output);
|
||||||
|
let message = Encryptor::for_recipients(message, total_recipients).build()?;
|
||||||
|
let mut message = LiteralWriter::new(message).build()?;
|
||||||
|
message.write_all(&pp)?;
|
||||||
|
message.finalize()?;
|
||||||
|
writer.write_all(&message_output)?;
|
||||||
|
|
||||||
|
for message in messages {
|
||||||
|
writer.write_all(&message)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
writer.finalize()?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
use crate::openpgp::{
|
use super::openpgp::{
|
||||||
self,
|
self,
|
||||||
cert::Cert,
|
cert::Cert,
|
||||||
packet::{PKESK, SKESK},
|
packet::{PKESK, SKESK},
|
|
@ -8,7 +8,11 @@
|
||||||
- [Binaries](./bin/index.md)
|
- [Binaries](./bin/index.md)
|
||||||
- [keyfork](./bin/keyfork/index.md)
|
- [keyfork](./bin/keyfork/index.md)
|
||||||
- [mnemonic](./bin/keyfork/mnemonic/index.md)
|
- [mnemonic](./bin/keyfork/mnemonic/index.md)
|
||||||
|
- [shard](./bin/keyfork/shard/index.md)
|
||||||
- [keyforkd](./bin/keyforkd.md)
|
- [keyforkd](./bin/keyforkd.md)
|
||||||
|
- [keyfork-shard](./bin/keyfork-shard/index.md)
|
||||||
|
- [keyfork-shard-split-openpgp](./bin/keyfork-shard/openpgp/split.md)
|
||||||
|
- [keyfork-shard-combine-openpgp](./bin/keyfork-shard/openpgp/combine.md)
|
||||||
- [keyfork-entropy](./bin/keyfork-plumbing/entropy.md)
|
- [keyfork-entropy](./bin/keyfork-plumbing/entropy.md)
|
||||||
- [keyfork-mnemonic-from-seed](./bin/keyfork-plumbing/mnemonic-from-seed.md)
|
- [keyfork-mnemonic-from-seed](./bin/keyfork-plumbing/mnemonic-from-seed.md)
|
||||||
- [keyfork-derive-key](./bin/keyfork-derive-key.md)
|
- [keyfork-derive-key](./bin/keyfork-derive-key.md)
|
||||||
|
|
|
@ -0,0 +1,19 @@
|
||||||
|
# keyfork-shard
|
||||||
|
|
||||||
|
<!-- Linked to: keyfork-user-guide/src/bin/keyfork/shard/index.md -->
|
||||||
|
|
||||||
|
The `keyfork-shard` crate contains some binaries to enable M-of-N sharing of
|
||||||
|
data. All binaries use Shamir's Secret Sharing through the [`sharks`] crate.
|
||||||
|
|
||||||
|
## OpenPGP
|
||||||
|
|
||||||
|
Keyfork provides OpenPGP compatible [`split`][openpgp-split] and
|
||||||
|
[`combine`][openpgp-combine] versions of Shard binaries. These binaries use
|
||||||
|
Sequoia OpenPGP and while they require all the necessary certificates for the
|
||||||
|
splitting stage, the certificates are included in the payload, and once Keyfork
|
||||||
|
supports decrypting using OpenPGP smartcards, certificates will not be required
|
||||||
|
to decrypt the shares.
|
||||||
|
|
||||||
|
[`sharks`]: https://docs.rs/sharks/latest/sharks/
|
||||||
|
[openpgp-split]: ./openpgp/split.md
|
||||||
|
[openpgp-combine]: ./openpgp/combine.md
|
|
@ -0,0 +1,26 @@
|
||||||
|
# keyfork-shard-combine-openpgp
|
||||||
|
|
||||||
|
Combine `threshold` shares into a previously [`split`] secret.
|
||||||
|
|
||||||
|
## Arguments
|
||||||
|
|
||||||
|
`keyfork-shard-combine-openpgp threshold key_discovery`
|
||||||
|
|
||||||
|
* `threshold`: Minimum number of operators present to recover the secret, as
|
||||||
|
previously configured when creating the secret
|
||||||
|
* `key_discovery`: Either a file or a directory containing OpenPGP keys.
|
||||||
|
If a file, load all keys from the file.
|
||||||
|
If a directory, for every file in the directory (non-recursively), load
|
||||||
|
keys from the file.
|
||||||
|
If the amount of keys found is less than `threshold`, an OpenPGP Card
|
||||||
|
fallback will be used to decrypt the rest of the messages.
|
||||||
|
|
||||||
|
## Input
|
||||||
|
|
||||||
|
OpenPGP Messages from [`split`].
|
||||||
|
|
||||||
|
## Output
|
||||||
|
|
||||||
|
Hex-encoded secret.
|
||||||
|
|
||||||
|
[`split`]: ./split.md
|
|
@ -0,0 +1,33 @@
|
||||||
|
# keyfork-shard-split-openpgp
|
||||||
|
|
||||||
|
<!-- Linked to: keyfork-user-guide/src/bin/keyfork-shard/index.md -->
|
||||||
|
|
||||||
|
Split a secret into threshold-of-max shares, encrypting each share to an
|
||||||
|
OpenPGP certificate. The resulting file may be kept by any share operator, but
|
||||||
|
requires at least `threshold` operators to be present to combine into the
|
||||||
|
original secret.
|
||||||
|
|
||||||
|
## Arguments
|
||||||
|
|
||||||
|
`keyfork-shard-split-openpgp threshold max key_discovery`
|
||||||
|
|
||||||
|
* `threshold`: Minimum number of operators present to recover the secret
|
||||||
|
* `max`: Maximum number of operators; this many OpenPGP certs must be available
|
||||||
|
* `key_discovery`: Either a file or a directory containing OpenPGP certs.
|
||||||
|
If a file, load all certificates from the file.
|
||||||
|
If a directory, for every file in the directory (non-recursively), load
|
||||||
|
certificates from the file.
|
||||||
|
|
||||||
|
## Input
|
||||||
|
|
||||||
|
Hex-encoded secret, ideally up to 2048 characters. For larger secrets, encrypt
|
||||||
|
beforehand using a symmetric key (AES256, for example), and split the symmetric
|
||||||
|
key.
|
||||||
|
|
||||||
|
## Output
|
||||||
|
|
||||||
|
OpenPGP ASCII armored message containing several sequential encrypted messages.
|
||||||
|
|
||||||
|
**Note:** While it is possible to decrypt some of the messages using a tool
|
||||||
|
like GnuPG or Sequoia, it is not recommended to handle these messages using
|
||||||
|
tooling outside of Keyfork Shard.
|
|
@ -1 +1,11 @@
|
||||||
# keyfork
|
# keyfork
|
||||||
|
|
||||||
|
The primary interface for interacting with the Keyfork utilities.
|
||||||
|
|
||||||
|
## `shard`
|
||||||
|
|
||||||
|
Utilities for splitting and combining secrets using various formats.
|
||||||
|
|
||||||
|
## `mnemonic`
|
||||||
|
|
||||||
|
Utilities pertaining to the creation and management of mnemonics.
|
||||||
|
|
|
@ -0,0 +1,76 @@
|
||||||
|
# `keyfork shard`
|
||||||
|
|
||||||
|
<!-- Linked to: keyfork-user-guide/src/bin/keyfork-shard/index.md -->
|
||||||
|
|
||||||
|
Utilities to enable M-of-N sharing of data, using Shamir's Secret Sharing,
|
||||||
|
supporting multiple formats.
|
||||||
|
|
||||||
|
## Options
|
||||||
|
|
||||||
|
* `--format`: Either `openpgp` or `p256`, provided anywhere after `split`
|
||||||
|
|
||||||
|
### Format: OpenPGP
|
||||||
|
|
||||||
|
The secret is split and shares are automatically encrypted to provided OpenPGP
|
||||||
|
certificates. The resulting output is an OpenPGP ASCII armored message
|
||||||
|
containing several sequential encrypted messages.
|
||||||
|
|
||||||
|
When decrypting, for any missing keys that do not meet the threshold, the
|
||||||
|
command will prompt for a Yubikey to provide smartcard-based decryption of
|
||||||
|
shares.
|
||||||
|
|
||||||
|
### Format: p256
|
||||||
|
|
||||||
|
This section is incomplete as the functionality for p256 keys is not yet
|
||||||
|
implemented.
|
||||||
|
|
||||||
|
## `keyfork shard split`
|
||||||
|
|
||||||
|
Split a secret into threshold-of-max shares.
|
||||||
|
|
||||||
|
### Arguments
|
||||||
|
|
||||||
|
`keyfork shard split --threshold=threshold --max=max key_discovery`
|
||||||
|
|
||||||
|
* `threshold`: Minimum number of operators present to recover the secret
|
||||||
|
* `max`: Maximum number of operators, must equal the amount of keys in
|
||||||
|
`key_discovery`
|
||||||
|
* `key_discovery`: Either a file or a directory containing public keys.
|
||||||
|
If a file, load all public keys from a file.
|
||||||
|
If a directory, for every file in the directory (non-recursively), load
|
||||||
|
public keys from the file.
|
||||||
|
|
||||||
|
### Input
|
||||||
|
|
||||||
|
Hex-encoded secret, ideally up to 2048 characters. For larger secrets, encrypt
|
||||||
|
beforehand using a symmetric key (AES256, for example), and split the symmetric
|
||||||
|
key.
|
||||||
|
|
||||||
|
### Output
|
||||||
|
|
||||||
|
The output of the command is dependent on the format.
|
||||||
|
|
||||||
|
## `keyfork shard combine`
|
||||||
|
|
||||||
|
Combine `threshold` shares into a secret.
|
||||||
|
|
||||||
|
### Arguments
|
||||||
|
|
||||||
|
`keyfork shard combine --threshold=threshold [key_discovery]`
|
||||||
|
|
||||||
|
* `threshold`: Mini mum number of operators present to recover the secret
|
||||||
|
* `key_discovery`: Either a file or a directory containing public keys.
|
||||||
|
If a file, load all private keys from a file.
|
||||||
|
If a directory, for every file in the directory (non-recursively), load
|
||||||
|
private keys from the file.
|
||||||
|
If the amount of keys found is less than `threshold`, it is up to the format
|
||||||
|
to determine how to discover the keys.
|
||||||
|
|
||||||
|
### Input
|
||||||
|
|
||||||
|
The input of the command is dependent on the format, but should be the exact
|
||||||
|
same as the output from the `split` command previously used.
|
||||||
|
|
||||||
|
### Output
|
||||||
|
|
||||||
|
Hex-encoded secret.
|
|
@ -11,3 +11,4 @@ clap = { version = "4.4.2", features = ["derive", "env"] }
|
||||||
thiserror = "1.0.48"
|
thiserror = "1.0.48"
|
||||||
smex = { version = "0.1.0", path = "../smex" }
|
smex = { version = "0.1.0", path = "../smex" }
|
||||||
keyfork-plumbing = { version = "0.1.0", path = "../keyfork-plumbing" }
|
keyfork-plumbing = { version = "0.1.0", path = "../keyfork-plumbing" }
|
||||||
|
keyfork-shard = { version = "0.1.0", path = "../keyfork-shard" }
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
use clap::{Parser, Subcommand};
|
use clap::{Parser, Subcommand};
|
||||||
|
|
||||||
mod mnemonic;
|
mod mnemonic;
|
||||||
|
mod shard;
|
||||||
|
|
||||||
/// The Kitchen Sink of Entropy.
|
/// The Kitchen Sink of Entropy.
|
||||||
#[derive(Parser, Clone, Debug)]
|
#[derive(Parser, Clone, Debug)]
|
||||||
|
@ -16,6 +17,9 @@ pub enum KeyforkCommands {
|
||||||
/// Mnemonic generation and persistence utilities.
|
/// Mnemonic generation and persistence utilities.
|
||||||
Mnemonic(mnemonic::Mnemonic),
|
Mnemonic(mnemonic::Mnemonic),
|
||||||
|
|
||||||
|
/// Secret sharing utilities.
|
||||||
|
Shard(shard::Shard),
|
||||||
|
|
||||||
/// Keyforkd background daemon to manage seed creation.
|
/// Keyforkd background daemon to manage seed creation.
|
||||||
Daemon,
|
Daemon,
|
||||||
}
|
}
|
||||||
|
@ -27,6 +31,10 @@ impl KeyforkCommands {
|
||||||
let response = m.command.handle(m, keyfork)?;
|
let response = m.command.handle(m, keyfork)?;
|
||||||
println!("{response}");
|
println!("{response}");
|
||||||
}
|
}
|
||||||
|
KeyforkCommands::Shard(s) => {
|
||||||
|
// TODO: When actually fleshing out, this takes a `Read` and a `Write`
|
||||||
|
s.command.handle(s, keyfork)?;
|
||||||
|
}
|
||||||
KeyforkCommands::Daemon => {
|
KeyforkCommands::Daemon => {
|
||||||
todo!()
|
todo!()
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
}
|
Loading…
Reference in New Issue