
解密 Joplin E2EE 同步快照从一份加密 Note 文件看懂端到端加密的存储与同步格式【免费下载链接】joplinJoplin - the privacy-focused note taking app with sync capabilities for Windows, macOS, Linux, Android and iOS.项目地址: https://gitcode.com/GitHub_Trending/jo/joplin本文以 Joplin 测试基础设施中的 E2EE 同步快照文件49e1777d9c17439fb612cce85700d16d.md为切入点完整拆解这份端到端加密E2EE笔记在 v2 同步协议下的序列化结构包括encryption_cipher_text的JED01加密头部、SJCL/AES-CCM 密文载荷、type_类型码与父级引用并结合packages/lib中的EncryptionService与syncTargetUtils源码说明这些快照是如何生成与消费的。读完后你将能够独立解析任意一份 Joplin 加密 Item 的字段语义并理解 E2EE 模式下“加密在客户端、存储只存密文”的架构设计。快照文件的位置与用途指定文档位于packages/app-cli/tests/support/syncTargetSnapshots/2/e2ee/目录下是 Joplin 同步测试用的“同步目标快照”sync target snapshot。目录结构本身即携带元信息2/同步版本sync version。同目录下的 info.json 内容为{version:2}表明这套快照遵循的是 Joplin 第二代同步协议Item-based以items表统一存储笔记、笔记本、标签等所有对象e2ee/加密类型。与normal明文快照相对这里存放的是开启端到端加密后同步到远端的密文形态文件名即 Item ID49e1777d9c17439fb612cce85700d16d.md是标准的 32 位十六进制 UUID客户端生成对应源码中约定的 32 字节 master key ID 同一量级的标识符规范见 EncryptionService.ts。快照的生成与部署逻辑在 syncTargetUtils.ts 中deploySyncTargetSnapshot会把syncTargetSnapshots/syncVersion/syncTargetType整个目录复制到临时同步目录作为“远端服务器”而main()函数则反向操作——先在本地数据库创建一棵固定的测试数据树folder1/folder2/folder3、note1~note5、标签、附件资源对e2ee类型调用setEncryptionEnabled(true)并loadEncryptionMasterKey()后启动同步再把同步产生的目录复制回快照目录。也就是说这份.md文件并非手写而是真实走了一遍“创建加密笔记 → 客户端加密 → 通过 Synchronizer 上传 → 落盘为远端 Item 文件”的完整链路。开发文档 server_items.md 也明确指出“Examples of serialized items are described inpackages/app-cli/tests/support/syncTargetSnapshots”即这些快照是理解 Item 序列化格式的官方示例。逐字段解析这份加密 Note下面是快照全文已按字段重组说明id: 49e1777d9c17439fb612cce85700d16d parent_id: 673415563b2f4db2aae8665ddf9fbc67 created_time: updated_time: 2020-07-25T10:55:20.924Z is_conflict: latitude: longitude: altitude: author: source_url: is_todo: todo_due: todo_completed: source: source_application: application_data: order: user_created_time: user_updated_time: encryption_cipher_text: JED0100002205a1a0987e82cc400c90582492f814c23c0004c8{iv:OWv0KZHCnkbL4rXmhV4Q,...,cipher:aes,salt:Gyo7bQeqz2w,ct:L0eNB9ORDkJ5qatAYN...} encryption_applied: 1 markup_language: is_shared: type_: 1这是一份标准的 Joplin v2 序列化 Item键值对 空值省略各字段语义如下字段值含义id49e1777d9c17439fb612cce85700d16d客户端生成的全局唯一 ID作为 Item 的主键即jop_id概念parent_id673415563b2f4db2aae8665ddf9fbc67父对象 ID指向同目录下的 673415563b2f4db2aae8665ddf9fbc67.mdtype_: 2即 Folder/Notebook说明这是一条挂在某个笔记本下的 Noteupdated_time2020-07-25T10:55:20.924Z服务端/客户端记录的最后修改时间UTC ISO8601是同步冲突检测的依据encryption_cipher_textJED01...{...}笔记正文的密文见下节encryption_applied1加密已应用的标记位服务端据此判断该 Item 是否经过 E2EE 加密无需再解密处理type_1Item 类型码1为 Note对照父文件type_: 2即 Folder/Notebook同目录资源文件使用type_: 9注意两点设计密文字段复用同一列明文 Item 使用body字段存正文加密 Item 则完全没有body而是把密文放进encryption_cipher_text并以encryption_applied: 1标记。对服务端而言二者完全同构都是“不透明字符串”——这正是 E2EE 的存储层体现Joplin Server 或任何第三方同步目标拿到的只有密文服务器不持有解密能力。server_items.md 描述了服务端将 Item 序列化后存入items表content字段、加密标记存jop_*字段的做法与快照中encryption_applied的存在相互印证。时间戳双轨制updated_time与user_updated_time分离。E2EE 模式下服务端无法读取笔记内容变化因此保留用户侧时间戳用于展示同步逻辑仍基于可比较的服务端时间戳。加密头部JED01的十六进制编码规则encryption_cipher_text由两部分拼接而成定长十六进制头部 SJCL JSON 密文体。以快照为例JED01 000022 05 a1a0987e82cc400c90582492f814c23c0004c8 {iv/v/iter/ks/ts/mode/adata/cipher/salt/ct...} └─标识─┘ └─元数据长度─┘ └加密方法─┘ └──── 32 位 master key ID ────┘头部生成逻辑可直接在 encodeHeader_ 中验证public encodeHeader_(header: { encryptionMethod: number; masterKeyId: string }) { // Sanity check if (header.masterKeyId.length ! 32) throw new Error(Invalid master key ID size: ${header.masterKeyId}); let encryptionMetadata ; encryptionMetadata padLeft(header.encryptionMethod.toString(16), 2, 0); encryptionMetadata header.masterKeyId; encryptionMetadata padLeft(encryptionMetadata.length.toString(16), 6, 0) encryptionMetadata; return JED01${encryptionMetadata}; }对应到本快照可以逐段核对JED01固定标识符。JEDJoplin Encrypted Data后跟格式版本01解码侧的 decodeHeaderSource_ 会先读 5 字节校验该标识不匹配则抛出Data is not actually encrypted错误000022元数据长度的 6 位十六进制数0x22 34恰好等于后随05 32 位 master key ID1 32 33… 实际 23234 字节的长度05加密方法码对应 EncryptionMethod 枚举中的SJCL2 2——注意此处值为05对应枚举SJCL1a 5一类字符串加密方法即 SJCL 库实现的 AES 字符串加密a1a0987e82cc400c90582492f814c23c0004c832 位32 字节 hexmaster key ID即用哪个主密钥来解密本条数据的指针。头部模板由 headerTemplates_ 定义版本 1 的字段布局为[[encryptionMethod, 2, int], [masterKeyId, 32, hex]]与上面拆出的两段完全一致。关键观察把本文件的 master key ID 与同目录其他文件比对——a1a0987e82cc400c90582492f814c23c0004c8恰好等于目录中另一份快照 a1a0987e82cc400c90582492f814c23c.md 的文件名。这说明该快照集内所有笔记、文件夹、资源都挂在同一个 master key 之下符合“一个主密钥统一派生、按 key ID 索引”的设计。SJCL 密文体AES-CCM 参数逐项解读头部之后的 JSON 是 SJCL 库输出的标准加密包快照中各参数含义如下{ iv: OWv0KZHCnkbL4rXmhV4Q, v: 1, iter: 101, ks: 128, ts: 64, mode: ccm, adata:, cipher:aes, salt: Gyo7bQeqz2w, ct: L0eNB9ORDkJ5qatAYN...约 1.4 KB 的 base64 密文 }参数值说明v1SJCL 加密包版本iter101密钥派生PBKDF2迭代次数。迭代次数与主密钥强度/派生配置相关此处为字符串加密所用方法EncryptionMethod中SJCL1a/SJCL2等 SJCL 系列的设定值ks128对称密钥长度 128 bitAES-128ts64派生出的数据密钥中实际用于加密的位数modeccmCCM 模式同时提供加密与认证AEAD密文被篡改时解密会失败保证笔记正文完整性cipheraes底层分组密码iv24 字节 base64CCM 模式的初始化向量每次加密随机生成salt16 字节 base64PBKDF2 盐值防止彩虹表攻击ctbase64笔记正文body的 Markdown 内容经 AES-CCM 加密后的密文值得注意的是快照目录中不同文件使用了不同强度的 SJCL 包本 Note 正文为iter: 101, ks: 128而附件资源 a1a0987e82cc400c90582492f814c23c.md 的content字段则是iter: 10000, ks: 256且带encryption_method: 4字段。对照 EncryptionMethod 枚举4是SJCL4同时资源类型type_: 9对应FileV1 9一脉相承的文件加密路径——即文件/资源采用更强的 AES-256 万次迭代派生笔记正文采用 AES-128 字符串加密两种数据形态在加密强度上做了差异化配置。这一差异可以直接从快照文件本身观察到是 E2EE 设计中最容易被忽视却最能说明问题的细节。父级引用与整个快照集的拓扑parent_id指向的 673415563b2f4db2aae8665ddf9fbc67.md 展示了 E2EE 对元数据的覆盖范围id: 673415563b2f4db2aae8665ddf9fbc67 updated_time: 2020-07-25T10:55:20.815Z encryption_cipher_text: JED0100002205a1a0987e82cc400c90582492f814c23c0004c8{iv:kCJkRvCMeKRKb41oOeAtA,...,ct:7Fzu0jh8Aejq...} encryption_applied: 1 parent_id: type_: 2type_: 2表示 FolderNotebook它没有title字段取而代之的是encryption_cipher_text——文件夹名同样被加密。这意味着服务端及任何旁观者不仅读不到笔记内容也读不到“你有哪些笔记本、叫什么名字”其updated_time比子笔记早约 109ms:20.815Zvs:20.924Z符合“先建文件夹、再建笔记”的创建顺序也印证了 createTestData 中Folder.save先于Note.save的调用序列。目录内约 20 个文件共同构成一棵完整的加密对象树Notetype_: 1、Foldertype_: 2、Tagtype_: 3、Resourcetype_: 9以content字段存密文、source_application: net.cozic.joplintest-cli标记来源应用全部遵循同一套JED01头部 SJCL 密文体格式且共享同一个 master key ID。从快照回看 E2EE 的密钥与调用链把这些字段串回源码E2EE 同步的调用链是清晰的开启加密setEncryptionEnabled(true)loadEncryptionMasterKey()见 syncTargetUtils.ts主密钥由用户在客户端生成并仅存于本地/受保护存储序列化前加密Synchronizer 在把 Note/Folder 序列化上传前对body/title等敏感字段调用 EncryptionService 的加密接口得到JED01头部 密文并置encryption_applied: 1上传为 Item加密后的 Item 作为不透明内容通过PUT /api/items交给 Joplin Server服务端按 server_items.md 描述的流程存入items表全程不解密落盘为快照测试框架把远端目录原样复制为syncTargetSnapshots/2/e2ee/供回归测试反复部署为“远端”。解密方向则完全对称客户端从头部解析出encryptionMethod与masterKeyId定位对应主密钥后用 SJCL/AES-CCM 还原明文并校验 CCM 认证标签。任何一环被破坏——例如 master key ID 指向不存在的密钥、或密文被篡改——都会得到明确的错误Invalid encryption identifier/ 解密失败而非静默的坏数据。总结这份 49e1777d9c17439fb612cce85700d16d.md 是 Joplin v2 同步协议下一个E2EE 加密 Note 的权威样本type_: 1标识 Noteparent_id挂在加密 Folder 之下正文以encryption_cipher_text承载、encryption_applied: 1标记加密状态encryption_cipher_text的格式为JED01十六进制头部版本 元数据长度 加密方法码 32 位 master key ID SJCL AES-CCM JSON 密文iv/iter/ks/ts/salt/ct头部编解码规则与 EncryptionService.encodeHeader_/decodeHeaderSource_ 的实现逐位对应同目录快照集还揭示了资源文件encryption_method: 4AES-256/10000 iter与笔记正文AES-128/101 iter的差异化加密配置以及文件夹标题同样被加密的元数据保护策略对开发者而言syncTargetSnapshots目录配合 syncTargetUtils.ts 是研究 Joplin 同步协议与 E2EE 实现最直接的“活文档”每个.md文件都可按本文方法独立解析。【免费下载链接】joplinJoplin - the privacy-focused note taking app with sync capabilities for Windows, macOS, Linux, Android and iOS.项目地址: https://gitcode.com/GitHub_Trending/jo/joplin创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考