Skip to content

Commit 162f609

Browse files
committed
.
1 parent 55c81d2 commit 162f609

6 files changed

Lines changed: 76 additions & 42 deletions

File tree

devolutions-gateway/src/api/webapp.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ use axum::{Json, Router, http};
1010
use axum_extra::TypedHeader;
1111
use axum_extra::headers::{self, HeaderMapExt as _};
1212
use picky::key::PrivateKey;
13+
use secrecy::ExposeSecret as _;
1314
use tap::prelude::*;
1415
use tower_http::services::ServeFile;
1516
use uuid::Uuid;

devolutions-gateway/src/config.rs

Lines changed: 51 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ use camino::{Utf8Path, Utf8PathBuf};
99
use cfg_if::cfg_if;
1010
use picky::key::{PrivateKey, PublicKey};
1111
use picky::pem::Pem;
12-
use secrecy::SecretString;
12+
use secrecy::{ExposeSecret as _, SecretString};
1313
use tap::prelude::*;
1414
use tokio::sync::Notify;
1515
use tokio_rustls::rustls::pki_types;
@@ -197,7 +197,7 @@ pub struct Conf {
197197
pub debug: dto::DebugConf,
198198
}
199199

200-
#[derive(PartialEq, Debug, Clone)]
200+
#[derive(Debug, Clone)]
201201
pub struct WebAppConf {
202202
pub enabled: bool,
203203
pub authentication: WebAppAuth,
@@ -206,13 +206,13 @@ pub struct WebAppConf {
206206
pub static_root_path: std::path::PathBuf,
207207
}
208208

209-
#[derive(PartialEq, Eq, Debug, Clone)]
209+
#[derive(Debug, Clone)]
210210
pub enum WebAppAuth {
211211
Custom(HashMap<String, WebAppUser>),
212212
None,
213213
}
214214

215-
#[derive(PartialEq, Eq, Debug, Clone)]
215+
#[derive(Debug, Clone)]
216216
pub struct WebAppUser {
217217
pub name: String,
218218
/// Hash of the password, in the PHC string format
@@ -1256,7 +1256,8 @@ fn read_pfx_file(
12561256
use picky::x509::certificate::CertType;
12571257

12581258
let crypto_context = password
1259-
.map(|pwd| Pkcs12CryptoContext::new_with_password(pwd.expose_secret()))
1259+
.map(|secret| secret.expose_secret())
1260+
.map(Pkcs12CryptoContext::new_with_password)
12601261
.unwrap_or_else(Pkcs12CryptoContext::new_without_password);
12611262
let parsing_params = Pkcs12ParsingParams::default();
12621263

@@ -1577,6 +1578,8 @@ fn to_listener_urls(conf: &dto::ListenerConf, hostname: &str, auto_ipv6: bool) -
15771578
pub mod dto {
15781579
use std::collections::HashMap;
15791580

1581+
use secrecy::ExposeSecret as _;
1582+
15801583
use super::*;
15811584

15821585
/// Source of truth for Gateway configuration
@@ -1585,7 +1588,7 @@ pub mod dto {
15851588
/// and is not trying to be too smart.
15861589
///
15871590
/// Unstable options are subject to change
1588-
#[derive(PartialEq, Debug, Clone, Serialize, Deserialize)]
1591+
#[derive(Debug, Clone, Serialize, Deserialize)]
15891592
#[serde(rename_all = "PascalCase")]
15901593
pub struct ConfFile {
15911594
/// This Gateway unique ID (e.g.: 123e4567-e89b-12d3-a456-426614174000)
@@ -1626,7 +1629,10 @@ pub mod dto {
16261629
#[serde(alias = "PrivateKeyFile", skip_serializing_if = "Option::is_none")]
16271630
pub tls_private_key_file: Option<Utf8PathBuf>,
16281631
/// Password to use for decrypting the TLS private key
1629-
#[serde(skip_serializing_if = "Option::is_none")]
1632+
#[serde(
1633+
skip_serializing_if = "Option::is_none",
1634+
serialize_with = "serialize_opt_secret_string"
1635+
)]
16301636
pub tls_private_key_password: Option<SecretString>,
16311637
/// Subject name of the certificate to use for TLS
16321638
#[serde(skip_serializing_if = "Option::is_none")]
@@ -1661,8 +1667,11 @@ pub mod dto {
16611667
pub credssp_private_key_file: Option<Utf8PathBuf>,
16621668

16631669
/// Password to use for decrypting the CredSSP private key
1664-
#[serde(skip_serializing_if = "Option::is_none")]
1665-
pub credssp_private_key_password: Option<Password>,
1670+
#[serde(
1671+
skip_serializing_if = "Option::is_none",
1672+
serialize_with = "serialize_opt_secret_string"
1673+
)]
1674+
pub credssp_private_key_password: Option<SecretString>,
16661675

16671676
/// Listeners to launch at startup
16681677
#[serde(default, skip_serializing_if = "Vec::is_empty")]
@@ -1811,15 +1820,16 @@ pub mod dto {
18111820
}
18121821

18131822
/// Domain user credentials.
1814-
#[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize)]
1823+
#[derive(Debug, Clone, Serialize, Deserialize)]
18151824
pub struct DomainUser {
18161825
/// Username in FQDN format (e.g. "pw13@example.com").
18171826
///
18181827
/// **Note**: the user's domain part must match the internal KDC realm.
18191828
/// The KDC realm is derived from the gateway ID using the [KerberosServer::realm] method.
18201829
pub fqdn: String,
18211830
/// User password.
1822-
pub password: String,
1831+
#[serde(serialize_with = "serialize_secret_string")]
1832+
pub password: SecretString,
18231833
/// Salt for generating the user's key.
18241834
///
18251835
/// Usually, it is equal to `{REALM}{username}` (e.g. "EXAMPLEpw13").
@@ -1832,7 +1842,7 @@ pub mod dto {
18321842

18331843
Self {
18341844
username: fqdn,
1835-
password,
1845+
password: password.expose_secret().to_owned(),
18361846
salt,
18371847
}
18381848
}
@@ -1841,7 +1851,7 @@ pub mod dto {
18411851
/// Kerberos server config
18421852
///
18431853
/// This config is used to configure the Kerberos server during RDP proxying.
1844-
#[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize)]
1854+
#[derive(Debug, Clone, Serialize, Deserialize)]
18451855
pub struct KerberosServer {
18461856
/// Users credentials inside fake KDC.
18471857
pub users: Vec<DomainUser>,
@@ -1896,7 +1906,7 @@ pub mod dto {
18961906
}
18971907

18981908
/// The Kerberos credentials-injection configuration.
1899-
#[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize)]
1909+
#[derive(Debug, Clone, Serialize, Deserialize)]
19001910
pub struct KerberosConfig {
19011911
/// Kerberos server and KDC configuration.
19021912
pub kerberos_server: KerberosServer,
@@ -1910,7 +1920,7 @@ pub mod dto {
19101920
///
19111921
/// Note to developers: all options should be safe by default, never add an option
19121922
/// that needs to be overridden manually in order to be safe.
1913-
#[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize)]
1923+
#[derive(Debug, Clone, Serialize, Deserialize)]
19141924
pub struct DebugConf {
19151925
/// Dump received tokens using a `debug` statement
19161926
#[serde(default)]
@@ -1974,7 +1984,15 @@ pub mod dto {
19741984

19751985
impl DebugConf {
19761986
pub fn is_default(&self) -> bool {
1977-
Self::default().eq(self)
1987+
!self.dump_tokens
1988+
&& !self.disable_token_validation
1989+
&& self.override_kdc.is_none()
1990+
&& self.log_directives.is_none()
1991+
&& self.capture_path.is_none()
1992+
&& self.lib_xmf_path.is_none()
1993+
&& !self.enable_unstable
1994+
&& self.kerberos.is_none()
1995+
&& self.ws_keep_alive_interval == ws_keep_alive_interval_default_value()
19781996
}
19791997
}
19801998

@@ -2355,6 +2373,23 @@ pub mod dto {
23552373
}
23562374
}
23572375

2376+
fn serialize_secret_string<S>(value: &SecretString, serializer: S) -> Result<S::Ok, S::Error>
2377+
where
2378+
S: serde::Serializer,
2379+
{
2380+
serializer.serialize_str(value.expose_secret())
2381+
}
2382+
2383+
fn serialize_opt_secret_string<S>(value: &Option<SecretString>, serializer: S) -> Result<S::Ok, S::Error>
2384+
where
2385+
S: serde::Serializer,
2386+
{
2387+
match value {
2388+
Some(s) => serializer.serialize_str(s.expose_secret()),
2389+
None => serializer.serialize_none(),
2390+
}
2391+
}
2392+
23582393
impl ProxyConf {
23592394
/// Convert this DTO to the http-client-proxy ProxyConfig.
23602395
pub fn to_proxy_config(&self) -> http_client_proxy::ProxyConfig {

devolutions-gateway/src/credential/crypto.rs

Lines changed: 14 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@ use core::fmt;
1616
use std::sync::LazyLock;
1717

1818
use anyhow::Context as _;
19-
use chacha20poly1305::aead::{Aead, KeyInit, OsRng};
20-
use chacha20poly1305::{ChaCha20Poly1305, Key, Nonce};
19+
use chacha20poly1305::aead::{Aead, AeadCore, KeyInit, OsRng};
20+
use chacha20poly1305::{ChaCha20Poly1305, Nonce};
2121
use parking_lot::Mutex;
2222
use rand::RngCore as _;
2323
use secrets::SecretBox;
@@ -28,7 +28,7 @@ use zeroize::{Zeroize, ZeroizeOnDrop};
2828
/// Initialized lazily on first access. The key material is stored in memory
2929
/// protected by mlock/mprotect via libsodium's SecretBox, wrapped in a Mutex
3030
/// for thread-safe access.
31-
pub static MASTER_KEY: LazyLock<Mutex<MasterKeyManager>> = LazyLock::new(|| {
31+
pub(super) static MASTER_KEY: LazyLock<Mutex<MasterKeyManager>> = LazyLock::new(|| {
3232
Mutex::new(MasterKeyManager::new().expect("failed to initialize credential encryption master key"))
3333
});
3434

@@ -39,7 +39,7 @@ pub static MASTER_KEY: LazyLock<Mutex<MasterKeyManager>> = LazyLock::new(|| {
3939
/// - Protected (mprotect) with appropriate access controls
4040
/// - Excluded from core dumps
4141
/// - Zeroized on drop
42-
pub struct MasterKeyManager {
42+
pub(super) struct MasterKeyManager {
4343
// SecretBox provides mlock/mprotect for the key material.
4444
key_material: SecretBox<[u8; 32]>,
4545
}
@@ -64,37 +64,31 @@ impl MasterKeyManager {
6464
/// Encrypt a password using ChaCha20-Poly1305.
6565
///
6666
/// Returns the nonce and ciphertext (which includes the Poly1305 auth tag).
67-
pub fn encrypt(&self, plaintext: &str) -> anyhow::Result<EncryptedPassword> {
67+
pub(super) fn encrypt(&self, plaintext: &str) -> anyhow::Result<EncryptedPassword> {
6868
let key_ref = self.key_material.borrow();
69-
let cipher = ChaCha20Poly1305::new(Key::from_slice(key_ref.as_ref()));
69+
let cipher = ChaCha20Poly1305::new_from_slice(key_ref.as_ref()).expect("key is exactly 32 bytes");
7070

7171
// Generate random 96-bit nonce (12 bytes for ChaCha20-Poly1305).
72-
let mut nonce_bytes = [0u8; 12];
73-
OsRng.fill_bytes(&mut nonce_bytes);
74-
let nonce = Nonce::from_slice(&nonce_bytes);
72+
let nonce = ChaCha20Poly1305::generate_nonce(OsRng);
7573

7674
// Encrypt (ciphertext includes 16-byte Poly1305 tag).
7775
let ciphertext = cipher
78-
.encrypt(nonce, plaintext.as_bytes())
76+
.encrypt(&nonce, plaintext.as_bytes())
7977
.map_err(|e| anyhow::anyhow!("encryption failed: {e}"))?;
8078

81-
Ok(EncryptedPassword {
82-
nonce: nonce_bytes,
83-
ciphertext,
84-
})
79+
Ok(EncryptedPassword { nonce, ciphertext })
8580
}
8681

8782
/// Decrypt a password, returning a zeroize-on-drop wrapper.
8883
///
8984
/// The returned `DecryptedPassword` should have a short lifetime.
9085
/// Use it immediately and let it drop to zeroize the plaintext.
91-
pub fn decrypt(&self, encrypted: &EncryptedPassword) -> anyhow::Result<DecryptedPassword> {
86+
pub(super) fn decrypt(&self, encrypted: &EncryptedPassword) -> anyhow::Result<DecryptedPassword> {
9287
let key_ref = self.key_material.borrow();
93-
let cipher = ChaCha20Poly1305::new(Key::from_slice(key_ref.as_ref()));
94-
let nonce = Nonce::from_slice(&encrypted.nonce);
88+
let cipher = ChaCha20Poly1305::new_from_slice(key_ref.as_ref()).expect("key is exactly 32 bytes");
9589

9690
let plaintext_bytes = cipher
97-
.decrypt(nonce, encrypted.ciphertext.as_ref())
91+
.decrypt(&encrypted.nonce, encrypted.ciphertext.as_ref())
9892
.map_err(|e| anyhow::anyhow!("decryption failed: {e}"))?;
9993

10094
// Convert bytes to String.
@@ -113,7 +107,7 @@ impl MasterKeyManager {
113107
#[derive(Clone)]
114108
pub struct EncryptedPassword {
115109
/// 96-bit nonce (12 bytes) for ChaCha20-Poly1305.
116-
nonce: [u8; 12],
110+
nonce: Nonce,
117111

118112
/// Ciphertext + 128-bit auth tag (plaintext_len + 16 bytes).
119113
ciphertext: Vec<u8>,
@@ -151,6 +145,7 @@ impl fmt::Debug for DecryptedPassword {
151145
}
152146

153147
#[cfg(test)]
148+
#[expect(clippy::unwrap_used, reason = "test code, panics are expected")]
154149
mod tests {
155150
use super::*;
156151

devolutions-gateway/src/credential/mod.rs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,20 @@
11
mod crypto;
22

3-
pub use crypto::{DecryptedPassword, EncryptedPassword, MASTER_KEY};
4-
use secrecy::ExposeSecret;
3+
#[rustfmt::skip]
4+
pub use crypto::{DecryptedPassword, EncryptedPassword};
55

6-
use core::fmt;
76
use std::collections::HashMap;
87
use std::sync::Arc;
98

109
use anyhow::Context;
1110
use async_trait::async_trait;
1211
use devolutions_gateway_task::{ShutdownSignal, Task};
1312
use parking_lot::Mutex;
14-
use serde::{de, ser};
13+
use secrecy::ExposeSecret as _;
1514
use uuid::Uuid;
1615

16+
use self::crypto::MASTER_KEY;
17+
1718
/// Credential at the application protocol level
1819
#[derive(Debug, Clone)]
1920
pub enum AppCredential {

devolutions-gateway/src/rd_clean_path.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ use std::sync::Arc;
44

55
use anyhow::Context as _;
66
use ironrdp_connector::sspi;
7+
use secrecy::ExposeSecret as _;
78
use ironrdp_pdu::nego;
89
use ironrdp_rdcleanpath::RDCleanPathPdu;
910
use tap::prelude::*;
@@ -404,7 +405,7 @@ async fn handle_with_credential_injection(
404405
} = user;
405406

406407
// The username is in the FQDN format. Thus, the domain field can be empty.
407-
sspi::CredentialsBuffers::AuthIdentity(sspi::AuthIdentityBuffers::from_utf8(fqdn, "", password))
408+
sspi::CredentialsBuffers::AuthIdentity(sspi::AuthIdentityBuffers::from_utf8(fqdn, "", password.expose_secret()))
408409
});
409410

410411
Some(sspi::KerberosServerConfig {

devolutions-gateway/src/rdp_proxy.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ use std::sync::Arc;
33

44
use anyhow::Context as _;
55
use ironrdp_acceptor::credssp::CredsspProcessGenerator as CredsspServerProcessGenerator;
6+
use secrecy::ExposeSecret as _;
67
use ironrdp_connector::credssp::CredsspProcessGenerator as CredsspClientProcessGenerator;
78
use ironrdp_connector::sspi;
89
use ironrdp_connector::sspi::generator::{GeneratorState, NetworkRequest};
@@ -130,8 +131,8 @@ where
130131
salt: _,
131132
} = user;
132133

133-
// The username is in the FQDN format. Thus, the domain field can be empty.
134-
sspi::CredentialsBuffers::AuthIdentity(sspi::AuthIdentityBuffers::from_utf8(fqdn, "", password))
134+
// The username is an the FQDN format. Thus, the domain field can be empty.
135+
sspi::CredentialsBuffers::AuthIdentity(sspi::AuthIdentityBuffers::from_utf8(fqdn, "", password.expose_secret()))
135136
});
136137

137138
Some(sspi::KerberosServerConfig {

0 commit comments

Comments
 (0)