keyfork-shard: make remote operation algorithm agnostic
This commit is contained in:
parent
3240ab9e1f
commit
a79c4a4079
|
@ -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",
|
||||||
|
|
|
@ -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"] }
|
|
||||||
|
|
|
@ -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>;
|
||||||
|
|
|
@ -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(())
|
||||||
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
Loading…
Reference in New Issue