How can I encrypt selected properties when serializing my objects?
Solution 1:
Json.Net does not have built-in encryption. If you want to be able to encrypt and decrypt during the serialization process, you will need to write some custom code. One approach is to use a custom IContractResolver
in conjunction with an IValueProvider
. The value provider gives you a hook where you can transform values within the serialization process, while the contract resolver gives you control over when and where the value provider gets applied. Together, they can give you the solution you are looking for.
Below is an example of the code you would need. First off, you'll notice I've defined a new [JsonEncrypt]
attribute; this will be used to indicate which properties you want to be encrypted. The EncryptedStringPropertyResolver
class extends the DefaultContractResolver
provided by Json.Net. I've overridden the CreateProperties()
method so that I can inspect the JsonProperty
objects created by the base resolver and attach an instance of my custom EncryptedStringValueProvider
to any string properties which have the [JsonEncrypt]
attribute applied. The EncryptedStringValueProvider
later handles the actual encryption/decryption of the target string properties via the respective GetValue()
and SetValue()
methods.
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Security.Cryptography;
using System.Text;
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
[AttributeUsage(AttributeTargets.Property)]
public class JsonEncryptAttribute : Attribute
{
}
public class EncryptedStringPropertyResolver : DefaultContractResolver
{
private byte[] encryptionKeyBytes;
public EncryptedStringPropertyResolver(string encryptionKey)
{
if (encryptionKey == null)
throw new ArgumentNullException("encryptionKey");
// Hash the key to ensure it is exactly 256 bits long, as required by AES-256
using (SHA256Managed sha = new SHA256Managed())
{
this.encryptionKeyBytes =
sha.ComputeHash(Encoding.UTF8.GetBytes(encryptionKey));
}
}
protected override IList<JsonProperty> CreateProperties(Type type, MemberSerialization memberSerialization)
{
IList<JsonProperty> props = base.CreateProperties(type, memberSerialization);
// Find all string properties that have a [JsonEncrypt] attribute applied
// and attach an EncryptedStringValueProvider instance to them
foreach (JsonProperty prop in props.Where(p => p.PropertyType == typeof(string)))
{
PropertyInfo pi = type.GetProperty(prop.UnderlyingName);
if (pi != null && pi.GetCustomAttribute(typeof(JsonEncryptAttribute), true) != null)
{
prop.ValueProvider =
new EncryptedStringValueProvider(pi, encryptionKeyBytes);
}
}
return props;
}
class EncryptedStringValueProvider : IValueProvider
{
PropertyInfo targetProperty;
private byte[] encryptionKey;
public EncryptedStringValueProvider(PropertyInfo targetProperty, byte[] encryptionKey)
{
this.targetProperty = targetProperty;
this.encryptionKey = encryptionKey;
}
// GetValue is called by Json.Net during serialization.
// The target parameter has the object from which to read the unencrypted string;
// the return value is an encrypted string that gets written to the JSON
public object GetValue(object target)
{
string value = (string)targetProperty.GetValue(target);
byte[] buffer = Encoding.UTF8.GetBytes(value);
using (MemoryStream inputStream = new MemoryStream(buffer, false))
using (MemoryStream outputStream = new MemoryStream())
using (AesManaged aes = new AesManaged { Key = encryptionKey })
{
byte[] iv = aes.IV; // first access generates a new IV
outputStream.Write(iv, 0, iv.Length);
outputStream.Flush();
ICryptoTransform encryptor = aes.CreateEncryptor(encryptionKey, iv);
using (CryptoStream cryptoStream = new CryptoStream(outputStream, encryptor, CryptoStreamMode.Write))
{
inputStream.CopyTo(cryptoStream);
}
return Convert.ToBase64String(outputStream.ToArray());
}
}
// SetValue gets called by Json.Net during deserialization.
// The value parameter has the encrypted value read from the JSON;
// target is the object on which to set the decrypted value.
public void SetValue(object target, object value)
{
byte[] buffer = Convert.FromBase64String((string)value);
using (MemoryStream inputStream = new MemoryStream(buffer, false))
using (MemoryStream outputStream = new MemoryStream())
using (AesManaged aes = new AesManaged { Key = encryptionKey })
{
byte[] iv = new byte[16];
int bytesRead = inputStream.Read(iv, 0, 16);
if (bytesRead < 16)
{
throw new CryptographicException("IV is missing or invalid.");
}
ICryptoTransform decryptor = aes.CreateDecryptor(encryptionKey, iv);
using (CryptoStream cryptoStream = new CryptoStream(inputStream, decryptor, CryptoStreamMode.Read))
{
cryptoStream.CopyTo(outputStream);
}
string decryptedValue = Encoding.UTF8.GetString(outputStream.ToArray());
targetProperty.SetValue(target, decryptedValue);
}
}
}
}
Once you have the resolver in place, the next step is to apply the custom [JsonEncrypt]
attribute to the string properties within your classes that you wish to be encrypted during serialization. For example, here is a contrived class that might represent a user:
public class UserInfo
{
public string UserName { get; set; }
[JsonEncrypt]
public string UserPassword { get; set; }
public string FavoriteColor { get; set; }
[JsonEncrypt]
public string CreditCardNumber { get; set; }
}
The last step is to inject the custom resolver into the serialization process. To do that, create a new JsonSerializerSettings
instance, then set the ContractResolver
property to a new instance of the custom resolver. Pass the settings to the JsonConvert.SerializeObject()
or DeserializeObject()
methods and everything should just work.
Here is a round-trip demo:
public class Program
{
public static void Main(string[] args)
{
try
{
UserInfo user = new UserInfo
{
UserName = "jschmoe",
UserPassword = "Hunter2",
FavoriteColor = "atomic tangerine",
CreditCardNumber = "1234567898765432",
};
// Note: in production code you should not hardcode the encryption
// key into the application-- instead, consider using the Data Protection
// API (DPAPI) to store the key. .Net provides access to this API via
// the ProtectedData class.
JsonSerializerSettings settings = new JsonSerializerSettings();
settings.Formatting = Formatting.Indented;
settings.ContractResolver = new EncryptedStringPropertyResolver("My-Sup3r-Secr3t-Key");
Console.WriteLine("----- Serialize -----");
string json = JsonConvert.SerializeObject(user, settings);
Console.WriteLine(json);
Console.WriteLine();
Console.WriteLine("----- Deserialize -----");
UserInfo user2 = JsonConvert.DeserializeObject<UserInfo>(json, settings);
Console.WriteLine("UserName: " + user2.UserName);
Console.WriteLine("UserPassword: " + user2.UserPassword);
Console.WriteLine("FavoriteColor: " + user2.FavoriteColor);
Console.WriteLine("CreditCardNumber: " + user2.CreditCardNumber);
}
catch (Exception ex)
{
Console.WriteLine(ex.GetType().Name + ": " + ex.Message);
}
}
}
Output:
----- Serialize -----
{
"UserName": "jschmoe",
"UserPassword": "sK2RvqT6F61Oib1ZittGBlv8xgylMEHoZ+1TuOeYhXQ=",
"FavoriteColor": "atomic tangerine",
"CreditCardNumber": "qz44JVAoJEFsBIGntHuPIgF1sYJ0uyYSCKdYbMzrmfkGorxgZMx3Uiv+VNbIrbPR"
}
----- Deserialize -----
UserName: jschmoe
UserPassword: Hunter2
FavoriteColor: atomic tangerine
CreditCardNumber: 1234567898765432
Fiddle: https://dotnetfiddle.net/trsiQc
Solution 2:
Though @Brian's solution is quite clever, I don't like the complexity of a custom ContractResolver
. I converted Brian's code to a JsonConverter
, so your code would become
public class UserInfo
{
public string UserName { get; set; }
[JsonConverter(typeof(EncryptingJsonConverter), "My-Sup3r-Secr3t-Key")]
public string UserPassword { get; set; }
public string FavoriteColor { get; set; }
[JsonConverter(typeof(EncryptingJsonConverter), "My-Sup3r-Secr3t-Key")]
public string CreditCardNumber { get; set; }
}
I've posted the (quite lengthy) EncryptingJsonConverter
as a Gist and also blogged about it.
Solution 3:
My solution:
public string PasswordEncrypted { get; set; }
[JsonIgnore]
public string Password
{
get
{
var encrypted = Convert.FromBase64String(PasswordEncrypted);
var data = ProtectedData.Unprotect(encrypted, AdditionalEntropy, DataProtectionScope.LocalMachine);
var res = Encoding.UTF8.GetString(data);
return res;
}
set
{
var data = Encoding.UTF8.GetBytes(value);
var encrypted = ProtectedData.Protect(data, AdditionalEntropy, DataProtectionScope.LocalMachine);
PasswordEncrypted = Convert.ToBase64String(encrypted);
}
(can be made less verbose)