Skip to content

Commit 60ad39d

Browse files
authored
Security: Use SecureString for secure password handling (#263)
1 parent caed754 commit 60ad39d

File tree

5 files changed

+133
-9
lines changed

5 files changed

+133
-9
lines changed

README.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,13 @@
3030
* **Observable**: Configure up to [TRACE level logging](https://hivemq.github.io/hivemq-mqtt-client-dotnet/docs/how-to/debug) for package internals.
3131
* **Fast**: Optimized & benchmarked. See the benchmark results [here](https://github.com/hivemq/hivemq-mqtt-client-dotnet/blob/main/Benchmarks/ClientBenchmarkApp/README.md).
3232

33+
### 🔒 Security
34+
* **Secure Password Storage**: Passwords are stored using `SecureString` to prevent exposure in memory dumps and process memory. Use `WithPassword(SecureString)` for enhanced security.
35+
* **Memory-Safe Password Handling**: Temporary password strings are automatically cleared from memory after use, ensuring no sensitive data persists in memory.
36+
* **Backward Compatibility**: Existing code using string passwords continues to work while being automatically converted to secure storage internally.
37+
* **TLS/SSL Support**: Full support for encrypted connections with configurable certificate validation and custom certificate handling.
38+
* **X.509 Certificate Authentication**: Complete support for client certificate authentication with secure private key handling.
39+
3340
### 🏝️ Ease of Use
3441
* **Easy to Use**: Smart defaults, excellent interfaces and intelligent automation makes implementing a breeze.
3542
* **Easy Integration**: Simple and intuitive API makes it easy to integrate with your .NET applications.
@@ -55,7 +62,7 @@ MQTT is an [open standard protocol](https://mqtt.org) for publishing and consumi
5562

5663
This client library is used to publish and consume messages over MQTT. So you can get a the temperature from a remote sensor, send a control message to a factory robot, tunnel WhatsApp messages to a Twitter account or anything else you can imagine.
5764

58-
This is the client library that speaks with an MQTT broker that delivers messages to their final destination.
65+
This is the client library that speaks with an MQTT broker that delivers messages to their final destination.
5966

6067
Need a broker? Sign up for a free broker at [HiveMQ Cloud](https://www.hivemq.com/mqtt-cloud-broker/) and be up and running in a couple minutes. Connect up to 100 devices - no credit card required.
6168

Source/HiveMQtt/Client/HiveMQClientOptionsBuilder.cs

Lines changed: 66 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
*/
1616
namespace HiveMQtt.Client;
1717

18+
using System.Security;
1819
using System.Security.Cryptography.X509Certificates;
1920
using HiveMQtt.Client.Options;
2021

@@ -468,7 +469,55 @@ public HiveMQClientOptionsBuilder WithUserName(string username)
468469
}
469470

470471
/// <summary>
471-
/// Sets the password.
472+
/// Sets the password using a SecureString for enhanced security.
473+
/// <para>
474+
/// The Password is an optional parameter that can be included in the CONNECT packet during the
475+
/// connection establishment process. It is used for client authentication in conjunction with the
476+
/// Username or other authentication mechanisms.
477+
/// </para>
478+
/// <para>
479+
/// The Password field allows clients to provide a secret credential or authentication token
480+
/// associated with the provided Username. The format and interpretation of the Password are
481+
/// specific to the chosen authentication method and agreed upon by the client and the broker.
482+
/// </para>
483+
/// <para>
484+
/// The broker will use the provided Password, along with the Username or other authentication
485+
/// information, to verify the client's identity and grant access to the MQTT communication based on
486+
/// the configured authentication rules.
487+
/// </para>
488+
/// <para>
489+
/// If the client does not require authentication or the chosen authentication method does not involve
490+
/// a password, the Password field may be omitted from the CONNECT packet.
491+
/// </para>
492+
/// <para>
493+
/// This method accepts a SecureString for enhanced security, preventing password exposure in memory.
494+
/// </para>
495+
/// </summary>
496+
/// <param name="password">The password as a SecureString for enhanced security.</param>
497+
/// <returns>The HiveMQClientOptionsBuilder instance.</returns>
498+
public HiveMQClientOptionsBuilder WithPassword(SecureString password)
499+
{
500+
if (password == null)
501+
{
502+
throw new ArgumentNullException(nameof(password));
503+
}
504+
505+
if (password.Length > 65535)
506+
{
507+
Logger.Error("Password must be between 0 and 65535 characters.");
508+
throw new ArgumentException("Password must be between 0 and 65535 characters.");
509+
}
510+
511+
this.options.Password = password;
512+
return this;
513+
}
514+
515+
/// <summary>
516+
/// Sets the password using a plain string (for backward compatibility).
517+
/// <para>
518+
/// WARNING: This method stores the password as a plain string in memory, which is less secure.
519+
/// Consider using WithPassword(SecureString) for enhanced security.
520+
/// </para>
472521
/// <para>
473522
/// The Password is an optional parameter that can be included in the CONNECT packet during the
474523
/// connection establishment process. It is used for client authentication in conjunction with the
@@ -489,17 +538,31 @@ public HiveMQClientOptionsBuilder WithUserName(string username)
489538
/// a password, the Password field may be omitted from the CONNECT packet.
490539
/// </para>
491540
/// </summary>
492-
/// <param name="password">The password value.</param>
541+
/// <param name="password">The password value as a plain string.</param>
493542
/// <returns>The HiveMQClientOptionsBuilder instance.</returns>
543+
[Obsolete("Use WithPassword(SecureString) for enhanced security. This method stores passwords as plain text in memory.")]
494544
public HiveMQClientOptionsBuilder WithPassword(string password)
495545
{
546+
if (password == null)
547+
{
548+
throw new ArgumentNullException(nameof(password));
549+
}
550+
496551
if (password.Length is < 0 or > 65535)
497552
{
498553
Logger.Error("Password must be between 0 and 65535 characters.");
499554
throw new ArgumentException("Password must be between 0 and 65535 characters.");
500555
}
501556

502-
this.options.Password = password;
557+
// Convert string to SecureString
558+
var securePassword = new SecureString();
559+
foreach (char c in password)
560+
{
561+
securePassword.AppendChar(c);
562+
}
563+
securePassword.MakeReadOnly();
564+
565+
this.options.Password = securePassword;
503566
return this;
504567
}
505568

Source/HiveMQtt/Client/Options/HiveMQClientOptions.cs

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ namespace HiveMQtt.Client.Options;
1717

1818
using System;
1919
using System.Linq;
20+
using System.Security;
2021
using System.Security.Cryptography.X509Certificates;
2122
using HiveMQtt.Client;
2223
using HiveMQtt.Client.Exceptions;
@@ -97,7 +98,8 @@ public HiveMQClientOptions()
9798
// The MQTT CONNECT packet supports basic authentication of a Network Connection using the User Name
9899
// and Password fields. While these fields are named for a simple password authentication, they can
99100
// be used to carry other forms of authentication such as passing a token as the Password.
100-
public string? Password { get; set; }
101+
// Note: Password is stored securely using SecureString to prevent memory exposure.
102+
public SecureString? Password { get; set; }
101103

102104
/// <summary>
103105
/// Gets or sets a value that represents the session expiration interval in use by the MQTT broker.
@@ -354,4 +356,31 @@ internal static long RangeValidateFourByteInteger(long value)
354356

355357
return value;
356358
}
359+
360+
/// <summary>
361+
/// Gets the password as a string for MQTT protocol use.
362+
/// Note: This creates a temporary string that should be cleared after use.
363+
/// </summary>
364+
/// <returns>The password as a string, or null if no password is set.</returns>
365+
internal string? GetPasswordAsString()
366+
{
367+
if (this.Password == null)
368+
{
369+
return null;
370+
}
371+
372+
var ptr = IntPtr.Zero;
373+
try
374+
{
375+
ptr = System.Runtime.InteropServices.Marshal.SecureStringToGlobalAllocUnicode(this.Password);
376+
return System.Runtime.InteropServices.Marshal.PtrToStringUni(ptr);
377+
}
378+
finally
379+
{
380+
if (ptr != IntPtr.Zero)
381+
{
382+
System.Runtime.InteropServices.Marshal.ZeroFreeGlobalAllocUnicode(ptr);
383+
}
384+
}
385+
}
357386
}

Source/HiveMQtt/MQTT5/Packets/ConnectPacket.cs

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -93,9 +93,12 @@ public byte[] Encode()
9393
}
9494

9595
// Password
96-
if (this.clientOptions.Password != null)
96+
var passwordString = this.clientOptions.GetPasswordAsString();
97+
if (passwordString != null)
9798
{
98-
_ = EncodeUTF8String(vhAndPayloadStream, this.clientOptions.Password);
99+
_ = EncodeUTF8String(vhAndPayloadStream, passwordString);
100+
// Clear the temporary password string from memory
101+
Array.Clear(passwordString.ToCharArray(), 0, passwordString.Length);
99102
}
100103

101104
// Construct the final packet
@@ -148,9 +151,12 @@ internal void GatherConnectFlagsAndProperties()
148151
}
149152
}
150153

