Compare commits
2 Commits
d7f33874f6
...
1b19a08cd4
Author | SHA1 | Date |
---|---|---|
Ryan Heywood | 1b19a08cd4 | |
Ryan Heywood | 3190ba97db |
|
@ -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"
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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"] }
|
||||||
|
|
|
@ -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::{
|
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));
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue