diff --git a/Cargo.lock b/Cargo.lock index a905bc06..67a9c483 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -142,18 +142,6 @@ version = "1.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" -[[package]] -name = "argon2" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072" -dependencies = [ - "base64ct", - "blake2", - "cpufeatures", - "password-hash", -] - [[package]] name = "arrayvec" version = "0.7.6" @@ -408,15 +396,6 @@ version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" -[[package]] -name = "blake2" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" -dependencies = [ - "digest 0.10.7", -] - [[package]] name = "block-buffer" version = "0.7.3" @@ -1721,7 +1700,6 @@ dependencies = [ name = "mostro" version = "0.17.0" dependencies = [ - "argon2", "axum", "bech32", "bitcoin", @@ -1739,8 +1717,6 @@ dependencies = [ "once_cell", "prost 0.14.1", "reqwest", - "rpassword", - "secrecy", "serde", "serde_json", "sqlx", @@ -2520,27 +2496,6 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "rpassword" -version = "7.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66d4c8b64f049c6721ec8ccec37ddfc3d641c4a7fca57e8f2a89de509c73df39" -dependencies = [ - "libc", - "rtoolbox", - "windows-sys 0.59.0", -] - -[[package]] -name = "rtoolbox" -version = "0.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7cc970b249fbe527d6e02e0a227762c9108b2f49d81094fe357ffc6d14d7f6f" -dependencies = [ - "libc", - "windows-sys 0.52.0", -] - [[package]] name = "rustc-hash" version = "2.1.1" @@ -2687,15 +2642,6 @@ dependencies = [ "cc", ] -[[package]] -name = "secrecy" -version = "0.10.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e891af845473308773346dc847b2c23ee78fe442e0472ac50e22a18a93d3ae5a" -dependencies = [ - "zeroize", -] - [[package]] name = "serde" version = "1.0.228" diff --git a/Cargo.toml b/Cargo.toml index a264090c..d3f42255 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -77,9 +77,6 @@ clap = { version = "4.5.45", features = ["derive"] } lnurl-rs = { version = "0.9.0", default-features = false, features = ["ureq"] } once_cell = "1.20.2" bitcoin = "0.32.5" -rpassword = "7.3.1" -argon2 = "0.5" -secrecy = "0.10.0" dirs = "6.0.0" clearscreen = "4.0.1" tonic = "0.14.2" diff --git a/docker/README.md b/docker/README.md index 65292aef..ebacc628 100644 --- a/docker/README.md +++ b/docker/README.md @@ -69,14 +69,12 @@ To build and run the Docker container using Docker Compose, follow these steps: make docker-up ``` - Or pass it inline: + Or set the variable on one line before `make docker-up`: ```sh MOSTRO_RELAY_LOCAL_PORT=7000 make docker-up ``` -5. **Note:** Database encryption has been removed. The `MOSTRO_DB_PASSWORD` environment variable (if set in `compose.yml`) is no longer used for the database; you can omit it. For more details about environment variables, see [ENV_VARIABLES.md](ENV_VARIABLES.md). - -6. Run the docker compose file: +5. Run the docker compose file: ```sh make docker-up diff --git a/docker/compose.yml b/docker/compose.yml index d2fd9673..7bb9f1d0 100644 --- a/docker/compose.yml +++ b/docker/compose.yml @@ -3,8 +3,6 @@ services: build: context: .. dockerfile: docker/Dockerfile - environment: - MOSTRO_DB_PASSWORD: ${MOSTRO_DB_PASSWORD-} volumes: - ./config:/config # settings.toml and mostro.db platform: linux/amd64 @@ -15,8 +13,6 @@ services: build: context: .. dockerfile: docker/dockerfile-startos - environment: - MOSTRO_DB_PASSWORD: ${MOSTRO_DB_PASSWORD-} platform: linux/amd64 networks: - default diff --git a/docs/RPC.md b/docs/RPC.md index 74be459b..a9aaf855 100644 --- a/docs/RPC.md +++ b/docs/RPC.md @@ -29,67 +29,84 @@ port = 50051 The RPC interface supports the following admin operations: ### 1. Cancel Order + Cancel an order as an admin. **Request:** + - `order_id`: UUID of the order to cancel - `request_id`: Optional request identifier **Response:** + - `success`: Boolean indicating operation success - `error_message`: Optional error message if operation failed ### 2. Settle Order + Settle a disputed order as an admin. **Request:** + - `order_id`: UUID of the order to settle - `request_id`: Optional request identifier **Response:** + - `success`: Boolean indicating operation success - `error_message`: Optional error message if operation failed ### 3. Add Solver + Add a new dispute solver. **Request:** + - `solver_pubkey`: Public key of the solver to add (in bech32 format) - `request_id`: Optional request identifier **Response:** + - `success`: Boolean indicating operation success - `error_message`: Optional error message if operation failed ### 4. Take Dispute + Take a dispute for resolution. **Request:** + - `dispute_id`: UUID of the dispute to take - `request_id`: Optional request identifier **Response:** + - `success`: Boolean indicating operation success - `error_message`: Optional error message if operation failed ### 5. Validate Database Password -Kept for backward compatibility. Database encryption has been removed; this RPC always succeeds and does not validate a password. + +Kept for backward compatibility with older clients. The SQLite database is **not** encrypted and this RPC does **not** validate any password; it always succeeds. **Request:** -- `password`: Ignored -- `request_id`: Optional request identifier + +- `password`: Ignored (kept in the protobuf for compatibility only) **Response:** + - `success`: Always `true` - `error_message`: Always `None` ### 6. Get Version + Retrieve the Mostro daemon version. **Request:** + - No parameters required **Response:** + - `version`: String containing the daemon version (from CARGO_PKG_VERSION) ## Protocol Details diff --git a/docs/RPC_RATE_LIMITING.md b/docs/RPC_RATE_LIMITING.md index 9d9f32d9..d0519d28 100644 --- a/docs/RPC_RATE_LIMITING.md +++ b/docs/RPC_RATE_LIMITING.md @@ -2,69 +2,47 @@ ## Overview -The `ValidateDbPassword` RPC endpoint is protected against brute-force attacks -with an in-memory rate limiter that tracks failed attempts per client IP. +The gRPC method **`ValidateDbPassword`** (protobuf RPC) is a **backward-compatibility** stub: the SQLite database is **not** encrypted, the request **`password`** field is **ignored**, and the response is always success after a per-IP gate. -## Problem +The implementation is **`validate_db_password`** in `src/rpc/service.rs`. It: -The `ValidateDbPassword` endpoint is kept for backward compatibility (database -encryption was removed, so it always succeeds). The rate limiter remains to -throttle abuse of this endpoint. +1. Resolves the client address and runs **`check_rate_limit`** on the shared in-memory **`RateLimiter`** (`src/rpc/rate_limiter.rs`). +2. Drops **`password`** on the floor (`let _ = req.password;`). +3. Calls **`record_success`** on the limiter for that IP (clears any tracked state for that key). +4. Returns **`ValidateDbPasswordResponse`** with `success: true`. -See [Issue #569](https://github.com/MostroP2P/mostro/issues/569) for full details. +**Important:** **`validate_db_password` does not call `record_failure`.** So exponential backoff and lockout described below are **generic `RateLimiter` capabilities** (used by unit tests and available if another call site ever records failures). They are **not** driven by repeated **`ValidateDbPassword`** calls with “wrong passwords,” because passwords are not validated. -## Implementation +See [Issue #569](https://github.com/MostroP2P/mostro/issues/569) for background. -### Rate Limiter (`src/rpc/rate_limiter.rs`) +## `ValidateDbPassword` ↔ code map -A lightweight, in-memory rate limiter keyed by client IP address. No external -dependencies required — uses only `tokio::sync::Mutex` and `std::collections::HashMap`. +| Concept | Where | +|--------|--------| +| RPC name | `ValidateDbPassword` in `proto/admin.proto` | +| Handler | `validate_db_password` in `src/rpc/service.rs` | +| Limiter | `password_rate_limiter: Arc` on `AdminServiceImpl` | +| Success path | `record_success(&remote_addr)` after ignoring `password` | -**Behavior:** +## Generic `RateLimiter` behavior (`src/rpc/rate_limiter.rs`) -| Failed Attempts | Response | -|----------------|----------| +The in-memory limiter is keyed by client IP. It exposes **`check_rate_limit`**, **`record_failure`**, and **`record_success`**. + +**When `record_failure` is used** (e.g. in unit tests, or a hypothetical future handler), the limiter can apply exponential backoff and lockout: + +| Failed attempts (`record_failure`) | Effect | +|-----------------------------------|--------| | 1st | Immediate + 1s delay | | 2nd | Immediate + 2s delay | | 3rd | Immediate + 4s delay | | 4th | Immediate + 8s delay | | 5th+ | **Locked out for 5 minutes** | -After a successful validation, the client's failure state is reset. - -### Integration (`src/rpc/service.rs`) - -The `validate_db_password` method now: - -1. Extracts the client's remote address from the gRPC request -2. Checks the rate limiter — returns `RESOURCE_EXHAUSTED` if locked out -3. Does not validate a password (database encryption was removed); always succeeds -4. On success: resets the client's failure state - -### Audit Logging - -All attempts are logged via `tracing`: - -- **Rate-limited requests:** `WARN` with client IP -- **Failed attempts:** `WARN` with client IP and attempt count -- **Lockouts:** `WARN` with client IP and lockout duration - -### Security Layers +After **`record_success`**, that IP’s failure state is cleared (see `record_success` in `rate_limiter.rs`). -This implementation addresses the issue's suggestions: +**Not exercised by `ValidateDbPassword` today:** the **`validate_db_password`** handler never invokes **`record_failure`**, so clients only exercising this RPC do not accumulate “failed attempts” through wrong passwords. -| Suggestion | Status | Notes | -|-----------|--------|-------| -| Rate limiting | ✅ | Per-IP tracking with exponential backoff | -| Exponential backoff | ✅ | 1s → 2s → 4s → 8s → lockout | -| Lockout | ✅ | 5-minute lockout after 5 failures | -| Audit logging | ✅ | All attempts logged via tracing | -| Localhost-only | ℹ️ | Default config already binds to `127.0.0.1` | -| Auth requirement | ℹ️ | Out of scope — would require session/API key infra | - -### Constants - -Configurable via constants in `src/rpc/rate_limiter.rs`: +### Constants (`src/rpc/rate_limiter.rs`) ```rust const MAX_ATTEMPTS: u32 = 5; @@ -72,20 +50,30 @@ const LOCKOUT_DURATION: Duration = Duration::from_secs(300); // 5 minutes const BASE_DELAY_MS: u64 = 1000; // 1 second ``` -### Thread Safety +### Thread safety -The rate limiter uses `tokio::sync::Mutex` for async-safe access. The lock is -dropped before applying the exponential backoff sleep to avoid holding it during -the delay. +The limiter uses `tokio::sync::Mutex`. The lock is dropped before the exponential backoff sleep inside **`record_failure`** so the mutex is not held across the delay. -## Testing +## Audit logging + +- **`validate_db_password`** logs receipt of the RPC at **INFO** (client IP). +- **`RateLimiter`** may emit **WARN** for backoff/lockout when **`check_rate_limit`** denies an IP that already has failure state, or when **`record_failure`** runs — paths that matter for **unit tests** and for **generic** use of the limiter, not for password validation on **`ValidateDbPassword`**. -Unit tests in `src/rpc/rate_limiter.rs` verify: +## Security layers (historical issue checklist) + +How the original issue’s ideas map to the codebase today: + +| Suggestion | Notes | +|-----------|--------| +| Per-IP rate limiting | **`check_rate_limit`** runs before the handler body. | +| Exponential backoff / lockout | Implemented inside **`RateLimiter`**; **not** triggered by **`ValidateDbPassword`** (no **`record_failure`**). | +| Audit logging | **tracing** in service + limiter. | +| Localhost-only | Default RPC bind **`127.0.0.1`** (see `settings.toml` / `docs/RPC.md`). | +| Strong auth | Out of scope for this stub; would need API keys or similar. | + +## Testing -- First attempt is always allowed -- Lockout triggers after `MAX_ATTEMPTS` failures -- Success resets the failure state -- Different IPs are tracked independently +Unit tests in **`src/rpc/rate_limiter.rs`** exercise **`record_failure`**, lockout, **`record_success`**, and eviction — they document **limiter** behavior, not password checking. ## Related diff --git a/docs/STARTUP_AND_CONFIG.md b/docs/STARTUP_AND_CONFIG.md index 22d5b814..c162372f 100644 --- a/docs/STARTUP_AND_CONFIG.md +++ b/docs/STARTUP_AND_CONFIG.md @@ -9,9 +9,7 @@ This guide explains Mostro’s boot sequence and configuration surfaces. ## Pre-Boot Initialization -**Lines 33-48 in src/main.rs**: - -Before settings initialization, the daemon performs: +Before settings initialization, the daemon performs (see `src/main.rs`): 1. **Screen clearing**: Clears terminal for clean output 2. **Logging setup**: @@ -86,8 +84,9 @@ Configuration is loaded from `~/.mostro/settings.toml` (template: `settings.tpl. ### Configuration Sections: **Database** (`src/config/types.rs:21-26`): -- `url` (String): Database connection URL - - Example: `"sqlite://mostro.db"` or `"postgres://user:pass@localhost/dbname"` +- `url` (String): Database connection URL (Mostro uses SQLite) + - Example (relative to the process working directory): `"sqlite://mostro.db"` + - Example (absolute path; use a real path — **do not** use `~`; SQLx does not expand tilde): `"sqlite:///home/youruser/.mostro/mostro.db"` - Default: `"sqlite://mostro.db"` **Nostr** (`src/config/types.rs:47-54`): @@ -148,25 +147,22 @@ Configuration is loaded from `~/.mostro/settings.toml` (template: `settings.tpl. ## Global Variables -**Source**: `src/config/mod.rs:26-48` +**Source**: `src/config/mod.rs` ```rust -// Settings and configuration pub static MOSTRO_CONFIG: OnceLock = OnceLock::new(); - -// Infrastructure connections pub static NOSTR_CLIENT: OnceLock = OnceLock::new(); pub static LN_STATUS: OnceLock = OnceLock::new(); pub static DB_POOL: OnceLock> = OnceLock::new(); -// Security (MOSTRO_DB_PASSWORD unused; database encryption was removed) -pub static MOSTRO_DB_PASSWORD: OnceLock = OnceLock::new(); - -// Message routing -pub static MESSAGE_QUEUES: LazyLock>>>> = - LazyLock::new(|| Arc::new(Mutex::new(HashMap::new()))); +pub static MESSAGE_QUEUES: LazyLock = + LazyLock::new(MessageQueues::default); ``` +(`MessageQueues` holds `Arc>` queues for order DMs, cant-do messages, rating events, and restore-session messages.) + +There is **no** database password or separate global for SQLite; the daemon opens the file URL from `[database]` in `settings.toml` only. + **Access patterns**: - `Settings::get_mostro()` → Mostro settings - `Settings::get_ln()` → Lightning settings diff --git a/examples/rpc_client.rs b/examples/rpc_client.rs index 520275db..dcc64483 100644 --- a/examples/rpc_client.rs +++ b/examples/rpc_client.rs @@ -21,10 +21,10 @@ async fn main() -> Result<(), Box> { .await?; let mut client = AdminServiceClient::new(channel); - // Example 0: ValidateDbPassword (backward compatibility; DB encryption removed, always succeeds) + // Example 0: ValidateDbPassword — backward-compat only; password is ignored by the server println!("Calling ValidateDbPassword (backward-compat endpoint)..."); let validate_request = tonic::Request::new(ValidateDbPasswordRequest { - password: std::env::var("MOSTRO_DB_TEST_PASSWORD").unwrap_or_default(), + password: String::new(), }); match client.validate_db_password(validate_request).await { diff --git a/proto/admin.proto b/proto/admin.proto index e1013042..620d0368 100644 --- a/proto/admin.proto +++ b/proto/admin.proto @@ -16,7 +16,7 @@ service AdminService { // Take a dispute for resolution rpc TakeDispute(TakeDisputeRequest) returns (TakeDisputeResponse); - // Validate database password (kept for backward compatibility; DB encryption removed) + // Backward compatibility only: password is ignored; SQLite is not encrypted rpc ValidateDbPassword(ValidateDbPasswordRequest) returns (ValidateDbPasswordResponse); // Get Mostro version @@ -71,7 +71,7 @@ message TakeDisputeResponse { optional string error_message = 2; } -// Validate database password (backward compatibility; no longer used for DB encryption) +// Backward compatibility: `password` is ignored (no DB encryption) message ValidateDbPasswordRequest { string password = 1; } diff --git a/src/util.rs b/src/util.rs index c8896df8..c2646df2 100644 --- a/src/util.rs +++ b/src/util.rs @@ -1274,12 +1274,12 @@ pub async fn notify_taker_reputation( false => order.master_buyer_pubkey.clone(), }; - let user_decrypted_key = match user { + let master_key = match user { Some(user) => user.to_string(), None => return Err(MostroCantDo(CantDoReason::InvalidPubkey)), }; - let reputation_data = match is_user_present(pool, user_decrypted_key).await { + let reputation_data = match is_user_present(pool, master_key).await { Ok(user) => { let now = Timestamp::now().as_u64(); UserInfo {