6

I'd like to encrypt a SecureString using DPAPI so that I can save it to disk.

The .net DPAPI class is the ProtectedData class, however, ProtectedData.Protect has a single overload which takes a byte array. There is no overload which accepts a SecureString.

In Encrypting Passwords in a .NET app.config File, John Galloway makes use of the above overload by first converting the SecureString to an unsecured string. I'd like to avoid this as it defeats the purpose of using the SecureString in the first place.

The ConvertFrom-SecureString PowerShell cmdlet seems to do what I need since "if no key is specified, the Windows Data Protection API (DPAPI) is used to encrypt the standard string representation" but I'm not sure how I'd use this cmdlet directly from .net or even if it's a good idea to do so.

1

3 Answers 3

4

The blog post SecureString: Soup to Nuts, Part I by Jeff Griffin shows how this can be done. The approach is to convert the SecureString to an unmanaged BSTR and then use P/Invoke to call the unmanaged DPAPI functions.

Sign up to request clarification or add additional context in comments.

1 Comment

This URL is dead and not available on archive.org
2

For anyone who comes across this question in the future, I've written a pair of extension methods for SecureString which replicate the behaviour of ProtectedData.Protect (SecureStringExtensions.Protect) and ProtectedData.Unprotect (SecureStringExtensions.AppendProtected), but work seamlessly with secure strings rather than byte arrays.

They've been designed to be secure and robust, so I hope they'll be of use. (.NET Framework version directly below; newer .NET 8 version further down.)

/*
 * This code is licensed under a Creative Commons Attribution 4.0 International License.
 * See: https://creativecommons.org/licenses/by/4.0/
 */

using System;
using System.Runtime.InteropServices;
using System.Security;
using System.Security.Cryptography;

namespace MyNamespace
{
    /// <summary>Extension methods for <see cref="T:System.Security.SecureString" /> to enable safe serialisation and deserialisation of secure strings. This class cannot be inherited.</summary>
    /// <remarks>The <see cref="M:Protect(SecureString, System.Byte[], DataProtectionScope)" /> and <see cref="M:AppendProtected(SecureString, System.Byte[], System.Byte[], DataProtectionScope)" /> methods can be treated as secure string-based equivalents of <see cref="M:System.Security.Cryptography.ProtectedData.Protect(System.Byte[], System.Byte[], System.Security.Cryptography.DataProtectionScope)" /> and <see cref="M:System.Security.Cryptography.ProtectedData.Unprotect(System.Byte[], System.Byte[], System.Security.Cryptography.DataProtectionScope)" />, respectively.</remarks>
    /// <seealso cref="T:System.Security.Cryptography.ProtectedData" />
    public static class SecureStringExtensions
    {
        /// <summary>Specifies the scope of the data protection to be applied by the <see cref="Protect(SecureString, byte[], DataProtectionScope)" /> and <see cref="AppendProtected(SecureString, byte[], byte[], DataProtectionScope)" /> methods.</summary>
        /// <remarks>This enumeration is equivalent to <see cref="T:System.Security.Cryptography.DataProtectionScope" />.</remarks>
        /// <seealso cref="T:System.Security.Cryptography.DataProtectionScope" />
        public enum DataProtectionScope
        {
            /// <summary>
            /// The protected data is associated with the current user. Only threads running under the current user context can unprotect the data.
            /// </summary>
            CurrentUser,

            /// <summary>
            /// The protected data is associated with the machine context. Any process running on the computer can unprotect data.
            /// This enumeration value is usually used in server-specific applications that run on a server where untrusted users are not allowed access.
            /// </summary>
            LocalMachine
        }

