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::{
|
use std::{
|
||||||
env,
|
env,
|
||||||
fs::File,
|
|
||||||
path::{Path, PathBuf},
|
|
||||||
process::ExitCode,
|
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>;
|
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<()> {
|
fn run() -> Result<()> {
|
||||||
let mut args = env::args();
|
let mut args = env::args();
|
||||||
let program_name = args.next().expect("program name");
|
let program_name = args.next().expect("program name");
|
||||||
let args = args.collect::<Vec<_>>();
|
let args = args.collect::<Vec<_>>();
|
||||||
let (messages_file, cert_list) = match args.as_slice() {
|
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}"),
|
||||||
_ => panic!("Usage: {program_name} messages_file [key_discovery]"),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut encrypted_messages = parse_messages(messages_file)?;
|
remote_decrypt()?;
|
||||||
|
|
||||||
let encrypted_metadata = encrypted_messages
|
|
||||||
.pop_front()
|
|
||||||
.expect("any pgp encrypted message");
|
|
||||||
|
|
||||||
remote_decrypt(
|
|
||||||
&cert_list,
|
|
||||||
encrypted_metadata,
|
|
||||||
encrypted_messages.make_contiguous(),
|
|
||||||
)?;
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,7 +7,7 @@ use std::{
|
||||||
|
|
||||||
use aes_gcm::{
|
use aes_gcm::{
|
||||||
aead::{consts::U12, Aead, AeadCore, OsRng},
|
aead::{consts::U12, Aead, AeadCore, OsRng},
|
||||||
Aes256Gcm, KeyInit, Nonce, Error as AesError
|
Aes256Gcm, Error as AesError, KeyInit, Nonce,
|
||||||
};
|
};
|
||||||
use keyfork_derive_openpgp::derive_util::{
|
use keyfork_derive_openpgp::derive_util::{
|
||||||
request::{DerivationAlgorithm, DerivationRequest},
|
request::{DerivationAlgorithm, DerivationRequest},
|
||||||
|
@ -48,7 +48,15 @@ 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;
|
/// 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)]
|
#[derive(Debug, thiserror::Error)]
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
|
@ -411,22 +419,23 @@ pub fn decrypt(
|
||||||
let our_key = EphemeralSecret::random();
|
let our_key = EphemeralSecret::random();
|
||||||
let our_mnemonic =
|
let our_mnemonic =
|
||||||
Mnemonic::from_entropy(PublicKey::from(&our_key).as_bytes(), Default::default())?;
|
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
|
let shared_secret = our_key
|
||||||
.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 (mut share, threshold, ..) = decrypt_one(encrypted_messages.to_vec(), &certs, metadata)?;
|
||||||
assert!(share.len() <= 65, "invalid share length (too long)");
|
share.insert(0, HUNK_VERSION);
|
||||||
const LEN: u8 = 24 * 3;
|
share.insert(1, threshold);
|
||||||
let mut encrypted_payload = [(LEN - share.len() as u8); LEN as usize];
|
assert!(
|
||||||
encrypted_payload[..share.len()].copy_from_slice(&share);
|
share.len() <= ENC_LEN as usize,
|
||||||
|
"invalid share length (too long, max {ENC_LEN} bytes)"
|
||||||
|
);
|
||||||
|
|
||||||
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())?;
|
||||||
|
dbg!(bytes.len());
|
||||||
shared_key.decrypt(their_nonce, &bytes[..])?;
|
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 ENC_LEN < u8::MAX
|
||||||
|
@ -451,25 +460,23 @@ pub fn decrypt(
|
||||||
// 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()) };
|
||||||
|
|
||||||
pm.prompt_message(PromptMessage::Text(format!("Our payload: {mnemonic}")))?;
|
pm.prompt_message(PromptMessage::Text(format!(
|
||||||
|
"Our words: {our_mnemonic} {mnemonic}"
|
||||||
|
)))?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn remote_decrypt(
|
pub fn remote_decrypt() -> Result<()> {
|
||||||
certs: &[Cert],
|
|
||||||
metadata: EncryptedMessage,
|
|
||||||
encrypted_messages: &[EncryptedMessage],
|
|
||||||
) -> Result<()> {
|
|
||||||
let mut pm = PromptManager::new(stdin(), stdout())?;
|
let mut pm = PromptManager::new(stdin(), stdout())?;
|
||||||
let wordlist = Wordlist::default();
|
let wordlist = Wordlist::default();
|
||||||
|
|
||||||
// Get our cert so we know our metadata
|
let mut iter_count = None;
|
||||||
let (share, threshold, root_cert) = decrypt_one(encrypted_messages.to_vec(), &certs, metadata)?;
|
let mut shares = vec![];
|
||||||
let mut shares = Vec::with_capacity(threshold as usize);
|
|
||||||
shares.push(share);
|
|
||||||
|
|
||||||
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 = Aes256Gcm::generate_nonce(&mut OsRng);
|
||||||
let nonce_mnemonic =
|
let nonce_mnemonic =
|
||||||
unsafe { Mnemonic::from_raw_entropy(nonce.as_slice(), Default::default()) };
|
unsafe { Mnemonic::from_raw_entropy(nonce.as_slice(), Default::default()) };
|
||||||
|
@ -480,7 +487,25 @@ pub fn remote_decrypt(
|
||||||
"Our words: {nonce_mnemonic} {key_mnemonic}"
|
"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 = Mnemonic::from_str(&pubkey_mnemonic)?.entropy();
|
||||||
let their_key: [u8; 32] = their_key.try_into().expect("24 words");
|
let their_key: [u8; 32] = their_key.try_into().expect("24 words");
|
||||||
|
|
||||||
|
@ -490,11 +515,24 @@ pub fn remote_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 payload_mnemonic = pm.prompt_wordlist("Their payload: ", &wordlist)?;
|
|
||||||
let payload = Mnemonic::from_str(&payload_mnemonic)?.entropy();
|
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])?;
|
match &mut iter_count {
|
||||||
shares.push(decrypted_share);
|
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
|
let shares = shares
|
||||||
|
@ -506,6 +544,8 @@ pub fn remote_decrypt(
|
||||||
.recover(&shares)
|
.recover(&shares)
|
||||||
.map_err(|e| Error::CombineShares(e.to_string()))?;
|
.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 userid = UserID::from("keyfork-sss");
|
||||||
let kdr = DerivationRequest::new(
|
let kdr = DerivationRequest::new(
|
||||||
DerivationAlgorithm::Ed25519,
|
DerivationAlgorithm::Ed25519,
|
||||||
|
@ -524,6 +564,7 @@ pub fn remote_decrypt(
|
||||||
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));
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
print!("{}", smex::encode(&secret));
|
print!("{}", smex::encode(&secret));
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue