diff --git a/crates/keyfork/src/cli/recover.rs b/crates/keyfork/src/cli/recover.rs index d25960b..d8b6274 100644 --- a/crates/keyfork/src/cli/recover.rs +++ b/crates/keyfork/src/cli/recover.rs @@ -21,9 +21,14 @@ pub enum RecoverSubcommands { /// Combine remotely decrypted shards. The shards should be sent using the command `keyfork /// shard transport`. RemoteShard {}, + + /// Read a mnemonic from input. + Mnemonic {}, } impl RecoverSubcommands { + /// Return the 128-bit or 256-bit entropy for a bip39 mnemonic. This is _not_ the same as the + /// 512-bit seed used by bip32. fn handle(&self) -> Result> { match self { RecoverSubcommands::Shard { @@ -51,6 +56,27 @@ impl RecoverSubcommands { remote_decrypt(&mut seed)?; Ok(seed) } + RecoverSubcommands::Mnemonic {} => { + use keyfork_prompt::{ + default_terminal, + validators::{ + mnemonic::{MnemonicChoiceValidator, WordLength}, + Validator, + }, + PromptHandler, + }; + let mut term = default_terminal()?; + let validator = MnemonicChoiceValidator { + word_lengths: [WordLength::Count(12), WordLength::Count(24)], + }; + let mnemonic = term.prompt_validated_wordlist( + "Mnemonic: ", + &Default::default(), + 3, + validator.to_fn(), + )?; + Ok(mnemonic.entropy()) + } } } } diff --git a/crates/util/keyfork-prompt/src/validators.rs b/crates/util/keyfork-prompt/src/validators.rs index b7b5afe..833c2ae 100644 --- a/crates/util/keyfork-prompt/src/validators.rs +++ b/crates/util/keyfork-prompt/src/validators.rs @@ -142,6 +142,47 @@ pub mod mnemonic { } } + /// A mnemonic of a given choice of lengths. For example, a 128-bit or 256-bit bip32 seed. + #[derive(thiserror::Error, Debug)] + pub enum MnemonicChoiceValidationError { + /// The provided mnemonic did not match any of the valid ranges. + #[error("Invalid word length: {0} was not in any {1:?}")] + InvalidLength(usize, Vec), + + /// A mnemonic could not be parsed from the provided mnemonic. + #[error("{0}")] + MnemonicFromStrError(#[from] MnemonicFromStrError), + } + + /// Validate a single mnemonic against a set of possible word lengths. + #[derive(Clone)] + pub struct MnemonicChoiceValidator { + /// The accepted [`WordLength`] of the mnemonic. + pub word_lengths: [WordLength; N], + } + + impl Validator for MnemonicChoiceValidator { + type Output = Mnemonic; + type Error = MnemonicChoiceValidationError; + + fn to_fn(&self) -> Box Result> { + let word_lengths = self.word_lengths.clone(); + Box::new(move |s: String| { + let count = s.split_whitespace().count(); + for word_length in &word_lengths { + if word_length.matches(count) { + let m = Mnemonic::from_str(&s)?; + return Ok(m); + } + } + Err(MnemonicChoiceValidationError::InvalidLength( + count, + word_lengths.to_vec(), + )) + }) + } + } + /// A mnemonic in the set of mnemonics could not be validated from the given inputs. #[derive(thiserror::Error, Debug)] pub enum MnemonicSetValidationError {