        /// <summary>Encrypts the data in a secure string and returns a byte array that contains the encrypted data.</summary>
        /// <remarks>This method can be treated as equivalent to <see cref="M:System.Security.Cryptography.ProtectedData.Protect(System.Byte[], System.Byte[], System.Security.Cryptography.DataProtectionScope)" />, except that it encrypts a secure string instead of a byte array.</remarks>
        /// <param name="secureString">The secure string.</param>
        /// <param name="optionalEntropy">An optional additional byte array used to increase the complexity of the encryption, or <see langword="null" /> for no additional complexity.</param>
        /// <param name="scope">One of the enumeration values that specifies the scope of encryption.</param>
        /// <returns>A byte array representing the encrypted data.</returns>
        /// <exception cref="T:System.Security.Cryptography.CryptographicException">The encryption failed.</exception>
        /// <exception cref="T:System.OutOfMemoryException">The system ran out of memory while encrypting the data.</exception>
        /// <seealso cref="M:System.Security.Cryptography.ProtectedData.Protect(System.Byte[], System.Byte[], System.Security.Cryptography.DataProtectionScope)" />
        public static byte[] Protect(this SecureString secureString, byte[] optionalEntropy, DataProtectionScope scope)
        {
            byte[] result = null;
            NativeMethods.DATA_BLOB dataIn = null;
            NativeMethods.DATA_BLOB entropy = null;
            NativeMethods.DATA_BLOB dataOut = new NativeMethods.DATA_BLOB();
            GCHandle ptrOptionalEntropy = new GCHandle();

            try
            {
                // +++ Handle secureString
                dataIn = new NativeMethods.DATA_BLOB
                {
                    cbData = secureString.Length * sizeof(char),
                    pbData = Marshal.SecureStringToGlobalAllocUnicode(secureString)
                };
                // ---

                // +++ Handle optionalEntropy
                if (optionalEntropy != null)
                {
                    ptrOptionalEntropy = GCHandle.Alloc(optionalEntropy, GCHandleType.Pinned);

                    entropy = new NativeMethods.DATA_BLOB
                    {
                        cbData = optionalEntropy.Length,
                        pbData = ptrOptionalEntropy.AddrOfPinnedObject()
                    };
                }
                // ---

                // +++ Handle scope
                NativeMethods.CryptProtectFlags flags = NativeMethods.CryptProtectFlags.CRYPTPROTECT_UI_FORBIDDEN;

                if (scope.HasFlag(DataProtectionScope.LocalMachine))
                    flags |= NativeMethods.CryptProtectFlags.CRYPTPROTECT_LOCAL_MACHINE;
                // ---

                if (!NativeMethods.CryptProtectData(dataIn, IntPtr.Zero, entropy, IntPtr.Zero, IntPtr.Zero, flags, dataOut))
                    throw new CryptographicException(Marshal.GetLastWin32Error());
                else
                {
                    if (dataOut.pbData == IntPtr.Zero)
                        throw new OutOfMemoryException();

                    result = new byte[dataOut.cbData];
                    Marshal.Copy(dataOut.pbData, result, 0, dataOut.cbData);
                }
            }
            finally
            {
                if (dataOut.pbData != IntPtr.Zero)
                {
                    NativeMethods.ZeroMemory(dataOut.pbData, (UIntPtr)dataOut.cbData);
                    Marshal.FreeHGlobal(dataOut.pbData);
                }

                if (ptrOptionalEntropy.IsAllocated)
                    ptrOptionalEntropy.Free();

                if (dataIn != null)
                    Marshal.ZeroFreeGlobalAllocUnicode(dataIn.pbData);
            }

            return (result);
        }

