keyfork/keyfork-shard/src/openpgp.rs

694 lines
22 KiB
Rust

use std::{
collections::{HashMap, VecDeque},
io::{stdin, stdout, Read, Write},
path::Path,
str::FromStr,
};
use aes_gcm::{
aead::{consts::U12, Aead},
aes::cipher::InvalidLength,
Aes256Gcm, Error as AesError, KeyInit, Nonce,
};
use hkdf::{Hkdf, InvalidLength as HkdfInvalidLength};
use sha2::Sha256;
use keyfork_derive_openpgp::derive_util::{
request::{DerivationAlgorithm, DerivationRequest},
DerivationPath,
};
use keyfork_mnemonic_util::{Mnemonic, MnemonicFromStrError, MnemonicGenerationError, Wordlist};
use keyfork_prompt::{qrencode, Error as PromptError, Message as PromptMessage, PromptManager};
use openpgp::{
armor::{Kind, Writer},
cert::{Cert, CertParser, ValidCert},
packet::{Packet, Tag, UserID, PKESK, SEIP},
parse::{
stream::{DecryptionHelper, DecryptorBuilder, VerificationHelper},
Parse,
},
policy::{NullPolicy, Policy, StandardPolicy},
serialize::{
stream::{ArbitraryWriter, Encryptor2, LiteralWriter, Message, Recipient, Signer},
Marshal,
},
types::KeyFlags,
Fingerprint, KeyID, PacketPile,
};
pub use sequoia_openpgp as openpgp;
use sharks::{Share, Sharks};
use x25519_dalek::{EphemeralSecret, PublicKey};
mod keyring;
use keyring::Keyring;
mod smartcard;
use smartcard::SmartcardManager;
/// Shard metadata verson 1:
/// 1 byte: Version
/// 1 byte: Threshold
/// Packet Pile of Certs
const SHARD_METADATA_VERSION: u8 = 1;
const SHARD_METADATA_OFFSET: usize = 2;
use super::{SharksError, InvalidMnemonicData, HUNK_VERSION};
// 256 bit share is 49 bytes + some amount of hunk bytes, gives us reasonable padding
const ENC_LEN: u8 = 4 * 16;
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("{0}")]
Sharks(#[from] SharksError),
#[error("Error decrypting share: {0}")]
SymDecryptShare(#[from] AesError),
#[error("Invalid length of AES key: {0}")]
AesLength(#[from] InvalidLength),
#[error("Invalid KDF length: {0}")]
HkdfLength(#[from] HkdfInvalidLength),
#[error("Derived secret hash {0} != expected {1}")]
InvalidSecret(Fingerprint, Fingerprint),
#[error("OpenPGP error: {0}")]
Sequoia(#[source] anyhow::Error),
#[error("OpenPGP IO error: {0}")]
SequoiaIo(#[source] std::io::Error),
#[error("Keyring error: {0}")]
Keyring(#[from] keyring::Error),
#[error("Smartcard error: {0}")]
Smartcard(#[from] smartcard::Error),
#[error("Prompt error: {0}")]
Prompt(#[from] PromptError),
#[error("Mnemonic generation error: {0}")]
MnemonicGeneration(#[from] MnemonicGenerationError),
#[error("Mnemonic parse error: {0}")]
MnemonicFromStr(#[from] MnemonicFromStrError),
#[error("{0}")]
InvalidMnemonicData(#[from] InvalidMnemonicData),
#[error("IO error: {0}")]
Io(#[source] std::io::Error),
#[error("Derivation path: {0}")]
DerivationPath(#[from] keyfork_derive_openpgp::derive_util::path::Error),
#[error("Derivation request: {0}")]
DerivationRequest(#[from] keyfork_derive_openpgp::derive_util::request::DerivationError),
#[error("Keyfork OpenPGP: {0}")]
KeyforkOpenPGP(#[from] keyfork_derive_openpgp::Error),
}
pub type Result<T, E = Error> = std::result::Result<T, E>;
#[derive(Debug, Clone)]
pub struct EncryptedMessage {
pkesks: Vec<PKESK>,
message: SEIP,
}
impl EncryptedMessage {
pub fn new(pkesks: &mut Vec<PKESK>, seip: SEIP) -> Self {
Self {
pkesks: std::mem::take(pkesks),
message: seip,
}
}
pub fn decrypt_with<H>(&self, policy: &'_ dyn Policy, decryptor: H) -> Result<Vec<u8>>
where
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)
.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)?
.with_policy(policy, None, decryptor)
.map_err(Error::Sequoia)?;
let mut content = vec![];
decryptor
.read_to_end(&mut content)
.map_err(Error::SequoiaIo)?;
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).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)
}
}
/// # 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.
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)
.map_err(Error::Sequoia)?
.into_children()
{
match packet {
Packet::PKESK(p) => pkesks.push(p),
Packet::SEIP(s) => {
encrypted_messages.push_back(EncryptedMessage::new(&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()
}
fn decode_metadata_v1(buf: &[u8]) -> Result<(u8, Cert, Vec<Cert>)> {
assert_eq!(
SHARD_METADATA_VERSION, buf[0],
"Incompatible metadata version"
);
let threshold = buf[1];
let mut cert_parser =
CertParser::from_bytes(&buf[SHARD_METADATA_OFFSET..]).map_err(Error::Sequoia)?;
let root_cert = match cert_parser.next() {
Some(Ok(c)) => c,
Some(Err(e)) => return Err(Error::Sequoia(e)),
None => panic!("No data found"),
};
let certs = cert_parser
.collect::<openpgp::Result<Vec<_>>>()
.map_err(Error::Sequoia)?;
Ok((threshold, root_cert, certs))
}
// NOTE: When using single-decryptor mechanism, use this method with `threshold = 1` to return a
// single message.
fn decrypt_with_manager(
threshold: u8,
messages: &mut HashMap<KeyID, EncryptedMessage>,
certs: &[Cert],
policy: &dyn Policy,
manager: &mut SmartcardManager,
) -> Result<HashMap<KeyID, Vec<u8>>> {
let mut decrypted_messages = HashMap::new();
while threshold as usize - decrypted_messages.len() > 0 {
// Build list of fingerprints that haven't yet been used for decrypting
let mut cert_by_fingerprint = HashMap::new();
let mut unused_fingerprints = vec![];
for valid_cert in certs
.iter()
.filter(|cert| !decrypted_messages.contains_key(&cert.keyid()))
.map(|cert| cert.with_policy(policy, None))
{
let valid_cert = valid_cert.map_err(Error::Sequoia)?;
let fp = valid_cert
.keys()
.for_storage_encryption()
.map(|k| k.fingerprint())
.collect::<Vec<_>>();
for fp in &fp {
cert_by_fingerprint.insert(fp.clone(), valid_cert.keyid());
}
unused_fingerprints.extend(fp.into_iter());
}
// Iterate over all fingerprints and use key_by_fingerprints to assoc with Enc. Message
if let Some(fp) = manager.load_any_fingerprint(unused_fingerprints)? {
let cert_keyid = cert_by_fingerprint.get(&fp).unwrap().clone();
if let Some(message) = messages.remove(&cert_keyid) {
let message = message.decrypt_with(policy, &mut *manager)?;
decrypted_messages.insert(cert_keyid, message);
}
}
}
Ok(decrypted_messages)
}
// NOTE: When using single-decryptor mechanism, only a single key should be provided in Keyring to
// decrypt messages with.
fn decrypt_with_keyring(
messages: &mut HashMap<KeyID, EncryptedMessage>,
certs: &[Cert],
policy: &NullPolicy,
keyring: &mut Keyring,
) -> Result<HashMap<KeyID, Vec<u8>>, Error> {
let mut decrypted_messages = HashMap::new();
for valid_cert in certs.iter().map(|cert| cert.with_policy(policy, None)) {
let valid_cert = valid_cert.map_err(Error::Sequoia)?;
let Some(secret_cert) = keyring.get_cert_for_primary_keyid(&valid_cert.keyid()) else {
continue;
};
let secret_cert = secret_cert
.with_policy(policy, None)
.map_err(Error::Sequoia)?;
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
decrypted_messages.insert(
valid_cert.keyid(),
message.decrypt_with(policy, &mut *keyring)?,
);
}
}
}
Ok(decrypted_messages)
}
fn decrypt_metadata(
message: &EncryptedMessage,
policy: &NullPolicy,
keyring: &mut Keyring,
manager: &mut SmartcardManager,
) -> Result<Vec<u8>> {
Ok(if keyring.is_empty() {
manager.load_any_card()?;
message.decrypt_with(policy, manager)?
} else {
message.decrypt_with(policy, keyring)?
})
}
fn decrypt_one(
messages: Vec<EncryptedMessage>,
certs: &[Cert],
metadata: &EncryptedMessage,
) -> Result<(Vec<u8>, u8, Cert)> {
let policy = NullPolicy::new();
let mut keyring = Keyring::new(certs)?;
let mut manager = SmartcardManager::new()?;
let content = decrypt_metadata(metadata, &policy, &mut keyring, &mut manager)?;
let (threshold, root_cert, certs) = decode_metadata_v1(&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(messages).collect();
let decrypted_messages = decrypt_with_keyring(&mut messages, &certs, &policy, &mut keyring)?;
if let Some(message) = decrypted_messages.into_values().next() {
return Ok((message, threshold, root_cert));
}
let decrypted_messages = decrypt_with_manager(1, &mut messages, &certs, &policy, &mut manager)?;
if let Some(message) = decrypted_messages.into_values().next() {
return Ok((message, threshold, root_cert));
}
unreachable!("smartcard manager should always decrypt")
}
/// # Panics
///
/// 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`].
pub fn decrypt(
certs: &[Cert],
metadata: &EncryptedMessage,
encrypted_messages: &[EncryptedMessage],
) -> Result<()> {
let mut pm = PromptManager::new(stdin(), stdout())?;
let wordlist = Wordlist::default();
let their_words = pm.prompt_wordlist("Their words: ", &wordlist)?;
let mut nonce_words = their_words.split_whitespace().take(9).peekable();
let mut pubkey_words = their_words.split_whitespace().skip(9).take(24).peekable();
let mut nonce_mnemonic = String::new();
let mut pubkey_mnemonic = String::new();
while let Some(word) = nonce_words.next() {
nonce_mnemonic.push_str(word);
if nonce_words.peek().is_some() {
nonce_mnemonic.push(' ');
}
}
while let Some(word) = pubkey_words.next() {
pubkey_mnemonic.push_str(word);
if pubkey_words.peek().is_some() {
pubkey_mnemonic.push(' ');
}
}
let their_key = Mnemonic::from_str(&pubkey_mnemonic)?.entropy();
let their_key: [u8; 32] = their_key
.try_into()
.map_err(|_| InvalidMnemonicData)?;
let their_nonce = Mnemonic::from_str(&nonce_mnemonic)?.entropy();
let their_nonce = Nonce::<U12>::from_slice(&their_nonce);
let our_key = EphemeralSecret::random();
let our_mnemonic =
Mnemonic::from_entropy(PublicKey::from(&our_key).as_bytes(), Default::default())?;
let shared_secret = our_key
.diffie_hellman(&PublicKey::from(their_key))
.to_bytes();
let (mut share, threshold, ..) = decrypt_one(encrypted_messages.to_vec(), certs, metadata)?;
share.insert(0, HUNK_VERSION);
share.insert(1, threshold);
assert!(
share.len() <= ENC_LEN as usize,
"invalid share length (too long, max {ENC_LEN} 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)?;
let bytes = shared_key.encrypt(their_nonce, share.as_slice())?;
shared_key.decrypt(their_nonce, &bytes[..])?;
// NOTE: Padding length is less than u8::MAX because ENC_LEN < u8::MAX
// NOTE: This previously used a single 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 done below by having sequentially increasing numbers up to but not
// including the last byte.
#[allow(clippy::assertions_on_constants)]
{
assert!(ENC_LEN < u8::MAX, "padding byte can be u8");
}
#[allow(clippy::cast_possible_truncation)]
let mut out_bytes = [bytes.len() as u8; ENC_LEN as usize];
assert!(
bytes.len() < out_bytes.len(),
"encrypted payload larger than acceptable limit"
);
out_bytes[..bytes.len()].clone_from_slice(&bytes);
#[allow(clippy::cast_possible_truncation)]
for (i, byte) in (out_bytes[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 mnemonic = unsafe { Mnemonic::from_raw_entropy(&out_bytes, Default::default()) };
let combined_mnemonic = format!("{our_mnemonic} {mnemonic}");
pm.prompt_message(&PromptMessage::Text(format!(
"Our words: {combined_mnemonic}"
)))?;
if let Ok(qrcode) = qrencode::qrencode(&combined_mnemonic) {
pm.prompt_message(&PromptMessage::Data(qrcode))?;
}
Ok(())
}
pub fn combine(
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 mut manager = SmartcardManager::new()?;
let content = decrypt_metadata(metadata, &policy, &mut keyring, &mut manager)?;
let (threshold, root_cert, certs) = decode_metadata_v1(&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.
let mut messages: HashMap<KeyID, EncryptedMessage> =
certs.iter().map(Cert::keyid).zip(messages).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()))?;
let secret = Sharks(threshold)
.recover(&shares)
.map_err(|e| SharksError::CombineShare(e.to_string()))?;
// TODO: extract as function
let userid = UserID::from("keyfork-sss");
let kdr = DerivationRequest::new(
DerivationAlgorithm::Ed25519,
&DerivationPath::from_str("m/7366512'/0'")?,
)
.derive_with_master_seed(secret.clone())?;
let derived_cert = keyfork_derive_openpgp::derive(
kdr,
&[KeyFlags::empty().set_certification().set_signing()],
&userid,
)?;
// NOTE: Signatures on certs will be different. Compare fingerprints instead.
let derived_fp = derived_cert.fingerprint();
let expected_fp = root_cert.fingerprint();
if derived_fp != expected_fp {
return Err(Error::InvalidSecret(derived_fp, expected_fp));
}
output
.write_all(&secret)
.map_err(Error::Io)?;
Ok(())
}
/// # Panics
///
/// The function may panic if the metadata can't properly store the certificates used to generate
/// the encrypted shares.
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()
.map_err(Error::Sequoia)?
.key()
.clone()
.into_keypair()
.map_err(Error::Sequoia)?;
let sharks = Sharks(threshold);
let dealer = sharks.dealer(secret);
let generated_shares = dealer.map(|s| Vec::from(&s)).collect::<Vec<_>>();
let policy = StandardPolicy::new();
let mut writer = Writer::new(output, Kind::Message).map_err(Error::SequoiaIo)?;
let mut total_recipients = vec![];
let mut messages = vec![];
for (share, cert) in generated_shares.iter().zip(certs) {
total_recipients.push(cert.clone());
let valid_cert = cert.with_policy(&policy, None).map_err(Error::Sequoia)?;
let encryption_keys = get_encryption_keys(&valid_cert).collect::<Vec<_>>();
let mut message_output = vec![];
let message = Message::new(&mut message_output);
let message = Encryptor2::for_recipients(
message,
encryption_keys
.iter()
.map(|k| Recipient::new(KeyID::wildcard(), k.key())),
)
.build()
.map_err(Error::Sequoia)?;
let message = Signer::new(message, signing_key.clone())
.build()
.map_err(Error::Sequoia)?;
let mut message = LiteralWriter::new(message)
.build()
.map_err(Error::Sequoia)?;
message.write_all(share).map_err(Error::SequoiaIo)?;
message.finalize().map_err(Error::Sequoia)?;
messages.push(message_output);
}
let mut pp = vec![SHARD_METADATA_VERSION, threshold];
// store derived cert to verify provided shares
derived_cert.serialize(&mut pp).map_err(Error::Sequoia)?;
for recipient in &total_recipients {
recipient.serialize(&mut pp).map_err(Error::Sequoia)?;
}
// verify packet pile
for (packet_cert, cert) in openpgp::cert::CertParser::from_bytes(&pp[2..])
.map_err(Error::Sequoia)?
.skip(1)
.zip(total_recipients.iter())
{
assert_eq!(
&packet_cert.map_err(Error::Sequoia)?,
cert,
"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<_>>>()
.map_err(Error::Sequoia)?;
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 = Encryptor2::for_recipients(message, total_recipients)
.build()
.map_err(Error::Sequoia)?;
let mut message = LiteralWriter::new(message)
.build()
.map_err(Error::Sequoia)?;
message.write_all(&pp).map_err(Error::SequoiaIo)?;
message.finalize().map_err(Error::Sequoia)?;
writer
.write_all(&message_output)
.map_err(Error::SequoiaIo)?;
for message in messages {
writer.write_all(&message).map_err(Error::SequoiaIo)?;
}
writer.finalize().map_err(Error::SequoiaIo)?;
Ok(())
}