创建一个安全高效的备份客户端






3.56/5 (9投票s)
描述了在需要安全高效地复制数据时可能有用的底层概念和技术。
引言
常规的增量备份会检查文件的存档属性,以确定它是否已更改,因此需要备份。然后,备份程序仅备份已更改的文件。当存储到磁带时,这意味着增量备份的数据往往分布在多盘磁带上,从而导致恢复耗时。这就是为什么偶尔进行完全备份是必要的。
但是,如果我们能够以最小的备份介质空间进行增量备份文件呢?如果我们能做到这一点,那么磁盘存储的相对成本(已经很低)可能会接近于磁带存储的成本,从而使得以易于访问的方式将所有备份数据存储在硬盘上成为可能。本文提出了一种实现方法,并简要介绍了我们可以使用的一些 .NET 代码。
此外,我们还解决了确保私人数据保持私密的问题,即使是那些可以直接访问备份介质的人也无法访问。
背景
简而言之,对于那些不熟悉某些基本概念的人来说:
- 哈希算法 - 一种为字节序列创建唯一签名的算法。无法将哈希逆向工程回文件本身。文件的一丁点改动都会导致完全不同的哈希。常见的例子是 MD5 和 SHA1。
- 对称加密 - 使用单个私钥对数据进行加密。要解密文件,需要私钥。
- IV - 初始化向量,在对称算法中用作加密的起点,确保相同的数据永远不会以相同的方式加密两次。这提高了加密的安全性。要解密文件,**不**需要拥有 IV。
文件签名
首先使用的技术是加密哈希函数。文件的内容加密哈希几乎可以保证是该文件的唯一签名。它非常小,通常为 128 到 160 位。我们可以将这些签名与服务器的备份日志进行匹配,以确定文件是否已被备份。
如果备份系统知道文件 ABC.DLL 已经存在,那么就无需第二个人再次备份 ABC.DLL - 我们可以直接创建一个指向原始文件的指针。这样,我们就可以允许人们通过网络备份整个硬盘驱动器,从而最大限度地减少带宽和存储使用量。我们只会存储实际数据,不会重复。
我们可以创建一个巨大的、一次写入的硬盘驱动器,并将我们的数据尽可能高效地打包。
要备份文件,程序首先计算文件内容的哈希。这很简单。我们只需使用 FileStream
对象打开文件,然后将其传递给返回哈希的哈希函数。
private byte[] SHA1Hash(FileStream inFile)
{
SHA1 sha1 = new SHA1CryptoServiceProvider();
byte[] hash = sha1.ComputeHash(inFile);
//reset back to the start of the file
inFile.Seek(0, SeekOrigin.Begin);
return hash;
}
这很简单。唯一的复杂之处在于,计算完哈希后,我们将文件流重置回开头。这是为了进一步处理文件。
加密文件
其他一些代码(未显示)然后查询服务器以确定该文件是否存在。如果我们确定需要将文件发送到服务器,那么我们需要加密该文件。但是,如果您还记得,文件需要对所有拥有它的客户端都可访问 - 否则,我们将存储相同的数据两次 = 不好 :(
我们使用所有拥有该文件的客户端都知道的密钥来加密该文件 - 该文件的 MD5 哈希。
RC2 rc2 = new RC2CryptoServiceProvider();
//use the MD5 hash as the key for the encryption
rc2.Key = MD5Hash(inFile);
rc2.GenerateIV();
ICryptoTransform encryptTransform = rc2.CreateEncryptor();
CryptoStream encryptStream = new CryptoStream(
netStream,
encryptTransform,
CryptoStreamMode.Write);
在上面的代码块中,我们使用一个 MD5Hash
函数(未显示,因为它基本上与 SHA1Hash
相同)根据文件内容生成文件的密钥。我们告诉 RC2 提供程序生成一个随机 IV,然后设置一个 CryptoStream
对象,直接将数据加密到已在其他地方设置好的 NetworkStream
。
CryptoStream
构造函数的参数是:
netStream
- 一个已在其他地方设置好的NetworkStream
对象。数据将发送到这里。encryptTransform
- 我们创建的加密器。它将执行加密数据的操作。CryptoStreamMode.Write
- 一个标志,告诉流我们正在写入netStream
,而不是从中读取。
我喜欢分层流的概念,因为它消除了对磁盘或内存中中间存储的需求。上面代码中的数据流如下:
FileStream --> CryptoStream --> NetworkStream
最终,当我们下载(恢复)和解密文件时,我们可以遵循反向流程:
NetworkStream --> CryptoStream --> FIleStream
钥匙串
说拥有文件的客户端拥有解密它所需的密钥说起来很好,但当客户端不再拥有该文件时,这种情况就有点站不住脚了。毕竟,这是一个**备份**系统,如果他们需要下载文件,我们可以肯定他们不再有能力计算其内容的哈希。
因此,客户端将每个备份文件的指纹(以及其他相关的 FileInfo
数据)存储在一个虚拟钥匙串中。这个钥匙串被序列化到一个文件,并用客户端的密码进行加密。然后将其发送到服务器进行安全保管。因此,只要用户能够访问他们的密码,他们就可以从服务器检索他们的钥匙串,并用它来解锁他们想要恢复的任何文件。
关注点
只将文件发送到文件服务器一次的想法并不新鲜。Bell Labs 的一些人已经提出了 这个想法。
他们不加密数据,但他们确实使用了 SHA1 签名来表示数据块(许多块可以构成一个文件)。我给他们点赞,因为他们提出了使用 SHA1 而不是 MD5 作为指纹的想法 - 他们费尽心思计算了非唯一指纹的概率,即 10^-20,假设存储了 exabyte 的数据,其中包含 8K 个带指纹的块。
最终,我也计划将较大的文件分成块。这对大型文件(如数据库)来说效果会更好,因为在任何特定时间只有一个部分文件会发生变化。
脚注
提供下载的文件是我在备份客户端上使用的一个类文件。它包含了本文讨论的所有代码,以及一些与服务器通信所需的其他代码。
在后续的文章中,我计划更详细地讨论与服务器的通信。关于一个可扩展的 TCP 套接字服务器。但等我把它做出来再写吧 :)