diff --git a/keyfork-prompt/Cargo.toml b/keyfork-prompt/Cargo.toml index b69772e..578823b 100644 --- a/keyfork-prompt/Cargo.toml +++ b/keyfork-prompt/Cargo.toml @@ -6,8 +6,9 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [features] -default = ["mnemonic"] +default = ["mnemonic", "qrencode"] mnemonic = ["keyfork-mnemonic-util"] +qrencode = [] [dependencies] crossterm = { version = "0.27.0", default-features = false, features = ["use-dev-tty", "events", "bracketed-paste"] } diff --git a/keyfork-prompt/src/bin/test-basic-prompt.rs b/keyfork-prompt/src/bin/test-basic-prompt.rs index 26ce910..51f598d 100644 --- a/keyfork-prompt/src/bin/test-basic-prompt.rs +++ b/keyfork-prompt/src/bin/test-basic-prompt.rs @@ -3,12 +3,14 @@ use std::{io::{stdin, stdout}, str::FromStr}; use keyfork_prompt::*; use keyfork_mnemonic_util::Mnemonic; -pub fn main() -> Result<()> { +pub fn main() -> Result<(), Box> { let mut mgr = PromptManager::new(stdin(), stdout())?; mgr.prompt_passphrase("Passphrase: ")?; let string = mgr.prompt_wordlist("Mnemonic: ", &Default::default())?; let mnemonic = Mnemonic::from_str(&string).unwrap(); let entropy = mnemonic.entropy(); mgr.prompt_message(Message::Text(format!("Your entropy is: {entropy:X?}")))?; + let qrcode = qrencode::qrencode(&string)?; + mgr.prompt_message(Message::Data(qrcode))?; Ok(()) } diff --git a/keyfork-prompt/src/lib.rs b/keyfork-prompt/src/lib.rs index ef45874..4b275f4 100644 --- a/keyfork-prompt/src/lib.rs +++ b/keyfork-prompt/src/lib.rs @@ -20,6 +20,9 @@ mod raw_mode; use alternate_screen::*; use raw_mode::*; +#[cfg(feature = "qrencode")] +pub mod qrencode; + #[derive(thiserror::Error, Debug)] pub enum Error { #[error("The given handler is not a TTY")] @@ -261,7 +264,7 @@ where let mut terminal = RawMode::new(&mut terminal)?; loop { - let (cols, _) = terminal::size()?; + let (cols, rows) = terminal::size()?; terminal .queue(terminal::Clear(terminal::ClearType::All))? @@ -289,11 +292,24 @@ where } } Data(data) => { - for line in data.lines() { + let count = data.lines().count(); + // NOTE: GE to allow a MoveDown(1) + if count >= rows as usize { + let msg = format!( + "{} {count} {} {rows} {}", + "Could not print data", "lines long (screen is", "lines long)" + ); terminal - .queue(Print(line))? + .queue(Print(msg))? .queue(cursor::MoveDown(1))? .queue(cursor::MoveToColumn(0))?; + } else { + for line in data.lines() { + terminal + .queue(Print(line))? + .queue(cursor::MoveDown(1))? + .queue(cursor::MoveToColumn(0))?; + } } } } diff --git a/keyfork-prompt/src/qrencode.rs b/keyfork-prompt/src/qrencode.rs new file mode 100644 index 0000000..eedfd9c --- /dev/null +++ b/keyfork-prompt/src/qrencode.rs @@ -0,0 +1,31 @@ +use std::{ + io::Write, + process::{Command, Stdio}, +}; + +#[derive(thiserror::Error, Debug)] +pub enum QrGenerationError { + #[error("{0}")] + Io(#[from] std::io::Error), + + #[error("{0}")] + StringParse(#[from] std::string::FromUtf8Error) +} + +/// Generate a terminal-printable QR code for a given string. Uses the `qrencode` CLI utility. +pub fn qrencode(text: &str) -> Result { + let mut qrencode = Command::new("qrencode") + .arg("-t") + .arg("ansiutf8") + .arg("-m") + .arg("2") + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .spawn()?; + if let Some(stdin) = qrencode.stdin.as_mut() { + stdin.write_all(text.as_bytes())?; + } + let output = qrencode.wait_with_output()?; + let result = String::from_utf8(output.stdout)?; + Ok(result) +} diff --git a/keyfork-shard/src/lib.rs b/keyfork-shard/src/lib.rs index 9cbca98..f23898a 100644 --- a/keyfork-shard/src/lib.rs +++ b/keyfork-shard/src/lib.rs @@ -8,7 +8,7 @@ use aes_gcm::{ Aes256Gcm, KeyInit, }; use keyfork_mnemonic_util::{Mnemonic, Wordlist}; -use keyfork_prompt::{Message as PromptMessage, PromptManager}; +use keyfork_prompt::{qrencode, Message as PromptMessage, PromptManager}; use sharks::{Share, Sharks}; use x25519_dalek::{EphemeralSecret, PublicKey}; @@ -47,10 +47,15 @@ pub fn remote_decrypt() -> Result<(), Box> { let our_key = EphemeralSecret::random(); let key_mnemonic = Mnemonic::from_entropy(PublicKey::from(&our_key).as_bytes(), Default::default())?; + let combined_mnemonic = format!("{nonce_mnemonic} {key_mnemonic}"); pm.prompt_message(PromptMessage::Text(format!( - "Our words: {nonce_mnemonic} {key_mnemonic}" + "Our words: {combined_mnemonic}" )))?; + if let Ok(qrcode) = qrencode::qrencode(&combined_mnemonic) { + pm.prompt_message(PromptMessage::Data(qrcode))?; + } + let their_words = pm.prompt_wordlist("Their words: ", &wordlist)?; let mut pubkey_words = their_words.split_whitespace().take(24).peekable(); diff --git a/keyfork-shard/src/openpgp.rs b/keyfork-shard/src/openpgp.rs index 5446e8f..5aefb82 100644 --- a/keyfork-shard/src/openpgp.rs +++ b/keyfork-shard/src/openpgp.rs @@ -14,7 +14,7 @@ use keyfork_derive_openpgp::derive_util::{ DerivationPath, }; use keyfork_mnemonic_util::{Mnemonic, MnemonicFromStrError, MnemonicGenerationError, Wordlist}; -use keyfork_prompt::{Error as PromptError, Message as PromptMessage, PromptManager}; +use keyfork_prompt::{qrencode, Error as PromptError, Message as PromptMessage, PromptManager}; use openpgp::{ armor::{Kind, Writer}, cert::{Cert, CertParser, ValidCert}, @@ -48,7 +48,7 @@ use smartcard::SmartcardManager; const SHARD_METADATA_VERSION: u8 = 1; const SHARD_METADATA_OFFSET: usize = 2; -use super::{HUNK_VERSION, SharksError}; +use super::{SharksError, HUNK_VERSION}; // 256 bit share is 49 bytes + some amount of hunk bytes, gives us reasonable padding const ENC_LEN: u8 = 4 * 16; @@ -427,7 +427,6 @@ pub fn decrypt( let shared_key = Aes256Gcm::new_from_slice(&shared_secret).expect("Invalid length of constant key size"); let bytes = shared_key.encrypt(their_nonce, share.as_slice())?; - dbg!(bytes.len()); shared_key.decrypt(their_nonce, &bytes[..])?; // NOTE: Padding length is less than u8::MAX because ENC_LEN < u8::MAX @@ -451,11 +450,16 @@ pub fn decrypt( // safety: size of out_bytes is constant and always % 4 == 0 let mnemonic = unsafe { Mnemonic::from_raw_entropy(&out_bytes, Default::default()) }; + let combined_mnemonic = format!("{our_mnemonic} {mnemonic}"); pm.prompt_message(PromptMessage::Text(format!( - "Our words: {our_mnemonic} {mnemonic}" + "Our words: {combined_mnemonic}" )))?; + if let Ok(qrcode) = qrencode::qrencode(&combined_mnemonic) { + pm.prompt_message(PromptMessage::Data(qrcode))?; + } + Ok(()) }