65.9K
CodeProject 正在变化。 阅读更多。
Home

SerializableSecureString

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.88/5 (12投票s)

2013年10月19日

CPOL

5分钟阅读

viewsIcon

31349

downloadIcon

541

SerializableSecureString 封装了 System.SecureString,使其能够作为 DataMember,同时保持最高的数据安全性。

引言

在现代编程中,处理敏感数据已成为常态,并且以一致的方式处理应该是任何安全编程最佳实践的一部分。在许多情况下,还必须满足监管或行业标准机构的要求。例如,信用卡行业的数据安全标准甚至规定了对传输中数据和静态数据(包括内存中瞬时静态数据)的期望。.NET 提供了 System.SecureString 来处理后者,WCF 则提供传输和消息加密来处理前者。然而,这两者不能简单地在 DataContract 中结合,因为 SecureString 是故意设计为不可序列化的。本文介绍了一个 SecureString 的封装器,它可以使用预配置的 X.509 证书来保护序列化时的数据内容,从而能够作为 DataMember数据安全标准 允许以下形式的 DataContracts

        [DataContract]
        public class SecureDataContract
        {
            :
            [DataMember]
            public SerializableSecureString SecureContent { get; private set; }
        }

这种方法论的编程简洁性是这项工作的主要动力。除了通过构造函数注入将 DataContract 绑定到正确的 X.509 证书外,开发人员无需担心安全数据存储或传输的任何其他加密问题;他们只需以常规方式与类型进行交互。通过将数据存储问题与数据消耗问题分离,安全数据处理的范围仅限于代码中将敏感数据提取为明文以实现业务问题的区域。范围限制有助于制定最佳实践、数据处理指南,甚至可能自动检查潜在的暴露。

基础知识

SerializableSecureString 封装了一个 private 成员变量 Content。该封装器实现了 IXmlSerializable 以挂钩序列化过程,并实现了 IDisposable 以进行 Content 的清理工作,这是 SecureString 的最佳实践。一些辅助方法允许使用代码访问底层 Content 或其属性。

    [Serializable]
    public class SerializableSecureString : IXmlSerializable, IDisposable
    {
        :
        SecureString Content = new SecureString();
        :
        #region Content helpers
        /// <summary>
        ///     Clear and load initial data into the SecureString
        /// </summary>
        /// <param name="clearText"></param>
        public void Initialize(string clearText)
        {
            Content.Clear();
            Append(clearText);
        }

        /// <summary>
        ///     Append to current SecureString content
        ///     Throws ReadOnlyContentException if SecureString has been locked
        /// </summary>
        /// <param name="clearText">text to append</param>
        public void Append(string clearText)
        {
            if (clearText != null)
            {
                if (Content.IsReadOnly()) throw new ReadOnlyContentException();
                // iterate the loop to avoid making a copy of the
                // cleartext accidentally
                foreach (char t in clearText)
                {
                    Content.AppendChar(t);
                }
            }
        }

        /// <summary>
        ///     Retrieve the data currently within the SecureString
        ///     Caller has the responsibility for keeping this data hopefully
        ///     in a GC0 collection usage.
        /// </summary>
        /// <returns>ClearText from SecureString</returns>
        public string Extract()
        {
            IntPtr bstr = Marshal.SecureStringToBSTR(Content);
            string copiedText = Marshal.PtrToStringAuto(bstr);
            Marshal.ZeroFreeBSTR(bstr);
            return copiedText;
        }

        /// <summary>
        ///     Seal the SecureString from further modification
        /// </summary>
        public void MakeReadOnly()
        {
            Content.MakeReadOnly();
        }

        /// <summary>
        ///     Check read only state of SecureString
        /// </summary>
        /// <returns>false if data can be added</returns>
        public bool IsReadOnly()
        {
            return Content.IsReadOnly();
        }

        /// <summary>
        ///     Return a copy of the internal SecureString
        /// </summary>
        /// <returns></returns>
        public SecureString CloneData()
        {
            return Content.Copy();
        }
        #endregion
         :
}

另外两个辅助函数提供对配置的证书存储的访问。这些在实现 DataContract 构造函数以初始化 SerializableSecureString 时非常有用,如下文所示。

    [Serializable]
    public class SerializableSecureString : IXmlSerializable, IDisposable
    {
        :
        public IEnumerable<X509Certificate2> Find(string thumbPrint)
        {
            return Find(thumbPrint, CertificateStore);
        }

        public static IEnumerable<X509Certificate2> Find(string thumbPrint, X509Store store)
        {
            store.Open(OpenFlags.ReadOnly);
            IEnumerable<X509Certificate2> results =
                store.Certificates.Find(X509FindType.FindByThumbprint, thumbPrint, false)
                     .Cast<X509Certificate2>();
            store.Close();
            return results;
        }
         :
    }

IXmlSerializable

IXmlSerializable 的实现始于定义序列化后的表示模式。

