Skip to content

Commit 77c2ce9

Browse files
Yassin Lokhatcursoragent
andcommitted
feat(security): read login password through SecurePassword with ZeroFreeBSTR
MainWindow no longer materialises _password_PB.Password (which would leak a managed string copy that lives until GC). The PasswordBox.SecurePassword is now bridged through SecureStringExtensions.UseAsString, which marshals the SecureString to an unmanaged BSTR, hands a short-lived managed string to Database.Login, then zero-frees the BSTR with Marshal.ZeroFreeBSTR. PasswordBox.Clear() replaces the previous Password = string.Empty assignments so the underlying SecureString buffer is wiped explicitly. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 9b97547 commit 77c2ce9

2 files changed

Lines changed: 68 additions & 5 deletions

File tree

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
using System.Runtime.InteropServices;
2+
using System.Security;
3+
4+
namespace Upsilon.Apps.Passkey.GUI.WPF.Helper
5+
{
6+
/// <summary>
7+
/// Helpers that bridge between WPF's <see cref="SecureString"/> (used by
8+
/// <c>PasswordBox.SecurePassword</c>) and the rest of the API surface, while
9+
/// keeping the unmanaged BSTR copy alive only for the duration of the
10+
/// supplied callback and zero-ing it out afterwards.
11+
/// </summary>
12+
public static class SecureStringExtensions
13+
{
14+
/// <summary>
15+
/// Pins <paramref name="value"/> as an unmanaged BSTR, invokes
16+
/// <paramref name="action"/> with the resulting (managed) string and then
17+
/// zeros the unmanaged buffer. The managed string returned to the caller
18+
/// still lives in the heap until the GC collects it; callers should keep
19+
/// their use of it as short as possible.
20+
/// </summary>
21+
public static T UseAsString<T>(this SecureString value, Func<string, T> action)
22+
{
23+
ArgumentNullException.ThrowIfNull(value);
24+
ArgumentNullException.ThrowIfNull(action);
25+
26+
IntPtr bstr = IntPtr.Zero;
27+
28+
try
29+
{
30+
bstr = Marshal.SecureStringToBSTR(value);
31+
string managed = Marshal.PtrToStringBSTR(bstr);
32+
return action(managed);
33+
}
34+
finally
35+
{
36+
if (bstr != IntPtr.Zero)
37+
{
38+
Marshal.ZeroFreeBSTR(bstr);
39+
}
40+
}
41+
}
42+
43+
/// <summary>
44+
/// Same as <see cref="UseAsString{T}"/> but with an <see cref="Action{T}"/>
45+
/// callback when no value needs to be returned.
46+
/// </summary>
47+
public static void UseAsString(this SecureString value, Action<string> action)
48+
{
49+
_ = value.UseAsString(s =>
50+
{
51+
action(s);
52+
return 0;
53+
});
54+
}
55+
}
56+
}

GUI/WPF/MainWindow.xaml.cs

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ private void _credential_TB_KeyUp(object sender, KeyEventArgs e)
7979
_submitPassword();
8080
}
8181

82-
_password_PB.Password = string.Empty;
82+
_password_PB.Clear();
8383
_timer.Start();
8484
}
8585
else if (e.Key == Key.Escape)
@@ -131,14 +131,14 @@ private void _submitUsername()
131131
_username_TB.Text = string.Empty;
132132
_username_TB.Visibility = Visibility.Collapsed;
133133

134-
_password_PB.Password = string.Empty;
134+
_password_PB.Clear();
135135
_password_PB.Visibility = Visibility.Visible;
136136
_ = _password_PB.Focus();
137137
}
138138

139139
private void _submitPassword()
140140
{
141-
if (string.IsNullOrEmpty(_password_PB.Password))
141+
if (_password_PB.SecurePassword.Length == 0)
142142
{
143143
_timer.Start();
144144
return;
@@ -149,7 +149,14 @@ private void _submitPassword()
149149
return;
150150
}
151151

152-
_ = Session.Database.Login(_password_PB.Password);
152+
_password_PB.SecurePassword.UseAsString(passkey =>
153+
{
154+
_ = Session.Database.Login(passkey);
155+
});
156+
157+
// Erase the PasswordBox buffer right after submitting so the secret
158+
// is not kept alive longer than necessary.
159+
_password_PB.Clear();
153160

154161
if (Session.Database.User is null)
155162
{
@@ -211,7 +218,7 @@ private void _resetCredentials()
211218
_username_TB.Visibility = Visibility.Visible;
212219
_ = _username_TB.Focus();
213220

214-
_password_PB.Password = string.Empty;
221+
_password_PB.Clear();
215222
_password_PB.Visibility = Visibility.Collapsed;
216223

217224
_timer.Stop();

0 commit comments

Comments
 (0)