在文件传输这个已经被“做烂了”的领域里,真正难做的从来不是“让用户点一下就能上传”,而是如何在不把服务器当作可信明文持有者的前提下,仍然把体验做得足够顺手,尤其是当接收方既可能是人,也可能是运行在远程主机、容器或自动化环境里的智能体时。

Xdrop 的定位很明确:它不是一个泛泛而谈的“网盘替代品”,也不是一个只服务浏览器用户的加密分享页,而是一套面向人类与智能体的开源端到端加密文件传输系统。它的核心承诺也很克制但很重要:明文文件名、明文文件内容以及解密密钥都不留在服务器上

本文系统分析它的设计重点与工程取舍。

一、Xdrop 真正解决的问题:可信交接,而不是普通上传

很多文件分享工具的默认假设是:服务器既负责接收文件,也负责理解文件。它知道文件名,知道目录结构,往往也知道文件内容,剩下的差别只是“有没有登录”“有没有过期时间”“有没有分享链接”。

Xdrop 走的是另一条路线。它把整个系统拆成三个边界:

1.1 发送端负责理解明文,服务端只负责协调生命周期

在 Xdrop 里,真正理解文件的人只有发送端浏览器或本地 Agent 运行时。文件的名称、路径、MIME 类型、修改时间、分块信息,会先进入浏览器内的加密流程,然后再以密文或不透明标识的形式进入后端。

服务端负责的事情被刻意收缩成几类:

  • 创建传输记录并返回管理令牌与上传限制。
  • 记录文件和分块的元数据状态。
  • 为对象存储签发带时效的上传、下载 URL。
  • 执行过期清理、删除、限流和基础访问控制。

这意味着它是一个协调面,而不是明文数据面。

1.2 它的受众不是只有浏览器用户,还包括 Agent 工作流

Xdrop 从产品定义上就强调 “for humans and agents”,这不是营销包装,而是协议设计上的真实约束。浏览器分享和 Agent 终端上传并不是两套系统,而是共用同一套传输格式、同一套分享链接格式以及同一套解密模型。

这点很关键。很多产品会把“网页上传”和“自动化交接”分成两个平行能力,最终导致:

  • 浏览器端有完整 UI,但自动化只能退回到明文上传。
  • 命令行能用,但和网页端的分享链接格式不兼容。
  • 加密模型在不同入口之间不一致,审计和理解成本很高。

Xdrop 选择了统一协议,因此同一个 /t/:transferId#k=... 链接既可以被人类在浏览器中打开,也可以被 Agent 拿来做本地解密。

1.3 它不是“零元数据”,而是“服务器不持有关键明文”

这一点值得单独强调。Xdrop 并没有夸张地声称服务器“什么都不知道”。在这套架构里,服务端依然会看到一部分操作性元数据,例如:

  • 传输创建与过期时间。
  • 文件数量、分块数量和密文大小。
  • 限流所需的客户端标识。
  • 分块是否上传完成。

它刻意不保留的,是最敏感也最能定义内容语义的部分:文件名、路径、文件内容以及解密密钥。这种表述更准确,也更值得信任。

二、核心密码学模型:把分享链接变成能力令牌

Xdrop 的密码学设计并不花哨,但非常工程化。它没有试图自创算法,而是用浏览器原生 WebCrypto 把几个成熟组件拼成了一套清晰的模型。

2.1 双密钥结构:传输根密钥与链接密钥分离

发送端创建传输时,会生成两个 32 字节随机秘密:

  • rootKey:传输根密钥,真正用于派生清单与文件的加密密钥。
  • linkKey:分享链接密钥,用来包裹 rootKey,并作为 #k=... 放进最终分享链接的 fragment。

最终链接形态类似:

/t/<transferId>#k=<base64url-link-key>

服务端保存的是被 linkKey 包裹后的 wrappedRootKey,而不是 rootKey 本身。接收端打开链接后,浏览器从 fragment 里取出 linkKey,本地解包得到 rootKey,随后再去解密清单和文件。

这套结构有两个直接好处:

  • 传输 ID 可以公开出现在路径里,便于服务端路由与状态查询。
  • 真正决定解密能力的密钥停留在 fragment 中,不进入常规 HTTP 请求。

这背后的关键点是:链接 fragment 留在浏览器侧,不随常规 HTTP 请求发送

2.2 HKDF 做密钥分域,AES-256-GCM 负责机密性与完整性

