news 2026/9/10 7:45:51

Joplin 端到端加密(E2EE)密文结构与同步快照格式深度解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Joplin 端到端加密(E2EE)密文结构与同步快照格式深度解析

Joplin 端到端加密(E2EE)密文结构与同步快照格式深度解析

【免费下载链接】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 是一款以隐私为核心的跨平台笔记应用(Windows、macOS、Linux、Android、iOS),其端到端加密(E2EE)是保障笔记内容安全的关键机制。本文以仓库中真实存储的 E2EE 同步快照文件 packages/app-cli/tests/support/syncTargetSnapshots/1/e2ee/6db42e4e9c0b43ed891269d8a0508c76.md 为标本,结合 EncryptionService.ts、BaseItem.ts 等源码,逐字节拆解 JED 密文头格式、SJCL 负载结构、可保留明文元数据字段,以及同步快照目录的生成与迁移测试机制。读完本文,你将能够独立阅读、验证任意一条 Joplin E2EE 密文记录,并理解快照迁移测试(Migration Handler)如何保证同步版本升级时加密数据不被破坏。

一、E2EE 快照文件是什么:一份真实的密文标本

packages/app-cli/tests/support/syncTargetSnapshots/1/e2ee/目录下,存放着一批用于测试的历史同步目标快照(snapshot)。其中6db42e4e9c0b43ed891269d8a0508c76.md是一个NoteTag(笔记-标签关联项,type_: 6)在端到端加密后的磁盘序列化形态,全文如下:

id: 6db42e4e9c0b43ed891269d8a0508c76 note_id: 1241f69ccbf54c0188b2f4a02e862b40 tag_id: acbba73d7a1a46848ddc96b96c64ec8b created_time: updated_time: 2020-07-25T10:37:00.415Z user_created_time: user_updated_time: encryption_cipher_text: JED0100002205c24138199f5b403fa3e9b8b4f22685c50002d8{"iv":"4y/iBWttvQyriXqtS2CGHw==","v":1,"iter":101,"ks":128,"ts":64,"mode":"ccm","adata":"","cipher":"aes","salt":"O2duAuTVjV4=","ct":"0p7EzmL/RzbEWyNcYfAL74fe3biMcNKyzNniXLNFcQphuBFTfLRp7zo6w6wSZ+cX//DTlA7bzPUz4/Vh7KRqbjqdEreSS6ZDRFfc2mT5rMW7msfbxvYOwg4iu3B/FfEUXM55GGHuzHYirkEQvpO0tjI84StZ0vXvUXmXxnOuizcKWrMHWDAAo41pAVIvncOFTnNfYPB+KIlG0WQ1gHgQgclpAYRrm0P/CM/sr/hQQWyH8exc+ToUg0JRmamLs12qHAYmXMlXfwf66vHhYBBGEj8mXErnp0uj6CtENE6AV2QdrEz1QcRa/XwmNb2xDNxS6+UaISzE79UrGNY4uO5om0YQRRhZpmC3A0QUzyd4VF5dEKaar/9W5wXJ/PKSw8l0/KJaNVNGhqLux0mlfEaAhRnzaFkru/JuIGY79hasyJl5l1ANEx9eByRAaLCq8HcQvm7yKva5/7zwHJi/zMVc25sQvmV/owUb+hVFifRfp3azkSj2826kRqJDwc2bXJHRGs7yVCeNuRdoSbAZuve9hUud46MjqOr2/MQ/ml7LQXtufba+WBB0gtheVE12l5ceKps5ejEa6mu2wQ=="} encryption_applied: 1 is_shared: type_: 6

这份文件同时揭示了 Joplin E2EE 的两层信息暴露面:

  • 以明文保留的“同步关键字段”idnote_idtag_idupdated_timetype_。同步引擎必须依赖这些字段完成增量传输、冲突检测和关系建立,因此它们不能被加密;
  • 被加密的内容created_timeuser_created_timeuser_updated_time等其余字段被序列化后整体加密进encryption_cipher_text。对 NoteTag 这类关联实体,真正的“正文”几乎都藏在密文里。

二、JED01 密文头:解密的第一道钥匙

