From c3aad0f40c94ae86d1b14ff1b05178c0c043f732 Mon Sep 17 00:00:00 2001 From: jp1ac4 <121959000+jp1ac4@users.noreply.github.com> Date: Mon, 26 Feb 2024 15:38:43 +0000 Subject: [PATCH] gui: add optional rpcauth to internal bitcoind config With the updated liana dependency, we also need to pass `None` to `list_spend_txs`. --- gui/Cargo.lock | 3 +- gui/Cargo.toml | 1 + gui/src/bitcoind.rs | 112 ++++++++++++++++++++++++++++- gui/src/daemon/embedded.rs | 4 +- gui/src/installer/step/bitcoind.rs | 1 + 5 files changed, 116 insertions(+), 5 deletions(-) diff --git a/gui/Cargo.lock b/gui/Cargo.lock index e33c7e61..ed047fa3 100644 --- a/gui/Cargo.lock +++ b/gui/Cargo.lock @@ -2668,7 +2668,7 @@ dependencies = [ [[package]] name = "liana" version = "4.0.0" -source = "git+https://github.com/wizardsardine/liana?branch=master#5a56fdb108351d0a6877a11a1dec7a27a3b0928f" +source = "git+https://github.com/wizardsardine/liana?branch=master#16afa3e9925cd016db03dbe954403bfa348b89e7" dependencies = [ "backtrace", "bdk_coin_select", @@ -2692,6 +2692,7 @@ version = "4.0.0" dependencies = [ "async-hwi", "backtrace", + "base64 0.21.6", "bitcoin_hashes 0.12.0", "chrono", "dirs 3.0.2", diff --git a/gui/Cargo.toml b/gui/Cargo.toml index 09d471c1..7278e79d 100644 --- a/gui/Cargo.toml +++ b/gui/Cargo.toml @@ -42,6 +42,7 @@ toml = "0.5" chrono = "0.4" # Used for managing internal bitcoind +base64 = "0.21" bitcoin_hashes = "0.12" reqwest = { version = "0.11", default-features=false, features = ["rustls-tls"] } rust-ini = "0.19.0" diff --git a/gui/src/bitcoind.rs b/gui/src/bitcoind.rs index 83653dba..db165949 100644 --- a/gui/src/bitcoind.rs +++ b/gui/src/bitcoind.rs @@ -1,6 +1,9 @@ +use base64::Engine; +use bitcoin_hashes::{sha256, Hash, HashEngine, Hmac, HmacEngine}; use liana::{ config::{BitcoindConfig, BitcoindRpcAuth}, miniscript::bitcoin::{self, Network}, + random::{random_bytes, RandomnessError}, }; use liana_ui::component::form; use std::collections::BTreeMap; @@ -121,12 +124,89 @@ pub fn bitcoind_network_dir(network: &Network) -> Option { Some(dir.to_string()) } +#[derive(PartialEq, Eq, Debug, Clone)] +pub enum RpcAuthParseError { + MissingColon, + MissingDollarSign, +} + +impl std::fmt::Display for RpcAuthParseError { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + Self::MissingColon => write!( + f, + "RPC auth string should contain colon between user and salt." + ), + Self::MissingDollarSign => write!( + f, + "RPC auth string should contain dollar sign between salt and password HMAC." + ), + } + } +} + +/// Represents RPC auth credentials as stored in bitcoin.conf. +#[derive(PartialEq, Eq, Debug, Clone)] +pub struct RpcAuth { + pub user: String, + salt: String, + password_hmac: String, +} + +impl RpcAuth { + /// Returns a new `RpcAuth` object for the given `user` with a random salt and password. + /// This random password is also returned. + pub fn new(user: &str) -> Result<(Self, String), RandomnessError> { + // RPC auth generation follows approach in + // https://github.com/bitcoin/bitcoin/blob/master/share/rpcauth/rpcauth.py + let password = + random_bytes().map(|bytes| base64::prelude::BASE64_URL_SAFE_NO_PAD.encode(bytes))?; + // As per the Python script, only use 16 bytes for the salt. + let salt = random_bytes().map(|bytes| hex::encode(&bytes[..16]))?; + let mut engine = HmacEngine::::new(salt.as_bytes()); + engine.input(password.as_bytes()); + let password_hmac = Hmac::::from_engine(engine); + + Ok(( + Self { + user: user.to_string(), + salt, + password_hmac: hex::encode(&password_hmac[..]), + }, + password, + )) + } +} + +impl std::fmt::Display for RpcAuth { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "{}:{}${}", self.user, self.salt, self.password_hmac) + } +} + +impl std::str::FromStr for RpcAuth { + type Err = RpcAuthParseError; + + fn from_str(s: &str) -> Result { + let (user, salt_pw) = s.split_once(':').ok_or(RpcAuthParseError::MissingColon)?; + let (salt, pw) = salt_pw + .split_once('$') + .ok_or(RpcAuthParseError::MissingDollarSign)?; + Ok(Self { + user: user.to_string(), + salt: salt.to_string(), + password_hmac: pw.to_string(), + }) + } +} + /// Represents section for a single network in `bitcoin.conf` file. #[derive(PartialEq, Eq, Debug, Clone)] pub struct InternalBitcoindNetworkConfig { pub rpc_port: u16, pub p2p_port: u16, pub prune: u32, + pub rpc_auth: Option, } /// Represents the `bitcoin.conf` file to be used by internal bitcoind. @@ -183,7 +263,7 @@ impl InternalBitcoindConfig { if let Some(sec) = maybe_sec { let network = Network::from_core_arg(sec) .map_err(|e| InternalBitcoindConfigError::UnexpectedSection(e.to_string()))?; - if prop.len() > 3 { + if prop.len() > 4 { return Err(InternalBitcoindConfigError::TooManyElements( sec.to_string(), )); @@ -203,12 +283,22 @@ impl InternalBitcoindConfig { .ok_or_else(|| InternalBitcoindConfigError::KeyNotFound("prune".to_string()))? .parse::() .map_err(|e| InternalBitcoindConfigError::CouldNotParseValue(e.to_string()))?; + let rpc_auth = prop + .get("rpcauth") + .map(|v| { + v.parse::().map_err(|e| { + InternalBitcoindConfigError::CouldNotParseValue(e.to_string()) + }) + }) + .transpose()?; + networks.insert( network, InternalBitcoindNetworkConfig { rpc_port, p2p_port, prune, + rpc_auth, }, ); } else if !prop.is_empty() { @@ -239,6 +329,11 @@ impl InternalBitcoindConfig { .set("rpcport", network_conf.rpc_port.to_string()) .set("port", network_conf.p2p_port.to_string()) .set("prune", network_conf.prune.to_string()); + if let Some(rpc_auth) = network_conf.rpc_auth.as_ref() { + conf_ini + .with_section(Some(network.to_core_arg())) + .set("rpcauth", rpc_auth.to_string()); + } } conf_ini } @@ -477,17 +572,24 @@ mod tests { .with_section(Some("regtest")) .set("rpcport", "34067") .set("port", "45175") - .set("prune", "2043"); + .set("prune", "2043") + .set("rpcauth", "my_user:my_salt$my_pw_hmac"); let conf = InternalBitcoindConfig::from_ini(&conf_ini).expect("Loading conf from ini"); let main_conf = InternalBitcoindNetworkConfig { rpc_port: 43345, p2p_port: 42355, prune: 15246, + rpc_auth: None, }; let regtest_conf = InternalBitcoindNetworkConfig { rpc_port: 34067, p2p_port: 45175, prune: 2043, + rpc_auth: Some(RpcAuth { + user: "my_user".to_string(), + salt: "my_salt".to_string(), + password_hmac: "my_pw_hmac".to_string(), + }), }; assert_eq!(conf.networks.len(), 2); assert_eq!( @@ -506,18 +608,22 @@ mod tests { conf.networks.insert(Network::Regtest, regtest_conf); for (sec, prop) in &conf.to_ini() { if let Some(sec) = sec { - assert_eq!(prop.len(), 3); let rpc_port = prop.get("rpcport").expect("rpcport"); let p2p_port = prop.get("port").expect("port"); let prune = prop.get("prune").expect("prune"); + let rpc_auth = prop.get("rpcauth"); if sec == "main" { + assert_eq!(prop.len(), 3); assert_eq!(rpc_port, "43345"); assert_eq!(p2p_port, "42355"); assert_eq!(prune, "15246"); + assert!(rpc_auth.is_none()); } else if sec == "regtest" { + assert_eq!(prop.len(), 4); assert_eq!(rpc_port, "34067"); assert_eq!(p2p_port, "45175"); assert_eq!(prune, "2043"); + assert_eq!(rpc_auth, Some("my_user:my_salt$my_pw_hmac")); } else { panic!("Unexpected section"); } diff --git a/gui/src/daemon/embedded.rs b/gui/src/daemon/embedded.rs index 2978c181..5091bb28 100644 --- a/gui/src/daemon/embedded.rs +++ b/gui/src/daemon/embedded.rs @@ -64,7 +64,9 @@ impl Daemon for EmbeddedDaemon { } fn list_spend_txs(&self) -> Result { - Ok(self.control()?.list_spend()) + self.control()? + .list_spend(None) + .map_err(|e| DaemonError::Unexpected(e.to_string())) } fn list_confirmed_txs( diff --git a/gui/src/installer/step/bitcoind.rs b/gui/src/installer/step/bitcoind.rs index 00ec02ee..2f1ad1f2 100644 --- a/gui/src/installer/step/bitcoind.rs +++ b/gui/src/installer/step/bitcoind.rs @@ -614,6 +614,7 @@ impl Step for InternalBitcoindStep { rpc_port, p2p_port, prune: PRUNE_DEFAULT, + rpc_auth: None, } } (Ok(_), Err(e)) | (Err(e), Ok(_)) => {