Wesh协议是一个点对点的安全通信协议,最初被称为Berty协议。该协议在属于同一账户的设备之间提供安全通信,在一对一对话中提供联系人之间的通信,以及在多成员群组中提供多个用户之间的通信。Wesh协议采用分布式和异步的方式实现这些功能,无论是否有互联网访问,都可以使用IPFS和BLE等直接传输方式进行通信。
Wesh协议的核心特点包括:
IPFS(InterPlanetary File System)是一个用于存储和共享数据的分布式文件系统的点对点网络。Wesh协议使用IPFS进行即时消息传输,这提供了两个主要优势:
然而,使用IPFS也带来了一些技术限制:
汇合点是一个在点对点网络上的易变地址,两个设备可以在那里相遇。对等方可以在给定的汇合点上注册它们的对等方ID,并/或获取已注册对等方的列表。通过这种方式,需要连接在一起在对话中交换消息的对等方,可以找到彼此。
在Wesh协议中,一个会晤点的地址由两个值生成:
基于时间生成令牌的过程遵循RFC 6238的核心原则。以下是生成会晤点的代码示例:
func rendezvousPoint(id, seed []byte, date time.Time) []byte {
buf := make([]byte, 32)
mac := hmac.New(sha256.New, seed)
binary.BigEndian.PutUint64(buf, uint64(date.Unix()))
mac.Write(buf)
sum := mac.Sum(nil)
rendezvousPoint := sha256.Sum256(append(id, sum...))
return rendezvousPoint[:]
}
Wesh协议中有两种类型的汇合点:
该协议依赖于三种不同的会面点系统:
当没有互联网连接时,在一定的物理距离限制下,仍然可以通过直接传输进行通信。这些传输直接与IPFS集成,更具体地说是与其网络层:libp2p集成。
这些直接传输基于:
通过Android Nearby和Multipeer Connectivity,消息可以通过Wi-Fi直连进行交换,这比通过BLE要快得多,也更可靠。无需访问互联网也能完全使用Wesh协议:创建账户,添加联系人,加入对话并发送消息,只要蓝牙范围内有Wesh用户即可。
由于可以通过直接传输进行在线和离线通信,因此需要一种方法来保持所有消息之间的连贯性和顺序,尤其是在有多参与者参与的对话中。这个问题的解决方案是冲突复制数据类型(CRDT),这是一种允许对数据进行一致排序的数据结构,用于分布式系统上的消息。Wesh依赖于OrbitDB,该协议实现了CRDT。
CRDT提供乐观复制和强最终一致性,这确保了一旦同步,每个节点都将拥有相同版本的消息列表。每条消息都链接到其父消息,即当前时刻连接在一起的节点中某一方发送的最后一条消息。当在线版本和离线版本的对话同步时会出现问题:某些消息链接到相同的父消息,链表变成一个有向无环图。
OrbitDB通过使用兰伯特时钟实现这一点:每条消息将包含一个兰伯特时钟,合并后的列表将根据其值排序。兰伯特时钟是一个包含两个字段的结构体:一个身份公钥和一个计数器,该计数器为关联用户/身份发布的每条消息递增。
type lamportClock struct {
time int
id crypto.PublicKey
}
比较函数非常简单,它首先会检查计数器值之间的距离,如果没有,则会检查身份公钥之间的字典序距离:
func compareClock(a, b lamportClock) int {
dist := a.time - b.time
if dist == 0 {
dist = comparePubKey(a.id, b.id) // Returns lexicographic distance
}
return dist
}
为了使用Wesh协议,用户必须创建一个账户。账户创建不需要任何个人信息。在整个Wesh协议中,所有密钥对都将使用X25519进行加密和Ed25519进行签名。
创建账户步骤:
由于没有中央目录,创建账户和发送/接收联系人请求不需要访问互联网。如果两个用户离线创建账户,然后通过直接传输连接,他们将交换他们的公共会晤点(用于联系人请求),因此将能够互加为联系人。
在Wesh协议中,用户可以在同一个账号下使用多个设备,这意味着这些设备需要相互链接,以便同步账号的联系人列表、群组列表、设置等。要在设备A上向现有账号添加设备B,将遵循以下链接步骤:
设备A可以向设备B发送三种不同类型的挑战:
如果账户A想要与账户B开始一对一对话,它必须先添加B为联系人。A需要向B发送一个联系人请求,B必须接受该请求后对话才能开始。
当账户A(请求者)想要将账户B(响应者)添加到其联系人列表时,它需要知道响应者的公共会面点。这个会面点是由RDV种子和账户ID派生出来的。因此,响应者首先需要将他的RDV种子和账户ID分享给请求者,以便后者可以计算RDV点。这些信息可以通过不同的方式发送:通过消息发送的URL、响应者设备上显示的二维码并由请求者的智能手机扫描,等等……
响应者可以随时更新他们的RDV种子。如果它这样做,请求者将无法再发送联系人请求,除非响应者共享他们新的RDV种子。响应者还可以通过从其公共汇合点注销设备来完全禁用传入的联系人请求。
以下是请求者向响应者发送联系人请求时发生的握手过程:
该协议强烈基于群组的概念。群组是一个逻辑结构,成员及其设备连接到其中交换消息和元数据。元数据种类繁多,其中一些用于通知群组有新成员或新成员的设备加入了群组,另一些则用于成员之间交换加密密钥等。消息和元数据通过OrbitDB提供的两个不可变日志进行交换。
一个组分为两个日志:消息日志和元数据日志。
在Wesh协议中,有三种不同的群组类型:账户群组、联系人群组和多成员群组。群组成员是群组中的Wesh用户(一个账户)。群组对于Wesh协议中的通信至关重要,它们拥有自己的密钥和秘密信息,并在所有群组成员之间共享:
账户组是由所有连接到同一账户的设备组成的群组。同一账户下的设备需要在一个私密群组中才能相互通信并共享账户信息,例如发送和接收的消息、加入的群组、添加的联系人的信息等……账户组仅由一个群组成员组成,即拥有所有设备的账户。每次有新设备连接到账户时,它都会加入账户组。账户组的密钥和密钥秘密在账户创建时随机生成,与多成员群组的方式相同。
一个联系组是由恰好两名互为联系人的组成员组成的群组。当一名账户将另一名账户添加为联系人时,联系组即被创建。群组密钥、群组ID和附件密钥由发送联系人请求的一方使用X25519密钥协商协议生成。
一个多成员群组是由若干个群组成员组成的,这些成员可能是彼此的联系人,也可能不是。多成员群组的一个特点是,用户在群组中不会使用他们的账户ID,而是会使用一个特定于该群组的成员ID(由群组ID和一些秘密账户派生而来)。同样地,他们的设备会使用一个成员设备ID(随机生成)。因此,知道彼此账户ID(即联系人)的用户在多成员群组中无法互相识别,除非他们愿意这样做(参见别名身份)。
多成员群组的密钥和秘密在群组创建者创建群组时随机生成。一旦秘密生成,群组创建者会在元数据日志上发布一个初始化成员条目。如上图所示,初始化成员条目由一个用群组密钥封存的密钥盒组成,其中包含以下元素:
在将此条目发布到元数据日志后,群组创建者丢弃群组ID私钥。群组中的所有成员具有相同的状态,除了可以用此初始化成员条目识别的群组创建者。但它并没有赋予他们比其他成员更多的权利或能力。
在多成员组中,用户不会在组中使用他们的账户ID,而是会使用一个特定于该组的成员ID(由组ID和某个账户的秘密派生而来)。同样地,他们的设备将使用一个成员设备ID(随机生成)。因此,知道某人账户ID的用户将无法在多成员组中识别他们。但是,如果他们希望被联系人识别,他们可以向组共享一个别名条目,该条目由别名解析器和别名证明组成:
别名密钥对在账户创建时生成,一旦联系人请求被接受,别名公钥就会与联系人共享。通过别名条目,群组中知道某人别名公钥的每个人都能识别他们的成员ID。当用户加入一个新的多成员群组时,它会为每个联系人计算别名解析器,以便每当有其他成员披露别名条目时,匹配过程都能即时完成。
在Wesh协议中,所有通信均使用对称密钥旋转机制进行端到端加密。每次用户想要向某人发送消息时,都会使用HKDF从其链密钥中导出消息密钥。HKDF每次导出后也会更新链密钥。消息密钥随后用于加密消息,且不会重复用于加密其他消息。
每个群组成员的设备拥有不同的链密钥。群组ID包含在HKDF的参数中,以使导出的密钥具有上下文相关性。对话开始时,成员们与其他参与者共享其设备的链密钥。为了解密其他参与者发送的消息,他们必须遵循相同的过程,并从链密钥中导出消息密钥。发送方每收到一条消息的HKDF密钥。
func deriveNextKeys(currChainKey [32]byte, salt [64]byte, groupID []byte)
(nextChainKey, nextMsgKey [32]byte) {
// Salt length must be equal to hash length (64 bytes for sha256)
hash := sha256.New
// Generate Pseudo Random Key using currChainKey as IKM and salt
prk := hkdf.Extract(hash, currChainKey[:], salt[:])
// Expand using extracted prk and groupID as info (kind of namespace)
kdf := hkdf.Expand(hash, prk, groupID)
// Generate next chain and message keys
io.ReadFull(kdf, nextChainKey[:])
io.ReadFull(kdf, nextMsgKey[:])
return nextChainKey, nextMsgKey
}
要与其他设备(或用户)通信,设备(或用户)必须加入一个群组。只有拥有邀请函,设备(或用户)才能加入群组。
一个邀请由群组ID、群组密钥、群组密钥签名和附件密钥组成。因此,邀请可以由群组的任何成员创建。通过邀请,Wesh用户可以计算汇合点,该组的标识符,它源自组ID。
一旦RDV点计算完成,用户便能够下载群组的元数据日志,并使用群组密钥解密其部分条目。他将会获得所有群组成员的列表,这对于与他们会话并能够解密他们的消息至关重要。
一旦用户收到加入多成员群的邀请并下载了元数据日志,他们必须宣布自己的加入。为此,他们需要在元数据日志上为每个设备发布一个成员条目。一个成员条目由一个用群组密钥密封的秘密盒子组成,其中包含以下元素:
现在新成员宣布到达后,需要与其他成员交换他们的链密钥。为此,对于他们的每台设备,他们会在每个已在群组中的成员的元数据日志上发布一个秘密条目。一个秘密入口由一个用群组密钥封印的秘密盒子组成,其中包含以下元素:
这个操作是双向的,一旦一个成员在元数据日志上获取了一个秘密条目,他们也会发布一个针对新成员的秘密条目。最终,每个人都拥有新成员的链密钥,新成员也拥有群组中每个成员的当前链密钥。
现在群组中的每个成员都拥有新成员的链密钥,新成员也拥有群组中每个成员的链密钥,每个人都能够发送和接收消息。为此,他们必须首先按照对称棘轮协议从他们的链密钥中导出消息密钥。
想要向群组发送消息的成员必须在消息日志上发布一个消息条目。一个秘密条目由一个用群组秘密封印的秘密盒子组成,其中包含以下元素:
一旦成员从消息日志中获取了消息条目,他们必须使用发送者的链密钥解密它。首先,他们需要旋转发送者的链密钥,直到达到消息计数器。然后,他们需要使用消息密钥来解密消息。一旦消息密钥被使用,它就会被丢弃,因为它只能解密一条消息。
由于Wesh协议中没有中央服务器,消息和文件只存储在用户设备上。因此,如果某个设备拥有某些信息并且处于离线状态,其他设备将无法获取这些信息。例如,如果用户使用设备A添加了一个联系人,然后将设备A离线并使用设备B,设备B将不会知道这个新联系人,也无法与其通信。
为了缓解这个问题并提供高可用性,用户可以使用以下配置之一设置专用设备:机器人账户、链接设备、复制设备和复制服务器。
机器人账户是在专用设备(例如服务器)上创建的账户,并作为联系人添加到用户账户中。用户必须手动将机器人联系人添加到其所有多成员群组中。由于机器人账户与用户账户没有任何区别,这个账户可以执行用户账户可以执行的所有操作,包括发送和读取消息。机器人账户仅为多成员群组提供高可用性,因为它不能被添加到联系人群组中。
链接设备可以专用于复制,但这只是一个使用上的差异,没有任何东西将这个设备与链接到账户的其他设备区分开来,这意味着它们可以读取和发送消息,以及加入群组或将新设备链接到账户。链接设备为账户所属的每个群组提供高可用性。
复制设备是链接到账户的专用设备,但不会获取所有账户秘密,并且无法发送联系人请求或链接新设备。它会自动添加到账户所属的所有群组中,但它不会获得任何群组秘密,除了群组ID公钥、群组签名和附件密钥。因此,它无法读取消息,也无法发送消息。它所能做的就是存储消息并使用群组ID公钥验证其真实性,以避免存储来自群组外部的垃圾邮件。它还需要附件密钥来解密附件cID并存储附件。
复制服务器基本上是一个复制设备,它不链接到用户账户,而是由第三方拥有并提供。任何人都可以将现有的复制服务器添加到群组(通过API)以提供高可用性。与复制设备一样,复制服务器在被添加到群组时只获得群组ID公钥、群组签名和附件密钥,因此无法解密消息。
Wesh协议中使用的所有密钥对都是X25519用于加密和Ed25519用于签名,主要有两个原因:
Wesh协议中使用的大多数加密库都是包含在标准Go库中的包:
Wesh协议中使用的唯一非标准包是以下两个,尽管它们是由专家编写的并经过社区的广泛审查:
当Wesh用户通过直接传输连接到另一个Wesh用户时,他们首先要做的是发送他们正在监听的所有RDV点列表:他们的公共RDV点(用于联系人请求)以及他们所属的所有群组的RDV点。如果第二个用户已经在其中一些RDV点上监听,他们将能够使用直接传输在这些群组中进行通信。
例如,如果Alice和Bob是联系人,他们都将监听他们联系人群组的RDV点,因此当Alice将她所有的RDV点发送给Bob时,他将看到他们有一个共同的RDV点,他将能够在他们的联系人群组中向她发送消息。这对于多成员群组也是有效的。
因此,直接传输不像IPFS那样使用DHT,这是一种同步通信协议,因为两个设备需要相互连接才能交换消息。除此之外,Wesh协议的工作方式完全相同。