diff --git a/README.md b/README.md index f61fefe19..6d53769d2 100644 --- a/README.md +++ b/README.md @@ -20,12 +20,16 @@ immediately, without going through the acceptance testing process of our quality ### From sources -Ensure that you have [the Rust toolchain installed][install_rust], then clone this repository and run: +Ensure that you have [the Rust toolchain installed][install_rust] and [libsodium][libsodium] installed on your system, then clone this repository and run: ```shell cargo install --path ./devolutions-gateway ``` +> **Note:** `libsodium` is required as a native dependency for in-memory credential protection. +> On Windows, it is vendored automatically via vcpkg. +> On Linux and macOS, install it using your system package manager (e.g., `apt install libsodium-dev` or `brew install libsodium`). + ## Configuration Devolutions Gateway is configured using a JSON document. @@ -339,6 +343,7 @@ See the dedicated [README.md file](./.github/workflows/README.md) in the `workfl [official_website]: https://devolutions.net/gateway/download/ [github_release]: https://github.com/Devolutions/devolutions-gateway/releases [install_rust]: https://www.rust-lang.org/tools/install +[libsodium]: https://libsodium.org/ [psmodule]: https://www.powershellgallery.com/packages/DevolutionsGateway/ [rustls]: https://crates.io/crates/rustls [microsoft_tls]: https://learn.microsoft.com/en-us/windows-server/security/tls/tls-registry-settings diff --git a/devolutions-gateway/src/api/preflight.rs b/devolutions-gateway/src/api/preflight.rs index afa2e1eea..24929b5f1 100644 --- a/devolutions-gateway/src/api/preflight.rs +++ b/devolutions-gateway/src/api/preflight.rs @@ -11,7 +11,7 @@ use uuid::Uuid; use crate::DgwState; use crate::config::Conf; -use crate::credential::CredentialStoreHandle; +use crate::credential::{CredentialStoreHandle, InsertError}; use crate::extract::PreflightScope; use crate::http::HttpError; use crate::session::SessionMessageSender; @@ -340,7 +340,14 @@ async fn handle_operation( let previous_entry = credential_store .insert(token, mapping, time_to_live) .inspect_err(|error| warn!(%operation.id, error = format!("{error:#}"), "Failed to insert credentials")) - .map_err(|e| PreflightError::new(PreflightAlertStatus::InternalServerError, format!("{e:#}")))?; + .map_err(|e| match e { + InsertError::InvalidToken(_) => { + PreflightError::new(PreflightAlertStatus::InvalidParams, format!("{e:#}")) + } + InsertError::Internal(_) => { + PreflightError::new(PreflightAlertStatus::InternalServerError, format!("{e:#}")) + } + })?; if previous_entry.is_some() { outputs.push(PreflightOutput { diff --git a/devolutions-gateway/src/credential/mod.rs b/devolutions-gateway/src/credential/mod.rs index 9bd1114da..792e8bb1e 100644 --- a/devolutions-gateway/src/credential/mod.rs +++ b/devolutions-gateway/src/credential/mod.rs @@ -4,6 +4,7 @@ mod crypto; pub use crypto::{DecryptedPassword, EncryptedPassword}; use std::collections::HashMap; +use std::fmt; use std::sync::Arc; use anyhow::Context; @@ -15,6 +16,28 @@ use uuid::Uuid; use self::crypto::MASTER_KEY; +/// Error returned by [`CredentialStoreHandle::insert`]. +#[derive(Debug)] +pub enum InsertError { + /// The provided token is invalid (e.g., missing or malformed JTI). + /// + /// This is a client-side error: the caller supplied bad input. + InvalidToken(anyhow::Error), + /// An internal error occurred (e.g., encryption failure). + Internal(anyhow::Error), +} + +impl fmt::Display for InsertError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::InvalidToken(e) => e.fmt(f), + Self::Internal(e) => e.fmt(f), + } + } +} + +impl std::error::Error for InsertError {} + /// Credential at the application protocol level #[derive(Debug, Clone)] pub enum AppCredential { @@ -112,8 +135,11 @@ impl CredentialStoreHandle { token: String, mapping: Option, time_to_live: time::Duration, - ) -> anyhow::Result> { - let mapping = mapping.map(CleartextAppCredentialMapping::encrypt).transpose()?; + ) -> Result, InsertError> { + let mapping = mapping + .map(CleartextAppCredentialMapping::encrypt) + .transpose() + .map_err(InsertError::Internal)?; self.0.lock().insert(token, mapping, time_to_live) } @@ -148,8 +174,10 @@ impl CredentialStore { token: String, mapping: Option, time_to_live: time::Duration, - ) -> anyhow::Result> { - let jti = crate::token::extract_jti(&token).context("failed to extract token ID")?; + ) -> Result, InsertError> { + let jti = crate::token::extract_jti(&token) + .context("failed to extract token ID") + .map_err(InsertError::InvalidToken)?; let entry = CredentialEntry { token, diff --git a/devolutions-gateway/src/rd_clean_path.rs b/devolutions-gateway/src/rd_clean_path.rs index 10ff4cd5b..6d4614b5e 100644 --- a/devolutions-gateway/src/rd_clean_path.rs +++ b/devolutions-gateway/src/rd_clean_path.rs @@ -4,9 +4,9 @@ use std::sync::Arc; use anyhow::Context as _; use ironrdp_connector::sspi; -use secrecy::ExposeSecret as _; use ironrdp_pdu::nego; use ironrdp_rdcleanpath::RDCleanPathPdu; +use secrecy::ExposeSecret as _; use tap::prelude::*; use thiserror::Error; use tokio::io::{AsyncRead, AsyncReadExt as _, AsyncWrite, AsyncWriteExt as _}; @@ -405,7 +405,11 @@ async fn handle_with_credential_injection( } = user; // The username is in the FQDN format. Thus, the domain field can be empty. - sspi::CredentialsBuffers::AuthIdentity(sspi::AuthIdentityBuffers::from_utf8(fqdn, "", password.expose_secret())) + sspi::CredentialsBuffers::AuthIdentity(sspi::AuthIdentityBuffers::from_utf8( + fqdn, + "", + password.expose_secret(), + )) }); Some(sspi::KerberosServerConfig { diff --git a/devolutions-gateway/src/rdp_proxy.rs b/devolutions-gateway/src/rdp_proxy.rs index c8cfda6ea..b3dc466a7 100644 --- a/devolutions-gateway/src/rdp_proxy.rs +++ b/devolutions-gateway/src/rdp_proxy.rs @@ -3,11 +3,11 @@ use std::sync::Arc; use anyhow::Context as _; use ironrdp_acceptor::credssp::CredsspProcessGenerator as CredsspServerProcessGenerator; -use secrecy::ExposeSecret as _; use ironrdp_connector::credssp::CredsspProcessGenerator as CredsspClientProcessGenerator; use ironrdp_connector::sspi; use ironrdp_connector::sspi::generator::{GeneratorState, NetworkRequest}; use ironrdp_pdu::{mcs, nego, x224}; +use secrecy::ExposeSecret as _; use tokio::io::{AsyncRead, AsyncWrite, AsyncWriteExt}; use typed_builder::TypedBuilder; @@ -131,8 +131,12 @@ where salt: _, } = user; - // The username is an the FQDN format. Thus, the domain field can be empty. - sspi::CredentialsBuffers::AuthIdentity(sspi::AuthIdentityBuffers::from_utf8(fqdn, "", password.expose_secret())) + // The username is in the FQDN format. Thus, the domain field can be empty. + sspi::CredentialsBuffers::AuthIdentity(sspi::AuthIdentityBuffers::from_utf8( + fqdn, + "", + password.expose_secret(), + )) }); Some(sspi::KerberosServerConfig {