news 2026/9/13 17:24:08

Super Productivity SuperSync 端到端加密架构解析:AES-256-GCM 与 Argon2id 的实现与纵深防御

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Super Productivity SuperSync 端到端加密架构解析:AES-256-GCM 与 Argon2id 的实现与纵深防御

Super Productivity SuperSync 端到端加密架构解析:AES-256-GCM 与 Argon2id 的实现与纵深防御

【免费下载链接】super-productivitySuper Productivity is an advanced todo list app with integrated Timeboxing and time tracking capabilities. It also comes with integrations for Jira, GitLab, GitHub and Open Project.项目地址: https://gitcode.com/GitHub_Trending/su/super-productivity

本篇技术指南以 Super Productivity 开源仓库中 supersync-encryption-architecture.md 为骨架,系统讲解 SuperSync 同步后端端到端加密(E2EE)的完整设计:从 AES-256-GCM 加密算法、Argon2id 密钥派生的底层实现,到操作日志(Operation Log)上传/下载链路中层层递进的 fail-closed 安全校验。读完本文,你将掌握该项目的加密线格式、会话级密钥缓存机制、四个纵深防御向量、密码对话框的选择逻辑,以及混合加密/明文历史的恢复流程,并能在 src/app/op-log/sync/ 与 packages/sync-core/src/encryption/ 中直接定位对应实现与回归测试。

概述:SuperSync 的端到端加密全景

SuperSync 是 Super Productivity 自托管的同步后端(服务端位于 packages/super-sync-server/),它采用AES-256-GCM认证加密算法与Argon2id内存困难型密钥派生函数(KDF)实现端到端加密。核心原则是:

  • 操作载荷(operation payload)的加密/解密全部在客户端完成,服务端只能看到操作信封(envelope)的明文元数据;
  • 服务端存储的是原样密文,既无法读取载荷内容,也拿不到任何加密密钥;
  • 加密意图与密钥只存在于提供方的私有配置(private config)中,从不作为同步状态的一部分发给服务器。

