恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
restic 仓库设计全解:存储格式、加密认证、索引去重与安全模型
首页
资讯中心
/
restic 仓库设计全解:存储格式、加密认证、索引去重与安全模型
restic 仓库设计全解:存储格式、加密认证、索引去重与安全模型
发布时间:2026/9/10 16:46:10
restic 仓库设计全解存储格式、加密认证、索引去重与安全模型【免费下载链接】resticFast, secure, efficient backup program项目地址: https://gitcode.com/GitHub_Trending/re/resticrestic 是一款用 Go 编写的快速、安全、高效的备份程序。它把要备份的数据先切块、压缩、加密再上传到一个仓库repository中统一存储并允许同一仓库被多台主机并行读写。本文以仓库设计文档 doc/design.rst 为骨架系统讲解 restic 仓库从文件格式、加密体系到读写时序与威胁模型的完整设计并结合仓库源码验证每个关键结论。读完你会具备独立分析一个 restic 仓库原始数据、理解cat/init/check等命令底层行为以及判断某操作是否安全的能力。核心概念与术语在设计文档中restic 用五个术语刻画整套数据模型它们是理解后续所有格式细节的基石术语含义Repository仓库一次备份产生的全部数据都会被发送并结构化存储到仓库中例如一个带若干子目录的文件系统目录树。仓库实现必须能完成列举内容等一组基础操作。Blob若干数据字节加上标识信息如数据的 SHA-256 哈希及其长度的组合体。Pack一个或多个 Blob 的组合通常对应仓库中的一个物理文件。Snapshot快照某个时刻被备份文件/目录的状态包括内容与元数据文件名、修改时间或目录及其内容。Storage ID存储 ID仓库中存储内容的 SHA-256 哈希值。要加载某个文件就必须知道它的存储 ID。Repository Format仓库格式总览仓库能按 ID 存储多种类型的数据。所谓存储 ID就是文件内容的 SHA-256 哈希。仓库里所有文件只写入一次、之后永不修改且写入必须是原子的以避免并发操作读到不完整的文件——这使多个客户端可以并行地读写同一个仓库。只有prune操作才会真正删除仓库中的数据。仓库由若干目录和一个顶层文件config构成。除config与keys目录内文件之外其他文件的文件名就是其内容 SHA-256 哈希的小写十六进制表示。因此可以直接对仓库文件执行sha256sum并把输出与文件名比对快速发现磁盘误写之类的意外篡改。若某文件名的前缀在同一目录内唯一则允许用该前缀代替完整文件名restic 对用户输入的 ID 支持这种短 ID缩写。加密布局上除keys目录中的文件外仓库内所有文件都用AES-256 计数器模式CTR加密密文完整性由Poly1305-AES 消息认证码MAC保护。data目录中的 pack 文件由多个彼此独立加密和认证的部分组成每个 Blob 以及 pack 头见下文。每个加密文件的前 16 字节存放初始化向量IV其后是密文最后是 16 字节 MAC即IV || CIPHERTEXT || MAC每个文件的加密总开销是 32 字节且每个文件都会选取一个新的随机 IV。config文件同样按上述方式加密解密后是一个 JSON 文档例如{ version: 2, id: 5956a3f67a6230d4a92cefb29529f10196c7d92582ec305fd71ff6d331d6271b, chunker_polynomial: 25b468838dcb75 }解密后 restic 首先校验version是否为它能理解的版本否则直接中止。当前支持版本 1 或 2。id字段是 32 字节随机数编码成的十六进制串用来唯一标识仓库——无论仓库位于本地还是远程后端这个 ID 都唯一。chunker_polynomial是切分大文件时使用的随机参数见备份与去重一节。以上约定在源码中有精确对应。仓库版本边界定义在 internal/restic/config.goMinRepoVersion 1、MaxRepoVersion 2而StableRepoVersion 2——即restic init新建仓库时默认写入的版本。CreateConfig通过chunker.RandomPolynomial()随机生成不可约多项式并写入配置LoadConfig在读取后会检查版本范围还会校验多项式确实不可约LoadConfig。Repository Layout目录布局local与sftp后端直接使用文件系统上的目录与文件两者的目录布局一致且该布局同样被其他所有远程后端沿用/tmp/restic-repo ├── config ├── data │ ├── 21 │ │ └── 2159dd48f8a24f33c307b750592773f8b71ff8d11452132a7b2e2a6a01611be1 │ ├── 32 │ │ └── 32ea976bc30771cebad8285cd99120ac8786f9ffd42141d452458089985043a5 │ ├── 59 │ │ └── 59fe4bcde59bd6222eba87795e35a90d82cd2f138a27b6835032b7b58173a426 │ ├── 73 │ │ └── 73d04e6125cf3c28a299cc2f3cca3b78ceac396e4fcf9575e34536b26782413c │ [...] ├── index │ ├── c38f5fb68307c6a3e3aa945d556e325dc38f5fb68307c6a3e3aa945d556e325d │ └── ca171b1b7394d90d330b265d90f506f9984043b342525f019788f97e745c71fd ├── keys │ └── b02de829beeb3c01a63e6b25cbd421a98fef144f03b9a02e46eff9e2ca3f0bd7 ├── locks ├── snapshots │ └── 22a5af1bdc6e616f8a29579458c49627e01b32210d09adb288d1ecda7c5711ec └── tmp要点data目录下按存储 ID 的前两个十六进制字符分片存放 pack 文件避免单个目录文件过多config是仓库根目录下唯一的明文目标文件密文存储但内容本身是配置 JSONindex、keys、locks、snapshots、tmp各有专职索引、口令密钥、锁、快照与临时文件上传时的中间产物先落在tmp写入完成后原子地移动到最终位置。本地仓库可用restic init创建$ restic -r /tmp/restic-repo initS3 Legacy Layout已废弃早期开发阶段Amazon S3 后端使用了略微不同的路径目录名使用单数key、lock、snapshot而非复数且 pack 文件直接放在data目录之下而非二级分片/config /data ├── 2159dd48f8a24f33c307b750592773f8b71ff8d11452132a7b2e2a6a01611be1 ├── 32ea976bc30771cebad8285cd99120ac8786f9ffd42141d452458089985043a5 ├── 59fe4bcde59bd6222eba87795e35a90d82cd2f138a27b6835032b7b58173a426 ├── 73d04e6125cf3c28a299cc2f3cca3b78ceac396e4fcf9575e34536b26782413c [...] /index ├── c38f5fb68307c6a3e3aa945d556e325dc38f5fb68307c6a3e3aa945d556e325d └── ca171b1b7394d90d330b265d90f506f9984043b342525f019788f97e745c71fd /key └── b02de829beeb3c01a63e6b25cbd421a98fef144f03b9a02e46eff9e2ca3f0bd7 /lock /snapshot └── 22a5af1bdc6e616f8a29579458c49627e01b32210d09adb288d1ecda7c5711ec该旧布局已废弃restic 0.17 是最后一个支持 legacy 布局的版本。设计文档明确提示若你的仓库仍使用该布局需要提前规划迁移。Pack 文件格式仓库中除 Key密钥文件与 Pack 文件外的所有文件都只是以IV || Ciphertext || MAC存储的原始数据。而 Pack 文件可能包含一个或多个数据 Blob其整体结构如下EncryptedBlob1 || ... || EncryptedBlobN || EncryptedHeader || Header_LengthPack 文件末尾是描述内容的头header头被加密并认证Header_Length是加密头长度的四字节小端整数。把头部放在文件尾部使得备份阶段一旦读到 Blob 就能连续地把它们流式写入无需等整个 Pack 完成、已知头部内容与长度后再重写文件——既降低了代码复杂度也避免了二次写入。源码 internal/repository/pack/pack.go 中entrySize Type(1) 2×4 ID(32)并与headerLengthSize 4对应印证了这一小端四字节头长约定。所有 Blob 都独立加密与认证因此仓库重组如prune重新打包无需触碰已加密的 Blob 内容只需读头部即可知道 Pack 里有哪些 Blob索引高效头部自带认证不必读完整 Pack 就能校验头部真实性。解密后Pack 头部由如下元素组成Type_Blob1 || Data_Blob1 || [...] Type_BlobN || Data_BlobN ||其中 Blob 类型字段是单字节其后跟随的数据因类型而异类型含义数据0b00data blob数据块Length(encrypted_blob) || Hash(plaintext_blob)0b01tree blob树块Length(encrypted_blob) || Hash(plaintext_blob)0b10compressed data blob压缩数据块Length(encrypted_blob) || Length(plaintext_blob) || Hash(plaintext_blob)0b11compressed tree blob压缩树块Length(encrypted_blob) || Length(plaintext_blob) || Hash(plaintext_blob)据此足以推算出 Pack 内所有 Blob 的偏移长度字段为四字节小端整数Length(plaintext_blob)表示一个 Blob 经解密与解压后的长度。其余类型值非法未来可能新增更多类型。压缩类型仅在仓库格式版本 2 中有效data/tree Blob 可使用 zstandardzstd算法压缩。关于不同类型 Blob 能否混放格式版本 1 中 data 与 tree Blob应该分开放入不同 Pack 文件版本 2 中则必须分开放。而同一 Pack 内同类型的压缩与非压缩 Blob 可以混放。在不依赖索引解析 Pack 时先读文件最后四字节获得头长再读并解析头部即可得到所有 Blob 的明文哈希、类型、偏移与长度。Blob 类型的枚举DataBlob/TreeBlob及其 JSON 表示data/tree定义在 internal/restic/blob.go。Unpacked Data 格式未打包文件索引、锁、快照这类单个文件与 Data/Tree Blob 一样先加密再认证外层同样是IV || Ciphertext || MAC。版本 1明文始终是一个 JSON 文档JSON 对象或数组。JSON 编码必须是确定性的且行为应与 Go 标准库encoding/json一致——这是跨实现可互操作与去重的前提。版本 2新增压缩支持。明文以 1 字节的encoding_version开头来标识编码版本以区分纯 JSON 并为将来格式演进留出空间encoding_version || data。为了向后兼容编码版本[0x5b与{0x7b被用来标记包括该编码字节在内的整个明文都应按 JSON 文档处理。对新写入的数据编码版本目前恒为2此时data是用 zstandard 压缩后的 JSON 文档。索引Indexing索引文件保存 Data/Tree Blob 与其所在 Pack 的对应信息并存入仓库。当本地缓存索引不可用时可下载索引文件并据此重建索引。索引明文与 Unpacked Data Format 一节描述的一致是形如下面的 JSON{ supersedes: [ ed54ae36197f4745ebc4b54d10e0f623eaaaedd03013eb7ae90df881b7781452 ], packs: [ { id: 73d04e6125cf3c28a299cc2f3cca3b78ceac396e4fcf9575e34536b26782413c, blobs: [ { id: 3ec79977ef0cf5de7b08cd12b874cd0f62bbaf7f07f3497a5b1bbcc8cb39b1ce, type: data, offset: 0, length: 38 // 该 blob 未压缩因此没有 uncompressed_length 字段 }, { id: 9ccb846e60d90d4eb915848add7aa7ea1e4bbabfc60e573db9f7bfb2789afbae, type: data, offset: 38, length: 112, uncompressed_length: 511 }, { id: d3dc577b4ffd38cc4b32122cabf8655a0223ed22edfd93b353dc0c3f2b0fdf66, type: data, offset: 150, length: 123, uncompressed_length: 234 } ] } ] }语义要点该文档列出 Pack 及其包含的 Blob。例中 Pack73d04e61...含三个 data Blob后列明文哈希。length对应 Pack 头中的Length(encrypted_blob)offset是该 Blob 在 Pack 内的字节偏移。uncompressed_length只出现在压缩 Blob上因而在格式版本 1 中永远不会出现其值为Length(blob)解压后长度。supersedes列出被当前索引文件替换掉的旧索引文件存储 ID。索引重打包如删除旧快照、重新组合 Pack时会发生替换。索引文件数量任意各文件可描述非互斥的 Pack 集合单个文件描述的 Pack 数会让文件大小保持在8 MiB以下。索引 Blob 条目中的length/uncompressed_length等字段与 Pack 解析逻辑一一对应见 internal/repository/pack/blob.go检查索引文件明文是否为合法 JSON 的逻辑可参考 internal/repository/index 目录下的解析实现。Keys、加密与消息认证restic 仓库内所有数据都用AES-256 计数器模式加密、用Poly1305-AES认证。加密新数据时先从密码学安全伪随机数生成器读取 16 字节随机 nonce它同时充当 CTR 模式的 IV 与 Poly1305 的 nonce。该操作需要三把密钥32 字节的 AES-256 加密密钥、16 字节的 AES 密钥与 16 字节的 Poly1305 密钥。数据先用 AES-256 加密再对密文计算 MAC最终存成IV || CIPHERTEXT || MAC。这些常量在 internal/repository/crypto/crypto.go 中有精确定义aesKeySize 32、macKeySizeK 16、macKeySizeR 16、Extension ivSize macSize 16 16 32字节加密开销。加密对象Key同时实现 Gocipher.AEAD接口Seal/Open其中Seal用 AES-CTR 加密、poly1305MAC在密文后追加认证标签Open先poly1305Verify校验 MAC失败即返回ErrUnauthenticatedciphertext verification failed且拒绝解密被篡改的数据crypto.go。keys目录存放密钥文件是包含从用户口令派生出仓库主加密/认证密钥所需的全部信息的 JSON 文档。可用 Python 的json模块美化输出此处为可读性做了截断$ 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.4830719601:00, data: tGwYeKoM0C4j4/9DFrVEmMGAldvEn/iKC3te/QE/6ox/V4qz58FUOgMa0Bb1cIJ6asrypCx/Ti/pRXCPHLDkIJbNYd2ybCfLhFIJVLCvkMStrdywsUkglUbTbi7Ldsul5jpAj9vTZ25ajDc4FKtWEcCWL5ICAOoTAxnPgTLh8ByGQBH6KbdWabqamLzTRWxePFoYuxa7yXgmj9A, salt: uW4fEI1IOzj7ED9mVoryTSJFd68DGlGOeLgJELYsTU5ikhG/83/jGd4KKAaQdSrsfzrdOhAMftTSih5Ux6w, }打开仓库时 restic 会提示输入密码然后以scrypt密钥派生函数KDF配合参数N、r、p、salt派生出64 字节密钥材料前 32 字节用作加密密钥AES-256后 32 字节用作消息认证密钥Poly1305-AES。后 32 字节再切分为 16 字节 AES 密钥k加 16 字节 Poly1305 秘密密钥r其中r在使用前需按 Poly1305 规范做掩码处理。密钥派生实现位于 internal/repository/crypto/kdf.gomacKeyFromSlice即完成k‖r的拆分crypto.go。随后用这两把密钥去认证并解密 JSON 字段data先去 Base64——解密方式与处理任何普通 Blob 相同。若密码错误或密钥文件被篡改计算出的 MAC 与 data 末尾 16 字节不匹配restic 直接报错退出否则解密出的 JSON 文档里是仓库的主加密密钥与主认证密钥Base64 编码。可用如下命令解密并美化输出主密钥$ restic -r /tmp/restic-repo cat masterkey { mac: { k: evFWd9wWlndL9jc501268g, r: E9eEDnSJZgqwTOkDtOpDw }, encrypt: UQCqa0lKZ94PygPxMRqkePTZnHRYh1k1pX2k2lM2v3Q, }仓库中所有数据都由这对主密钥加密与认证AES-256-CTR Poly1305-AES。一个仓库可以有多个不同口令对应keys目录下的多个密钥文件——这样更换密码时不必重加密全部数据仅需用新口令派生新密钥、并用旧主密钥把主密钥重新封存为新密钥文件。快照Snapshots快照代表某一时刻某个目录及其全部文件/子目录的状态。每次备份都会创建新快照。快照是存放在仓库snapshots目录下的 JSON 文档采用 Unpacked Data Format 的编码文件名即存储 IDrestic 用它唯一标识快照。解密并美化快照$ restic -r /tmp/restic-repo cat snapshot 251c2e58 enter password for repository: { time: 2015-01-02T18:10:50.89520855901:00, tree: 2da81727b6585232894cfbb8f8bdab8d1eccd3d8f7c92bc934d62e62e618ffdf, paths: [ /tmp/testdata ], hostname: kasimir, username: fd0, uid: 1000, gid: 100, tags: [ NL ] }可见该快照代表目录/tmp/testdata。最关键的字段是tree——它指向快照文件树的根。当快照元数据如标签改变时快照需要重新加密保存这会改变存储 ID。为了把看起来不同的两个快照关联起来引入了original字段保存原始快照的 ID。例如给上面快照追加标签DE后$ restic -r /tmp/restic-repo cat snapshot 22a5af1b enter password for repository: { time: 2015-01-02T18:10:50.89520855901:00, tree: 2da81727b6585232894cfbb8f8bdab8d1eccd3d8f7c92bc934d62e62e618ffdf, paths: [ /tmp/testdata ], hostname: kasimir, username: fd0, uid: 1000, gid: 100, tags: [ NL, DE ], original: 251c2e5841355f743f9d4ffd3260bee765acee40a6229857e32b60446991b837 }一旦引入original字段在后续再次修改快照元数据时不再变更始终指回最初的那个快照。仓库中的所有内容都按其 SHA-256 哈希被引用。保存文件前每个文件被切分成大小不等的 Blob所有 Blob 的 SHA-256 哈希按顺序排成的列表即表示该文件的内容。要把这些明文哈希映射到 Pack 文件中的实际位置靠的是索引索引不可用时则可通过读取所有 data Blob 的头部重建映射。树与数据Trees and Data快照通过其内容 JSON 字符串表示形式的 SHA-256 哈希引用一棵树。树与数据都保存在data目录子目录下的 Pack 文件中。JSON 编码必须是确定性的、与 Goencoding/json行为一致否则无法可靠去重。用restic cat blob查看前面快照引用的树输出经jq .缩进$ 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.91241870101:00, atime: 2014-12-06T17:49:21.74846880301:00, ctime: 2014-12-22T14:47:59.91241870101:00, uid: 1000, gid: 100, user: fd0, inode: 409704562, content: null, subtree: b26e315b0988ddcd1cee64c351d13a100fedbc9fdbb144a67d1b765ab280b4dc } ] }树由nodes字段中的条目列表组成每个条目保存名称、时间戳等元数据。文档特别提醒几处元数据生成的细节名称在保存前会经strconv.Quote加引号处理以支持非 Unicode 名称但这也改变了含或\的名称的表示保存的文件模式是fs.FileMode定义的模式并与os.ModePerm | os.ModeType | os.ModeSetuid | os.ModeSetgid | os.ModeSticky做掩码条目指向目录时subtree字段存另一个树对象的明文 ID完整字段定义可查看 Node 结构体实现位于 internal/restic/node.go 一带的树相关源码。继续 dump 上面目录引用的子树$ restic -r /tmp/restic-repo cat blob b26e315b0988ddcd1cee64c351d13a100fedbc9fdbb144a67d1b765ab280b4dc | jq . enter password for repository: { nodes: [ { name: testfile, type: file, mode: 420, mtime: 2014-12-06T17:50:23.3451353801:00, atime: 2014-12-06T17:50:23.33846871301:00, ctime: 2014-12-06T17:50:23.3451353801:00, uid: 1000, gid: 100, user: fd0, inode: 416863351, size: 1234, links: 1, content: [ 50f77b3b4291e8411a027b9f9b9e64658181cc676ce6ba9958b95f268cb1109d ] }, [...] ] }这次是文件条目没有subtreecontent字段是包含一个明文 SHA-256 哈希的列表——该列表按顺序描述构成文件内容的各数据 Blob。符号链接的结构略有不同$ restic -r /tmp/restic-repo cat blob 4c0a7d500bd1482ba01752e77c8d5a923304777d96b6522fae7c11e99b4e6fa6 | jq . enter password for repository: { nodes: [ { name: testlink, type: symlink, mode: 134218239, mtime: 2023-07-25T20:01:44.00746537402:00, atime: 2023-07-25T20:01:44.00746537402:00, ctime: 2023-07-25T20:01:44.00746537402:00, uid: 1000, gid: 100, user: fd0, inode: 33734827, links: 1, linktarget: example_target, content: null }, [...] ] }链接目标存在linktarget中。由于 JSON 字符串只能包含合法 Unicode若linktarget不是合法 UTF-8 字符串则例外处理自 restic 0.16.0 起此时linktarget_raw字段保存原始链接目标的 Base64 编码版本linktarget_raw仅在linktarget无法被正确编码时才设置。restic cat blob也能按明文 ID 提取并解密数据。对上面的数据块做校验$ restic -r /tmp/restic-repo cat blob 50f77b3b4291e8411a027b9f9b9e64658181cc676ce6ba9958b95f268cb1109d | sha256sum enter password for repository: 50f77b3b4291e8411a027b9f9b9e64658181cc676ce6ba9958b95f268cb1109d -sha256sum的输出与树中记录的明文哈希一致证明返回了正确的数据——这也演示了内容寻址 哈希校验如何让 restic 天然具备完整性验证能力。锁Locks仓库结构被设计为允许多个 restic 实例并行访问、甚至并行写入。但某些操作独占访问时更高效或必须独占。因此 restic 进程在操作前必须先在仓库上创建锁。锁分两种排他锁与非排他锁。任意时刻至多一个进程持有排他锁且持有期间不得存在任何其他锁排他或非排他而多个非排他锁可以并行共存。锁是locks子目录下的一个文件文件名是其内容的存储 ID采用 Unpacked Data Format 编码内容是如下 JSON{ time: 2015-06-27T12:18:51.75923961202:00, exclusive: false, hostname: kasimir, username: fd0, pid: 13607, uid: 1000, gid: 100 }exclusive字段定义锁类型。创建新锁时 restic 会检查仓库中现有全部锁若发现锁就判断其是否过期——时间戳早于 30 分钟的锁视为过期若锁由本机创建即使未满 30 分钟也会通过向进程发送信号来探测进程是否存活发送失败则认为进程已死、锁过期。若未检测到冲突锁restic 创建新锁后等待片刻再复查是否出现新锁再根据已有锁与新锁类型决定继续还是失败。指定--retry-lock选项时restic 会周期性地重试加锁直到成功或超时。锁文件创建/探测的实现可参见 internal/repository/lock.go 与 internal/repository/lock_file.go。读写顺序Read and Write Ordering仓库格式允许写入如 backup与读取如 restore并发进行。由于每个快照的数据横跨多个文件快照、索引、Pack读写文件的顺序必须遵循特定规则这些规则还保证即使客户端或存储后端崩溃断电、网络中断仓库修改也始终维持正确状态。正确访问顺序源自一组任何时刻都必须保持的不变量快照must只引用已存在的 tree Blob可达的 tree Blobmust递归地只引用已存在的 tree 与 data Blob可达指从某快照出发能到达索引must只引用既有 Pack 中的合法 Blob快照引用的所有 Blobshould都被列在索引中。此处must是严格约束违反将导致数据丢失should违反后需要修复步骤如重建索引但一般不会丢数据已存在意味着被引用数据已持久化落库。由此得到推荐的写入顺序先写含 data/tree Blob 的 Pack 文件 → 再写引用这些已写 Pack 内 Blob 的索引 → 最后写对应的快照。注意备份过程中 data 与 tree Blob 之间无需特定写入顺序因为只有对应快照上传后这些 Blob 才会被引用。读取则应与写入顺序相反只有当快照写入完成后才能保证所需数据全部存在。尤其应先收集要读的快照列表、再加载仓库索引反过来做可能产生竞态——某快照刚被加载但其配套索引还没写入导致访问该快照的 tree Blob 失败。删除或重写数据的规则由上述不变量导出删除数据的客户端must先获取排他锁避免与其他客户端冲突Packmust先从其引用索引中移除之后才能删除重写 Pack 时must先写新 Pack、更新索引新增更新后的索引并删除旧的最后才删除旧 Pack。备份与去重Backups and Deduplication创建备份时 restic 扫描源目录中的全部文件、子目录与其他条目把每个文件的数据切成可变长度的 Blob切分点由64 字节滑动窗口决定。实现采用Rabin 指纹做内容定义分块Content Defined ChunkingCDC。仓库初始化时在config文件中随机保存一个不可约多项式使基于水印watermark的攻击难得多。分块尺寸策略小于512 KiB的文件不切分Blob 尺寸范围为512 KiB 8 MiB平均 Blob 尺寸目标为1 MiB。得益于内容寻址分块修改后的文件再次备份时只需上传发生变化的那几个 Blob——即使在文件中间任意位置插入或删除字节未受影响的数据块也无需重传。这正是 restic 增量备份与跨快照全局去重能力的根基。切分用的多项式与 CDC 逻辑由 restic 分块算法驱动仓库引入的 chunker 实现被封装在 internal/restic/chunker.gorestic init时在 cmd/restic/cmd_init.go 生成随机不可约多项式并随 config 写入。威胁模型Threat Modelrestic 的设计目标之一是能安全地把备份存放在并非完全可信的位置例如共享系统他人可访问文件若管理员具备权限甚至可修改、删除文件。理解威胁模型能帮助你在部署时做出正确权衡。总体假设生成备份的宿主机是可信的——这是最基本、也最关键的前提用户使用的是正版/未被篡改的 restic 副本用户不会把仓库密码分享给攻击者restic不设计为抵御存储位置处的文件删除型攻击——对此无计可施若需要这种保证请把备份放到第三方无法访问的安全位置一旦某密钥泄露整个仓库需要重新加密在当前密钥管理设计下无法在不重加密整个仓库的情况下安全吊销泄露的密钥restic 依赖的密码学原语AES-256-CTR-Poly1305-AES 与 SHA-256未出现可用的密码分析突破计算能力未进步到使暴力破解 restic 加密保护变得可行。restic 保证的能力没有仓库密码无法读取已存文件与元数据的明文。除密钥文件中用于信息的元数据外一切皆加密且认证缓存也经过加密以防元数据泄露仓库内数据的修改坏内存、坏硬盘等造成可被检测被篡改的数据不会被解密认证失败即拒绝见 internal/repository/crypto/crypto.go 中Open对 MAC 的强制校验。各类攻击者的可达目标举例具备存储位置读权限的攻击者可以对仓库副本发起暴力口令猜测请使用熵足够的高强度口令通过文件访问模式推断哪些 Pack 可能含树依据仓库对象的创建时间戳推断备份规模依据内容定义分块CDC的尺寸侧信道推导机密的分块多项式。相关攻击已由学术论文《Chunking Attacks on File Backup Services using Content-Defined Chunking》论证能观察到某已知文件所产生块大小的攻击者可推导出秘密 chunker 多项式进而在某些场景下判断仓库是否存有特定大文件。restic 0.18.0 已缓解该攻击将块随机分配到 Pack 文件使攻击者无法再把块归属到具体文件与文件内位置从而无法得知块尺寸。具备网络访问权限的攻击者可以对承载仓库的服务器或网络连接发起 DoS获知你备份发起的位置请求来源获知你备份存储的目标哪个服务商/目标系统通过观察网络流量推断备份规模。攻陷了备份宿主机恶意软件、物理访问等的攻击者可以让整个备份过程不可信拦截口令、复制文件、操纵数据创建覆盖全部已修改文件的垃圾快照并等可信主机forget足够多次后删除所有正确快照为每个现有快照伪造一个时间戳略有差异的垃圾快照待某些forget策略运行后一次性清空全部正确快照。对存储位置具备写权限的攻击者可以删除或操纵备份破坏从该位置恢复的能力基于文件时间戳判断哪些文件属于哪个快照定向删除后让该快照及依赖其数据的后续快照无法完整恢复。restic 不设计为检测此类攻击。攻陷了仅追加可读写、不可删改访问仓库的宿主机的攻击者可以捕获口令并解密过去与未来的备份在宿主机被攻陷之后让新备份不可信对新备份有完全控制权但无法删除或篡改旧备份——因此恢复攻陷前创建的旧快照仍然可行可能操纵forget命令删除全部合法快照、只保留攻击者注入的伪造快照勒索软件常用手法安全使用forget请参见备份快照删除与仅追加模式append-only的相关文档。持有仓库泄露密钥已解密的攻击者可以解密现有与未来的备份数据若多台主机共享同一仓库将获得所有主机的备份数据。注意由于本地加密密钥能解开主密钥单纯更换密码并不能阻止这种访问。当前更换主密钥只能通过copy命令把数据迁入带新主密钥的新仓库或干脆新建仓库并重新备份。格式演进Repository Version 2设计文档末尾的Changes章节记录了格式变更当前版本 2 的变更点为为 Blobdata/tree以及 index / lock / snapshot 文件提供压缩支持。结合前文压缩仅对仓库格式版本 2 有效采用 zstandard 算法未打包文件索引、锁、快照新增 1 字节encoding_version前缀新数据恒为2其负载是 zstd 压缩后的 JSON同时用[/{兼容旧版明文 JSONdata 与 tree Blob 在版本 2 中must存放于不同 Pack 文件。新建仓库默认写入版本 2internal/restic/config.go而 restic 仍能读取版本 1 仓库LoadConfig 校验版本 ∈ [1, 2]。小结一张图理解 restic 数据流一次备份在仓库层面经历的完整链路是init生成随机 ID、随机不可约 chunker 多项式写入加密的config用随机口令 scrypt派生密钥创建密钥文件封存主密钥到keys/备份时以 Rabin 指纹 CDC 把文件切成 512 KiB 8 MiB 的 Blob逐个压缩v2、以主密钥 AES-256-CTR Poly1305-AES 加密流式写入data/下的 Pack写索引index/记录明文哈希 → Pack 内位置/长度写快照 JSONsnapshots/引用根 tree形成一次原子可见的备份结果prune等修改操作遵循读写顺序规则并先获取锁保证崩溃安全。理解这套设计后你可以直接阅读仓库其余部分深入细节格式化与打包入口见 internal/repository/pack/pack.go密钥派生见 internal/repository/crypto/kdf.go快照与树对象模型见 internal/restic 目录如 snapshot.go、tree.go锁机制见 internal/repository/lock.go。若想亲手验证每个格式声明可对任意测试仓库执行restic cat snapshot / tree / blob / masterkey系列命令对照本文示例。【免费下载链接】resticFast, secure, efficient backup program项目地址: https://gitcode.com/GitHub_Trending/re/restic创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考