diff --git a/crates/keyfork-shard/src/bin/keyfork-shard-decrypt-openpgp.rs b/crates/keyfork-shard/src/bin/keyfork-shard-decrypt-openpgp.rs index d01d163..0261929 100644 --- a/crates/keyfork-shard/src/bin/keyfork-shard-decrypt-openpgp.rs +++ b/crates/keyfork-shard/src/bin/keyfork-shard-decrypt-openpgp.rs @@ -8,7 +8,7 @@ use std::{ }; use keyfork_prompt::default_handler; -use keyfork_shard::{openpgp::OpenPGP, Format}; +use keyfork_shard::{openpgp::OpenPGP, Format, default_transfer}; type Result> = std::result::Result; @@ -34,11 +34,13 @@ fn run() -> Result<()> { let openpgp = OpenPGP; let prompt_handler = default_handler()?; + let mut transfer = default_transfer()?; openpgp.decrypt_one_shard_for_transport( key_discovery.as_deref(), messages_file, prompt_handler, + &mut *transfer, )?; Ok(()) diff --git a/crates/keyfork-shard/src/bin/keyfork-shard-remote.rs b/crates/keyfork-shard/src/bin/keyfork-shard-remote.rs index 6a67372..8b6f73a 100644 --- a/crates/keyfork-shard/src/bin/keyfork-shard-remote.rs +++ b/crates/keyfork-shard/src/bin/keyfork-shard-remote.rs @@ -2,7 +2,7 @@ use std::{env, process::ExitCode}; -use keyfork_shard::remote_decrypt; +use keyfork_shard::{remote_decrypt, default_transfer}; type Result> = std::result::Result; @@ -15,8 +15,10 @@ fn run() -> Result<()> { _ => panic!("Usage: {program_name}"), } + let mut transfer = default_transfer()?; + let mut bytes = vec![]; - remote_decrypt(&mut bytes)?; + remote_decrypt(&mut bytes, &mut *transfer)?; print!("{}", smex::encode(bytes)); Ok(()) diff --git a/crates/keyfork-shard/src/lib.rs b/crates/keyfork-shard/src/lib.rs index 5ebbbde..5c6a681 100644 --- a/crates/keyfork-shard/src/lib.rs +++ b/crates/keyfork-shard/src/lib.rs @@ -3,32 +3,23 @@ use std::{ io::{Read, Write}, rc::Rc, - str::FromStr, - sync::{LazyLock, Mutex}, + sync::Mutex, }; use aes_gcm::{ aead::{consts::U12, Aead}, - Aes256Gcm, KeyInit as _, Nonce, + Aes256Gcm, KeyInit, Nonce, }; -use base64::prelude::{Engine, BASE64_STANDARD}; use blahaj::{Share, Sharks}; use hkdf::Hkdf; use keyfork_bug::{bug, POISONED_MUTEX}; -use keyfork_mnemonic::{English, Mnemonic}; -use keyfork_prompt::{ - prompt_validated_wordlist, - validators::{ - mnemonic::{MnemonicSetValidator, MnemonicValidator, WordLength}, - Validator, - }, - Message as PromptMessage, PromptHandler, YesNo, -}; -// Bug with rust-analyzer: https://codeberg.org/ryan-distrust.co/rust-analyzer-trait-resolver-bug -#[allow(unused_imports)] -use sha2::{Digest, Sha256, digest::KeyInit as _}; +use keyfork_prompt::PromptHandler; +use sha2::Sha256; use x25519_dalek::{EphemeralSecret, PublicKey}; +pub mod transfer; +pub use transfer::{default_transfer, Transfer}; + const PLAINTEXT_LENGTH: u8 = 32 // shard + 1 // index + 1 // threshold @@ -36,45 +27,6 @@ const PLAINTEXT_LENGTH: u8 = 32 // shard + 1; // length; 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 { - 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 { - // 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")] pub mod openpgp; @@ -271,6 +223,7 @@ pub trait Format { private_key_discovery: Option>, reader: impl Read + Send + Sync, prompt: Box, + transfer: &mut dyn Transfer, ) -> Result<(), Box> { let prompt = Rc::new(Mutex::new(prompt)); @@ -281,80 +234,12 @@ pub trait Format { let encrypted_messages = self.parse_shard_file(reader)?; // establish AES-256-GCM key via ECDH - let mut pubkey_data: Option<[u8; 32]> = None; - - // 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::( - &mut **prompt, - QRCODE_COULDNT_READ, - 3, - &*validator.to_fn(), - )? - .as_bytes() - .try_into() - .map_err(|_| InvalidData)? - }; + let their_pubkey = + transfer.receive_pubkey(&mut **prompt.lock().expect(bug!(POISONED_MUTEX)))?; // create our shared key let our_key = EphemeralSecret::random(); - let our_pubkey_mnemonic = Mnemonic::try_from_slice(PublicKey::from(&our_key).as_bytes())?; + let our_pubkey = PublicKey::from(&our_key).to_bytes(); let shared_secret = our_key.diffie_hellman(&PublicKey::from(their_pubkey)); assert!( shared_secret.was_contributory(), @@ -403,44 +288,14 @@ pub trait Format { ENCRYPTED_LENGTH as usize, bug!("encrypted bytes size != expected len"), ); - let mut mnemonic_bytes = [0u8; ENCRYPTED_LENGTH as usize]; - mnemonic_bytes.copy_from_slice(&encrypted_bytes); + let mut payload_bytes = [0u8; ENCRYPTED_LENGTH as usize]; + payload_bytes.copy_from_slice(&encrypted_bytes); - let payload_mnemonic = Mnemonic::from_array(mnemonic_bytes); - - #[cfg(feature = "qrcode")] - { - 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}" - )))?; + transfer.send_encrypted_payload( + &mut **prompt.lock().expect(bug!(POISONED_MUTEX)), + our_pubkey, + payload_bytes, + )?; Ok(()) } @@ -514,15 +369,6 @@ pub struct InvalidData; pub(crate) const HUNK_VERSION: u8 = 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 = 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 /// shares, and combine them. /// @@ -536,7 +382,10 @@ static QRCODE_TIMEOUT: LazyLock = LazyLock::new(|| { /// The function may panic if it is given payloads generated using a version of Keyfork that is /// incompatible with the currently running version. #[allow(clippy::too_many_lines)] -pub fn remote_decrypt(w: &mut impl Write) -> Result<(), Box> { +pub fn remote_decrypt( + w: &mut impl Write, + transfer: &mut dyn Transfer, +) -> Result<(), Box> { let mut pm = keyfork_prompt::default_handler()?; let mut iter_count = None; @@ -548,104 +397,12 @@ pub fn remote_decrypt(w: &mut impl Write) -> Result<(), Box 0) { iter += 1; let our_key = EphemeralSecret::random(); - let key_mnemonic = Mnemonic::try_from_slice(PublicKey::from(&our_key).as_bytes())?; - #[cfg(feature = "qrcode")] - { - use keyfork_qrcode::{qrencode, ErrorCorrection}; - 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::( - &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 (pubkey, payload) = transfer.exchange_pubkey_for_encrypted_payload( + &mut *pm, + PublicKey::from(&our_key).to_bytes(), + iter, + )?; let shared_secret = our_key.diffie_hellman(&PublicKey::from(pubkey)); assert!( diff --git a/crates/keyfork-shard/src/transfer.rs b/crates/keyfork-shard/src/transfer.rs new file mode 100644 index 0000000..d386516 --- /dev/null +++ b/crates/keyfork-shard/src/transfer.rs @@ -0,0 +1,113 @@ +//! Transfer shards between computers. + +use keyfork_prompt::PromptHandler; + +mod prompt; +// mod enclave; + +pub use prompt::PromptTransfer; +// pub use enclave::EnclaveTransfer; + +pub(crate) type Result> = std::result::Result; + +/// 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, DefaultTransferError> { + Ok(Box::new(PromptTransfer)) +} diff --git a/crates/keyfork-shard/src/transfer/prompt.rs b/crates/keyfork-shard/src/transfer/prompt.rs new file mode 100644 index 0000000..e8e4c12 --- /dev/null +++ b/crates/keyfork-shard/src/transfer/prompt.rs @@ -0,0 +1,292 @@ +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 { + 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 { + // 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 = 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::( + &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::( + &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)) + } +} diff --git a/crates/keyfork/src/cli/derive.rs b/crates/keyfork/src/cli/derive.rs index 5376c73..65eb218 100644 --- a/crates/keyfork/src/cli/derive.rs +++ b/crates/keyfork/src/cli/derive.rs @@ -22,7 +22,6 @@ type Result> = std::result::Result; pub trait Deriver { type Prv: PrivateKey + Clone; - const DERIVATION_ALGORITHM: DerivationAlgorithm; fn derivation_path(&self) -> DerivationPath; @@ -207,7 +206,6 @@ impl OpenPGP { impl Deriver for OpenPGP { type Prv = keyfork_derive_openpgp::XPrvKey; - const DERIVATION_ALGORITHM: DerivationAlgorithm = DerivationAlgorithm::Ed25519; fn derivation_path(&self) -> DerivationPath { self.derivation_path.derivation_path() @@ -247,7 +245,6 @@ impl Deriver for OpenPGP { impl Deriver for Key { // HACK: We're abusing that we use the same key as OpenPGP. Maybe we should use ed25519_dalek. type Prv = keyfork_derive_openpgp::XPrvKey; - const DERIVATION_ALGORITHM: DerivationAlgorithm = DerivationAlgorithm::Ed25519; fn derivation_path(&self) -> DerivationPath { DerivationPath::default().chain_push(self.slug.0.clone()) diff --git a/crates/keyfork/src/cli/recover.rs b/crates/keyfork/src/cli/recover.rs index 32e2fbc..b459e0d 100644 --- a/crates/keyfork/src/cli/recover.rs +++ b/crates/keyfork/src/cli/recover.rs @@ -14,7 +14,7 @@ use keyfork_prompt::{ Validator, }, }; -use keyfork_shard::{remote_decrypt, Format}; +use keyfork_shard::{remote_decrypt, Format, default_transfer}; type Result> = std::result::Result; @@ -60,7 +60,10 @@ impl RecoverSubcommands { } RecoverSubcommands::RemoteShard {} => { let mut seed = vec![]; - remote_decrypt(&mut seed)?; + + let mut transfer = default_transfer()?; + remote_decrypt(&mut seed, &mut *transfer)?; + Ok(seed) } RecoverSubcommands::Mnemonic {} => { diff --git a/crates/keyfork/src/cli/shard.rs b/crates/keyfork/src/cli/shard.rs index 0869003..35201d5 100644 --- a/crates/keyfork/src/cli/shard.rs +++ b/crates/keyfork/src/cli/shard.rs @@ -1,7 +1,7 @@ use super::Keyfork; use clap::{builder::PossibleValue, Parser, Subcommand, ValueEnum}; use keyfork_prompt::default_handler; -use keyfork_shard::Format as _; +use keyfork_shard::{Format as _, default_transfer}; use std::{ io::{stdin, stdout, Read, Write}, path::{Path, PathBuf}, @@ -97,7 +97,8 @@ impl ShardExec for OpenPGP { ) -> Result<(), Box> { let openpgp = keyfork_shard::openpgp::OpenPGP; let prompt = default_handler()?; - openpgp.decrypt_one_shard_for_transport(key_discovery, input, prompt)?; + let mut transfer = default_transfer()?; + openpgp.decrypt_one_shard_for_transport(key_discovery, input, prompt, &mut *transfer)?; Ok(()) } @@ -266,7 +267,8 @@ impl ShardSubcommands { } ShardSubcommands::RemoteCombine => { let mut output = vec![]; - keyfork_shard::remote_decrypt(&mut output)?; + let mut transfer = default_transfer()?; + keyfork_shard::remote_decrypt(&mut output, &mut *transfer)?; println!("{}", smex::encode(output)); Ok(()) }