所有 Joplin 加密数据都以JED01开头。这个标识符由 EncryptionService.encodeHeader_ 生成,其结构严格对应 headerTemplates_ 中定义的模板版本 1:

字段字节数取值(以本快照为例)含义
标识符3JEDJoplin Encrypted Data
模板版本201头模板版本号
元数据长度6000022十六进制,表示后面元数据共 0x22 = 34 字节
加密方法205十六进制,对应EncryptionMethod枚举
主密钥 ID32c24138199f5b403fa3e9b8b4f22685c5用于解密此数据的主密钥

对照代码中的 encodeHeader_:

let encryptionMetadata = ''; encryptionMetadata += padLeft(header.encryptionMethod.toString(16), 2, '0'); // 加密方法,占 2 字节 encryptionMetadata += header.masterKeyId; // 主密钥 ID,固定 32 字节 encryptionMetadata = padLeft(encryptionMetadata.length.toString(16), 6, '0') + encryptionMetadata; return `JED01${encryptionMetadata}`;

可以手工拆解本文件的密文头JED0100002205c24138199f5b403fa3e9b8b4f22685c5

  1. JED01:标识符 + 模板版本;
  2. 000022:元数据区长度 34 字节;
  3. 05:加密方法 = 5,即EncryptionMethod.SJCL1a
  4. c24138199f5b403fa3e9b8b4f22685c5:32 字节十六进制主密钥 ID,指向同目录下的主密钥记录 c24138199f5b403fa3e9b8b4f22685c5.md。

解密时 decodeHeaderSource_ 会先读取 5 字节调用isValidHeaderIdentifier校验JED\d\d格式(见 EncryptionService.ts),再读 6 字节长度、按模板解析出encryptionMethodmasterKeyId,随后从已加载的主密钥中取出明文密钥对负载解密。

三、SJCL 负载与 EncryptionMethod 演进

密文头之后的{"iv":"...","v":1,...}是 SJCL(Stanford JavaScript Crypto Library)的 JSON 密文格式。本快照的负载参数如下:

参数说明
v1SJCL 内部格式版本
iter101PBKDF2 密钥派生迭代次数(SJCL 强制要求 >100)
ks128AES 密钥长度 128 位
ts64GCM/CCM 认证标签长度 64 位
modeccmAES-CCM 认证加密模式
adata未使用关联数据
cipheraes加密算法
saltO2duAuTVjV4=PBKDF2 随机盐
ctbase64密文本体

