Implement mnemonic support for solana-keygen grind (solana-labs#9325) (#16108)

* Implement mnemonic support for solana-keygen grind (solana-labs#9325)

* Updated to include feedback from review.

* Renaming as per review feedback

* Fixed an incorrectly transcribed underscore

* Properly re-use string constants.
This commit is contained in:
bji
2021-03-27 22:47:50 -07:00
committed by GitHub
parent aabe186e3f
commit e50f598449
2 changed files with 180 additions and 69 deletions

View File

@ -157,6 +157,15 @@ You can generate a custom vanity keypair using solana-keygen. For instance:
solana-keygen grind --starts-with e1v1s:1 solana-keygen grind --starts-with e1v1s:1
``` ```
You may request that the generated vanity keypair be expressed as a seed phrase
which allows recovery of the keypair from the seed phrase and an optionally
supplied passphrase (note that this is significantly slower than grinding without
a mnemonic):
```bash
solana-keygen grind --use-mnemonic --starts-with e1v1s:1
```
Depending on the string requested, it may take days to find a match... Depending on the string requested, it may take days to find a match...
--- ---

View File

@ -10,7 +10,7 @@ use solana_clap_utils::{
keypair_from_seed_phrase, prompt_passphrase, signer_from_path, keypair_from_seed_phrase, prompt_passphrase, signer_from_path,
SKIP_SEED_PHRASE_VALIDATION_ARG, SKIP_SEED_PHRASE_VALIDATION_ARG,
}, },
DisplayError, ArgConstant, DisplayError,
}; };
use solana_cli_config::{Config, CONFIG_FILE}; use solana_cli_config::{Config, CONFIG_FILE};
use solana_remote_wallet::remote_wallet::RemoteWalletManager; use solana_remote_wallet::remote_wallet::RemoteWalletManager;
@ -41,6 +41,86 @@ struct GrindMatch {
count: AtomicU64, count: AtomicU64,
} }
const WORD_COUNT_ARG: ArgConstant<'static> = ArgConstant {
long: "word-count",
name: "word_count",
help: "Specify the number of words that will be present in the generated seed phrase",
};
const LANGUAGE_ARG: ArgConstant<'static> = ArgConstant {
long: "language",
name: "language",
help: "Specify the mnemonic lanaguage that will be present in the generated seed phrase",
};
const NO_PASSPHRASE_ARG: ArgConstant<'static> = ArgConstant {
long: "no-bip39-passphrase",
name: "no_passphrase",
help: "Do not prompt for a BIP39 passphrase",
};
const NO_OUTFILE_ARG: ArgConstant<'static> = ArgConstant {
long: "no-outfile",
name: "no_outfile",
help: "Only print a seed phrase and pubkey. Do not output a keypair file",
};
fn word_count_arg<'a, 'b>() -> Arg<'a, 'b> {
Arg::with_name(WORD_COUNT_ARG.name)
.long(WORD_COUNT_ARG.long)
.possible_values(&["12", "15", "18", "21", "24"])
.default_value("12")
.value_name("NUMBER")
.takes_value(true)
.help(WORD_COUNT_ARG.help)
}
fn language_arg<'a, 'b>() -> Arg<'a, 'b> {
Arg::with_name(LANGUAGE_ARG.name)
.long(LANGUAGE_ARG.long)
.possible_values(&[
"english",
"chinese-simplified",
"chinese-traditional",
"japanese",
"spanish",
"korean",
"french",
"italian",
])
.default_value("english")
.value_name("LANGUAGE")
.takes_value(true)
.help(LANGUAGE_ARG.help)
}
fn no_passphrase_arg<'a, 'b>() -> Arg<'a, 'b> {
Arg::with_name(NO_PASSPHRASE_ARG.name)
.long(NO_PASSPHRASE_ARG.long)
.alias("no-passphrase")
.help(NO_PASSPHRASE_ARG.help)
}
fn no_outfile_arg<'a, 'b>() -> Arg<'a, 'b> {
Arg::with_name(NO_OUTFILE_ARG.name)
.long(NO_OUTFILE_ARG.long)
.conflicts_with_all(&["outfile", "silent"])
.help(NO_OUTFILE_ARG.help)
}
trait KeyGenerationCommonArgs {
fn key_generation_common_args(self) -> Self;
}
impl KeyGenerationCommonArgs for App<'_, '_> {
fn key_generation_common_args(self) -> Self {
self.arg(word_count_arg())
.arg(language_arg())
.arg(no_passphrase_arg())
.arg(no_outfile_arg())
}
}
fn check_for_overwrite(outfile: &str, matches: &ArgMatches) { fn check_for_overwrite(outfile: &str, matches: &ArgMatches) {
let force = matches.is_present("force"); let force = matches.is_present("force");
if !force && Path::new(outfile).exists() { if !force && Path::new(outfile).exists() {
@ -131,6 +211,45 @@ fn grind_validator_starts_and_ends_with(v: String) -> Result<(), String> {
Ok(()) Ok(())
} }
fn acquire_language(matches: &ArgMatches<'_>) -> Language {
match matches.value_of(LANGUAGE_ARG.name).unwrap() {
"english" => Language::English,
"chinese-simplified" => Language::ChineseSimplified,
"chinese-traditional" => Language::ChineseTraditional,
"japanese" => Language::Japanese,
"spanish" => Language::Spanish,
"korean" => Language::Korean,
"french" => Language::French,
"italian" => Language::Italian,
_ => unreachable!(),
}
}
fn no_passphrase_and_message() -> (String, String) {
(NO_PASSPHRASE.to_string(), "".to_string())
}
fn acquire_passphrase_and_message(
matches: &ArgMatches<'_>,
) -> Result<(String, String), Box<dyn error::Error>> {
if matches.is_present(NO_PASSPHRASE_ARG.name) {
Ok(no_passphrase_and_message())
} else {
match prompt_passphrase(
"\nFor added security, enter a BIP39 passphrase\n\
\nNOTE! This passphrase improves security of the recovery seed phrase NOT the\n\
keypair file itself, which is stored as insecure plain text\n\
\nBIP39 Passphrase (empty for none): ",
) {
Ok(passphrase) => {
println!();
Ok((passphrase, " and your BIP39 passphrase".to_string()))
}
Err(e) => Err(e),
}
}
}
fn grind_print_info(grind_matches: &[GrindMatch], num_threads: usize) { fn grind_print_info(grind_matches: &[GrindMatch], num_threads: usize) {
println!("Searching with {} threads for:", num_threads); println!("Searching with {} threads for:", num_threads);
for gm in grind_matches { for gm in grind_matches {
@ -265,42 +384,13 @@ fn main() -> Result<(), Box<dyn error::Error>> {
.long("force") .long("force")
.help("Overwrite the output file if it exists"), .help("Overwrite the output file if it exists"),
) )
.arg(
Arg::with_name("word_count")
.long("word-count")
.possible_values(&["12", "15", "18", "21", "24"])
.default_value("12")
.value_name("NUMBER")
.takes_value(true)
.help("Specify the number of words that will be present in the generated seed phrase"),
)
.arg(
Arg::with_name("language")
.long("language")
.possible_values(&["english", "chinese-simplified", "chinese-traditional", "japanese", "spanish", "korean", "french", "italian"])
.default_value("english")
.value_name("LANGUAGE")
.takes_value(true)
.help("Specify the mnemonic lanaguage that will be present in the generated seed phrase"),
)
.arg(
Arg::with_name("no_passphrase")
.long("no-bip39-passphrase")
.alias("no-passphrase")
.help("Do not prompt for a BIP39 passphrase"),
)
.arg(
Arg::with_name("no_outfile")
.long("no-outfile")
.conflicts_with_all(&["outfile", "silent"])
.help("Only print a seed phrase and pubkey. Do not output a keypair file"),
)
.arg( .arg(
Arg::with_name("silent") Arg::with_name("silent")
.short("s") .short("s")
.long("silent") .long("silent")
.help("Do not display seed phrase. Useful when piping output to other programs that prompt for user input, like gpg"), .help("Do not display seed phrase. Useful when piping output to other programs that prompt for user input, like gpg"),
) )
.key_generation_common_args()
) )
.subcommand( .subcommand(
SubCommand::with_name("grind") SubCommand::with_name("grind")
@ -349,7 +439,13 @@ fn main() -> Result<(), Box<dyn error::Error>> {
.validator(is_parsable::<usize>) .validator(is_parsable::<usize>)
.default_value(&default_num_threads) .default_value(&default_num_threads)
.help("Specify the number of grind threads"), .help("Specify the number of grind threads"),
), )
.arg(
Arg::with_name("use_mnemonic")
.long("use-mnemonic")
.help("Generate using a mnemonic key phrase. Expect a significant slowdown in this mode"),
)
.key_generation_common_args()
) )
.subcommand( .subcommand(
SubCommand::with_name("pubkey") SubCommand::with_name("pubkey")
@ -438,7 +534,7 @@ fn do_main(matches: &ArgMatches<'_>) -> Result<(), Box<dyn error::Error>> {
let mut path = dirs_next::home_dir().expect("home directory"); let mut path = dirs_next::home_dir().expect("home directory");
let outfile = if matches.is_present("outfile") { let outfile = if matches.is_present("outfile") {
matches.value_of("outfile") matches.value_of("outfile")
} else if matches.is_present("no_outfile") { } else if matches.is_present(NO_OUTFILE_ARG.name) {
None None
} else { } else {
path.extend(&[".config", "solana", "id.json"]); path.extend(&[".config", "solana", "id.json"]);
@ -451,43 +547,16 @@ fn do_main(matches: &ArgMatches<'_>) -> Result<(), Box<dyn error::Error>> {
None => (), None => (),
} }
let word_count = value_t!(matches.value_of("word_count"), usize).unwrap(); let word_count = value_t!(matches.value_of(WORD_COUNT_ARG.name), usize).unwrap();
let mnemonic_type = MnemonicType::for_word_count(word_count)?; let mnemonic_type = MnemonicType::for_word_count(word_count)?;
let language = match matches.value_of("language").unwrap() { let language = acquire_language(matches);
"english" => Language::English,
"chinese-simplified" => Language::ChineseSimplified,
"chinese-traditional" => Language::ChineseTraditional,
"japanese" => Language::Japanese,
"spanish" => Language::Spanish,
"korean" => Language::Korean,
"french" => Language::French,
"italian" => Language::Italian,
_ => unreachable!(),
};
let silent = matches.is_present("silent"); let silent = matches.is_present("silent");
if !silent { if !silent {
println!("Generating a new keypair"); println!("Generating a new keypair");
} }
let mnemonic = Mnemonic::new(mnemonic_type, language); let mnemonic = Mnemonic::new(mnemonic_type, language);
let passphrase = if matches.is_present("no_passphrase") { let (passphrase, passphrase_message) = acquire_passphrase_and_message(matches).unwrap();
NO_PASSPHRASE.to_string()
} else {
let passphrase = prompt_passphrase(
"\nFor added security, enter a BIP39 passphrase\n\
\nNOTE! This passphrase improves security of the recovery seed phrase NOT the\n\
keypair file itself, which is stored as insecure plain text\n\
\nBIP39 Passphrase (empty for none): ",
)?;
println!();
passphrase
};
let passphrase_message = if passphrase == NO_PASSPHRASE {
"".to_string()
} else {
" and your BIP39 passphrase".to_string()
};
let seed = Seed::new(&mnemonic, &passphrase); let seed = Seed::new(&mnemonic, &passphrase);
let keypair = keypair_from_seed(seed.as_bytes())?; let keypair = keypair_from_seed(seed.as_bytes())?;
@ -571,6 +640,19 @@ fn do_main(matches: &ArgMatches<'_>) -> Result<(), Box<dyn error::Error>> {
num_threads, num_threads,
); );
let use_mnemonic = matches.is_present("use_mnemonic");
let word_count = value_t!(matches.value_of(WORD_COUNT_ARG.name), usize).unwrap();
let mnemonic_type = MnemonicType::for_word_count(word_count)?;
let language = acquire_language(matches);
let (passphrase, passphrase_message) = if use_mnemonic {
acquire_passphrase_and_message(matches).unwrap()
} else {
no_passphrase_and_message()
};
let no_outfile = matches.is_present(NO_OUTFILE_ARG.name);
let grind_matches_thread_safe = Arc::new(grind_matches); let grind_matches_thread_safe = Arc::new(grind_matches);
let attempts = Arc::new(AtomicU64::new(1)); let attempts = Arc::new(AtomicU64::new(1));
let found = Arc::new(AtomicU64::new(0)); let found = Arc::new(AtomicU64::new(0));
@ -583,6 +665,8 @@ fn do_main(matches: &ArgMatches<'_>) -> Result<(), Box<dyn error::Error>> {
let attempts = attempts.clone(); let attempts = attempts.clone();
let found = found.clone(); let found = found.clone();
let grind_matches_thread_safe = grind_matches_thread_safe.clone(); let grind_matches_thread_safe = grind_matches_thread_safe.clone();
let passphrase = passphrase.clone();
let passphrase_message = passphrase_message.clone();
thread::spawn(move || loop { thread::spawn(move || loop {
if done.load(Ordering::Relaxed) { if done.load(Ordering::Relaxed) {
@ -597,7 +681,13 @@ fn do_main(matches: &ArgMatches<'_>) -> Result<(), Box<dyn error::Error>> {
found.load(Ordering::Relaxed), found.load(Ordering::Relaxed),
); );
} }
let keypair = Keypair::new(); let (keypair, phrase) = if use_mnemonic {
let mnemonic = Mnemonic::new(mnemonic_type, language);
let seed = Seed::new(&mnemonic, &passphrase);
(keypair_from_seed(seed.as_bytes()).unwrap(), mnemonic.phrase().to_string())
} else {
(Keypair::new(), "".to_string())
};
let mut pubkey = bs58::encode(keypair.pubkey()).into_string(); let mut pubkey = bs58::encode(keypair.pubkey()).into_string();
if ignore_case { if ignore_case {
pubkey = pubkey.to_lowercase(); pubkey = pubkey.to_lowercase();
@ -623,12 +713,24 @@ fn do_main(matches: &ArgMatches<'_>) -> Result<(), Box<dyn error::Error>> {
grind_matches_thread_safe[i] grind_matches_thread_safe[i]
.count .count
.fetch_sub(1, Ordering::Relaxed); .fetch_sub(1, Ordering::Relaxed);
println!( if !no_outfile {
"Wrote keypair to {}", write_keypair_file(&keypair, &format!("{}.json", keypair.pubkey()))
&format!("{}.json", keypair.pubkey())
);
write_keypair_file(&keypair, &format!("{}.json", keypair.pubkey()))
.unwrap(); .unwrap();
println!(
"Wrote keypair to {}",
&format!("{}.json", keypair.pubkey())
);
}
if use_mnemonic {
let divider = String::from_utf8(vec![b'='; phrase.len()]).unwrap();
println!(
"{}\nFound matching key {}",
&divider, keypair.pubkey());
println!(
"\nSave this seed phrase{} to recover your new keypair:\n{}\n{}",
passphrase_message, phrase, &divider
);
}
} }
} }
if total_matches_found == grind_matches_thread_safe.len() { if total_matches_found == grind_matches_thread_safe.len() {