需要先澄清一个容易混淆的点:这里的 E2EE 保护范围是操作载荷(op.payload,而actionTypeopTypeentityTypeentityIdvectorClocktimestamp等信封元数据以明文传输——这部分不完整性由客户端的一整套纵深防御校验来兜底,详见下文「安全属性与完整性边界」。

加密流程全景:上传、存储、下载三端接力

原文档用一张 ASCII 流程图完整描述了「客户端 A 上传 → SuperSync 服务器存储 → 客户端 B 下载应用」的完整数据流,这里完整保留并标注对应的源码位置:

┌─────────────────────────────────────────────────────────────────────────────┐ │ CLIENT A (Upload) │ ├─────────────────────────────────────────────────────────────────────────────┤ │ │ │ 1. User Action │ │ ┌──────────────┐ │ │ │ Add Task │ │ │ │ "Buy milk" │ │ │ └──────┬───────┘ │ │ │ │ │ ▼ │ │ 2. NgRx Action Dispatched │ │ ┌──────────────────────────────────────────────────────────────┐ │ │ │ { type: '[Task] Add Task', │ │ │ │ task: { id: 'abc123', title: 'Buy milk', ... }, │ │ │ │ meta: { isPersistent: true, entityType: 'task', ... } } │ │ │ └──────────────────────────┬───────────────────────────────────┘ │ │ │ │ │ ▼ │ │ 3. Operation Capture (operation-capture.meta-reducer.ts) │ │ ┌──────────────────────────────────────────────────────────────┐ │ │ │ MultiEntityPayload { │ │ │ │ actionPayload: { task: {...}, isAddToBottom: false, ... }, │ │ │ │ entityChanges: [{ entityType: 'task', entityId: 'abc123', │ │ │ │ changeType: 'create' }] │ │ │ │ } │ │ │ └──────────────────────────┬───────────────────────────────────┘ │ │ │ │ │ ▼ │ │ 4. Encryption (operation-encryption.service.ts) │ │ ┌─────────────────────────────────────────────────────────────┐ │ │ │ │ │ │ │ User Password: "mySecretPass123" │ │ │ │ │ │ │ │ │ ▼ │ │ │ │ ┌─────────────────┐ │ │ │ │ │ Argon2id │ Key Derivation │ │ │ │ │ + Salt │ (CPU/memory-hard) │ │ │ │ └────────┬────────┘ │ │ │ │ │ │ │ │ │ ▼ │ │ │ │ 256-bit Encryption Key │ │ │ │ │ │ │ │ │ ▼ │ │ │ │ ┌─────────────────┐ │ │ │ │ │ AES-256-GCM │ Authenticated Encryption │ │ │ │ │ + Random IV │ (confidentiality + integrity) │ │ │ │ └────────┬────────┘ │ │ │ │ │ │ │ │ │ ▼ │ │ │ │ Encrypted Payload (base64 string) │ │ │ │ "U2FsdGVkX1+abc123..." │ │ │ │ │ │ │ └─────────────────────────┬───────────────────────────────────┘ │ │ │ │ │ ▼ │ │ 5. SyncOperation Ready for Upload │ │ ┌──────────────────────────────────────────────────────────────┐ │ │ │ { id: 'op-xyz', clientId: 'client-A', │ │ │ │ actionType: '[Task] Add Task', │ │ │ │ payload: "U2FsdGVkX1+abc123...", ← Encrypted! │ │ │ │ isPayloadEncrypted: true, ← Flag set │ │ │ │ vectorClock: { 'client-A': 5 }, ... } │ │ │ └──────────────────────────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────────────────────┘ │ │ HTTPS ▼ ┌─────────────────────────────────────────────────────────────────────────────┐ │ SUPERSYNC SERVER │ ├─────────────────────────────────────────────────────────────────────────────┤ │ │ │ Server stores encrypted payload AS-IS │ │ ┌──────────────────────────────────────────────────────────────────┐ │ │ │ operations table: │ │ │ │ ┌─────────┬────────────────────────────┬───────────────────┐ │ │ │ │ │ seq │ payload │ is_encrypted │ │ │ │ │ ├─────────┼────────────────────────────┼───────────────────┤ │ │ │ │ │ 42 │ "U2FsdGVkX1+abc123..." │ true │ │ │ │ │ └─────────┴────────────────────────────┴───────────────────┘ │ │ │ │ │ │ │ │ ⚠️ Server CANNOT read payload contents │ │ │ │ ⚠️ Server has NO access to encryption key │ │ │ └──────────────────────────────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────────────────────┘ │ │ HTTPS ▼ ┌─────────────────────────────────────────────────────────────────────────────┐ │ CLIENT B (Download) │ ├─────────────────────────────────────────────────────────────────────────────┤ │ │ │ 1. Download Operations (operation-log-download.service.ts) │ │ ┌──────────────────────────────────────────────────────────────┐ │ │ │ Received: { payload: "U2FsdGVkX1+abc123...", │ │ │ │ isPayloadEncrypted: true, ... } │ │ │ └──────────────────────────┬───────────────────────────────────┘ │ │ │ │ │ ▼ │ │ 2. Decryption (operation-encryption.service.ts) │ │ ┌─────────────────────────────────────────────────────────────┐ │ │ │ │ │ │ │ User Password: "mySecretPass123" (same as Client A) │ │ │ │ │ │ │ │ │ ▼ │ │ │ │ ┌─────────────────┐ │ │ │ │ │ Argon2id │ Same key derivation │ │ │ │ │ + Salt │ → Same 256-bit key │ │ │ │ └────────┬────────┘ │ │ │ │ │ │ │ │ │ ▼ │ │ │ │ ┌─────────────────┐ │ │ │ │ │ AES-256-GCM │ Decrypt + verify integrity │ │ │ │ │ Decrypt │ │ │ │ │ └────────┬────────┘ │ │ │ │ │ │ │ │ │ ▼ │ │ │ │ Original Payload (JSON) │ │ │ │ { actionPayload: { task: {...} }, entityChanges: [...] } │ │ │ │ │ │ │ └─────────────────────────┬───────────────────────────────────┘ │ │ │ │ │ ▼ │ │ 3. Convert to Action (operation-converter.util.ts) │ │ ┌──────────────────────────────────────────────────────────────┐ │ │ │ extractActionPayload() → { task: {...}, isAddToBottom, ... } │ │ │ └──────────────────────────┬───────────────────────────────────┘ │ │ │ │ │ ▼ │ │ 4. Dispatch Action (operation-applier.service.ts) │ │ ┌──────────────────────────────────────────────────────────────┐ │ │ │ { type: '[Task] Add Task', │ │ │ │ task: { id: 'abc123', title: 'Buy milk', ... }, │ │ │ │ meta: { isPersistent: true, isRemote: true, ... } } │ │ │ └──────────────────────────┬───────────────────────────────────┘ │ │ │ │ │ ▼ │ │ 5. State Updated │ │ ┌──────────────┐ │ │ │ Task appears │ │ │ │ "Buy milk" │ │ │ └──────────────┘ │ │ │ └─────────────────────────────────────────────────────────────────────────────┘

从源码看,这条链路的两端分别落在 OperationLogUploadService(第 4 步加密后上传)与 OperationLogDownloadService(下载后解密再应用)上,而真正的加解密原语由@sp/sync-core包(packages/sync-core/src/encryption.ts)提供。

加密算法与线格式(Wire Format)细节

AES-256-GCM:机密性 + 完整性

加密核心采用AES-256-GCM(Galois/Counter Mode)认证加密算法,即一次调用同时保证机密性与完整性——GCM 认证标签(auth tag)能检测任何对密文的篡改。在 web-crypto.ts 中定义了算法常量:

  • ALGORITHM = 'AES-GCM'KEY_LENGTH = 32(即 256-bit 密钥);
  • 优先使用浏览器/运行时原生WebCryptocrypto.subtle),在不支持 WebCrypto 的环境(如部分 Android Capacitor WebView)自动回退到@noble/ciphers的纯 JS 实现(aesEncrypt/aesDecrypt);
  • 每个加密载荷使用12 字节全新随机 IV(取自 CSPRNG),这是 AES-GCM 在固定派生密钥下安全性的前提。

