Compare commits

..

No commits in common. "b4dbc6ff347353c4476dab7fc1fba92b694f1581" and "e071dc1cfc57eb882c6661b68264e6658b59691d" have entirely different histories.

13 changed files with 326 additions and 501 deletions

View File

@ -16,7 +16,7 @@ multithread = ["tokio/rt-multi-thread"]
[dependencies] [dependencies]
keyfork-bug = { workspace = true } keyfork-bug = { workspace = true }
keyfork-derive-util = { workspace = true, default_features = true } keyfork-derive-util = { workspace = true }
keyfork-frame = { workspace = true, features = ["async"] } keyfork-frame = { workspace = true, features = ["async"] }
keyfork-mnemonic = { workspace = true } keyfork-mnemonic = { workspace = true }
keyfork-derive-path-data = { workspace = true } keyfork-derive-path-data = { workspace = true }

View File

@ -8,7 +8,7 @@ use std::{
}; };
use keyfork_prompt::default_handler; use keyfork_prompt::default_handler;
use keyfork_shard::{openpgp::OpenPGP, Format, default_transfer}; use keyfork_shard::{openpgp::OpenPGP, Format};
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>;
@ -34,13 +34,11 @@ fn run() -> Result<()> {
let openpgp = OpenPGP; let openpgp = OpenPGP;
let prompt_handler = default_handler()?; let prompt_handler = default_handler()?;
let mut transfer = default_transfer()?;
openpgp.decrypt_one_shard_for_transport( openpgp.decrypt_one_shard_for_transport(
key_discovery.as_deref(), key_discovery.as_deref(),
messages_file, messages_file,
prompt_handler, prompt_handler,
&mut *transfer,
)?; )?;
Ok(()) Ok(())

View File

@ -2,7 +2,7 @@
use std::{env, process::ExitCode}; use std::{env, process::ExitCode};
use keyfork_shard::{remote_decrypt, default_transfer}; 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>;
@ -15,10 +15,8 @@ fn run() -> Result<()> {
_ => panic!("Usage: {program_name}"), _ => panic!("Usage: {program_name}"),
} }
let mut transfer = default_transfer()?;
let mut bytes = vec![]; let mut bytes = vec![];
remote_decrypt(&mut bytes, &mut *transfer)?; remote_decrypt(&mut bytes)?;
print!("{}", smex::encode(bytes)); print!("{}", smex::encode(bytes));
Ok(()) Ok(())

View File

