keyfork-shard: make remote operation algorithm agnostic

This commit is contained in:
Ryan Heywood 2024-01-04 23:05:30 -05:00
parent 3240ab9e1f
commit a79c4a4079
Signed by: ryan
GPG Key ID: 8E401478A3FBEF72
5 changed files with 143 additions and 127 deletions

6
Cargo.lock generated
View File

@ -132,9 +132,9 @@ dependencies = [
[[package]] [[package]]
name = "anyhow" name = "anyhow"
version = "1.0.75" version = "1.0.79"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" checksum = "080e9890a082662b09c1ad45f567faeeb47f22b5fb23895fbe1e651e718e25ca"
[[package]] [[package]]
name = "ascii-canvas" name = "ascii-canvas"
@ -1162,7 +1162,6 @@ version = "0.1.0"
dependencies = [ dependencies = [
"aes-gcm", "aes-gcm",
"anyhow", "anyhow",
"bincode",
"card-backend", "card-backend",
"card-backend-pcsc", "card-backend-pcsc",
"keyfork-derive-openpgp", "keyfork-derive-openpgp",
@ -1171,7 +1170,6 @@ dependencies = [
"openpgp-card", "openpgp-card",
"openpgp-card-sequoia", "openpgp-card-sequoia",
"sequoia-openpgp", "sequoia-openpgp",
"serde",
"sharks", "sharks",
"smex", "smex",
"thiserror", "thiserror",

View File

@ -8,24 +8,26 @@ license = "AGPL-3.0-only"
[features] [features]
default = ["openpgp", "openpgp-card"] default = ["openpgp", "openpgp-card"]
openpgp = ["sequoia-openpgp", "prompt"] openpgp = ["sequoia-openpgp", "prompt", "anyhow"]
openpgp-card = ["openpgp-card-sequoia", "card-backend-pcsc", "card-backend", "dep:openpgp-card"] openpgp-card = ["openpgp-card-sequoia", "card-backend-pcsc", "card-backend", "dep:openpgp-card"]
prompt = ["keyfork-prompt"] prompt = ["keyfork-prompt"]
[dependencies] [dependencies]
anyhow = "1.0.75" sharks = "0.5.0"
bincode = "1.3.3" smex = { version = "0.1.0", path = "../smex" }
thiserror = "1.0.50"
# Remote operator mode
keyfork-mnemonic-util = { version = "0.1.0", path = "../keyfork-mnemonic-util" }
x25519-dalek = { version = "2.0.0", features = ["getrandom"] }
aes-gcm = { version = "0.10.3", features = ["std"] }
# OpenPGP
anyhow = { version = "1.0.79", optional = true }
card-backend = { version = "0.2.0", optional = true } card-backend = { version = "0.2.0", optional = true }
card-backend-pcsc = { version = "0.5.0", optional = true } card-backend-pcsc = { version = "0.5.0", optional = true }
keyfork-derive-openpgp = { version = "0.1.0", path = "../keyfork-derive-openpgp" } keyfork-derive-openpgp = { version = "0.1.0", path = "../keyfork-derive-openpgp" }
openpgp-card-sequoia = { version = "0.2.0", optional = true } openpgp-card-sequoia = { version = "0.2.0", optional = true }
openpgp-card = { version = "0.4.0", optional = true } openpgp-card = { version = "0.4.0", optional = true }
sequoia-openpgp = { version = "1.16.1", optional = true } sequoia-openpgp = { version = "1.16.1", optional = true }
serde = "1.0.188"
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 } 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 = { version = "0.10.3", features = ["std"] }

View File

@ -3,7 +3,7 @@ use std::{
process::ExitCode, process::ExitCode,
}; };
use keyfork_shard::openpgp::remote_decrypt; use keyfork_shard::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>;

View File

@ -1,2 +1,127 @@
use std::{
io::{stdin, stdout},
str::FromStr,
};
use aes_gcm::{
aead::{Aead, AeadCore, OsRng},
Aes256Gcm, KeyInit,
};
use keyfork_mnemonic_util::{Mnemonic, Wordlist};
use keyfork_prompt::{Message as PromptMessage, PromptManager};
use sharks::{Share, Sharks};
use x25519_dalek::{EphemeralSecret, PublicKey};
#[cfg(feature = "openpgp")] #[cfg(feature = "openpgp")]
pub mod openpgp; pub mod openpgp;
/// Decrypt hunk version 1:
/// 1 byte: Version
/// 1 byte: Threshold
/// Data: &[u8]
pub(crate) const HUNK_VERSION: u8 = 1;
pub(crate) const HUNK_OFFSET: usize = 2;
pub fn remote_decrypt() -> Result<(), Box<dyn std::error::Error>> {
let mut pm = PromptManager::new(stdin(), stdout())?;
let wordlist = Wordlist::default();
let mut iter_count = None;
let mut shares = vec![];
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()) };
let our_key = EphemeralSecret::random();
let key_mnemonic =
Mnemonic::from_entropy(PublicKey::from(&our_key).as_bytes(), Default::default())?;
pm.prompt_message(PromptMessage::Text(format!(
"Our words: {nonce_mnemonic} {key_mnemonic}"
)))?;
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");
let shared_secret = our_key
.diffie_hellman(&PublicKey::from(their_key))
.to_bytes();
let shared_key =
Aes256Gcm::new_from_slice(&shared_secret).expect("Invalid length of constant key size");
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");
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
.into_iter()
.map(|s| Share::try_from(s.as_slice()))
.collect::<Result<Vec<_>, &str>>()
.map_err(|e| anyhow::anyhow!("{e}"))?;
let secret = Sharks(threshold)
.recover(&shares)
.map_err(|e| anyhow::anyhow!("{e}"))?;
/*
* Verification would take up too much size, mnemonic would be very large
let userid = UserID::from("keyfork-sss");
let kdr = DerivationRequest::new(
DerivationAlgorithm::Ed25519,
&DerivationPath::from_str("m/7366512'/0'")?,
)
.derive_with_master_seed(secret.to_vec())?;
let derived_cert = keyfork_derive_openpgp::derive(
kdr,
&[KeyFlags::empty().set_certification().set_signing()],
userid,
)?;
// NOTE: Signatures on certs will be different. Compare fingerprints instead.
let derived_fp = derived_cert.fingerprint();
let expected_fp = root_cert.fingerprint();
if derived_fp != expected_fp {
return Err(Error::InvalidSecret(derived_fp, expected_fp));
}
*/
print!("{}", smex::encode(&secret));
Ok(())
}