        /// <summary>Decrypts the data in a specified byte array and appends it to a secure string.</summary>
        /// <remarks>This method can be treated as equivalent to <see cref="M:System.Security.Cryptography.ProtectedData.Unprotect(System.Byte[], System.Byte[], System.Security.Cryptography.DataProtectionScope)" />, except that it appends the decrypted data to a secure string instead returning it in a byte array.</remarks>
        /// <param name="secureString">The secure string.</param>
        /// <param name="encryptedData">A byte array containing data encrypted using the <see cref="M:Protect(SecureString, System.Byte[], DataProtectionScope)" /> method.</param>
        /// <param name="optionalEntropy">An optional additional byte array that was used to encrypt the data, or <see langword="null" /> if the additional byte array was not used.</param>
        /// <param name="scope">One of the enumeration values that specifies the scope of data protection that was used to encrypt the data.</param>
        /// <exception cref="T:System.ArgumentNullException">The <paramref name="encryptedData" /> parameter is <see langword="null" />.</exception>
        /// <exception cref="T:System.InvalidOperationException">The secure string is read only.</exception>
        /// <exception cref="T:System.Security.Cryptography.CryptographicException">The decryption failed.</exception>
        /// <exception cref="T:System.OutOfMemoryException">The system ran out of memory while decrypting the data.</exception>
        /// <seealso cref="M:System.Security.Cryptography.ProtectedData.Unprotect(System.Byte[], System.Byte[], System.Security.Cryptography.DataProtectionScope)" />
        public static void AppendProtected(this SecureString secureString, byte[] encryptedData, byte[] optionalEntropy, DataProtectionScope scope)
        {
            NativeMethods.DATA_BLOB dataIn = null;
            NativeMethods.DATA_BLOB entropy = null;
            NativeMethods.DATA_BLOB dataOut = new NativeMethods.DATA_BLOB();
            GCHandle ptrEncryptedData = new GCHandle();
            GCHandle ptrOptionalEntropy = new GCHandle();

            if (encryptedData == null)
                throw new ArgumentNullException("encryptedData");

            if (encryptedData.IsReadOnly)
                throw new InvalidOperationException();

            try
            {
                // +++ Handle encryptedData
                ptrEncryptedData = GCHandle.Alloc(encryptedData, GCHandleType.Pinned);

                dataIn = new NativeMethods.DATA_BLOB
                {
                    cbData = encryptedData.Length,
                    pbData = ptrEncryptedData.AddrOfPinnedObject()
                };
                // ---

                // +++ Handle optionalEntropy
                if (optionalEntropy != null)
                {
                    ptrOptionalEntropy = GCHandle.Alloc(optionalEntropy, GCHandleType.Pinned);

                    entropy = new NativeMethods.DATA_BLOB
                    {
                        cbData = optionalEntropy.Length,
                        pbData = ptrOptionalEntropy.AddrOfPinnedObject()
                    };
                }
                // ---

                // +++ Handle scope
                NativeMethods.CryptProtectFlags flags = NativeMethods.CryptProtectFlags.CRYPTPROTECT_UI_FORBIDDEN;

                if (scope.HasFlag(DataProtectionScope.LocalMachine))
                    flags |= NativeMethods.CryptProtectFlags.CRYPTPROTECT_LOCAL_MACHINE;
                // ---

                if (!NativeMethods.CryptUnprotectData(dataIn, IntPtr.Zero, entropy, IntPtr.Zero, IntPtr.Zero, flags, dataOut))
                    throw new CryptographicException(Marshal.GetLastWin32Error());
                else
                {
                    if (dataOut.pbData == IntPtr.Zero)
                        throw new OutOfMemoryException();

                    // Sanity check: can't be a valid string if length is not a multiple of sizeof(char)
                    if ((dataOut.cbData % sizeof(char)) != 0)
                        throw new CryptographicException();

                    for (int i = 0; i < dataOut.cbData; i += sizeof(char))
                        secureString.AppendChar((char)Marshal.ReadInt16(dataOut.pbData, i));
                }
            }
            finally
            {
                if (dataOut.pbData != IntPtr.Zero)
                {
                    NativeMethods.ZeroMemory(dataOut.pbData, (UIntPtr)dataOut.cbData);
                    Marshal.FreeHGlobal(dataOut.pbData);
                }

                if (ptrOptionalEntropy.IsAllocated)
                    ptrOptionalEntropy.Free();

                if (ptrEncryptedData.IsAllocated)
                    ptrEncryptedData.Free();
            }
        }

