diff --git a/Cargo.lock b/Cargo.lock index dfc231dfa73..dfd9c9e3769 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -678,6 +678,19 @@ version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" +[[package]] +name = "bcrypt" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b1866ecef4f2d06a0bb77880015fdf2b89e25a1c2e5addacb87e459c86dc67e" +dependencies = [ + "base64 0.22.1", + "blowfish", + "getrandom", + "subtle", + "zeroize", +] + [[package]] name = "bdk-macros" version = "0.6.0" @@ -984,6 +997,16 @@ dependencies = [ "piper", ] +[[package]] +name = "blowfish" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e412e2cd0f2b2d93e02543ceae7917b3c70331573df19ee046bcbc35e45e87d7" +dependencies = [ + "byteorder", + "cipher", +] + [[package]] name = "bls12_381" version = "0.8.0" @@ -2614,6 +2637,7 @@ dependencies = [ "async-stream", "async-trait", "axum 0.7.9", + "bcrypt", "bitcoin", "clap", "erased-serde", @@ -3165,6 +3189,7 @@ dependencies = [ "anyhow", "async-stream", "async-trait", + "bcrypt", "bitcoin", "bitcoincore-rpc", "fedimint-api-client", diff --git a/Cargo.toml b/Cargo.toml index 9ed7537eb72..a696fe5e4e8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -92,6 +92,7 @@ async-trait = "0.1.83" axum = "0.7.9" base64 = "0.22.1" base64-url = "3.0.0" +bcrypt = "0.16.0" bincode = "1.3.3" bitcoin = { version = "0.32.4", features = ["serde"] } bitcoincore-rpc = "0.19.0" diff --git a/devimint/src/gatewayd.rs b/devimint/src/gatewayd.rs index f3e516762d3..48a454bfce2 100644 --- a/devimint/src/gatewayd.rs +++ b/devimint/src/gatewayd.rs @@ -162,19 +162,6 @@ impl Gatewayd { ) } - pub fn change_password(&self, old_password: &str, new_password: &str) -> Command { - cmd!( - crate::util::get_gateway_cli_path(), - "--rpcpassword", - old_password, - "-a", - &self.addr, - "set-configuration", - "--password", - new_password, - ) - } - pub async fn get_info(&self) -> Result { retry( "Getting gateway info via gateway-cli info", diff --git a/devimint/src/vars.rs b/devimint/src/vars.rs index 48e76a051e6..18d3e00d5ed 100644 --- a/devimint/src/vars.rs +++ b/devimint/src/vars.rs @@ -166,9 +166,13 @@ declare_vars! { FM_LND_TLS_CERT: PathBuf = FM_LND_DIR.join("tls.cert"); env: "FM_LND_TLS_CERT"; FM_LND_MACAROON: PathBuf = FM_LND_DIR.join("data/chain/bitcoin/regtest/admin.macaroon"); env: "FM_LND_MACAROON"; + // TODO(support:v0.5): Remove this. It was used prior to `FM_GATEWAY_BCRYPT_PASSWORD_HASH` to provide a plaintext password to the gateway. FM_GATEWAY_PASSWORD: String = "theresnosecondbest"; env: "FM_GATEWAY_PASSWORD"; - // Enable to us to make an unbounded number of payments + // Bcrypt hash of "theresnosecondbest" with a cost of 10. + FM_GATEWAY_BCRYPT_PASSWORD_HASH: String = "$2y$10$Q/UTDeO84VGG1mRncxw.Nubqyi/HsNRJ40k0TSexFy9eVess1yi/u"; env: "FM_GATEWAY_BCRYPT_PASSWORD_HASH"; + + // Enable to us to make an unbounded number of payments FM_DEFAULT_GATEWAY_FEES: String = "0,0"; env: "FM_DEFAULT_GATEWAY_FEES"; FM_GATEWAY_SKIP_WAIT_FOR_SYNC: String = "1"; env: "FM_GATEWAY_SKIP_WAIT_FOR_SYNC"; diff --git a/fedimint-testing/Cargo.toml b/fedimint-testing/Cargo.toml index 9ac6447d8fc..a0c3646ed3e 100644 --- a/fedimint-testing/Cargo.toml +++ b/fedimint-testing/Cargo.toml @@ -23,6 +23,7 @@ path = "src/lib.rs" anyhow = { workspace = true } async-stream = { workspace = true } async-trait = { workspace = true } +bcrypt = { workspace = true } bitcoin = { workspace = true } bitcoincore-rpc = { workspace = true } fedimint-api-client = { workspace = true } diff --git a/fedimint-testing/src/fixtures.rs b/fedimint-testing/src/fixtures.rs index 1cc934e3b84..6970fcec39d 100644 --- a/fedimint-testing/src/fixtures.rs +++ b/fedimint-testing/src/fixtures.rs @@ -1,5 +1,6 @@ use std::env; use std::net::SocketAddr; +use std::str::FromStr; use std::sync::Arc; use std::time::Duration; @@ -206,7 +207,10 @@ impl Fixtures { client_builder, listen, address.clone(), - Some(DEFAULT_GATEWAY_PASSWORD.to_string()), + bcrypt::HashParts::from_str( + &bcrypt::hash(DEFAULT_GATEWAY_PASSWORD, bcrypt::DEFAULT_COST).unwrap(), + ) + .unwrap(), Some(bitcoin::Network::Regtest), RoutingFees { base_msat: 0, diff --git a/gateway/cli/src/general_commands.rs b/gateway/cli/src/general_commands.rs index 27c12ba1167..dab5274133e 100644 --- a/gateway/cli/src/general_commands.rs +++ b/gateway/cli/src/general_commands.rs @@ -73,9 +73,6 @@ pub enum GeneralCommands { Seed, /// Set or update the gateway configuration. SetConfiguration { - #[clap(long)] - password: Option, - #[clap(long)] num_route_hints: Option, @@ -168,7 +165,6 @@ impl GeneralCommands { print_response(response); } Self::SetConfiguration { - password, num_route_hints, routing_fees, network, @@ -178,7 +174,6 @@ impl GeneralCommands { .map(|input| input.into_iter().map(Into::into).collect()); create_client() .set_configuration(SetConfigurationPayload { - password, num_route_hints, routing_fees, network, diff --git a/gateway/integration_tests/src/main.rs b/gateway/integration_tests/src/main.rs index a61ad6e8914..6cb2c85ab81 100644 --- a/gateway/integration_tests/src/main.rs +++ b/gateway/integration_tests/src/main.rs @@ -388,21 +388,12 @@ async fn config_test(gw_type: LightningNodeType) -> anyhow::Result<()> { ); info!("Verified per-federation routing fees changed"); - // Change password for gateway - gw.change_password("theresnosecondbest", "newpassword") - .run() - .await?; - get_gateway_info(gw) - .await - .expect_err("Expected info to return error since the password has changed"); - gw.change_password("newpassword", "theresnosecondbest") - .run() - .await?; + // Try to change the network while connected to a lightning node cmd!(gw, "set-configuration", "--network", "regtest") .run() .await .expect_err("Cannot change the network while connected to a federation"); - info!("Verified password change and network cannot be changed."); + info!("Verified network cannot be changed."); // Get the federation's config and verify it parses correctly let config_val = cmd!(gw, "config", "--federation-id", fed_id) diff --git a/gateway/ln-gateway/Cargo.toml b/gateway/ln-gateway/Cargo.toml index e860eb23a41..c1595d62da1 100644 --- a/gateway/ln-gateway/Cargo.toml +++ b/gateway/ln-gateway/Cargo.toml @@ -30,6 +30,7 @@ aquamarine = { workspace = true } async-stream = { workspace = true } async-trait = { workspace = true } axum = { version = "0.7.9", features = [ "json" ] } +bcrypt = { workspace = true } bitcoin = { workspace = true } clap = { workspace = true } erased-serde = { workspace = true } diff --git a/gateway/ln-gateway/src/config.rs b/gateway/ln-gateway/src/config.rs index 7a69dfdc862..ff9c8aeb639 100644 --- a/gateway/ln-gateway/src/config.rs +++ b/gateway/ln-gateway/src/config.rs @@ -32,9 +32,9 @@ pub struct GatewayOpts { #[arg(long = "api-addr", env = envs::FM_GATEWAY_API_ADDR_ENV)] api_addr: SafeUrl, - /// Gateway webserver authentication password - #[arg(long = "password", env = envs::FM_GATEWAY_PASSWORD_ENV)] - password: Option, + /// Gateway webserver authentication bcrypt password hash + #[arg(long = "bcrypt-password-hash", env = envs::FM_GATEWAY_BCRYPT_PASSWORD_HASH_ENV)] + bcrypt_password_hash: String, /// Bitcoin network this gateway will be running on #[arg(long = "network", env = envs::FM_GATEWAY_NETWORK_ENV)] @@ -68,10 +68,13 @@ impl GatewayOpts { api_addr = self.api_addr, ) })?; + + let bcrypt_password_hash = bcrypt::HashParts::from_str(&self.bcrypt_password_hash)?; + Ok(GatewayParameters { listen: self.listen, versioned_api, - password: self.password.clone(), + bcrypt_password_hash, network: self.network, num_route_hints: self.num_route_hints, fees: self.default_fees.clone(), @@ -86,11 +89,11 @@ impl GatewayOpts { /// /// If `GatewayConfiguration is set in the database, that takes precedence and /// the optional parameters will have no affect. -#[derive(Clone, Debug)] +#[derive(Debug)] pub struct GatewayParameters { pub listen: SocketAddr, pub versioned_api: SafeUrl, - pub password: Option, + pub bcrypt_password_hash: bcrypt::HashParts, pub network: Option, pub num_route_hints: u32, pub fees: Option, diff --git a/gateway/ln-gateway/src/db.rs b/gateway/ln-gateway/src/db.rs index 22f35bb1f1a..3164029fcb9 100644 --- a/gateway/ln-gateway/src/db.rs +++ b/gateway/ln-gateway/src/db.rs @@ -1,6 +1,6 @@ use std::collections::BTreeMap; -use bitcoin::hashes::sha256; +use bitcoin::hashes::{sha256, Hash}; use fedimint_api_client::api::net::Connector; use fedimint_core::config::FederationId; use fedimint_core::db::{ @@ -22,8 +22,6 @@ use serde::{Deserialize, Serialize}; use strum::IntoEnumIterator; use strum_macros::EnumIter; -use crate::rpc::rpc_server::hash_password; - pub trait GatewayDbtxNcExt { async fn save_federation_config(&mut self, config: &FederationConfig); async fn load_federation_configs_v0(&mut self) -> BTreeMap; @@ -330,10 +328,10 @@ struct GatewayConfigurationV0 { } #[derive(Debug, Clone, Eq, PartialEq, Encodable, Decodable)] -pub struct GatewayConfigurationKey; +pub struct GatewayConfigurationKeyV1; #[derive(Debug, Clone, Eq, PartialEq, Encodable, Decodable, Serialize, Deserialize)] -pub struct GatewayConfiguration { +pub struct GatewayConfigurationV1 { pub hashed_password: sha256::Hash, pub num_route_hints: u32, #[serde(with = "serde_routing_fees")] @@ -342,12 +340,29 @@ pub struct GatewayConfiguration { pub password_salt: [u8; 16], } +#[derive(Debug, Clone, Eq, PartialEq, Encodable, Decodable)] +pub struct GatewayConfigurationKey; + +#[derive(Debug, Clone, Eq, PartialEq, Encodable, Decodable, Serialize, Deserialize)] +pub struct GatewayConfiguration { + pub num_route_hints: u32, + #[serde(with = "serde_routing_fees")] + pub routing_fees: RoutingFees, + pub network: NetworkLegacyEncodingWrapper, +} + impl_db_record!( key = GatewayConfigurationKeyV0, value = GatewayConfigurationV0, db_prefix = DbKeyPrefix::GatewayConfiguration, ); +impl_db_record!( + key = GatewayConfigurationKeyV1, + value = GatewayConfigurationV1, + db_prefix = DbKeyPrefix::GatewayConfiguration, +); + impl_db_record!( key = GatewayConfigurationKey, value = GatewayConfiguration, @@ -378,24 +393,34 @@ pub fn get_gatewayd_database_migrations() -> BTreeMap = BTreeMap::new(); migrations.insert(DatabaseVersion(0), |ctx| migrate_to_v1(ctx).boxed()); migrations.insert(DatabaseVersion(1), |ctx| migrate_to_v2(ctx).boxed()); + migrations.insert(DatabaseVersion(2), |ctx| migrate_to_v3(ctx).boxed()); migrations } async fn migrate_to_v1(mut ctx: MigrationContext<'_>) -> Result<(), anyhow::Error> { + /// Creates a password hash by appending a 4 byte salt to the plaintext + /// password. + fn hash_password(plaintext_password: &str, salt: [u8; 16]) -> sha256::Hash { + let mut bytes = Vec::new(); + bytes.append(&mut plaintext_password.consensus_encode_to_vec()); + bytes.append(&mut salt.consensus_encode_to_vec()); + sha256::Hash::hash(&bytes) + } + let mut dbtx = ctx.dbtx(); // If there is no old gateway configuration, there is nothing to do. if let Some(old_gateway_config) = dbtx.remove_entry(&GatewayConfigurationKeyV0).await { let password_salt: [u8; 16] = rand::thread_rng().gen(); let hashed_password = hash_password(&old_gateway_config.password, password_salt); - let new_gateway_config = GatewayConfiguration { + let new_gateway_config = GatewayConfigurationV1 { hashed_password, num_route_hints: old_gateway_config.num_route_hints, routing_fees: old_gateway_config.routing_fees, network: old_gateway_config.network, password_salt, }; - dbtx.insert_entry(&GatewayConfigurationKey, &new_gateway_config) + dbtx.insert_entry(&GatewayConfigurationKeyV1, &new_gateway_config) .await; } @@ -430,6 +455,23 @@ async fn migrate_to_v2(mut ctx: MigrationContext<'_>) -> Result<(), anyhow::Erro Ok(()) } +async fn migrate_to_v3(mut ctx: MigrationContext<'_>) -> Result<(), anyhow::Error> { + let mut dbtx = ctx.dbtx(); + + // If there is no old gateway configuration, there is nothing to do. + if let Some(old_gateway_config) = dbtx.remove_entry(&GatewayConfigurationKeyV1).await { + let new_gateway_config = GatewayConfiguration { + num_route_hints: old_gateway_config.num_route_hints, + routing_fees: old_gateway_config.routing_fees, + network: old_gateway_config.network, + }; + dbtx.insert_entry(&GatewayConfigurationKey, &new_gateway_config) + .await; + } + + Ok(()) +} + #[derive(Debug, Encodable, Decodable)] struct RegisteredIncomingContractKey(pub PaymentImage); diff --git a/gateway/ln-gateway/src/envs.rs b/gateway/ln-gateway/src/envs.rs index 55017c05ed0..61ff2842bc0 100644 --- a/gateway/ln-gateway/src/envs.rs +++ b/gateway/ln-gateway/src/envs.rs @@ -9,9 +9,8 @@ pub const FM_GATEWAY_LISTEN_ADDR_ENV: &str = "FM_GATEWAY_LISTEN_ADDR"; /// requests to the gateway. pub const FM_GATEWAY_API_ADDR_ENV: &str = "FM_GATEWAY_API_ADDR"; -/// Environment variable that specifies the password. This is only applied if -/// there is no password set, otherwise it is ignored. -pub const FM_GATEWAY_PASSWORD_ENV: &str = "FM_GATEWAY_PASSWORD"; +/// Environment variable that specifies the bcrypt password hash. +pub const FM_GATEWAY_BCRYPT_PASSWORD_HASH_ENV: &str = "FM_GATEWAY_BCRYPT_PASSWORD_HASH"; /// Environment variable that specifies that Bitcoin network that the gateway /// should use. Must match the network of the Lightning node. diff --git a/gateway/ln-gateway/src/lib.rs b/gateway/ln-gateway/src/lib.rs index 833078e30bf..98022ae1402 100644 --- a/gateway/ln-gateway/src/lib.rs +++ b/gateway/ln-gateway/src/lib.rs @@ -40,7 +40,7 @@ use clap::Parser; use client::GatewayClientBuilder; use config::GatewayOpts; pub use config::GatewayParameters; -use db::{GatewayConfiguration, GatewayConfigurationKey, GatewayDbtxNcExt}; +use db::{GatewayConfiguration, GatewayDbtxNcExt}; use error::FederationNotConnected; use events::ALL_GATEWAY_EVENTS; use federation_manager::FederationManager; @@ -87,7 +87,7 @@ use lightning::{ PaymentAction, }; use lightning_invoice::{Bolt11Invoice, RoutingFees}; -use rand::{thread_rng, Rng}; +use rand::thread_rng; use rpc::{ CloseChannelsWithPeerPayload, CreateInvoiceForOperatorPayload, FederationInfo, GatewayFedConfig, GatewayInfo, LeaveFedPayload, MnemonicResponse, OpenChannelPayload, @@ -105,7 +105,7 @@ use crate::envs::FM_GATEWAY_MNEMONIC_ENV; use crate::error::{AdminGatewayError, LNv1Error, LNv2Error, PublicGatewayError}; use crate::gateway_module_v2::GatewayClientModuleV2; use crate::lightning::{GatewayLightningBuilder, LightningContext, LightningMode, RouteHtlcStream}; -use crate::rpc::rpc_server::{hash_password, run_webserver}; +use crate::rpc::rpc_server::run_webserver; use crate::rpc::{ BackupPayload, ConnectFedPayload, DepositAddressPayload, FederationBalanceInfo, GatewayBalances, WithdrawPayload, @@ -157,36 +157,29 @@ const DEFAULT_MODULE_KINDS: [(ModuleInstanceId, &ModuleKind); 2] = [ /// graph LR /// classDef virtual fill:#fff,stroke-dasharray: 5 5 /// -/// Initializing -- begin intercepting lightning payments --> Connected -/// Initializing -- gateway needs config --> Configuring -/// Configuring -- configuration set --> Connected +/// Disconnected -- establish lightning connection --> Connected /// Connected -- load federation clients --> Running /// Connected -- not synced to chain --> Syncing /// Syncing -- load federation clients --> Running /// Running -- disconnected from lightning node --> Disconnected /// Running -- shutdown initiated --> ShuttingDown -/// Disconnected -- re-established lightning connection --> Connected /// ``` #[derive(Clone, Debug)] pub enum GatewayState { - Initializing, - Configuring, + Disconnected, Syncing, Connected, Running { lightning_context: LightningContext }, - Disconnected, ShuttingDown { lightning_context: LightningContext }, } impl Display for GatewayState { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { match self { - GatewayState::Initializing => write!(f, "Initializing"), - GatewayState::Configuring => write!(f, "Configuring"), + GatewayState::Disconnected => write!(f, "Disconnected"), GatewayState::Syncing => write!(f, "Syncing"), GatewayState::Connected => write!(f, "Connected"), GatewayState::Running { .. } => write!(f, "Running"), - GatewayState::Disconnected => write!(f, "Disconnected"), GatewayState::ShuttingDown { .. } => write!(f, "ShuttingDown"), } } @@ -209,7 +202,7 @@ pub struct Gateway { lightning_builder: Arc, /// The gateway's current configuration - gateway_config: Arc>>, + gateway_config: Arc>, /// The current state of the Gateway. state: Arc>, @@ -236,6 +229,10 @@ pub struct Gateway { /// The task group for all tasks related to the gateway. task_group: TaskGroup, + + /// The bcrypt password hash used to authenticate the gateway. + /// This is an `Arc` because `bcrypt::HashParts` does not implement `Clone`. + bcrypt_password_hash: Arc, } impl std::fmt::Debug for Gateway { @@ -262,7 +259,7 @@ impl Gateway { client_builder: GatewayClientBuilder, listen: SocketAddr, api_addr: SafeUrl, - cli_password: Option, + bcrypt_password_hash: bcrypt::HashParts, network: Option, fees: RoutingFees, num_route_hints: u32, @@ -278,7 +275,7 @@ impl Gateway { GatewayParameters { listen, versioned_api, - password: cli_password, + bcrypt_password_hash, num_route_hints, fees: Some(GatewayFee(fees)), network, @@ -340,7 +337,7 @@ impl Gateway { gateway_parameters, gateway_db, client_builder, - GatewayState::Initializing, + GatewayState::Disconnected, ) .await } @@ -383,6 +380,7 @@ impl Gateway { listen: gateway_parameters.listen, lightning_module_mode: gateway_parameters.lightning_module_mode, task_group, + bcrypt_password_hash: Arc::new(gateway_parameters.bcrypt_password_hash), }) } @@ -402,7 +400,7 @@ impl Gateway { &self.versioned_api } - pub async fn clone_gateway_config(&self) -> Option { + pub async fn clone_gateway_config(&self) -> GatewayConfiguration { self.gateway_config.read().await.clone() } @@ -521,15 +519,7 @@ impl Gateway { } }; - let gateway_config = if let Some(config) = self.clone_gateway_config().await { - config - } else { - self.set_gateway_state(GatewayState::Configuring).await; - info!("Waiting for gateway to be configured..."); - self.gateway_db - .wait_key_exists(&GatewayConfigurationKey) - .await - }; + let gateway_config = self.clone_gateway_config().await; if gateway_config.network.0 != lightning_network { warn!( @@ -541,7 +531,6 @@ impl Gateway { lightning_network ); self.handle_set_configuration_msg(SetConfigurationPayload { - password: None, network: Some(lightning_network), num_route_hints: None, routing_fees: None, @@ -803,10 +792,7 @@ impl Gateway { // `GatewayConfiguration` should always exist in the database when we are in the // `Running` state. - let gateway_config = self - .clone_gateway_config() - .await - .expect("Gateway configuration should be set"); + let gateway_config = self.clone_gateway_config().await; let dbtx = self.gateway_db.begin_transaction_nc().await; let federations = self @@ -1137,10 +1123,7 @@ impl Gateway { // `GatewayConfiguration` should always exist in the database when we are in the // `Running` state. - let gateway_config = self - .clone_gateway_config() - .await - .expect("Gateway configuration should be set"); + let gateway_config = self.clone_gateway_config().await; // The gateway deterministically assigns a unique identifier (u64) to each // federation connected. @@ -1295,39 +1278,26 @@ impl Gateway { pub async fn handle_set_configuration_msg( &self, SetConfigurationPayload { - password, network, num_route_hints, routing_fees, per_federation_routing_fees, }: SetConfigurationPayload, ) -> AdminResult<()> { - let gw_state = self.get_state().await; - let lightning_network = match gw_state { - GatewayState::Running { lightning_context } => { - if network.is_some() && network != Some(lightning_context.lightning_network) { + if let Some(network) = network { + if let GatewayState::Running { lightning_context } = self.get_state().await { + if network != lightning_context.lightning_network { return Err(AdminGatewayError::GatewayConfigurationError( "Cannot change network while connected to a lightning node".to_string(), )); } - lightning_context.lightning_network } - // In the case the gateway is not yet running and not yet connected to a lightning node, - // we start off with a default network configuration. This default gets replaced later - // when the gateway connects to a lightning node, or when a user sets a different - // configuration - _ => DEFAULT_NETWORK, - }; + } let mut dbtx = self.gateway_db.begin_transaction().await; - let prev_gateway_config = self.clone_gateway_config().await; - let new_gateway_config = if let Some(mut prev_config) = prev_gateway_config { - if let Some(password) = password.as_ref() { - let hashed_password = hash_password(password, prev_config.password_salt); - prev_config.hashed_password = hashed_password; - } - + let mut prev_config = self.clone_gateway_config().await; + let new_gateway_config = { if let Some(network) = network { if !self.federation_manager.read().await.is_empty() { return Err(AdminGatewayError::GatewayConfigurationError( @@ -1349,20 +1319,6 @@ impl Gateway { } prev_config - } else { - let password = password.ok_or(AdminGatewayError::GatewayConfigurationError( - "The password field is required when initially configuring the gateway".to_string(), - ))?; - let password_salt: [u8; 16] = rand::thread_rng().gen(); - let hashed_password = hash_password(&password, password_salt); - - GatewayConfiguration { - hashed_password, - network: NetworkLegacyEncodingWrapper(lightning_network), - num_route_hints: DEFAULT_NUM_ROUTE_HINTS, - routing_fees: DEFAULT_FEES, - password_salt, - } }; dbtx.set_gateway_config(&new_gateway_config).await; @@ -1405,7 +1361,7 @@ impl Gateway { dbtx.commit_tx().await; let mut curr_gateway_config = self.gateway_config.write().await; - *curr_gateway_config = Some(new_gateway_config.clone()); + *curr_gateway_config = new_gateway_config.clone(); info!("Set GatewayConfiguration successfully."); @@ -1738,17 +1694,14 @@ impl Gateway { async fn get_gateway_configuration( gateway_db: Database, gateway_parameters: &GatewayParameters, - ) -> Option { + ) -> GatewayConfiguration { let mut dbtx = gateway_db.begin_transaction_nc().await; // Always use the gateway configuration from the database if it exists. if let Some(gateway_config) = dbtx.load_gateway_config().await { - return Some(gateway_config); + return gateway_config; } - // If the password is not provided, return None - let password = gateway_parameters.password.as_ref()?; - // If the DB does not have the gateway configuration, we can construct one from // the provided password (required) and the defaults. // Use gateway parameters provided by the environment or CLI @@ -1759,17 +1712,11 @@ impl Gateway { .unwrap_or(GatewayFee(DEFAULT_FEES)); let network = NetworkLegacyEncodingWrapper(gateway_parameters.network.unwrap_or(DEFAULT_NETWORK)); - let password_salt: [u8; 16] = rand::thread_rng().gen(); - let hashed_password = hash_password(password, password_salt); - let gateway_config = GatewayConfiguration { - hashed_password, + GatewayConfiguration { network, num_route_hints, routing_fees: routing_fees.0, - password_salt, - }; - - Some(gateway_config) + } } /// Retrieves a `ClientHandleArc` from the Gateway's in memory structures @@ -1871,21 +1818,17 @@ impl Gateway { self.task_group.spawn_cancellable("register clients", async move { loop { let gateway_config = gateway.clone_gateway_config().await; - if let Some(gateway_config) = gateway_config { - let gateway_state = gateway.get_state().await; - if let GatewayState::Running { .. } = &gateway_state { - let mut dbtx = gateway.gateway_db.begin_transaction_nc().await; - let all_federations_configs: Vec<_> = dbtx.load_federation_configs().await.into_iter().collect(); - gateway.register_federations(&gateway_config, &all_federations_configs, ®ister_task_group).await; - } else { - // We need to retry more often if the gateway is not in the Running state - const NOT_RUNNING_RETRY: Duration = Duration::from_secs(10); - info!("Will not register federation yet because gateway still not in Running state. Current state: {gateway_state:?}. Will keep waiting, next retry in {NOT_RUNNING_RETRY:?}..."); - sleep(NOT_RUNNING_RETRY).await; - continue; - } + let gateway_state = gateway.get_state().await; + if let GatewayState::Running { .. } = &gateway_state { + let mut dbtx = gateway.gateway_db.begin_transaction_nc().await; + let all_federations_configs: Vec<_> = dbtx.load_federation_configs().await.into_iter().collect(); + gateway.register_federations(&gateway_config, &all_federations_configs, ®ister_task_group).await; } else { - warn!("Cannot register clients because gateway configuration is not set."); + // We need to retry more often if the gateway is not in the Running state + const NOT_RUNNING_RETRY: Duration = Duration::from_secs(10); + info!("Will not register federation yet because gateway still not in Running state. Current state: {gateway_state:?}. Will keep waiting, next retry in {NOT_RUNNING_RETRY:?}..."); + sleep(NOT_RUNNING_RETRY).await; + continue; } // Allow a 15% buffer of the TTL before the re-registering gateway diff --git a/gateway/ln-gateway/src/rpc/mod.rs b/gateway/ln-gateway/src/rpc/mod.rs index 1d8d1f05b85..a80b3e93c88 100644 --- a/gateway/ln-gateway/src/rpc/mod.rs +++ b/gateway/ln-gateway/src/rpc/mod.rs @@ -175,7 +175,6 @@ impl FromStr for FederationRoutingFees { #[derive(Debug, Serialize, Deserialize, Clone)] pub struct SetConfigurationPayload { - pub password: Option, pub num_route_hints: Option, pub routing_fees: Option, pub network: Option, diff --git a/gateway/ln-gateway/src/rpc/rpc_server.rs b/gateway/ln-gateway/src/rpc/rpc_server.rs index deec3aa1ab7..21d86508c9d 100644 --- a/gateway/ln-gateway/src/rpc/rpc_server.rs +++ b/gateway/ln-gateway/src/rpc/rpc_server.rs @@ -6,9 +6,7 @@ use axum::middleware::{self, Next}; use axum::response::IntoResponse; use axum::routing::{get, post}; use axum::{Extension, Json, Router}; -use bitcoin::hashes::{sha256, Hash}; use fedimint_core::config::FederationId; -use fedimint_core::encoding::Encodable; use fedimint_core::task::TaskGroup; use fedimint_ln_common::gateway_endpoint_constants::{ GET_GATEWAY_ID_ENDPOINT, PAY_INVOICE_ENDPOINT, @@ -92,50 +90,10 @@ async fn auth_middleware( request: Request, next: Next, ) -> Result { - // These routes are not available unless the gateway's configuration is set. - let gateway_config = gateway - .clone_gateway_config() - .await - .ok_or(StatusCode::NOT_FOUND)?; - let gateway_hashed_password = gateway_config.hashed_password; - let password_salt = gateway_config.password_salt; - authenticate(gateway_hashed_password, password_salt, request, next).await -} - -/// Middleware to authenticate an incoming request. Routes that are -/// authenticated with this middleware are un-authenticated if the gateway has -/// not yet been configured. After the gateway is configured, this middleware -/// enforces that a Bearer token must be supplied in the Authorization header. -async fn auth_after_config_middleware( - Extension(gateway): Extension>, - request: Request, - next: Next, -) -> Result { - // If the gateway's config has not been set, allow the request to continue, so - // that the gateway can be configured - let gateway_config = gateway.clone_gateway_config().await; - if gateway_config.is_none() { - return Ok(next.run(request).await); - } - - // Otherwise, validate that the Bearer token matches the gateway's hashed - // password - let gateway_config = gateway_config.expect("Already validated the gateway config is not none"); - let gateway_hashed_password = gateway_config.hashed_password; - let password_salt = gateway_config.password_salt; - authenticate(gateway_hashed_password, password_salt, request, next).await -} - -/// Validate that the Bearer token matches the gateway's hashed password -async fn authenticate( - gateway_hashed_password: sha256::Hash, - password_salt: [u8; 16], - request: Request, - next: Next, -) -> Result { let token = extract_bearer_token(&request)?; - let hashed_password = hash_password(&token, password_salt); - if gateway_hashed_password == hashed_password { + if bcrypt::verify(token, &gateway.bcrypt_password_hash.to_string()) + .expect("Bcrypt hash is valid since we just stringified it") + { return Ok(next.run(request).await); } @@ -180,8 +138,8 @@ fn v1_routes(gateway: Arc, task_group: TaskGroup) -> Router { public_routes = public_routes.merge(lnv2_routes()); } - // Authenticated, public routes used for gateway administration - let always_authenticated_routes = Router::new() + // Authenticated routes used for gateway administration + let authenticated_routes = Router::new() .route(ADDRESS_ENDPOINT, post(address)) .route(WITHDRAW_ENDPOINT, post(withdraw)) .route(CONNECT_FED_ENDPOINT, post(connect_fed)) @@ -208,36 +166,21 @@ fn v1_routes(gateway: Arc, task_group: TaskGroup) -> Router { .route(MNEMONIC_ENDPOINT, get(mnemonic)) .route(STOP_ENDPOINT, get(stop)) .route(PAYMENT_LOG_ENDPOINT, post(payment_log)) - .layer(middleware::from_fn(auth_middleware)); - - // Routes that are un-authenticated before gateway configuration, then become - // authenticated after a password has been set. - let authenticated_after_config_routes = Router::new() .route(SET_CONFIGURATION_ENDPOINT, post(set_configuration)) .route(CONFIGURATION_ENDPOINT, post(configuration)) // FIXME: deprecated >= 0.3.0 .route(GATEWAY_INFO_POST_ENDPOINT, post(handle_post_info)) .route(GATEWAY_INFO_ENDPOINT, get(info)) - .layer(middleware::from_fn(auth_after_config_middleware)); + .layer(middleware::from_fn(auth_middleware)); Router::new() .merge(public_routes) - .merge(always_authenticated_routes) - .merge(authenticated_after_config_routes) + .merge(authenticated_routes) .layer(Extension(gateway)) .layer(Extension(task_group)) .layer(CorsLayer::permissive()) } -/// Creates a password hash by appending a 4 byte salt to the plaintext -/// password. -pub fn hash_password(plaintext_password: &str, salt: [u8; 16]) -> sha256::Hash { - let mut bytes = Vec::new(); - bytes.append(&mut plaintext_password.consensus_encode_to_vec()); - bytes.append(&mut salt.consensus_encode_to_vec()); - sha256::Hash::hash(&bytes) -} - /// Display high-level information about the Gateway // FIXME: deprecated >= 0.3.0 // This endpoint exists only to remain backwards-compatible with the original POST endpoint diff --git a/gateway/ln-gateway/tests/tests.rs b/gateway/ln-gateway/tests/tests.rs index 7341dec2d6c..1fb386124c2 100644 --- a/gateway/ln-gateway/tests/tests.rs +++ b/gateway/ln-gateway/tests/tests.rs @@ -266,7 +266,6 @@ async fn test_gateway_enforces_fees() -> anyhow::Result<()> { let fee = "10,10000".to_string(); let federation_fee = FederationRoutingFees::from_str(&fee)?; let set_configuration_payload = SetConfigurationPayload { - password: None, num_route_hints: None, routing_fees: None, network: None, @@ -826,7 +825,6 @@ async fn test_gateway_executes_swaps_between_connected_federations() -> anyhow:: // setting specific routing fees for fed1 let fed_routing_fees = FederationRoutingFees::from_str("10,10000")?; let set_configuration_payload = SetConfigurationPayload { - password: None, num_route_hints: None, routing_fees: None, network: None,