Encrypting credentials in a WPF application

18,641

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"));
Share:
18,641
Gigi
Author by

Gigi

I run Gigi Labs and Dino's Ultima Page.

Updated on June 13, 2022

Comments

  • Gigi
    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
    Gigi about 10 years
    Works 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
    meziantou about 10 years
    I've tested this scenario. The credential is updated so you can't have two creds for the same application name.
  • Gigi
    Gigi about 10 years
    Just adding a note as per this question stackoverflow.com/questions/22528292/… : use CredentialPersistence.LocalMachine to persist credentials across login sessions.
  • ASA
    ASA about 10 years
    How 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
    meziantou about 10 years
    Sorry, 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
    ASA about 10 years
    I just read it up: This method is supported in .net Framework 4.5.1 only, not in 4.5.
  • Anodyne
    Anodyne over 9 years
    I 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
    tofutim over 7 years
    Note that the Password appears to have a limit of 512 bytes. (I was trying to use it to store JWT tokens...)
  • Rasmus
    Rasmus over 7 years
    Is there a way to clear a stored credential with this method?
  • meziantou
    meziantou over 7 years
    Use 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
    3spot about 5 years
    EnumerateCrendentials 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
    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
    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
    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