Argon2id 密钥派生:抗 GPU 暴力破解

密钥由用户密码经Argon2id派生,Argon2id 是内存困难型(memory-hard)KDF,可显著提高暴力破解成本。默认参数定义在 argon2.ts:

参数默认值说明
parallelism1并行度
iterations3迭代轮数
memorySize65536(KiB,即 64 MB)内存占用,这是抗 GPU 攻击的关键

派生函数deriveKeyFromPassword(password, salt?)输出DerivedKey { keyBytes, salt }:未提供 salt 时自动生成随机 16 字节 saltsetArgon2ParamsForTesting()允许测试环境使用弱参数加速(如{ memorySize: 8, iterations: 1 }),并在生产构建中禁止调用。

线格式与格式探测

密文的传输格式是一个冻结的公共契约(修改必须走版本字节迁移,参见 encryption.ts 的模块级 JSDoc):

Argon2id 密文 : [SALT (16)][IV (12)][AES-GCM ciphertext + auth tag] → base64(≥ 44 字节) Legacy PBKDF2 : [IV (12)][AES-GCM ciphertext + auth tag] → base64(≥ 28 字节)

detectFormat()通过长度判定格式:< 28字节判为 invalid;28..43字节必然为 legacy;≥ 44字节先按 Argon2id 处理,认证失败再回退 legacy(因为长 legacy 密文可能被长度启发式误判)。解密时,Argon2id 路径先从密文头部读取 salt 并复用会话缓存中的派生密钥,跳过前 16 字节 salt 后按 12 字节 IV 切片执行 AES-GCM 解密。

有意思的是,这个格式还有一个服务端侧的配套检查:由于服务器没有密钥、无法证明某载荷确实是密文,transport-shape.ts 提供了一个无依赖的「密文形状分类器」isEncryptedPayloadTransportShape()——它只做纯字符串运算,校验该值是「标准字母表 base64、长度是 4 的倍数、解码后 ≥ 28 字节」。服务器用它在上传边界拒绝意外明文或畸形上传,而不做解码、分配或日志记录。

会话级密钥缓存:摊薄昂贵的 KDF