        private static class NativeMethods
        {
            [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1049:TypesThatOwnNativeResourcesShouldBeDisposable")]
            [StructLayout(LayoutKind.Sequential)]
            public class DATA_BLOB
            {
                public int cbData;
                public IntPtr pbData;
            }

            [Flags]
            public enum CryptProtectFlags : uint
            {
                CRYPTPROTECT_UI_FORBIDDEN = 0x01,
                CRYPTPROTECT_LOCAL_MACHINE = 0x04,
                CRYPTPROTECT_VERIFY_PROTECTION = 0x40
            }

            [DllImport("kernel32.dll", EntryPoint = "RtlZeroMemory")]
            public static extern void ZeroMemory(IntPtr destination, UIntPtr length);

            [DllImport("crypt32.dll", SetLastError = true)]
            public static extern bool CryptProtectData(DATA_BLOB pDataIn, IntPtr szDataDescr, DATA_BLOB pOptionalEntropy, IntPtr pvReserved, IntPtr pPromptStruct, CryptProtectFlags dwFlags, DATA_BLOB pDataOut);

            [DllImport("crypt32.dll", SetLastError = true)]
            public static extern bool CryptUnprotectData(DATA_BLOB pDataIn, IntPtr ppszDataDescr, DATA_BLOB pOptionalEntropy, IntPtr pvReserved, IntPtr pPromptStruct, CryptProtectFlags dwFlags, DATA_BLOB pDataOut);
        }
    }
}

Example:

byte[] entropy = { 1, 2, 3, 4, 5 }; // Example entropy
string password = "This is my password";    // Don't store passwords like this!

// Add password to secure string
SecureString ss1 = new SecureString();
Array.ForEach(password.ToCharArray(), ss1.AppendChar);
ss1.MakeReadOnly();

// Encrypt secure string
byte[] protectedBytes = ss1.Protect(entropy, SecureStringExtensions.DataProtectionScope.CurrentUser);
Console.WriteLine("Encrypted secure string: {0}", Convert.ToBase64String(protectedBytes));

// Decrypt and add to a new secure string
SecureString ss2 = new SecureString();
ss2.AppendProtected(protectedBytes, entropy, SecureStringExtensions.DataProtectionScope.CurrentUser);
ss2.MakeReadOnly();

After many years, I've recently had to port this code to .NET 8. Here's the updated version:

/*
 * This code is licensed under a Creative Commons Attribution 4.0 International License.
 * See: https://creativecommons.org/licenses/by/4.0/
 */

using System;
using System.Runtime.InteropServices;
using System.Security;
using System.Security.Cryptography;

namespace MyNamespace
{
    /// <summary>Extension methods for <see cref="System.Security.SecureString" /> to enable safe serialisation and deserialisation of secure strings. This class cannot be inherited.</summary>
    /// <remarks>The <see cref="Protect(SecureString, System.Byte[], DataProtectionScope)" /> and <see cref="AppendProtected(SecureString, System.Byte[], System.Byte[], DataProtectionScope)" /> methods can be treated as secure string-based equivalents of <see cref="System.Security.Cryptography.ProtectedData.Protect(System.Byte[], System.Byte[], System.Security.Cryptography.DataProtectionScope)" /> and <see cref="System.Security.Cryptography.ProtectedData.Unprotect(System.Byte[], System.Byte[], System.Security.Cryptography.DataProtectionScope)" />, respectively.</remarks>
    /// <seealso cref="System.Security.Cryptography.ProtectedData" />
    public static partial class SecureStringExtensions
    {
        /// <summary>Specifies the scope of the data protection to be applied by the <see cref="Protect(SecureString, byte[], DataProtectionScope)" /> and <see cref="AppendProtected(SecureString, byte[], byte[], DataProtectionScope)" /> methods.</summary>
        /// <remarks>This enumeration is equivalent to <see cref="System.Security.Cryptography.DataProtectionScope" />.</remarks>
        /// <seealso cref="System.Security.Cryptography.DataProtectionScope" />
        public enum DataProtectionScope
        {
            /// <summary>
            /// The protected data is associated with the current user. Only threads running under the current user context can unprotect the data.
            /// </summary>
            CurrentUser,

            /// <summary>
            /// The protected data is associated with the machine context. Any process running on the computer can unprotect data.
            /// This enumeration value is usually used in server-specific applications that run on a server where untrusted users are not allowed access.
            /// </summary>
            LocalMachine
        }