<SecureStringEnvelope>
    <EncryptionContext>
        <ClearText />
        <IsReadOnly>false</IsReadOnly>
        <CertificateContext>
            <ThumbPrint />
            <Store />
            <Location />
        </CertificateContext>
    </EncryptionContext>
</SecureStringEnvelope>"

此模式的相关内部元素包括:

  • ClearText:序列化时 Content 的值。
  • IsReadOnly:序列化时 Content 的只读状态。
  • ThumbPrint:用于在序列化过程中加密 ClearText 的 X.509 证书的缩略图。
  • Store:指定证书缩略图所安装的证书存储的名称。
  • LocationStore 的位置,可以是 CurrentUserLocalMachine

这些模式元素在封装器中具有相关或后备属性,这些属性在序列化过程中起着至关重要的作用。

    public class SerializableSecureString : IXmlSerializable, IDisposable
    {
        :
        // Design note: The ge">    public class SerializableSecureString : IXmlSerializable, IDisposable
    {
        :
        #region IXmlSerializable
        public XmlSchema GetSchema()
        {
            return null;
        }

        public void ReadXml(XmlReader reader)
        {
            var doc = new XmlDocument();
            // position reader to the start of our serialization stream
            // because at entry we are in the DataContractSerializer's envelope
            reader.MoveToContent();

           while (reader.Read())
           {
                if (reader.NodeType == XmlNodeType.Element
                    && reader.LocalName == _Envelope)
                {
                    break;
                }
            }

            doc.LoadXml(reader.ReadOuterXml());
            Value = doc;
        }

        public void WriteXml(XmlWriter writer)
        {
            Value.Save(writer);
        }

        #endregion
    }
}

XML 加密

W3C 关于 XML 加密的建议描述了加密 XML 文档中一个或多个元素的过程。基本过程是在输出时用加密的等效内容替换文档的某个片段,并在输入时反向执行此过程。在 .NET 中,此标准由 EncryptedXml 类(System.Security.Cryptography.Xml)实现。虽然支持各种加密机制,但在使用 X.509 证书指定加密过程时,EncryptedXml 的最直接用法是可用的。封装器的私有 Value 属性访问器利用 EncryptedXml 来保护从 SecureString 中提取的 ClearText

        /// <summary>
        ///     Moves Content into and out of serialization streams
        /// </summary>
        private XmlDocument Value
        {
            // Move the SecureString into the serialization in encrypted xml
            get
            {
                var doc = new XmlDocument();
                doc.LoadXml(SerializationXsi);
                // copy Content into document
                //A priori knowledge of location of ClearText element
                XmlNode elmt = doc.DocumentElement.FirstChild.FirstChild;
                elmt.InnerText = Extract();
                var xmlEnc = new EncryptedXml(doc);
                EncryptedData encrData = xmlEnc.Encrypt(elmt as XmlElement, Certificate);
                EncryptedXml.ReplaceElement(elmt as XmlElement, encrData, false);
                LoadCertificateContext(doc);
                return doc;
            }
            set
            {
                // Decrypt the xml and reload the SecureString
                XmlDocument doc = value;
                bool bReadOnly = UnloadCertificateContext(doc);
                // Decrypt the document
                var encrXml = new EncryptedXml(doc);
                encrXml.DecryptDocument();
                // load the secure string
                Initialize(doc.DocumentElement.FirstChild.FirstChild.InnerText);
                if (bReadOnly) Content.MakeReadOnly();
            }
        }

get 访问器加载一个序列化模式实例,其中包含 Content 的当前 ClearText 值,调用 EncryptedXml 加密 ClearText 节点,然后从当前成员变量填充模式的其余部分。set 访问器则反向执行此过程,首先反序列化当前成员变量,然后解密 ClearText 并重新加载 ContentContentIsReadOnly() 状态会得到保留。请注意,如果传入序列化实例中指定的证书未安装在指定位置和证书存储中,set 访问器将抛出 CertificateException。当前实现要求发送方和接收方使用相同的证书,并且发送方和接收方都可以访问证书的私钥。下图展示了加密效果的前后对比。

以前 操作后

任何基于字段的加密策略都存在一个问题:有效载荷膨胀,尤其是使用 X.509 证书时。然而,有几个因素可以抵消这种膨胀,使解决方案可行:

  • 数据在传输过程中高度可压缩。
  • 由于基础类库对证书提供了深入的支持,因此编程实现相对简单。
  • 在处理证书时,DataContract 语法是熟悉且直接的。
  • 经验表明,每个 DataContract 中通常只有少数(一到两个)加密字段。
  • 在现场部署中,使用 X.509 证书来传递加密细节具有显著的管理简便性。

DataContract 建议

当为包含 SerializableSecureString 的类实现 DataContract 时,建议执行以下步骤:

  • 提供一个 private 的默认构造函数。序列化器需要一个默认构造函数,但它不必是 public
  • SerializableSecureString 属性的 set 访问器设置为 private
  • 实现一个或多个参数化构造函数,用于创建带有适当证书的 SerializableSecureString 成员。

