Compare commits
2 Commits
d7f33874f6
...
1b19a08cd4
Author | SHA1 | Date |
---|---|---|
Ryan Heywood | 1b19a08cd4 | |
Ryan Heywood | 3190ba97db |
keyfork-prompt
keyfork-shard
|
@ -10,6 +10,6 @@ default = ["mnemonic"]
|
|||
mnemonic = ["keyfork-mnemonic-util"]
|
||||
|
||||
[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 }
|
||||
thiserror = "1.0.51"
|
||||
|
|
|
@ -8,7 +8,7 @@ use keyfork_mnemonic_util::Wordlist;
|
|||
|
||||
use crossterm::{
|
||||
cursor,
|
||||
event::{read, Event, KeyCode, KeyModifiers},
|
||||
event::{read, DisableBracketedPaste, EnableBracketedPaste, Event, KeyCode, KeyModifiers},
|
||||
style::{Print, PrintStyledContent, Stylize},
|
||||
terminal,
|
||||
tty::IsTty,
|
||||
|
@ -77,6 +77,7 @@ where
|
|||
Ok(line)
|
||||
}
|
||||
|
||||
// TODO: create a wrapper for bracketed paste similar to RawMode
|
||||
#[cfg(feature = "mnemonic")]
|
||||
pub fn prompt_wordlist(&mut self, prompt: &str, wordlist: &Wordlist) -> Result<String> {
|
||||
let mut terminal = AlternateScreen::new(&mut self.write)?;
|
||||
|
@ -84,7 +85,8 @@ where
|
|||
|
||||
terminal
|
||||
.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 prefix_length = 0;
|
||||
while let Some(line) = lines.next() {
|
||||
|
@ -107,6 +109,10 @@ where
|
|||
cols = new_cols;
|
||||
_rows = new_rows;
|
||||
}
|
||||
Event::Paste(mut p) => {
|
||||
p.retain(|c| c != '\n');
|
||||
input.push_str(&p);
|
||||
}
|
||||
Event::Key(k) => match k.code {
|
||||
KeyCode::Enter => {
|
||||
input.push('\n');
|
||||
|
@ -184,6 +190,8 @@ where
|
|||
terminal.flush()?;
|
||||
}
|
||||
|
||||
terminal.queue(DisableBracketedPaste)?.flush()?;
|
||||
|
||||
Ok(input)
|
||||
}
|
||||
|
||||
|
|
|
@ -28,4 +28,4 @@ thiserror = "1.0.50"
|
|||
keyfork-prompt = { version = "0.1.0", path = "../keyfork-prompt", optional = true }
|
||||
x25519-dalek = { version = "2.0.0", features = ["getrandom"] }
|
||||
keyfork-mnemonic-util = { version = "0.1.0", path = "../keyfork-mnemonic-util" }
|
||||
aes-gcm = "0.10.3"
|
||||
aes-gcm = { version = "0.10.3", features = ["std"] }
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -6,8 +6,8 @@ use std::{
|
|||
};
|
||||
|
||||
use aes_gcm::{
|
||||
aead::{consts::U12, Aead},
|
||||
Aes256Gcm, KeyInit, Nonce,
|
||||
aead::{consts::U12, Aead, AeadCore, OsRng},
|
||||
Aes256Gcm, KeyInit, Nonce, Error as AesError
|
||||
};
|
||||
use keyfork_derive_openpgp::derive_util::{
|
||||
request::{DerivationAlgorithm, DerivationRequest},
|
||||
|
@ -48,11 +48,16 @@ use x25519_dalek::{EphemeralSecret, PublicKey};
|
|||
const SHARD_METADATA_VERSION: u8 = 1;
|
||||
const SHARD_METADATA_OFFSET: usize = 2;
|
||||
|
||||
const ENC_LEN: u8 = 24 * 4;
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum Error {
|
||||
#[error("Error with creating Share: {0}")]
|
||||
Share(String),
|
||||
|
||||
#[error("Error decrypting share: {0}")]
|
||||
SymDecryptShare(#[from] AesError),
|
||||
|
||||
#[error("Error combining shares: {0}")]
|
||||
CombineShares(String),
|
||||
|
||||
|
@ -343,7 +348,7 @@ fn decrypt_one(
|
|||
messages: Vec<EncryptedMessage>,
|
||||
certs: &[Cert],
|
||||
metadata: EncryptedMessage,
|
||||
) -> Result<Vec<u8>> {
|
||||
) -> Result<(Vec<u8>, u8, Cert)> {
|
||||
let policy = NullPolicy::new();
|
||||
|
||||
let mut keyring = Keyring::new(certs)?;
|
||||
|
@ -351,10 +356,10 @@ fn decrypt_one(
|
|||
|
||||
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());
|
||||
manager.set_root_cert(root_cert);
|
||||
manager.set_root_cert(root_cert.clone());
|
||||
|
||||
let mut messages: HashMap<KeyID, EncryptedMessage> =
|
||||
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)?;
|
||||
|
||||
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)?;
|
||||
|
||||
if let Some(message) = decrypted_messages.into_values().next() {
|
||||
return Ok(message);
|
||||
return Ok((message, threshold, root_cert));
|
||||
}
|
||||
|
||||
unreachable!("smartcard manager should always decrypt")
|
||||
|
@ -413,7 +418,7 @@ pub fn decrypt(
|
|||
.diffie_hellman(&PublicKey::from(their_key))
|
||||
.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)");
|
||||
const LEN: u8 = 24 * 3;
|
||||
let mut encrypted_payload = [(LEN - share.len() as u8); LEN as usize];
|
||||
|
@ -421,16 +426,27 @@ pub fn decrypt(
|
|||
|
||||
let shared_key =
|
||||
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
|
||||
const ENC_LEN: u8 = 24 * 4;
|
||||
let mut out_bytes = [(ENC_LEN - bytes.len() as u8); ENC_LEN as usize];
|
||||
// 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.
|
||||
assert!(ENC_LEN < u8::MAX, "padding byte can be u8");
|
||||
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);
|
||||
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
|
||||
let mnemonic = unsafe { Mnemonic::from_raw_entropy(&out_bytes, Default::default()) };
|
||||
|
@ -440,6 +456,80 @@ pub fn decrypt(
|
|||
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(
|
||||
certs: Vec<Cert>,
|
||||
metadata: EncryptedMessage,
|
||||
|
@ -457,7 +547,7 @@ pub fn combine(
|
|||
let (threshold, root_cert, certs) = decode_metadata_v1(&content)?;
|
||||
|
||||
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
|
||||
// because we control the order packets are encrypted and certificates are stored.
|
||||
|
@ -492,6 +582,7 @@ pub fn combine(
|
|||
.recover(&shares)
|
||||
.map_err(|e| Error::CombineShares(e.to_string()))?;
|
||||
|
||||
// TODO: extract as function
|
||||
let userid = UserID::from("keyfork-sss");
|
||||
let kdr = DerivationRequest::new(
|
||||
DerivationAlgorithm::Ed25519,
|
||||
|
@ -506,10 +597,7 @@ pub fn combine(
|
|||
|
||||
// NOTE: Signatures on certs will be different. Compare fingerprints instead.
|
||||
let derived_fp = derived_cert.fingerprint();
|
||||
let expected_fp = keyring
|
||||
.root_cert()
|
||||
.expect("cert was previously set")
|
||||
.fingerprint();
|
||||
let expected_fp = root_cert.fingerprint();
|
||||
if derived_fp != expected_fp {
|
||||
return Err(Error::InvalidSecret(derived_fp, expected_fp));
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue