Compare commits

...

3 Commits

16 changed files with 712 additions and 342 deletions

1
Cargo.lock generated
View File

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

View File

@ -5,11 +5,15 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[features]
default = ["openpgp"]
openpgp = ["sequoia-openpgp"]
[dependencies]
anyhow = "1.0.75"
bincode = "1.3.3"
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"
sharks = "0.5.0"
smex = { version = "0.1.0", path = "../smex" }

View File

@ -6,7 +6,7 @@ use std::{
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>;

View File

@ -1,6 +1,6 @@
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)]
enum Error {

View File

@ -1,338 +1,2 @@
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(())
}
#[cfg(feature = "openpgp")]
pub mod openpgp;

View File

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

View File

@ -1,4 +1,4 @@
use crate::openpgp::{
use super::openpgp::{
self,
cert::Cert,
packet::{PKESK, SKESK},

View File

@ -8,7 +8,11 @@
- [Binaries](./bin/index.md)
- [keyfork](./bin/keyfork/index.md)
- [mnemonic](./bin/keyfork/mnemonic/index.md)
- [shard](./bin/keyfork/shard/index.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-mnemonic-from-seed](./bin/keyfork-plumbing/mnemonic-from-seed.md)
- [keyfork-derive-key](./bin/keyfork-derive-key.md)

View File

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

View File

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

View File

@ -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.

View File

@ -1 +1,11 @@
# 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.

View File

@ -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.

View File

@ -11,3 +11,4 @@ clap = { version = "4.4.2", features = ["derive", "env"] }
thiserror = "1.0.48"
smex = { version = "0.1.0", path = "../smex" }
keyfork-plumbing = { version = "0.1.0", path = "../keyfork-plumbing" }
keyfork-shard = { version = "0.1.0", path = "../keyfork-shard" }

View File

@ -1,6 +1,7 @@
use clap::{Parser, Subcommand};
mod mnemonic;
mod shard;
/// The Kitchen Sink of Entropy.
#[derive(Parser, Clone, Debug)]
@ -16,6 +17,9 @@ pub enum KeyforkCommands {
/// Mnemonic generation and persistence utilities.
Mnemonic(mnemonic::Mnemonic),
/// Secret sharing utilities.
Shard(shard::Shard),
/// Keyforkd background daemon to manage seed creation.
Daemon,
}
@ -27,6 +31,10 @@ impl KeyforkCommands {
let response = m.command.handle(m, keyfork)?;
println!("{response}");
}
KeyforkCommands::Shard(s) => {
// TODO: When actually fleshing out, this takes a `Read` and a `Write`
s.command.handle(s, keyfork)?;
}
KeyforkCommands::Daemon => {
todo!()
}

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

@ -0,0 +1,186 @@
use super::Keyfork;
use clap::{builder::PossibleValue, Parser, Subcommand, ValueEnum};
use std::{
io::{stdin, stdout, BufRead, BufReader, Read, Write},
path::{Path, PathBuf},
};
#[derive(Debug, Clone)]
enum Format {
OpenPGP(OpenPGP),
P256(P256),
}
impl ValueEnum for Format {
fn value_variants<'a>() -> &'a [Self] {
&[Self::OpenPGP(OpenPGP), Self::P256(P256)]
}
fn to_possible_value(&self) -> Option<PossibleValue> {
Some(match self {
Format::OpenPGP(_) => PossibleValue::new("openpgp"),
Format::P256(_) => PossibleValue::new("p256"),
})
}
}
trait ShardExec {
fn split(
&self,
threshold: u8,
max: u8,
key_discovery: impl AsRef<Path>,
secret: &[u8],
output: &mut impl Write,
) -> Result<(), Box<dyn std::error::Error>>;
fn combine<T>(
&self,
threshold: u8,
key_discovery: Option<T>,
input: impl Read + Send + Sync,
output: &mut impl Write,
) -> Result<(), Box<dyn std::error::Error>>
where
T: AsRef<Path>;
}
#[derive(Clone, Debug)]
struct OpenPGP;
impl ShardExec for OpenPGP {
fn split(
&self,
threshold: u8,
max: u8,
key_discovery: impl AsRef<Path>,
secret: &[u8],
output: &mut impl Write,
) -> Result<(), Box<dyn std::error::Error>> {
// Get certs and input
let certs = keyfork_shard::openpgp::discover_certs(key_discovery.as_ref())?;
assert_eq!(
certs.len(),
max.into(),
"cert count {} != max {max}",
certs.len()
);
keyfork_shard::openpgp::split(threshold, certs, secret, output)
}
fn combine<T>(
&self,
threshold: u8,
key_discovery: Option<T>,
input: impl Read + Send + Sync,
output: &mut impl Write,
) -> Result<(), Box<dyn std::error::Error>>
where
T: AsRef<Path>,
{
let certs = key_discovery
.map(|kd| keyfork_shard::openpgp::discover_certs(kd.as_ref()))
.transpose()?
.unwrap_or(vec![]);
let mut encrypted_messages = keyfork_shard::openpgp::parse_messages(input)?;
let encrypted_metadata = encrypted_messages
.pop_front()
.expect("any pgp encrypted message");
keyfork_shard::openpgp::combine(
threshold,
certs,
encrypted_metadata,
encrypted_messages.into(),
output,
)?;
Ok(())
}
}
#[derive(Clone, Debug)]
struct P256;
#[derive(Subcommand, Clone, Debug)]
pub enum ShardSubcommands {
/// Split a secret into multiple shares, using Shamir's Secret Sharing.
Split {
/// The amount of shares required to recombine a secret.
#[arg(long)]
threshold: u8,
/// The total amount of shares to generate.
#[arg(long)]
max: u8,
/// The path to discover public keys from.
key_discovery: PathBuf,
},
/// Combine multiple shares into a secret
Combine {
/// The amount of sharesr equired to recombine a secret.
#[arg(long)]
threshold: u8,
/// The path to discover private keys from.
key_discovery: Option<PathBuf>,
},
}
impl ShardSubcommands {
pub fn handle(
&self,
shard: &Shard,
_keyfork: &Keyfork,
) -> Result<(), Box<dyn std::error::Error>> {
let stdin = stdin();
let mut stdout = stdout();
match self {
ShardSubcommands::Split {
threshold,
max,
key_discovery,
} => {
assert!(threshold <= max, "threshold {threshold} <= max {max}");
let mut input = BufReader::new(stdin);
let mut hex_line = String::new();
input.read_line(&mut hex_line)?;
let secret = smex::decode(hex_line.trim())?;
match &shard.format {
Some(Format::OpenPGP(o)) => {
o.split(*threshold, *max, key_discovery, &secret, &mut stdout)
}
Some(Format::P256(_p)) => {
todo!()
}
None => panic!("--format was not given"),
}
}
ShardSubcommands::Combine {
threshold,
key_discovery,
} => match &shard.format {
Some(Format::OpenPGP(o)) => {
o.combine(*threshold, key_discovery.as_ref(), stdin, &mut stdout)
}
Some(Format::P256(_p)) => {
todo!()
}
None => panic!("--format was not given"),
},
}
}
}
#[derive(Parser, Debug, Clone)]
pub struct Shard {
/// Which format to use for encoding/encrypting and decoding/decrypting shares.
#[arg(long, value_enum, global = true)]
format: Option<Format>,
#[command(subcommand)]
pub command: ShardSubcommands,
}