Argon2id 在移动端单次派生可达500ms–2000ms(64MB 内存、3 轮迭代)。为此 session-cache.ts 实现了会话级缓存:

  • 加密侧:维护最近使用(MRU)的单个派生密钥sessionEncryptKeyCache,按密码的缓存哈希命中;
  • 解密侧sessionDecryptKeyCache"passwordHash:saltBase64"为键缓存派生密钥,容量上限SESSION_DECRYPT_CACHE_MAX_SIZE = 100,超出时按插入顺序淘汰(LRU 风格);
  • 清除时机:用户修改密码、退出登录或关闭加密时调用clearSessionKeyCache()(同时清空 legacy PBKDF2 缓存)。

密钥只在内存中存活,不落盘。密码的缓存哈希采用长度前缀字符串"${password.length}:${password}",见 hashPasswordForCache)——此前版本使用 32 位 djb2 哈希,碰撞会静默返回用不同密码派生的密钥,产生不可解密的密文。

批量加解密优化:为移动端设计

同步通常涉及成批操作,encryption.ts 暴露了encryptBatch/decryptBatch/decryptBatchSettled

  • encryptBatch只派生一次密钥,批内所有明文共享同一 salt、各自使用唯一 IV;
  • decryptBatch分三阶段:先分析并解码每个条目、收集所有唯一 salt;再串行派生各 salt 的密钥(并行派生无法真正并行、还可能因 64MB/次的内存占用导致移动端 OOM),同时将密钥保存在批内局部 Map中,避免超过 LRU 容量的批次发生抖动;最后并行执行 AES-GCM 解密;
  • decryptBatchSettled是逐项安顿(settled)变体,单项失败不影响整批:每个条目要么返回明文、要么返回净化后的错误名toSyncLogError只取错误类名,绝不携带可能内嵌用户数据的错误对象或消息),用于失败诊断。OperationError是 WebCrypto 的 AES-GCM 认证失败签名(密钥错误或密文损坏),InvalidCiphertextError表示过短/损坏数据,WebCryptoNotAvailableError是环境问题而非密码错误。

旧版 PBKDF2 兼容路径

早期版本使用PBKDF2(密码本身作为 salt,1000 轮迭代,SHA-256),这在密码学上是弱设计,仅保留用于向后兼容解密。legacy.ts 实现了该路径,并暴露setLegacyKdfWarningHandler(handler):宿主可以在每次 legacy 解密成功时收到回调,从而向用户提示「数据使用旧格式,建议重新同步以迁移」。注意 legacy 路径只支持 WebCrypto(没有 @noble 回退),不支持 WebCrypto 的移动端必须先从桌面端同步一次以完成数据迁移。

跨平台契约:Android 后台解密的独立复刻

线格式与 Argon2 参数不只是前端内部契约——Android 后台提醒 Worker 需要在无 WebCrypto 的 Kotlin 环境中独立解密载荷。OpPayloadDecryptor.kt 用 Kotlin 复刻了Argon2id(password, salt, p=1, t=3, m=64MiB, len=32)(配套的 Argon2.kt 是纯 Kotlin 的 RFC 9106 实现),并通过Argon2Test.kt用 hash-wasm 生成的测试向量锁定与 JS 侧输出完全一致。它同样实现了按 salt 缓存的派生密钥缓存,且支持deriveOnMiss=false的缓存只读模式——用于 BroadcastReceiver 约 10 秒的goAsync窗口,避免秒级 KDF 拖垮进程。CI 会通过 generate-android-crypto-fixtures.mjs 用前端encrypt()的实时输出喂给 Kotlin 测试,确保两端永不漂移。

关键组件:OperationEncryptionService

OperationEncryptionService 是操作与快照载荷加密的拥有者,它的契约远不止一次加密/解密往返:

  • 上传:将 JSON payload 序列化后加密,并把结果的isPayloadEncrypted置为true
  • 下载:先认证并解密密文,解析出 payload,然后在返回给应用管线之前,用已认证的 payload 数据核对未认证的信封元数据
  • LWW 目标/足迹不匹配以及明文opType被提升为 full-state 操作的情况都会 fail-closed(拒绝应用)。

