In this age of identity theft, data security is more important than ever. Sensitive data such as credit card numbers should always be encrypted. In this recipe, I'll show you how to use NHibernate to encrypt a single property.
EncryptedStringExample
.NHibernate.dll
, log4net.dll
, and NHibernate.Castle.ByteCode.dll
.IEncryptor
with the following three method definitions:public interface IEncryptor { string Encrypt(string plainText); string Decrypt(string encryptedText); string EncryptionKey { get; set; } }
IEncryptor
named SymmetricEncryptorBase
using the following code:public abstract class SymmetricEncryptorBase : IEncryptor { private readonly SymmetricAlgorithm _cryptoProvider; private byte[] _myBytes; protected SymmetricEncryptorBase( SymmetricAlgorithm cryptoProvider) { _cryptoProvider = cryptoProvider; } public string EncryptionKey { get; set; } public string Encrypt(string plainText) { var bytes = GetEncryptionKeyBytes(); using (var memoryStream = new MemoryStream()) { ICryptoTransform encryptor = _cryptoProvider .CreateEncryptor(bytes, bytes); using (var cryptoStream = new CryptoStream( memoryStream, encryptor, CryptoStreamMode.Write)) { using (var writer = new StreamWriter(cryptoStream)) { writer.Write(plainText); writer.Flush(); cryptoStream.FlushFinalBlock(); return Convert.ToBase64String( memoryStream.GetBuffer(), 0, (int) memoryStream.Length); } } } } private byte[] GetEncryptionKeyBytes() { if (_myBytes == null) _myBytes = Encoding.ASCII.GetBytes(EncryptionKey); return _myBytes; } public string Decrypt(string encryptedText) { var bytes = GetEncryptionKeyBytes(); using (var memoryStream = new MemoryStream( Convert.FromBase64String(encryptedText))) { ICryptoTransform decryptor = _cryptoProvider .CreateDecryptor(bytes, bytes); using (var cryptoStream = new CryptoStream( memoryStream, decryptor, CryptoStreamMode.Read)) { using (var reader = new StreamReader(cryptoStream)) { return reader.ReadToEnd(); } } } } }
DESEncryptor
with the following code:public class DESEncryptor : SymmetricEncryptorBase { public DESEncryptor() : base(new DESCryptoServiceProvider()) { } }
IUserType
named EncryptedString
using the following code:public class EncryptedString : IUserType, IParameterizedType { private IEncryptor _encryptor; public object NullSafeGet( IDataReader rs, string[] names, object owner) { //treat for the posibility of null values object passwordString = NHibernateUtil.String.NullSafeGet(rs, names[0]); if (passwordString != null) { return _encryptor.Decrypt((string)passwordString); } return null; } public void NullSafeSet( IDbCommand cmd, object value, int index) { if (value == null) { NHibernateUtil.String.NullSafeSet(cmd, null, index); return; } string encryptedValue = _encryptor.Encrypt((string)value); NHibernateUtil.String.NullSafeSet( cmd, encryptedValue, index); } public object DeepCopy(object value) { return value == null ? null : string.Copy((string)value); } public object Replace(object original, object target, object owner) { return original; } public object Assemble(object cached, object owner) { return DeepCopy(cached); } public object Disassemble(object value) { return DeepCopy(value); } public SqlType[] SqlTypes { get { return new[] { new SqlType(DbType.String) }; } } public Type ReturnedType { get { return typeof(string); } } public bool IsMutable { get { return false; } } public new bool Equals(object x, object y) { if (ReferenceEquals(x, y)) { return true; } if (x == null || y == null) { return false; } return x.Equals(y); } public int GetHashCode(object x) { if (x == null) { throw new ArgumentNullException("x"); } return x.GetHashCode(); } public void SetParameterValues( IDictionary<string, string> parameters) { if (parameters != null) { var encryptorTypeName = parameters["encryptor"]; _encryptor = !string.IsNullOrEmpty(encryptorTypeName) ? (IEncryptor) Instantiate(encryptorTypeName) : new DESEncryptor(); var encryptionKey = parameters["encryptionKey"]; if (!string.IsNullOrEmpty(encryptionKey)) _encryptor.EncryptionKey = encryptionKey; } else { _encryptor = new DESEncryptor(); } } private static object Instantiate(string typeName) { var type = Type.GetType(typeName); return Activator.CreateInstance(type); } }
Account
with the following properties:public virtual Guid Id { get; set; } public virtual string EMail { get; set; } public virtual string Name { get; set; } public virtual string CardNumber { get; set; } public virtual int ExpirationMonth { get; set; } public virtual int ExpirationYear { get; set; } public virtual string ZipCode { get; set; }
<?xml version="1.0" encoding="utf-8" ?> <hibernate-mapping xmlns="urn:nhibernate-mapping-2.2" assembly="EncryptedStringExample" namespace="EncryptedStringExample"> <typedef name="encrypted" class="EncryptedStringExample.EncryptedString, EncryptedStringExample"> <param name="encryptor"> EncryptedStringExample.DESEncryptor, EncryptedStringExample </param> <param name="encryptionKey">12345678</param> </typedef> <class name="Account"> <id name="Id"> <generator class="guid.comb" /> </id> <property name="Name" not-null="true" /> <property name="EMail" not-null="true" /> <property name="CardNumber" not-null="true" type="encrypted" /> <property name="ExpirationMonth" not-null="true" /> <property name="ExpirationYear" not-null="true" /> <property name="ZipCode" not-null="true" /> </class> </hibernate-mapping>
As we saw in the Mapping Enumerations recipe in Chapter 1, we can set the type attribute on a property to specify a class used for converting data between our application and the database. We'll use this to encrypt and decrypt our credit card number.
Our Account
class mapping defined a type using the <typedef>
element. The name
attribute defines a nickname for our encryption
type. This nickname matches the type
attribute on our CardNumber
property's mapping. The
class
attribute specifies the .NET class that will be used to convert our data in standard namespace.typeName
, assemblyName
format.
Our EncryptedString
type happens to use two parameters—encryptor
and
encryptionKey
. These are set in the mapping as well.
DESEncryptor
, our implementation of IEncryptor
, uses DESCryptoServiceProvider
to encrypt and decrypt our data. This is one of the symmetric encryption algorithms available in the .NET framework.
Our EncryptedString
type implements IUserType
, NHibernate's interface for defining custom types. When implementing IUserType
, NullSafeGet
is responsible for reading data from the ADO.NET data reader and returning an appropriate object, in this case, a string. In our EncryptedString
, we read the encrypted data, use IEncryptor
to decrypt it, and return the unencrypted string, which is used to set the CardNumber
property. NullSafeSet
takes some value, in this case, our unencrypted CardNumber
, and sets a parameter on the ADO.NET command
. In EncryptedString
, we encrypt the card number before setting it on the command
. The SqlTypes
property returns an array representing the types of each database field used to store this user type. In our case, we have a single string field. The ReturnedType
property returns the .NET type. Since our CardNumber
is a string, we return the string type.
EncryptedString
also implements IParameterizedType
. The SetParameterValues
method provides a dictionary of parameters from the mapping document. From that dictionary, we get the IEncryptor
implementation to use, as well as the encryption key.
There are three categories of encryption algorithms. A symmetric algorithm uses the same key to encrypt and decrypt the data. Because our application is responsible for the encryption and decryption, this type of algorithm makes the most sense.
An asymmetric algorithm encrypts data using a pair of keys—one public and one private. The key pairs are generated in such a way that the public key used to encrypt the data gives no hint as to the private key required to decrypt that data. It's not necessary to keep the public key a secret. This type of algorithm is typically used when the data is encrypted in one location and decrypted in another. System A generates the keys, holds onto the private key, and shares the public key with the System B. System B encrypts data with the public key. Only System A has the private key necessary to decrypt the data.
Finally, a hash algorithm is used when the data doesn't need to be decrypted. With a hash algorithm, the original data can't be calculated from the hash value, the chance of finding different data with the same hash value is extremely small, and even a slight change in the data produces a wildly different hash value. This type of algorithm is typically used for passwords. We store the hash of the real password. When a user logs in, we don't need to know what the real password is, only that the attempted password matches the real password. We hash the attempted password. If the hash of the attempted password matches the previously stored hash of the real password, we know that the passwords match.
18.117.186.46