keyfork-shard: decrypt only in `decrypt` command

The way this previously runs, the combining operator would be *required*
to decrypt a share. This was not ideal for enclaves, where the process
should just send out public keys and read in public keys and payloads.
This is now resolved.
This commit is contained in:
Ryan Heywood 2024-01-03 19:58:39 -05:00
parent 1b19a08cd4
commit 3240ab9e1f
Signed by: ryan
GPG Key ID: 8E401478A3FBEF72
2 changed files with 70 additions and 58 deletions

View File

@ -1,51 +1,22 @@
use std::{
env,
fs::File,
path::{Path, PathBuf},
process::ExitCode,
};
use keyfork_shard::openpgp::{remote_decrypt, discover_certs, openpgp::Cert, parse_messages};
use keyfork_shard::openpgp::remote_decrypt;
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]"),
match args.as_slice() {
[] => (),
_ => panic!("Usage: {program_name}"),
};
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(),
)?;
remote_decrypt()?;
Ok(())
}

View File

@ -7,7 +7,7 @@ use std::{
use aes_gcm::{
aead::{consts::U12, Aead, AeadCore, OsRng},
Aes256Gcm, KeyInit, Nonce, Error as AesError
Aes256Gcm, Error as AesError, KeyInit, Nonce,
};
use keyfork_derive_openpgp::derive_util::{
request::{DerivationAlgorithm, DerivationRequest},
@ -48,7 +48,15 @@ use x25519_dalek::{EphemeralSecret, PublicKey};
const SHARD_METADATA_VERSION: u8 = 1;
const SHARD_METADATA_OFFSET: usize = 2;
const ENC_LEN: u8 = 24 * 4;
/// Decrypt hunk version 1:
/// 1 byte: Version
/// 1 byte: Threshold
/// Data: &[u8]
const HUNK_VERSION: u8 = 1;
const HUNK_OFFSET: usize = 2;
// 256 bit share is 49 bytes + some amount of hunk bytes, gives us reasonable padding
const ENC_LEN: u8 = 4 * 16;
#[derive(Debug, thiserror::Error)]
pub enum Error {
@ -411,22 +419,23 @@ pub fn decrypt(
let our_key = EphemeralSecret::random();
let our_mnemonic =
Mnemonic::from_entropy(PublicKey::from(&our_key).as_bytes(), Default::default())?;
// TODO: Encode using `qrencode -t ansiutf8 -m 2`
pm.prompt_message(PromptMessage::Text(format!("Our words: {our_mnemonic}")))?;
let shared_secret = our_key
.diffie_hellman(&PublicKey::from(their_key))
.to_bytes();
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];
encrypted_payload[..share.len()].copy_from_slice(&share);
let (mut share, threshold, ..) = decrypt_one(encrypted_messages.to_vec(), &certs, metadata)?;
share.insert(0, HUNK_VERSION);
share.insert(1, threshold);
assert!(
share.len() <= ENC_LEN as usize,
"invalid share length (too long, max {ENC_LEN} bytes)"
);
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())?;
dbg!(bytes.len());
shared_key.decrypt(their_nonce, &bytes[..])?;
// NOTE: Padding length is less than u8::MAX because ENC_LEN < u8::MAX
@ -451,25 +460,23 @@ pub fn decrypt(
// safety: size of out_bytes is constant and always % 4 == 0
let mnemonic = unsafe { Mnemonic::from_raw_entropy(&out_bytes, Default::default()) };
pm.prompt_message(PromptMessage::Text(format!("Our payload: {mnemonic}")))?;
pm.prompt_message(PromptMessage::Text(format!(
"Our words: {our_mnemonic} {mnemonic}"
)))?;
Ok(())
}
pub fn remote_decrypt(
certs: &[Cert],
metadata: EncryptedMessage,
encrypted_messages: &[EncryptedMessage],
) -> Result<()> {
pub fn remote_decrypt() -> 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);
let mut iter_count = None;
let mut shares = vec![];
for _ in 1..threshold {
let mut threshold = 0;
while iter_count.is_none() || iter_count.is_some_and(|i| i > 0) {
let nonce = Aes256Gcm::generate_nonce(&mut OsRng);
let nonce_mnemonic =
unsafe { Mnemonic::from_raw_entropy(nonce.as_slice(), Default::default()) };
@ -480,7 +487,25 @@ pub fn remote_decrypt(
"Our words: {nonce_mnemonic} {key_mnemonic}"
)))?;
let pubkey_mnemonic = pm.prompt_wordlist("Their words: ", &wordlist)?;
let their_words = pm.prompt_wordlist("Their words: ", &wordlist)?;
let mut pubkey_words = their_words.split_whitespace().take(24).peekable();
let mut payload_words = their_words.split_whitespace().skip(24).take(48).peekable();
let mut pubkey_mnemonic = String::new();
let mut payload_mnemonic = String::new();
while let Some(word) = pubkey_words.next() {
pubkey_mnemonic.push_str(word);
if pubkey_words.peek().is_some() {
pubkey_mnemonic.push(' ');
}
}
while let Some(word) = payload_words.next() {
payload_mnemonic.push_str(word);
if payload_words.peek().is_some() {
payload_mnemonic.push(' ');
}
}
let their_key = Mnemonic::from_str(&pubkey_mnemonic)?.entropy();
let their_key: [u8; 32] = their_key.try_into().expect("24 words");
@ -490,11 +515,24 @@ pub fn remote_decrypt(
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 payload =
shared_key.decrypt(&nonce, &payload[..payload[payload.len() - 1] as usize])?;
assert_eq!(HUNK_VERSION, payload[0], "Incompatible hunk version");
let decrypted_share = shared_key.decrypt(&nonce, &payload[..payload[payload.len() - 1] as usize])?;
shares.push(decrypted_share);
match &mut iter_count {
Some(n) => {
// Must be > 0 to start loop, can't go lower
*n -= 1;
}
None => {
// NOTE: Should always be >= 1, < 256 due to Shamir constraints
threshold = payload[1];
let _ = iter_count.insert(threshold - 1);
}
}
shares.push(payload[HUNK_OFFSET..].to_vec());
}
let shares = shares
@ -506,6 +544,8 @@ pub fn remote_decrypt(
.recover(&shares)
.map_err(|e| Error::CombineShares(e.to_string()))?;
/*
* Verification would take up too much size, mnemonic would be very large
let userid = UserID::from("keyfork-sss");
let kdr = DerivationRequest::new(
DerivationAlgorithm::Ed25519,
@ -524,6 +564,7 @@ pub fn remote_decrypt(
if derived_fp != expected_fp {
return Err(Error::InvalidSecret(derived_fp, expected_fp));
}
*/
print!("{}", smex::encode(&secret));