【问题标题】:How can I encrypt selected properties when serializing my objects?序列化对象时如何加密选定的属性?
【发布时间】:2017-02-11 20:50:39
【问题描述】:

我正在使用 JSON 在我的应用程序中存储某些设置。一些设置包含敏感信息(例如密码),而其他设置不敏感。理想情况下,我希望能够序列化我的对象,其中敏感属性自动加密,同时保持非敏感设置可读。有没有办法使用 Json.Net 做到这一点?我没有看到任何与加密相关的设置。

【问题讨论】:

  • 你可以通过编写代码来做到这一点。现在,请编辑您的问题,解释当您尝试这样做时出了什么问题。
  • @Scott Chamberlain,出了什么问题:所有属性都“按原样”序列化,我别无选择。
  • @user626528 - 我认为 Scott 的观点是我们不是代码编写服务。您需要尝试编写代码,当您遇到困难时提出问题(并发布您的代码)。
  • @Enigmativity,我只需要解释走什么路线。
  • @Enigmativity,我认为 没有抓住重点。我没有要求编码,我要求解释如何做我需要的一般想法。

标签: c# .net json.net


【解决方案1】:

Json.Net 没有内置加密。如果您希望能够在序列化过程中进行加密和解密,您将需要编写一些自定义代码。一种方法是将自定义IContractResolverIValueProvider 结合使用。值提供者为您提供了一个钩子,您可以在其中转换序列化过程中的值,而合同解析器使您可以控制何时何地应用值提供者。他们可以一起为您提供您正在寻找的解决方案。

以下是您需要的代码示例。首先,您会注意到我定义了一个新的[JsonEncrypt] 属性;这将用于指示您要加密的属性。 EncryptedStringPropertyResolver 类扩展了 Json.Net 提供的 DefaultContractResolver。我已经覆盖了CreateProperties() 方法,以便我可以检查由基本解析器创建的JsonProperty 对象,并将我的自定义EncryptedStringValueProvider 的一个实例附加到任何应用了[JsonEncrypt] 属性的字符串属性。 EncryptedStringValueProvider 稍后通过各自的 GetValue()SetValue() 方法处理目标字符串属性的实际加密/解密。

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);
            }
        }

    }
}

一旦您有了解析器,下一步就是将自定义[JsonEncrypt] 属性应用于您希望在序列化期间加密的类中的字符串属性。例如,这是一个可能代表用户的人为类:

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; }
}

最后一步是将自定义解析器注入到序列化过程中。为此,创建一个新的JsonSerializerSettings 实例,然后将ContractResolver 属性设置为自定义解析器的一个新实例。将设置传递给JsonConvert.SerializeObject()DeserializeObject() 方法,一切正常。

这是一个往返演示:

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);
        }
    }
}

输出:

----- Serialize -----
{
  "UserName": "jschmoe",
  "UserPassword": "sK2RvqT6F61Oib1ZittGBlv8xgylMEHoZ+1TuOeYhXQ=",
  "FavoriteColor": "atomic tangerine",
  "CreditCardNumber": "qz44JVAoJEFsBIGntHuPIgF1sYJ0uyYSCKdYbMzrmfkGorxgZMx3Uiv+VNbIrbPR"
}

----- Deserialize -----
UserName: jschmoe
UserPassword: Hunter2
FavoriteColor: atomic tangerine
CreditCardNumber: 1234567898765432

小提琴:https://dotnetfiddle.net/trsiQc

【讨论】:

  • “为什么不加密整个 JSON”——因为我使用 JSON 来存储设置并且完全加密文件会很不方便。
  • 我在尝试使用 int 或 DateTime 类型时遇到了加密问题。
  • @Velkumar 获得帮助的最佳方式是提出一个新问题,详细描述该问题是什么以及在尝试解决问题时遇到的问题。请参阅帮助中心的How do I ask a good question?。如果需要,您可以添加回此问题或答案的链接以提供上下文。 cmets 区域不适合提出新问题。
  • @Velkumar 这是因为您可以“仅”加密 stringbyte[] 数据类型。要加密 DateTime 或数字,需要一种新方法,例如,您可以调整合约解析器以以不同方式处理这些数据类型。否则,您的所有敏感信息都需要在 string
  • @BrianRogers 你知道什么会导致SetValue(object target, object value) 不被调用吗?如果我只有一个带有自定义合同解析器的 JsonSettings,它会在每个属性上设置一个 cstom 值提供程序。我在序列化和反序列化时使用设置,我可以看到在序列化时正在执行GetValue,但在反序列化时从不执行SetValue。对于反序列化,这个值提供者有什么问题吗?
【解决方案2】:

虽然@Brian 的解决方案非常聪明,但我不喜欢自定义ContractResolver 的复杂性。我将 Brian 的代码转换为 JsonConverter,因此您的代码将变为

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; }
}

我已将(相当长的)EncryptingJsonConverter 发布为Gistblogged about it

【讨论】:

    【解决方案3】:

    我的解决方案:

        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);
            }
    

    (可以不那么冗长)

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2013-07-29
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2013-02-12
      相关资源
      最近更新 更多