Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ page. See [DEVELOPMENT_CYCLE.md](DEVELOPMENT_CYCLE.md) for more details.
- Fixed `create_tx`, `create_dns_tx`, `create_sp_tx` and `bump_fee` building transactions over Bitcoin Core's 400,000 wu (100,000 vB) standard weight limit, which no default node relays
- Fixed `key restore` ignoring `--password`, which restored passphrase-protected mnemonics to the wrong key
- Fixed `key generate` silently producing a 24-word mnemonic for any `--entropy` value other than 12; values other than 12 and 24 are now rejected
- BREAKING: renamed `key generate --entropy` to `--word_count` (`-e` to `-w`), and `--password` to `--passphrase` (env `PASSWORD` to `PASSPHRASE`) in `key generate` and `key restore`

## [4.0.0]

Expand Down
78 changes: 36 additions & 42 deletions src/handlers/key.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use crate::error::BDKCliError as Error;
use crate::handlers::{AppCommand, AppContext, Init};
use crate::utils::{output::FormatOutput, types::KeyResult};
use bdk_wallet::bip39::{Language, Mnemonic};
use bdk_wallet::bitcoin::Network;
use bdk_wallet::bitcoin::bip32::{DerivationPath, KeySource, Xpriv};
use bdk_wallet::bitcoin::key::Secp256k1;
use bdk_wallet::keys::{
Expand Down Expand Up @@ -48,46 +49,29 @@ impl From<MnemonicWordCount> for WordCount {

#[derive(Parser, Debug, Clone, PartialEq, Eq)]
pub struct GenerateKeyCommand {
/// Entropy level based on number of random seed mnemonic words.
/// Number of words in the generated mnemonic.
#[arg(
env = "WORD_COUNT",
short = 'e',
long = "entropy",
short = 'w',
long = "word_count",
value_enum,
default_value_t = MnemonicWordCount::Words12
)]
word_count: MnemonicWordCount,
/// Seed password.
#[arg(env = "PASSWORD", short = 'p', long = "password")]
password: Option<String>,
/// Optional BIP-39 passphrase. Restoring the key needs the same mnemonic and
/// passphrase; a different passphrase gives a different key, not an error.
#[arg(env = "PASSPHRASE", short = 'p', long = "passphrase")]
passphrase: Option<String>,
}

impl AppCommand<AppContext<Init>> for GenerateKeyCommand {
type Output = KeyResult;

fn execute(&self, ctx: &mut AppContext<Init>) -> Result<Self::Output, Error> {
let secp = Secp256k1::new();
let mnemonic: GeneratedKey<_, miniscript::BareCtx> =
Mnemonic::generate((self.word_count.into(), Language::English))
.map_err(|_| Error::Generic("Mnemonic generation error".to_string()))?;
let mnemonic = mnemonic.into_key();
let xkey: ExtendedKey = (mnemonic.clone(), self.password.clone()).into_extended_key()?;
let xprv = xkey.into_xprv(ctx.network.into()).ok_or_else(|| {
Error::Generic("Privatekey info not found (should not happen)".to_string())
})?;
let fingerprint = xprv.fingerprint(&secp);
let phrase = mnemonic
.words()
.fold("".to_string(), |phrase, w| phrase + w + " ")
.trim()
.to_string();

Ok(KeyResult {
xprv: xprv.to_string(),
mnemonic: Some(phrase),
fingerprint: Some(fingerprint.to_string()),
xpub: None,
})
key_from_mnemonic(mnemonic.into_key(), self.passphrase.clone(), ctx.network)
}
}

Expand Down Expand Up @@ -142,29 +126,39 @@ pub struct RestoreKeyCommand {
/// Seed mnemonic words, must be quoted (eg. "word1 word2 ...").
#[arg(env = "MNEMONIC", short = 'm', long = "mnemonic")]
mnemonic: String,
/// Seed password.
#[arg(env = "PASSWORD", short = 'p', long = "password")]
password: Option<String>,
/// Optional BIP-39 passphrase. Restoring the key needs the same mnemonic and
/// passphrase; a different passphrase gives a different key, not an error.
#[arg(env = "PASSPHRASE", short = 'p', long = "passphrase")]
passphrase: Option<String>,
}

