SerializableSecureString






4.88/5 (12投票s)
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
:指定证书缩略图所安装的证书存储的名称。Location
:Store
的位置,可以是CurrentUser
或LocalMachine
。
这些模式元素在封装器中具有相关或后备属性,这些属性在序列化过程中起着至关重要的作用。
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
并重新加载 Content
。Content
的 IsReadOnly()
状态会得到保留。请注意,如果传入序列化实例中指定的证书未安装在指定位置和证书存储中,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日:初始版本