String Encryption using DPAPI and Extension Methods





4.00/5 (7 votes)
Extension methods that encrypt / decrypt strings through the Windows Data Protection API (DPAPI) with optional usage of secure strings that protect data in memory
The Windows Data Protection API (DPAPI) is a great technology to securely encrypt user or machine specific data without having to worry about an encryption key. Since .NET 2.0, DPAPI is part of the .NET Framework, so encrypting data is as easy as this:
public static byte[] Encrypt(byte[] data)
{
var scope = DataProtectionScope.CurrentUser;
return ProtectedData.Protect(data, null, scope);
}
As you can see, the Protect
method of the ProtectedData class takes binary input and returns a byte array that contains the encrypted data. This means that you’ll have to do some conversions when dealing with string
s, and the result of the encryption is a byte array anyway.
I recently published NetDrives, a tool that relies on the DPAPI to encrypt user passwords that are stored on disk in XML format. Accordingly, I didn’t want to deal with binary data at all: Both input and output were supposed to be string
s, which is why I came up with a few extension methods that nicely wrap string
encryptions for me:
Basic String Encryption
In case in-memory protection is not an issue and you just need to encrypt/decrypt string
s (e.g. to store encrypted data in a configuration file), you just need two extension methods. First, in order to encrypt a string
, just invoke the Encrypt
extension method:
string password = "hello world";
string encrypted = password.Encrypt();
Encrypt
returns you the encrypted data, represented as base64
encoded string
. In order to get your password back, just invoke the Decrypt
extension method:
string plainText = encrypted.Decrypt();
Managed Strings vs. SecureString
The above methods are convenient to encrypt sensitive data that is supposed to be serialized or transmitted in any way. They do, however, not protect data at runtime as the decrypted string
s remain in memory. In case this is an issue, you should revert to the SecureString rather than using plain string
s (but keep in mind that this may lure you into a false sense of security!).
Accordingly, I also created extension methods that use SecureString
instances rather than managed string
s and allow you to wrap / unwrap string
s quite easily. Here’s a test that shows off the various conversions:
Attention: Always keep in mind that once you are dealing with a managed string
(such as the plainText
variable below), your code can be compromised! Accordingly, the ToSecureString
/ Unwrap
methods should be treated carefully.
[Test]
public void Encryption_And_Decryption_Cycle_Should_Return_Original_Value()
{
string plainText = "this is a password";
//encrypt plain text
string cipher = plainText.Encrypt();
Assert.AreNotEqual(plainText, cipher);
//decrypt cipher into managed string
string decrypted = cipher.Decrypt();
Assert.AreEqual(plainText, decrypted);
//create a SecureString from the plain text
SecureString plainSecure = plainText.ToSecureString();
//test unwrapping of a SecureString
Assert.AreEqual(plainText, plainSecure.Unwrap());
//encrypt the string that is wrapped into the SecureString
string cipherFromSecure = plainSecure.Encrypt();
//decrypt the cipher that was created from the the SecureString
Assert.AreEqual(plainText, cipherFromSecure.Decrypt());
}
Implementation
Here’s the class that provides the extension methods including a few helper methods that facilitate dealing with SecureString
(e.g. SecureString.IsNullOrEmpty
).
Note that you need to set an assembly reference to the System.Security
assembly. Also keep in mind that the class always performs DPAPI encryption with user scope. You might want to provide some additional overloads in order to support encryption that uses the context of the machine rather than the users. The same goes for the optional entropy that is not used here all for simplicity.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Security;
using System.Security.Cryptography;
using System.Text;
namespace Hardcodet.NetDrives.Platform
{
/// <summary>
/// Provides extension methods that deal with
/// string encryption/decryption and
/// <see cref="SecureString"/> encapsulation.
/// </summary>
public static class SecurityExtensions
{
/// <summary>
/// Specifies the data protection scope of the DPAPI.
/// </summary>
private const DataProtectionScope Scope = DataProtectionScope.CurrentUser;
/// <summary>
/// Encrypts a given password and returns the encrypted data
/// as a base64 string.
/// </summary>
/// <param name="plainText">An unencrypted string that needs
/// to be secured.</param>
/// <returns>A base64 encoded string that represents the encrypted
/// binary data.
/// </returns>
/// <remarks>This solution is not really secure as we are
/// keeping strings in memory. If runtime protection is essential,
/// <see cref="SecureString"/> should be used.</remarks>
/// <exception cref="ArgumentNullException">If <paramref name="plainText"/>
/// is a null reference.</exception>
public static string Encrypt(this string plainText)
{
if (plainText == null) throw new ArgumentNullException("plainText");
//encrypt data
var data = Encoding.Unicode.GetBytes(plainText);
byte[] encrypted = ProtectedData.Protect(data, null, Scope);
//return as base64 string
return Convert.ToBase64String(encrypted);
}
/// <summary>
/// Decrypts a given string.
/// </summary>
/// <param name="cipher">A base64 encoded string that was created
/// through the <see cref="Encrypt(string)"/> or
/// <see cref="Encrypt(SecureString)"/> extension methods.</param>
/// <returns>The decrypted string.</returns>
/// <remarks>Keep in mind that the decrypted string remains in memory
/// and makes your application vulnerable per se. If runtime protection
/// is essential, <see cref="SecureString"/> should be used.</remarks>
/// <exception cref="ArgumentNullException">If <paramref name="cipher"/>
/// is a null reference.</exception>
public static string Decrypt(this string cipher)
{
if (cipher == null) throw new ArgumentNullException("cipher");
//parse base64 string
byte[] data = Convert.FromBase64String(cipher);
//decrypt data
byte[] decrypted = ProtectedData.Unprotect(data, null, Scope);
return Encoding.Unicode.GetString(decrypted);
}
/// <summary>
/// Encrypts the contents of a secure string.
/// </summary>
/// <param name="value">An unencrypted string that needs
/// to be secured.</param>
/// <returns>A base64 encoded string that represents the encrypted
/// binary data.
/// </returns>
/// <exception cref="ArgumentNullException">If <paramref name="value"/>
/// is a null reference.</exception>
public static string Encrypt(this SecureString value)
{
if (value == null) throw new ArgumentNullException("value");
IntPtr ptr = Marshal.SecureStringToCoTaskMemUnicode(value);
try
{
char[] buffer = new char[value.Length];
Marshal.Copy(ptr, buffer, 0, value.Length);
byte[] data = Encoding.Unicode.GetBytes(buffer);
byte[] encrypted = ProtectedData.Protect(data, null, Scope);
//return as base64 string
return Convert.ToBase64String(encrypted);
}
finally
{
Marshal.ZeroFreeCoTaskMemUnicode(ptr);
}
}
/// <summary>
/// Decrypts a base64 encrypted string and returns the decrpyted data
/// wrapped into a <see cref="SecureString"/> instance.
/// </summary>
/// <param name="cipher">A base64 encoded string that was created
/// through the <see cref="Encrypt(string)"/> or
/// <see cref="Encrypt(SecureString)"/> extension methods.</param>
/// <returns>The decrypted string, wrapped into a
/// <see cref="SecureString"/> instance.</returns>
/// <exception cref="ArgumentNullException">If <paramref name="cipher"/>
/// is a null reference.</exception>
public static SecureString DecryptSecure(this string cipher)
{
if (cipher == null) throw new ArgumentNullException("cipher");
//parse base64 string
byte[] data = Convert.FromBase64String(cipher);
//decrypt data
byte[] decrypted = ProtectedData.Unprotect(data, null, Scope);
SecureString ss = new SecureString();
//parse characters one by one - doesn't change the fact that
//we have them in memory however...
int count = Encoding.Unicode.GetCharCount(decrypted);
int bc = decrypted.Length/count;
for (int i = 0; i < count; i++)
{
ss.AppendChar(Encoding.Unicode.GetChars(decrypted, i*bc, bc)[0]);
}
//mark as read-only
ss.MakeReadOnly();
return ss;
}
/// <summary>
/// Wraps a managed string into a <see cref="SecureString"/>
/// instance.
/// </summary>
/// <param name="value">A string or char sequence that
/// should be encapsulated.</param>
/// <returns>A <see cref="SecureString"/> that encapsulates the
/// submitted value.</returns>
/// <exception cref="ArgumentNullException">If <paramref name="value"/>
/// is a null reference.</exception>
public static SecureString ToSecureString(this IEnumerable<char> value)
{
if (value == null) throw new ArgumentNullException("value");
var secured = new SecureString();
var charArray = value.ToArray();
for (int i = 0; i < charArray.Length; i++)
{
secured.AppendChar(charArray[i]);
}
secured.MakeReadOnly();
return secured;
}
/// <summary>
/// Unwraps the contents of a secured string and
/// returns the contained value.
/// </summary>
/// <param name="value"></param>
/// <returns></returns>
/// <remarks>Be aware that the unwrapped managed string can be
/// extracted from memory.</remarks>
/// <exception cref="ArgumentNullException">If <paramref name="value"/>
/// is a null reference.</exception>
public static string Unwrap(this SecureString value)
{
if (value == null) throw new ArgumentNullException("value");
IntPtr ptr = Marshal.SecureStringToCoTaskMemUnicode(value);
try
{
return Marshal.PtrToStringUni(ptr);
}
finally
{
Marshal.ZeroFreeCoTaskMemUnicode(ptr);
}
}
/// <summary>
/// Checks whether a <see cref="SecureString"/> is either
/// null or has a <see cref="SecureString.Length"/> of 0.
/// </summary>
/// <param name="value">The secure string to be inspected.</param>
/// <returns>True if the string is either null or empty.</returns>
public static bool IsNullOrEmpty(this SecureString value)
{
return value == null || value.Length == 0;
}
/// <summary>
/// Performs bytewise comparison of two secure strings.
/// </summary>
/// <param name="value"></param>
/// <param name="other"></param>
/// <returns>True if the strings are equal.</returns>
public static bool Matches(this SecureString value, SecureString other)
{
if (value == null && other == null) return true;
if (value == null || other == null) return false;
if (value.Length != other.Length) return false;
if (value.Length == 0 && other.Length == 0) return true;
IntPtr ptrA = Marshal.SecureStringToCoTaskMemUnicode(value);
IntPtr ptrB = Marshal.SecureStringToCoTaskMemUnicode(other);
try
{
//parse characters one by one - doesn't change the fact that
//we have them in memory however...
byte byteA = 1;
byte byteB = 1;
int index = 0;
while (((char)byteA) != '\0' && ((char)byteB) != '\0')
{
byteA = Marshal.ReadByte(ptrA, index);
byteB = Marshal.ReadByte(ptrB, index);
if (byteA != byteB) return false;
index += 2;
}
return true;
}
finally
{
Marshal.ZeroFreeCoTaskMemUnicode(ptrA);
Marshal.ZeroFreeCoTaskMemUnicode(ptrB);
}
}
}
}