11 个版本 (2 个稳定版)
1.0.1 | 2021 年 12 月 3 日 |
---|---|
1.0.0 | 2021 年 2 月 21 日 |
0.2.5 | 2018 年 1 月 10 日 |
0.2.4 | 2017 年 8 月 15 日 |
0.1.3 | 2017 年 2 月 25 日 |
#1066 在 密码学
每月 35 次下载
1MB
23K SLoC
Ensync
简介
Ensync 是一个加密文件同步器。也就是说,它通过一个中心“服务器”(可能只是一个闪存驱动器)位置在客户端之间同步文件,但加密服务器上的数据,使得在没有密钥的情况下无法恢复存储在服务器上的数据。
功能
-
支持全双向同步。
-
灵活、分层的同步规则配置。
-
可选的透明压缩,用于文件传输和服务器端存储。
-
透明块去重。如果两个客户端在相同的存储位置存储相同的文件,即使客户端无法读取彼此的数据,底层数据存储也将被共享。
-
支持多个密码/密钥访问服务器存储。
-
支持单独的密钥组,以防止某些客户端读取/写入其他客户端的数据。
为什么使用 Ensync?
-
您想在星型模型(即一个所有系统都同步的中心存储库)中同步多个系统之间的文件,但中心点无法使用加密或存在问题。例如,中心点可能是一个需要无人工干预启动的服务器,或者是一个托管服务器。
-
您想在多个系统之间同步文件,并希望使用比 Unison 等其他类似工具更灵活或更透明的工具。
-
您想将多个系统备份到同一存储,并去重共享数据,而无需从对其中任何一个系统的读取访问升级到对所有系统的读取访问。(仅为此用途,还值得考虑 Tarsnap。)
请注意,Ensync 的加密属性尚未经过独立验证。
支持的平台
-
作者经常在 FreeBSD (AMD64)、DragonFly BSD 和 Linux (AMD64 和 ARM7) 上使用 Ensync,因此已知这些系统上运行良好。
-
一般来说,任何 Rust 支持的 POSIX-like 系统都应该能够运行 Ensync。
-
目前不支持Windows。虽然从本质上讲不是不可能,但Windows文件系统的怪癖使得支持难以实现。目前没有计划实现此类支持,但欢迎贡献力量。
状态
稳定。该应用程序已经稳定使用多年,没有出现重大问题。
内容
入门指南
首先当然是要安装Ensync。现在最简单的方法是使用cargo
cargo install ensync
如果你使用的是DragonFly,还需要传递--no-default-features
。
如果你将同步到远程主机,Ensync也需要在那里安装。
之后,你需要设置配置,如果需要,初始化远程存储。这可以通过ensync setup
自动完成,或者通过手动完成每个步骤。
自动设置
ensync setup
命令将写入配置并自动执行所有设置步骤,同时进行一些合理性检查。如果你之前没有使用过ensync,建议使用。对于不寻常的设置,可能需要手动设置。
ensync setup
的基本调用需要三个参数
-
配置位置。这必须命名一个要创建的目录。配置文件和同步状态将放置在此目录中。
-
本地文件位置。这是一个包含你想要同步的文件的目录。
-
远程存储位置。这可能是在你的本地文件系统上的目录,也可能是一个
user@host:path
格式的scp-style字符串。在后一种情况下,ensync setup
将使用ssh user@host
连接到远程主机。
如果你不想从终端读取密码,可以将--key
参数传递给ensync setup
,它接受一个字符串,格式为密码配置。如果你使用一个file
密码,生成的配置将引用该文件的绝对路径。在shell
密码的情况下,配置仅保留你传入的字符串。在这两种情况下,你可以在设置完成后将文件移动到配置目录中,并相应地编辑配置。
一旦ensync setup
成功完成,你应该可以正常使用。要实际同步文件,只需运行
ensync sync /path/to/config
手动设置
首先,创建一个新的目录作为配置目录。在此目录中,创建一个名为config.toml
的文件。有关配置的含义的详细信息,请参阅配置参考部分。一个最小示例
[general]
path = "/path/to/your/files"
server = "shell:ssh yourserver.example.com ensync server ensync-data"
server_root = "your-logical-root"
passphrase = "prompt"
compression = "best"
[[rules.root.files]]
mode = "cud/cud"
如果是首次使用该特定存储实例,需要初始化密钥存储
ensync key init /path/to/config
如果你选择的server_root
尚未在服务器上创建,你也需要现在处理这个问题
ensync mkdir /path/to/config /your-logical-root
处理完这些问题后,你应该可以正常使用,并可以运行
ensync sync /path/to/config
以开始同步。
关于同步过程
概述
Ensync使用三阶段同步过程
- 搜索更改。
- 协调并应用更改。
- 清理。
在第一阶段,Ensync扫描之前标记为干净的每个本地目录,以查看是否有任何更改,并类似地测试标记为干净的远程端目录是否已更改。此阶段仅用于确定需要处理的目录。
在“协调并应用更改”阶段,Ensync递归地遍历客户端和服务器目录树,忽略仍然干净的目录(包括所有子目录)。不同的目录并行处理。
对于每个检查的目录,Ensync列出了所有文件,通过名称进行匹配,然后确定应应用哪些更改。当对账完成时,它按顺序将所选更改应用到每个文件。如果所有操作都成功,则标记目录为干净。
在清理阶段,清理临时文件和孤立文件,并提交易变数据。
高效的文件重命名处理
Ensync不知道文件重命名操作;它只是将它们视为删除一个文件然后创建另一个文件。然而,Ensync确实采取了一些步骤来防止在这种情况下需要重新传输文件内容。
在服务器上,删除文件的块会在某个客户端运行其清理阶段之前保留。这意味着如果在同一同步过程中创建了具有相同内容的新文件,它将能够重用这些块,而无需重新上传。
本地,删除的文件被移动到配置/状态目录内的临时位置,直到清理阶段才会实际删除。如果再次遇到相同的内容,则将数据从移动的文件中复制出来,而不是从服务器重新下载。然而,这种功能要求正在同步的目录与配置目录位于同一文件系统上。如果不是,删除的文件将立即被删除,因此在这种情况下,重命名将需要从服务器重新下载。
请注意,由于这些优化,磁盘使用量不会因删除而减少,直到Ensync完成,这可能会在同时创建和删除大量大文件时导致问题。
并发性和故障
如果在Ensync运行时任何客户端文件被修改,它不会表现错误,只要它不会损坏服务器上的存储。然而,这些文件不能被捕获在原子状态,因此可能不太有用。例如,不要尝试使用Ensync复制或备份实时数据库。
同一配置上不能同时运行多个Ensync实例。可以在同一本地目录树上运行具有不同配置的多个Ensync实例,但要意识到它们可能会看到彼此的中间状态并传播它们。可以对同一服务器存储运行任意数量的Ensync实例。
如果服务器进程被无礼地杀死,它可能会泄露临时文件,但不会损坏存储。
如果客户端进程在完成之前死亡,可能会泄露一些临时文件,文件系统可能会处于中间状态,但不会丢失数据。在某些情况下,缓存的数据可能会丢失,这将导致下一次同步比平常慢得多。
在电源中断的情况下,所有已提交的更改预计都会幸存,条件是操作系统的fsync
调用实际上同步了数据,并且底层硬件表现正常。一个例外是祖先状态,在某些情况下可能会由于操作系统的意外终止而被破坏。如果发生这种情况,下一次同步将比正常更保守,这通常表现为撤销删除并创建额外的冲突文件。
处理单个文件时发生的错误通常会在不中断其余同步过程的情况下记录。
文件系统限制
仅支持常规文件、符号链接和目录。不支持其他类型的文件。
同步常规文件和目录的基本读写/执行权限,以及常规文件的修改时间。其他属性和备用数据流将被忽略。
在大小写不敏感的文件系统(或者更普遍地,任何认为一个字节序列与名称为不同字节序列的目录条目匹配的文件系统)中使用Ensync通常不是一个好主意,但只要没有导致服务器上的两个文件在大小写不敏感的文件系统中被认为是相同的,则部分支持。如果发生这种情况,或者在服务器和客户端上创建了相同但名称不同的文件,结果通常涉及一个版本覆盖另一个或两个合并。这里不提供任何保证,Ensync在这些条件下也没有经过测试。
强烈不建议在执行名称规范化的文件系统(即,尝试创建一个字节序列名称的文件会导致创建一个不同字节序列名称的文件)中使用Ensync。这些规范化将作为对Ensync的重命名出现,并将作为此类(即,删除和创建)进行传播。在实践中,只要所有参与者使用完全相同的规范化或永远不会创建任何被任何规范化更改的名称,就不会出现严重问题。如果有多个参与者使用不同的规范化,每次每个参与者同步时都会发生重命名冲突。请注意,某些具有特定风味的水果操作系统不仅默认具有规范化文件系统,而且使用与世界上其他地方不同的规范化。
Ensync不识别硬链接。由于它从不就地覆盖文件,因此具有硬链接不会引起问题,但Ensync可能将它们转换为单独的文件,并且它们将在其他系统上作为单独的文件创建。如果您的文件系统实际上支持它们,目录之间的硬链接根本不受支持,并且可能会引起许多问题。
如果您的系统允许将目录作为常规文件打开(例如,FreeBSD),如果某个时刻某个东西将常规文件更改为目录,您可能会遇到奇怪的情况。本地不会丢失任何数据,但目录的原始内容可能会以常规文件的形式传播。
配置参考
概览
配置是一个存储在配置目录下名为 config.toml
的 TOML 文件。它有两个必填部分:[general]
和 [rules]
。所有文件名都是相对于配置目录的。配置看起来像这样
[general]
# The path to the local files, i.e., the directory containing the cleartext
# files to be synced.
path = "/some/path"
# The server location, i.e., where the encrypted data is stored. The exact
# syntax for this described below.
server = "path:/another/path"
# The name of the logical root on the server to sync with. This is the name of
# a directory under the physical root of the server which is used as the top of
# the directory tree to be synced.
server_root = "the-root-name"
# How to get the passphrase to derive the encryption keys. The formats
# supported are described below.
passphrase = "prompt"
# What level of transparent file compression to use. Valid values are "none",
# "fast", "default", "best". This configuration can be omitted, in which case
# it defaults to "default".
compression = "default"
# Files uploaded to the server are split into blocks of this size. Identical
# blocks are only stored once on the server. A smaller block size may make this
# deduplication more effective, but will slow some things down. This can be
# omitted and will default to one megabyte. If you change it, take care that
# the block size actually corresponds to what you intend to deduplicate.
#
# All ensync configurations for the same store MUST use the same block size, as
# the block size influences the way ensync computes file identity. Two
# configurations operating on the same files with different block sizes will
# perceive edit conflicts where there are none. Differing configurations
# operating on separate files simply fail to deduplicate files between each
# other effectively.
#
# To change this value on an already-existing store, ensure your local files
# are up to date with the server, then run `ensync sync` with
# `--override-mode=reset-server --strategy=scrub`. This will re-upload all
# files affected by the block size change.
#
# The default value is 512 bytes less than 1MB, which gives a reasonable
# balance for most use cases and gives some headroom so that maximum-size
# blocks do not ever so slightly spill into additional file system allocation
# units.
block_size = 1048064
# Specifies the sync rules. This is described in detail in the "Advanced Sync
# Rules" section. The example here is sufficient to apply one sync mode to
# all files.
[[rules.root.files]]
# The sync mode to use; that is, it describes how various changes are or are
# not propagated. This example is conservative full bidirectional sync. See
# "Understanding the Sync Model" for a full description of what this means.
mode = "cud/cud"
服务器配置
服务器配置可以采用两种形式之一。
使用 path:some-path
时,将“服务器”简单地视为本地文件系统上的 some-path
路径。例如,这就是用于同步到闪存驱动器的做法。
使用 shell:some command
时,将启动一个服务器进程 some command
。客户端进程通过子进程的标准输入和输出与服务器进行通信。通常,与服务器通信的是 ensync server
命令。由于这种设计,您可以将其与任何可以转发标准输入和输出的东西组合起来,例如 ssh,这是通常用于同步到远程主机的常用方法。例如,配置
server = "shell:ssh myserver.example.org ensync server sync-data
这将导致Ensync客户端通过ssh连接到myserver.example.org
并运行ensync server sync-data
,这反过来又导致加密数据存储在~/sync-data
中。
密码配置
passphrase
配置可以采用四种形式之一。
prompt
指定从控制终端读取密码。这在大多数平台上都得到支持,但并非所有平台(DragonFly是主要的例外)。
string:xxx
指定使用xxx
作为字面密码。
file:some-file
指定读取some-file
的内容,并将其用作密码。任何尾随的CR或LF字符都将从输入中删除。
shell:some command
指定将some command
传递给shell,并使用命令的标准输出作为密码。与file
类似,尾随的CR和LF字符也将被删除。
理解同步模型
简介
Ensync同步模型本质上是在每个目录的内容上执行三次合并。
如果我们首先考虑同步作为目录内容的双向合并可能如何工作,那么这种工作方式可能更清晰。双向目录合并意味着列出每个相应目录的内容,然后根据名称将每个副本中的文件匹配在一起。然后,确定每个文件或文件对需要做什么更改。例如,我们可能访问一个目录并构建如下表格
文件名 | 客户端内容 | 服务器内容 |
---|---|---|
hello.txt | hello world | hello world |
password.txt | hunter2 | hunter2 |
这个目录显然是同步的;客户端和服务器对一切意见一致。现在让我们编辑其中一个文件。
文件名 | 客户端内容 | 服务器内容 |
---|---|---|
hello.txt | hallo welt | hello world |
password.txt | hunter2 | hunter2 |
当我们的双向合并器运行时,它会看到两个副本对hello.txt
的内容意见不一致,因此显然必须对其中一个进行编辑以匹配另一个。但没有明确的选择方式。一个选项是使用修改时间较晚的那个;在这种情况下,将是客户端,这正是我们想要的。
这种方法称为“最后写入者胜出”,对于许多情况确实有效,但它也存在很多问题。例如,如果您从备份中恢复了一些文件,并且备份工具还恢复了它们的修改时间,那么我们的双向合并器会立即撤销恢复,因为服务器端的文件会更新。
但是,一旦我们将文件创建和删除纳入考虑,双向合并就会完全破裂。让我们看看如果我们在客户端删除“password.txt”,而有人在服务器上创建了一个新文件会发生什么
文件名 | 客户端内容 | 服务器内容 |
---|---|---|
hello.txt | hello world | hello world |
memo.txt | (none) | some text here |
password.txt | (none) | hunter2 |
注意,对于这两个文件,表格看起来是完全相同的。没有任何逻辑可以正确处理创建和删除。而且这次我们没有任何时间戳可以用来工作。
Ensync所做的是添加一个第三个副本,即“祖先”副本,它存储客户端和服务器副本曾经同步的每个文件的最后状态。这个祖先副本用于确定哪个端副本实际上已经更改。因此,我们的“一切同步”表实际上看起来像
文件名 | 客户端内容 | 祖先内容 | 服务器内容 |
---|---|---|---|
hello.txt | hello world | hello world | hello world |
password.txt | hunter2 | hunter2 | hunter2 |
并且创建和删除的表格实际上看起来像
文件名 | 客户端内容 | 祖先内容 | 服务器内容 |
---|---|---|---|
hello.txt | hello world | hello world | hello world |
memo.txt | (none) | (none) | some text here |
password.txt | (none) | hunter2 | hunter2 |
现在,创建和删除的情况不同。我们可以清楚地看到,“password.txt”曾经存在于两个端副本中(因为它在祖先副本中),但现在已从客户端删除,因此被删除了,以及“memo.txt”之前从未见过,因此必须是创建。
注意,为了简化,我们标记祖先副本具有特定内容,但在现实中它只存储内容哈希值,不存储实际的文件内容。
同步模式
祖先副本允许Ensync确定什么已经改变。配置的同步模式告诉Ensync应该响应哪些改变。
通常将同步指定为7个字符的字符串,格式如下:cud/cud
。每个字母是一个标志;小写字母表示“开启”,大写字母表示“强制”,用连字符替换字母表示“关闭”。以下显示了每个标志的名称和含义。
┌─────── "Sync inbound create"
│ New remote files are downloaded to the local filesystem
│┌────── "Sync inbound update"
││ Edits to remote files are applied to local files
││┌───── "Sync inbound delete"
│││ Files deleted remotely are deleted in the local filesystem
cud/cud
│││
││└─ "Sync outbound delete"
││ Files deleted in the local filesystem are deleted remotely
│└── "Sync outbound update"
│ Edits to local files are applied to remote files
└─── "Sync outbound create"
New local files are uploaded to remote storage
更具体地说,每个标志适用于在任何条件下可能对该副本进行的更改,因此它们也可以被视为给予Ensync执行该类型操作“权限”。例如,如果“同步入站创建”关闭,则Ensync永远不会在本地创建任何新文件。
将标志设置为“强制”将导致Ensync在必要时执行该操作以使副本同步,即使这可能导致数据丢失。
-
“强制创建”将在反向边界删除设置关闭的情况下重新创建已删除的文件。
-
“强制删除”将在反向边界创建设置关闭的情况下删除新文件。它还将导致在反向边界创建设置关闭的情况下解决编辑-删除冲突,有利于删除。
-
“强制更新”将在反向更新设置关闭的情况下撤销更新。它还将导致在反向更新设置关闭的情况下解决编辑-编辑冲突,有利于没有“强制更新”的一侧。如果两侧都有关“强制更新”,则编辑-编辑冲突将自动有利于修改时间较新的一侧或客户端。
最有用的同步模式也有别名
-
mirror
和reset-server
是---/CUD
(所有出站设置为“强制”,所有入站设置为“关闭”)的别名。这会导致服务器副本被修改以精确匹配客户端,而不对客户端进行任何修改。 -
reset-client
是CUD/---
(所有入站设置为“强制”,所有出站设置为“关闭”)的别名。这是mirror
/reset-server
的相反,它将本地文件系统修改为精确反映服务器端的状态。 -
conservative-sync
是cud/cud
的别名,即,保守的双向同步。 -
aggressive-sync
是CUD/CUD
的别名,即,双向同步,自动解决所有冲突。
其他同步标志
还有一些不太常用的标志用于调整协调过程。有关详细信息,请参阅每个标志的文档。
非冲突状态
下表显示了针对各种(客户端、祖先、服务器)状态和同步模式的操作。同步模式或状态中的 *
表示“任何”;状态中的大写字母表示内容(例如,(A,A,A)
表示客户端、祖先和服务器具有相同的文件内容;(A,∅,B)
表示客户端和服务器具有不同的文件内容,而祖先没有内容)。冲突在此显示,但在下一节中讨论。
状态 | 同步模式 | 操作 |
---|---|---|
(∅,*,∅) |
***/*** |
无 |
(∅,∅,A) |
c**/*** |
在客户端创建文件 |
(∅,∅,A) |
-**/**D |
在服务器上删除文件 |
(∅,∅,A) |
(否则) | 保持不同步 |
(∅,A,A) |
***/**d |
在服务器上删除文件 |
(∅,A,A) |
C**/**- |
在客户端重新创建文件 |
(∅,A,A) |
(否则) | 保持不同步 |
(∅,A,B) |
***/*** |
编辑-删除冲突 |
(A,∅,∅) |
***/c** |
在服务器上创建文件 |
(A,∅,∅) |
**D/-** |
在客户端删除文件 |
(A,∅,∅) |
(否则) | 保持不同步 |
(A,A,∅) |
**d/*** |
在客户端删除文件 |
(A,A,∅) |
**-/C** |
在服务器上重新创建文件 |
(A,B,∅) |
***/*** |
编辑-删除冲突 |
(A,*,A) |
***/*** |
无 |
(A,A,B) |
*u*/*** |
将客户端更新到B |
(A,A,B) |
*-*/*U* |
将服务器重置为A |
(A,A,B) |
(否则) | 保持不同步 |
(A,B,B) |
***/*u* |
将服务器更新到A |
(A,B,B) |
*U*/*-* |
将客户端重置为B |
(A,B,B) |
(否则) | 保持不同步 |
(A,∅,C) |
***/*** |
编辑-编辑冲突 |
(A,B,C) |
***/*** |
编辑-编辑冲突 |
冲突状态
当客户端和服务器自上次同步以来都改变了状态时,就会发生冲突。Ensync通常尽可能保守地处理冲突,但这可以通过同步模式进行控制。
当一个副本自上次同步以来已修改文件,而另一个副本已删除该文件时,会发生编辑-删除冲突。Ensync根据同步模式采取以下第一种选择来解决编辑-删除冲突
-
如果删除文件的方面启用了创建,则使用新内容重新创建文件,因为处理一个不需要的额外文件比恢复已删除的数据更容易。
-
如果编辑文件的方面将删除设置为“强制”,则从该方面删除它。
-
否则,保持文件不同步。
当一个副本自上次同步以来已更改文件的状态,并且这些状态不同时,就会发生编辑-编辑冲突。如果副本各自创建具有不同内容的文件,这也被认为是编辑-编辑冲突。编辑-编辑冲突通过根据同步模式采取以下第一种选择来处理
-
如果两个更新设置都是“强制”,并且两个副本都具有修改时间,则使用较晚修改时间版本的版本。
-
如果只有一个更新设置是“强制”,则使用来自相反副本的版本。
-
如果两个更新设置都不是“开启”,则保持文件不同步。
-
如果两个创建设置都至少是“开启”,则将冲突文件在服务器上重命名(例如,
foo.txt
→foo~1.txt
)然后将两个版本都作为创建传播到每一方。 -
否则,保持文件不同步。
对于“不太重要”的文件属性,如模式或时间戳,Ensync总是隐式解决冲突,不会重命名或创建新文件。此外,这些字段总是从传播的任何版本中携带。例如,如果您在本地对文件执行 chmod
,但实际上在服务器上更新了内容,并且两个“更新”设置都是开启的,则Ensync将更新文件以具有来自服务器的内容和模式,本质上忽略了chmod
的影响。
目录的奇怪之处
目录包含更多受同步影响的文件,这使上述模型变得复杂。不深入细节
-
如果上述规则会删除一个目录,Ensync会递归进入它并正常同步,将副本视为已删除且包含空目录。如果副本中仍包含目录的所有文件确实都被删除,则该目录本身也会被删除。但是,如果那里引入了新文件,则该目录路径将在其他副本上[重新]创建。
-
如果将目录编辑为非目录,Ensync将重命名仍然持有它的副本上的目录,然后按照上述递归删除情况继续。
高级同步规则
同步规则定义在配置文件中的rules
表中。规则分为一个或多个状态;初始状态称为root
。初始当前同步模式为---/---
,即“不执行任何操作”。
每个状态有两个规则组,规则在其中匹配,称为files
和siblings
。这些是状态下的子表;因此,例如,最小规则由[[rules.root.files]]
和[[rules.root.siblings]]
表数组组成。
每个规则组内的表具有类似的结构。每个规则由任意数量的不同条件组成,并具有一个或多个操作。(为了一致性,允许零操作,但单独使用没有用。)每个规则按照定义的顺序评估,并处理任何操作。
示例
在详细说明之前,以下一些示例可能会使所有这些语法的语法更清晰。首先,文档中之前看到的最低配置
[[rules.root.files]]
mode = "cud/cud"
这定义了在root
状态中定义的单个规则。由于没有条件,它适用于所有文件,并将每个文件的同步模式设置为cud/cud
。
让我们考虑一个更复杂的示例
[[rules.root.files]]
mode = "cud/cud"
[[rules.root.files]]
name = '~$'
mode = "---/---"
[[rules.root.siblings]]
name = '^\.git$'
switch = "git"
[[rules.git.files]]
mode = "---/---"
此配置在root
状态的files
规则组中定义了两个规则,在root
状态的siblings
规则组中定义了一个规则,并在git
状态的files
规则组中定义了一个规则。它可以按以下方式阅读
-
除非另有说明,否则对所有文件使用完整的对称同步。(第一规则:没有条件,将模式设置为
cud/cud
。) -
不同步以
~
结尾的文件。(第二规则:文件名匹配~$
的文件,将模式设置为---/---
。) -
如果进入包含
.git
文件的目录,则咨询git
状态。(第三规则:兄弟匹配^\.git$
,切换到git
。) -
不同步git仓库的内容。(第四规则:在
git
状态中,没有条件,将模式设置为---/---
。)
files
规则组
files规则组用于匹配单个文件(如目录条目)。通常,大多数配置都在这里进行,尽管无法使用它匹配git仓库。
目录中的每个文件都会单独与条件进行评估。任何匹配的操作只会应用于该文件本身。对于目录类型的文件,会保留目录内容的通用状态(例如,当前规则状态和当前同步模式)。
目录中的每个文件名都会与规则集进行一次精确评估。如果文件存在于客户端,则使用该处的数据进行匹配;否则,使用服务器端的文件版本。
siblings
规则组
siblings 规则组用于根据目录内容的其他部分来影响目录的全部内容。它专门设计为可以排除 git 仓库,但也可能有其他用途。
进入目录时,会检查目录中的每个文件,并测试每个规则的所有条件,记录匹配的规则集合。一旦检查完所有文件,就会按顺序应用每个匹配的规则。
需要注意的是,这适用于目录的全部内容;某些规则组合的匹配方式可能对单个文件来说是不可能的。例如,在以下配置中,
[[rules.root.siblings]]
name = '^a$'
mode = "cud/cud"
[[rules.root.siblings]]
name = '^b$'
stop = "all"
[[rules.root.siblings]]
name = '^a$'
mode = "---/---"
包含名为 a
和 b
的文件的目录内容将应用同步模式 cud/cud
,因为所有三个规则都匹配,但第二个规则阻止了第三个规则的处理。
请注意,为了使目录能够被处理 至少,包含目录的同步模式必须允许该目录最初存在;siblings
只在正常进入包含目录后被评估。
与 files
规则组不同,客户端和服务器上每个文件的每个版本都会测试是否匹配规则。如果两者不一致,这可能会导致意想不到的效果。例如,在以下配置中,如果自上次同步以来客户端的文件模式从 0666 更改为 0777,两个 规则都将应用。
[[rules.root.siblings]]
permissions = "0666"
include = "other-state-A"
[[rules.root.siblings]]
permissions = "0777"
include = "other-state-B"
因此,在构建依赖于文件内容的同步规则时必须谨慎。
条件
每个条件都是一个字符串值的 TOML 对。请注意,这意味着您不能在同一个规则中使用两个具有相同名称的条件;由于大多数条件都使用正则表达式,因此这不应该是一个问题。
所有匹配字符串的条件都将配置解释为正则表达式。目标字符串将被转换为 UTF-8,无效序列将被替换字符替换。正则表达式不是锚定的;如果需要,必须显式使用 ^
和 $
。
如果 Ensync 被移植到一个不传统地使用 /
作为路径分隔符的平台,规则引擎仍然会使用 /
,这既是为了简单,也是为了保持表达式可读。
name
匹配基本名称与给定表达式匹配的文件。例如,在路径 /foo/bar/baz.txt
中,baz.txt
是基本名称。
path
匹配路径与给定表达式匹配的文件,该路径是相对于同步根的。例如,如果同步 /foo/bar
,则文件 /foo/bar/plugh/xyzzy
将与 plugh/xyzzy
进行测试。
permissions
匹配模式与给定表达式匹配的文件。在匹配之前,模式被转换为左填充零的 4 位八进制字符串。
type
匹配给定物理类型的文件。目标字符串将是以下之一:f
(常规文件)、d
(目录)或 s
(符号链接)。无法匹配其他类型的文件,因为它们被硬编码为具有---/---
模式。
目标
匹配目标与给定表达式匹配的符号链接。非符号链接的文件不匹配。
更大
匹配大小大于给定字节数的常规文件。非常规文件不匹配。
更小
匹配大小小于给定字节数的常规文件。非常规文件不匹配。
操作
每个操作都是一个字符串值TOML对。规则内的操作按以下顺序评估。
模式
将当前同步模式设置为给定值。这将完全替换当前同步模式。
trust_client_unix_mode
可以是 true
或 false
(不带引号)。设置合并过程是否信任本地文件系统上文件的UNIX模式。
如果为 false
,合并过程将忽略远程副本上也存在的文件的实际UNIX模式,将其替换为远程副本上相应文件的模式。这可以防止模式更改在任一方向上的传播,这在客户端同步到不存储(例如,FAT32)或返回(例如,noexec
挂载选项)UNIX模式的本地文件系统时很有用。
默认值为 true
,即,通常同步UNIX权限。
包含
值可以是字符串或字符串数组。每个字符串标识不同的规则状态。规则处理递归到每个列出的规则状态,并处理当前文件组的所有规则。如果处理未停止,则正常从这个状态恢复。如果列出的状态在评估 include
时已被评估,则忽略它。
开关
值是标识规则状态的字符串。设置当前的“开关状态”。当前处理完成后,规则状态将切换到该状态,并清除开关状态。本质上,这控制了目录内文件使用的状态,而不影响目录本身。
停止
值必须是 return
或 all
。如果为 return
,则停止当前规则状态的评估;如果是通过 include
到达的,则继续在上级状态中处理。如果为 all
,则停止所有处理,包括通过 include
到达的上级状态。
示例
以下是一个简单的双向同步示例,但排除了git和hg仓库以及所有备份文件。
[[rules.root.files]]
mode = "cud/cud"
[[rules.root.files]]
name = "~$"
mode = "---/---"
[[rules.root.siblings]]
name = '^\.git$'
switch = "git"
[[rules.git.files]]
mode = "---/---"
以下是为每个机器制作特定文件的可能约定
[[rules.root.files]]
target = '/^\.![^/]*$'
mode = "---/---"
[[rules.root.files]]
name = '^\.!'
mode = "cud/cud"
switch = "private"
[[rules.private.files]]
mode = "cud/cud"
上述操作是建立一种约定,将以.!
开头的文件视为特定于机器的,然后不同步这些文件的符号链接。文件本身仍然同步。(这样做的目的是,像.bashrc
这样的可能变化的文件仍然可以在正确的位置供所有客户端访问,同时允许底层文件正常同步。)
密钥管理
Ensync允许将任何数量的密码/密钥与服务器存储关联。可以使用 ensync key
子命令来检查和操作密钥存储。
单独的密钥本身并不能控制对底层数据的访问;相反,多个密钥主要用于确保在无需更改其他地方的密钥的情况下,可以轻松撤销特定客户端的访问权限。
对于较短的密码短语(少于64个字符),Ensync使用一个非常强大的哈希函数,这可能会导致存储中的每个密钥需要几秒钟才能完成。如果您计划使用大量密钥,则生成更长的随机“密码短语”会有所帮助(例如,dd if=/dev/urandom of=key count=1
,然后将在配置中将file:key
作为密码短语)。
Ensync要求密钥存储中的所有密码短语都必须是唯一的。这确实是一个不寻常的要求。但是请注意,没有信息被泄露;客户端已经获得了整个密钥存储来测试其密码短语,所以密码短语冲突的简单事实并不是攻击者可以利用的新信息。限制存在的原因是,为了人体工程学,Ensync不使用密钥名和密码对,而是只使用密码,所以如果两个密钥有相同的密码,只能访问其中一个。
使用密钥组
密钥存储中的每个密钥都可以与任何数量的密钥组相关联。与密钥不同,密钥组实际上可以限制对信息的访问。具体来说,每个密钥组对应一个内部加密密钥,将密钥添加到密钥组中会给该密钥添加生成内部密钥所需的信息。
默认情况下,有两个密钥组
-
everyone
。所有密钥都始终在这个组中。everyone
组用作所有文件块的HMAC密钥,并用作物理根目录的加密密钥,默认情况下所有子目录也是如此。 -
root
。root
组授予编辑密钥存储的权限,并且是物理根目录的写入密钥,默认情况下所有子目录也是如此。
可以使用ensync key group
子命令来创建和分配密钥组。为了使密钥组有用,必须了解在服务器上加密事物时如何使用读取密钥和写入密钥。
目录的读取密钥是用于加密其内容的加密密钥。没有读取密钥的读取器无法读取目录或以有意义的方式操作目录内容。
写入密钥是必需的,以便通过正常的ensync实现写入目录或密钥存储。与读取密钥不同,它没有加密意义;相反,它是一种深度防御措施,以确保拥有对ensync一些访问权限但没有任何其他访问权限的客户端的泄露无法用于破坏密钥存储或造成更多数据丢失。
默认读取密钥来自everyone
组,默认写入密钥来自root
组。通常,目录继承其父目录的密钥。要更改密钥,请在该目录的名称中放入特殊语法。(这个机制非常响亮且“粘性”是故意的。)语法是在名称中任何地方放置.ensync[config=value,...]
。《code>config是以下之一
-
r
。将读取密钥设置为标识为value
的组的内部密钥。 -
w
. 将写入键设置为由value
标识的组内部键。 -
rw
. 将读取和写入键都设置为由value
标识的组内部键。
例如,一个名为 foo.ensync[r=my-group]
的目录将从 my-group
组中获得读取键。 foo.ensync[r=my-group,w=another-group]
分别将读取和写入键设置为 my-group
和 another-group
。
使用此功能的最简单方式是在逻辑根名称(即 server_root
配置键)中。例如,为了创建一个只有您的客户端可以访问的同步根,您可能需要这样做
$ ensync key group create /path/to/config my-group
$ ensync mkdir /path/to/config /my-client.ensync[rw=my-group]
然后编辑配置,将 my-client.ensync[rw=my-group]
作为 server_root
。
Ensync Shell
可以将 ensync
设置为用户的shell。这将使其始终表现得像以特定的(固定的)路径作为 ensync server
调用。这样做的目的是让远程客户端能够通过ssh访问Ensync,而不需要实际的shell访问。
此操作的基本步骤是
-
将
ensync
的完整路径添加到/etc/shells
。 -
使用
ensync
作为他们的shell创建用户。 -
在用户的家目录中,创建一个名为
ensync-server-dir
的符号链接,指向Ensync应该存储服务器数据的位置。
然后您可以直接使用 shell:ssh -T user@host
作为 server
配置。对于 ensync setup
,使用 user@host:
作为远程路径。
像往常一样,防御深度措施也是明智的。例如,使用户的家目录只读,并且不由他们所有,甚至可以在chroot中运行整个程序(这相当容易,因为 ensync
运行不需要除共享对象外的东西)。
安全考虑
Ensync如何进行加密
为了不重复文档,本节仅简要概述了细节。源代码有更详细的文档。
密码通过 scrypt 进行散列。客户端通过比较从服务器存储的值的SHA-3散列与导出密钥的SHA-3来验证密码是否正确。
每个密钥组代表一个随机生成的32字节内部键。要从导出密钥转换为内部密钥组,客户端将组名称和导出密钥的SHA-3 HMAC,然后与服务器上密钥存储中存储的序列进行XOR操作。每个32字节的内部键被分成一个16字节的AES密钥和一个16字节的HMAC密钥。
每个目录文件都通过AES CBC(128位)加密,使用随机的会话密钥和IV,这些密钥和IV本身也使用该目录的读取键加密,并存储在目录文件的开始处。目录使用读取键中的HMAC密钥通过SHA-3 HMAC进行内部校验和。
文件块通过AES CBC加密,使用从块内容及其内部密钥组的HMAC密钥的SHA-3 HMAC中获取的密钥和IV进行加密。
以下信息在服务器上以明文形式保存,被视为“本质上公开”的信息
-
密钥和密钥组的名称。
-
用于哈希每个口令的算法。
-
每个口令的盐。
-
每个口令的其他元数据。
-
每个口令派生密钥的SHA-3散列值。
-
将每个口令派生密钥和关联的密钥组名称的HMAC的XOR与该密钥组的内部密钥。
-
目录的随机32字节ID及其加密版本号。
-
使用
everyone
组的HMAC密钥对文件块内容进行HMAC的SHA-3散列值。 -
目录文件和文件块的长度。
当然,理想情况下,应该保护服务器,防止这种情况实际发生。
攻击者能够完成的事情
尽管Ensync设计用于保护您的文件安全,但也有一些权衡需要注意(包括一些可能打开特定侧信道的权衡),并且应该了解如果攻击者获取对各种信息的访问权限,他们可以学习或做什么。
以下内容应先注意
-
实践深度防御。也就是说,采取标准措施来保护服务器数据安全;使用加密通道与服务器通信(例如,ssh,而不是telnet);等等。
-
关于攻击者能做什么的声明是硬事实。关于攻击者不能做什么的声明,可能会因某些东西被破解而改变。
以下各节按严重程度大致顺序排列。
被动观察加密的Ensync ssh会话
很可能可以根据其流量模式识别Ensync。
观察者可以使用时间估计密钥存储中被拒绝的关键条目数量。这个侧信道并不会降低口令处理强度,低于只有一个口令的情况。
观察者可以大致了解同步的树中包含多少目录以及根据流量量的大致估计有多少更改。
连接到ensync服务器
例如,攻击者获取了运行ensync服务器
的ssh会话。
获取密钥存储(这是明文)是微不足道的。
可以轻松获取加密的物理根目录,这可以用来大致估计根目录中的项目数量。
也可以尝试探测特定的目录ID或对象ID并获取其内容,尽管找到这些ID可能并不容易。
模仿Ensync服务器
例如,中间人攻击一个不安全的ssh设置。
包括上述所有场景。
攻击者将了解各种目录ID、对象ID以及一些目录的“秘密版本”ID,这些ID保护对那些目录的写访问。
假设攻击者可以代理到真实的Ensync服务器,它可以根据客户端请求它们的顺序,获取有关目录ID层次结构的一些信息。
访问Ensync服务器数据的转储
攻击者了解所有目录ID、版本(秘密和其它)以及对象ID。这也包括明文的密钥存储。可能可以确定某些目录更新的频率。
结合对Ensync服务器会话的访问,这允许用任意数据覆盖任意目录(尽管是垃圾数据,因为攻击者无法正确加密它,而无需同时能够解密数据)。
实际数据在这里仍然是加密的,攻击者需要确定内部密钥才能做任何事情。
对Ensync服务器数据的shell访问
所有上述内容。攻击者当然可以随意更改存储在服务器上的数据。
然而,攻击者无法以客户端无法察觉的方式操纵服务器数据。这包括甚至对数据进行快照并在以后恢复(希望导致客户端恢复自己的数据作为回应);客户端将检测到这种恢复并拒绝继续。
客户端的妥协(包括其Ensync密钥)
攻击者可以阅读和解密客户端密钥被授予访问权限的所有内容,并写入Ensync服务器允许的内容。攻击者可以确定特定文件块的HMAC值,并探测服务器以查看该内容是否已存储(即使是在攻击者没有其他访问权限的区域)。
攻击者没有读取密钥的目录仍然安全,以及它们引用的对象也是如此。
从泄露中恢复
如果密码短语泄露了,但没有服务器访问权限,只需使用 ensync key change
替换密码短语即可。这里重要的是要确保任何知道密码的攻击者从未在密码短语在密钥存储中表示的时间看到过Ensync密钥存储。
如果Ensync密钥存储泄露,你有一个更大但不是立即的问题,因为更改密码短语不会更改底层内部密钥。也就是说,如果攻击者可以在他们自己的时间使用密钥存储来导出内部密钥,并且完成后,该内部密钥仍然有效。
假设使用了合理的密码短语,从密钥存储中导出内部密钥应该是困难的。您仍然应该更改密码短语,以便如果它们以后也泄露了,它们不能与泄露的密钥存储一起使用。然而,如果担心密钥存储被单独破坏的可能性,您可能还想更换内部密钥。没有快速的方法来做这件事;本质上,这是一个多步骤的过程。
-
从服务器获取所有数据并将其存储在安全的地方。可以使用
ensync get
来完成此操作。 -
销毁服务器上的数据。
-
重新设置服务器,重新创建密钥等。
-
将数据放入新的服务器存储。可以使用
ensync put
来完成此操作。 -
从每个客户端的
internal.ensync
目录中删除server-state.sqlite
文件。如果不这样做,客户端将检测到上述步骤为可能的恢复攻击,并且不会继续。
许可证
依赖关系
~14-26MB
~384K SLoC