keyfork-shard: make remote operation algorithm agnostic
This commit is contained in:
parent
3240ab9e1f
commit
a79c4a4079
|
@ -132,9 +132,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.75"
|
||||
version = "1.0.79"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6"
|
||||
checksum = "080e9890a082662b09c1ad45f567faeeb47f22b5fb23895fbe1e651e718e25ca"
|
||||
|
||||
[[package]]
|
||||
name = "ascii-canvas"
|
||||
|
@ -1162,7 +1162,6 @@ version = "0.1.0"
|
|||
dependencies = [
|
||||
"aes-gcm",
|
||||
"anyhow",
|
||||
"bincode",
|
||||
"card-backend",
|
||||
"card-backend-pcsc",
|
||||
"keyfork-derive-openpgp",
|
||||
|
@ -1171,7 +1170,6 @@ dependencies = [
|
|||
"openpgp-card",
|
||||
"openpgp-card-sequoia",
|
||||
"sequoia-openpgp",
|
||||
"serde",
|
||||
"sharks",
|
||||
"smex",
|
||||
"thiserror",
|
||||
|
|
|
@ -8,24 +8,26 @@ license = "AGPL-3.0-only"
|
|||
|
||||
[features]
|
||||
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"]
|
||||
prompt = ["keyfork-prompt"]
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.75"
|
||||
bincode = "1.3.3"
|
||||
sharks = "0.5.0"
|
||||
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-pcsc = { version = "0.5.0", optional = true }
|
||||
keyfork-derive-openpgp = { version = "0.1.0", path = "../keyfork-derive-openpgp" }
|
||||
openpgp-card-sequoia = { version = "0.2.0", optional = true }
|
||||
openpgp-card = { version = "0.4.0", 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 }
|
||||
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"] }
|
||||
|
|
|
@ -3,7 +3,7 @@ use std::{
|
|||
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>;
|
||||
|
|
@ -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")]
|
||||
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(())
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@ use std::{
|
|||
};
|
||||
|
||||
use aes_gcm::{
|
||||
aead::{consts::U12, Aead, AeadCore, OsRng},
|
||||
aead::{consts::U12, Aead},
|
||||
Aes256Gcm, Error as AesError, KeyInit, Nonce,
|
||||
};
|
||||
use keyfork_derive_openpgp::derive_util::{
|
||||
|
@ -33,13 +33,13 @@ use openpgp::{
|
|||
};
|
||||
pub use sequoia_openpgp as openpgp;
|
||||
use sharks::{Share, Sharks};
|
||||
use x25519_dalek::{EphemeralSecret, PublicKey};
|
||||
|
||||
mod keyring;
|
||||
use keyring::Keyring;
|
||||
|
||||
mod smartcard;
|
||||
use smartcard::SmartcardManager;
|
||||
use x25519_dalek::{EphemeralSecret, PublicKey};
|
||||
|
||||
/// Shard metadata verson 1:
|
||||
/// 1 byte: Version
|
||||
|
@ -48,12 +48,7 @@ use x25519_dalek::{EphemeralSecret, PublicKey};
|
|||
const SHARD_METADATA_VERSION: u8 = 1;
|
||||
const SHARD_METADATA_OFFSET: usize = 2;
|
||||
|
||||
/// Decrypt hunk version 1:
|
||||
/// 1 byte: Version
|
||||
/// 1 byte: Threshold
|
||||
/// Data: &[u8]
|
||||
const HUNK_VERSION: u8 = 1;
|
||||
const HUNK_OFFSET: usize = 2;
|
||||
use super::HUNK_VERSION;
|
||||
|
||||
// 256 bit share is 49 bytes + some amount of hunk bytes, gives us reasonable padding
|
||||
const ENC_LEN: u8 = 4 * 16;
|
||||
|
@ -467,110 +462,6 @@ pub fn decrypt(
|
|||
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(
|
||||
certs: Vec<Cert>,
|
||||
metadata: EncryptedMessage,
|
||||
|
|
Loading…
Reference in New Issue