Xdrop 采用 HKDF-SHA-256 从源密钥派生用途隔离的 AES-256-GCM 密钥:

  • linkKey 通过 info = "wrap-root" 派生出根密钥包裹密钥。
  • rootKey 通过 info = "manifest" 派生出清单加密密钥。
  • rootKey 通过 info = "file:<fileId>" 为每个文件派生独立文件密钥。

这种“同源秘密,多用途派生”的方式比直接复用单把 AES 密钥更稳妥,原因在于它天然隔离了不同加密上下文。即使未来某个层次需要升级版本或替换格式,也不容易互相污染。

2.3 分块加密不是简单切片,而是绑定上下文的认证加密

Xdrop 对文件分块使用 AES-GCM,但它没有只做“加密文件块”这么简单。在 encryptChunk() 里,它把下面这些信息拼成 authenticated data:

  • transferId
  • fileId
  • chunkIndex
  • plaintextChunkSize
  • 协议版本号

这样做的意义是:即便攻击者拿到某个密文块,也不能把它跨文件、跨传输或跨位置重放,否则认证会失败。

另外,分块 IV 也不是完全随机生成,而是由:

  • 每个文件独有的 8 字节 noncePrefix
  • 4 字节的 chunkIndex

共同组成 12 字节 GCM IV。换句话说,Xdrop 把“文件级随机性”和“块级顺序性”组合在了一起,既降低了状态复杂度,也让接收端可以确定性地重建每个块的解密参数。

2.4 图片元数据剥离是附加隐私层,不是主加密逻辑

上传前,前端还提供一个默认开启的可选能力:对 image/jpegimage/pngimage/webp 这类图片先做一次重绘导出,从而去掉可移除的 EXIF 元数据。

这一步发生在加密之前,解决的是另一个常被忽略的问题:即使内容被加密,如果你在发送前就把带地理位置、拍摄设备等信息的图片原样装进去,接收方拿到后依然会得到这些元数据。Xdrop 通过“先剥离,再加密”把这层风险提前处理掉。

三、上传管线:浏览器如何把大文件切块、加密并支持恢复

Xdrop 最值得看的地方,不只是“它做了端到端加密”,而是它把一条完整的上传链路做成了可恢复、可中断、可继续的浏览器运行时。

3.1 先准备源文件,再估算密文体积,而不是边传边赌

在上传准备阶段,前端会为每个待上传文件完成几件事:

  • 生成不透明的 fileId
  • 为该文件生成 noncePrefix
  • 根据分块大小计算 totalChunks
  • 预估每个密文块的大小为 plaintext + 16 字节,用于 GCM tag。
  • 汇总整次传输的总密文大小。

这让 Xdrop 能在真正上传前就做限制检查。当前默认限制来自共享常量与后端配置:

  • 分块大小:8 MiB
  • 最大文件数:100
  • 最大传输密文体积:256 MiB

这些限制看上去保守,但它们与“浏览器内加密 + 本地暂存 + 恢复能力”的目标是匹配的。Xdrop 当前并没有把自己包装成 TB 级归档通道,而是优先保证这条加密交接链路在真实浏览器环境里稳定、可理解、可恢复。

3.2 OPFS 优先,IndexedDB 兜底,保证刷新后还能继续

如果只做“边选文件边上传”,一旦浏览器刷新,用户就要重新选文件,断点续传就无从谈起。Xdrop 的做法是把源文件本地持久化这件事纳入正式运行时的一部分。

它优先使用 OPFS(Origin Private File System)保存源文件;如果浏览器不支持 OPFS,或者需要兜底,则在阈值范围内退回到 IndexedDB Blob 存储。

这带来一个非常实际的结果:

  • 浏览器刷新时,进行中的上传会先被标记为 paused
  • 页面重新打开后,会读取本地 transfer 记录和 source 记录。
  • 客户端通过 /resume 接口询问哪些块已经完成。
  • 后续只补传缺失块,而不是从头开始。

这也说明恢复逻辑不是补充功能,而是整条上传模型的一部分。

3.3 Web Worker 承担重活,主线程负责 orchestration

Xdrop 把这些高开销操作都挪到了专用 Worker:

  • 包裹 / 解包根密钥。
  • 加密 / 解密 manifest。
  • 加密 / 解密文件块。

这意味着主线程不需要在 React 渲染和大块加解密之间抢时间片。对用户来说,最直接的收益就是 UI 不容易在大文件处理时卡死;对工程上来说,则是把“加密能力”从“界面状态”里抽成了独立执行单元。

3.4 上传 URL 分批预签名,避免把整个传输一次性锁死

