keyfork-shard: add basic oneshot decrypt mechanism

This commit is contained in:
Ryan Heywood 2023-12-26 18:09:11 -05:00
parent b873ef4d5c
commit 7eeb494819
Signed by: ryan
GPG Key ID: 8E401478A3FBEF72
4 changed files with 256 additions and 6 deletions

117
Cargo.lock generated
View File

@ -17,6 +17,41 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
[[package]]
name = "aead"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0"
dependencies = [
"crypto-common",
"generic-array",
]
[[package]]
name = "aes"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac1f845298e95f983ff1944b728ae08b8cebab80d684f0a832ed0fc74dfa27e2"
dependencies = [
"cfg-if",
"cipher",
"cpufeatures",
]
[[package]]
name = "aes-gcm"
version = "0.10.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1"
dependencies = [
"aead",
"aes",
"cipher",
"ctr",
"ghash",
"subtle",
]
[[package]]
name = "ahash"
version = "0.4.7"
@ -341,6 +376,16 @@ dependencies = [
"windows-targets 0.48.5",
]
[[package]]
name = "cipher"
version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
dependencies = [
"crypto-common",
"inout",
]
[[package]]
name = "clang-sys"
version = "1.6.1"
@ -481,9 +526,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
dependencies = [
"generic-array",
"rand_core 0.6.4",
"typenum",
]
[[package]]
name = "ctr"
version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835"
dependencies = [
"cipher",
]
[[package]]
name = "curve25519-dalek"
version = "4.1.0"
@ -783,6 +838,16 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "ghash"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d930750de5717d2dd0b8c0d42c076c0e884c81a73e6cab859bbd2339c71e3e40"
dependencies = [
"opaque-debug",
"polyval",
]
[[package]]
name = "gimli"
version = "0.28.0"
@ -903,6 +968,15 @@ dependencies = [
"hashbrown 0.14.1",
]
[[package]]
name = "inout"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5"
dependencies = [
"generic-array",
]
[[package]]
name = "insta"
version = "1.31.0"
@ -1086,11 +1160,13 @@ dependencies = [
name = "keyfork-shard"
version = "0.1.0"
dependencies = [
"aes-gcm",
"anyhow",
"bincode",
"card-backend",
"card-backend-pcsc",
"keyfork-derive-openpgp",
"keyfork-mnemonic-util",
"keyfork-prompt",
"openpgp-card",
"openpgp-card-sequoia",
@ -1099,6 +1175,7 @@ dependencies = [
"sharks",
"smex",
"thiserror",
"x25519-dalek",
]
[[package]]
@ -1426,6 +1503,12 @@ version = "1.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d"
[[package]]
name = "opaque-debug"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"
[[package]]
name = "openpgp-card"
version = "0.4.0"
@ -1624,6 +1707,18 @@ version = "3.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4503fa043bf02cee09a9582e9554b4c6403b2ef55e4612e96561d294419429f8"
[[package]]
name = "polyval"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d52cff9d1d4dee5fe6d03729099f4a310a41179e0a10dbf542039873f2e826fb"
dependencies = [
"cfg-if",
"cpufeatures",
"opaque-debug",
"universal-hash",
]
[[package]]
name = "ppv-lite86"
version = "0.2.17"
@ -2440,6 +2535,16 @@ version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c"
[[package]]
name = "universal-hash"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea"
dependencies = [
"crypto-common",
"subtle",
]
[[package]]
name = "untrusted"
version = "0.9.0"
@ -2693,6 +2798,18 @@ version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
[[package]]
name = "x25519-dalek"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fb66477291e7e8d2b0ff1bcb900bf29489a9692816d79874bea351e7a8b6de96"
dependencies = [
"curve25519-dalek",
"rand_core 0.6.4",
"serde",
"zeroize",
]
[[package]]
name = "xxhash-rust"
version = "0.8.7"

View File

@ -26,3 +26,6 @@ sharks = "0.5.0"
smex = { version = "0.1.0", path = "../smex" }
thiserror = "1.0.50"
keyfork-prompt = { version = "0.1.0", path = "../keyfork-prompt", optional = true }
x25519-dalek = { version = "2.0.0", features = ["getrandom"] }
keyfork-mnemonic-util = { version = "0.1.0", path = "../keyfork-mnemonic-util" }
aes-gcm = "0.10.3"

View File

@ -0,0 +1,128 @@
use std::{
env,
fs::File,
io::{stdin, stdout},
path::{Path, PathBuf},
process::ExitCode,
str::FromStr,
};
use aes_gcm::{aead::Aead, aes::cipher::consts::U12, Aes256Gcm, KeyInit, Nonce};
use x25519_dalek::{EphemeralSecret, PublicKey};
use keyfork_mnemonic_util::{Mnemonic, Wordlist};
use keyfork_prompt::PromptManager;
use keyfork_shard::openpgp::{decrypt_one, 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");
// Receive their key
let mut pm = PromptManager::new(stdin(), stdout())?;
let wordlist = Wordlist::default();
let their_words = pm.prompt_wordlist("Their words: ", &wordlist)?;
let mut nonce_words = their_words
.split_whitespace()
.take(9)
.peekable();
let mut pubkey_words = their_words
.split_whitespace()
.skip(9)
.take(24)
.peekable();
let mut nonce_mnemonic = String::new();
let mut pubkey_mnemonic = String::new();
while let Some(word) = nonce_words.next() {
nonce_mnemonic.push_str(word);
if nonce_words.peek().is_some() {
nonce_mnemonic.push(' ');
}
}
while let Some(word) = pubkey_words.next() {
pubkey_mnemonic.push_str(word);
if pubkey_words.peek().is_some() {
pubkey_mnemonic.push(' ');
}
}
let their_key = Mnemonic::from_str(&pubkey_mnemonic)?.entropy();
let their_key: [u8; 32] = their_key.try_into().expect("24 words");
let their_nonce = Mnemonic::from_str(&nonce_mnemonic)?.entropy();
let their_nonce = Nonce::<U12>::from_slice(&their_nonce);
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(&format!("Our words: {our_mnemonic}"))?;
let shared_secret = our_key
.diffie_hellman(&PublicKey::from(their_key))
.to_bytes();
let share = decrypt_one(encrypted_messages.into(), &cert_list, encrypted_metadata)?;
assert_eq!(share.len(), 65, "non-constant share length");
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 shared_key = Aes256Gcm::new_from_slice(&shared_secret)?;
let bytes = shared_key.encrypt(their_nonce, share.as_slice()).unwrap();
const ENC_LEN: u8 = 24 * 4;
let mut out_bytes = [(ENC_LEN - bytes.len() as u8); ENC_LEN as usize];
assert!(bytes.len() < out_bytes.len(), "encrypted payload larger than acceptable limit");
out_bytes[..bytes.len()].clone_from_slice(&bytes);
// safety: size of out_bytes is immutable and always % 32 == 0
let mnemonic = unsafe { Mnemonic::from_raw_entropy(&out_bytes, Default::default()) };
pm.prompt_message(&format!("Our payload: {mnemonic}"))?;
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
}

View File

@ -323,15 +323,17 @@ fn decrypt_metadata(
})
}
fn decrypt_one(
pub fn decrypt_one(
messages: Vec<EncryptedMessage>,
keyring: &mut Keyring,
manager: &mut SmartcardManager,
certs: &[Cert],
metadata: EncryptedMessage,
) -> Result<Vec<u8>> {
let policy = NullPolicy::new();
let content = decrypt_metadata(&metadata, &policy, &mut *keyring, &mut *manager)?;
let mut keyring = Keyring::new(certs)?;
let mut manager = SmartcardManager::new()?;
let content = decrypt_metadata(&metadata, &policy, &mut keyring, &mut manager)?;
let (_threshold, root_cert, certs) = decode_metadata_v1(&content)?;
@ -341,13 +343,13 @@ fn decrypt_one(
let mut messages: HashMap<KeyID, EncryptedMessage> =
HashMap::from_iter(certs.iter().map(|c| c.keyid()).zip(messages));
let decrypted_messages = decrypt_with_keyring(&mut messages, &certs, &policy, keyring)?;
let decrypted_messages = decrypt_with_keyring(&mut messages, &certs, &policy, &mut keyring)?;
if let Some(message) = decrypted_messages.into_values().next() {
return Ok(message);
}
let decrypted_messages = decrypt_with_manager(1, &mut messages, &certs, policy, manager)?;
let decrypted_messages = decrypt_with_manager(1, &mut messages, &certs, policy, &mut manager)?;
if let Some(message) = decrypted_messages.into_values().next() {
return Ok(message);