View File

@ -6,7 +6,7 @@ use std::{
}; };
use aes_gcm::{ use aes_gcm::{
aead::{consts::U12, Aead, AeadCore, OsRng}, aead::{consts::U12, Aead},
Aes256Gcm, Error as AesError, KeyInit, Nonce, Aes256Gcm, Error as AesError, KeyInit, Nonce,
}; };
use keyfork_derive_openpgp::derive_util::{ use keyfork_derive_openpgp::derive_util::{
@ -33,13 +33,13 @@ use openpgp::{
}; };
pub use sequoia_openpgp as openpgp; pub use sequoia_openpgp as openpgp;
use sharks::{Share, Sharks}; use sharks::{Share, Sharks};
use x25519_dalek::{EphemeralSecret, PublicKey};
mod keyring; mod keyring;
use keyring::Keyring; use keyring::Keyring;
mod smartcard; mod smartcard;
use smartcard::SmartcardManager; use smartcard::SmartcardManager;
use x25519_dalek::{EphemeralSecret, PublicKey};
/// Shard metadata verson 1: /// Shard metadata verson 1:
/// 1 byte: Version /// 1 byte: Version
@ -48,12 +48,7 @@ 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;
/// Decrypt hunk version 1: use super::HUNK_VERSION;
/// 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 // 256 bit share is 49 bytes + some amount of hunk bytes, gives us reasonable padding
const ENC_LEN: u8 = 4 * 16; const ENC_LEN: u8 = 4 * 16;
@ -467,110 +462,6 @@ pub fn decrypt(
Ok(()) Ok(())
} }
pub fn remote_decrypt() -> Result<()> {
let mut pm = PromptManager::new(stdin(), stdout())?;
let wordlist = Wordlist::default();
let mut iter_count = None;
let mut shares = vec![];
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()) };
let our_key = EphemeralSecret::random();
let key_mnemonic =
Mnemonic::from_entropy(PublicKey::from(&our_key).as_bytes(), Default::default())?;
pm.prompt_message(PromptMessage::Text(format!(
"Our words: {nonce_mnemonic} {key_mnemonic}"
)))?;
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");
let shared_secret = our_key
.diffie_hellman(&PublicKey::from(their_key))
.to_bytes();
let shared_key =
Aes256Gcm::new_from_slice(&shared_secret).expect("Invalid length of constant key size");
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");
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
.into_iter()
.map(|s| Share::try_from(s.as_slice()))
.collect::<Result<Vec<_>, &str>>()
.map_err(|e| Error::Share(e.to_string()))?;
let secret = Sharks(threshold)
.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,
&DerivationPath::from_str("m/7366512'/0'")?,
)
.derive_with_master_seed(secret.to_vec())?;
let derived_cert = keyfork_derive_openpgp::derive(
kdr,
&[KeyFlags::empty().set_certification().set_signing()],
userid,
)?;
// NOTE: Signatures on certs will be different. Compare fingerprints instead.
let derived_fp = derived_cert.fingerprint();
let expected_fp = root_cert.fingerprint();
if derived_fp != expected_fp {
return Err(Error::InvalidSecret(derived_fp, expected_fp));
}
*/
print!("{}", smex::encode(&secret));
Ok(())
}
pub fn combine( pub fn combine(
certs: Vec<Cert>, certs: Vec<Cert>,
metadata: EncryptedMessage, metadata: EncryptedMessage,