diff --git a/src/display_secret.rs b/src/display_secret.rs index b7b001a2..8a98f829 100644 --- a/src/display_secret.rs +++ b/src/display_secret.rs @@ -5,6 +5,7 @@ pub struct DisplaySecret(pub(crate) PrivateKey); impl Display for DisplaySecret { fn fmt(&self, f: &mut Formatter) -> fmt::Result { let mut encoder = Bech32Encoder::new(Bech32Type::PrivateKey); + encoder.bytes(&self.0.public_key().inner().to_bytes()); encoder.bytes(&self.0.as_secret_bytes()); write!(f, "{encoder}") } diff --git a/src/error.rs b/src/error.rs index 93d3b70f..090c7395 100644 --- a/src/error.rs +++ b/src/error.rs @@ -214,7 +214,7 @@ pub enum Error { PrivateKeyLoad { backtrace: Option, path: DisplayPath, - source: Bech32Error, + source: PrivateKeyError, }, #[snafu(display("private key not found: `{path}`"))] PrivateKeyNotFound { diff --git a/src/lib.rs b/src/lib.rs index 76353d3c..53c1a3b2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -52,6 +52,7 @@ use { owo_colorize_ext::OwoColorizeExt, package::Package, path_error::PathError, + private_key_error::PrivateKeyError, public_key_error::PublicKeyError, sign_options::SignOptions, signature_error::SignatureError, @@ -187,6 +188,7 @@ mod owo_colorize_ext; mod package; mod path_error; mod private_key; +mod private_key_error; mod progress_bar; mod public_key; mod public_key_error; diff --git a/src/private_key.rs b/src/private_key.rs index fc9c3fdc..9107289d 100644 --- a/src/private_key.rs +++ b/src/private_key.rs @@ -58,12 +58,22 @@ impl PrivateKey { } impl FromStr for PrivateKey { - type Err = Bech32Error; + type Err = PrivateKeyError; fn from_str(key: &str) -> Result { - let inner = Bech32Decoder::decode_byte_array(Bech32Type::PrivateKey, key)?; - let inner = ed25519_dalek::SigningKey::from_bytes(&inner); + let mut decoder = Bech32Decoder::new(Bech32Type::PrivateKey, key)?; + let public_key = decoder.byte_array()?; + let private_key = decoder.byte_array()?; + decoder.done()?; + + let inner = ed25519_dalek::SigningKey::from_bytes(&private_key); assert!(!inner.verifying_key().is_weak()); + + ensure!( + inner.verifying_key().to_bytes() == public_key, + private_key_error::PublicKeyMismatch, + ); + Ok(Self(inner)) } } @@ -85,6 +95,32 @@ mod tests { ); } + #[test] + fn private_key_begins_with_public_key() { + let prefix = format!( + "private1a{}", + &test::PUBLIC_KEY["public1a".len()..test::PUBLIC_KEY.len() - 6], + ); + assert!(test::PRIVATE_KEY.starts_with(&prefix)); + } + + #[test] + fn public_key_mismatch_error() { + let other = PrivateKey::generate(); + let other_public_key_data = + &other.public_key().to_string()["public1a".len()..test::PUBLIC_KEY.len() - 6]; + let public_key_data_len = test::PUBLIC_KEY.len() - "public1a".len() - 6; + let private_key_data = + &test::PRIVATE_KEY["private1a".len() + public_key_data_len..test::PRIVATE_KEY.len() - 6]; + let mismatched = test::checksum(&format!( + "private1a{other_public_key_data}{private_key_data}" + )); + assert_eq!( + mismatched.parse::().unwrap_err().to_string(), + "private key derived public key does not match embedded public key", + ); + } + #[test] fn serialized_private_key_is_not_valid_public_key() { test::PRIVATE_KEY.parse::().unwrap_err(); diff --git a/src/private_key_error.rs b/src/private_key_error.rs new file mode 100644 index 00000000..3a014a4a --- /dev/null +++ b/src/private_key_error.rs @@ -0,0 +1,10 @@ +use super::*; + +#[derive(Debug, Snafu)] +#[snafu(context(suffix(false)), visibility(pub(crate)))] +pub enum PrivateKeyError { + #[snafu(transparent)] + Bech32 { source: Bech32Error }, + #[snafu(display("private key derived public key does not match embedded public key"))] + PublicKeyMismatch, +} diff --git a/src/test.rs b/src/test.rs index ad9bc9e1..fc0a8cd1 100644 --- a/src/test.rs +++ b/src/test.rs @@ -5,8 +5,10 @@ pub(crate) const FINGERPRINT: &str = pub(crate) const HASH: &str = "af1349b9f5f9a1a6a0404dea36dcc9499bcb25c9adc112b7cc9a93cae41f3262"; -pub(crate) const PRIVATE_KEY: &str = - "private1a24p4zsr2nh04f4pkgtxfzv5yle473x4jue7s6lkwg9tdkk73q59qluezpp"; +pub(crate) const PRIVATE_KEY: &str = concat!( + "private1a67dndhhmae7p6fsfnj0z37zf78cde6mwqgtms0y87h8ldlvvflyq24p4zsr2nh04f4pkgtxf", + "zv5yle473x4jue7s6lkwg9tdkk73q59qxqurh4", +); pub(crate) const PUBLIC_KEY: &str = "public1a67dndhhmae7p6fsfnj0z37zf78cde6mwqgtms0y87h8ldlvvflyqcxnd63"; diff --git a/tests/keygen.rs b/tests/keygen.rs index 238c03f4..2e8c2b31 100644 --- a/tests/keygen.rs +++ b/tests/keygen.rs @@ -5,7 +5,7 @@ fn custom_name() { let test = Test::new() .args(["keygen", "--name", "deploy"]) .assert_file_regex("keychain/deploy.public", "public1a.{58}\n") - .assert_file_regex("keychain/deploy.private", "private1a.{58}\n") + .assert_file_regex("keychain/deploy.private", "private1a.{110}\n") .success(); let public_key = test.read_public_key("keychain/deploy.public"); @@ -22,7 +22,7 @@ fn default_name() { let test = Test::new() .arg("keygen") .assert_file_regex("keychain/master.public", "public1a.{58}\n") - .assert_file_regex("keychain/master.private", "private1a.{58}\n") + .assert_file_regex("keychain/master.private", "private1a.{110}\n") .success(); let public_key = test.read_public_key("keychain/master.public");