iter: 101ks: 128正好对应 EncryptionService.ts 中EncryptionMethod.SJCL1a的实现:因为主密钥本身已经过密钥派生保护,正文无需再做高开销迭代,101 次即可在移动端保持快速解密;同时正文加密前先做escape()处理,以规避 SJCL 只接受合法 UTF-8 导致包含非法字节的笔记解密失败的问题(修复 issue #2591)。

EncryptionMethod枚举完整定义于 EncryptionService.ts,其演进脉络直接写进了代码注释:

枚举状态关键参数
SJCL1已弃用(OCB2 模式不再安全,2020-01-23)AES-128/OCB2, iter=1000
SJCL22已弃用(曾用于主密钥)AES-256/OCB2, iter=10000, 带 SHA-256 校验和
SJCL33保留兼容AES-128/CCM, iter=1000
SJCL44曾用于主密钥(本快照的主密钥即此方法)AES-256/CCM, iter=10000
SJCL1a5正文默认方法(本快照使用)AES-128/CCM, iter=101, 带 escape
Custom6自定义处理器扩展点EncryptionCustomHandler提供
SJCL1b7现行正文方法(2023-06-10 起)AES-256/CCM, iter=101
KeyV18现行主密钥方法(AES-256-GCM + PBKDF2)SHA-512 派生, keyLength=32, iterationCount=220000
FileV19现行文件/资源加密方法AES-256-GCM, 内容按 base64 处理
StringV110现行字符串正文方法AES-256-GCM, 内容按 utf16le 处理

快照中的05(SJCL1a)说明这套快照生成于 2020-03 之后、2023-06 之前,与文件头部的updated_time: 2020-07-25T10:37:00.415Z完全吻合。而主密钥记录 c24138199f5b403fa3e9b8b4f22685c5.md 中encryption_method: 4iter: 10000mode: ccm正是SJCL4的签名——同一套快照里,正文与主密钥使用不同世代的加密方法,是 Joplin E2EE 版本兼容设计的典型体现。

3.1 主密钥的加载与明文缓存

主密钥并不直接参与正文加解密,而是以“密文主密钥 + 内存明文缓存”的方式管理。核心逻辑在 EncryptionService.loadMasterKey:

  • encryptedMasterKeys_:只记录主密钥的decrypt回调与updated_time,不持有明文;
  • decryptedMasterKeys_:密码校验成功后缓存{ plainText, updatedTime },并以updated_time参与 isMasterKeyLoaded 的新鲜度判断;
  • 主动/惰性加载:makeActive=true时立即解密并设为活动主密钥,否则仅登记回调,待首次使用(loadedMasterKey)时才执行解密。

由于正文解密依赖loadedMasterKey(masterKeyId).plainText(见 masterKeyPlainText_),如果对应的主密钥未加载,会抛出masterKeyNotLoaded错误,并由 BaseItem.encrypt 捕获后派发MASTERKEY_ADD_NOT_LOADED事件,提示用户输入主密钥密码。

四、字段级加密:哪些字段保留明文,为什么

对照同目录下的加密笔记 1241f69ccbf54c0188b2f4a02e862b40.md,可以归纳出两条规律:

  • 明文保留的字段:idparent_idnote_idtag_idupdated_timetype_等;
  • 其余业务字段(标题、正文、创建时间、来源 URL 等)全部进入密文。

这个白名单直接由 BaseItem.encrypt 中的keepKeys数组定义:

// List of keys that won't be encrypted - mostly foreign keys required to link items // with each others and timestamp required for synchronisation. const keepKeys = ['id', 'note_id', 'tag_id', 'parent_id', 'share_id', 'updated_time', 'deleted_time', 'type_', 'is_locked', 'extracted_resource_ids'];

从源码注释可以看出设计取舍:外键用于在密文状态下建立实体关联(如 NoteTag 通过note_id/tag_id关联笔记与标签),时间戳用于增量同步与冲突检测,这两类信息对同步引擎是“必需品”,因此保持明文;而真正承载用户隐私的字段全部进入encryption_cipher_text,并以encryption_applied: 1标记。加解密后处理见 BaseItem.decrypt:解密不会改变updated_time(“解密不算修改”),随后以ItemChange.SOURCE_DECRYPTION变更源保存明文版本。

五、快照机制:E2EE 数据如何被复制与迁移

5.1 快照的生成与部署

快照目录结构为syncTargetSnapshots/{syncVersion}/{normal|e2ee}/,其中数字 1/2/3 对应同步目标版本。生成脚本位于 syncTargetUtils.ts 的main()

  1. 初始化数据库与同步器、切换到客户端 1;
  2. 通过createTestData(testData)构造固定的测试数据(含文件夹、带附件的笔记、标签等,见 syncTargetUtils.ts);
  3. 若生成e2ee快照,则调用setEncryptionEnabled(true)loadEncryptionMasterKey()加载测试主密钥;
  4. 执行一次完整同步,把syncDir中的文件复制到syncTargetSnapshots/{version}/{type}/形成快照。

测试侧则通过deploySyncTargetSnapshot('e2ee', migrationVersion - 1)把历史快照复制回同步目录,模拟“旧版本同步目标被新版本客户端打开”的场景(见 synchronizer_MigrationHandler.test.ts)。

5.2 迁移测试如何验证加密数据不被破坏

synchronizer_MigrationHandler.test.ts 是理解快照用途的入口。文件头注释说明了快照的完整生命周期:先用createSyncTargetSnapshot.js normal && createSyncTargetSnapshot.js e2ee生成各版本快照,测试再把 n 版快照升级到 n+1 版。E2EE 分支的关键断言如下:

await deploySyncTargetSnapshot('e2ee', migrationVersion - 1); Setting.setConstant('syncVersion', migrationVersion); await migrationHandler().upgrade(migrationVersion); // ... await synchronizer().start(); const masterKey = (await MasterKey.all())[0]; Setting.setObjectValue('encryption.passwordCache', masterKey.id, '123456'); await loadMasterKeysFromSettings(encryptionService()); await decryptionWorker().start(); await expectNotThrow(async () => await checkTestData(testData));

其验证链路是:部署快照 → 升级同步版本 → 同步拉取密文 → 用固定密码123456解密主密钥 → 运行decryptionWorker解密全部条目 → 断言测试数据完整无缺checkTestData(syncTargetUtils.ts)会逐条加载文件夹、笔记、资源与标签并校验关联关系,从而证明“同步版本升级不破坏任何已加密数据”。这就是为什么快照中会出现明文updated_time——迁移测试需要这些字段来判断数据是否在升级后被意外改写。

六、给开发者的验证与调试建议

  • 手工验证密文头:任何 JED 密文都可以用JED01+ 6 字节长度 + 2 字节方法 + 32 字节主密钥 ID 的规则手工解析,无需先解密负载;
  • 用测试密码复现解密:迁移测试固定使用密码123456加载主密钥(见 synchronizer_MigrationHandler.test.ts),对快照调试时可沿用;
  • 追踪加解密调用链:正文加密从 BaseItem.encrypt 进入EncryptionService.encryptString(EncryptionService.ts),其核心是encryptAbstract_(EncryptionService.ts)——先写 JED 头,再按chunkSize分块加密、每块前置 6 字节十六进制块长;解密则按块长循环读取,天然支持流式处理大文件;
  • 关注分块大小:不同方法的分块大小定义在 chunkSize:SJCL 系列与 KeyV1 为 5000 字节、FileV1 为 131072 字节(128KB)、StringV1 为 65536 字节(64KB)。代码注释特别说明移动端解密性能随块大小呈指数恶化(50KB≈1000ms,5KB≈10ms),因此文本数据刻意使用小块;
  • 兼容旧快照:仓库保留了多代加密方法(含已弃用的 OCB2 模式),任何新方法上线都必须保留旧方法的解密分支,这解释了 EncryptionMethod 枚举只增不减的原因。

七、总结

一份看似普通的快照文件6db42e4e9c0b43ed891269d8a0508c76.md,完整呈现了 Joplin E2EE 的核心设计:JED01头携带加密方法与主密钥 ID,SJCL/AES-GCM 负载承载被加密的业务字段,keepKeys白名单保留同步必需的外键与时间戳,而整套快照目录则服务于“跨版本升级不破坏密文”的迁移测试体系。理解这一结构,无论是排查解密失败、研究同步协议,还是为 Joplin 开发新的存储后端,都能迅速定位到 EncryptionService.ts、BaseItem.ts 与 syncTargetUtils.ts 这三处核心实现。

【免费下载链接】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),仅供参考

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

