keyfork-shard: traitify functionality
This commit is contained in:
parent
dfcf4b1740
commit
6093cf9be4
|
@ -7,50 +7,33 @@ use std::{
|
||||||
process::ExitCode,
|
process::ExitCode,
|
||||||
};
|
};
|
||||||
|
|
||||||
use keyfork_shard::openpgp::{combine, discover_certs, openpgp::Cert, parse_messages};
|
use keyfork_shard::{Format, openpgp::OpenPGP};
|
||||||
|
|
||||||
type Result<T, E = Box<dyn std::error::Error>> = std::result::Result<T, E>;
|
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, Vec<Cert>)> {
|
) -> Result<(File, Option<PathBuf>)> {
|
||||||
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()?;
|
||||||
|
|
||||||
// Load certs from path
|
Ok((File::open(shard)?, key_discovery))
|
||||||
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, cert_list) = match args.as_slice() {
|
let (messages_file, key_discovery) = 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 mut encrypted_messages = parse_messages(messages_file)?;
|
let openpgp = OpenPGP;
|
||||||
|
|
||||||
let encrypted_metadata = encrypted_messages
|
let bytes = openpgp.decrypt_all_shards_to_secret(key_discovery, messages_file)?;
|
||||||
.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));
|
print!("{}", smex::encode(&bytes));
|
||||||
|
|
||||||
|
|
|
@ -7,47 +7,33 @@ use std::{
|
||||||
process::ExitCode,
|
process::ExitCode,
|
||||||
};
|
};
|
||||||
|
|
||||||
use keyfork_shard::openpgp::{decrypt, discover_certs, openpgp::Cert, parse_messages};
|
use keyfork_shard::{Format, openpgp::OpenPGP};
|
||||||
|
|
||||||
type Result<T, E = Box<dyn std::error::Error>> = std::result::Result<T, E>;
|
type Result<T, E = Box<dyn std::error::Error>> = std::result::Result<T, E>;
|
||||||
|
|
||||||
fn validate<'a>(
|
fn validate(
|
||||||
messages_file: impl AsRef<Path>,
|
shard: impl AsRef<Path>,
|
||||||
key_discovery: impl Into<Option<&'a str>>,
|
key_discovery: Option<&str>,
|
||||||
) -> Result<(File, Vec<Cert>)> {
|
) -> Result<(File, Option<PathBuf>)> {
|
||||||
let key_discovery = key_discovery.into().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()?;
|
||||||
|
|
||||||
// Load certs from path
|
Ok((File::open(shard)?, key_discovery))
|
||||||
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, cert_list) = match args.as_slice() {
|
let (messages_file, key_discovery) = match args.as_slice() {
|
||||||
[messages_file, key_discovery] => validate(messages_file, key_discovery.as_str())?,
|
[shard, key_discovery] => validate(shard, Some(key_discovery))?,
|
||||||
[messages_file] => validate(messages_file, None)?,
|
[shard] => validate(shard, None)?,
|
||||||
_ => panic!("Usage: {program_name} messages_file [key_discovery]"),
|
_ => panic!("Usage: {program_name} <shard> [key_discovery]"),
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut encrypted_messages = parse_messages(messages_file)?;
|
let openpgp = OpenPGP;
|
||||||
|
|
||||||
let encrypted_metadata = encrypted_messages
|
openpgp.decrypt_one_shard_for_transport(key_discovery, messages_file)?;
|
||||||
.pop_front()
|
|
||||||
.expect("any pgp encrypted message");
|
|
||||||
|
|
||||||
decrypt(
|
|
||||||
&cert_list,
|
|
||||||
&encrypted_metadata,
|
|
||||||
encrypted_messages.make_contiguous(),
|
|
||||||
)?;
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,13 @@
|
||||||
#![doc = include_str!("../README.md")]
|
#![doc = include_str!("../README.md")]
|
||||||
|
|
||||||
use std::io::{stdin, stdout, Write};
|
use std::{
|
||||||
|
io::{stdin, stdout, Read, Write},
|
||||||
|
path::Path,
|
||||||
|
};
|
||||||
|
|
||||||
use aes_gcm::{
|
use aes_gcm::{
|
||||||
aead::{Aead, AeadCore, OsRng},
|
aead::{consts::U12, Aead, AeadCore, OsRng},
|
||||||
Aes256Gcm, KeyInit,
|
Aes256Gcm, KeyInit, Nonce,
|
||||||
};
|
};
|
||||||
use hkdf::Hkdf;
|
use hkdf::Hkdf;
|
||||||
use keyfork_mnemonic_util::{English, Mnemonic};
|
use keyfork_mnemonic_util::{English, Mnemonic};
|
||||||
|
@ -16,9 +19,287 @@ 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 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 the public key recipients of shards.
|
||||||
|
type PublicKeyData;
|
||||||
|
|
||||||
|
/// A type encapsulating the private key recipients of shards.
|
||||||
|
type PrivateKeyData;
|
||||||
|
|
||||||
|
/// A type representing the parsed, but encrypted, Shard data.
|
||||||
|
type ShardData;
|
||||||
|
|
||||||
|
/// A type representing a Signer derived from the secret.
|
||||||
|
type Signer;
|
||||||
|
|
||||||
|
/// Parse the public key data from a readable type.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// The method may return an error if private key data could not be properly parsed from the
|
||||||
|
/// path.
|
||||||
|
/// occurred while parsing the public key data.
|
||||||
|
fn parse_public_key_data(
|
||||||
|
&self,
|
||||||
|
key_data_path: impl AsRef<Path>,
|
||||||
|
) -> Result<Self::PublicKeyData, Self::Error>;
|
||||||
|
|
||||||
|
/// Parse the private key data from a readable type. The private key may not be accessible (it
|
||||||
|
/// may be hardware only, such as a smartcard), for which this method may return None.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// The method may return an error if private key data could not be properly parsed from the
|
||||||
|
/// path.
|
||||||
|
fn parse_private_key_data(
|
||||||
|
&self,
|
||||||
|
key_data_path: impl AsRef<Path>,
|
||||||
|
) -> Result<Self::PrivateKeyData, 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<Self::ShardData, 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,
|
||||||
|
shard_data: Self::ShardData,
|
||||||
|
shard_file: impl Write,
|
||||||
|
) -> Result<(), Self::Error>;
|
||||||
|
|
||||||
|
/// Derive a Signer from the secret.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// This function may return an error if a Signer could not be properly created.
|
||||||
|
fn derive_signer(&self, secret: &[u8]) -> Result<Self::Signer, Self::Error>;
|
||||||
|
|
||||||
|
/// Encrypt multiple shares to public keys.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// The method may return an error if the share could not be encrypted to a public key or if
|
||||||
|
/// the ShardData could not be created.
|
||||||
|
fn generate_shard_data(
|
||||||
|
&self,
|
||||||
|
shares: &[Share],
|
||||||
|
signer: &Self::Signer,
|
||||||
|
public_keys: Self::PublicKeyData,
|
||||||
|
) -> Result<Self::ShardData, 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>,
|
||||||
|
shard_data: Self::ShardData,
|
||||||
|
) -> 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>,
|
||||||
|
shard_data: Self::ShardData,
|
||||||
|
) -> 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_data_path: Option<impl AsRef<Path>>,
|
||||||
|
reader: impl Read + Send + Sync,
|
||||||
|
) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
|
||||||
|
let private_keys = private_key_data_path
|
||||||
|
.map(|p| self.parse_private_key_data(p))
|
||||||
|
.transpose()?;
|
||||||
|
let shard_data = self.parse_shard_file(reader)?;
|
||||||
|
let (shares, threshold) = self.decrypt_all_shards(private_keys, shard_data)?;
|
||||||
|
|
||||||
|
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_data_path: Option<impl AsRef<Path>>,
|
||||||
|
reader: impl Read + Send + Sync,
|
||||||
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let mut pm = Terminal::new(stdin(), stdout())?;
|
||||||
|
let wordlist = Wordlist::default();
|
||||||
|
|
||||||
|
// parse input
|
||||||
|
let private_keys = private_key_data_path
|
||||||
|
.map(|p| self.parse_private_key_data(p))
|
||||||
|
.transpose()?;
|
||||||
|
let shard_data = self.parse_shard_file(reader)?;
|
||||||
|
|
||||||
|
// establish AES-256-GCM key via ECDH
|
||||||
|
let mut nonce_data: Option<[u8; 12]> = None;
|
||||||
|
let mut pubkey_data: Option<[u8; 32]> = None;
|
||||||
|
|
||||||
|
// receive remote data via scanning QR code from camera
|
||||||
|
#[cfg(feature = "qrcode")]
|
||||||
|
{
|
||||||
|
pm.prompt_message(PromptMessage::Text(QRCODE_PROMPT.to_string()))?;
|
||||||
|
if let Ok(Some(hex)) =
|
||||||
|
keyfork_qrcode::scan_camera(std::time::Duration::from_secs(30), 0)
|
||||||
|
{
|
||||||
|
let decoded_data = smex::decode(&hex)?;
|
||||||
|
nonce_data = Some(decoded_data[..12].try_into().map_err(|_| InvalidData)?);
|
||||||
|
pubkey_data = Some(decoded_data[12..].try_into().map_err(|_| InvalidData)?)
|
||||||
|
} else {
|
||||||
|
pm.prompt_message(PromptMessage::Text(QRCODE_ERROR.to_string()))?;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// if QR code scanning failed or was unavailable, read from a set of mnemonics
|
||||||
|
let (nonce, their_pubkey) = match (nonce_data, pubkey_data) {
|
||||||
|
(Some(nonce), Some(pubkey)) => (nonce, pubkey),
|
||||||
|
_ => {
|
||||||
|
let validator = MnemonicSetValidator {
|
||||||
|
word_lengths: [9, 24],
|
||||||
|
};
|
||||||
|
let [nonce_mnemonic, pubkey_mnemonic] = pm.prompt_validated_wordlist(
|
||||||
|
QRCODE_COULDNT_READ,
|
||||||
|
&wordlist,
|
||||||
|
3,
|
||||||
|
validator.to_fn(),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let nonce = nonce_mnemonic
|
||||||
|
.as_bytes()
|
||||||
|
.try_into()
|
||||||
|
.map_err(|_| InvalidData)?;
|
||||||
|
let pubkey = pubkey_mnemonic
|
||||||
|
.as_bytes()
|
||||||
|
.try_into()
|
||||||
|
.map_err(|_| InvalidData)?;
|
||||||
|
(nonce, pubkey)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// create our shared key
|
||||||
|
let our_key = EphemeralSecret::random();
|
||||||
|
let our_pubkey_mnemonic =
|
||||||
|
Mnemonic::from_entropy(PublicKey::from(&our_key).as_bytes(), Default::default())?;
|
||||||
|
let shared_secret = our_key
|
||||||
|
.diffie_hellman(&PublicKey::from(their_pubkey))
|
||||||
|
.to_bytes();
|
||||||
|
let hkdf = Hkdf::<Sha256>::new(None, &shared_secret);
|
||||||
|
let mut hkdf_output = [0u8; 256 / 8];
|
||||||
|
hkdf.expand(&[], &mut hkdf_output)?;
|
||||||
|
let shared_key = Aes256Gcm::new_from_slice(&hkdf_output)?;
|
||||||
|
|
||||||
|
// decrypt a single shard and create the payload
|
||||||
|
let (share, threshold) = self.decrypt_one_shard(private_keys, shard_data)?;
|
||||||
|
let mut payload = Vec::from(&share);
|
||||||
|
payload.insert(0, HUNK_VERSION);
|
||||||
|
payload.insert(1, threshold);
|
||||||
|
assert!(
|
||||||
|
payload.len() <= ENC_LEN as usize,
|
||||||
|
"invalid share length (too long, max {ENC_LEN} bytes)"
|
||||||
|
);
|
||||||
|
|
||||||
|
// encrypt data
|
||||||
|
let nonce = Nonce::<U12>::from_slice(&nonce);
|
||||||
|
let payload_bytes = shared_key.encrypt(nonce, payload.as_slice())?;
|
||||||
|
|
||||||
|
// convert data to a static-size payload
|
||||||
|
// NOTE: Padding length is less than u8::MAX because ENC_LEN < u8::MAX
|
||||||
|
#[allow(clippy::assertions_on_constants)]
|
||||||
|
{
|
||||||
|
assert!(ENC_LEN < u8::MAX, "padding byte can be u8");
|
||||||
|
}
|
||||||
|
#[allow(clippy::cast_possible_truncation)]
|
||||||
|
let mut out_bytes = [payload_bytes.len() as u8; ENC_LEN as usize];
|
||||||
|
assert!(
|
||||||
|
payload_bytes.len() < out_bytes.len(),
|
||||||
|
"encrypted payload larger than acceptable limit"
|
||||||
|
);
|
||||||
|
out_bytes[..payload_bytes.len()].clone_from_slice(&payload_bytes);
|
||||||
|
|
||||||
|
// NOTE: This previously used a single repeated value as the padding byte, but resulted in
|
||||||
|
// difficulty when entering in prompts manually, as one's place could be lost due to
|
||||||
|
// repeated keywords. This is resolved below by having sequentially increasing numbers up to
|
||||||
|
// but not including the last byte.
|
||||||
|
#[allow(clippy::cast_possible_truncation)]
|
||||||
|
for (i, byte) in (out_bytes[payload_bytes.len()..(ENC_LEN as usize - 1)])
|
||||||
|
.iter_mut()
|
||||||
|
.enumerate()
|
||||||
|
{
|
||||||
|
*byte = (i % u8::MAX as usize) as u8;
|
||||||
|
}
|
||||||
|
|
||||||
|
// safety: size of out_bytes is constant and always % 4 == 0
|
||||||
|
let payload_mnemonic =
|
||||||
|
unsafe { Mnemonic::from_raw_entropy(&out_bytes, Default::default()) };
|
||||||
|
|
||||||
|
#[cfg(feature = "qrcode")]
|
||||||
|
{
|
||||||
|
use keyfork_qrcode::{qrencode, ErrorCorrection};
|
||||||
|
let mut qrcode_data = our_pubkey_mnemonic.to_bytes();
|
||||||
|
qrcode_data.extend(payload_mnemonic.as_bytes());
|
||||||
|
if let Ok(qrcode) = qrencode(&smex::encode(&qrcode_data), ErrorCorrection::Highest) {
|
||||||
|
pm.prompt_message(PromptMessage::Text(
|
||||||
|
concat!(
|
||||||
|
"A QR code will be displayed after this prompt. ",
|
||||||
|
"Send the QR code back to the operator combining the shards. ",
|
||||||
|
"Nobody else should scan this QR code."
|
||||||
|
)
|
||||||
|
.to_string(),
|
||||||
|
))?;
|
||||||
|
pm.prompt_message(PromptMessage::Data(qrcode))?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pm.prompt_message(PromptMessage::Text(format!(
|
||||||
|
"Upon request, these words should be sent: {our_pubkey_mnemonic} {payload_mnemonic}"
|
||||||
|
)))?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// 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 {
|
||||||
|
|
|
@ -59,6 +59,7 @@ const SHARD_METADATA_OFFSET: usize = 2;
|
||||||
use super::{
|
use super::{
|
||||||
InvalidData, SharksError, HUNK_VERSION, QRCODE_COULDNT_READ, QRCODE_ERROR, QRCODE_PROMPT,
|
InvalidData, SharksError, HUNK_VERSION, QRCODE_COULDNT_READ, QRCODE_ERROR, QRCODE_PROMPT,
|
||||||
QRCODE_TIMEOUT,
|
QRCODE_TIMEOUT,
|
||||||
|
Format,
|
||||||
};
|
};
|
||||||
|
|
||||||
// 256 bit share is 49 bytes + some amount of hunk bytes, gives us reasonable padding
|
// 256 bit share is 49 bytes + some amount of hunk bytes, gives us reasonable padding
|
||||||
|
@ -163,6 +164,18 @@ impl EncryptedMessage {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Serialize all contents of the message to a writer.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// The function may error for any condition in Sequoia's Serialize trait.
|
||||||
|
pub fn serialize(&self, o: &mut dyn std::io::Write) -> openpgp::Result<()> {
|
||||||
|
for pkesk in &self.pkesks {
|
||||||
|
pkesk.serialize(o)?;
|
||||||
|
}
|
||||||
|
self.message.serialize(o)?;
|
||||||
|
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
|
||||||
|
@ -207,12 +220,273 @@ impl EncryptedMessage {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
///
|
||||||
|
pub struct OpenPGP;
|
||||||
|
|
||||||
|
impl OpenPGP {
|
||||||
|
/// Read all OpenPGP certificates in a path and return a [`Vec`] of them. Certificates are read
|
||||||
|
/// from a file, or from files one level deep in a directory.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// The function may return an error if it is unable to read the directory or if Sequoia is unable
|
||||||
|
/// to load certificates from the file.
|
||||||
|
pub fn discover_certs(path: impl AsRef<Path>) -> Result<Vec<Cert>> {
|
||||||
|
let path = path.as_ref();
|
||||||
|
|
||||||
|
if path.is_file() {
|
||||||
|
let mut vec = vec![];
|
||||||
|
for cert in CertParser::from_file(path).map_err(Error::Sequoia)? {
|
||||||
|
vec.push(cert.map_err(Error::Sequoia)?);
|
||||||
|
}
|
||||||
|
Ok(vec)
|
||||||
|
} else {
|
||||||
|
let mut vec = vec![];
|
||||||
|
for entry in path
|
||||||
|
.read_dir()
|
||||||
|
.map_err(Error::Io)?
|
||||||
|
.filter_map(Result::ok)
|
||||||
|
.filter(|p| p.path().is_file())
|
||||||
|
{
|
||||||
|
vec.push(Cert::from_file(entry.path()).map_err(Error::Sequoia)?);
|
||||||
|
}
|
||||||
|
Ok(vec)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Format for OpenPGP {
|
||||||
|
type Error = Error;
|
||||||
|
type PublicKeyData = Vec<Cert>;
|
||||||
|
type PrivateKeyData = Vec<Cert>;
|
||||||
|
type ShardData = Vec<EncryptedMessage>;
|
||||||
|
type Signer = openpgp::crypto::KeyPair;
|
||||||
|
|
||||||
|
fn parse_public_key_data(
|
||||||
|
&self,
|
||||||
|
key_data_path: impl AsRef<Path>,
|
||||||
|
) -> std::result::Result<Self::PublicKeyData, Self::Error> {
|
||||||
|
Self::discover_certs(key_data_path)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_private_key_data(
|
||||||
|
&self,
|
||||||
|
key_data_path: impl AsRef<Path>,
|
||||||
|
) -> std::result::Result<Self::PrivateKeyData, Self::Error> {
|
||||||
|
Self::discover_certs(key_data_path)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_shard_file(
|
||||||
|
&self,
|
||||||
|
shard_file: impl Read + Send + Sync,
|
||||||
|
) -> Result<Self::ShardData, Self::Error> {
|
||||||
|
let mut pkesks = Vec::new();
|
||||||
|
let mut encrypted_messages = vec![];
|
||||||
|
|
||||||
|
for packet in PacketPile::from_reader(shard_file)
|
||||||
|
.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)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn derive_signer(&self, secret: &[u8]) -> Result<Self::Signer, Self::Error> {
|
||||||
|
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,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let signing_key = derived_cert
|
||||||
|
.primary_key()
|
||||||
|
.parts_into_secret()
|
||||||
|
.map_err(Error::Sequoia)?
|
||||||
|
.key()
|
||||||
|
.clone()
|
||||||
|
.into_keypair()
|
||||||
|
.map_err(Error::Sequoia)?;
|
||||||
|
|
||||||
|
Ok(signing_key)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn format_shard_file(
|
||||||
|
&self,
|
||||||
|
shard_data: Self::ShardData,
|
||||||
|
shard_file: impl Write,
|
||||||
|
) -> Result<(), Self::Error> {
|
||||||
|
let mut writer = Writer::new(shard_file, Kind::Message).map_err(Error::SequoiaIo)?;
|
||||||
|
for message in shard_data {
|
||||||
|
message.serialize(&mut writer).map_err(Error::Sequoia)?;
|
||||||
|
}
|
||||||
|
writer.finalize().map_err(Error::SequoiaIo)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn generate_shard_data(
|
||||||
|
&self,
|
||||||
|
shares: &[Share],
|
||||||
|
signer: &Self::Signer,
|
||||||
|
public_keys: Self::PublicKeyData,
|
||||||
|
) -> std::result::Result<Self::ShardData, Self::Error> {
|
||||||
|
let policy = StandardPolicy::new();
|
||||||
|
let mut total_recipients = vec![];
|
||||||
|
let mut messages = vec![];
|
||||||
|
|
||||||
|
for (share, cert) in shares.iter().zip(public_keys) {
|
||||||
|
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, signer.clone())
|
||||||
|
.build()
|
||||||
|
.map_err(Error::Sequoia)?;
|
||||||
|
let mut message = LiteralWriter::new(message)
|
||||||
|
.build()
|
||||||
|
.map_err(Error::Sequoia)?;
|
||||||
|
// NOTE: This shouldn't be an alloc, but it's a minor alloc, so it's fine.
|
||||||
|
message
|
||||||
|
.write_all(&Vec::from(share))
|
||||||
|
.map_err(Error::SequoiaIo)?;
|
||||||
|
message.finalize().map_err(Error::Sequoia)?;
|
||||||
|
|
||||||
|
messages.push(message_output);
|
||||||
|
}
|
||||||
|
|
||||||
|
// A little bit of back and forth, we're going to parse the messages just to serialize them
|
||||||
|
// later.
|
||||||
|
let message = messages.into_iter().flatten().collect::<Vec<_>>();
|
||||||
|
let data = self.parse_shard_file(message.as_slice())?;
|
||||||
|
Ok(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn decrypt_all_shards(
|
||||||
|
&self,
|
||||||
|
private_keys: Option<Self::PrivateKeyData>,
|
||||||
|
mut shard_data: Self::ShardData,
|
||||||
|
) -> 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 metadata = shard_data.remove(0);
|
||||||
|
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(shard_data)
|
||||||
|
.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>,
|
||||||
|
mut shard_data: Self::ShardData,
|
||||||
|
) -> 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 metadata = shard_data.remove(0);
|
||||||
|
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(shard_data)
|
||||||
|
.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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Read all OpenPGP certificates in a path and return a [`Vec`] of them. Certificates are read
|
/// 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.
|
/// from a file, or from files one level deep in a directory.
|
||||||
///
|
///
|
||||||
/// # Errors
|
/// # Errors
|
||||||
/// The function may return an error if it is unable to read the directory or if Sequoia is unable
|
/// 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.
|
/// to load certificates from the file.
|
||||||
|
#[deprecated]
|
||||||
pub fn discover_certs(path: impl AsRef<Path>) -> Result<Vec<Cert>> {
|
pub fn discover_certs(path: impl AsRef<Path>) -> Result<Vec<Cert>> {
|
||||||
let path = path.as_ref();
|
let path = path.as_ref();
|
||||||
|
|
||||||
|
@ -245,6 +519,7 @@ pub fn discover_certs(path: impl AsRef<Path>) -> Result<Vec<Cert>> {
|
||||||
/// # Panics
|
/// # Panics
|
||||||
/// When given packets that are not a list of PKESK packets and SEIP packets, the function 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.
|
/// The `split` utility should never give packets that are not in this format.
|
||||||
|
#[deprecated]
|
||||||
pub fn parse_messages(reader: impl Read + Send + Sync) -> Result<VecDeque<EncryptedMessage>> {
|
pub fn parse_messages(reader: impl Read + Send + Sync) -> Result<VecDeque<EncryptedMessage>> {
|
||||||
let mut pkesks = Vec::new();
|
let mut pkesks = Vec::new();
|
||||||
let mut encrypted_messages = VecDeque::new();
|
let mut encrypted_messages = VecDeque::new();
|
||||||
|
@ -416,6 +691,7 @@ fn decrypt_metadata(
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[deprecated]
|
||||||
fn decrypt_one(
|
fn decrypt_one(
|
||||||
messages: Vec<EncryptedMessage>,
|
messages: Vec<EncryptedMessage>,
|
||||||
certs: &[Cert],
|
certs: &[Cert],
|
||||||
|
@ -465,6 +741,8 @@ fn decrypt_one(
|
||||||
/// The function may panic if a share is decrypted but has a length larger than 256 bits. This is
|
/// 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
|
/// atypical usage and should not be encountered in normal usage, unless something that is not a
|
||||||
/// Keyfork seed has been fed into [`split`].
|
/// Keyfork seed has been fed into [`split`].
|
||||||
|
#[deprecated]
|
||||||
|
#[allow(deprecated)]
|
||||||
pub fn decrypt(
|
pub fn decrypt(
|
||||||
certs: &[Cert],
|
certs: &[Cert],
|
||||||
metadata: &EncryptedMessage,
|
metadata: &EncryptedMessage,
|
||||||
|
|
Loading…
Reference in New Issue