@ -3,23 +3,30 @@
use std::{ use std::{
io::{Read, Write}, io::{Read, Write},
rc::Rc, rc::Rc,
sync::Mutex, str::FromStr,
sync::{LazyLock, Mutex},
}; };
use aes_gcm::{ use aes_gcm::{
aead::{consts::U12, Aead}, aead::{consts::U12, Aead},
Aes256Gcm, KeyInit, Nonce, Aes256Gcm, KeyInit, Nonce,
}; };
use base64::prelude::{Engine, BASE64_STANDARD};
use blahaj::{Share, Sharks}; use blahaj::{Share, Sharks};
use hkdf::Hkdf; use hkdf::Hkdf;
use keyfork_bug::{bug, POISONED_MUTEX}; use keyfork_bug::{bug, POISONED_MUTEX};
use keyfork_prompt::PromptHandler; use keyfork_mnemonic::{English, Mnemonic};
use sha2::Sha256; use keyfork_prompt::{
prompt_validated_wordlist,
validators::{
mnemonic::{MnemonicSetValidator, MnemonicValidator, WordLength},
Validator,
},
Message as PromptMessage, PromptHandler, YesNo,
};
use sha2::{Digest, Sha256};
use x25519_dalek::{EphemeralSecret, PublicKey}; use x25519_dalek::{EphemeralSecret, PublicKey};
pub mod transfer;
pub use transfer::{default_transfer, Transfer};
const PLAINTEXT_LENGTH: u8 = 32 // shard const PLAINTEXT_LENGTH: u8 = 32 // shard
+ 1 // index + 1 // index
+ 1 // threshold + 1 // threshold
@ -27,6 +34,45 @@ const PLAINTEXT_LENGTH: u8 = 32 // shard
+ 1; // length; + 1; // length;
const ENCRYPTED_LENGTH: u8 = PLAINTEXT_LENGTH + 16; const ENCRYPTED_LENGTH: u8 = PLAINTEXT_LENGTH + 16;
#[derive(PartialEq, Eq, Clone, Copy)]
enum RetryScanMnemonic {
Retry,
Continue,
}
impl keyfork_prompt::Choice for RetryScanMnemonic {
fn identifier(&self) -> Option<char> {
Some(match self {
RetryScanMnemonic::Retry => 'r',
RetryScanMnemonic::Continue => 'c',
})
}
}
impl std::fmt::Display for RetryScanMnemonic {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
RetryScanMnemonic::Retry => write!(f, "Retry scanning mnemonic."),
RetryScanMnemonic::Continue => write!(f, "Continue to manual mnemonic entry."),
}
}
}
fn calculate_checksum(slice: &[u8]) -> Vec<u8> {
// generate a verification checksum
// this checksum should be expensive to calculate
let mut payload = vec![];
for _ in 0..1_000_000 {
payload.extend(slice);
let mut hasher = Sha256::new();
hasher.update(&payload);
let result = hasher.finalize();
payload.clear();
payload.extend(result);
}
payload
}
#[cfg(feature = "openpgp")] #[cfg(feature = "openpgp")]
pub mod openpgp; pub mod openpgp;
@ -223,7 +269,6 @@ pub trait Format {
private_key_discovery: Option<impl KeyDiscovery<Self>>, private_key_discovery: Option<impl KeyDiscovery<Self>>,
reader: impl Read + Send + Sync, reader: impl Read + Send + Sync,
prompt: Box<dyn PromptHandler>, prompt: Box<dyn PromptHandler>,
transfer: &mut dyn Transfer,
) -> Result<(), Box<dyn std::error::Error>> { ) -> Result<(), Box<dyn std::error::Error>> {
let prompt = Rc::new(Mutex::new(prompt)); let prompt = Rc::new(Mutex::new(prompt));
@ -234,12 +279,80 @@ pub trait Format {
let encrypted_messages = self.parse_shard_file(reader)?; let encrypted_messages = self.parse_shard_file(reader)?;
// establish AES-256-GCM key via ECDH // establish AES-256-GCM key via ECDH
let their_pubkey = let mut pubkey_data: Option<[u8; 32]> = None;
transfer.receive_pubkey(&mut **prompt.lock().expect(bug!(POISONED_MUTEX)))?;
// receive remote data via scanning QR code from camera
#[cfg(feature = "qrcode")]
{
prompt
.lock()
.expect(bug!(POISONED_MUTEX))
.prompt_message(PromptMessage::Text(QRCODE_PROMPT.to_string()))?;
loop {
if let Ok(Some(qrcode_content)) =
keyfork_qrcode::scan_camera(std::time::Duration::from_secs(*QRCODE_TIMEOUT), 0)
{
let decoded_data = BASE64_STANDARD
.decode(qrcode_content)
.expect(bug!("qrcode should contain base64 encoded data"));
let data: [u8; 32] = decoded_data.try_into().map_err(|_| InvalidData)?;
let checksum = calculate_checksum(&data);
let small_sum = &checksum[..8];
let small_mnemonic = Mnemonic::from_raw_bytes(small_sum);
let mut prompt = prompt.lock().expect(bug!(POISONED_MUTEX));
let question =
format!("Do these words match the expected words? {small_mnemonic}");
let response = keyfork_prompt::prompt_choice(
&mut **prompt,
&question,
&[YesNo::No, YesNo::Yes],
)?;
if response == YesNo::No {
prompt.prompt_message(PromptMessage::Text(String::from(
"Could not establish secure channel, exiting.",
)))?;
std::process::exit(1);
}
pubkey_data = Some(data);
break;
} else {
let mut prompt = prompt.lock().expect(bug!(POISONED_MUTEX));
let choice = keyfork_prompt::prompt_choice(
&mut **prompt,
"A QR code could not be scanned. Retry or continue?",
&[RetryScanMnemonic::Retry, RetryScanMnemonic::Continue],
)?;
if choice == RetryScanMnemonic::Continue {
break;
}
}
}
}
// if QR code scanning failed or was unavailable, read from a set of mnemonics
let their_pubkey = if let Some(pubkey) = pubkey_data {
pubkey
} else {
let validator = MnemonicValidator {
word_length: Some(WordLength::Count(24)),
};
let mut prompt = prompt.lock().expect(bug!(POISONED_MUTEX));
prompt_validated_wordlist::<English, _>(
&mut **prompt,
QRCODE_COULDNT_READ,
3,
&*validator.to_fn(),
)?
.as_bytes()
.try_into()
.map_err(|_| InvalidData)?
};
// create our shared key // create our shared key
let our_key = EphemeralSecret::random(); let our_key = EphemeralSecret::random();
let our_pubkey = PublicKey::from(&our_key).to_bytes(); let our_pubkey_mnemonic = Mnemonic::try_from_slice(PublicKey::from(&our_key).as_bytes())?;
let shared_secret = our_key.diffie_hellman(&PublicKey::from(their_pubkey)); let shared_secret = our_key.diffie_hellman(&PublicKey::from(their_pubkey));
assert!( assert!(
shared_secret.was_contributory(), shared_secret.was_contributory(),
@ -288,14 +401,44 @@ pub trait Format {
ENCRYPTED_LENGTH as usize, ENCRYPTED_LENGTH as usize,
bug!("encrypted bytes size != expected len"), bug!("encrypted bytes size != expected len"),
); );
let mut payload_bytes = [0u8; ENCRYPTED_LENGTH as usize]; let mut mnemonic_bytes = [0u8; ENCRYPTED_LENGTH as usize];
payload_bytes.copy_from_slice(&encrypted_bytes); mnemonic_bytes.copy_from_slice(&encrypted_bytes);
transfer.send_encrypted_payload( let payload_mnemonic = Mnemonic::from_array(mnemonic_bytes);
&mut **prompt.lock().expect(bug!(POISONED_MUTEX)),
our_pubkey, #[cfg(feature = "qrcode")]
payload_bytes, {
)?; use keyfork_qrcode::{qrencode, ErrorCorrection};
let mut qrcode_data = our_pubkey_mnemonic.to_bytes();
qrcode_data.extend(payload_mnemonic.as_bytes());
if let Ok(qrcode) = qrencode(
&BASE64_STANDARD.encode(qrcode_data),
ErrorCorrection::Highest,
) {
prompt
.lock()
.expect(bug!(POISONED_MUTEX))
.prompt_message(PromptMessage::Text(
concat!(
"A QR code will be displayed after this prompt. ",
"Send the QR code back to the operator combining the shards. ",
"Nobody else should scan this QR code."
)
.to_string(),
))?;
prompt
.lock()
.expect(bug!(POISONED_MUTEX))
.prompt_message(PromptMessage::Data(qrcode))?;
}
}
prompt
.lock()
.expect(bug!(POISONED_MUTEX))
.prompt_message(PromptMessage::Text(format!(
"Upon request, these words should be sent: {our_pubkey_mnemonic} {payload_mnemonic}"
)))?;
Ok(()) Ok(())
} }
@ -369,6 +512,15 @@ pub struct InvalidData;
pub(crate) const HUNK_VERSION: u8 = 2; pub(crate) const HUNK_VERSION: u8 = 2;
pub(crate) const HUNK_OFFSET: usize = 2; pub(crate) const HUNK_OFFSET: usize = 2;
const QRCODE_PROMPT: &str = "Press enter, then present QR code to camera.";
const QRCODE_COULDNT_READ: &str = "A QR code could not be scanned. Please enter their words: ";
static QRCODE_TIMEOUT: LazyLock<u64> = LazyLock::new(|| {
std::env::var("KEYFORK_QRCODE_TIMEOUT")
.ok()
.and_then(|t| u64::from_str(&t).ok())
.unwrap_or(60)
});
/// Establish ECDH transport for remote operators, receive transport-encrypted shares, decrypt the /// Establish ECDH transport for remote operators, receive transport-encrypted shares, decrypt the
/// shares, and combine them. /// shares, and combine them.
/// ///
@ -382,10 +534,7 @@ pub(crate) const HUNK_OFFSET: usize = 2;
/// The function may panic if it is given payloads generated using a version of Keyfork that is /// The function may panic if it is given payloads generated using a version of Keyfork that is
/// incompatible with the currently running version. /// incompatible with the currently running version.
#[allow(clippy::too_many_lines)] #[allow(clippy::too_many_lines)]
pub fn remote_decrypt( pub fn remote_decrypt(w: &mut impl Write) -> Result<(), Box<dyn std::error::Error>> {
w: &mut impl Write,
transfer: &mut dyn Transfer,
) -> Result<(), Box<dyn std::error::Error>> {
let mut pm = keyfork_prompt::default_handler()?; let mut pm = keyfork_prompt::default_handler()?;
let mut iter_count = None; let mut iter_count = None;
@ -397,12 +546,104 @@ pub fn remote_decrypt(
while iter_count.is_none() || iter_count.is_some_and(|i| i > 0) { while iter_count.is_none() || iter_count.is_some_and(|i| i > 0) {
iter += 1; iter += 1;
let our_key = EphemeralSecret::random(); let our_key = EphemeralSecret::random();
let key_mnemonic = Mnemonic::try_from_slice(PublicKey::from(&our_key).as_bytes())?;
let (pubkey, payload) = transfer.exchange_pubkey_for_encrypted_payload( #[cfg(feature = "qrcode")]
&mut *pm, {
PublicKey::from(&our_key).to_bytes(), use keyfork_qrcode::{qrencode, ErrorCorrection};
iter, let qrcode_data = key_mnemonic.to_bytes();
)?; if let Ok(qrcode) = qrencode(
&BASE64_STANDARD.encode(qrcode_data),
ErrorCorrection::Highest,
) {
let checksum = calculate_checksum(key_mnemonic.as_bytes());
let small_sum = &checksum[..8];
let small_mnemonic = Mnemonic::from_raw_bytes(small_sum);
pm.prompt_message(PromptMessage::Text(format!(
concat!(
"QR code #{iter} will be displayed after this prompt. ",
"Send the QR code to the next shardholder. ",
"Only the next shardholder should scan the QR code. ",
),
iter = iter,
)))?;
pm.prompt_message(PromptMessage::Data(qrcode))?;
pm.prompt_message(PromptMessage::Text(format!(
"The following should be sent to verify the QR code: {small_mnemonic}"
)))?;
}
}
pm.prompt_message(PromptMessage::Text(format!(
concat!(
"Upon request, these words should be sent to the shardholder: ",
"{key_mnemonic}"
),
key_mnemonic = key_mnemonic,
)))?;
let mut pubkey_data: Option<[u8; 32]> = None;
let mut payload_data = None;
#[cfg(feature = "qrcode")]
{
pm.prompt_message(PromptMessage::Text(QRCODE_PROMPT.to_string()))?;
loop {
if let Ok(Some(qrcode_content)) =
keyfork_qrcode::scan_camera(std::time::Duration::from_secs(*QRCODE_TIMEOUT), 0)
{
let decoded_data = BASE64_STANDARD
.decode(qrcode_content)
.expect(bug!("qrcode should contain base64 encoded data"));
assert_eq!(
decoded_data.len(),
// Include length of public key
ENCRYPTED_LENGTH as usize + 32,
bug!("invalid payload data")
);
let _ =
pubkey_data.insert(decoded_data[..32].try_into().map_err(|_| InvalidData)?);
let _ = payload_data.insert(decoded_data[32..].to_vec());
break;
} else {
let choice = keyfork_prompt::prompt_choice(
&mut *pm,
"A QR code could not be scanned. Retry or continue?",
&[RetryScanMnemonic::Retry, RetryScanMnemonic::Continue],
)?;
if choice == RetryScanMnemonic::Continue {
break;
}
}
}
}
let (pubkey, payload) = if let Some((pubkey, payload)) = pubkey_data.zip(payload_data) {
(pubkey, payload)
} else {
let validator = MnemonicSetValidator {
word_lengths: [24, 39],
};
let [pubkey_mnemonic, payload_mnemonic] = prompt_validated_wordlist::<English, _>(
&mut *pm,
QRCODE_COULDNT_READ,
3,
&*validator.to_fn(),
)?;
let pubkey = pubkey_mnemonic
.as_bytes()
.try_into()
.map_err(|_| InvalidData)?;
let payload = payload_mnemonic.to_bytes();
(pubkey, payload)
};
assert_eq!(
payload.len(),
ENCRYPTED_LENGTH as usize,
bug!("invalid payload data")
);
let shared_secret = our_key.diffie_hellman(&PublicKey::from(pubkey)); let shared_secret = our_key.diffie_hellman(&PublicKey::from(pubkey));
assert!( assert!(

View File

@ -1,113 +0,0 @@
//! Transfer shards between computers.
use keyfork_prompt::PromptHandler;
mod prompt;
// mod enclave;
pub use prompt::PromptTransfer;
// pub use enclave::EnclaveTransfer;
pub(crate) type Result<T, E = Box<dyn std::error::Error>> = std::result::Result<T, E>;
/// An interface for facilitating the transfer of shards between systems.
///
/// The transfer system should be the same on each side of the operation.
pub trait Transfer {
/// Send a public key to a Shardholder.
///
/// For human transfer, this could display the public key in a human-compatible mechanism.
/// For automatic transfer, this could wait for a client to connect to a server. Once the
/// client has connected and authenticated the server, the server can send a public key.
///
/// # Errors
///
/// The method may return an error if the transfer was, for some reason, unable to finish.
fn send_pubkey(
&mut self,
prompt_handler: &mut dyn PromptHandler,
pubkey: [u8; 32],
pubkey_index: u8,
) -> Result<()>;
/// As a shardholder, receive a public key.
///
/// For human transfer, the shardholder should authenticate the reconstitution operator in a
/// channel that is not necessarily spy-proof (i.e. the public key can be _seen_), but must be
/// tamper-proof (i.e. the public key can't be _modified_). A public communication channel with
/// authenticated messages would be permissible.
///
/// For automated transfer, the system should authenticate the remote system and establish a
/// channel under the previously-discussed conditions. A TLS connection satisfies these
/// requirements. Ideally, attestation of the remote server should be accomplished once the
/// channel has been created.
///
/// Once the channel has been established, the public key can be received.
///
/// # Errors
///
/// The method may return an error if the transfer was, for some reason, unable to finish.
fn receive_pubkey(&mut self, prompt_handler: &mut dyn PromptHandler) -> Result<[u8; 32]>;
/// As a shardholder, send the encrypted shard payload.
///
/// # Errors
///
/// The method may return an error if the transfer was, for some reason, unable to finish.
fn send_encrypted_payload(
&mut self,
prompt_handler: &mut dyn PromptHandler,
pubkey: [u8; 32],
payload: [u8; super::ENCRYPTED_LENGTH as usize],
) -> Result<()>;
/// Receive an encrypted shard payload.
///
/// This method should be invoked directly after [`Transfer::send_pubkey`], as the payload will
/// be decrypted using the private key that is associated with the previously-send public key.
///
/// # Errors
///
/// The method may return an error if the transfer was, for some reason, unable to finish.
fn receive_encrypted_payload(
&mut self,
prompt_handler: &mut dyn PromptHandler,
) -> Result<([u8; 32], [u8; super::ENCRYPTED_LENGTH as usize])>;
/// Utility function to send a pubkey and receive a payload, as one operation follows the
/// other.
///
/// # Errors
///
/// The method may return an error if the transfer was, for some reason, unable to finish.
fn exchange_pubkey_for_encrypted_payload(
&mut self,
prompt_handler: &mut dyn PromptHandler,
pubkey: [u8; 32],
pubkey_index: u8,
) -> Result<([u8; 32], [u8; super::ENCRYPTED_LENGTH as usize])> {
self.send_pubkey(prompt_handler, pubkey, pubkey_index)?;
self.receive_encrypted_payload(prompt_handler)
}
}
/// An error occurred in the process of loading a default transfer mechanism.
#[derive(thiserror::Error, Debug)]
pub enum DefaultTransferError {
}
/// Get a Transfer mechanism that is most suitable for the given environment.
///
/// The following handlers will be used based on the `KEYFORK_TRANSFER_TYPE` variable:
/// * `KEYFORK_TRANSFER_TYPE=prompt`: [`PromptTransfer`]
///
/// Otherwise, the following heuristics are followed:
/// * default: [`PromptTransfer`]
///
/// # Errors
///
/// The function will return an error if a specific transfer mechanism was requested but could not
/// be constructed.
pub fn default_transfer() -> Result<Box<dyn Transfer>, DefaultTransferError> {
Ok(Box::new(PromptTransfer))
}

View File

@ -1,292 +0,0 @@
use std::{str::FromStr, sync::LazyLock};
use keyfork_bug::bug;
use keyfork_mnemonic::{English, Mnemonic};
use keyfork_prompt::{
prompt_validated_wordlist,
validators::{
mnemonic::{MnemonicSetValidator, MnemonicValidator, WordLength},
Validator,
},
Message as PromptMessage, PromptHandler, YesNo,
};
use sha2::{Digest, Sha256};
// NOTE: Base64 is only used as the transport for QR codes.
#[cfg(feature = "qrcode")]
use base64::prelude::{Engine, BASE64_STANDARD};
use super::Result;
#[derive(PartialEq, Eq, Clone, Copy)]
enum RetryScanMnemonic {
Retry,
Continue,
}
impl keyfork_prompt::Choice for RetryScanMnemonic {
fn identifier(&self) -> Option<char> {
Some(match self {
RetryScanMnemonic::Retry => 'r',
RetryScanMnemonic::Continue => 'c',
})
}
}
impl std::fmt::Display for RetryScanMnemonic {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
RetryScanMnemonic::Retry => write!(f, "Retry scanning mnemonic."),
RetryScanMnemonic::Continue => write!(f, "Continue to manual mnemonic entry."),
}
}
}
fn calculate_checksum(slice: &[u8]) -> Vec<u8> {
// generate a verification checksum
// this checksum should be expensive to calculate
let mut payload = vec![];
for _ in 0..1_000_000 {
payload.extend(slice);
let mut hasher = Sha256::new();
hasher.update(&payload);
let result = hasher.finalize();
payload.clear();
payload.extend(result);
}
payload
}
const QRCODE_PROMPT: &str = "Press enter, then present QR code to camera.";
const QRCODE_COULDNT_READ: &str = "A QR code could not be scanned. Please enter their words: ";
static QRCODE_TIMEOUT: LazyLock<u64> = LazyLock::new(|| {
std::env::var("KEYFORK_QRCODE_TIMEOUT")
.ok()
.and_then(|t| u64::from_str(&t).ok())
.unwrap_or(60)
});
/// The mnemonic or QR code used to transport an encrypted shard did not store the correct amount
/// of data.
#[derive(thiserror::Error, Debug)]
#[error("Mnemonic or QR code did not store enough data")]
pub struct InvalidData;
/// A transfer mechanism based on prompts being sent to and from operators.
pub struct PromptTransfer;
impl super::Transfer for PromptTransfer {
fn send_pubkey(
&mut self,
prompt_handler: &mut dyn PromptHandler,
pubkey: [u8; 32],
pubkey_index: u8,
) -> Result<()> {
#[cfg(feature = "qrcode")]
{
use keyfork_qrcode::{qrencode, ErrorCorrection};
if let Ok(qrcode) = qrencode(&BASE64_STANDARD.encode(pubkey), ErrorCorrection::Highest)
{
let checksum = calculate_checksum(&pubkey);
let small_sum: [u8; 8] = checksum[..8].try_into().expect(bug!(
"Mnemonic {checksum:?} must have at least 8 bytes",
checksum = checksum
));
let small_mnemonic = Mnemonic::from_array(small_sum);
prompt_handler.prompt_message(PromptMessage::Text(format!(
concat!(
"QR code #{iter} will be displayed after this prompt. ",
"Send the QR code to the next shardholder. ",
"Only the next shardholder should scan the QR code.",
),
iter = pubkey_index
)))?;
prompt_handler.prompt_message(PromptMessage::Data(qrcode))?;
prompt_handler.prompt_message(PromptMessage::Text(format!(
"The following should be sent to verify the QR code: {small_mnemonic}"
)))?;
}
}
let mnemonic = Mnemonic::from_array(pubkey);
prompt_handler.prompt_message(PromptMessage::Text(format!(
"Upon request, these words should be sent to the shardholder: {mnemonic}"
)))?;
Ok(())
}
fn receive_pubkey(&mut self, prompt_handler: &mut dyn PromptHandler) -> Result<[u8; 32]> {
let mut pubkey_data: Option<[u8; 32]> = None;
#[cfg(feature = "qrcode")]
{
prompt_handler.prompt_message(PromptMessage::Text(QRCODE_PROMPT.to_string()))?;
loop {
if let Ok(Some(qrcode_content)) =
keyfork_qrcode::scan_camera(std::time::Duration::from_secs(*QRCODE_TIMEOUT), 0)
{
let decoded_data = BASE64_STANDARD
.decode(qrcode_content)
.expect(bug!("qrcode should contain base64 encoded data"));
let data: [u8; 32] = decoded_data.try_into().map_err(|_| InvalidData)?;
let checksum = calculate_checksum(&data);
let small_sum = &checksum[..8];
let small_mnemonic = Mnemonic::from_raw_bytes(small_sum);
let question =
format!("Do these words match the expected words? {small_mnemonic}");
let response = keyfork_prompt::prompt_choice(
&mut *prompt_handler,
&question,
&[YesNo::No, YesNo::Yes],
)?;
if response == YesNo::No {
prompt_handler.prompt_message(PromptMessage::Text(String::from(
"Could not establish secure channel, exiting.",
)))?;
std::process::exit(1);
}
pubkey_data = Some(data);
break;
} else {
let choice = keyfork_prompt::prompt_choice(
&mut *prompt_handler,
"A QR code could not be scanned. Retry or continue?",
&[RetryScanMnemonic::Retry, RetryScanMnemonic::Continue],
)?;
if choice == RetryScanMnemonic::Continue {
break;
}
}
}
}
let their_pubkey = if let Some(pubkey) = pubkey_data {
pubkey
} else {
let validator = MnemonicValidator {
word_length: Some(WordLength::Count(24)),
};
prompt_validated_wordlist::<English, _>(
&mut *prompt_handler,
QRCODE_COULDNT_READ,
3,
&*validator.to_fn(),
)?
.as_bytes()
.try_into()
.map_err(|_| InvalidData)?
};
Ok(their_pubkey)
}
fn send_encrypted_payload(
&mut self,
prompt_handler: &mut dyn PromptHandler,
pubkey: [u8; 32],
payload: [u8; crate::ENCRYPTED_LENGTH as usize],
) -> Result<()> {
#[cfg(feature = "qrcode")]
{
use keyfork_qrcode::{qrencode, ErrorCorrection};
let mut qrcode_data = pubkey.to_vec();
qrcode_data.extend(payload);
if let Ok(qrcode) = qrencode(
&BASE64_STANDARD.encode(qrcode_data),
ErrorCorrection::Highest,
) {
prompt_handler.prompt_message(PromptMessage::Text(
concat!(
"A QR code will be displayed after this prompt. ",
"Send the QR code back to the operator combining the shards. ",
"Nobody else should scan this QR code."
)
.to_string(),
))?;
prompt_handler.prompt_message(PromptMessage::Data(qrcode))?;
}
}
let pubkey_mnemonic = Mnemonic::from_array(pubkey);
let payload_mnemonic = Mnemonic::from_array(payload);
prompt_handler.prompt_message(PromptMessage::Text(format!(
"Upon request, these words should be sent: {pubkey_mnemonic} {payload_mnemonic}"
)))?;
Ok(())
}
fn receive_encrypted_payload(
&mut self,
prompt_handler: &mut dyn PromptHandler,
) -> Result<([u8; 32], [u8; crate::ENCRYPTED_LENGTH as usize])> {
let mut pubkey_data: Option<[u8; 32]> = None;
let mut payload_data = None;
#[cfg(feature = "qrcode")]
{
prompt_handler.prompt_message(PromptMessage::Text(QRCODE_PROMPT.to_string()))?;
loop {
if let Ok(Some(qrcode_content)) =
keyfork_qrcode::scan_camera(std::time::Duration::from_secs(*QRCODE_TIMEOUT), 0)
{
let decoded_data = BASE64_STANDARD
.decode(qrcode_content)
.expect(bug!("qrcode should contain base64 encoded data"));
assert_eq!(
decoded_data.len(),
// Include length of public key
crate::ENCRYPTED_LENGTH as usize + 32,
bug!("invalid payload data")
);
let _ =
pubkey_data.insert(decoded_data[..32].try_into().map_err(|_| InvalidData)?);
let _ = payload_data.insert(decoded_data[32..].to_vec());
break;
} else {
let choice = keyfork_prompt::prompt_choice(
&mut *prompt_handler,
"A QR code could not be scanned. Retry or continue?",
&[RetryScanMnemonic::Retry, RetryScanMnemonic::Continue],
)?;
if choice == RetryScanMnemonic::Continue {
break;
}
}
}
}
let (pubkey, payload) = if let Some((pubkey, payload)) = pubkey_data.zip(payload_data) {
(pubkey, payload)
} else {
let validator = MnemonicSetValidator {
word_lengths: [24, 39],
};
let [pubkey_mnemonic, payload_mnemonic] = prompt_validated_wordlist::<English, _>(
&mut *prompt_handler,
QRCODE_COULDNT_READ,
3,
&*validator.to_fn(),
)?;
let pubkey = pubkey_mnemonic
.as_bytes()
.try_into()
.map_err(|_| InvalidData)?;
let payload = payload_mnemonic.to_bytes();
(pubkey, payload)
};
let payload: [u8; crate::ENCRYPTED_LENGTH as usize] =
payload.try_into().map_err(|_| InvalidData)?;
Ok((pubkey, payload))
}
}

View File

@ -10,7 +10,7 @@ workspace = true
[features] [features]
default = [ default = [
"completion", "completion",
"qrcode-decode-backend-zbar", "qrcode-decode-backend-rqrr",
"sequoia-crypto-backend-nettle", "sequoia-crypto-backend-nettle",
] ]

View File

@ -70,7 +70,7 @@ where
/// A mapping between keys and values. /// A mapping between keys and values.
pub values: HashMap<String, String>, pub values: HashMap<String, String>,
/// The first variable for the argument, such as a [`std::path::PathBuf`]. /// The first variable for the argument, such as a [`PathBuf`].
pub inner: T, pub inner: T,
} }

View File

@ -22,6 +22,7 @@ type Result<T, E = Box<dyn std::error::Error>> = std::result::Result<T, E>;
pub trait Deriver { pub trait Deriver {
type Prv: PrivateKey + Clone; type Prv: PrivateKey + Clone;
const DERIVATION_ALGORITHM: DerivationAlgorithm;
fn derivation_path(&self) -> DerivationPath; fn derivation_path(&self) -> DerivationPath;
@ -206,6 +207,7 @@ impl OpenPGP {
impl Deriver for OpenPGP { impl Deriver for OpenPGP {
type Prv = keyfork_derive_openpgp::XPrvKey; type Prv = keyfork_derive_openpgp::XPrvKey;
const DERIVATION_ALGORITHM: DerivationAlgorithm = DerivationAlgorithm::Ed25519;
fn derivation_path(&self) -> DerivationPath { fn derivation_path(&self) -> DerivationPath {
self.derivation_path.derivation_path() self.derivation_path.derivation_path()
@ -245,6 +247,7 @@ impl Deriver for OpenPGP {
impl Deriver for Key { impl Deriver for Key {
// HACK: We're abusing that we use the same key as OpenPGP. Maybe we should use ed25519_dalek. // HACK: We're abusing that we use the same key as OpenPGP. Maybe we should use ed25519_dalek.
type Prv = keyfork_derive_openpgp::XPrvKey; type Prv = keyfork_derive_openpgp::XPrvKey;
const DERIVATION_ALGORITHM: DerivationAlgorithm = DerivationAlgorithm::Ed25519;
fn derivation_path(&self) -> DerivationPath { fn derivation_path(&self) -> DerivationPath {
DerivationPath::default().chain_push(self.slug.0.clone()) DerivationPath::default().chain_push(self.slug.0.clone())

View File

@ -14,7 +14,7 @@ use keyfork_prompt::{
Validator, Validator,
}, },
}; };
use keyfork_shard::{remote_decrypt, Format, default_transfer}; use keyfork_shard::{remote_decrypt, Format};
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>;
@ -60,10 +60,7 @@ impl RecoverSubcommands {
} }
RecoverSubcommands::RemoteShard {} => { RecoverSubcommands::RemoteShard {} => {
let mut seed = vec![]; let mut seed = vec![];
remote_decrypt(&mut seed)?;
let mut transfer = default_transfer()?;
remote_decrypt(&mut seed, &mut *transfer)?;
Ok(seed) Ok(seed)
} }
RecoverSubcommands::Mnemonic {} => { RecoverSubcommands::Mnemonic {} => {

View File

@ -1,7 +1,7 @@
use super::Keyfork; use super::Keyfork;
use clap::{builder::PossibleValue, Parser, Subcommand, ValueEnum}; use clap::{builder::PossibleValue, Parser, Subcommand, ValueEnum};
use keyfork_prompt::default_handler; use keyfork_prompt::default_handler;
use keyfork_shard::{Format as _, default_transfer}; use keyfork_shard::Format as _;
use std::{ use std::{
io::{stdin, stdout, Read, Write}, io::{stdin, stdout, Read, Write},
path::{Path, PathBuf}, path::{Path, PathBuf},
@ -97,8 +97,7 @@ impl ShardExec for OpenPGP {
) -> Result<(), Box<dyn std::error::Error>> { ) -> Result<(), Box<dyn std::error::Error>> {
let openpgp = keyfork_shard::openpgp::OpenPGP; let openpgp = keyfork_shard::openpgp::OpenPGP;
let prompt = default_handler()?; let prompt = default_handler()?;
let mut transfer = default_transfer()?; openpgp.decrypt_one_shard_for_transport(key_discovery, input, prompt)?;
openpgp.decrypt_one_shard_for_transport(key_discovery, input, prompt, &mut *transfer)?;
Ok(()) Ok(())
} }
@ -267,8 +266,7 @@ impl ShardSubcommands {
} }
ShardSubcommands::RemoteCombine => { ShardSubcommands::RemoteCombine => {
let mut output = vec![]; let mut output = vec![];
let mut transfer = default_transfer()?; keyfork_shard::remote_decrypt(&mut output)?;
keyfork_shard::remote_decrypt(&mut output, &mut *transfer)?;
println!("{}", smex::encode(output)); println!("{}", smex::encode(output));
Ok(()) Ok(())
} }

View File

@ -7,55 +7,50 @@ use openpgp::{
types::KeyFlags, types::KeyFlags,
Cert, Cert,
}; };
use keyforkd::test_util::{run_test, Panicable};
const KEYFORK_BIN: &str = "keyfork"; const KEYFORK_BIN: &str = "keyfork";
#[test] #[test]
fn test() { fn test() {
run_test(b"AAAA", |_| { let policy = StandardPolicy::new();
let policy = StandardPolicy::new();
let command_output = Command::cargo_bin(KEYFORK_BIN)
.unwrap()
.args([
"derive",
"openpgp",
"--to-stdout",
"Ryan Heywood (RyanSquared) <ryan@distrust.co>",
])
.assert()
.success();
let packets = PacketParser::from_bytes(&command_output.get_output().stdout).unwrap(); let command_output = Command::cargo_bin(KEYFORK_BIN)
let cert = Cert::try_from(packets).unwrap(); .unwrap()
.args([
"derive",
"openpgp",
"Ryan Heywood (RyanSquared) <ryan@distrust.co>",
])
.assert()
.success();
// assert the cert contains _any_ secret key data let packets = PacketParser::from_bytes(&command_output.get_output().stdout).unwrap();
let cert = Cert::try_from(packets).unwrap();
// assert the cert contains _any_ secret key data
assert!(
cert.is_tsk(),
"exported key should contain secret key data, indicated by the key being a TSK"
);
// assert the correct keys were added in the correct order
let mut key_formats = std::collections::HashSet::from([
KeyFlags::empty().set_certification(),
KeyFlags::empty().set_signing(),
KeyFlags::empty()
.set_transport_encryption()
.set_storage_encryption(),
KeyFlags::empty().set_authentication(),
]);
let valid_cert = cert.with_policy(&policy, None).unwrap();
for key in valid_cert.keys() {
let flags = key.key_flags().unwrap();
assert!( assert!(
cert.is_tsk(), key_formats.remove(&flags),
"exported key should contain secret key data, indicated by the key being a TSK" "could not find key flag set: {flags:?}"
); );
key.alive().expect("is live after being generated");
// assert the correct keys were added in the correct order key.parts_into_secret().expect("has secret keys");
let mut key_formats = std::collections::HashSet::from([ }
KeyFlags::empty().set_certification(), assert!(key_formats.is_empty(), "remaining key formats: {key_formats:?}");
KeyFlags::empty().set_signing(),
KeyFlags::empty()
.set_transport_encryption()
.set_storage_encryption(),
KeyFlags::empty().set_authentication(),
]);
let valid_cert = cert.with_policy(&policy, None).unwrap();
for key in valid_cert.keys() {
let flags = key.key_flags().unwrap();
assert!(
key_formats.remove(&flags),
"could not find key flag set: {flags:?}"
);
key.alive().expect("is live after being generated");
key.parts_into_secret().expect("has secret keys");
}
assert!(key_formats.is_empty(), "remaining key formats: {key_formats:?}");
Panicable::Ok(())
}).unwrap();
} }

View File

@ -55,13 +55,13 @@ impl TerminalIoctl {
fn get_termios(&self) -> Result<Termios> { fn get_termios(&self) -> Result<Termios> {
let mut termios = unsafe { std::mem::zeroed() }; let mut termios = unsafe { std::mem::zeroed() };
assert_io(unsafe { libc::tcgetattr(self.fd, &raw mut termios) })?; assert_io(unsafe { libc::tcgetattr(self.fd, &mut termios) })?;
Ok(termios) Ok(termios)
} }
/// Enable raw mode for the given terminal. /// Enable raw mode for the given terminal.
/// ///
/// Replaces: `crossterm::terminal::enable_raw_mode`. /// Replaces: [`crossterm::terminal::enable_raw_mode`].
/// ///
/// # Errors /// # Errors
/// ///
@ -71,8 +71,8 @@ impl TerminalIoctl {
let mut termios = self.get_termios()?; let mut termios = self.get_termios()?;
let original_mode_ios = termios; let original_mode_ios = termios;
unsafe { libc::cfmakeraw(&raw mut termios) }; unsafe { libc::cfmakeraw(&mut termios) };
assert_io(unsafe { libc::tcsetattr(self.fd, libc::TCSANOW, &raw const termios) })?; assert_io(unsafe { libc::tcsetattr(self.fd, libc::TCSANOW, &termios) })?;
self.stored_termios = Some(original_mode_ios); self.stored_termios = Some(original_mode_ios);
} }
Ok(()) Ok(())
@ -80,21 +80,21 @@ impl TerminalIoctl {
/// Disable raw mode for the given terminal. /// Disable raw mode for the given terminal.
/// ///
/// Replaces: `crossterm::terminal::disable_raw_mode`. /// Replaces: [`crossterm::terminal::disable_raw_mode`].
/// ///
/// # Errors /// # Errors
/// ///
/// The method may propagate errors encountered when interacting with the terminal. /// The method may propagate errors encountered when interacting with the terminal.
pub fn disable_raw_mode(&mut self) -> Result<()> { pub fn disable_raw_mode(&mut self) -> Result<()> {
if let Some(termios) = self.stored_termios.take() { if let Some(termios) = self.stored_termios.take() {
assert_io(unsafe { libc::tcsetattr(self.fd, libc::TCSANOW, &raw const termios) })?; assert_io(unsafe { libc::tcsetattr(self.fd, libc::TCSANOW, &termios) })?;
} }
Ok(()) Ok(())
} }
/// Return the size for the given terminal. /// Return the size for the given terminal.
/// ///
/// Replaces: `crossterm::terminal::size`. /// Replaces: [`crossterm::terminal::size`].
/// ///
/// # Errors /// # Errors
/// ///
@ -107,7 +107,7 @@ impl TerminalIoctl {
ws_ypixel: 0, ws_ypixel: 0,
}; };
assert_io(unsafe { libc::ioctl(self.fd, libc::TIOCGWINSZ, &raw mut size) })?; assert_io(unsafe { libc::ioctl(self.fd, libc::TIOCGWINSZ, &mut size) })?;
Ok((size.ws_col, size.ws_row)) Ok((size.ws_col, size.ws_row))
} }
} }