资讯动态

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

发布时间:2026/9/10 7:46:01 来源:尧图企业网站定制
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/joplinJoplin 是一款以隐私为核心的跨平台笔记应用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/RzbEWyNcYfAL74fe3biMcNKyzNniXLNFcQphuBFTfLRp7zo6w6wSZcX//DTlA7bzPUz4/Vh7KRqbjqdEreSS6ZDRFfc2mT5rMW7msfbxvYOwg4iu3B/FfEUXM55GGHuzHYirkEQvpO0tjI84StZ0vXvUXmXxnOuizcKWrMHWDAAo41pAVIvncOFTnNfYPBKIlG0WQ1gHgQgclpAYRrm0P/CM/sr/hQQWyH8excToUg0JRmamLs12qHAYmXMlXfwf66vHhYBBGEj8mXErnp0uj6CtENE6AV2QdrEz1QcRa/XwmNb2xDNxS6UaISzE79UrGNY4uO5om0YQRRhZpmC3A0QUzyd4VF5dEKaar/9W5wXJ/PKSw8l0/KJaNVNGhqLux0mlfEaAhRnzaFkru/JuIGY79hasyJl5l1ANEx9eByRAaLCq8HcQvm7yKva5/7zwHJi/zMVc25sQvmV/owUbhVFifRfp3azkSj2826kRqJDwc2bXJHRGs7yVCeNuRdoSbAZuve9hUud46MjqOr2/MQ/ml7LQXtufbaWBB0gtheVE12l5ceKps5ejEa6mu2wQ} encryption_applied: 1 is_shared: type_: 6这份文件同时揭示了 Joplin E2EE 的两层信息暴露面以明文保留的“同步关键字段”id、note_id、tag_id、updated_time、type_。同步引擎必须依赖这些字段完成增量传输、冲突检测和关系建立因此它们不能被加密被加密的内容created_time、user_created_time、user_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};可以手工拆解本文件的密文头JED0100002205c24138199f5b403fa3e9b8b4f22685c5JED01标识符 模板版本000022元数据区长度 34 字节05加密方法 5即EncryptionMethod.SJCL1ac24138199f5b403fa3e9b8b4f22685c532 字节十六进制主密钥 ID指向同目录下的主密钥记录 c24138199f5b403fa3e9b8b4f22685c5.md。解密时 decodeHeaderSource_ 会先读取 5 字节调用isValidHeaderIdentifier校验JED\d\d格式见 EncryptionService.ts再读 6 字节长度、按模板解析出encryptionMethod与masterKeyId随后从已加载的主密钥中取出明文密钥对负载解密。三、SJCL 负载与 EncryptionMethod 演进密文头之后的{iv:...,v:1,...}是 SJCLStanford JavaScript Crypto Library的 JSON 密文格式。本快照的负载参数如下参数值说明v1SJCL 内部格式版本iter101PBKDF2 密钥派生迭代次数SJCL 强制要求 100ks128AES 密钥长度 128 位ts64GCM/CCM 认证标签长度 64 位modeccmAES-CCM 认证加密模式adata空未使用关联数据cipheraes加密算法saltO2duAuTVjV4PBKDF2 随机盐ctbase64密文本体iter: 101与ks: 128正好对应 EncryptionService.ts 中EncryptionMethod.SJCL1a的实现因为主密钥本身已经过密钥派生保护正文无需再做高开销迭代101 次即可在移动端保持快速解密同时正文加密前先做escape()处理以规避 SJCL 只接受合法 UTF-8 导致包含非法字节的笔记解密失败的问题修复 issue #2591。EncryptionMethod枚举完整定义于 EncryptionService.ts其演进脉络直接写进了代码注释枚举值状态关键参数SJCL1已弃用OCB2 模式不再安全2020-01-23AES-128/OCB2, iter1000SJCL22已弃用曾用于主密钥AES-256/OCB2, iter10000, 带 SHA-256 校验和SJCL33保留兼容AES-128/CCM, iter1000SJCL44曾用于主密钥本快照的主密钥即此方法AES-256/CCM, iter10000SJCL1a5正文默认方法本快照使用AES-128/CCM, iter101, 带 escapeCustom6自定义处理器扩展点由EncryptionCustomHandler提供SJCL1b7现行正文方法2023-06-10 起AES-256/CCM, iter101KeyV18现行主密钥方法AES-256-GCM PBKDF2SHA-512 派生, keyLength32, iterationCount220000FileV19现行文件/资源加密方法AES-256-GCM, 内容按 base64 处理StringV110现行字符串正文方法AES-256-GCM, 内容按 utf16le 处理快照中的05SJCL1a说明这套快照生成于 2020-03 之后、2023-06 之前与文件头部的updated_time: 2020-07-25T10:37:00.415Z完全吻合。而主密钥记录 c24138199f5b403fa3e9b8b4f22685c5.md 中encryption_method: 4、iter: 10000、mode: ccm正是SJCL4的签名——同一套快照里正文与主密钥使用不同世代的加密方法是 Joplin E2EE 版本兼容设计的典型体现。3.1 主密钥的加载与明文缓存主密钥并不直接参与正文加解密而是以“密文主密钥 内存明文缓存”的方式管理。核心逻辑在 EncryptionService.loadMasterKeyencryptedMasterKeys_只记录主密钥的decrypt回调与updated_time不持有明文decryptedMasterKeys_密码校验成功后缓存{ plainText, updatedTime }并以updated_time参与 isMasterKeyLoaded 的新鲜度判断主动/惰性加载makeActivetrue时立即解密并设为活动主密钥否则仅登记回调待首次使用loadedMasterKey时才执行解密。由于正文解密依赖loadedMasterKey(masterKeyId).plainText见 masterKeyPlainText_如果对应的主密钥未加载会抛出masterKeyNotLoaded错误并由 BaseItem.encrypt 捕获后派发MASTERKEY_ADD_NOT_LOADED事件提示用户输入主密钥密码。四、字段级加密哪些字段保留明文为什么对照同目录下的加密笔记 1241f69ccbf54c0188b2f4a02e862b40.md可以归纳出两条规律明文保留的字段id、parent_id、note_id、tag_id、updated_time、type_等其余业务字段标题、正文、创建时间、来源 URL 等全部进入密文。这个白名单直接由 BaseItem.encrypt 中的keepKeys数组定义// List of keys that wont 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通过createTestData(testData)构造固定的测试数据含文件夹、带附件的笔记、标签等见 syncTargetUtils.ts若生成e2ee快照则调用setEncryptionEnabled(true)并loadEncryptionMasterKey()加载测试主密钥执行一次完整同步把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 版快照升级到 n1 版。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解密全部条目 → 断言测试数据完整无缺。checkTestDatasyncTargetUtils.ts会逐条加载文件夹、笔记、资源与标签并校验关联关系从而证明“同步版本升级不破坏任何已加密数据”。这就是为什么快照中会出现明文updated_time——迁移测试需要这些字段来判断数据是否在升级后被意外改写。六、给开发者的验证与调试建议手工验证密文头任何 JED 密文都可以用JED01 6 字节长度 2 字节方法 32 字节主密钥 ID 的规则手工解析无需先解密负载用测试密码复现解密迁移测试固定使用密码123456加载主密钥见 synchronizer_MigrationHandler.test.ts对快照调试时可沿用追踪加解密调用链正文加密从 BaseItem.encrypt 进入EncryptionService.encryptStringEncryptionService.ts其核心是encryptAbstract_EncryptionService.ts——先写 JED 头再按chunkSize分块加密、每块前置 6 字节十六进制块长解密则按块长循环读取天然支持流式处理大文件关注分块大小不同方法的分块大小定义在 chunkSizeSJCL 系列与 KeyV1 为 5000 字节、FileV1 为 131072 字节128KB、StringV1 为 65536 字节64KB。代码注释特别说明移动端解密性能随块大小呈指数恶化50KB≈1000ms5KB≈10ms因此文本数据刻意使用小块兼容旧快照仓库保留了多代加密方法含已弃用的 OCB2 模式任何新方法上线都必须保留旧方法的解密分支这解释了 EncryptionMethod 枚举只增不减的原因。七、总结一份看似普通的快照文件6db42e4e9c0b43ed891269d8a0508c76.md完整呈现了 Joplin E2EE 的核心设计JED01头携带加密方法与主密钥 IDSJCL/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),仅供参考

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价