        /// <summary>Encrypts the data in a secure string and returns a byte array that contains the encrypted data.</summary>
        /// <remarks>This method can be treated as equivalent to <see cref="System.Security.Cryptography.ProtectedData.Protect(System.Byte[], System.Byte[], System.Security.Cryptography.DataProtectionScope)" />, except that it encrypts a secure string instead of a byte array.</remarks>
        /// <param name="secureString">The secure string.</param>
        /// <param name="optionalEntropy">An optional additional byte array used to increase the complexity of the encryption, or <see langword="null" /> for no additional complexity.</param>
        /// <param name="scope">One of the enumeration values that specifies the scope of encryption.</param>
        /// <returns>A byte array representing the encrypted data.</returns>
        /// <exception cref="System.Security.Cryptography.CryptographicException">The encryption failed.</exception>
        /// <seealso cref="System.Security.Cryptography.ProtectedData.Protect(System.Byte[], System.Byte[], System.Security.Cryptography.DataProtectionScope)" />
        public static unsafe byte[] Protect(this SecureString secureString, byte[]? optionalEntropy, DataProtectionScope scope)
        {
            byte[]? protectedBytes = null;
            NativeMethods.DATA_BLOB dataIn = default;
            NativeMethods.DATA_BLOB dataOut = default;

            try
            {
                bool result;
                NativeMethods.DATA_BLOB entropy;

                fixed (byte* ptrOptionalEntropy = optionalEntropy)
                {
                    NativeMethods.DATA_BLOB* ptrEntropy;

                    // +++ Handle secureString
                    dataIn = new NativeMethods.DATA_BLOB
                    {
                        cbData = (uint)secureString.Length * sizeof(char),
                        pbData = (byte*)Marshal.SecureStringToGlobalAllocUnicode(secureString)
                    };
                    // ---

                    // +++ Handle optionalEntropy
                    if (optionalEntropy is not null)
                    {
                        entropy = new NativeMethods.DATA_BLOB
                        {
                            cbData = (uint)optionalEntropy.Length,
                            pbData = ptrOptionalEntropy
                        };

                        ptrEntropy = &entropy;
                    }
                    else
                        ptrEntropy = (NativeMethods.DATA_BLOB*)nint.Zero;
                    // ---

                    // +++ Handle scope
                    NativeMethods.CryptProtectFlags flags = NativeMethods.CryptProtectFlags.CRYPTPROTECT_UI_FORBIDDEN;

                    if (scope.HasFlag(DataProtectionScope.LocalMachine))
                        flags |= NativeMethods.CryptProtectFlags.CRYPTPROTECT_LOCAL_MACHINE;
                    // ---

                    result = NativeMethods.CryptProtectData(&dataIn, nint.Zero, ptrEntropy, nint.Zero, nint.Zero, flags, &dataOut);
                }

                if (result)
                {
                    if (dataOut.pbData == (byte*)nint.Zero)
                        throw new CryptographicException();

                    ReadOnlySpan<byte> buffer = new(dataOut.pbData, (int)dataOut.cbData);
                    protectedBytes = buffer.ToArray();
                }
                else
                    throw new CryptographicException(Marshal.GetHRForLastWin32Error());
            }
            finally
            {
                if (dataOut.pbData != (byte*)nint.Zero)
                {
                    CryptographicOperations.ZeroMemory(new Span<byte>(dataOut.pbData, (int)dataOut.cbData));
                    Marshal.FreeHGlobal((nint)dataOut.pbData);
                }

                if (dataIn.pbData != (byte*)nint.Zero)
                    Marshal.ZeroFreeGlobalAllocUnicode((nint)dataIn.pbData);
            }

            return (protectedBytes);
        }

        /// <summary>Decrypts the data in a specified byte array and appends it to a secure string.</summary>
        /// <remarks>This method can be treated as equivalent to <see cref="System.Security.Cryptography.ProtectedData.Unprotect(System.Byte[], System.Byte[], System.Security.Cryptography.DataProtectionScope)" />, except that it appends the decrypted data to a secure string instead returning it in a byte array.</remarks>
        /// <param name="secureString">The secure string.</param>
        /// <param name="encryptedData">A byte array containing data encrypted using the <see cref="Protect(SecureString, System.Byte[], DataProtectionScope)" /> method.</param>
        /// <param name="optionalEntropy">An optional additional byte array that was used to encrypt the data, or <see langword="null" /> if the additional byte array was not used.</param>
        /// <param name="scope">One of the enumeration values that specifies the scope of data protection that was used to encrypt the data.</param>
        /// <exception cref="System.ArgumentNullException">The <paramref name="encryptedData" /> parameter is <see langword="null" />.</exception>
        /// <exception cref="System.InvalidOperationException">The secure string is read only.</exception>
        /// <exception cref="System.Security.Cryptography.CryptographicException">The decryption failed.</exception>
        /// <seealso cref="System.Security.Cryptography.ProtectedData.Unprotect(System.Byte[], System.Byte[], System.Security.Cryptography.DataProtectionScope)" />
        public static unsafe void AppendProtected(this SecureString secureString, byte[] encryptedData, byte[]? optionalEntropy, DataProtectionScope scope)
        {
            ArgumentNullException.ThrowIfNull(encryptedData);

            if (encryptedData.IsReadOnly)
                throw new InvalidOperationException();

            NativeMethods.DATA_BLOB dataOut = default;

            try
            {
                bool result;
                NativeMethods.DATA_BLOB dataIn;
                NativeMethods.DATA_BLOB entropy;

                fixed (byte* ptrEncryptedData = encryptedData)
                fixed (byte* ptrOptionalEntropy = optionalEntropy)
                {
                    NativeMethods.DATA_BLOB* ptrEntropy;

                    // +++ Handle encryptedData
                    dataIn = new NativeMethods.DATA_BLOB
                    {
                        cbData = (uint)encryptedData.Length,
                        pbData = ptrEncryptedData
                    };
                    // ---

                    // +++ Handle optionalEntropy
                    if (optionalEntropy is not null)
                    {
                        entropy = new NativeMethods.DATA_BLOB
                        {
                            cbData = (uint)optionalEntropy.Length,
                            pbData = ptrOptionalEntropy
                        };

                        ptrEntropy = &entropy;
                    }
                    else
                        ptrEntropy = (NativeMethods.DATA_BLOB*)nint.Zero;
                    // ---

                    // +++ Handle scope
                    NativeMethods.CryptProtectFlags flags = NativeMethods.CryptProtectFlags.CRYPTPROTECT_UI_FORBIDDEN;

                    if (scope.HasFlag(DataProtectionScope.LocalMachine))
                        flags |= NativeMethods.CryptProtectFlags.CRYPTPROTECT_LOCAL_MACHINE;
                    // ---

                    result = NativeMethods.CryptUnprotectData(&dataIn, nint.Zero, ptrEntropy, nint.Zero, nint.Zero, flags, &dataOut);
                }

                if (result)
                {
                    // Sanity check: can't be a valid string if length is not a multiple of sizeof(char), or if dataOut.pbData is null
                    if (((dataOut.cbData % sizeof(char)) != 0) || (dataOut.pbData == (byte*)nint.Zero))
                        throw new CryptographicException();

                    ReadOnlySpan<char> buffer = new(dataOut.pbData, (int)dataOut.cbData / sizeof(char));

                    foreach (char c in buffer)
                        secureString.AppendChar(c);
                }
                else
                    throw new CryptographicException(Marshal.GetHRForLastWin32Error());
            }
            finally
            {
                if (dataOut.pbData != (byte*)nint.Zero)
                {
                    CryptographicOperations.ZeroMemory(new Span<byte>(dataOut.pbData, (int)dataOut.cbData));
                    Marshal.FreeHGlobal((nint)dataOut.pbData);
                }
            }
        }