impl AppCommand<AppContext<Init>> for RestoreKeyCommand {
type Output = KeyResult;

fn execute(&self, ctx: &mut AppContext<Init>) -> Result<Self::Output, Error> {
let secp = Secp256k1::new();

let mnemonic = Mnemonic::parse_in(Language::English, &self.mnemonic)?;
let xkey: ExtendedKey = (mnemonic.clone(), self.password.clone()).into_extended_key()?;
let xprv = xkey.into_xprv(ctx.network.into()).ok_or_else(|| {
Error::Generic("Privatekey info not found (should not happen)".to_string())
})?;
let fingerprint = xprv.fingerprint(&secp);

Ok(KeyResult {
xprv: xprv.to_string(),
mnemonic: Some(mnemonic.to_string()),
fingerprint: Some(fingerprint.to_string()),
xpub: None,
})
key_from_mnemonic(mnemonic, self.passphrase.clone(), ctx.network)
}
}

/// Derives the master key from a mnemonic and optional passphrase, as used by
/// both `key generate` and `key restore`.
fn key_from_mnemonic(
mnemonic: Mnemonic,
passphrase: Option<String>,
network: Network,
) -> Result<KeyResult, Error> {
let secp = Secp256k1::new();
let xkey: ExtendedKey = (mnemonic.clone(), passphrase).into_extended_key()?;
let xprv = xkey.into_xprv(network.into()).ok_or_else(|| {
Error::Generic("Privatekey info not found (should not happen)".to_string())
})?;
let fingerprint = xprv.fingerprint(&secp);

Ok(KeyResult {
xprv: xprv.to_string(),
mnemonic: Some(mnemonic.to_string()),
fingerprint: Some(fingerprint.to_string()),
xpub: None,
})
}
26 changes: 13 additions & 13 deletions tests/integration/init.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ mod test_key {

for word_count in [12, 24] {
let output = cli
.key_cmd(&["generate", "--entropy", &word_count.to_string()])
.key_cmd(&["generate", "--word_count", &word_count.to_string()])
.output()
.expect("Failed to execute generate command");
assert!(output.status.success(), "Generate command failed");
Expand All @@ -43,7 +43,7 @@ mod test_key {
let cli = BdkCli::new("testnet", Some(temp_dir.path().to_path_buf()));

for word_count in ["0", "13", "15", "18", "21", "999"] {
cli.key_cmd(&["generate", "--entropy", word_count])
cli.key_cmd(&["generate", "--word_count", word_count])
.assert()
.failure()
.stderr(predicate::str::contains("possible values"));
Expand Down Expand Up @@ -134,22 +134,22 @@ mod test_key {
}

#[test]
fn test_cli_key_restore_with_password() {
fn test_cli_key_restore_with_passphrase() {
let temp_dir = TempDir::new().unwrap();
let cli = BdkCli::new("testnet", Some(temp_dir.path().to_path_buf()));

let generate_cmd = cli
.key_cmd(&["generate", "--password", "foo"])
.key_cmd(&["generate", "--passphrase", "foo"])
.output()
.expect("Failed to execute generate command");
assert!(generate_cmd.status.success(), "Generate command failed");
let generate_json: Value =
serde_json::from_slice(&generate_cmd.stdout).expect("Failed to parse JSON");
let mnemonic = generate_json["mnemonic"].as_str().unwrap();

// Restoring with the same password must give back the same key
// Restoring with the same passphrase must give back the same key
let restore_cmd = cli
.key_cmd(&["restore", "--mnemonic", mnemonic, "--password", "foo"])
.key_cmd(&["restore", "--mnemonic", mnemonic, "--passphrase", "foo"])
.output()
.expect("Failed to execute restore command");
assert!(restore_cmd.status.success(), "Restore command failed");
Expand All @@ -158,16 +158,16 @@ mod test_key {
assert_eq!(generate_json["xprv"], restore_json["xprv"]);
assert_eq!(generate_json["fingerprint"], restore_json["fingerprint"]);

// Restoring without the password must give a different key
let no_password_cmd = cli
// Restoring without the passphrase must give a different key
let no_passphrase_cmd = cli
.key_cmd(&["restore", "--mnemonic", mnemonic])
.env_remove("PASSWORD")
.env_remove("PASSPHRASE")
.output()
.expect("Failed to execute restore command");
assert!(no_password_cmd.status.success(), "Restore command failed");
let no_password_json: Value =
serde_json::from_slice(&no_password_cmd.stdout).expect("Failed to parse JSON");
assert_ne!(generate_json["xprv"], no_password_json["xprv"]);
assert!(no_passphrase_cmd.status.success(), "Restore command failed");
let no_passphrase_json: Value =
serde_json::from_slice(&no_passphrase_cmd.stdout).expect("Failed to parse JSON");
assert_ne!(generate_json["xprv"], no_passphrase_json["xprv"]);
}
}

Expand Down
Loading