diff --git a/cli/src/modules/message.rs b/cli/src/modules/message.rs new file mode 100644 index 000000000..e1a7a1e3d --- /dev/null +++ b/cli/src/modules/message.rs @@ -0,0 +1,155 @@ +use kaspa_addresses::Version; +use kaspa_bip32::secp256k1::XOnlyPublicKey; +use kaspa_wallet_core::message::{sign_message, verify_message, PersonalMessage}; + +use crate::imports::*; + +#[derive(Default)] +pub struct Message; + +#[async_trait] +impl Handler for Message { + fn verb(&self, _ctx: &Arc) -> Option<&'static str> { + Some("message") + } + + fn help(&self, _ctx: &Arc) -> &'static str { + "Sign a message or verify a message signature" + } + + async fn handle(self: Arc, ctx: &Arc, argv: Vec, cmd: &str) -> cli::Result<()> { + let ctx = ctx.clone().downcast_arc::()?; + self.main(ctx, argv, cmd).await.map_err(|e| e.into()) + } +} + +impl Message { + async fn main(self: Arc, ctx: Arc, argv: Vec, _cmd: &str) -> Result<()> { + if argv.is_empty() { + return self.display_help(ctx, argv).await; + } + + match argv.get(0).unwrap().as_str() { + "sign" => { + if argv.len() != 2 { + return self.display_help(ctx, argv).await; + } + + let kaspa_address = argv[1].as_str(); + let asked_message = ctx.term().ask(false, "Message: ").await?; + let message = asked_message.as_str(); + + self.sign(ctx, kaspa_address, message).await?; + } + "verify" => { + if argv.len() != 3 { + return self.display_help(ctx, argv).await; + } + let kaspa_address = argv[1].as_str(); + let signature = argv[2].as_str(); + let asked_message = ctx.term().ask(false, "Message: ").await?; + let message = asked_message.as_str(); + + self.verify(ctx, kaspa_address, signature, message).await?; + } + v => { + tprintln!(ctx, "unknown command: '{v}'\r\n"); + return self.display_help(ctx, argv).await; + } + } + + Ok(()) + } + + async fn display_help(self: Arc, ctx: Arc, _argv: Vec) -> Result<()> { + ctx.term().help( + &[ + ("sign ", "Sign a message with the private key that matches the given address. Prompts for message."), + ( + "verify ", + "Verify the signature against the message and kaspa_address. Prompts for message.", + ), + ], + None, + )?; + + Ok(()) + } + + async fn sign(self: Arc, ctx: Arc, kaspa_address: &str, message: &str) -> Result<()> { + let kaspa_address = Address::try_from(kaspa_address)?; + if kaspa_address.version != Version::PubKey { + return Err(Error::custom("Address not supported for message signing. Only supports PubKey addresses")); + } + + let pm = PersonalMessage(message); + let privkey = self.get_address_private_key(&ctx, kaspa_address).await?; + + let sig_result = sign_message(&pm, &privkey); + + match sig_result { + Ok(signature) => { + let sig_hex = faster_hex::hex_string(signature.as_slice()); + tprintln!(ctx, "Signature: {}", sig_hex); + Ok(()) + } + Err(_) => Err(Error::custom("Message signing failed")), + } + } + + async fn verify(self: Arc, ctx: Arc, kaspa_address: &str, signature: &str, message: &str) -> Result<()> { + let kaspa_address = Address::try_from(kaspa_address)?; + if kaspa_address.version != Version::PubKey { + return Err(Error::custom("Address not supported for message signing. Only supports PubKey addresses")); + } + + let pubkey = XOnlyPublicKey::from_slice(&kaspa_address.payload[0..32]).unwrap(); + + let mut signature_hex = [0u8; 64]; + faster_hex::hex_decode(signature.as_bytes(), &mut signature_hex)?; + + let pm = PersonalMessage(message); + let verify_result = verify_message(&pm, &signature_hex.to_vec(), &pubkey); + + match verify_result { + Ok(()) => { + tprintln!(ctx, "Message verified successfully!"); + } + Err(_) => { + return Err(Error::custom("Verification failed")); + } + } + + Ok(()) + } + + async fn get_address_private_key(self: Arc, ctx: &Arc, kaspa_address: Address) -> Result<[u8; 32]> { + let account = ctx.wallet().account()?; + + match account.account_kind() { + AccountKind::Bip32 => { + let (wallet_secret, payment_secret) = ctx.ask_wallet_secret(Some(&account)).await?; + let keydata = account.prv_key_data(wallet_secret).await?; + let account = account.clone().as_derivation_capable().expect("expecting derivation capable"); + + let (receive, change) = account.derivation().addresses_indexes(&[&kaspa_address])?; + let private_keys = account.create_private_keys(&keydata, &payment_secret, &receive, &change)?; + for (address, private_key) in private_keys { + if kaspa_address == *address { + return Ok(private_key.secret_bytes()); + } + } + + Err(Error::custom("Could not find address in any derivation path in account")) + } + AccountKind::Keypair => { + let (wallet_secret, payment_secret) = ctx.ask_wallet_secret(Some(&account)).await?; + let keydata = account.prv_key_data(wallet_secret).await?; + let decrypted_privkey = keydata.payload.decrypt(payment_secret.as_ref()).unwrap(); + let secretkey = decrypted_privkey.as_secret_key()?.unwrap(); + Ok(secretkey.secret_bytes()) + } + _ => Err(Error::custom("Unsupported account kind")), + } + } +} diff --git a/cli/src/modules/mod.rs b/cli/src/modules/mod.rs index 1e0a4eeab..8e9a6fc3a 100644 --- a/cli/src/modules/mod.rs +++ b/cli/src/modules/mod.rs @@ -18,6 +18,7 @@ pub mod help; pub mod history; pub mod import; pub mod list; +pub mod message; pub mod miner; pub mod monitor; pub mod mute; @@ -52,7 +53,7 @@ pub fn register_handlers(cli: &Arc) -> Result<()> { cli.handlers(), [ account, address, close, connect, details, disconnect, estimate, exit, export, guide, help, history, import, rpc, list, - miner, monitor, mute, network, node, open, ping, reload, select, send, server, settings, sweep, track, transfer, + miner, message, monitor, mute, network, node, open, ping, reload, select, send, server, settings, sweep, track, transfer, wallet, // halt, // theme, start, stop