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:
parent
1b19a08cd4
commit
3240ab9e1f
|
@ -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(())
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
|
||||
|
|
Loading…
Reference in New Issue