Compare commits
No commits in common. "425aa30aa66aa5b89edfff1bc217cf7e33c0703a" and "dfcf4b1740b268d8fc167887d610b5949c59733e" have entirely different histories.
425aa30aa6
...
dfcf4b1740
|
@ -7,33 +7,52 @@ use std::{
|
||||||
process::ExitCode,
|
process::ExitCode,
|
||||||
};
|
};
|
||||||
|
|
||||||
use keyfork_shard::{openpgp::OpenPGP, Format};
|
use keyfork_shard::openpgp::{combine, discover_certs, openpgp::Cert, parse_messages};
|
||||||
|
|
||||||
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>;
|
||||||
|
|
||||||
fn validate(
|
fn validate(
|
||||||
shard: impl AsRef<Path>,
|
shard: impl AsRef<Path>,
|
||||||
key_discovery: Option<&str>,
|
key_discovery: Option<&str>,
|
||||||
) -> Result<(File, Option<PathBuf>)> {
|
) -> Result<(File, Vec<Cert>)> {
|
||||||
let key_discovery = key_discovery.map(PathBuf::from);
|
let key_discovery = key_discovery.map(PathBuf::from);
|
||||||
key_discovery.as_ref().map(std::fs::metadata).transpose()?;
|
key_discovery.as_ref().map(std::fs::metadata).transpose()?;
|
||||||
|
|
||||||
Ok((File::open(shard)?, key_discovery))
|
// Load certs from path
|
||||||
|
let certs = key_discovery
|
||||||
|
.map(discover_certs)
|
||||||
|
.transpose()?
|
||||||
|
.unwrap_or(vec![]);
|
||||||
|
|
||||||
|
Ok((File::open(shard)?, certs))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run() -> Result<()> {
|
fn run() -> Result<()> {
|
||||||
let mut args = env::args();
|
let mut args = env::args();
|
||||||
let program_name = args.next().expect("program name");
|
let program_name = args.next().expect("program name");
|
||||||
let args = args.collect::<Vec<_>>();
|
let args = args.collect::<Vec<_>>();
|
||||||
let (messages_file, key_discovery) = match args.as_slice() {
|
let (messages_file, cert_list) = match args.as_slice() {
|
||||||
[shard, key_discovery] => validate(shard, Some(key_discovery))?,
|
[shard, key_discovery] => validate(shard, Some(key_discovery))?,
|
||||||
[shard] => validate(shard, None)?,
|
[shard] => validate(shard, None)?,
|
||||||
_ => panic!("Usage: {program_name} <shard> [key_discovery]"),
|
_ => panic!("Usage: {program_name} <shard> [key_discovery]"),
|
||||||
};
|
};
|
||||||
|
|
||||||
let openpgp = OpenPGP;
|
let mut encrypted_messages = parse_messages(messages_file)?;
|
||||||
let bytes = openpgp.decrypt_all_shards_to_secret(key_discovery.as_deref(), messages_file)?;
|
|
||||||
print!("{}", smex::encode(bytes));
|
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,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
print!("{}", smex::encode(&bytes));
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,33 +7,47 @@ use std::{
|
||||||
process::ExitCode,
|
process::ExitCode,
|
||||||
};
|
};
|
||||||
|
|
||||||
use keyfork_shard::{Format, openpgp::OpenPGP};
|
use keyfork_shard::openpgp::{decrypt, discover_certs, openpgp::Cert, parse_messages};
|
||||||
|
|
||||||
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>;
|
||||||
|
|
||||||
fn validate(
|
fn validate<'a>(
|
||||||
shard: impl AsRef<Path>,
|
messages_file: impl AsRef<Path>,
|
||||||
key_discovery: Option<&str>,
|
key_discovery: impl Into<Option<&'a str>>,
|
||||||
) -> Result<(File, Option<PathBuf>)> {
|
) -> Result<(File, Vec<Cert>)> {
|
||||||
let key_discovery = key_discovery.map(PathBuf::from);
|
let key_discovery = key_discovery.into().map(PathBuf::from);
|
||||||
key_discovery.as_ref().map(std::fs::metadata).transpose()?;
|
key_discovery.as_ref().map(std::fs::metadata).transpose()?;
|
||||||
|
|
||||||
Ok((File::open(shard)?, key_discovery))
|
// Load certs from path
|
||||||
|
let certs = key_discovery
|
||||||
|
.map(discover_certs)
|
||||||
|
.transpose()?
|
||||||
|
.unwrap_or(vec![]);
|
||||||
|
|
||||||
|
Ok((File::open(messages_file)?, certs))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run() -> Result<()> {
|
fn run() -> Result<()> {
|
||||||
let mut args = env::args();
|
let mut args = env::args();
|
||||||
let program_name = args.next().expect("program name");
|
let program_name = args.next().expect("program name");
|
||||||
let args = args.collect::<Vec<_>>();
|
let args = args.collect::<Vec<_>>();
|
||||||
let (messages_file, key_discovery) = match args.as_slice() {
|
let (messages_file, cert_list) = match args.as_slice() {
|
||||||
[shard, key_discovery] => validate(shard, Some(key_discovery))?,
|
[messages_file, key_discovery] => validate(messages_file, key_discovery.as_str())?,
|
||||||
[shard] => validate(shard, None)?,
|
[messages_file] => validate(messages_file, None)?,
|
||||||
_ => panic!("Usage: {program_name} <shard> [key_discovery]"),
|
_ => panic!("Usage: {program_name} messages_file [key_discovery]"),
|
||||||
};
|
};
|
||||||
|
|
||||||
let openpgp = OpenPGP;
|
let mut encrypted_messages = parse_messages(messages_file)?;
|
||||||
|
|
||||||
openpgp.decrypt_one_shard_for_transport(key_discovery.as_deref(), messages_file)?;
|
let encrypted_metadata = encrypted_messages
|
||||||
|
.pop_front()
|
||||||
|
.expect("any pgp encrypted message");
|
||||||
|
|
||||||
|
decrypt(
|
||||||
|
&cert_list,
|
||||||
|
&encrypted_metadata,
|
||||||
|
encrypted_messages.make_contiguous(),
|
||||||
|
)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,7 +20,7 @@ fn run() -> Result<()> {
|
||||||
|
|
||||||
let mut bytes = vec![];
|
let mut bytes = vec![];
|
||||||
remote_decrypt(&mut bytes)?;
|
remote_decrypt(&mut bytes)?;
|
||||||
print!("{}", smex::encode(bytes));
|
print!("{}", smex::encode(&bytes));
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,12 +2,14 @@
|
||||||
|
|
||||||
use std::{env, path::PathBuf, process::ExitCode, str::FromStr};
|
use std::{env, path::PathBuf, process::ExitCode, str::FromStr};
|
||||||
|
|
||||||
use keyfork_shard::{Format, openpgp::OpenPGP};
|
use keyfork_shard::openpgp::{discover_certs, openpgp::Cert, split};
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
enum Error {
|
enum Error {
|
||||||
Usage(String),
|
Usage(String),
|
||||||
Input,
|
Input,
|
||||||
|
Threshold(u8, u8),
|
||||||
|
InvalidCertCount(usize, u8),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::fmt::Display for Error {
|
impl std::fmt::Display for Error {
|
||||||
|
@ -17,6 +19,15 @@ impl std::fmt::Display for Error {
|
||||||
write!(f, "Usage: {program_name} threshold max key_discovery")
|
write!(f, "Usage: {program_name} threshold max key_discovery")
|
||||||
}
|
}
|
||||||
Error::Input => f.write_str("Expected hex encoded input"),
|
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}")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -25,20 +36,31 @@ impl std::error::Error for Error {}
|
||||||
|
|
||||||
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>;
|
||||||
|
|
||||||
fn validate(threshold: &str, max: &str, key_discovery: &str) -> Result<(u8, u8, PathBuf)> {
|
fn validate(threshold: &str, max: &str, key_discovery: &str) -> Result<(u8, Vec<Cert>)> {
|
||||||
let threshold = u8::from_str(threshold)?;
|
let threshold = u8::from_str(threshold)?;
|
||||||
let max = u8::from_str(max)?;
|
let max = u8::from_str(max)?;
|
||||||
let key_discovery = PathBuf::from(key_discovery);
|
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)?;
|
std::fs::metadata(&key_discovery)?;
|
||||||
|
|
||||||
Ok((threshold, max, 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))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run() -> Result<()> {
|
fn run() -> Result<()> {
|
||||||
let mut args = env::args();
|
let mut args = env::args();
|
||||||
let program_name = args.next().expect("program name");
|
let program_name = args.next().expect("program name");
|
||||||
let args = args.collect::<Vec<_>>();
|
let args = args.collect::<Vec<_>>();
|
||||||
let (threshold, max, key_discovery) = match args.as_slice() {
|
let (threshold, cert_list) = match args.as_slice() {
|
||||||
[threshold, max, key_discovery] => validate(threshold, max, key_discovery)?,
|
[threshold, max, key_discovery] => validate(threshold, max, key_discovery)?,
|
||||||
_ => return Err(Error::Usage(program_name).into()),
|
_ => return Err(Error::Usage(program_name).into()),
|
||||||
};
|
};
|
||||||
|
@ -50,9 +72,8 @@ fn run() -> Result<()> {
|
||||||
smex::decode(line?)?
|
smex::decode(line?)?
|
||||||
};
|
};
|
||||||
|
|
||||||
let openpgp = OpenPGP;
|
split(threshold, cert_list, &input, std::io::stdout())?;
|
||||||
|
|
||||||
openpgp.shard_and_encrypt(threshold, max, &input, key_discovery.as_path(), std::io::stdout())?;
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
#![doc = include_str!("../README.md")]
|
#![doc = include_str!("../README.md")]
|
||||||
|
|
||||||
use std::io::{stdin, stdout, Read, Write};
|
use std::io::{stdin, stdout, Write};
|
||||||
|
|
||||||
use aes_gcm::{
|
use aes_gcm::{
|
||||||
aead::{consts::U12, Aead, AeadCore, OsRng},
|
aead::{Aead, AeadCore, OsRng},
|
||||||
Aes256Gcm, KeyInit, Nonce,
|
Aes256Gcm, KeyInit,
|
||||||
};
|
};
|
||||||
use hkdf::Hkdf;
|
use hkdf::Hkdf;
|
||||||
use keyfork_mnemonic_util::{English, Mnemonic};
|
use keyfork_mnemonic_util::{English, Mnemonic};
|
||||||
|
@ -16,338 +16,9 @@ use sha2::Sha256;
|
||||||
use sharks::{Share, Sharks};
|
use sharks::{Share, Sharks};
|
||||||
use x25519_dalek::{EphemeralSecret, PublicKey};
|
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")]
|
#[cfg(feature = "openpgp")]
|
||||||
pub mod 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())?;
|
|
||||||
|
|
||||||
// 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::<English, _>(
|
|
||||||
QRCODE_COULDNT_READ,
|
|
||||||
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_bytes(PublicKey::from(&our_key).as_bytes())?;
|
|
||||||
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_bytes(&out_bytes) };
|
|
||||||
|
|
||||||
#[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.
|
/// Errors encountered while creating or combining shares using Shamir's Secret Sharing.
|
||||||
#[derive(thiserror::Error, Debug)]
|
#[derive(thiserror::Error, Debug)]
|
||||||
pub enum SharksError {
|
pub enum SharksError {
|
||||||
|
|
|
@ -1,16 +1,27 @@
|
||||||
//! OpenPGP Shard functionality.
|
//! OpenPGP Shard functionality.
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
collections::HashMap,
|
collections::{HashMap, VecDeque},
|
||||||
io::{Read, Write},
|
io::{stdin, stdout, Read, Write},
|
||||||
path::Path,
|
path::Path,
|
||||||
str::FromStr,
|
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 keyfork_derive_openpgp::{
|
use keyfork_derive_openpgp::{
|
||||||
derive_util::{DerivationPath, VariableLengthSeed},
|
derive_util::{DerivationPath, PathError, VariableLengthSeed},
|
||||||
XPrv,
|
XPrv,
|
||||||
};
|
};
|
||||||
|
use keyfork_mnemonic_util::{English, Mnemonic, MnemonicFromStrError, MnemonicGenerationError};
|
||||||
|
use keyfork_prompt::{
|
||||||
|
validators::{mnemonic::MnemonicSetValidator, Validator},
|
||||||
|
Error as PromptError, Message as PromptMessage, PromptHandler, Terminal,
|
||||||
|
};
|
||||||
use openpgp::{
|
use openpgp::{
|
||||||
armor::{Kind, Writer},
|
armor::{Kind, Writer},
|
||||||
cert::{Cert, CertParser, ValidCert},
|
cert::{Cert, CertParser, ValidCert},
|
||||||
|
@ -25,10 +36,12 @@ use openpgp::{
|
||||||
Marshal,
|
Marshal,
|
||||||
},
|
},
|
||||||
types::KeyFlags,
|
types::KeyFlags,
|
||||||
KeyID, PacketPile,
|
Fingerprint, KeyID, PacketPile,
|
||||||
};
|
};
|
||||||
pub use sequoia_openpgp as openpgp;
|
pub use sequoia_openpgp as openpgp;
|
||||||
use sharks::Share;
|
use sha2::Sha256;
|
||||||
|
use sharks::{Share, Sharks};
|
||||||
|
use x25519_dalek::{EphemeralSecret, PublicKey};
|
||||||
|
|
||||||
mod keyring;
|
mod keyring;
|
||||||
use keyring::Keyring;
|
use keyring::Keyring;
|
||||||
|
@ -43,7 +56,13 @@ use smartcard::SmartcardManager;
|
||||||
const SHARD_METADATA_VERSION: u8 = 1;
|
const SHARD_METADATA_VERSION: u8 = 1;
|
||||||
const SHARD_METADATA_OFFSET: usize = 2;
|
const SHARD_METADATA_OFFSET: usize = 2;
|
||||||
|
|
||||||
use super::{Format, KeyDiscovery, SharksError};
|
use super::{
|
||||||
|
InvalidData, SharksError, HUNK_VERSION, QRCODE_COULDNT_READ, QRCODE_ERROR, QRCODE_PROMPT,
|
||||||
|
QRCODE_TIMEOUT,
|
||||||
|
};
|
||||||
|
|
||||||
|
// 256 bit share is 49 bytes + some amount of hunk bytes, gives us reasonable padding
|
||||||
|
const ENC_LEN: u8 = 4 * 16;
|
||||||
|
|
||||||
/// Errors encountered while performing operations using OpenPGP.
|
/// Errors encountered while performing operations using OpenPGP.
|
||||||
#[derive(Debug, thiserror::Error)]
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
@ -52,6 +71,22 @@ pub enum Error {
|
||||||
#[error("{0}")]
|
#[error("{0}")]
|
||||||
Sharks(#[from] SharksError),
|
Sharks(#[from] SharksError),
|
||||||
|
|
||||||
|
/// Unable to decrypt a share.
|
||||||
|
#[error("Error decrypting share: {0}")]
|
||||||
|
SymDecryptShare(#[from] AesError),
|
||||||
|
|
||||||
|
/// The generated AES key is of an invalid length.
|
||||||
|
#[error("Invalid length of AES key: {0}")]
|
||||||
|
AesLength(#[from] InvalidLength),
|
||||||
|
|
||||||
|
/// The HKDF function was given an input of an invalid length.
|
||||||
|
#[error("Invalid KDF length: {0}")]
|
||||||
|
HkdfLength(#[from] HkdfInvalidLength),
|
||||||
|
|
||||||
|
/// The secret did not match the previously-known secret fingerprint.
|
||||||
|
#[error("Derived secret hash {0} != expected {1}")]
|
||||||
|
InvalidSecret(Fingerprint, Fingerprint),
|
||||||
|
|
||||||
/// An error occurred while performing an OpenPGP operation.
|
/// An error occurred while performing an OpenPGP operation.
|
||||||
#[error("OpenPGP error: {0}")]
|
#[error("OpenPGP error: {0}")]
|
||||||
Sequoia(#[source] anyhow::Error),
|
Sequoia(#[source] anyhow::Error),
|
||||||
|
@ -68,9 +103,45 @@ pub enum Error {
|
||||||
#[error("Smartcard error: {0}")]
|
#[error("Smartcard error: {0}")]
|
||||||
Smartcard(#[from] smartcard::Error),
|
Smartcard(#[from] smartcard::Error),
|
||||||
|
|
||||||
|
/// An error occurred while displaying a prompt.
|
||||||
|
#[error("Prompt error: {0}")]
|
||||||
|
Prompt(#[from] PromptError),
|
||||||
|
|
||||||
|
/// An error occurred while generating a mnemonic.
|
||||||
|
#[error("Mnemonic generation error: {0}")]
|
||||||
|
MnemonicGeneration(#[from] MnemonicGenerationError),
|
||||||
|
|
||||||
|
/// An error occurred while parsing a mnemonic.
|
||||||
|
#[error("Mnemonic parse error: {0}")]
|
||||||
|
MnemonicFromStr(#[from] MnemonicFromStrError),
|
||||||
|
|
||||||
|
/// An error occurred while converting mnemonic data.
|
||||||
|
#[error("{0}")]
|
||||||
|
InvalidMnemonicData(#[from] InvalidData),
|
||||||
|
|
||||||
/// An IO error occurred.
|
/// An IO error occurred.
|
||||||
#[error("IO error: {0}")]
|
#[error("IO error: {0}")]
|
||||||
Io(#[source] std::io::Error),
|
Io(#[source] std::io::Error),
|
||||||
|
|
||||||
|
/// An error occurred while deriving data.
|
||||||
|
#[error("Derivation: {0}")]
|
||||||
|
Derivation(#[from] keyfork_derive_openpgp::derive_util::extended_key::private_key::Error),
|
||||||
|
|
||||||
|
/// An error occurred while parsing a derivation path.
|
||||||
|
#[error("Derivation path: {0}")]
|
||||||
|
DerivationPath(#[from] PathError),
|
||||||
|
|
||||||
|
/// An error occurred while requesting derivation.
|
||||||
|
#[error("Derivation request: {0}")]
|
||||||
|
DerivationRequest(#[from] keyfork_derive_openpgp::derive_util::request::DerivationError),
|
||||||
|
|
||||||
|
/// An error occurred while decoding hex.
|
||||||
|
#[error("Unable to decode hex: {0}")]
|
||||||
|
HexDecode(#[from] smex::DecodeError),
|
||||||
|
|
||||||
|
/// An error occurred while creating an OpenPGP cert.
|
||||||
|
#[error("Keyfork OpenPGP: {0}")]
|
||||||
|
KeyforkOpenPGP(#[from] keyfork_derive_openpgp::Error),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(missing_docs)]
|
#[allow(missing_docs)]
|
||||||
|
@ -92,61 +163,6 @@ impl EncryptedMessage {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parse OpenPGP packets for encrypted messages.
|
|
||||||
///
|
|
||||||
/// # Errors
|
|
||||||
/// The function may return an error if Sequoia is unable to parse packets.
|
|
||||||
///
|
|
||||||
/// # Panics
|
|
||||||
/// The function may panic if an unexpected packet is encountered.
|
|
||||||
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.
|
/// Decrypt the message with a Sequoia policy and decryptor.
|
||||||
///
|
///
|
||||||
/// This method creates a container containing the packets and passes the serialized container
|
/// This method creates a container containing the packets and passes the serialized container
|
||||||
|
@ -160,7 +176,23 @@ impl EncryptedMessage {
|
||||||
H: VerificationHelper + DecryptionHelper,
|
H: VerificationHelper + DecryptionHelper,
|
||||||
{
|
{
|
||||||
let mut packets = vec![];
|
let mut packets = vec![];
|
||||||
self.serialize(&mut packets).map_err(Error::Sequoia)?;
|
|
||||||
|
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)
|
let mut decryptor = DecryptorBuilder::from_bytes(&packets)
|
||||||
.map_err(Error::Sequoia)?
|
.map_err(Error::Sequoia)?
|
||||||
|
@ -175,17 +207,13 @@ impl EncryptedMessage {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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.
|
||||||
///
|
///
|
||||||
pub struct OpenPGP;
|
/// # Errors
|
||||||
|
/// The function may return an error if it is unable to read the directory or if Sequoia is unable
|
||||||
impl OpenPGP {
|
/// to load certificates from the file.
|
||||||
/// Read all OpenPGP certificates in a path and return a [`Vec`] of them. Certificates are read
|
pub fn discover_certs(path: impl AsRef<Path>) -> Result<Vec<Cert>> {
|
||||||
/// 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();
|
let path = path.as_ref();
|
||||||
|
|
||||||
if path.is_file() {
|
if path.is_file() {
|
||||||
|
@ -206,317 +234,37 @@ impl OpenPGP {
|
||||||
}
|
}
|
||||||
Ok(vec)
|
Ok(vec)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Format for OpenPGP {
|
/// Parse messages from a type implementing [`Read`] and store them as [`EncryptedMessage`].
|
||||||
type Error = Error;
|
///
|
||||||
type PublicKey = Cert;
|
/// # Errors
|
||||||
type PrivateKeyData = Vec<Cert>;
|
/// The function may return an error if the reader has run out of data or if the data is not
|
||||||
type SigningKey = Cert;
|
/// properly formatted OpenPGP messages.
|
||||||
type EncryptedData = EncryptedMessage;
|
///
|
||||||
|
/// # 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();
|
||||||
|
|
||||||
/// Derive an OpenPGP Shard certificate from the given seed.
|
for packet in PacketPile::from_reader(reader)
|
||||||
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)?
|
.map_err(Error::Sequoia)?
|
||||||
.into_children()
|
.into_children()
|
||||||
{
|
{
|
||||||
match packet {
|
match packet {
|
||||||
Packet::PKESK(p) => pkesks.push(p),
|
Packet::PKESK(p) => pkesks.push(p),
|
||||||
Packet::SEIP(s) => return Ok(EncryptedMessage::new(&mut pkesks, s)),
|
Packet::SEIP(s) => {
|
||||||
s => panic!("Invalid variant found: {}", s.tag()),
|
encrypted_messages.push_back(EncryptedMessage::new(&mut pkesks, s));
|
||||||
|
}
|
||||||
|
s => {
|
||||||
|
panic!("Invalid variant found: {}", s.tag());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
panic!("Unable to build EncryptedMessage from PacketPile");
|
Ok(encrypted_messages)
|
||||||
}
|
|
||||||
|
|
||||||
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())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_encryption_keys<'a>(
|
fn get_encryption_keys<'a>(
|
||||||
|
@ -667,3 +415,378 @@ fn decrypt_metadata(
|
||||||
message.decrypt_with(policy, keyring)?
|
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")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Decrypt a single shard, encrypt to a remote operator, and present the transport shard as a QR
|
||||||
|
/// code and mnemonic to be sent to the remote operator.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// The function may error if an error occurs while displaying a prompt or while decrypting the
|
||||||
|
/// shard. An error will not be returned if the camera has a hardware error while scanning a QR
|
||||||
|
/// code; instead, a mnemonic prompt will be used.
|
||||||
|
///
|
||||||
|
/// # 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 = Terminal::new(stdin(), stdout())?;
|
||||||
|
|
||||||
|
let mut nonce_data: Option<[u8; 12]> = None;
|
||||||
|
let mut pubkey_data: Option<[u8; 32]> = None;
|
||||||
|
|
||||||
|
#[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(QRCODE_TIMEOUT), 0)
|
||||||
|
{
|
||||||
|
let decoded_data = smex::decode(&hex)?;
|
||||||
|
let _ = nonce_data.insert(decoded_data[..12].try_into().map_err(|_| InvalidData)?);
|
||||||
|
let _ = pubkey_data.insert(decoded_data[12..].try_into().map_err(|_| InvalidData)?);
|
||||||
|
} else {
|
||||||
|
pm.prompt_message(PromptMessage::Text(QRCODE_ERROR.to_string()))?;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let (nonce, 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::<English, _>(
|
||||||
|
QRCODE_COULDNT_READ,
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let nonce = Nonce::<U12>::from_slice(&nonce);
|
||||||
|
|
||||||
|
let our_key = EphemeralSecret::random();
|
||||||
|
let our_pubkey_mnemonic = Mnemonic::from_bytes(PublicKey::from(&our_key).as_bytes())?;
|
||||||
|
|
||||||
|
let shared_secret = our_key.diffie_hellman(&PublicKey::from(pubkey)).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(nonce, share.as_slice())?;
|
||||||
|
shared_key.decrypt(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 payload_mnemonic = unsafe { Mnemonic::from_raw_bytes(&out_bytes) };
|
||||||
|
|
||||||
|
#[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(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Combine mulitple shards into a secret.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// The function may return an error if an error occurs while decrypting shards, parsing shards, or
|
||||||
|
/// combining the shards into a secret.
|
||||||
|
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 path = DerivationPath::from_str("m/7366512'/0'")?;
|
||||||
|
let seed = VariableLengthSeed::new(&secret);
|
||||||
|
let xprv = XPrv::new(seed).derive_path(&path)?;
|
||||||
|
let derived_cert = keyfork_derive_openpgp::derive(
|
||||||
|
xprv,
|
||||||
|
&[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(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Split a secret into an OpenPGP formatted Shard file.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// The function may return an error if the shards can't be encrypted to the provided OpenPGP
|
||||||
|
/// certs or if an error happens while writing the Shard file.
|
||||||
|
///
|
||||||
|
/// # 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<()> {
|
||||||
|
let seed = VariableLengthSeed::new(secret);
|
||||||
|
// build cert to sign encrypted shares
|
||||||
|
let userid = UserID::from("keyfork-sss");
|
||||||
|
let path = DerivationPath::from_str("m/7366512'/0'")?;
|
||||||
|
let xprv = XPrv::new(seed).derive_path(&path)?;
|
||||||
|
let derived_cert = keyfork_derive_openpgp::derive(
|
||||||
|
xprv,
|
||||||
|
&[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(())
|
||||||
|
}
|
||||||
|
|
|
@ -111,10 +111,12 @@ impl DecryptionHelper for &mut Keyring {
|
||||||
pkesk.recipient().is_wildcard()
|
pkesk.recipient().is_wildcard()
|
||||||
|| cert.keys().any(|k| &k.keyid() == pkesk.recipient())
|
|| cert.keys().any(|k| &k.keyid() == pkesk.recipient())
|
||||||
}) {
|
}) {
|
||||||
|
#[allow(deprecated, clippy::map_flatten)]
|
||||||
let name = cert
|
let name = cert
|
||||||
.userids()
|
.userids()
|
||||||
.next()
|
.next()
|
||||||
.and_then(|userid| userid.userid().name2().transpose())
|
.map(|userid| userid.userid().name().transpose())
|
||||||
|
.flatten()
|
||||||
.transpose()
|
.transpose()
|
||||||
.ok()
|
.ok()
|
||||||
.flatten();
|
.flatten();
|
||||||
|
|
|
@ -3,7 +3,10 @@ use clap::{Parser, Subcommand};
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use keyfork_mnemonic_util::{English, Mnemonic};
|
use keyfork_mnemonic_util::{English, Mnemonic};
|
||||||
use keyfork_shard::{remote_decrypt, Format};
|
use keyfork_shard::{
|
||||||
|
openpgp::{combine, discover_certs, parse_messages},
|
||||||
|
remote_decrypt,
|
||||||
|
};
|
||||||
|
|
||||||
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>;
|
||||||
|
|
||||||
|
@ -34,10 +37,15 @@ impl RecoverSubcommands {
|
||||||
} => {
|
} => {
|
||||||
let content = std::fs::read_to_string(shard_file)?;
|
let content = std::fs::read_to_string(shard_file)?;
|
||||||
if content.contains("BEGIN PGP MESSAGE") {
|
if content.contains("BEGIN PGP MESSAGE") {
|
||||||
let openpgp = keyfork_shard::openpgp::OpenPGP;
|
let certs = key_discovery
|
||||||
// TODO: remove .clone() by making handle() consume self
|
.as_ref()
|
||||||
let seed = openpgp
|
.map(discover_certs)
|
||||||
.decrypt_all_shards_to_secret(key_discovery.as_deref(), content.as_bytes())?;
|
.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)?;
|
||||||
Ok(seed)
|
Ok(seed)
|
||||||
} else {
|
} else {
|
||||||
panic!("unknown format of shard file");
|
panic!("unknown format of shard file");
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
use super::Keyfork;
|
use super::Keyfork;
|
||||||
use clap::{builder::PossibleValue, Parser, Subcommand, ValueEnum};
|
use clap::{builder::PossibleValue, Parser, Subcommand, ValueEnum};
|
||||||
use keyfork_shard::Format as _;
|
|
||||||
use std::{
|
use std::{
|
||||||
io::{stdin, stdout, Read, Write},
|
io::{stdin, stdout, Read, Write},
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
|
@ -32,23 +31,27 @@ trait ShardExec {
|
||||||
&self,
|
&self,
|
||||||
threshold: u8,
|
threshold: u8,
|
||||||
max: u8,
|
max: u8,
|
||||||
key_discovery: &Path,
|
key_discovery: impl AsRef<Path>,
|
||||||
secret: &[u8],
|
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,
|
output: &mut impl Write,
|
||||||
) -> Result<(), Box<dyn std::error::Error>>;
|
) -> Result<(), Box<dyn std::error::Error>>;
|
||||||
|
|
||||||
fn decrypt(
|
fn combine<T>(
|
||||||
&self,
|
&self,
|
||||||
key_discovery: Option<&Path>,
|
key_discovery: Option<T>,
|
||||||
input: impl Read + Send + Sync,
|
input: impl Read + Send + Sync,
|
||||||
) -> Result<(), Box<dyn std::error::Error>>;
|
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>;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
|
@ -59,36 +62,77 @@ impl ShardExec for OpenPGP {
|
||||||
&self,
|
&self,
|
||||||
threshold: u8,
|
threshold: u8,
|
||||||
max: u8,
|
max: u8,
|
||||||
key_discovery: &Path,
|
key_discovery: impl AsRef<Path>,
|
||||||
secret: &[u8],
|
secret: &[u8],
|
||||||
output: &mut (impl Write + Send + Sync),
|
output: &mut impl Write,
|
||||||
) -> Result<(), Box<dyn std::error::Error>> {
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let opgp = keyfork_shard::openpgp::OpenPGP;
|
// Get certs and input
|
||||||
opgp.shard_and_encrypt(threshold, max, secret, key_discovery, output)
|
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn combine(
|
fn combine<T>(
|
||||||
&self,
|
&self,
|
||||||
key_discovery: Option<&Path>,
|
key_discovery: Option<T>,
|
||||||
input: impl Read + Send + Sync,
|
input: impl Read + Send + Sync,
|
||||||
output: &mut impl Write,
|
output: &mut impl Write,
|
||||||
) -> Result<(), Box<dyn std::error::Error>>
|
) -> Result<(), Box<dyn std::error::Error>>
|
||||||
|
where
|
||||||
|
T: AsRef<Path>,
|
||||||
{
|
{
|
||||||
let openpgp = keyfork_shard::openpgp::OpenPGP;
|
let certs = key_discovery
|
||||||
let bytes = openpgp.decrypt_all_shards_to_secret(key_discovery, input)?;
|
.map(|kd| keyfork_shard::openpgp::discover_certs(kd.as_ref()))
|
||||||
write!(output, "{}", smex::encode(bytes))?;
|
.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,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
write!(output, "{}", smex::encode(&bytes))?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn decrypt(
|
fn decrypt<T>(
|
||||||
&self,
|
&self,
|
||||||
key_discovery: Option<&Path>,
|
key_discovery: Option<T>,
|
||||||
input: impl Read + Send + Sync,
|
input: impl Read + Send + Sync,
|
||||||
) -> Result<(), Box<dyn std::error::Error>>
|
) -> Result<(), Box<dyn std::error::Error>>
|
||||||
|
where
|
||||||
|
T: AsRef<Path>,
|
||||||
{
|
{
|
||||||
let openpgp = keyfork_shard::openpgp::OpenPGP;
|
let certs = key_discovery
|
||||||
openpgp.decrypt_one_shard_for_transport(key_discovery, input)?;
|
.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(),
|
||||||
|
)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -181,7 +225,7 @@ impl ShardSubcommands {
|
||||||
|
|
||||||
match format {
|
match format {
|
||||||
Some(Format::OpenPGP(o)) => {
|
Some(Format::OpenPGP(o)) => {
|
||||||
o.decrypt(key_discovery.as_deref(), shard_content.as_bytes())
|
o.decrypt(key_discovery.as_ref(), shard_content.as_bytes())
|
||||||
}
|
}
|
||||||
Some(Format::P256(_p)) => todo!(),
|
Some(Format::P256(_p)) => todo!(),
|
||||||
None => panic!("{COULD_NOT_DETERMINE_FORMAT}"),
|
None => panic!("{COULD_NOT_DETERMINE_FORMAT}"),
|
||||||
|
@ -198,7 +242,7 @@ impl ShardSubcommands {
|
||||||
|
|
||||||
match format {
|
match format {
|
||||||
Some(Format::OpenPGP(o)) => o.combine(
|
Some(Format::OpenPGP(o)) => o.combine(
|
||||||
key_discovery.as_deref(),
|
key_discovery.as_ref(),
|
||||||
shard_content.as_bytes(),
|
shard_content.as_bytes(),
|
||||||
&mut stdout,
|
&mut stdout,
|
||||||
),
|
),
|
||||||
|
|
|
@ -15,8 +15,6 @@ use keyfork_prompt::{
|
||||||
Message, PromptHandler, Terminal,
|
Message, PromptHandler, Terminal,
|
||||||
};
|
};
|
||||||
|
|
||||||
use keyfork_shard::{Format, openpgp::OpenPGP};
|
|
||||||
|
|
||||||
#[derive(thiserror::Error, Debug)]
|
#[derive(thiserror::Error, Debug)]
|
||||||
#[error("Invalid PIN length: {0}")]
|
#[error("Invalid PIN length: {0}")]
|
||||||
pub struct PinLength(usize);
|
pub struct PinLength(usize);
|
||||||
|
@ -165,13 +163,11 @@ fn generate_shard_secret(
|
||||||
certs.push(cert);
|
certs.push(cert);
|
||||||
}
|
}
|
||||||
|
|
||||||
let opgp = OpenPGP;
|
|
||||||
|
|
||||||
if let Some(output_file) = output_file {
|
if let Some(output_file) = output_file {
|
||||||
let output = File::create(output_file)?;
|
let output = File::create(output_file)?;
|
||||||
opgp.shard_and_encrypt(threshold, certs.len() as u8, &seed, &certs[..], output)?;
|
keyfork_shard::openpgp::split(threshold, certs, &seed, output)?;
|
||||||
} else {
|
} else {
|
||||||
opgp.shard_and_encrypt(threshold, certs.len() as u8, &seed, &certs[..], std::io::stdout())?;
|
keyfork_shard::openpgp::split(threshold, certs, &seed, std::io::stdout())?;
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue