
协同办公后端前端密码学【免费下载链接】cryptpadCollaborative office suite, end-to-end encrypted and open-source.项目地址https://gitcode.com/gh_mirrors/cr/cryptpad点击查看免费下载这篇技术指南以 CryptPad 官方架构文档docs/ARCHITECTURE.md为骨架完整剖析 XWiki Labs 如何用 Chainpad 共识算法、Netflux 传输抽象与预共享密钥加密构建多客户端协同构造单一权威文档的实时协作系统。读者将理解 diff/patch/message 三步模型、History Keeper 的存储职责、WebChannel 传输层的三条硬性要求以及内容规范化HyperJSON与端到端加密带来的工程取舍并对照本仓库源码验证每一项设计。从零开始实时协作应用的核心难题编写一个实时协作应用难点在于一个物理学事实被空间距离隔开的两个事件不可能瞬时相互作用。因此客户端必须事先约定一套协议用以解决冲突使得双方在收到彼此的消息后能够构造出完全相同的文档。本文聚焦的场景是多个客户端协同构造一份单一权威文档Authoritative Document。XWiki Labs 在开发 CryptPad仓库 readme.md 描述为 Collaborative office suite, end-to-end encrypted and open-source的过程中积累了相关技术经验以下内容即围绕这些原型中采用的技术展开。共识Consensus让所有客户端得到同一份文档假设 Alice 和 Bob 同时编辑同一份文本文档。他们各自从一份双方认可的初始状态O出发Alice 对O做出修改aBob 对O做出修改b。接下来他们需要完成三步操作diff各自对比修改前后的文档确定到底改了什么patch把改动提炼成能够用最少信息把旧文档更新到新状态的形式message把 patch 序列化转成能在通信媒介上传输的格式。要让双方在以不同顺序收到彼此消息的情况下仍产出相同文档必须使用一种算法Operational Transformation操作转换OT。其核心是两个判定共同祖先冲突编辑发生时文档处于什么状态如果两条事件处于已经分叉的时间线中它们的共同祖先是什么即O[n]变换规则如果操作a和b都试图把O[n]变成O[n1]那么a该如何变换才能让它在b生效之后再作用于O[n]即充分考虑b造成的影响两条操作若能成功化解其中一条实际上变成O[n1]另一条变成O[n2]。对文档而言谁先谁后并不重要关键是两种变换都必须成立。Chainpad基于 Nakamoto 区块链的共识库XWiki Labs 的研究团队创建并持续维护着 JavaScript 库Chainpad用于简化上述过程并提供冲突化解的保证。它的算法基于 Nakamoto 区块链即比特币推广开来的链式结构其核心数据类型可被视为一种CRDT无冲突复制数据类型——Git 这类版本控制软件也采用了 CRDT 思想。Chainpad 的关键设计每个 patch 引用文档前一状态的 SHA-256 哈希。哈希的存在使客户端能够独立验证 patch 的真实性确认它能否被成功应用到历史中特定位置被判定无效的 patch 会被拒绝。patch 由 Operation 组成每个 Operation 包含三个要素从文档开头算起的偏移量offset指明改动应用的起点要删除的字符数remove要插入的字符串insert。正因为操作类型非常少变换矩阵是方形的——文档原文如此解释转换关系才易于维护。作为应用作者你需要正确使用 Chainpad你负责提供 diff、patch 逻辑和界面渲染逻辑而 Chainpad 负责提供从区块链上取回已达成共识文档的 API。一个特别有价值的特性是达成共识无需中央权威。既然只有客户端自己能决定我们同意什么那么负责投递和存储消息的服务完全可以对消息内容一无所知。这意味着只要每个客户端都持有一个预先共享的密钥pre-shared key就能在端到端加密的前提下实时协作。在 CryptPad 的现代实现中Chainpad 与 Netflux 之间的桥接层位于 www/common/sframe-chainpad-netflux-outer.jsmsgIn负责对收到的消息调用Crypto.decrypt解密后交给 ChainpadmsgOut则对发出的消息调用Crypto.encrypt加密后再发送——正是存储与传输服务对内容无感知这一设计在代码层面的直接体现第 39-71 行。该文件还展示了checkpoint机制当消息以[4开头即 Chainpad 的检查点消息时会用 NaCl 计算消息哈希并拼出cp|id|前缀的密文第 57-66 行让服务器能够索引可恢复点。数据存储DatastoreHistory Keeper 与存储适配器理论上消息可以通过gossip 协议传播从而不依赖集中式存储但 gossip 网络存在Netsplit网络分裂的风险而 Chainpad 目前没有解决该事件的机制。具体表现是会话中某个成员断线在限定时间内未对 ping 回以 pong其后续对文档的修改将被忽略。只有当所有在线成员都接收并整合了某个 patch该 patch 才能被视为权威文档的一部分。要保证会话内所有客户端拿到同一份文档最稳妥的做法是所有客户端都从单一实体按序获取消息——该实体负责把接收到的消息按特定顺序编译成历史记录并按相同顺序分发给各客户端。Chainpad 能处理乱序消息但当消息按哈希引用顺序投递时表现最佳。因此把系统架构设计为所有客户端发送到服务器、服务器再转发给其他客户端即可保证会话参与者之间 patch 链的一致性。这份承担存储职责的实体在本指南中统称为History Keeper。在 CryptPad 仓库中History Keeper 的服务端实现位于 lib/historyKeeper.js其职责与上述描述完全吻合channelMessage在收到客户端广播时按渠道存储消息第 23-28 行channelOpen负责处理加入会话的握手第 34-105 行channelClose在所有人离开时清理缓存的元数据与索引第 29-33 行。可插拔的存储适配器CryptPad 支持多种数据存储实例使用哪种存储可在配置中轻松切换见 config/config.example.js。你只需编写一个符合简单 API 的适配器即可。仓库中的实现位于 lib/storage包括basic.js、blob.js、block.js、challenge.js、file.js、invite.js、mfa.js、moderator.js、sessions.js、sso.js、tasks.js、user.js等。以 lib/storage/basic.js 为例适配器 API 的核心是一组极小的方法集合read、readDir、write、delete、deleteDir、archive、restore第 37-91 行。文件头注释还说明了设计取舍MFA 所需的挑战码、账户设置、会话令牌等数据类型都只需要 read/write/delete 三个方法虽然可以用关系型数据库的表实现但承诺使用关系型数据库是一个重大决定因此先用文件系统实现第 5-24 行。存储层还体现了一些与 History Keeper 强相关的细节见 lib/hk-util.js渠道 ID 长度约定32 字符为标准渠道会被永久存储第 38 行、33 字符为管理渠道第 39 行、34 字符为临时渠道不存储第 43 行checkpoint 索引裁剪sliceCpIndex只保留最近 100 条消息内的旧 checkpoint 外加最后两个防止客户端在 checkpoint 上分叉并丢弃分叉历史第 73-82 行受限渠道带元数据且restricted的渠道会校验会话是否在允许名单ownersallowed中否则返回ERESTRICTED错误lib/historyKeeper.js。传输TransportNetflux 与 WebChannel 抽象CryptPad 最初使用WebSocket传输消息。由于中继服务器在这种模型中不可或缺该服务器同时扮演 History Keeper 并实现数据存储。只要满足 Chainpad 的前提也可以采用其他传输模型但有三条硬性要求必须告知实时会话某客户端是否仍在会话中——这决定权威文档的判定必须保证任意客户端能在任意时刻取回链的完整历史必须保证客户端在处理其消息之前已经知道权威文档的内容。Netflux正是为满足这些要求而设计的 API由 OpenPaaS::ng 项目开发。它提供了对传输细节的抽象核心概念是WebChannel客户端向渠道发送消息API 保证消息送达渠道内所有成员且发送方会收到消息已被分发的确认。这个高层 API 可以基于 WebSocket、WebRTC 或任何能提供相同保证的新传输来实现且不限于 Web 环境。它还能抽象网络的拓扑结构从而容纳多台服务器。Netflux API 还允许会话中的任意成员充当 History Keeper。XWiki Labs 通过另一个基于 WebRTC 的 Realtime CKEditor 原型验证了这条路径存储消息的负担落在某个参与客户端身上通常是第一个加入会话者。在 CryptPad 源码中客户端侧 WebChannel 与会话管理的实际逻辑集中在 www/common/sframe-chainpad-netflux-outer.jsChainpad 与 Netflux 的 shim 层以及 www/common/sframe-chainpad-netflux-inner.js。配置层面WebSocket 服务端的端口默认websocketPort与路径/cryptpad_websocket均在 config/config.example.js 中说明示例 Nginx 配置见 docs/example.nginx.conf。界面Interface同步中的三大难题实时协作应用的界面千差万别但有几个共性问题几乎必然出现。光标校正Cursor Correction如果是普通文本编辑器维护光标位置相对简单检查选区位置可能有起点和终点判断文档改动发生在这两个点之前还是之后必要时更新选区边界即可。但如果是WYSIWYG 编辑器难度大增权威文档中字符数的变化与用户感知的字符数变化并不一一对应。应用开发者必须从新内容中推断光标应处的位置并以不打扰用户体验的方式渲染修改后的内容。开发 Realtime CKEditor 时XWiki Labs 发现许多现成的 DOM 更新库非常暴力——一旦在树中发现差异从左到右扫描就把元素整个替换掉。经过多方测试最终采用了打过补丁的DiffDOM。其 diff 算法大体正确但缺少 move 操作当段落文本被加粗时它只会判定 P 中的 textNode 应被移入 STRONG 元素、STRONG 再放入 P因此在大段文档上应用样式、同时有人在该段内编辑会引发困难。总体而言contentEditable 文档避免了文本编辑器渲染周期中的许多问题DOM 选区由起点和终点组成每个点都是元素引用 元素内偏移。只要避免无谓地重绘 DOM 元素光标就不会受牵连。规范化Canonicalization并非所有浏览器都以相同方式表示界面内容。单机使用时通常无碍但同步数据时可能陷入浏览器战争两个客户端各自强迫对方接受自己的表示有些浏览器会接受某些自身不会生成的表示有些则会立刻纠正回来从而陷入仅受网络往返速度限制的死循环。例如文本编辑器用 textarea 时有的浏览器用\n表示换行有的用\r\n。所幸所有浏览器似乎都容忍\r被剥离因此可以通过规范化Canonical Form修复。这是实时应用开发中最不可预测的部分最简单的应对是限制支持的客户端类型只支持支持 WebSocket 的浏览器因为必须支持其传输系统。对于 contentEditable 实时应用CKEditorXWiki Labs 没有同步文档的 outerHTML而是采用 DOM 的中间表示HyperJSON。HyperJSON 可由任意 DOM 元素生成且是 JSON 的子集、易于序列化它完整描述 DOM 结构包括元素可能携带的所有属性。HyperJSON 模块还提供递归地对 HyperJSON 应用回调并返回结果的函数其灵感来自dom2hscript把 DOM 转成 HyperscriptHyperjson.callOn接受 Hyperscript 作为第二参数将序列化的 DOM 变回真实 DOM 元素。本地更新时客户端把内容序列化成字符串交给 Chainpad若字符串比较不相等就执行 diff 并把变更发送出去。但序列化 DOM 有个陷阱带属性的元素存在多种序列化方式。比如p styleborder: 1px solid white; color:red;pewpew/p转成 HyperJSON 后style 属性映射可能序列化为{border:1px solid white,color:red}也可能反过来——因为映射的键顺序没有定义行为。解法是把属性按确定性顺序排序项目使用了JSON.sortify定义序列化时的规范形式由于 HyperJSON 结构中除映射外的一切已经是确定性的这便简单杜绝了一类浏览器打架。过滤本地元素Filtering Local Elements某些元素或属性只在特定环境中有意义。一个典型案例是Bogus BR某些浏览器为了让空 P 标签在 contentEditable 中可选中而插入它Firefox 中表现为BR type_moz/其他浏览器会丢弃该属性导致糟糕的合并或 DOM 修改循环。另一个例子是 CKEditor 的MagicLine 插件它会向 DOM 插入 SPAN 元素以暴露可点击的界面区域——这是应用功能修改内容本身造成的污染。对策是在递归遍历 DOM 时提供忽略元素、修改 HyperJSON 序列化输出的能力。XWiki Labs 采用在运行时以可选函数注入过滤能力的方式形成非常灵活的体系便于扩展以适配不同用户群的需求当时的目标是研究新技术并改进公司核心产品 XWiki。加密Encryption端到端加密及其取舍在本文讨论的所有组件中加密是构建实时协作编辑器时唯一可选的技巧。因为数据存储和传输系统不需要了解文档内容Chainpad 让加密成为可自由利用的选项。但这并非没有代价——把信息对服务器隐藏会带来若干挑战。加密方案CryptPad 采用对称加密使用所有参与者共知的单一预共享密钥。预共享密钥属于现有密码学工具中最弱的一类但几乎没有其他方案能扩展到任意数量的用户。为了让加密的好处不增加使用难度项目利用了一个 Web 惯例协作通常意味着分享 URL。因此用URL 的 hash 来承载预共享密钥——URL 中#之后的一切都不会发送给服务器于是 Web 应用可以在完全不知道用户消息内容的情况下存储消息。需要明确的安全边界任何能读到该 URL 的人都能访问同一份文档所以文档内容的安全性等同于 URL 分享方式的安全性。加密如何限制应用加密本身不是难点真正的约束在于一旦涉及加密用户就会期望内容被当作敏感信息对待。这排除了许多依赖 History Keeper 介入会话的功能。局限性一垃圾 patch 无法被过滤。客户端可能发送垃圾 patch其他客户端应该拒绝它们但 History Keeper 不知道消息内容无法代为判断于是这些 patch 被不可撤销地加入文档链。新加入会话的客户端必须先下载完整历史才能参与恶意客户端因此可以无代价地灌满渠道垃圾消息。局限性二长同步时间。即使没有恶意行为者只要 patch 数量多同步就会变慢——这对同步困难的设备如移动端和频繁断线的客户端尤其麻烦。文档还指出目前没有允许客户端跳过此前编辑过的文档的重新同步的实现尽管理论上可以借助LocalStorage API达成。作为对照XWiki Labs 也构建过不适用加密的实时编辑器场景主要在 XWiki 平台内既然平台目的是分享信息禁止服务器直接读取文档反而适得其反。其方案是实时会话中第一个用户从服务器取回文档的完整最新状态后续用户必须同步消息链才能参与但仍能以 XWiki 原生方式保存当最后一个用户离开实时会话时历史被删除下次编辑实时功能时从头再来。会话仍可能遇到超大链的问题但实践中会话很少长到足以构成明显困扰。结论从 docs/ARCHITECTURE.md 全文可以看到构建实时协作应用的关键决策链是共识层用 Chainpad基于 Nakamoto 区块链的 CRDT解决多客户端如何从不同顺序的消息中收敛到同一权威文档哈希引用保证了 patch 可验证、可拒绝存储层用 History Keeper 统一收编消息历史并按序分发通过极简的适配器 APIlib/storage支持可插拔的多种存储传输层用 Netflux 的 WebChannel 抽象满足存活感知、历史可回溯、权威文档先行三条要求WebSocket 与 WebRTC 皆可实现界面层用光标校正、内容规范化HyperJSON JSON.sortify与本地元素过滤驯服浏览器间的表示差异加密层利用存储与传输对内容无感知的特性用 URL hash 承载的预共享密钥实现端到端加密同时接受垃圾 patch 与长同步等代价。对想要基于 CryptPad 技术栈构建实时应用或研究其架构的开发者仓库中的 lib/historyKeeper.js、lib/hk-util.js、lib/storage、www/common/sframe-chainpad-netflux-outer.js 是继续深入的最佳入口而 docs/example.nginx.conf 与 config/config.example.js 则展示了传输与存储配置的落地方式。赞分享协同办公后端前端密码学【免费下载链接】cryptpadCollaborative office suite, end-to-end encrypted and open-source.项目地址https://gitcode.com/gh_mirrors/cr/cryptpad点击查看免费下载相关推荐CryptPad 部署与安全架构指南端到端加密的开源实时协作套件CryptPad 部署与安全架构指南端到端加密的开源实时协作套件 CryptPad 是一套端到端加密E2EE、开源、可自托管的实时协作办公套件覆盖文档、协同办公后端前端密码学端到端加密认证器终极指南用ente/auth彻底告别密码泄露风险端到端加密认证器终极指南用ente/auth彻底告别密码泄露风险 你是否曾经担心过自己的两步验证2FA令牌被黑客窃取是否厌倦了那些依赖第三方云服务的认证后端前端移动开发桌面应用密码学认证鉴权存储google-search-results-python性能优化终极指南帮你将搜索速度提升300%google search results python性能优化终极指南帮你将搜索速度提升300% google search results python是创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考