Encrypting credentials in a WPF application
Here's a summary of my blog post: How to store a password on Windows?
You can use the Data Protection API and its .NET implementation (ProtectedData) to encrypt the password. Here's an example:
public static string Protect(string str)
{
byte[] entropy = Encoding.ASCII.GetBytes(Assembly.GetExecutingAssembly().FullName);
byte[] data = Encoding.ASCII.GetBytes(str);
string protectedData = Convert.ToBase64String(ProtectedData.Protect(data, entropy, DataProtectionScope.CurrentUser));
return protectedData;
}
public static string Unprotect(string str)
{
byte[] protectedData = Convert.FromBase64String(str);
byte[] entropy = Encoding.ASCII.GetBytes(Assembly.GetExecutingAssembly().FullName);
string data = Encoding.ASCII.GetString(ProtectedData.Unprotect(protectedData, entropy, DataProtectionScope.CurrentUser));
return data;
}
Or you can use the Windows Credential Manager (This is the way I prefer because it allows users to backup/restore/edit their credentials even if your application has no such functionality). I've created a NuGet package Meziantou.Framework.Win32.CredentialManager. How to use it:
CredentialManager.WriteCredential("ApplicationName", "username", "Pa$$w0rd", CredentialPersistence.Session);
var cred = CredentialManager.ReadCredential("ApplicationName");
Assert.AreEqual("username", cred.UserName);
Assert.AreEqual("Pa$$w0rd", cred.Password);
CredentialManager.DeleteCredential("ApplicationName");
Original answer with the native API wrapper (A more recent version of this is available on GitHub):
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using Microsoft.Win32.SafeHandles;
using System.Text;
using System.ComponentModel;
public static class CredentialManager
{
public static Credential ReadCredential(string applicationName)
{
IntPtr nCredPtr;
bool read = CredRead(applicationName, CredentialType.Generic, 0, out nCredPtr);
if (read)
{
using (CriticalCredentialHandle critCred = new CriticalCredentialHandle(nCredPtr))
{
CREDENTIAL cred = critCred.GetCredential();
return ReadCredential(cred);
}
}
return null;
}
private static Credential ReadCredential(CREDENTIAL credential)
{
string applicationName = Marshal.PtrToStringUni(credential.TargetName);
string userName = Marshal.PtrToStringUni(credential.UserName);
string secret = null;
if (credential.CredentialBlob != IntPtr.Zero)
{
secret = Marshal.PtrToStringUni(credential.CredentialBlob, (int)credential.CredentialBlobSize / 2);
}
return new Credential(credential.Type, applicationName, userName, secret);
}
public static int WriteCredential(string applicationName, string userName, string secret)
{
byte[] byteArray = Encoding.Unicode.GetBytes(secret);
if (byteArray.Length > 512)
throw new ArgumentOutOfRangeException("secret", "The secret message has exceeded 512 bytes.");
CREDENTIAL credential = new CREDENTIAL();
credential.AttributeCount = 0;
credential.Attributes = IntPtr.Zero;
credential.Comment = IntPtr.Zero;
credential.TargetAlias = IntPtr.Zero;
credential.Type = CredentialType.Generic;
credential.Persist = (UInt32)CredentialPersistence.Session;
credential.CredentialBlobSize = (UInt32)Encoding.Unicode.GetBytes(secret).Length;
credential.TargetName = Marshal.StringToCoTaskMemUni(applicationName);
credential.CredentialBlob = Marshal.StringToCoTaskMemUni(secret);
credential.UserName = Marshal.StringToCoTaskMemUni(userName ?? Environment.UserName);
bool written = CredWrite(ref credential, 0);
int lastError = Marshal.GetLastWin32Error();
Marshal.FreeCoTaskMem(credential.TargetName);
Marshal.FreeCoTaskMem(credential.CredentialBlob);
Marshal.FreeCoTaskMem(credential.UserName);
if (written)
return 0;
throw new Exception(string.Format("CredWrite failed with the error code {0}.", lastError));
}
public static IReadOnlyList<Credential> EnumerateCrendentials()
{
List<Credential> result = new List<Credential>();
int count;
IntPtr pCredentials;
bool ret = CredEnumerate(null, 0, out count, out pCredentials);
if (ret)
{
for (int n = 0; n < count; n++)
{
IntPtr credential = Marshal.ReadIntPtr(pCredentials, n * Marshal.SizeOf(typeof(IntPtr)));
result.Add(ReadCredential((CREDENTIAL)Marshal.PtrToStructure(credential, typeof(CREDENTIAL))));
}
}
else
{
int lastError = Marshal.GetLastWin32Error();
throw new Win32Exception(lastError);
}
return result;
}
[DllImport("Advapi32.dll", EntryPoint = "CredReadW", CharSet = CharSet.Unicode, SetLastError = true)]
static extern bool CredRead(string target, CredentialType type, int reservedFlag, out IntPtr credentialPtr);
[DllImport("Advapi32.dll", EntryPoint = "CredWriteW", CharSet = CharSet.Unicode, SetLastError = true)]
static extern bool CredWrite([In] ref CREDENTIAL userCredential, [In] UInt32 flags);
[DllImport("advapi32", SetLastError = true, CharSet = CharSet.Unicode)]
static extern bool CredEnumerate(string filter, int flag, out int count, out IntPtr pCredentials);
[DllImport("Advapi32.dll", EntryPoint = "CredFree", SetLastError = true)]
static extern bool CredFree([In] IntPtr cred);
private enum CredentialPersistence : uint
{
Session = 1,
LocalMachine,
Enterprise
}
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
private struct CREDENTIAL
{
public UInt32 Flags;
public CredentialType Type;
public IntPtr TargetName;
public IntPtr Comment;
public System.Runtime.InteropServices.ComTypes.FILETIME LastWritten;
public UInt32 CredentialBlobSize;
public IntPtr CredentialBlob;
public UInt32 Persist;
public UInt32 AttributeCount;
public IntPtr Attributes;
public IntPtr TargetAlias;
public IntPtr UserName;
}
sealed class CriticalCredentialHandle : CriticalHandleZeroOrMinusOneIsInvalid
{
public CriticalCredentialHandle(IntPtr preexistingHandle)
{
SetHandle(preexistingHandle);
}
public CREDENTIAL GetCredential()
{
if (!IsInvalid)
{
CREDENTIAL credential = (CREDENTIAL)Marshal.PtrToStructure(handle, typeof(CREDENTIAL));
return credential;
}
throw new InvalidOperationException("Invalid CriticalHandle!");
}
protected override bool ReleaseHandle()
{
if (!IsInvalid)
{
CredFree(handle);
SetHandleAsInvalid();
return true;
}
return false;
}
}
}
public enum CredentialType
{
Generic = 1,
DomainPassword,
DomainCertificate,
DomainVisiblePassword,
GenericCertificate,
DomainExtended,
Maximum,
MaximumEx = Maximum + 1000,
}
public class Credential
{
private readonly string _applicationName;
private readonly string _userName;
private readonly string _password;
private readonly CredentialType _credentialType;
public CredentialType CredentialType
{
get { return _credentialType; }
}
public string ApplicationName
{
get { return _applicationName; }
}
public string UserName
{
get { return _userName; }
}
public string Password
{
get { return _password; }
}
public Credential(CredentialType credentialType, string applicationName, string userName, string password)
{
_applicationName = applicationName;
_userName = userName;
_password = password;
_credentialType = credentialType;
}
public override string ToString()
{
return string.Format("CredentialType: {0}, ApplicationName: {1}, UserName: {2}, Password: {3}", CredentialType, ApplicationName, UserName, Password);
}
}
Usage:
WriteCredential("ApplicationName", "Meziantou", "Passw0rd");
Console.WriteLine(ReadCredential("Demo"));
Comments
-
Gigi almost 2 years
In a WPF application, I would like to provide the typical "Remember Me" option to remember credentials and use them automatically next time the application is launched.
Using a one-way hash is clearly not an option, and while I can store credentials in isolated storage or in the registry, there is one issue to deal with when encrypting the credentials.
If I use a symmetric key encryption algorithm, I will need to store the key somewhere. And if the key is, for example, hardcoded in memory, then I imagine it would be easy to disassemble the .NET assemblies and find it.
What is the best way to encrypt credentials in .NET and keep them secure, keeping the encryption key completely out of reach?
-
Gigi about 10 yearsWorks great, and is simple to use. Just a couple of questions... does the use of an "applicationName" parameter mean you can only have one credential pair per distinct application name, and is there anything that is usually done to attempt to minimise application name conflicts?
-
meziantou about 10 yearsI've tested this scenario. The credential is updated so you can't have two creds for the same application name.
-
Gigi about 10 yearsJust adding a note as per this question stackoverflow.com/questions/22528292/… : use CredentialPersistence.LocalMachine to persist credentials across login sessions.
-
ASA about 10 yearsHow about adding all the required using x statements to your answer? I could catch all (but one?) but I am still missing the source for the generic method PtrToStructure<T>. Did you mean the following: (CREDENTIAL)Marshal.PtrToStructure(credential, typeof(CREDENTIAL)) ?
-
meziantou about 10 yearsSorry,
PtrToStructure<T>
is new with .NET 4.5. If you don't use .NET 4.5 use the non generic version and cast the result -
ASA about 10 yearsI just read it up: This method is supported in .net Framework 4.5.1 only, not in 4.5.
-
Anodyne over 9 yearsI just edited the code in the answer to fix a memory leak (3 calls to Marshal.StringToCoTaskMemUni in WriteCredential without corresponding calls to Marshal.FreeCoTaskMem).
-
tofutim over 7 yearsNote that the Password appears to have a limit of 512 bytes. (I was trying to use it to store JWT tokens...)
-
Rasmus over 7 yearsIs there a way to clear a stored credential with this method?
-
meziantou over 7 yearsUse the
CredDelete
method:[DllImport("Advapi32.dll", EntryPoint = "CredDeleteW", CharSet = CharSet.Unicode, SetLastError = true)]public static extern bool CredDelete(string target, CredentialType type, int reservedFlag);
-
3spot about 5 yearsEnumerateCrendentials should free the pCredentials memory. Call CredFree(pCredentials); after the for loop. Thanks for the answer. Also, if you want more than one user per app, just use "AppName\\UserName" as the app name. Then add the filter arg to EnumerateCrendentials and call it with "AppName\*".
-
meziantou about 5 years@3spot The latest version of this code is available on GitHub and does call
CredFree
. I've added the link to the repository in the answer. -
Cherubim almost 4 years@meziantou is there any possibility of adding an optional entropy similar to DP-API in the case of Credential Manager? Or is there any other way to add additional layer of security via Credential manager
-
meziantou almost 4 years@Cherubim You can only set the
CredentialPersistence
to restrict usage. The passwords are editable through the Credential Manager (UI) so you cannot protect them the same way as when using the DP-API