Encrypting credentials in a WPF application

2020-01-25 04:56发布

问题:

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?

回答1:

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"));