单条与批量原语共享@sp/sync-core的会话缓存,因此同一密码的重复调用会复用派生密钥。测试建议使用真实加密 + 弱化 Argon2 参数(setArgon2ParamsForTesting({ memorySize: 8, iterations: 1 })),而不是 mock 包导出。

可执行校验位于 verify-decrypted-op-integrity.ts,其 spec 定义了可接受的 legacy 与 full-state 形状(verify-decrypted-op-integrity.spec.ts)。

上传集成:fail-closed 的加密边界

OperationLogUploadService 通过提供方契约取得密钥,在传输前加密操作与快照载荷。上传边界是 fail-closed 的:

  • 强制 E2EE 的提供方(SuperSync)没有可用密钥时不能上传任何待同步操作或快照——待同步工作保持未同步状态等待后续加密重试,结果如实报告「加密配置不完整」;
  • 文件类提供方若配置声明启用了加密但密钥缺失,则在上传前直接抛错,绝不回退为明文;
  • 文件格式加密的咽喉点 encrypt-and-compress-handler.service.ts 独立强制同一条「无密钥不上传」规则。

回归覆盖位于 operation-log-upload.service.spec.ts 与 encrypt-and-compress-handler.service.spec.ts。

从提供方实现看,SuperSyncProvider 将isEncryptionMandatory固定为true,并通过isEncryptionEnabled()(读取配置意图位,而非密钥存在性)与_isEncryptionHalfConfigured(cfg)isEncryptionEnabled && !encryptKey即「半配置」状态)保证:密钥暂时丢失的 dropped-credential 状态下,isReady()返回false,阻止自动同步把无法解密的操作下载下来或把明文推进加密数据集。

下载集成:应用前的安全校验管线

OperationLogDownloadService 在应用下载的操作前做全量筛查,上传服务对 piggybacked(捎带回传的)操作施加同样的入站检查:

  1. 期望加密却收到明文 → 整批拒绝:若 SuperSync 配置期望加密,任何明文入站操作都使该批失败。这防止伪造的isPayloadEncrypted=false标志绕过解密及所有解密后校验——拥有者是 assert-ops-encryption-expected.ts。注意判断依据是配置意图isEncryptionMandatory && isEncryptionEnabled())而非密钥存在性,因此在 dropped-credential 状态下依然 fail-closed;
  2. 加密输入但无密钥 → 抛出密码恢复错误:绝不当作明文处理;
  3. AES-GCM 认证成功后,才进行 payload 解析与元数据/full-state 校验(上文所述),解密后的操作不会先释放到应用管线

配置存储:密钥只存在于私有配置

加密密码/密钥只存储于提供方私有配置(private config),不属于同步的应用状态,也从不发送给服务器。加密意图同样存储于私有配置,但会镜像到globalConfig.sync.isEncryptionEnabled,使同步管线能够 fail-closed。该意图位可能随操作或快照载荷在网络上传输,但远程值是非权威的:hydration 会重新应用设备本地的值。

意图与密钥存在性分开暴露是刻意设计:如果只暴露密钥存在性,一个「配置为加密但密钥丢失」的客户端会静默地把加密配置降级为明文。新增代码时应参考 credential-store.service.ts、provider-types.ts 与具体的 SuperSyncProvider,而不是自行复制私有配置的数据结构。

安全属性与完整性边界

安全属性总览

属性保证
机密性(Confidentiality)服务器无法读取操作载荷
载荷完整性(Payload integrity)GCM 认证标签可检测对加密载荷的篡改
密钥安全(Key security)Argon2id 使密码暴力破解成本高昂
Nonce 唯一性(Nonce uniqueness)每个加密载荷在缓存密钥下使用全新随机 IV
前向保密(Forward secrecy)不提供;IV 唯一性不等于前向保密
错误密码(Wrong password)解密失败,操作被拒绝

完整性范围(重要)

