keyfork-shard: add basic oneshot decrypt mechanism
This commit is contained in:
parent
b873ef4d5c
commit
7eeb494819
|
@ -17,6 +17,41 @@ version = "1.0.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
|
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]]
|
[[package]]
|
||||||
name = "ahash"
|
name = "ahash"
|
||||||
version = "0.4.7"
|
version = "0.4.7"
|
||||||
|
@ -341,6 +376,16 @@ dependencies = [
|
||||||
"windows-targets 0.48.5",
|
"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]]
|
[[package]]
|
||||||
name = "clang-sys"
|
name = "clang-sys"
|
||||||
version = "1.6.1"
|
version = "1.6.1"
|
||||||
|
@ -481,9 +526,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
|
checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"generic-array",
|
"generic-array",
|
||||||
|
"rand_core 0.6.4",
|
||||||
"typenum",
|
"typenum",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ctr"
|
||||||
|
version = "0.9.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835"
|
||||||
|
dependencies = [
|
||||||
|
"cipher",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "curve25519-dalek"
|
name = "curve25519-dalek"
|
||||||
version = "4.1.0"
|
version = "4.1.0"
|
||||||
|
@ -783,6 +838,16 @@ dependencies = [
|
||||||
"wasm-bindgen",
|
"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]]
|
[[package]]
|
||||||
name = "gimli"
|
name = "gimli"
|
||||||
version = "0.28.0"
|
version = "0.28.0"
|
||||||
|
@ -903,6 +968,15 @@ dependencies = [
|
||||||
"hashbrown 0.14.1",
|
"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]]
|
[[package]]
|
||||||
name = "insta"
|
name = "insta"
|
||||||
version = "1.31.0"
|
version = "1.31.0"
|
||||||
|
@ -1086,11 +1160,13 @@ dependencies = [
|
||||||
name = "keyfork-shard"
|
name = "keyfork-shard"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"aes-gcm",
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"bincode",
|
"bincode",
|
||||||
"card-backend",
|
"card-backend",
|
||||||
"card-backend-pcsc",
|
"card-backend-pcsc",
|
||||||
"keyfork-derive-openpgp",
|
"keyfork-derive-openpgp",
|
||||||
|
"keyfork-mnemonic-util",
|
||||||
"keyfork-prompt",
|
"keyfork-prompt",
|
||||||
"openpgp-card",
|
"openpgp-card",
|
||||||
"openpgp-card-sequoia",
|
"openpgp-card-sequoia",
|
||||||
|
@ -1099,6 +1175,7 @@ dependencies = [
|
||||||
"sharks",
|
"sharks",
|
||||||
"smex",
|
"smex",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
|
"x25519-dalek",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -1426,6 +1503,12 @@ version = "1.18.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d"
|
checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "opaque-debug"
|
||||||
|
version = "0.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "openpgp-card"
|
name = "openpgp-card"
|
||||||
version = "0.4.0"
|
version = "0.4.0"
|
||||||
|
@ -1624,6 +1707,18 @@ version = "3.1.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4503fa043bf02cee09a9582e9554b4c6403b2ef55e4612e96561d294419429f8"
|
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]]
|
[[package]]
|
||||||
name = "ppv-lite86"
|
name = "ppv-lite86"
|
||||||
version = "0.2.17"
|
version = "0.2.17"
|
||||||
|
@ -2440,6 +2535,16 @@ version = "0.2.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c"
|
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]]
|
[[package]]
|
||||||
name = "untrusted"
|
name = "untrusted"
|
||||||
version = "0.9.0"
|
version = "0.9.0"
|
||||||
|
@ -2693,6 +2798,18 @@ version = "0.48.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
|
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]]
|
[[package]]
|
||||||
name = "xxhash-rust"
|
name = "xxhash-rust"
|
||||||
version = "0.8.7"
|
version = "0.8.7"
|
||||||
|
|
|
@ -26,3 +26,6 @@ sharks = "0.5.0"
|
||||||
smex = { version = "0.1.0", path = "../smex" }
|
smex = { version = "0.1.0", path = "../smex" }
|
||||||
thiserror = "1.0.50"
|
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"] }
|
||||||
|
keyfork-mnemonic-util = { version = "0.1.0", path = "../keyfork-mnemonic-util" }
|
||||||
|
aes-gcm = "0.10.3"
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -323,15 +323,17 @@ fn decrypt_metadata(
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn decrypt_one(
|
pub fn decrypt_one(
|
||||||
messages: Vec<EncryptedMessage>,
|
messages: Vec<EncryptedMessage>,
|
||||||
keyring: &mut Keyring,
|
certs: &[Cert],
|
||||||
manager: &mut SmartcardManager,
|
|
||||||
metadata: EncryptedMessage,
|
metadata: EncryptedMessage,
|
||||||
) -> Result<Vec<u8>> {
|
) -> Result<Vec<u8>> {
|
||||||
let policy = NullPolicy::new();
|
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)?;
|
let (_threshold, root_cert, certs) = decode_metadata_v1(&content)?;
|
||||||
|
|
||||||
|
@ -341,13 +343,13 @@ fn decrypt_one(
|
||||||
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));
|
||||||
|
|
||||||
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() {
|
if let Some(message) = decrypted_messages.into_values().next() {
|
||||||
return Ok(message);
|
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() {
|
if let Some(message) = decrypted_messages.into_values().next() {
|
||||||
return Ok(message);
|
return Ok(message);
|
||||||
|
|
Loading…
Reference in New Issue