gui: add optional rpcauth to internal bitcoind config

With the updated liana dependency, we also need to pass `None` to
`list_spend_txs`.
This commit is contained in:
jp1ac4 2024-02-26 15:38:43 +00:00
parent e172ecd2f1
commit c3aad0f40c
No known key found for this signature in database
GPG Key ID: A7ACD32423568D7B
5 changed files with 116 additions and 5 deletions

3
gui/Cargo.lock generated
View File

@ -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",

View File

@ -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"

View File

@ -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<String> {
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::<sha256::Hash>::new(salt.as_bytes());
engine.input(password.as_bytes());
let password_hmac = Hmac::<sha256::Hash>::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<Self, Self::Err> {
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<RpcAuth>,
}
/// 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::<u32>()
.map_err(|e| InternalBitcoindConfigError::CouldNotParseValue(e.to_string()))?;
let rpc_auth = prop
.get("rpcauth")
.map(|v| {
v.parse::<RpcAuth>().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");
}

View File

@ -64,7 +64,9 @@ impl Daemon for EmbeddedDaemon {
}
fn list_spend_txs(&self) -> Result<ListSpendResult, DaemonError> {
Ok(self.control()?.list_spend())
self.control()?
.list_spend(None)
.map_err(|e| DaemonError::Unexpected(e.to_string()))
}
fn list_confirmed_txs(

View File

@ -614,6 +614,7 @@ impl Step for InternalBitcoindStep {
rpc_port,
p2p_port,
prune: PRUNE_DEFAULT,
rpc_auth: None,
}
}
(Ok(_), Err(e)) | (Err(e), Ok(_)) => {