Restic用户数据的映射
本文内容基于restic v0.12.0
Restic数据概念
仓库(Repository)
存放Restic备份数据的位置,可以是本地文件系统的某个路径,也可以使用远程存储服务, 如sftp server,rest server,存储提供商等。每个仓库相互独立,不同仓库的备份数据相互独立。
v0.12.0中已支持的存储服务包括:aws s3,minio server,Wasabi, Aliyun OSS, OpenStack Swift,Backlbaze B2,Azure Blob Storage,Google Cloud Storage,rclone
数据块(Blob)
加密的数据块及元数据,其中元数据包括长度,SHA-256 哈希信息。
数据块可以存放文件数据(data),也可以存放目录结构数据(tree)。
Blob的大小在512KiB到8MiB之间,因此小于512KB的文件不会被拆分。Restic的实现目标是让Blob平均大小为1MiB。
数据包(Pack)
Restic中的单个数据文件,包括一个或多个Blob,一旦创建不再修改。
一般只创建不删除,仅prune操作会删除不再被引用的数据。
Storage ID
Pack文件的SHA256哈希值,通过这个ID可以在仓库中加载需要的数据文件。Restic将这个ID作为Pack的文件名,也就是文件的SHA256哈希值。Pack文件名即哈希值的设计也可以方便的检验数据文件是否被改动过。
快照(Snapshot)
备份文件/文件夹数据在某一时间点的状态,包括数据的内容和元数据信息。
Restic数据分布
Restic仓库的布局
除了keys文件夹下的文件以外,所有文件都经过AES-256 CTR加密,数据完整性则通过Poly1305-AES (Message Authentication Code, MAC)校验。
config文件
解密后的config文件:
{
"version": 1,
"id": "5956a3f67a6230d4a92cefb29529f10196c7d92582ec305fd71ff6d331d6271b",
"chunker_polynomial": "25b468838dcb75"
}
id是决定repository的唯一性标识。
chunker_polynomial用于把大文件分割成小文件。
keys文件夹
Restic仓库中唯一不使用加密的文件。
光有Key文件无法解密Restic仓库中的任何文件。
配合restic仓库的password才能获得AES key。
同一个仓库可以创建多个key,但是解析出来的masterkey是一样的。
Key文件示例:
$ cat keys/16bce2ed63bed663197fc07b90b38501743ebff7a7f86653cbb77b5f8474a55e | python -m json.tool
{
"N": 32768,
"created": "2021-04-18T22:47:36.620939749+08:00",
"data": "hIdz1aIXuGWa1ILNma/4Ke+VvHTteKsGdeKBHxFafdgY1htKOekBY8gEZuHWsKn0vyLeXx3VUeVfGWcl3SKHCyiHkwa0k8wPluUdP60EbxtgiT+hNsDz47GcriOmH/CUk/8sEx+GOSiEyl8iTklyq0L9yD/BH3TEUeF9Ry4AUaufh+vEhZbGqe+7OTSs5muOJAvAVCwZAX337HZoeGielw==",
"hostname": "k8sdev",
"kdf": "scrypt",
"p": 5,
"r": 8,
"salt": "xj2mRxc9zEEplLGNQJ+jUw62zdx5eqGAHqA2CHpZiiorsrSTFRlUQ8ToYqKvi8r3cNCYpHDRy6PfPdaJ5Nqchw==",
"username": "root"
}
data文件夹
所有Pack文件存放路径,每个Pack文件可以包含多个加密后的Blob数据,Pack文件的文件名为文件内容的sha256sum值。
index文件夹
存放Pack文件信息,包括每个Pack文件有哪些Blob,Blob原始数据的类型和哈希值信息。
解密后的数据示例:
{
"packs": [
{
"blobs": [
{
"id": "c18d92ccf1b69fa1c0a2854e6e2967c8f374215f33fd4df6bab838e35c4174a6",
"length": 60,
"offset": 0,
"type": "data"
}
],
"id": "984a09e24565201fd71de9276d9050e17e408e25adf6beefa7accf2039c0040c"
},
{
"blobs": [
{
"id": "81963a42c7d01564b7893e17d7ff30a00f5f745252b8b54ce2aec27be78b970f",
"length": 392,
"offset": 0,
"type": "tree"
},
{
"id": "01a2dd0625bbc6ec8676d1166010e74198e06437717a333b9239ed29b0bae583",
"length": 489,
"offset": 771,
"type": "tree"
},
{
"id": "6bd1574077efd77fe5705bbaca090e69c1c77967faac198b03aa85dde9fe703d",
"length": 379,
"offset": 392,
"type": "tree"
}
],
"id": "25d5ac46e385ea523ee96571290d70d1381eb3be5c897f775d770a49a9c2bcba"
}
]
}
snapshots文件夹
存放所有snapshot的信息
解密后的数据示例:
{
"hostname": "k8sdev",
"paths": [
"/mnt/data/test"
],
"time": "2021-04-18T23:02:07.59377716+08:00",
"tree": "01a2dd0625bbc6ec8676d1166010e74198e06437717a333b9239ed29b0bae583",
"username": "root"
}
Pack文件格式
e.g. openssl解密aes-256-ctr
echo <ciphertext> | openssl aes-256-ctr -d -iv <IV> -K <AES Key>
Type_Blob: 1 byte
Length: 4 bytes
Hash: 32 byte
通过定长4Byte的Header_Length得到加密后的Header长度,解密后的Header存放Pack中所有Blob的元数据信息,包括Blob类型(数据 / 树),加密后Blob长度,原始Blob数据哈希值。通过Header可以获取所有Blob信息。
实例
创建restic repo及数据备份
$ restic -r s3:http://localhost:31605/mapping/test init
创建restic repo时会要求用户输入密码,此密码将用于产生restic的masterkey。而masterkey用于解密数据。因此如果没有repo的password,repo中的所有数据都无法解析。
$ echo "Test Restic Data @2021.4.18" > /mnt/data/test
$ restic -r s3:http://localhost:31605/mapping/restictest backup /mnt/data/test
调试时可以通过全局变量RESTIC_PASSWORD和RESTIC_REPOSITORY来简化命令行输入,生产环境不建议把信息放在全局变量中。
通过restic命令查看备份文件内容
$ restic -r s3:http://localhost:31605/mapping/restictest find test
repository 293f88eb opened successfully, password is correct
found 2 old cache directories in /root/.cache/restic, run restic cache --cleanup to remove them
Found matching entries in snapshot f30f99e6 from 2021-04-18 23:02:07
/mnt/data/test
或:
$ restic -r s3:http://localhost:31605/mapping/restictest snapshots
repository 293f88eb opened successfully, password is correct
found 2 old cache directories in /root/.cache/restic, run restic cache --cleanup to remove them
ID Time Host Tags Paths
---------------------------------------------------------------------
f30f99e6 2021-04-18 23:02:07 k8sdev /mnt/data/test
---------------------------------------------------------------------
1 snapshots
获得snapshot id: f30f99e6
$ restic -r s3:http://localhost:31605/mapping/restictest cat snapshot f30f99e6
repository 293f88eb opened successfully, password is correct
found 2 old cache directories in /root/.cache/restic, run restic cache --cleanup to remove them
{
"time": "2021-04-18T23:02:07.59377716+08:00",
"tree": "01a2dd0625bbc6ec8676d1166010e74198e06437717a333b9239ed29b0bae583",
"paths": [
"/mnt/data/test"
],
"hostname": "k8sdev",
"username": "root"
}
信息树:文件夹mnt -> 文件夹data -> 文件test
$ restic -r s3:http://localhost:31605/mapping/restictest cat blob 01a2dd0625bbc6ec8676d1166010e74198e06437717a333b9239ed29b0bae583 | python -m json.tool
{
"nodes": [
{
...
"name": "mnt",
"subtree": "6bd1574077efd77fe5705bbaca090e69c1c77967faac198b03aa85dde9fe703d",
"type": "dir",
...
}
]
}
$ restic -r s3:http://localhost:31605/mapping/restictest cat blob 6bd1574077efd77fe5705bbaca090e69c1c77967faac198b03aa85dde9fe703d | python -m json.tool
{
"nodes": [
{
...
"name": "data",
"subtree": "81963a42c7d01564b7893e17d7ff30a00f5f745252b8b54ce2aec27be78b970f",
"type": "dir",
...
}
]
}
$ restic -r s3:http://localhost:31605/mapping/restictest cat blob 81963a42c7d01564b7893e17d7ff30a00f5f745252b8b54ce2aec27be78b970f | python -m json.tool
{
"nodes": [
{
"content": [
"c18d92ccf1b69fa1c0a2854e6e2967c8f374215f33fd4df6bab838e35c4174a6"
],
...
"name": "test",
"type": "file",
...
}
]
}
$ restic -r s3:http://localhost:31605/mapping/restictest cat blob c18d92ccf1b69fa1c0a2854e6e2967c8f374215f33fd4df6bab838e35c4174a6
repository 293f88eb opened successfully, password is correct
found 2 old cache directories in /root/.cache/restic, run restic cache --cleanup to remove them
Test Restic Data @2021.4.18
解密config文件数据
$ pwd
/var/lib/kubelet/pods/8a4e972b-890c-430b-98a5-cea49b9e8c19/volumes/kubernetes.io~empty-dir/storage/mapping/restictest
$ restic -r s3:http://localhost:31605/mapping/restictest cat masterkey
repository 293f88eb opened successfully, password is correct
found 2 old cache directories in /root/.cache/restic, run restic cache --cleanup to remove them
{
"mac": {
"k": "RGylXomY4XuPErQ2akibdQ==",
"r": "5PqZDNhWXA2wb0QCBGuACg=="
},
"encrypt": "AKQMbh9rlNgi00iZdSIeeYq9e+z/Tf/LO/2LomA6/Xs="
}
$ echo "AKQMbh9rlNgi00iZdSIeeYq9e+z/Tf/LO/2LomA6/Xs=" | base64 -d | xxd -p -c32
00a40c6e1f6b94d822d3489975221e798abd7becff4dffcb3bfd8ba2603afd7b
$ cat config | head -c 16 | xxd -p
23c13620928f7abd72a8374b88142dff
$ cat config | head -c -16 | tail -c +17 | openssl aes-256-ctr -d -iv 23c13620928f7abd72a8374b88142dff -K 00a40c6e1f6b94d822d3489975221e798abd7becff4dffcb3bfd8ba2603afd7b | python -m json.tool
{
"chunker_polynomial": "25cb5ee4aa0503",
"id": "293f88eb74731e997b413d08462210e09f2266f641ace775f835b005a651b73a",
"version": 1
}
Pack文件数据分布分析
root@k8sdev restic $ cat data/98/984a09e24565201fd71de9276d9050e17e408e25adf6beefa7accf2039c0040c | tail -c -4 | hexdump -C
00000000 45 00 00 00 |E...|
00000004
4 Bytes小端存储 => 4 * 16 + 5 = 69 bytes
69 bytes中,头16 bypes IV, 最后16 bytes MAC
$ cat data/98/984a09e24565201fd71de9276d9050e17e408e25adf6beefa7accf2039c0040c | head -c -4 | tail -c 69 > /tmp/encryted_header
IV信息:
$ cat /tmp/encryted_header | head -c 16 | xxd -p
dbcff665ec849258563837f9144c6798
解密后Header信息:
Restic代码中Header信息的定义:
// headerEntry describes the format of header entries. It serves only as
// documentation.
type headerEntry struct {
Type uint8
Length uint32
ID restic.ID
}
所以,在这个pack文件的header里的信息是:
只有一个blob
是一个数据blob
加密后的Blob数据长度:3c => 3 * 16 + 12 = 60
原始数据的sha256 哈希值是 c18d92ccf1b69fa1c0a2854e6e2967c8f374215f33fd4df6bab838e35c4174a6
对比实际数据:
$ echo "Test Restic Data @2021.4.18" | sha256sum
c18d92ccf1b69fa1c0a2854e6e2967c8f374215f33fd4df6bab838e35c4174a6 -
解密数据:
IV值:
$ cat data/98/984a09e24565201fd71de9276d9050e17e408e25adf6beefa7accf2039c0040c | head -c 16 | xxd -p
b6a4171e2da7239c30ce41f8707eaef7
解密:
$ cat data/98/984a09e24565201fd71de9276d9050e17e408e25adf6beefa7accf2039c0040c | head -c -89 | tail -c +17 | openssl aes-256-ctr -d -iv b6a4171e2da7239c30ce41f8707eaef7 -K 00a40c6e1f6b94d822d3489975221e798abd7becff4dffcb3bfd8ba2603afd7b
Test Restic Data @2021.4.18
<== 其中 head -c 89, 89 = MAC长度(16) + 加密后header长度(69) + header长度(4)
目标是截取数据中红色部分:
综上,给定一个想要查看的文件和snapshot的时间点,Restic可以通过snapshot及文件所在目录找到文件信息,从而获取文件信息content中Blob的哈希值,通过index找到每一个数据blob对应的pack和偏移量,或者直接从pack的header信息中获得改信息。解密后即可获得文件的原始数据。
小结
本文从以下几个方面介绍了Restic对于用户数据分布的映射:
Restic的基本数据单元概念
repository,pack,blob,storage ID,snapshot
Restic repository下数据分布及格式
config, keys, data, index, snapshots
通过实例讲解Repository中数据的映射及分布关系
- Repository的创建及备份
- 通过Restic命令查看文件信息
- 解密一个文件
- 解密一个pack文件中的一个blob