下面的示例说明了这些要点。

        [DataContract]
        public class SecureDataContract
        {
            private SecureDataContract() // required by serializer
            {
            }

            public SecureDataContract(X509Certificate2 cert, X509Store store)
            {
                SecureContent = new SerializableSecureString(cert, store);
            }

            public SecureDataContract(SerializableSecureString secureString)
            {
                SecureContent = secureString;
            }

            public SecureDataContract(string thumbPrint, X509Store store)
            {
                X509Certificate2 cert = SerializableSecureString.Find(
                    thumbPrint, store).FirstOrDefault();
                SecureContent = new SerializableSecureString(cert, store);
            }

            [DataMember]

            public SerializableSecureString SecureContent { get; private set; }
        }
    }

兴趣点

在单元测试环境中处理 X.509 证书是一种很好的尝试,尤其是在涉及私钥的情况下。为了避免繁琐,开发了一个证书生成器来提供动态创建的 X.509 证书。该生成器利用BouncyCastle 加密库来生成创建 X509Certificate2 实例的输入,以供单元测试使用。X509Certificate2 提供了一个方便的 API 来处理证书的公钥和私钥,包括在加密服务提供程序中持久化或销毁私钥的后备存储。该生成器作为一个单独的程序集 X509Generators 进行打包。

    namespace X509Generators
    {
        :
        public static X509Certificate2 GenerateCertificate(StoreLocation location,
                                                           string subjectName,
                                                           SecureString password,
                                                           ExtendedKeyUsageEnum purpose =
                                                               ExtendedKeyUsageEnum.EmailProtection)
        {
            // Create a BouncyCastle keypair and X509Certificate holding the public key
            GeneratedCertificate bouncyCert = GenerateCertificate(subjectName, purpose);
            // Create an exportable X509Certificate2 to return from the BouncyCastle cert
            var cert = new X509Certificate2(bouncyCert.Certificate.GetEncoded(), password,
                                            X509KeyStorageFlags.Exportable);
           :
           return cert;
        }
        :
   }

在 DEBUG 配置中,SerializedSecureString 提供了一个测试构造函数,该构造函数从动态生成的证书初始化自身。_autogenerated 标志表示使用了动态生成器。

    public class SerializableSecureString : IXmlSerializable, IDisposable
    {
    :
 :#if DEBUG
        /// <summary>
        ///     Testing constructor
        ///     Automatically generates and installs a new certificate for use in a
        ///     test run. Certificate is uninstalled and private key deleted on Dispose().
        /// </summary>
        /// <param name="purpose"></param>
        public SerializableSecureString(
            ExtendedKeyUsageEnum purpose = ExtendedKeyUsageEnum.EmailProtection)
        {
            CertificateStore = new X509Store(StoreName.My, StoreLocation.CurrentUser);
            Content = new SecureString();
            Certificate = X509Generation.GenerateCertificate(CertificateStore.Location, GetType().Name,
                                            new SecureString());
            _autoGenerated = true;
            ThumbPrint = Certificate.Thumbprint;
            SetCertificateInStore(Certificate);
        }#endif
        :
    }
#endif

_autogenerated 标志在 IDisposable 实现中使用,以检测动态证书并在证书被 Disposed 时删除后备私钥。

    public SerializableSecureString(SerializableSecureString instance, bool bCopyData = false)
        {
            :
        protected virtual void Dispose(bool isDisposing)
        {
            if (!_disposed)
            {
                if (isDisposing)
                {
                    Content.Dispose(); // kill the protected data now
                    if (Certificate != null && _autoGenerated)
                    {                        // house keep the autogenerated certificates from disk
                        // _autogenerated flag can be true only in DEBUG build
                        if (Certificate.HasPrivateKey)
                        {
                            // delete the private key
                            var pvKey = Certificate.PrivateKey as RSACryptoServiceProvider;
                            pvKey.PersistKeyInCsp = false;
                            pvKey.Clear();
                        }
                        CertificateStore.Open(OpenFlags.ReadWrite);
                        CertificateStore.Remove(Certificate);
                        CertificateStore.Close();
                    }
                }
                _disposed = true;
            }
        }
        :
    }

csproj 中的生成条件导致了对 X509Generators 项目的必要引用。这已手动编辑到 csproj 中,如下所示。

  <Choose>
    <When Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
      <ItemGroup>
        <ProjectReference Include="X509Generators\X509Generators.csproj">
          <Project>{483bd6f8-b1f8-402d-aca4-9ea13a173baf}</Project>
          <Name>X509Generators</Name>
        </ProjectReference>
      </ItemGroup>
    </When>
  </Choose>">

因此,X509Generator 程序集对于 Release 版本不是必需的。

Using the Code

代码作为单个 VS2012 解决方案提供,包含三个项目:

  • SerializableSecureString:实现
  • X509Generators:动态证书生成器
  • SerialzableSecureStringTests:xUnit 测试以执行代码

在成功生成后运行单元测试即可执行代码。

历史

  • 2013年10月19日:初始版本
© . All rights reserved.