151-
if (this.clientOptions.Password != null)
154+
var passwordString = this.clientOptions.GetPasswordAsString();
155+
if (passwordString != null)
152156
{
153157
this.flags |= 0x40;
158+
// Clear the temporary password string from memory
159+
Array.Clear(passwordString.ToCharArray(), 0, passwordString.Length);
154160
}
155161

156162
if (this.clientOptions.UserName != null)

Tests/HiveMQtt.Test/HiveMQClient/ClientOptionsBuilderTest.cs

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,26 @@ public void Build_WithValidParameters_ReturnsValidOptions(
6767
Assert.Equal(authMethod, options.AuthenticationMethod);
6868
Assert.Equal(Encoding.UTF8.GetBytes(authData), options.AuthenticationData);
6969
Assert.Equal(username, options.UserName);
70-
Assert.Equal(password, options.Password);
70+
71+
// Convert SecureString to string for comparison
72+
string? passwordString = null;
73+
if (options.Password != null)
74+
{
75+
var ptr = IntPtr.Zero;
76+
try
77+
{
78+
ptr = System.Runtime.InteropServices.Marshal.SecureStringToGlobalAllocUnicode(options.Password);
79+
passwordString = System.Runtime.InteropServices.Marshal.PtrToStringUni(ptr);
80+
}
81+
finally
82+
{
83+
if (ptr != IntPtr.Zero)
84+
{
85+
System.Runtime.InteropServices.Marshal.ZeroFreeGlobalAllocUnicode(ptr);
86+
}
87+
}
88+
}
89+
Assert.Equal(password, passwordString);
7190
Assert.Equal(preferIPv6, options.PreferIPv6);
7291
Assert.Equal(topicAliasMaximum, options.ClientTopicAliasMaximum);
7392
Assert.Equal(requestResponseInfo, options.RequestResponseInformation);

0 commit comments

Comments
 (0)