Xdrop 没有为所有块一次性签发全部 PUT URL,而是按批次调用 /upload-urls。当前前端逻辑会根据并发数分批申请,再并发上传这一批块,上传成功后通过 /chunks/complete 回写块大小与 SHA-256 校验值。

这个设计有几个优点:

  • presigned URL 生命周期更短,暴露面更小。
  • 失败重试只影响一批块,而不是整次传输。
  • 服务端可以在每轮申请时重新校验状态,避免失控。

全部块确认完成后,客户端再:

  1. 构建 manifest。
  2. 使用 rootKey 加密 manifest。
  3. 使用 linkKey 包裹 rootKey
  4. 上传 manifest。
  5. 调用 /finalize 把传输切换到 ready

也就是说,“可下载”这件事直到最后一步才成立,之前公共接口看到的状态始终是不完整的。

四、服务端职责被刻意压缩:只保存状态,不碰明文

Xdrop 后端是 Go 写的,但更值得注意的不是语言,而是职责边界。它非常像一个上传与下载协调器,而不是传统意义上的文件服务器。

4.1 API 设计围绕传输生命周期展开

服务端暴露的接口非常克制,基本都围绕 transfer 生命周期:

  • 创建传输
  • 注册文件
  • 申请上传 URL
  • 确认块完成
  • 上传 manifest
  • finalize
  • resume / 管理 / 删除
  • 公共读取传输状态
  • 公共申请下载 URL

这组 API 没有任何“上传明文文件”“下载明文文件”的入口。服务端只知道有一个 transfer,有若干 opaque file id,有若干 chunk index,以及对应对象存储中的 object key。

4.2 PostgreSQL 保存的是状态图,不是文件语义

数据库里有三张核心表:

  • transfers
  • transfer_files
  • transfer_chunks

其中重要字段包括:

  • wrapped_root_key
  • manifest_object_key
  • manage_token_hash
  • opaque_file_id
  • chunk_index
  • checksum_sha256

注意两个细节:

  • 管理令牌不是明文保存,而是以 SHA-256 摘要形式落库。
  • 文件 ID 不是文件名,而是客户端生成的不透明标识。

这正是 Xdrop 的核心思路:数据库要能表达“一个传输当前进展到了哪一步”,但不需要知道“这个文件叫合同.pdf”。

4.3 管理令牌是一次性发放的发送端控制权

创建 transfer 时,API 会返回一个 manageToken。这个令牌只在创建时明文交给发送端一次,服务端内部只保存它的哈希,后续使用常量时间比较进行校验。

这让发送端拥有一组明确的控制动作:

  • 恢复上传。
  • 延长过期时间。
  • 删除传输。

如果发送端启用了“privacy mode”,上传完成后本地会清空这些控制信息,只保留分享能力,不再保留 extend / delete 的本地控制权。这是一个很有意思的取舍:它不是增强密码学强度,而是减少本地残留敏感状态

4.4 对象存储才是密文落点,服务端只做 presign

对象存储层的设计也很明确:

  • API 用私有 S3 端点与对象存储交互。
  • 如有需要,可以为外部下载使用单独的公共 presign 端点。
  • manifest 与 chunk 都只是对象。
  • 清理时按 transfers/<id>/ 前缀整批删除。

对象 key 结构也很规整:

transfers/<transferId>/manifest.bin
transfers/<transferId>/files/<fileId>/chunks/<chunkIndex>.bin

这说明后端关注的是对象布局与生命周期,而不是文件内容解释。对 MinIO、S3 或其他兼容存储来说,Xdrop 只是把它们当作密文块存储库。

4.5 安全头、限流和清理任务构成了基础防线

Xdrop 没有把安全理解成“有端到端加密就够了”。后端还做了很多基础但必要的防护:

  • 通过 Redis 实现固定窗口限流。
  • 通过 ALLOWED_ORIGINS 控制允许的来源。
  • 默认注入 X-Content-Type-OptionsX-Frame-OptionsCross-Origin-Opener-Policy 等响应头。
  • 使用后台清理任务定期删除过期或已删除的对象前缀,并把数据库状态标记为已清理。

这些措施并不直接参与加密,但决定了整个系统能否作为一个公开服务稳定运行。

五、下载与 Agent 集成:一套协议同时服务人和自动化流程

如果说上传侧体现的是“浏览器内加密的工程实现”,那么下载侧体现的就是 Xdrop 对“同协议跨入口”的坚持。

5.1 接收端先拿状态,再拿 manifest,最后本地解密