只有op.payload被加密并由 AES-GCM 认证标签覆盖。操作的其他所有字段——actionTypeopTypeentityTypeentityIdentityIdsvectorClocktimestampschemaVersionsyncImportReason以及isPayloadEncrypted标志本身——都以明文传输,且没有作为附加认证数据(AAD)绑定到 GCM。因此一个恶意的/被攻陷的同步服务器或 TLS 中间人(MITM)可以篡改这些元数据。作为纵深防御,客户端在四个篡改向量上 fail-closed。

纵深防御向量一:明文注入降级(Plaintext-injection downgrade)

伪造一个isPayloadEncrypted=false的操作可以跳过解密与载荷校验、被原样应用——这在强制加密的客户端上等同于任意操作伪造。assertOpsEncryptedWhenExpected(assert-ops-encryption-expected.ts)在加密开启(配置意图)时拒绝任何入站明文操作(下载 + piggyback 两条路径)。之所以可以安全地全量拒绝,是因为开启加密会先删除并全部重新上传加密数据,服务器上不再存在任何合法明文操作——这依赖服务端契约deleteAllData()会移除所有可下载的明文操作。这是 SuperSync 操作级对文件型 GHSA-vrc7 下载防护与 GHSA-9544 上传防护的孪生实现。

纵深防御向量二:LWWentityId重定向(Retarget)

对 adapter 支撑的 LWW 更新而言,payload.id决定 reducer 应用到的实体。客户端拒绝认证后的payload.id不等于op.entityId的加密操作(verify-decrypted-op-integrity.ts 中的assertDecryptedOpMetadataIntegrity)。单例(Singleton)LWW 操作针对整个已注册的 feature 状态,因此像 TIME_TRACKING 的复合冲突 ID 没有规范的 payloadid,不在检查范围内。该门控与convertOpToAction基于 action 派生的存储模式检查保持镜像,防止两个边界漂移留下漏洞。

纵深防御向量三:项目移动足迹注入(Project-move footprint injection)

当加密的 TASK 项目移动载荷携带projectMoveSubTaskIds时,客户端要求明文op.entityIds与已认证集合{op.entityId} ∪ projectMoveSubTaskIds精确集合相等assertEncryptedProjectMoveFootprintIntegrity)。这防止被攻陷的服务器向一个合法移动操作追加受害任务 ID。同样的精确集合校验还扩展到了 Today 列表批量操作(assertEncryptedTodayListFootprintIntegrity,#9426 加固),把明文信封 ID 绑定到 GCM 认证的taskIds/toTaskId+fromTaskId足迹上。合成 LWW 操作(冲突解决产生、没有认证足迹)无法通过该临时防护检查,将完整信封绑定为 GCM AAD 仍是持久修复方案。

纵深防御向量四:full-stateopType提升(Promotion)

在解密一个标记为SYNC_IMPORTBACKUP_IMPORTREPAIR的操作后,客户端先对已认证的 payload 做结构化验证,确认它是完整的应用数据,之后元数据才能把它提升为loadAllDataassertDecryptedFullStateOpIntegrity)。支持直接载荷与appDataComplete包装载荷两种格式;受支持的 legacy 载荷会在验证副本上迁移;已知兼容的缺失(pre-section 备份、线快照中剥离的设备本地同步间隔)只在副本上恢复。原载荷保持不变,供既有操作处理管线继续使用。该验证使用 Typia 结构校验,并允许「可恢复的字段级模式漂移」(如旧版本缺少后来新增的必填标量)在验证副本上经autoFixTypiaErrors修复后通过——但缺失顶层根或容器类型错误的载荷仍然严格拒绝。

仍未覆盖的残余风险

不是完整完整性保证。持久修复落地前仍开放的问题:

  • LWW 内部entityType/actionType互换(ID 保持不变,因此能通过);
  • vectorClock/timestamp重排或重放
  • 恢复点路径getStateAtSeqimportCompleteBackup)应用服务器重建的状态时不受此防护——它本质上是服务器授权的,服务器对加密账户会阻止该操作,但 E2EE 无法认证它。

已知限制:运行在早于 GHSA-9544 上传防护版本的同级客户端仍可能推送明文操作;此时持有密钥的客户端会在此处 fail-closed 并显示篡改提示。应让旧版本同级保持离线并在再次同步前更新;若某个已更新客户端有已验证的完整副本,可导出备份并使用其显式的Force Overwrite操作,用加密的洁净全量状态替换混合历史——切勿从全新或不完整的客户端运行该操作。若没有任何已验证的完整客户端残留,请保留数据库与客户端用于事件恢复,不要跳过该行或将游标推进越过它。完整恢复流程见 backup-and-recovery.md。