实木板材真的环保吗?揭秘甲醛释放与环保等级的真相

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/10 7:42:26

轻量级AI Agent运行时设计:消息循环与工具调用实战

1. 项目定位与整体设计思路1.1 从“Hermes”这个名字聊起:Agent的本质是替人跑腿“hermes-agent”这名字起得有点意思。Hermes是希腊神话里的信使,职责是在众神之间传递消息、搬运指令。如果你把现代AI Agent拆开看,真正干活的角色其实也就是…

作者头像 李华
网站建设 2026/9/10 7:42:09

STM32并口驱动ILI9325/ILI9341实战指南

简介:本资源是正点原子推出的ILI9325/ILI9341 TFT-LCD并口驱动工程,面向嵌入式初学者与STM32开发工程师,解决TFT液晶屏在裸机环境下基于并行接口的稳定驱动难题。工程完整实现初始化配置、命令/数据写入、帧缓冲管理及RGB色彩格式转换等核心功…

作者头像 李华
网站建设 2026/9/10 7:40:30

如何用 rclone serve restic 为 restic 备份提供 REST 存储后端?

如何用 rclone serve restic 为 restic 备份提供 REST 存储后端? 【免费下载链接】rclone "rsync for cloud storage" - Google Drive, S3, Dropbox, Backblaze B2, One Drive, Swift, Hubic, Wasabi, Google Cloud Storage, Azure Blob, Azure Files, Ya…

作者头像 李华
网站建设 2026/9/10 7:39:59

Codex工程计算文档生成:从自然语言到合规报告的全链路解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华