        private static partial class NativeMethods
        {
            [StructLayout(LayoutKind.Sequential)]
            internal struct DATA_BLOB
            {
                public uint cbData;
                public unsafe byte* pbData;
            }

            [Flags]
            internal enum CryptProtectFlags : uint
            {
                CRYPTPROTECT_UI_FORBIDDEN = 0x01,
                CRYPTPROTECT_LOCAL_MACHINE = 0x04,
                CRYPTPROTECT_VERIFY_PROTECTION = 0x40
            }

            [LibraryImport("crypt32.dll", SetLastError = true)]
            [return: MarshalAs(UnmanagedType.Bool)]
            internal static unsafe partial bool CryptProtectData(DATA_BLOB* pDataIn, nint szDataDescr, DATA_BLOB* pOptionalEntropy, nint pvReserved, nint pPromptStruct, CryptProtectFlags dwFlags, DATA_BLOB* pDataOut);

            [LibraryImport("crypt32.dll", SetLastError = true)]
            [return: MarshalAs(UnmanagedType.Bool)]
            internal static unsafe partial bool CryptUnprotectData(DATA_BLOB* pDataIn, nint ppszDataDescr, DATA_BLOB* pOptionalEntropy, nint pvReserved, nint pPromptStruct, CryptProtectFlags dwFlags, DATA_BLOB* pDataOut);
        }
    }
}

2 Comments

I know it's been ~3 years since you wrote this answer, but may I ask why you decided to re-define DataProtectionScope ? It is functionally the same as using System.Security.Cryptography.DataProtectionScope
@MikeChristiansen It's also been ~3 years since I saw the code! The answer, unfortunately, is that I can't remember. I can see from the comments that I did it deliberately, so there must have been a reason at the time. I do remember originally writing the code for a particular project, and the duplication may be a hold-over from that. It's possible that I was trying to avoid a dependency on System.Security.Cryptography in the original code, but made use of System.Security.Cryptography.CryptographicException in this version. Either way, having re-assessed it, the code still seems solid.
1

After a lot of digging this weekend I think I've figured out how to do this, though let me preface by noting that I'm by no means an expert, and am still just figuring out most of this myself.

From what I can understand, right now there's a bit of an API gap when it comes to encrypting SecureStrings using the DPAPI. From looking through the available methods, and reading SecureString: Soup to Nuts, Part I by Jeff Griffin, it appears that you can't use the handy dandy ProtectedData.Protect and Unprotect methods since they don't take a byte array, and if you convert your SecureString to a byte array it would be sitting there unprotected again defeating the purpose.

So the solution involves using P/Invoke to directly call the underlying C++ commands that the ProtectedData methods use. I believe this is what Jeff Griffin's blog did but the link to the actual extension methods seems dead so I've tried to cobble together what they might've looked like so they don't get lost, but again, I'm no security expert so take this all with a grain of salt:

I also found this example super helpful as a reference: http://www.obviex.com/samples/dpapi.aspx

public static class EncryptionExtensions
{
    public static string Decrypt(this byte[] data)
    {
        DATA_BLOB plainTextBlob = new DATA_BLOB();//we need to pass all of these as parameters
        DATA_BLOB cipherTextBlob = new DATA_BLOB();
        DATA_BLOB entropyBlob = new DATA_BLOB();//though atm I'm omitting entropy so this will just be empty.

        CRYPTPROTECT_PROMPTSTRUCT prompt = new CRYPTPROTECT_PROMPTSTRUCT();
        InitPrompt(ref prompt);//make it empty.

        try
        {
            // Convert ciphertext bytes into a BLOB structure.
            try
            {
                // Use empty array for null parameter.
                if (data == null)
                    data = new byte[0];

                // Allocate memory for the BLOB data.
                cipherTextBlob.pbData = Marshal.AllocHGlobal(data.Length);

                // Make sure that memory allocation was successful.
                if (cipherTextBlob.pbData == IntPtr.Zero)
                    throw new Exception(
                        "Unable to allocate data buffer for BLOB structure.");

                // Specify number of bytes in the BLOB.
                cipherTextBlob.cbData = data.Length;

                // Copy data from original source to the BLOB structure.
                Marshal.Copy(data, 0, cipherTextBlob.pbData, data.Length);
            }
            catch (Exception ex)
            {
                throw new Exception(
                    "Cannot initialize ciphertext BLOB.", ex);
            }


            // Call DPAPI to decrypt data.
            bool success = CryptUnprotectData(ref cipherTextBlob, null, ref entropyBlob, IntPtr.Zero, ref prompt, CryptProtectFlags.CRYPTPROTECT_UI_FORBIDDEN, ref plainTextBlob);

            // Check the result.
            if (!success)
            {
                // If operation failed, retrieve last Win32 error.
                int errCode = Marshal.GetLastWin32Error();

                // Win32Exception will contain error message corresponding
                // to the Windows error code.
                throw new Exception(
                    "CryptUnprotectData failed.", new Win32Exception(errCode));
            }

            return Marshal.PtrToStringAuto(plainTextBlob.pbData);//convert your pointer back into a string. Not sure why PtrToStringBTSR doesn't work but Auto seems to.
        }
        catch (Exception ex)
        {
            throw new Exception("DPAPI was unable to decrypt data.", ex);
        }
        // Free all memory allocated for BLOBs.
        finally
        {
            if (plainTextBlob.pbData != IntPtr.Zero)
                Marshal.FreeHGlobal(plainTextBlob.pbData);

            if (cipherTextBlob.pbData != IntPtr.Zero)
                Marshal.FreeHGlobal(cipherTextBlob.pbData);

            if (entropyBlob.pbData != IntPtr.Zero)
                Marshal.FreeHGlobal(entropyBlob.pbData);
        }
    }