把元数据(及加密标志)绑定为 GCM AAD、配以信封版本迁移和单调递增的「加密下限」来阻止降级——这一持久修复已在GHSA-8pxh-mgc7-gp3g中跟踪。客户端决策点绝不信任明文元数据

初始设置:密码对话框的选择逻辑

首次 SuperSync 设置时,应用通过在打开任何对话框之前探测服务器来决定显示哪个加密对话框(DialogSyncInitialCfgComponent.save() 的流程):

DialogSyncInitialCfgComponent.save() │ ▼ Save config + auth │ ▼ Probe server: downloadOps(0, undefined, 1) │ ├─── Server has encrypted ops ──► DialogEnterEncryptionPasswordComponent │ (isPayloadEncrypted=true) (enter existing password) │ ├─── Server empty or ───────────► DialogEnableEncryptionComponent │ unencrypted ops (create new password) │ └─── Probe fails ───────────────► DialogEnableEncryptionComponent (network/auth error) (fallback; sync error handling catches mismatches later)

这一探测防止第二个客户端加入时出现令人困惑的双重提示:没有探测的话,应用总是先显示「创建密码」,随后在同步时立刻失败再显示「输入密码」。安全网:若探测结果错误(例如竞态条件),sync-wrapper.service.ts中的_handleMissingPasswordDialog()_promptSuperSyncEncryptionIfNeeded()会在后续同步中捕获不匹配。

错误密码处理

当客户端使用错误密码尝试同步时,流程如下:

Client C (wrong password) tries to sync: │ ▼ Download encrypted ops │ ▼ Attempt decryption with wrong key │ ▼ ┌─────────────────────────────┐ │ DecryptError thrown │ │ "Failed to decrypt payload"│ └─────────────────────────────┘ │ ▼ Operation NOT applied to state Sync error shown in UI

操作不会被应用到状态,UI 会显示同步错误。实现层面,OperationEncryptionService的批量解密在整批失败后还会调用_diagnoseFailedBatchdecryptBatchSettled):若同一批中存在成功解密的条目,passwordEvidenceconfirmed-for-some-operations(说明密钥并非全局错误,问题出在个别损坏条目);若所有条目都解密失败,则为no-operation-decrypted——这避免了把「错误密码」误报成单个条目的失败。诊断过程中的每个明文只做解析检查后即被丢弃,绝不保留在返回的错误对象上。

快照(全量状态)加密

Full-state 操作(备份导入与修复)走快照端点,但保留同样的 fail-closed 边界:

  • 上传:上传服务在传输前验证 full-state 结构,有密钥时加密载荷;对强制加密且有待同步工作但没有密钥的提供方,无法到达快照上传分支
  • 下载:加密的 full-state 操作只有在 AES-GCM 认证且assertDecryptedFullStateOpIntegrity()验证其为完整应用数据之后才被接受(包括在验证副本上执行受支持的 legacy 迁移)。

可执行代码的归属:

  • 上传路由与强制密钥防护:operation-log-upload.service.ts
  • 载荷密码学与解密后分发边界:operation-encryption.service.ts
  • Full-state 完整性校验:verify-decrypted-op-integrity.ts
  • Full-state 回归覆盖:verify-decrypted-op-integrity.spec.ts

结语:从加密原语到纵深防御的整体心智模型

回顾整个 SuperSync E2EE 设计,可以提炼出三个层层递进的层次:

  1. 原语层:AES-256-GCM + Argon2id + 冻结的[SALT(16)][IV(12)][ciphertext+tag]线格式,由 packages/sync-core/src/encryption/ 提供,并通过会话缓存与批量 API 解决移动端 KDF 昂贵的问题,同时以 Android Kotlin 复刻保证跨平台一致;
  2. 边界层:上传/下载两条链路在 op-log/sync/ 中严格 fail-closed——无密钥不上传、期望加密拒明文、认证失败即拒绝,密钥与意图只存于私有配置;
  3. 纵深防御层:由于信封元数据明文传输且未绑定为 AAD,客户端用四个针对性校验(明文注入降级、LWW 重定向、项目移动足迹、full-state 提升)堵住最危险的篡改向量,并把剩余风险(LWW 内部字段互换、向量时钟重放、恢复点路径)明确记录在 GHSA-8pxh-mgc7-gp3g 的持久修复路线中。