接收方访问 /t/:transferId#k=... 时,浏览器会先调用公共接口获取 PublicTransferDescriptor。只有在状态为 ready 时,服务端才返回:

  • wrappedRootKey
  • manifestUrl
  • 受限下载配置

接收端随后:

  1. 用 fragment 中的 linkKey 解开 wrappedRootKey
  2. 拉取加密 manifest。
  3. 在本地解密出文件列表。
  4. 再根据 manifest 中的 fileId 和块信息请求下载 URL。

这个流程意味着公共 API 永远只暴露可以公开暴露的密文描述符,不会把文件列表、目录路径或解密材料作为普通 JSON 直接吐出来。

5.2 ZIP 打包也在本地完成,目录结构由 manifest 决定

对于文件夹下载,Xdrop 并没有让服务端动态打包 ZIP,而是在浏览器侧流式解密每个文件,再调用 zip.js 在本地重新打包。

这件事的价值不只是“省一层后端逻辑”,而是保持一个原则:服务端只交付密文对象,不参与明文重组。目录路径保存在 manifest 中,ZIP 内部路径由接收端根据解密后的相对路径重建。

换句话说,服务器不需要知道“这些块属于一个叫 docs/readme.txt 的文件”,它只需要负责让浏览器拿到对应的块。

5.3 Agent 工作流复用了同一分享格式

Xdrop 的另一个亮点,是它没有把 Agent 支持做成一个“单独的 API 产品线”。配套的 Xdrop skill 和命令行工作流,围绕的都是同一件事:

  • 上传本地文件或目录。
  • 返回完整分享链接。
  • 使用包含 #k= 的链接做本地解密下载。

这意味着远程服务器、容器、CI 邻近环境里的 Agent,可以把 Xdrop 当成一个端到端加密交接通道:把产物、日志、诊断包或者构建结果安全地转交给本地用户,而不必要求服务器本身持有明文文件语义。

这正是 “for humans and agents” 在架构层面的真实含义。

六、工程取舍:为什么它适合可信临时交接,而不是万能网盘

Xdrop 的设计很完整,但它并没有试图变成一个无所不能的大平台。相反,它的很多限制都是为了保持边界清晰。

6.1 当前默认限制体现了它的目标场景

默认的 8 MiB 分块、100 文件上限和 256 MiB 密文总量,说明它优先优化的是:

  • 临时分享。
  • 人与人、Agent 与人之间的交接。
  • 浏览器可承受的恢复与本地暂存。

如果你的需求是 TB 级长期归档、分层权限、团队空间、细粒度审计,那不是 Xdrop 当前最擅长的问题域。

6.2 “无账号”与“本地历史”是简化,也是边界

Xdrop 明确选择了:

  • 没有账号体系。
  • 没有跨设备同步历史。
  • 发送端管理记录只保存在当前浏览器。

这带来的好处是系统更轻、信任面更小;代价则是如果你丢了本地状态,或者启用了 privacy mode 后又忘记保留分享信息,可管理性就会下降。这不是缺陷,而是它对“轻量、临时、最小服务端状态”的坚持。

6.3 部署模型偏向自托管且边界明确

默认 Docker 部署中,最终容器同时承载:

  • nginx 静态前端
  • Go API

再加上:

  • PostgreSQL
  • Redis
  • MinIO

在部署上,更稳妥的做法是:

  • 公网入口只暴露反向代理。
  • xdrop 容器和 MinIO 最好只绑定回环地址。
  • 若更换正式域名,前端构建时的 VITE_SITE_URL 需要同步考虑。

这套部署方式没有追求“极致云原生炫技”,但对个人开发者、小团队或自托管场景非常实用:结构简单,职责清晰,安全边界容易理解。

结语

Xdrop 最值得肯定的地方,不在于它喊出了“端到端加密”这几个字,而在于它把这套承诺贯彻到了实际工程细节里:

  • #k= fragment 把解密能力留在客户端。
  • 用 HKDF + AES-256-GCM 做用途隔离明确的加密模型。
  • 用 Web Worker、OPFS 和 IndexedDB 把浏览器上传做成可恢复运行时。
  • 用 Go API、PostgreSQL、Redis 和 S3-compatible storage 组成一个只处理生命周期的后端。
  • 用统一分享协议同时服务人类浏览器与 Agent 终端。

它不是一个“什么都能做”的文件平台,但在“可信、临时、跨人机边界的加密交接”这个问题上,Xdrop 已经展现出非常成熟的产品判断和工程实现。对今天越来越常见的远程开发、容器化工作流和 Agent 协作场景来说,这种设计比一个单纯的上传页要有价值得多。