    public static Byte[] Encrypt(this SecureString self, int length)
    {
        IntPtr unmanagedString = Marshal.SecureStringToBSTR(self);//get the basic unmanaged string representation
        int len = Marshal.ReadInt32(unmanagedString, -4) + 2; //get the length of the bstr structure from it's index, this doesn't include the null bytes hence + 2.

        DATA_BLOB plainTextBlob = new DATA_BLOB();//initiate our blobs
        DATA_BLOB cipherTextBlob = new DATA_BLOB();
        DATA_BLOB entropyTextBlob = new DATA_BLOB();
        CRYPTPROTECT_PROMPTSTRUCT prompt = new CRYPTPROTECT_PROMPTSTRUCT();

        try
        {
            //Processing code here. Resist the urge to Marshal.PtrToStringBSTR.

            plainTextBlob.cbData = len;//set the length of the array
            plainTextBlob.pbData = unmanagedString;//set the data to our pointer.
            InitPrompt(ref prompt);

            // Call DPAPI to encrypt data.

            bool success = CryptProtectData(ref plainTextBlob, null, ref entropyTextBlob, IntPtr.Zero, ref prompt, CryptProtectFlags.CRYPTPROTECT_UI_FORBIDDEN, ref cipherTextBlob);

            // Check the result.
            if (!success)
            {
                // If operation failed, retrieve last Win32 error.
                int errCode = Marshal.GetLastWin32Error();

                // Win32Exception will contain error message corresponding
                // to the Windows error code.
                throw new Exception(
                    "CryptProtectData failed.", new Win32Exception(errCode));
            }

            // Allocate memory to hold ciphertext.
            byte[] cipherTextBytes = new byte[cipherTextBlob.cbData];

            // Copy ciphertext from the BLOB to a byte array.
            Marshal.Copy(cipherTextBlob.pbData,
                            cipherTextBytes,
                            0,
                            cipherTextBlob.cbData);

            // Return the result.
            return cipherTextBytes;

        }
        finally
        {
            Marshal.ZeroFreeBSTR(unmanagedString); //free the buffer holding our secret

            if (cipherTextBlob.pbData != IntPtr.Zero)
                Marshal.FreeHGlobal(cipherTextBlob.pbData);

        }
    }


