Compare commits
No commits in common. "1b19a08cd492881d4147a06e8335a92c5ee75d4f" and "d7f33874f69f8a4b5f377e02be77d5c7272a083d" have entirely different histories.
1b19a08cd4
...
d7f33874f6
|
@ -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", "bracketed-paste"] }
|
crossterm = { version = "0.27.0", default-features = false, features = ["use-dev-tty", "events"] }
|
||||||
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, DisableBracketedPaste, EnableBracketedPaste, Event, KeyCode, KeyModifiers},
|
event::{read, Event, KeyCode, KeyModifiers},
|
||||||
style::{Print, PrintStyledContent, Stylize},
|
style::{Print, PrintStyledContent, Stylize},
|
||||||
terminal,
|
terminal,
|
||||||
tty::IsTty,
|
tty::IsTty,
|
||||||
|
@ -77,7 +77,6 @@ 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)?;
|
||||||
|
@ -85,8 +84,7 @@ 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() {
|
||||||
|
@ -109,10 +107,6 @@ 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');
|
||||||
|
@ -190,8 +184,6 @@ 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 = { version = "0.10.3", features = ["std"] }
|
aes-gcm = "0.10.3"
|
||||||
|
|
|
@ -1,65 +0,0 @@
|
||||||
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, AeadCore, OsRng},
|
aead::{consts::U12, Aead},
|
||||||
Aes256Gcm, KeyInit, Nonce, Error as AesError
|
Aes256Gcm, KeyInit, Nonce,
|
||||||
};
|
};
|
||||||
use keyfork_derive_openpgp::derive_util::{
|
use keyfork_derive_openpgp::derive_util::{
|
||||||
request::{DerivationAlgorithm, DerivationRequest},
|
request::{DerivationAlgorithm, DerivationRequest},
|
||||||
|
@ -48,16 +48,11 @@ 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),
|
||||||
|
|
||||||
|
@ -348,7 +343,7 @@ fn decrypt_one(
|
||||||
messages: Vec<EncryptedMessage>,
|
messages: Vec<EncryptedMessage>,
|
||||||
certs: &[Cert],
|
certs: &[Cert],
|
||||||
metadata: EncryptedMessage,
|
metadata: EncryptedMessage,
|
||||||
) -> Result<(Vec<u8>, u8, Cert)> {
|
) -> Result<Vec<u8>> {
|
||||||
let policy = NullPolicy::new();
|
let policy = NullPolicy::new();
|
||||||
|
|
||||||
let mut keyring = Keyring::new(certs)?;
|
let mut keyring = Keyring::new(certs)?;
|
||||||
|
@ -356,10 +351,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.clone());
|
manager.set_root_cert(root_cert);
|
||||||
|
|
||||||
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));
|
||||||
|
@ -367,13 +362,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, threshold, root_cert));
|
return Ok(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
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, threshold, root_cert));
|
return Ok(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
unreachable!("smartcard manager should always decrypt")
|
unreachable!("smartcard manager should always decrypt")
|
||||||
|
@ -418,7 +413,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];
|
||||||
|
@ -426,27 +421,16 @@ 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())?;
|
let bytes = shared_key.encrypt(their_nonce, share.as_slice()).unwrap();
|
||||||
shared_key.decrypt(their_nonce, &bytes[..])?;
|
|
||||||
|
|
||||||
// NOTE: Padding length is less than u8::MAX because ENC_LEN < u8::MAX
|
// NOTE: Padding length is less than u8::MAX because 24 * 4 < u8::MAX
|
||||||
// NOTE: This previously used a single value as the padding byte, but resulted in
|
const ENC_LEN: u8 = 24 * 4;
|
||||||
// difficulty when entering in prompts manually, as one's place could be lost due to repeated
|
let mut out_bytes = [(ENC_LEN - bytes.len() as u8); ENC_LEN as usize];
|
||||||
// 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()) };
|
||||||
|
@ -456,80 +440,6 @@ 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,
|
||||||
|
@ -547,7 +457,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.clone());
|
manager.set_root_cert(root_cert);
|
||||||
|
|
||||||
// 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.
|
||||||
|
@ -582,7 +492,6 @@ 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,
|
||||||
|
@ -597,7 +506,10 @@ 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 = root_cert.fingerprint();
|
let expected_fp = keyring
|
||||||
|
.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