diff --git a/translations/restic-references/restic-design.md b/translations/restic-references/restic-design.md new file mode 100644 index 0000000..07e3f7c --- /dev/null +++ b/translations/restic-references/restic-design.md @@ -0,0 +1,503 @@ +restic design +------------- + +原文链接:[restic/references/design][https://restic.readthedocs.io/en/latest/100_references.html] + + + +### 术语 + +本节介绍本文档中所使用的术语。 + +***Repository(仓库)***:备份过程中生成的所有数据都以结构化的形式发送并存储在仓库中。例如仓库可以保存在一个具有多个子目录具有层次结构的文件系统中。一个仓库的实现必须能够完成一系列的操作,例如列出内容。 + +***Blob(Binary Large Object,不可变的二进制对象)***:一个二进制对象包含一定数量的字节数据(加密的文件内容)和其标识信息(如数据的 SHA-256 哈希值、长度等信息)。 + +***Pack(打包对象)***:一个打包对象包含一个或多个二进制对象(Blob),例如在一个文件中将多个二进制对象(Blob)打包在一起 。 + +***Snapshot (快照)***:快照是指在某个时间点上所备份的文件或目录的状态。这里的状态指的是文件的内容和元数据(如文件或目录的名称、内容和修改时间等) + +***Storage ID(对象 ID)***:对象 ID 是所有保存在仓库中的对象的 SHA-256 哈希值。只有通过这个 ID 才可以从仓库中加载文件。 + + + +### Repository 格式 + +所有的数据都保存在 restic 仓库中。一个仓库能够存储几种不同类型的对象,我们可以根据对象 ID(参见术语解释)来获得这些对象。仓库中的所有文件只会被写入一次,之后就不再修改。写入是以原子方式进行的以防止并发操作读取到不完整的文件。这种设计使得多个客户端可以并行访问甚至对仓库进行写入操作。只有 `prune` 操作会从仓库中删除数据。 + +仓库由几个目录和一个名为 `config` 的顶级(位于根目录)文件组成。在仓库中的其他文件的名称都是以对象的 `Storage ID` 的小写十六进制形式命名(即对象内容的 SHA-256 哈希值)。这种设计让我们可以简单地验证文件是否发生了意外修改。只需在文件上运行 `sha256sum` 命令,并将其输出与文件名进行比较即可。如果一个文件名的前缀在同一目录下是唯一的,则可以使用该前缀而不是完整的文件名查找该文件。 + +除了存储在 `key` 目录中的文件外,其他文件都使用 AES-256-CTR(CTR,计数器模式)进行加密。加密数据的完整性是由 Poly1305-AES 消息认证码(有时也被称为”签名“)来保证的。 + +每个加密文件的前 16 个字节是加密算法所使用的初始化向量 (IV),其后是加密数据并在最后拼接 16 字节的 MAC(消息认证码)。我们将这种形式定义为:`IV || CIPHERTEXT || MAC`。加密的额外开销为 32 个字节。对于每个文件,都会选择一个随机的初始化向量 IV。 + +`config` 配置文件也是以这种方式加密,原始内容如下 JSON 文档所示: + +```json +{ + "version": 2, + "id": "5956a3f67a6230d4a92cefb29529f10196c7d92582ec305fd71ff6d331d6271b", + "chunker_polynomial": "25b468838dcb75" +} +``` + +解密后,restic 首先检查 `version` 字段的值是否正确,如果错误则直接退出程序。目前,`version` 字段的值应该为 `1` 或 `2`。关于仓库格式的更新日志请查阅最后的的「更新」章节。 + + `id` 字段是一个随机 32 字节组成的唯一 ID,并以十六进制编码记录保存,这个 ID 唯一标识一个仓库。字段 `chunker_polynomial` 是用于将大文件分割成更小的区块的参数(见下文)。 + + + +#### 仓库布局 + +使用 `local` 和 `sftp` 后端的仓库是通过常规文件系统中的文件和目录来实现的。这两种后端的目录结构是相同的,其他远程后端也是基于此实现的。 + +仓库的基本布局如下所示: + +```text +/tmp/restic-repo +├── config +├── data +│ ├── 21 +│ │ └── 2159dd48f8a24f33c307b750592773f8b71ff8d11452132a7b2e2a6a01611be1 +│ ├── 32 +│ │ └── 32ea976bc30771cebad8285cd99120ac8786f9ffd42141d452458089985043a5 +│ ├── 59 +│ │ └── 59fe4bcde59bd6222eba87795e35a90d82cd2f138a27b6835032b7b58173a426 +│ ├── 73 +│ │ └── 73d04e6125cf3c28a299cc2f3cca3b78ceac396e4fcf9575e34536b26782413c +│ [...] +├── index +│ ├── c38f5fb68307c6a3e3aa945d556e325dc38f5fb68307c6a3e3aa945d556e325d +│ └── ca171b1b7394d90d330b265d90f506f9984043b342525f019788f97e745c71fd +├── keys +│ └── b02de829beeb3c01a63e6b25cbd421a98fef144f03b9a02e46eff9e2ca3f0bd7 +├── locks +├── snapshots +│ └── 22a5af1bdc6e616f8a29579458c49627e01b32210d09adb288d1ecda7c5711ec +└── tmp +``` + +使用 `local` 后端的仓库可以通过 `restic init` 命令来初始化,如下: + +```bash +$ restic -r /tmp/restic-repo init +``` + +`local` 和 `sftp` 后端可以自动检测并兼容后续所描述的其他布局,这有助于将远程仓库挂载到本地(例如,通过 `fuse` 进行访问)。也可以通过指定选项 `-o local.layout=default` 来覆盖对仓库布局的自动检测,有效值是 `default` 和 `s3legacy`。为 `sftp` 后端配置自动检测的选项名为 `sftp.layout`,为 `s3` 后端配置自动检测的选项名则是 `s3.layout`。 + + + +#### S3 兼容的布局 + +不幸的是,在开发过程中 `Amazon S3` 后端使用的路径略有不同(目录 `key`、`lock`和 `snapshot` 的名称使用的是单数而不是复数)。并且打包对象( `Pack` )直接存储在 `data` 目录下。`S3` 兼容的仓库布局如下所示: + +```text +/config +/data + ├── 2159dd48f8a24f33c307b750592773f8b71ff8d11452132a7b2e2a6a01611be1 + ├── 32ea976bc30771cebad8285cd99120ac8786f9ffd42141d452458089985043a5 + ├── 59fe4bcde59bd6222eba87795e35a90d82cd2f138a27b6835032b7b58173a426 + ├── 73d04e6125cf3c28a299cc2f3cca3b78ceac396e4fcf9575e34536b26782413c +[...] +/index + ├── c38f5fb68307c6a3e3aa945d556e325dc38f5fb68307c6a3e3aa945d556e325d + └── ca171b1b7394d90d330b265d90f506f9984043b342525f019788f97e745c71fd +/key + └── b02de829beeb3c01a63e6b25cbd421a98fef144f03b9a02e46eff9e2ca3f0bd7 +/lock +/snapshot + └── 22a5af1bdc6e616f8a29579458c49627e01b32210d09adb288d1ecda7c5711ec +``` + +`S3` 后端能兼容这两种形式,出于兼容性考虑,新后端将始终使用 `default` 布局。 + + + +### 打包对象格式 + +除 `Key` 和 `Pack` 文件外,存储库中的其他文件仅包含原始数据,存储的形式为 `IV || Ciphertext || MAC` 。打包对象(`Pack`)文件可能包含一个或多个二进制对象。一个打包对象(`Pack`)的结构如下: + +```text +EncryptedBlob1 || ... || EncryptedBlobN || EncryptedHeader || Header_Length +``` + +在打包对象(`Pack`)的末尾是该对象的元数据,其中描述了该文件的内容。对象元数据经过加密和身份验证。`Header_Length` 字段是对象元数据的长度,并以小端字节序的四字节整形进行编码保存。将对象元数据放到文件的末尾有助于在备份时直接将读取到的数据直接写入到流中。这可以降低代码实现的复杂度,同时避免在完成对打包对象(`Pack`)的写入后再回过头来重写 `Header`。 + +所有二进制对象(`EncryptedBlob1`、`EncryptedBlobN` 等)都经过独立地加密和身份验证。方便在重建仓库时不用再次对二进制对象进行加密(在拆分二进制对象时如果复用同一个 `IV` 时则需要重新生成 `IV` 并再次加密)。除此之外,它还支持高效的索引,因为只需要读取对象元数据就可以找到打包对象(`Pack`)中包含的二进制对象。由于文件元数据是经过身份验证的,因此可以检查元数据的的真实性,而无需读取完整的打包对象(`Pack`)。 + +打包对象(`Pack`)的元数据解密后的格式如下所示: + +```text +Type_Blob1 || Data_Blob1 || +[...] +Type_BlobN || Data_BlobN || +``` + +`Type_Blob` 字段使用一个字节进行表示,后面的 `Data_Blob` 取决于类型。我们定义了以下 `Type_Blob` : + +| 类型 | 含义 | `Data_Blob` 格式定义 | +| ------ | ---------------- | ------------------------------------------------------------ | +| `0b00` | 文件对象 | `Length(encrypted_blob) || Hash(plaintext_blob)` | +| `0b01` | 文件树对象 | `Length(encrypted_blob) || Hash(plaintext_blob)` | +| `0b10` | 压缩的文件对象 | `Length(encrypted_blob) || Length(plaintext_blob) || Hash(plaintext_blob)` | +| `0b11` | 压缩的文件树对象 | `Length(encrypted_blob) || Length(plaintext_blob) || Hash(plaintext_blob)` | + +以上数据足以计算打包对象(`Pack`)中所有二进制对象的偏移量。所有的长度字段都被编码为小端字节序的四字节整数形式。在「`Data_Blob` 格式定义」列中,`Length(plaintext_blob)` 表示未加密且未压缩之前二进制对象的长度。 + +除此之外的其他类型都是无效的,未来我们可能会添加更多类型支持。以上支持压缩的对象类型仅支持 `version = 2` 的仓库。文件和文件树对象(`Tree Blob`)使用 `zstandard` 压缩算法进行压缩。 + +在 `version = 1` 的仓库中,文件对象(`Data Blob`)和文件树对象(`Tree Blob`)**应**存储在单独的打包对象(`Pack`)中。而在 `version = 2` 的仓库中,文件对象(`Data Blob`)和文件树对象(`Tree Blob`)**必须**被存储到单独的打包对象(`Pack`)中。只有具有相同类型的二进制对象才可以被合并存储到一个打包对象(`Pack`)中。 + +当需要重建索引或解析没有索引的打包对象(`Pack`)时,首先必须读取末尾的四个字节才能知道对象元数据的长度。随后我们可以读取和解析文件元数据,这可以得到 Pack 文件中包含的所有 Blob 的明文哈希值、类型、偏移量和相对应的长度。 + + + +### 未打包的对象格式 + +独立的索引、锁和快照文件和文件(树)对象一样经过加密和身份验证后保存在仓库中。所以这些文件的格式也是 `IV || Ciphertext || MAC` 。在 `version = 1` 的仓库中,文本文件总是以 JSON 文档的形式存储,且必须是一个对象或数组类型。 + +`version = 2` 的仓库增加了对压缩的支持,文本文件现在以一个一字节的元数据开头,其用于标明文件内容是否是纯文本,并支持在未来版本提供更多的选择。其存储格式为:`encoding_version || data` ,其中 `encoding_version` 是一个字节长度的编码类型。为了向后兼容,`encoding_version = 0x5b('[') | 0x7b('{')` 表示应该将该内容视为 JSON 的纯文本文件。 + +在新版本中,`encoding_version = 2` 表示 `data` 为一个使用 `zstandard` 压缩算法压缩后的 JSON 文档。 + + + +### 索引 + +索引文件包含仓库中文件、文件树对象(`Tree Blob`)以及所在的打包对象(`Pack`)的相关信息,索引文件本身也将保存在仓库中。当本地缓存的索引文件不可用时会重新下载并重建索引。索引文件的编码格式在「未打包的数据文件格式」中介绍。索引文件的明文是一个 JSON 文档,示例如下: + +```json +{ + "supersedes": [ + "ed54ae36197f4745ebc4b54d10e0f623eaaaedd03013eb7ae90df881b7781452" + ], + "packs": [ + { + "id": "73d04e6125cf3c28a299cc2f3cca3b78ceac396e4fcf9575e34536b26782413c", + "blobs": [ + { + "id": "3ec79977ef0cf5de7b08cd12b874cd0f62bbaf7f07f3497a5b1bbcc8cb39b1ce", + "type": "data", + "offset": 0, + "length": 38, + // no 'uncompressed_length' as blob is not compressed + }, + { + "id": "9ccb846e60d90d4eb915848add7aa7ea1e4bbabfc60e573db9f7bfb2789afbae", + "type": "tree", + "offset": 38, + "length": 112, + "uncompressed_length": 511, + }, + { + "id": "d3dc577b4ffd38cc4b32122cabf8655a0223ed22edfd93b353dc0c3f2b0fdf66", + "type": "data", + "offset": 150, + "length": 123, + "uncompressed_length": 234, + } + ] + }, [...] + ] +} +``` + +此 JSON 文件列出了打包对象(`Pack`)及其包含的二进制对象信息。在这个示例中,打包对象(`Pack`) `73d04e61` 中包含两个文件对象(`Data Blob`)和一个文件树对象(`Tree Blob`),二进制对象的哈希值也随之给出。`length` 字段对应打包对象(`Pack`)元数据中的 `Length(encrypted_blob)` 字段。字段 `uncompressed_length` 仅存在于压缩的二进制对象描述中,因此在 `version = 1` 的仓库中并不存在,这个值对应打包对象(`Pack`)元数组中的 `Length(plaintext_blob)`。 + +字段 `supersedes` 列出的是已经被当前索引对象取代的索引对象的对象 ID(`Storage ID`)。这会在重新打包索引对象时发生,例如删除旧快照或合并打包对象(`Pack`)。 + +仓库中可能有任意数量的索引对象,包含不相交的一组打包对象(`Pack`)的信息。在一个索引对象中描述的打包对象(`Pack`)的数量是经过计算的,主要为了让索引对象的大小保持在 8 MiB 以下。 + + + +### 密钥、加密和消息认证码 + +所有存储在仓库中的数据使用 AES-256-CTR 进行加密,并使用 Poly1305-AES 进行身份验证。为了加密新数据,需要生成 16 字节长度的随机数。这个随机数即用于 CTR 模式的初始化向量 IV ,同时也被用作初始化 Poly1305 。完整的加密操作需要三个密钥:用于 AES-256-CTR 加密的 32 字节密钥、在 Poly1305-AES 中 AES 加密所需的 16 字节密钥和 Poly1305 所需的 16 字节密钥。更多细节可以参考 Dan Bernstein 的 《[The Poly1305-AES message-authentication code](https://cr.yp.to/mac/poly1305-20050329.pdf)》。使用 AES-256-CTR 加密文件数据后,在密文上计算消息认证码(MAC),最后将其组装为 `IV || CIPHERTEXT || MAC` 的形式并保存到仓库中。 + +目录 `keys` 中包含仓库的密钥文件。这里面是简单的 JSON 文件,其中包含从用户密码派生出的仓库主密码和进行消息身份验证所需的所有参数。仓库的密钥文件可以通过 python 的 json 模块实现格式化输出: + +```bash +$ python -mjson.tool /tmp/restic-repo/keys/b02de82* +{ + "hostname": "kasimir", + "username": "fd0" + "kdf": "scrypt", + "N": 65536, + "r": 8, + "p": 1, + "created": "2015-01-02T18:10:13.48307196+01:00", + "data": "tGwYeKoM0C4j4/9DFrVEmMGAldvEn/+iKC3te/QE/6ox/V4qz58FUOgMa0Bb1cIJ6asrypCx/Ti/pRXCPHLDkIJbNYd2ybC+fLhFIJVLCvkMS+trdywsUkglUbTbi+7+Ldsul5jpAj9vTZ25ajDc+4FKtWEcCWL5ICAOoTAxnPgT+Lh8ByGQBH6KbdWabqamLzTRWxePFoYuxa7yXgmj9A==", + "salt": "uW4fEI1+IOzj7ED9mVor+yTSJFd68DGlGOeLgJELYsTU5ikhG/83/+jGd4KKAaQdSrsfzrdOhAMftTSih5Ux6w==", +} +``` + +当通过 restic 打开仓库时,会提示用户输入仓库的密码。然后使用 scrypt(密钥派生函数,KDF)算法和提供的参数(N、r、p 和 salt)派生出 64 个字节的主密钥。前 32 个字节用作 AES-256-CTR 的加密密钥,其后 32 个字节用于生成消息验证 Poly1305-AES 的密钥。最后的 32 字节密钥被分为 16 字节的 AES 密钥 `k` 和 16 字节的密钥 `r`。密钥 `r` 需要经过掩码(masked)之后才可以用于 Poly1305(详见论文)。 + +以上这三个密钥被用来解密通过 AES-256-CTR 和 Poly1305 加密的 `data` 字段中包含的数据(Base64 编码)。如果用户密码错误或者密钥文件被修改,我们计算出的 MAC 将会与 `data` 最后的 16 字节不匹配,restic 将会检测并报告这个错误。如果一些正确则 `data` 字段可以解密并得到一个 JSON 文件,其中包含仓库的主密码和消息验证所使用的密钥(以 Base64 进行编码)。 + +```bash +$ restic -r /tmp/restic-repo cat masterkey +{ + "mac": { + "k": "evFWd9wWlndL9jc501268g==", + "r": "E9eEDnSJZgqwTOkDtOp+Dw==" + }, + "encrypt": "UQCqa0lKZ94PygPxMRqkePTZnHRYh1k1pX2k2lM2v3Q=", +} +``` + +仓库中的所有加密对象都使用这个主密钥进行加密和验证。加密使用的是 AES-256-CTR 算法。而消息认证则使用的是 Poly1305-AES 算法。 + +仓库可以有多个不同的用户密码,每个密码都有一个配对的密钥文件。这样,更改密码就不需要重新加密所有对象了。 + + + +### 快照 + +一个快照表示某一个时刻所有的文件与文件夹的状态,对于每次备份都会创建一个新的快照。快照实际上就是一个存储在 `snapshots` 目录下的 JSON 文档。该文件使用「未打包的数据文件格式」节中描述的方式进行编码。文件名就是其对象 ID(`Storage ID`)。这个对象 ID 在仓库内是唯一的且用于唯一标识一个快照。 + +`restic cat snapshot` 命令可以解密并打印快照文件中的内容。 + +```bash +$ restic -r /tmp/restic-repo cat snapshot 251c2e58 +enter password for repository: +{ + "time": "2015-01-02T18:10:50.895208559+01:00", + "tree": "2da81727b6585232894cfbb8f8bdab8d1eccd3d8f7c92bc934d62e62e618ffdf", + "dir": "/tmp/testdata", + "hostname": "kasimir", + "username": "fd0", + "uid": 1000, + "gid": 100, + "tags": [ + "NL" + ] +} +``` + +从以上输出的内容可以看出,这个快照记录的是 `/tmp/testdata` 目录中的内容,最重要的是 `tree` 字段。当快照的元数据(例如标签)发生变化时,快照需要重新加密并保存。这将会改变其对象 ID,为了将这些快照顺序关联起来,所以我们引入了 `original` 字段,并在其中记录原始快照的对象 ID。例如将标签 `DE` 添加到上面的快照之后,它将变为: + +```bash +$ restic -r /tmp/restic-repo cat snapshot 22a5af1b +enter password for repository: +{ + "time": "2015-01-02T18:10:50.895208559+01:00", + "tree": "2da81727b6585232894cfbb8f8bdab8d1eccd3d8f7c92bc934d62e62e618ffdf", + "dir": "/tmp/testdata", + "hostname": "kasimir", + "username": "fd0", + "uid": 1000, + "gid": 100, + "tags": [ + "NL", + "DE" + ], + "original": "251c2e5841355f743f9d4ffd3260bee765acee40a6229857e32b60446991b837" +} +``` + +当快照的元数据在此被修改时,不会修改 `original` 字段,而是新增一个快照。 + +仓库中的所有内容都是根据其 SHA-256 的哈希值进行引用的。在保存之前,每个文件都会被分为不同大小的二进制对象。所有二进制对象的 SHA-256 哈希值都是按原是文件内容的顺序保存在相应的列表中。 + +索引文件的作用是将哈希值与打包对象(`Pack`)中的实际位置联系起来。如果索引不可用,可以直接读取所有二进制对象的元数据来实现。 + + + +### 文件树对象和文件对象 + +快照通过其 JSON 内容中的 SHA-256 哈希值来引用文件对对象。文件树对象(`Tree Blob`)和文件对象(`Data Blob`)都保存在 data 下子文件夹的打包对象(`Pack`)中。 + +通过命令 `restic cat blob` 可以获取上面快照所引用的文件树(将命令的输出通过管道给到 `jq` 可以查看格式化的 JSON 内容): + +```bash +$ restic -r /tmp/restic-repo cat blob 2da81727b6585232894cfbb8f8bdab8d1eccd3d8f7c92bc934d62e62e618ffdf | jq . +enter password for repository: +{ + "nodes": [ + { + "name": "testdata", + "type": "dir", + "mode": 493, + "mtime": "2014-12-22T14:47:59.912418701+01:00", + "atime": "2014-12-06T17:49:21.748468803+01:00", + "ctime": "2014-12-22T14:47:59.912418701+01:00", + "uid": 1000, + "gid": 100, + "user": "fd0", + "inode": 409704562, + "content": null, + "subtree": "b26e315b0988ddcd1cee64c351d13a100fedbc9fdbb144a67d1b765ab280b4dc" + } + ] +} +``` + +文件树对象(`Tree Blob`)中包含多个条目(保存在字段 `nodes` 中),其中每个条目包含名称、时间戳等元数据。当条目引用的是一个目录时,字段 `subtree` 指向另一个文件树对象(`Tree Blob`)的对象 ID。 + +我们可以使用 `restic cat blob` 并配合文件树的对象 ID 来打印它们的内容,上述被引用的子文件树对象(`Tree Blob`)可以通过如下命令进行查看: + +```bash +$ restic -r /tmp/restic-repo cat blob b26e315b0988ddcd1cee64c351d13a100fedbc9fdbb144a67d1b765ab280b4dc +enter password for repository: +{ + "nodes": [ + { + "name": "testfile", + "type": "file", + "mode": 420, + "mtime": "2014-12-06T17:50:23.34513538+01:00", + "atime": "2014-12-06T17:50:23.338468713+01:00", + "ctime": "2014-12-06T17:50:23.34513538+01:00", + "uid": 1000, + "gid": 100, + "user": "fd0", + "inode": 416863351, + "size": 1234, + "links": 1, + "content": [ + "50f77b3b4291e8411a027b9f9b9e64658181cc676ce6ba9958b95f268cb1109d" + ] + }, + [...] + ] +} +``` + +如上文件树对象(`Tree Blob`)中仅包含一个文件条目,没有额外的 `subtree` 字段,并且存在一个 `content` 字段包含文件对象(`Data Blob`)的对象 ID 列表。 + +使用命令 `restic cat blob` 也可以提取和解密给定 ID 的文件数据,例如对于如上数据块: + +```bash +restic cat blob 50f77b3b4291e8411a027b9f9b9e64658181cc676ce6ba9958b95f268cb1109d | sha256sum +enter password for repository: +50f77b3b4291e8411a027b9f9b9e64658181cc676ce6ba9958b95f268cb1109d - +``` + +从命令 `sha256sum` 的输出可以看出,文件内容的哈希值与文件树中包含的哈希 ID 相同,因此可以判断返回了正确的数据。 + + + +### 锁 + +restic 的对于仓库结构的设计允许多个客户的并行读取,甚至可以并行写入。然而有些功能在工作时会存在副作用甚至需要独占仓库访问。为了实现这些功能,restic 需要在做任何操作之前在仓库上创建一个锁。 + +锁有两种类型:独占锁和共享锁。最多只有一个进程可以获得仓库的独占锁,在这段时间内不能获取其他任何锁(独占和共享)。但是在同一时刻可以有多个共享锁同时存在。 + +锁就是在目录 `locks` 中的一个文件。其文件名是其内容的对象 ID。锁文件也是通过「未打包的数据文件格式」节中的描述的方式进行编码的一个 JSON 文档,其中包含以下内容: + +```json +{ + "time": "2015-06-27T12:18:51.759239612+02:00", + "exclusive": false, + "hostname": "kasimir", + "username": "fd0", + "pid": 13607, + "uid": 1000, + "gid": 100 +} +``` + +字段 `exclusive` 决定了锁的类型。当需要创建一个新的锁时,restic 会检查仓库中所有的锁。当找到锁时,他会检查锁是否超时(`time` 字段与当前时间相差超过 30 分钟)。如果锁是在同一台机器上创建的,即使锁未超时也会通过发送信号的方式检测进程是否存活。如果发送失败,restic 会假设这个进程已经挂掉并且锁是失效的。 + + + +### 读写优先级 + +仓库格式允许同时进行写入(例如:备份)和读取(例如:恢复)操作。由于仓库中的每个快照中的数据在多个不同的文件中(快照、索引、打包对象(`Pack`)),因此有必要为文件读取和写入制订一些优先级规则。这些优先级规则还保证对仓库的修改不会影响到仓库的正确性,即使是客户端或存储后端由于断电或者两者之间的网络中断而导致奔溃也不会损坏仓库。 + +访问仓库中的数据的正确顺序是由以下一些规则总结得到的,在一个正确的仓库中以下规则在任何时候都必须成立。以下规则中的*必须*( MUST )是一个严格的要求,如果不遵守将会导致数据丢失。如果不遵守规则中的*应该* ( SHOULD ),则需要采取措施来修复仓库(如重建索引),但不会导致数据丢失。而已存在(`existing`)则表示引用的数据已经持久地保存在仓库中。 + +* 快照*必须(MUST)*引用一个*已存在(existing)*的文件树对象(`Tree Blob`) +* 一个可访问的文件树对象(`Tree Blob`)必须(MUST)引用*已存在(existing)*的文件或文件树对象(递归地)。可访问表示文件树对象(`Tree Blob`)是可以从快照逐个遍历访问到 +* 一个索引*必须(MUST)*引用一个*已存在(existing)*的打包对象(`Pack`)中有效的文件对象(`Data Blob`) +* 快照中引用的所有文件对象(`Data Blob`)应该(SHOULD)可以通过索引文件列出 + +这些规则将引导我们如何将数据保存到仓库中。首先,必须先写入包含文件对象(`Data Blob`)和文件树对象(`Tree Blob`)的打包对象(`Pack`)。然后是对这些保存在打包对象(`Pack`)中对象的索引对象。最后则是相应的快照对象。 + +需要注意的是,在备份期间二进制对象和文件树对象(`Tree Blob`)的写入顺序是任意的。因为这些文件只有在创建快照文件时才会被引用。 + +读取则应该遵循与写入相反的顺序。只有在写入快照文件之后,就能确保所需的对象都已存在在仓库中。这意味着在加载仓库索引之前应该列出所有的快照文件。反过来就可能导致发生竞争,即访问的快照对象中不包含相应的索引文件,最终导致无法访问快照中的文件树对象(`Tree Blob`)。 + +如果需要从仓库中删除或重写数据,则必须遵循以下规则,这些规则源自以上写入规则: + +* 客户端在删除数据时必须先获取排他锁,以防止与其他客户端产生冲突 +* 在删除打包对象(`Pack`)之前,必须先将其从索引对象中删除 +* 重写打包对象(`Pack`)必须先写入一个新打包对象(`Pack`),并更新索引文件(添加新的索引项并删除旧的索引项),在这之后才能删除旧的打包对象(`Pack`) + + + +### 备份和重复数据检测 + +为了创建一个备份,restic 会扫描源目录中的所有文件、子目录。文件中的数据会被分割为长度不等(以 64 字节大小为单位定义偏移量)的 Blob 对象。restic 使用 Robin 指纹算法来实现内容定义区块(CDC,Content Defined Chunk)。当仓库初始化时,将会随机选择一个不可约多项式并保存在 `config` 文件中,同时这也会让水印攻击会变得更加困难。 + +小于 512KiB 的文件不会被拆分,数据块的大小会在 512KiB 到 8MiB 之间。该算法的目标是平均 1MiB 的数据块。 + +对于修改过的文件,只有产生修改的数据块被保存到后续备份中。这也可以在文件中插入或删除某些字节场景下正常工作。 + + + +### 威胁模型 + +restic 的设计目标包括能将备份安全地存储在不完全受信任的位置(例如,可供其他人访问的共享文件系统),甚至允许管理员在某些情况下删除或修改这些文件。 + +一般性假设: + +* 创建备份的主机系统是受信任的,这是最基本的需求,并且这对创建可信备份十分重要 +* 用户使用的是正版无修改的 restic 程序 +* 用户没有分享仓库密码给攻击者 +* restic 程序并不保证攻击者删除仓库中的文件,restic 对此无能为力。如果需要保证这一点,请在一个安全的位置(第三方无法访问)的环境创建仓库 +* 如果密钥泄漏,则整个存储库都需要重新加密。在当前密钥管理设计下,如果不重新加密整个存储库,就不可能安全地撤销泄漏的密钥 +* 对 restic 所使用的加密套件(AES256-CTR-Poly1305-AES 和 SHA-256)目前并没有显著的攻击方式。如果出现这样的攻击方式,则 restic 所提供的机密性和完整性保护将直接失效 +* 使用暴力破解的方式目前无法对 restic 的加密方案造成威胁 + +restic 程序保证以下内容: + +* 如果没有仓库的密码,则无法获取仓库中加密文件的原始内容。密钥文件(`key`)中除了用于信息目的元数组之外的所有内容都经过加密和验证。缓存也会被加密以防止元数据泄漏 +* 可以检测到仓库中数据的损坏(例如,由于内存、硬盘坏道等问题导致) +* 被篡改的数据不会被解密 + +考虑到上述假设和保证,以下是攻击者在各种情况下可以实现的操作。 + +对你的备份存储位置具有读取权限的攻击者可以做到: + +* 尝试对仓库进行暴力破解(请使用具有足够强度的密码) +* 通过文件访问模式推断哪些 Pack 可能包含文件树 +* 通过仓库中对象的创建时间戳推测备份的大小 + +具有网络访问权限的攻击者可以做到: + +* 尝试 DoS 保存有仓库的服务器或客户端和服务器之间的网络连接 +* 确定创建备份的位置(即请求的来源位置) +* 确定你备份所保存的位置(即服务器所在的位置) +* 通过观察网络流量推断备份的大小 + +以下是违反上述假设所受到的影响。 + +攻击者破坏了(通过恶意软件、物理访问等)备份所在的主机系统可以做到: + +* 使得备份过程不可信(例如,拦截密码、复制文件、操纵数据) +* 创建涵盖所有已修改文件的(错误的)快照(包含垃圾数据),并等到受信任的主机使用 `forgot` 从而删除所有正确的快照 +* 为每个现有快照创建一个具有略微不同时间戳的错误快照,并等到执行 `forgot` 时一次性删除所有正确的快照 + +在备份存储位置具有写访问权限的攻击者可以做到: + +* 删除或操作你的备份文件,从而削弱你从受损存储位置恢复文件的能力 +* 确定哪些文件属于哪个快照(例如,基于文件的时间戳)。当仅删除这些文件时,特定的快照会消失,并且依赖于该快照的所有其他快照都无法完全恢复。restic 并不负责防止这种攻击 + +攻击者可以以仅追加( append-only ,允许读取和写入,不允许删除和覆盖)访问仓库所在的位置可以做到: + +* 捕获密码并解密过去和未来的所有备份文件(更多相信信息可以参考下面「密钥泄漏」部分的示例) +* 在主机受到威胁后(对新备份具有完全控制权)会使得新的备份变得完全不可信。攻击者无法删除或操作旧备份。因此,恢复在主机受损之前创建的备份是可能的 +* 可能会使用 `forgot` 命令删除所有合法的快照,只保留攻击者添加的虚假快照。勒索软件可能会尝试这么做,以便只留下一个选项通过赎金来取回你的数据。为了安全的使用 `forgot` ,请查看有关删除备份快照和仅追加模式的相应文档 + +如果攻击者已获取泄漏(解密)的密钥可以做到: + +* 解密现在和未来的所有备份数据。如果多台主机备份到同一个仓库,攻击者可以访问每台主机的备份数据。请注意,由于本地加密密钥和访问主密钥,因此更改密码不能阻止攻击。目前仅能通过 `copy` 命令修改主密码,该命令将文件复制到具有新密钥的新仓库中,或者通过生成全新的仓库和备份来避免。 + +### 更新 + +#### 仓库版本 2(`version: 2`) + +* 支持对二进制对象(文件和文件树)、索引、锁、快照文件进行压缩存储 +