    //The below regions are all the PInvoke signatures. These translate C++ commands into usable C# commands. These come directly from pinvoke.net 
    #region PInvokeSignatures

    /// <summary>
    /// Initializes empty prompt structure.
    /// </summary>
    /// <param name="ps">
    /// Prompt parameter (which we do not actually need).
    /// </param>
    private static void InitPrompt(ref CRYPTPROTECT_PROMPTSTRUCT ps)
    {
        ps.cbSize = Marshal.SizeOf(typeof(CRYPTPROTECT_PROMPTSTRUCT));
        ps.dwPromptFlags = 0;
        ps.hwndApp = IntPtr.Zero;
        ps.szPrompt = null;
    }

    [DllImport("Crypt32.dll", SetLastError = true, CharSet = System.Runtime.InteropServices.CharSet.Auto)]
    [return: MarshalAs(UnmanagedType.Bool)]
    private static extern bool CryptProtectData(
        ref DATA_BLOB pDataIn,
        String szDataDescr,
        ref DATA_BLOB pOptionalEntropy,
        IntPtr pvReserved,
        ref CRYPTPROTECT_PROMPTSTRUCT pPromptStruct,
        CryptProtectFlags dwFlags,
        ref DATA_BLOB pDataOut
    );

    [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
    private struct DATA_BLOB
    {
        public int cbData;
        public IntPtr pbData;
    }

    [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
    private struct CRYPTPROTECT_PROMPTSTRUCT
    {
        public int cbSize;
        public CryptProtectPromptFlags dwPromptFlags;
        public IntPtr hwndApp;
        public String szPrompt;
    }

    [Flags]
    private enum CryptProtectPromptFlags
    {
        // prompt on unprotect
        CRYPTPROTECT_PROMPT_ON_UNPROTECT = 0x1,

        // prompt on protect
        CRYPTPROTECT_PROMPT_ON_PROTECT = 0x2
    }

    [Flags]
    private enum CryptProtectFlags
    {
        // for remote-access situations where ui is not an option
        // if UI was specified on protect or unprotect operation, the call
        // will fail and GetLastError() will indicate ERROR_PASSWORD_RESTRICTION
        CRYPTPROTECT_UI_FORBIDDEN = 0x1,

        // per machine protected data -- any user on machine where CryptProtectData
        // took place may CryptUnprotectData
        CRYPTPROTECT_LOCAL_MACHINE = 0x4,

        // force credential synchronize during CryptProtectData()
        // Synchronize is only operation that occurs during this operation
        CRYPTPROTECT_CRED_SYNC = 0x8,

        // Generate an Audit on protect and unprotect operations
        CRYPTPROTECT_AUDIT = 0x10,

        // Protect data with a non-recoverable key
        CRYPTPROTECT_NO_RECOVERY = 0x20,


        // Verify the protection of a protected blob
        CRYPTPROTECT_VERIFY_PROTECTION = 0x40
    }

    [DllImport("Crypt32.dll", SetLastError = true, CharSet = System.Runtime.InteropServices.CharSet.Auto)]
    [return: MarshalAs(UnmanagedType.Bool)]
    private static extern bool CryptUnprotectData(
        ref DATA_BLOB pDataIn,
        StringBuilder szDataDescr,
        ref DATA_BLOB pOptionalEntropy,
        IntPtr pvReserved,
        ref CRYPTPROTECT_PROMPTSTRUCT pPromptStruct,
        CryptProtectFlags dwFlags,
        ref DATA_BLOB pDataOut
    );

    #endregion
}

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.