理解这套设计的价值在于:它展示了在「服务端不可信」前提下,一个真实的、运行于 Web/桌面/移动多端的同步系统如何用「加密原语 + 边界纪律 + 纵深防御」三层结构来逼近端到端安全,以及为什么完整性边界必须与加密边界同时设计——只加密而不防护信封元数据,安全目标依然是残缺的。

【免费下载链接】super-productivitySuper Productivity is an advanced todo list app with integrated Timeboxing and time tracking capabilities. It also comes with integrations for Jira, GitLab, GitHub and Open Project.项目地址: https://gitcode.com/GitHub_Trending/su/super-productivity

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/13 17:18:22

STC8A8K64S4A12开发板实战:从原理图到例程移植与串口Modbus调试

简介&#xff1a;STC8A8K64S4A12开发板资料包面向单片机学习者与嵌入式开发工程师&#xff0c;汇集硬件设计与软件示例于一体&#xff0c;可帮助快速掌握增强型8051内核的编程方法与外设应用。压缩包约96.11MB&#xff0c;内含开发板PDF原理图及45个软件DEMO例程&#xff0c;原…

作者头像 李华
网站建设 2026/9/13 17:18:20

PMSM无感FOC实战:从硬件选型到滑模观测器落地

1. 为什么电机控制成了秋招“硬通货”&#xff1f;——从招聘JD反推能力图谱 去年帮三个应届生改简历&#xff0c;其中两个投递自动化、电力电子、机器人方向的岗位&#xff0c;一个卡在初筛&#xff0c;两个卡在二面技术环节。我挨个翻他们投的公司JD&#xff0c;发现一个共性…

作者头像 李华
网站建设 2026/9/13 17:18:17

ARM设备ADB调试实战:从tgz解压到logcat抓取

简介&#xff1a;一份 ADB&#xff08;Android Debug Bridge&#xff09;工具的完整源码包&#xff0c;面向嵌入式开发、驱动调试、系统移植等场景的工程师&#xff0c;也适合有交叉编译基础的中高级开发者。官方预编译版本常受架构与运行环境限制&#xff0c;而这份源码可直接…

作者头像 李华
网站建设 2026/9/13 17:16:57

WinForm自定义界面实战:无边框窗体+GDI+自绘仿360皮肤

简介&#xff1a;一款面向C# WinForm开发者设计的界面模仿项目&#xff0c;基于Visual Studio 2013与.NET 4.0框架实现&#xff0c;借鉴360安全卫士的界面布局&#xff0c;通过自定义控件高程度还原主窗口、功能导航与操作按钮等视觉元素。适合希望快速掌握WinForm界面美化、无…

作者头像 李华
网站建设 2026/9/13 17:16:23

智能优化算法改进BP神经网络的Matlab实现

1. 项目背景与核心价值在机器学习领域&#xff0c;BP神经网络作为经典的监督学习算法&#xff0c;长期面临着收敛速度慢、易陷入局部最优等痛点问题。最近两年&#xff0c;基于生物启发的新型智能优化算法在参数优化领域展现出显著优势。这个项目实现了六种前沿算法&#xff08…

作者头像 李华
网站建设 2026/9/13 17:14:53

DMA完成通知机制:从硬件IRQ到Linux中断处理全链路解析

1. 这个问题为什么值得花一整天去抠&#xff1f;——从“DMA干完活没人通知CPU”说起你有没有遇到过这样的场景&#xff1a;代码里调用了一次DMA memcpy&#xff0c;函数立刻返回了&#xff0c;但你一查目标内存&#xff0c;数据还是空的&#xff1b;或者在嵌入式设备上启动一个…

作者头像 李华