Compare commits

..

2 Commits

5 changed files with 182 additions and 21 deletions

View File

@ -10,6 +10,6 @@ default = ["mnemonic"]
mnemonic = ["keyfork-mnemonic-util"] mnemonic = ["keyfork-mnemonic-util"]
[dependencies] [dependencies]
crossterm = { version = "0.27.0", default-features = false, features = ["use-dev-tty", "events"] } crossterm = { version = "0.27.0", default-features = false, features = ["use-dev-tty", "events", "bracketed-paste"] }
keyfork-mnemonic-util = { version = "0.1.0", path = "../keyfork-mnemonic-util", optional = true } keyfork-mnemonic-util = { version = "0.1.0", path = "../keyfork-mnemonic-util", optional = true }
thiserror = "1.0.51" thiserror = "1.0.51"

View File

@ -8,7 +8,7 @@ use keyfork_mnemonic_util::Wordlist;
use crossterm::{ use crossterm::{
cursor, cursor,
event::{read, Event, KeyCode, KeyModifiers}, event::{read, DisableBracketedPaste, EnableBracketedPaste, Event, KeyCode, KeyModifiers},
style::{Print, PrintStyledContent, Stylize}, style::{Print, PrintStyledContent, Stylize},
terminal, terminal,
tty::IsTty, tty::IsTty,
@ -77,6 +77,7 @@ where
Ok(line) Ok(line)
} }
// TODO: create a wrapper for bracketed paste similar to RawMode
#[cfg(feature = "mnemonic")] #[cfg(feature = "mnemonic")]
pub fn prompt_wordlist(&mut self, prompt: &str, wordlist: &Wordlist) -> Result<String> { pub fn prompt_wordlist(&mut self, prompt: &str, wordlist: &Wordlist) -> Result<String> {
let mut terminal = AlternateScreen::new(&mut self.write)?; let mut terminal = AlternateScreen::new(&mut self.write)?;
@ -84,7 +85,8 @@ where
terminal terminal
.queue(terminal::Clear(terminal::ClearType::All))? .queue(terminal::Clear(terminal::ClearType::All))?
.queue(cursor::MoveTo(0, 0))?; .queue(cursor::MoveTo(0, 0))?
.queue(EnableBracketedPaste)?;
let mut lines = prompt.lines().peekable(); let mut lines = prompt.lines().peekable();
let mut prefix_length = 0; let mut prefix_length = 0;
while let Some(line) = lines.next() { while let Some(line) = lines.next() {
@ -107,6 +109,10 @@ where
cols = new_cols; cols = new_cols;
_rows = new_rows; _rows = new_rows;
} }
Event::Paste(mut p) => {
p.retain(|c| c != '\n');
input.push_str(&p);
}
Event::Key(k) => match k.code { Event::Key(k) => match k.code {
KeyCode::Enter => { KeyCode::Enter => {
input.push('\n'); input.push('\n');
@ -184,6 +190,8 @@ where
terminal.flush()?; terminal.flush()?;
} }
terminal.queue(DisableBracketedPaste)?.flush()?;
Ok(input) Ok(input)
} }

View File

@ -28,4 +28,4 @@ thiserror = "1.0.50"
keyfork-prompt = { version = "0.1.0", path = "../keyfork-prompt", optional = true } keyfork-prompt = { version = "0.1.0", path = "../keyfork-prompt", optional = true }
x25519-dalek = { version = "2.0.0", features = ["getrandom"] } x25519-dalek = { version = "2.0.0", features = ["getrandom"] }
keyfork-mnemonic-util = { version = "0.1.0", path = "../keyfork-mnemonic-util" } keyfork-mnemonic-util = { version = "0.1.0", path = "../keyfork-mnemonic-util" }
aes-gcm = "0.10.3" aes-gcm = { version = "0.10.3", features = ["std"] }

View File

@ -0,0 +1,65 @@
use std::{
env,
fs::File,
path::{Path, PathBuf},
process::ExitCode,
};
use keyfork_shard::openpgp::{remote_decrypt, discover_certs, openpgp::Cert, parse_messages};
type Result<T, E = Box<dyn std::error::Error>> = std::result::Result<T, E>;
fn validate<'a>(
messages_file: impl AsRef<Path>,
key_discovery: impl Into<Option<&'a str>>,
) -> Result<(File, Vec<Cert>)> {
let key_discovery = key_discovery.into().map(PathBuf::from);
key_discovery.as_ref().map(std::fs::metadata).transpose()?;
// Load certs from path
let certs = key_discovery
.map(discover_certs)
.transpose()?
.unwrap_or(vec![]);
Ok((File::open(messages_file)?, certs))
}
fn run() -> Result<()> {
let mut args = env::args();
let program_name = args.next().expect("program name");
let args = args.collect::<Vec<_>>();
let (messages_file, cert_list) = match args.as_slice() {
[messages_file, key_discovery] => validate(messages_file, key_discovery.as_str())?,
[messages_file] => validate(messages_file, None)?,
_ => panic!("Usage: {program_name} messages_file [key_discovery]"),
};
let mut encrypted_messages = parse_messages(messages_file)?;
let encrypted_metadata = encrypted_messages
.pop_front()
.expect("any pgp encrypted message");
remote_decrypt(
&cert_list,
encrypted_metadata,
encrypted_messages.make_contiguous(),
)?;
Ok(())
}
fn main() -> ExitCode {
let result = run();
if let Err(e) = result {
eprintln!("Error: {e}");
let mut source = e.source();
while let Some(new_error) = source.take() {
eprintln!("Source: {new_error}");
source = new_error.source();
}
return ExitCode::FAILURE;
}
ExitCode::SUCCESS
}

View File

@ -6,8 +6,8 @@ use std::{
}; };
use aes_gcm::{ use aes_gcm::{
aead::{consts::U12, Aead}, aead::{consts::U12, Aead, AeadCore, OsRng},
Aes256Gcm, KeyInit, Nonce, Aes256Gcm, KeyInit, Nonce, Error as AesError
}; };
use keyfork_derive_openpgp::derive_util::{ use keyfork_derive_openpgp::derive_util::{
request::{DerivationAlgorithm, DerivationRequest}, request::{DerivationAlgorithm, DerivationRequest},
@ -48,11 +48,16 @@ use x25519_dalek::{EphemeralSecret, PublicKey};
const SHARD_METADATA_VERSION: u8 = 1; const SHARD_METADATA_VERSION: u8 = 1;
const SHARD_METADATA_OFFSET: usize = 2; const SHARD_METADATA_OFFSET: usize = 2;
const ENC_LEN: u8 = 24 * 4;
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]
pub enum Error { pub enum Error {
#[error("Error with creating Share: {0}")] #[error("Error with creating Share: {0}")]
Share(String), Share(String),
#[error("Error decrypting share: {0}")]
SymDecryptShare(#[from] AesError),
#[error("Error combining shares: {0}")] #[error("Error combining shares: {0}")]
CombineShares(String), CombineShares(String),
@ -343,7 +348,7 @@ fn decrypt_one(
messages: Vec<EncryptedMessage>, messages: Vec<EncryptedMessage>,
certs: &[Cert], certs: &[Cert],
metadata: EncryptedMessage, metadata: EncryptedMessage,
) -> Result<Vec<u8>> { ) -> Result<(Vec<u8>, u8, Cert)> {
let policy = NullPolicy::new(); let policy = NullPolicy::new();
let mut keyring = Keyring::new(certs)?; let mut keyring = Keyring::new(certs)?;
@ -351,10 +356,10 @@ fn decrypt_one(
let content = decrypt_metadata(&metadata, &policy, &mut keyring, &mut manager)?; let content = decrypt_metadata(&metadata, &policy, &mut keyring, &mut manager)?;
let (_threshold, root_cert, certs) = decode_metadata_v1(&content)?; let (threshold, root_cert, certs) = decode_metadata_v1(&content)?;
keyring.set_root_cert(root_cert.clone()); keyring.set_root_cert(root_cert.clone());
manager.set_root_cert(root_cert); manager.set_root_cert(root_cert.clone());
let mut messages: HashMap<KeyID, EncryptedMessage> = let mut messages: HashMap<KeyID, EncryptedMessage> =
HashMap::from_iter(certs.iter().map(|c| c.keyid()).zip(messages)); HashMap::from_iter(certs.iter().map(|c| c.keyid()).zip(messages));
@ -362,13 +367,13 @@ fn decrypt_one(
let decrypted_messages = decrypt_with_keyring(&mut messages, &certs, &policy, &mut keyring)?; let decrypted_messages = decrypt_with_keyring(&mut messages, &certs, &policy, &mut keyring)?;
if let Some(message) = decrypted_messages.into_values().next() { if let Some(message) = decrypted_messages.into_values().next() {
return Ok(message); return Ok((message, threshold, root_cert));
} }
let decrypted_messages = decrypt_with_manager(1, &mut messages, &certs, policy, &mut manager)?; let decrypted_messages = decrypt_with_manager(1, &mut messages, &certs, policy, &mut manager)?;
if let Some(message) = decrypted_messages.into_values().next() { if let Some(message) = decrypted_messages.into_values().next() {
return Ok(message); return Ok((message, threshold, root_cert));
} }
unreachable!("smartcard manager should always decrypt") unreachable!("smartcard manager should always decrypt")
@ -413,7 +418,7 @@ pub fn decrypt(
.diffie_hellman(&PublicKey::from(their_key)) .diffie_hellman(&PublicKey::from(their_key))
.to_bytes(); .to_bytes();
let share = decrypt_one(encrypted_messages.to_vec(), &certs, metadata)?; let (share, ..) = decrypt_one(encrypted_messages.to_vec(), &certs, metadata)?;
assert!(share.len() <= 65, "invalid share length (too long)"); assert!(share.len() <= 65, "invalid share length (too long)");
const LEN: u8 = 24 * 3; const LEN: u8 = 24 * 3;
let mut encrypted_payload = [(LEN - share.len() as u8); LEN as usize]; let mut encrypted_payload = [(LEN - share.len() as u8); LEN as usize];
@ -421,16 +426,27 @@ pub fn decrypt(
let shared_key = let shared_key =
Aes256Gcm::new_from_slice(&shared_secret).expect("Invalid length of constant key size"); Aes256Gcm::new_from_slice(&shared_secret).expect("Invalid length of constant key size");
let bytes = shared_key.encrypt(their_nonce, share.as_slice()).unwrap(); let bytes = shared_key.encrypt(their_nonce, share.as_slice())?;
shared_key.decrypt(their_nonce, &bytes[..])?;
// NOTE: Padding length is less than u8::MAX because 24 * 4 < u8::MAX // NOTE: Padding length is less than u8::MAX because ENC_LEN < u8::MAX
const ENC_LEN: u8 = 24 * 4; // NOTE: This previously used a single value as the padding byte, but resulted in
let mut out_bytes = [(ENC_LEN - bytes.len() as u8); ENC_LEN as usize]; // 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.
assert!(ENC_LEN < u8::MAX, "padding byte can be u8");
let mut out_bytes = [bytes.len() as u8; ENC_LEN as usize];
assert!( assert!(
bytes.len() < out_bytes.len(), bytes.len() < out_bytes.len(),
"encrypted payload larger than acceptable limit" "encrypted payload larger than acceptable limit"
); );
out_bytes[..bytes.len()].clone_from_slice(&bytes); out_bytes[..bytes.len()].clone_from_slice(&bytes);
for (i, byte) in (&mut out_bytes[bytes.len()..(ENC_LEN as usize - 1)])
.into_iter()
.enumerate()
{
*byte = (i % u8::MAX as usize) as u8;
}
// safety: size of out_bytes is constant and always % 4 == 0 // safety: size of out_bytes is constant and always % 4 == 0
let mnemonic = unsafe { Mnemonic::from_raw_entropy(&out_bytes, Default::default()) }; let mnemonic = unsafe { Mnemonic::from_raw_entropy(&out_bytes, Default::default()) };
@ -440,6 +456,80 @@ pub fn decrypt(
Ok(()) Ok(())
} }
pub fn remote_decrypt(
certs: &[Cert],
metadata: EncryptedMessage,
encrypted_messages: &[EncryptedMessage],
) -> Result<()> {
let mut pm = PromptManager::new(stdin(), stdout())?;
let wordlist = Wordlist::default();
// Get our cert so we know our metadata
let (share, threshold, root_cert) = decrypt_one(encrypted_messages.to_vec(), &certs, metadata)?;
let mut shares = Vec::with_capacity(threshold as usize);
shares.push(share);
for _ in 1..threshold {
let nonce = Aes256Gcm::generate_nonce(&mut OsRng);
let nonce_mnemonic =
unsafe { Mnemonic::from_raw_entropy(nonce.as_slice(), Default::default()) };
let our_key = EphemeralSecret::random();
let key_mnemonic =
Mnemonic::from_entropy(PublicKey::from(&our_key).as_bytes(), Default::default())?;
pm.prompt_message(PromptMessage::Text(format!(
"Our words: {nonce_mnemonic} {key_mnemonic}"
)))?;
let pubkey_mnemonic = pm.prompt_wordlist("Their words: ", &wordlist)?;
let their_key = Mnemonic::from_str(&pubkey_mnemonic)?.entropy();
let their_key: [u8; 32] = their_key.try_into().expect("24 words");
let shared_secret = our_key
.diffie_hellman(&PublicKey::from(their_key))
.to_bytes();
let shared_key =
Aes256Gcm::new_from_slice(&shared_secret).expect("Invalid length of constant key size");
let payload_mnemonic = pm.prompt_wordlist("Their payload: ", &wordlist)?;
let payload = Mnemonic::from_str(&payload_mnemonic)?.entropy();
let decrypted_share = shared_key.decrypt(&nonce, &payload[..payload[payload.len() - 1] as usize])?;
shares.push(decrypted_share);
}
let shares = shares
.into_iter()
.map(|s| Share::try_from(s.as_slice()))
.collect::<Result<Vec<_>, &str>>()
.map_err(|e| Error::Share(e.to_string()))?;
let secret = Sharks(threshold)
.recover(&shares)
.map_err(|e| Error::CombineShares(e.to_string()))?;
let userid = UserID::from("keyfork-sss");
let kdr = DerivationRequest::new(
DerivationAlgorithm::Ed25519,
&DerivationPath::from_str("m/7366512'/0'")?,
)
.derive_with_master_seed(secret.to_vec())?;
let derived_cert = keyfork_derive_openpgp::derive(
kdr,
&[KeyFlags::empty().set_certification().set_signing()],
userid,
)?;
// 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));
}
print!("{}", smex::encode(&secret));
Ok(())
}
pub fn combine( pub fn combine(
certs: Vec<Cert>, certs: Vec<Cert>,
metadata: EncryptedMessage, metadata: EncryptedMessage,
@ -457,7 +547,7 @@ pub fn combine(
let (threshold, root_cert, certs) = decode_metadata_v1(&content)?; let (threshold, root_cert, certs) = decode_metadata_v1(&content)?;
keyring.set_root_cert(root_cert.clone()); keyring.set_root_cert(root_cert.clone());
manager.set_root_cert(root_cert); manager.set_root_cert(root_cert.clone());
// Generate a controlled binding from certificates to encrypted messages. This is stable // Generate a controlled binding from certificates to encrypted messages. This is stable
// because we control the order packets are encrypted and certificates are stored. // because we control the order packets are encrypted and certificates are stored.
@ -492,6 +582,7 @@ pub fn combine(
.recover(&shares) .recover(&shares)
.map_err(|e| Error::CombineShares(e.to_string()))?; .map_err(|e| Error::CombineShares(e.to_string()))?;
// TODO: extract as function
let userid = UserID::from("keyfork-sss"); let userid = UserID::from("keyfork-sss");
let kdr = DerivationRequest::new( let kdr = DerivationRequest::new(
DerivationAlgorithm::Ed25519, DerivationAlgorithm::Ed25519,
@ -506,10 +597,7 @@ pub fn combine(
// NOTE: Signatures on certs will be different. Compare fingerprints instead. // NOTE: Signatures on certs will be different. Compare fingerprints instead.
let derived_fp = derived_cert.fingerprint(); let derived_fp = derived_cert.fingerprint();
let expected_fp = keyring let expected_fp = root_cert.fingerprint();
.root_cert()
.expect("cert was previously set")
.fingerprint();
if derived_fp != expected_fp { if derived_fp != expected_fp {
return Err(Error::InvalidSecret(derived_fp, expected_fp)); return Err(Error::InvalidSecret(derived_fp, expected_fp));
} }