From 6c25cb8f31886697640eeb6b629c28aa5e2d6678 Mon Sep 17 00:00:00 2001 From: ryan Date: Fri, 22 Dec 2023 14:39:25 -0500 Subject: [PATCH] keyfork-prompt: fixup passphrase handling, add prompt_wordlist --- Cargo.lock | 1 + keyfork-prompt/Cargo.toml | 5 + keyfork-prompt/src/bin/test-basic-prompt.rs | 10 +- keyfork-prompt/src/lib.rs | 122 +++++++++++++++++++- 4 files changed, 130 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ddf3a12..cb97533 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1078,6 +1078,7 @@ name = "keyfork-prompt" version = "0.1.0" dependencies = [ "crossterm", + "keyfork-mnemonic-util", "thiserror", ] diff --git a/keyfork-prompt/Cargo.toml b/keyfork-prompt/Cargo.toml index bb542e3..4ab511d 100644 --- a/keyfork-prompt/Cargo.toml +++ b/keyfork-prompt/Cargo.toml @@ -5,6 +5,11 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[features] +default = ["mnemonic"] +mnemonic = ["keyfork-mnemonic-util"] + [dependencies] crossterm = { version = "0.27.0", default-features = false, features = ["use-dev-tty", "events"] } +keyfork-mnemonic-util = { version = "0.1.0", path = "../keyfork-mnemonic-util", optional = true } thiserror = "1.0.51" diff --git a/keyfork-prompt/src/bin/test-basic-prompt.rs b/keyfork-prompt/src/bin/test-basic-prompt.rs index 5361622..c14f671 100644 --- a/keyfork-prompt/src/bin/test-basic-prompt.rs +++ b/keyfork-prompt/src/bin/test-basic-prompt.rs @@ -1,12 +1,14 @@ -use std::io::{stdin, stdout}; +use std::{io::{stdin, stdout}, str::FromStr}; use keyfork_prompt::*; +use keyfork_mnemonic_util::Mnemonic; pub fn main() -> Result<()> { let mut mgr = PromptManager::new(stdin(), stdout())?; - mgr.prompt_input("Mnemonic: ")?; - mgr.prompt_message("Please press enter.")?; mgr.prompt_passphrase("Passphrase: ")?; - mgr.prompt_message("Please press space bar.")?; + let string = mgr.prompt_wordlist("Mnemonic: ", &Default::default())?; + let mnemonic = Mnemonic::from_str(&string).unwrap(); + let entropy = mnemonic.entropy(); + mgr.prompt_message(&format!("Your entropy is: {entropy:X?}"))?; Ok(()) } diff --git a/keyfork-prompt/src/lib.rs b/keyfork-prompt/src/lib.rs index c221aa9..4669cc6 100644 --- a/keyfork-prompt/src/lib.rs +++ b/keyfork-prompt/src/lib.rs @@ -3,6 +3,9 @@ use std::{ os::fd::AsRawFd, }; +#[cfg(feature = "mnemonic")] +use keyfork_mnemonic_util::Wordlist; + use crossterm::{ cursor, event::{read, Event, KeyCode}, @@ -69,8 +72,8 @@ where Ok(line) } - // TODO: return secrecy::Secret - pub fn prompt_passphrase(&mut self, prompt: &str) -> Result { + #[cfg(feature = "mnemonic")] + pub fn prompt_wordlist(&mut self, prompt: &str, wordlist: &Wordlist) -> Result { let mut terminal = AlternateScreen::new(&mut self.write)?; let mut terminal = RawMode::new(&mut terminal)?; @@ -78,7 +81,9 @@ where .queue(terminal::Clear(terminal::ClearType::All))? .queue(cursor::MoveTo(0, 0))?; let mut lines = prompt.lines().peekable(); + let mut prefix_length = 0; while let Some(line) = lines.next() { + prefix_length = line.len(); terminal.queue(Print(line))?; if lines.peek().is_some() { terminal @@ -88,16 +93,123 @@ where } terminal.flush()?; + let (mut cols, mut _rows) = terminal::size()?; + let mut input = String::new(); + + loop { + match read()? { + Event::Resize(new_cols, new_rows) => { + cols = new_cols; + _rows = new_rows; + } + Event::Key(k) => match k.code { + KeyCode::Enter => { + input.push('\n'); + break; + } + KeyCode::Backspace => { + input.pop(); + } + KeyCode::Char(' ') => { + if !input.chars().rev().next().is_some_and(char::is_whitespace) { + input.push(' '); + } + } + KeyCode::Char(c) => { + input.push(c); + } + _ => (), + }, + _ => (), + } + + let usable_space = cols as usize - prefix_length - 1; + + terminal + .queue(cursor::MoveToColumn(prefix_length as u16))? + .queue(terminal::Clear(terminal::ClearType::UntilNewLine))? + .flush()?; + + let printable_input_start = if input.len() > usable_space { + let start_index = input.len() - usable_space; + input + .chars() + .enumerate() + .skip(start_index) + .skip_while(|(_, ch)| !ch.is_whitespace()) + .next() + .expect("any printable character") + .0 + } else { + 0 + }; + + let printable_input = &input[printable_input_start..]; + let mut iter = printable_input.split_whitespace().peekable(); + + while let Some(word) = iter.next() { + if wordlist.contains(word) { + terminal.queue(PrintStyledContent(word.green()))?; + } else { + terminal.queue(PrintStyledContent(word.red()))?; + } + if iter.peek().is_some() + || printable_input + .chars() + .rev() + .next() + .is_some_and(char::is_whitespace) + { + terminal.queue(Print(" "))?; + } + } + + terminal.flush()?; + } + + Ok(input) + } + + // TODO: return secrecy::Secret + pub fn prompt_passphrase(&mut self, prompt: &str) -> Result { + let mut terminal = AlternateScreen::new(&mut self.write)?; + let mut terminal = RawMode::new(&mut terminal)?; + + terminal + .queue(terminal::Clear(terminal::ClearType::All))? + .queue(cursor::MoveTo(0, 0))?; + let mut lines = prompt.lines().peekable(); + let mut prefix_length = 0; + while let Some(line) = lines.next() { + prefix_length = line.len(); + terminal.queue(Print(line))?; + if lines.peek().is_some() { + terminal + .queue(cursor::MoveDown(1))? + .queue(cursor::MoveToColumn(0))?; + } + } + terminal.flush()?; + + let (mut cols, mut _rows) = terminal::size()?; + let mut passphrase = String::new(); loop { match read()? { + Event::Resize(new_cols, new_rows) => { + cols = new_cols; + _rows = new_rows; + } Event::Key(k) => match k.code { KeyCode::Enter => { passphrase.push('\n'); break; } KeyCode::Backspace => { - if passphrase.pop().is_some() { + let passphrase_len = passphrase.len(); + if passphrase.pop().is_some() + && prefix_length + passphrase_len < cols as usize + { terminal .queue(cursor::MoveLeft(1))? .queue(Print(" "))? @@ -106,7 +218,9 @@ where } } KeyCode::Char(c) => { - terminal.queue(Print("*"))?.flush()?; + if prefix_length + passphrase.len() < (cols - 1) as usize { + terminal.queue(Print("*"))?.flush()?; + } passphrase.push(c); } _ => (),