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";
string cipher = plainText.Encrypt();
Assert.AreNotEqual(plainText, cipher);
string decrypted = cipher.Decrypt();
Assert.AreEqual(plainText, decrypted);
SecureString plainSecure = plainText.ToSecureString();
Assert.AreEqual(plainText, plainSecure.Unwrap());
string cipherFromSecure = plainSecure.Encrypt();
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
{
public static class SecurityExtensions
{
private const DataProtectionScope Scope = DataProtectionScope.CurrentUser;
public static string Encrypt(this string plainText)
{
if (plainText == null) throw new ArgumentNullException("plainText");
var data = Encoding.Unicode.GetBytes(plainText);
byte[] encrypted = ProtectedData.Protect(data, null, Scope);
return Convert.ToBase64String(encrypted);
}
public static string Decrypt(this string cipher)
{
if (cipher == null) throw new ArgumentNullException("cipher");
byte[] data = Convert.FromBase64String(cipher);
byte[] decrypted = ProtectedData.Unprotect(data, null, Scope);
return Encoding.Unicode.GetString(decrypted);
}
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 Convert.ToBase64String(encrypted);
}
finally
{
Marshal.ZeroFreeCoTaskMemUnicode(ptr);
}
}
public static SecureString DecryptSecure(this string cipher)
{
if (cipher == null) throw new ArgumentNullException("cipher");
byte[] data = Convert.FromBase64String(cipher);
byte[] decrypted = ProtectedData.Unprotect(data, null, Scope);
SecureString ss = new SecureString();
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]);
}
ss.MakeReadOnly();
return ss;
}
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;
}
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);
}
}
public static bool IsNullOrEmpty(this SecureString value)
{
return value == null || value.Length == 0;
}
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
{
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);
}
}
}
}
codeproject