Skip to content

Commit 6edc63b

Browse files
committed
fix(freedesktop-rs#508): redact credentials without breaking the public API
1 parent c37899a commit 6edc63b

24 files changed

Lines changed: 778 additions & 454 deletions

Cargo.lock

Lines changed: 0 additions & 21 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,4 +33,3 @@ nmrs = { path = "nmrs", version = "3.0" }
3333
async-trait = "0.1.89"
3434
bitflags = "2.13.0"
3535
tokio = { version = "1.52.3", features = ["rt-multi-thread", "macros", "sync", "time", "process"] }
36-
zeroize = { version = "1.9.0", features = ["aarch64", "derive", "std"] }

nmrs/CHANGELOG.md

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,11 @@ All notable changes to the `nmrs` crate will be documented in this file.
44

55
## [Unreleased]
66

7-
### Added
8-
9-
- `Passphrase` keeps Wi-Fi, OpenVPN, and WireGuard secrets redacted from debug
10-
output and zeroizes their owned buffers when dropped. ([#508](https://github.com/freedesktop-rs/nmrs/pull/508))
11-
127
### Changed
138

14-
- **Breaking:** Wi-Fi, OpenVPN, and WireGuard secret fields now use
15-
`Passphrase` instead of `String`. ([#508](https://github.com/freedesktop-rs/nmrs/pull/508))
9+
- Credential-bearing Wi-Fi, EAP, OpenVPN, and WireGuard types now redact
10+
passwords and private keys from debug output, and connection tracing no
11+
longer logs raw settings, without changing public field types. ([#508](https://github.com/freedesktop-rs/nmrs/pull/508))
1612

1713
### Fixed
1814

nmrs/Cargo.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ base64.workspace = true
2525
tokio.workspace = true
2626
async-trait.workspace = true
2727
bitflags.workspace = true
28-
zeroize.workspace = true
2928

3029
[package.metadata.docs.rs]
3130
all-features = true

nmrs/examples/custom_timeouts.rs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,7 @@ async fn main() -> nmrs::Result<()> {
3131
"MyNetwork",
3232
None,
3333
WifiSecurity::WpaPsk {
34-
psk: std::env::var("WIFI_PASSWORD")
35-
.unwrap_or_else(|_| "password".to_string())
36-
.into(),
34+
psk: std::env::var("WIFI_PASSWORD").unwrap_or_else(|_| "password".to_string()),
3735
},
3836
)
3937
.await?;

nmrs/src/api/builders/mod.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -46,13 +46,13 @@
4646
//!
4747
//! ```rust
4848
//! use nmrs::builders::{build_ethernet_connection, build_wifi_connection};
49-
//! use nmrs::{ConnectionOptions, Passphrase, WifiSecurity};
49+
//! use nmrs::{ConnectionOptions, WifiSecurity};
5050
//!
5151
//! let opts = ConnectionOptions::new(true).with_priority(10);
5252
//!
5353
//! let wifi = build_wifi_connection(
5454
//! "MyNetwork",
55-
//! &WifiSecurity::WpaPsk { psk: Passphrase::new("password".to_string()) },
55+
//! &WifiSecurity::WpaPsk { psk: "password".into() },
5656
//! &opts,
5757
//! );
5858
//! let eth = build_ethernet_connection("eth0", &opts);
@@ -62,12 +62,12 @@
6262
//!
6363
//! ```no_run
6464
//! use nmrs::builders::{WifiConnectionBuilder, WifiMode};
65-
//! use nmrs::{NetworkManager, Passphrase};
65+
//! use nmrs::NetworkManager;
6666
//!
6767
//! # async fn example() -> nmrs::Result<()> {
6868
//! let nm = NetworkManager::new().await?;
6969
//! let settings = WifiConnectionBuilder::new("Hotspot")
70-
//! .wpa_psk(Passphrase::new("password".to_string()))
70+
//! .wpa_psk("password")
7171
//! .mode(WifiMode::Ap)
7272
//! .ipv4_shared()
7373
//! .build();
@@ -91,7 +91,7 @@
9191
//! ).with_persistent_keepalive(25);
9292
//!
9393
//! let settings = WireGuardBuilder::new("MyVPN")
94-
//! .private_key("YBk6X3pP8KjKz7+HFWzVHNqL3qTZq8hX9VxFQJ4zVmM=".to_string())
94+
//! .private_key("YBk6X3pP8KjKz7+HFWzVHNqL3qTZq8hX9VxFQJ4zVmM=")
9595
//! .address("10.0.0.2/24")
9696
//! .add_peer(peer)
9797
//! .dns(vec!["1.1.1.1".into()])

nmrs/src/api/builders/openvpn_builder.rs

Lines changed: 80 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,13 @@
99
//! struct. Use [`super::vpn::build_openvpn_connection`] to convert it into
1010
//! NetworkManager connection settings.
1111
12-
use std::path::Path;
12+
use std::{fmt, path::Path};
1313

1414
use uuid::Uuid;
1515

16-
use crate::Passphrase;
1716
use crate::api::models::{
1817
ConnectionError, OpenVpnAuthType, OpenVpnCompression, OpenVpnConfig, OpenVpnProxy, VpnRoute,
19-
vpn_route_from_parser,
18+
Redacted, redact_option, vpn_route_from_parser,
2019
};
2120
use crate::core::ovpn_parser::parser::{self, CertSource, OvpnFile};
2221
use crate::util::cert_store::store_inline_cert;
@@ -48,7 +47,6 @@ use crate::util::validation::validate_connection_name;
4847
/// .expect("Failed to build OpenVPN config");
4948
/// ```
5049
#[non_exhaustive]
51-
#[derive(Debug)]
5250
pub struct OpenVpnBuilder {
5351
name: String,
5452
remote: Option<String>,
@@ -63,9 +61,9 @@ pub struct OpenVpnBuilder {
6361
ca_cert: Option<String>,
6462
client_cert: Option<String>,
6563
client_key: Option<String>,
66-
key_password: Option<Passphrase>,
64+
key_password: Option<String>,
6765
username: Option<String>,
68-
password: Option<Passphrase>,
66+
password: Option<String>,
6967
compression: Option<OpenVpnCompression>,
7068
proxy: Option<OpenVpnProxy>,
7169
tls_auth_key: Option<String>,
@@ -90,6 +88,52 @@ pub struct OpenVpnBuilder {
9088
ncp_disable: bool,
9189
}
9290

91+
impl fmt::Debug for OpenVpnBuilder {
92+
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
93+
formatter
94+
.debug_struct("OpenVpnBuilder")
95+
.field("name", &self.name)
96+
.field("remote", &self.remote)
97+
.field("port", &self.port)
98+
.field("tcp", &self.tcp)
99+
.field("auth_type", &self.auth_type)
100+
.field("auth", &self.auth)
101+
.field("cipher", &self.cipher)
102+
.field("dns", &self.dns)
103+
.field("mtu", &self.mtu)
104+
.field("uuid", &self.uuid)
105+
.field("ca_cert", &self.ca_cert)
106+
.field("client_cert", &self.client_cert)
107+
.field("client_key", &self.client_key)
108+
.field("key_password", &redact_option(&self.key_password))
109+
.field("username", &self.username)
110+
.field("password", &redact_option(&self.password))
111+
.field("compression", &self.compression)
112+
.field("proxy", &self.proxy)
113+
.field("tls_auth_key", &self.tls_auth_key)
114+
.field("tls_auth_direction", &self.tls_auth_direction)
115+
.field("tls_crypt", &self.tls_crypt)
116+
.field("tls_crypt_v2", &self.tls_crypt_v2)
117+
.field("tls_version_min", &self.tls_version_min)
118+
.field("tls_version_max", &self.tls_version_max)
119+
.field("tls_cipher", &self.tls_cipher)
120+
.field("remote_cert_tls", &self.remote_cert_tls)
121+
.field("verify_x509_name", &self.verify_x509_name)
122+
.field("crl_verify", &self.crl_verify)
123+
.field("redirect_gateway", &self.redirect_gateway)
124+
.field("routes", &self.routes)
125+
.field("ping", &self.ping)
126+
.field("ping_exit", &self.ping_exit)
127+
.field("ping_restart", &self.ping_restart)
128+
.field("reneg_seconds", &self.reneg_seconds)
129+
.field("connect_timeout", &self.connect_timeout)
130+
.field("data_ciphers", &self.data_ciphers)
131+
.field("data_ciphers_fallback", &self.data_ciphers_fallback)
132+
.field("ncp_disable", &self.ncp_disable)
133+
.finish()
134+
}
135+
}
136+
93137
impl OpenVpnBuilder {
94138
/// Creates a new OpenVPN connection builder.
95139
#[must_use]
@@ -390,7 +434,7 @@ impl OpenVpnBuilder {
390434

391435
/// Sets the password for an encrypted private key.
392436
#[must_use]
393-
pub fn key_password(mut self, password: impl Into<Passphrase>) -> Self {
437+
pub fn key_password(mut self, password: impl Into<String>) -> Self {
394438
self.key_password = Some(password.into());
395439
self
396440
}
@@ -404,7 +448,7 @@ impl OpenVpnBuilder {
404448

405449
/// Sets the password for password authentication.
406450
#[must_use]
407-
pub fn password(mut self, password: impl Into<Passphrase>) -> Self {
451+
pub fn password(mut self, password: impl Into<String>) -> Self {
408452
self.password = Some(password.into());
409453
self
410454
}
@@ -1187,4 +1231,32 @@ key /etc/openvpn/client.key
11871231

11881232
let _ = std::fs::remove_dir_all(&dir);
11891233
}
1234+
1235+
#[test]
1236+
fn debug_output_redacts_passwords() {
1237+
let builder = OpenVpnBuilder::new("vpn")
1238+
.key_password("private-key-password")
1239+
.password("vpn-password")
1240+
.proxy(OpenVpnProxy::Http {
1241+
server: "proxy.example.com".into(),
1242+
port: 8080,
1243+
username: Some("proxy-user".into()),
1244+
password: Some("proxy-password".into()),
1245+
retry: true,
1246+
});
1247+
1248+
let output = format!("{builder:?}");
1249+
1250+
assert!(output.contains("[REDACTED]"));
1251+
for secret in [
1252+
"private-key-password",
1253+
"vpn-password",
1254+
"proxy-password",
1255+
] {
1256+
assert!(
1257+
!output.contains(secret),
1258+
"debug output exposed secret {secret:?}: {output}"
1259+
);
1260+
}
1261+
}
11901262
}

nmrs/src/api/builders/vpn.rs

Lines changed: 13 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
//! ).with_persistent_keepalive(25);
2828
//!
2929
//! let settings = WireGuardBuilder::new("MyVPN")
30-
//! .private_key("YBk6X3pP8KjKz7+HFWzVHNqL3qTZq8hX9VxFQJ4zVmM=".to_string())
30+
//! .private_key("YBk6X3pP8KjKz7+HFWzVHNqL3qTZq8hX9VxFQJ4zVmM=")
3131
//! .address("10.0.0.2/24")
3232
//! .add_peer(peer)
3333
//! .dns(vec!["1.1.1.1".into()])
@@ -53,7 +53,7 @@
5353
//! VpnKind::WireGuard,
5454
//! "MyVPN",
5555
//! "vpn.example.com:51820",
56-
//! "YBk6X3pP8KjKz7+HFWzVHNqL3qTZq8hX9VxFQJ4zVmM=".to_string(),
56+
//! "YBk6X3pP8KjKz7+HFWzVHNqL3qTZq8hX9VxFQJ4zVmM=",
5757
//! "10.0.0.2/24",
5858
//! vec![peer],
5959
//! ).with_dns(vec!["1.1.1.1".into()]);
@@ -71,7 +71,7 @@ use zvariant::{Dict, Value, signature};
7171
use super::wireguard_builder::WireGuardBuilder;
7272
use crate::api::models::{
7373
ConnectionError, ConnectionOptions, OpenVpnAuthType, OpenVpnCompression, OpenVpnConfig,
74-
OpenVpnProxy, Passphrase, VpnCredentials,
74+
OpenVpnProxy, VpnCredentials,
7575
};
7676

7777
/// Builds WireGuard VPN connection settings.
@@ -94,7 +94,7 @@ pub fn build_wireguard_connection(
9494
opts: &ConnectionOptions,
9595
) -> Result<HashMap<&'static str, HashMap<&'static str, Value<'static>>>, ConnectionError> {
9696
let mut builder = WireGuardBuilder::new(&creds.name)
97-
.private_key(creds.private_key.clone())
97+
.private_key(&creds.private_key)
9898
.address(&creds.address)
9999
.add_peers(creds.peers.iter().cloned())
100100
.options(opts);
@@ -141,16 +141,6 @@ fn push_opt_str(out: &mut Vec<(String, String)>, key: &str, value: Option<&Strin
141141
}
142142
}
143143

144-
/// Pushes a plaintext copy of a secret onto `out` if `value` is `Some`.
145-
///
146-
/// NetworkManager's VPN settings dictionary owns its strings, so the copy
147-
/// cannot retain the zeroization behavior of [`Passphrase`].
148-
fn push_opt_secret(out: &mut Vec<(String, String)>, key: &str, value: Option<&Passphrase>) {
149-
if let Some(value) = value {
150-
out.push((key.to_string(), value.expose_secret().to_owned()));
151-
}
152-
}
153-
154144
/// Pushes `(key, value.to_string())` onto `out` if `value` is `Some`.
155145
///
156146
/// Used for numeric/boolean OpenVPN options that NetworkManager stores as
@@ -311,7 +301,7 @@ pub fn build_openvpn_connection(
311301
vpn_data.push(("http-proxy-username".into(), u.clone()));
312302
}
313303
if let Some(p) = password {
314-
vpn_data.push(("http-proxy-password".into(), p.expose_secret().to_owned()));
304+
vpn_data.push(("http-proxy-password".into(), p.clone()));
315305
}
316306
}
317307
OpenVpnProxy::Socks {
@@ -338,8 +328,8 @@ pub fn build_openvpn_connection(
338328
let data_dict = string_pairs_to_dict(vpn_data)?;
339329

340330
let mut vpn_secrets: Vec<(String, String)> = Vec::new();
341-
push_opt_secret(&mut vpn_secrets, "password", config.password.as_ref());
342-
push_opt_secret(&mut vpn_secrets, "cert-pass", config.key_password.as_ref());
331+
push_opt_str(&mut vpn_secrets, "password", config.password.as_ref());
332+
push_opt_str(&mut vpn_secrets, "cert-pass", config.key_password.as_ref());
343333

344334
let mut vpn: HashMap<&'static str, Value<'static>> = HashMap::new();
345335
vpn.insert(
@@ -390,12 +380,11 @@ pub fn build_openvpn_connection(
390380

391381
Ok(settings)
392382
}
393-
394383
#[cfg(test)]
395384
mod tests {
396385
use super::*;
397-
use crate::{
398-
OpenVpnCompression, OpenVpnConfig, OpenVpnProxy, Passphrase, VpnKind, WireGuardPeer,
386+
use crate::api::models::{
387+
OpenVpnCompression, OpenVpnConfig, OpenVpnProxy, VpnKind, WireGuardPeer,
399388
};
400389

401390
fn create_test_credentials() -> VpnCredentials {
@@ -410,7 +399,7 @@ mod tests {
410399
VpnKind::WireGuard,
411400
"TestVPN",
412401
"vpn.example.com:51820",
413-
"YBk6X3pP8KjKz7+HFWzVHNqL3qTZq8hX9VxFQJ4zVmM=".to_string(),
402+
"YBk6X3pP8KjKz7+HFWzVHNqL3qTZq8hX9VxFQJ4zVmM=",
414403
"10.0.0.2/24",
415404
vec![peer],
416405
)
@@ -532,7 +521,7 @@ mod tests {
532521
"peer2.example.com:51821",
533522
vec!["192.168.0.0/16".into()],
534523
)
535-
.with_preshared_key("PSKABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklm=".to_string());
524+
.with_preshared_key("PSKABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklm=");
536525

537526
creds.peers.push(extra_peer);
538527
let opts = create_test_options();
@@ -726,9 +715,7 @@ mod tests {
726715
#[test]
727716
fn peer_with_preshared_key() {
728717
let mut creds = create_test_credentials();
729-
creds.peers[0].preshared_key = Some(Passphrase::new(
730-
"PSKABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklm=".to_string(),
731-
));
718+
creds.peers[0].preshared_key = Some("PSKABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklm=".into());
732719
let opts = create_test_options();
733720

734721
let settings = build_wireguard_connection(&creds, &opts).unwrap();
@@ -1133,7 +1120,7 @@ mod tests {
11331120
let config = create_openvpn_config()
11341121
.with_auth_type(OpenVpnAuthType::Password)
11351122
.with_username("user")
1136-
.with_password(Passphrase::new("secret".to_string()));
1123+
.with_password("secret");
11371124
let opts = create_test_options();
11381125
let settings = build_openvpn_connection(&config, &opts).unwrap();
11391126
let vpn = settings.get("vpn").unwrap();

0 commit comments

Comments
 (0)