
Joplin 端到端加密实战从 E2EE 同步快照解密 JED 密文格式与加密链路【免费下载链接】joplinJoplin - the privacy-focused note taking app with sync capabilities for Windows, macOS, Linux, Android and iOS.项目地址: https://gitcode.com/GitHub_Trending/jo/joplinJoplin 是一款以隐私为核心、内置多端同步能力的开源笔记应用其端到端加密E2EE是保护笔记内容不被同步服务商读取的关键机制。本文以仓库测试快照目录packages/app-cli/tests/support/syncTargetSnapshots/2/e2ee/中一条真实加密笔记为例逐字节剖析 Joplin 的 JED 加密数据格式、SJCL 密文结构、Master Key 派生链路并结合 EncryptionService.ts 源码与同步快照测试机制帮助你从底层理解并能独立解读、验证 Joplin 的 E2EE 数据。一、从一个真实的 E2EE 快照文件说起在 Joplin 仓库中同步目标快照sync target snapshot用于测试同步目标从旧版本向新版本的迁移。快照按同步版本1/、2/、3/和是否加密normal/、e2ee/分类存放。本文讨论的文件packages/app-cli/tests/support/syncTargetSnapshots/2/e2ee/04c4e932fe3c4c4a9450c09208bd6c21.md这是一条笔记type_: 1其元数据字段与一条普通未加密笔记几乎一致但存在两个关键差异encryption_cipher_text与encryption_applied: 1。快照中同目录的3d675395b5cd4d1e9d7ca4f045f41493.md是一条文件夹type_: 2a1a0987e82cc400c90582492f814c23c.md则是一条主密钥记录type_: 9其encryption_method: 4表示使用了SJCL4加密主密钥内容它们共同构成了一套完整的 E2EE 同步目录。快照文件类型说明04c4e932fe3c4c4a9450c09208bd6c21.md笔记type_: 1正文被加密到encryption_cipher_text3d675395b5cd4d1e9d7ca4f045f41493.md文件夹type_: 2标题等字段被加密a1a0987e82cc400c90582492f814c23c.md主密钥type_: 9存放用密码派生的密钥加密的 Master Key也就是说Joplin 开启端到端加密后所有可加密条目笔记、文件夹、标签等的title、body等敏感字段都会被抽离并整体加密进encryption_cipher_text字段而id、parent_id、updated_time等同步所需的结构字段仍以明文保留——这正是 Joplin 能在加密状态下依然高效增量同步的基础。二、逐字节解读encryption_cipher_text密文结构以快照中笔记的密文开头为例JED0100002205a1a0987e82cc400c90582492f814c23c000470{...}结合 EncryptionService.ts 中encodeHeader_与decodeHeaderBytes_的实现可以将其拆解为三层结构1. JED 头部HeaderJED加密数据标识符3 字节用于快速判断数据是否真的被加密01头部模板版本号。目前headerTemplates_仅定义了版本1其字段为[[encryptionMethod, 2, int], [masterKeyId, 32, hex]]见 源码第 80-86 行000022后续加密元数据encryption metadata的十六进制字节长度05加密方法编号十六进制即EncryptionMethod.SJCL1a 5a1a0987e82cc400c90582492f814c23c用于加密本条数据的 Master Key ID32 位十六进制与快照中的主密钥记录 ID 完全一致。源码中校验逻辑要求header.masterKeyId.length 32否则抛出 Invalid master key ID size解密时若标识符不是JED则会抛出invalidIdentifier错误提示 Data is not actually encrypted?。这也意味着可以从密文头部直接判断某条数据是否加密、使用哪种加密方法、由哪把主密钥加密。2. SJCL JSON 密文对象头部之后是一段 JSON它由 Stanford JavaScript Crypto LibrarySJCL生成结构如下{ iv: gJXa88pt5ZzaYAlD4ZCSkA, v: 1, iter: 101, ks: 128, ts: 64, mode: ccm, adata: , cipher: aes, salt: Gyo7bQeqz2w, ct: 8jM7A4Kx9RfLOHz/uriiE3r5zz6... }对照 EncryptionService.ts 中 SJCL1a 的加密参数第 385-402 行各字段含义如下字段示例值含义v1SJCL JSON 格式版本iter101PBKDF2 密钥派生迭代次数。源码注释说明主密钥本身已经经过高强度密钥派生因此此处不需要额外高迭代101 次足以防止暴力破解同时又保证移动端解密速度SJCL 强制要求大于 100ks128AES 密钥长度位。本快照使用 SJCL1a128 位新版 SJCL1b 已升级为 256 位ks: 256源码注释引用 256-bit is the golden standardts64GCM/CCM 认证标签长度位modeccm认证加密模式。CCM 模式未受专利限制、兼容性更广早期 SJCL 使用 OCB2 模式因安全问题于 2020 年弃用cipheraes底层对称算法ivbase64每个数据块的初始化向量随机生成防止相同明文产生相同密文saltbase64PBKDF2 盐值随机生成防止彩虹表攻击ctbase64实际密文ciphertext3. 数据块Block布局JED 头部之后、SJCL 密文之外还存在一个面向大数据的分块机制encryptAbstract_源码第 580-615 行将源数据按固定块大小切分每块依次追加6 位十六进制块长度 密文解密端decryptAbstract_第 618-643 行先读 6 位长度再读对应字节数。块大小由chunkSize(method)决定SJCL 系列为 5000 字节、FileV1为 131072 字节128K、StringV1为 65536 字节64K。源码注释还记录了移动端的实测性能数据在 Android 7.1 模拟器上50KB 块解密约需 1000ms而 5KB 块仅约 10ms——块越小速度越快10 倍更小约快 100 倍因此 SJCL 方法选择了 5KB 小块以避免阻塞 UI。三、Master Key 与密钥派生链路E2EE 的核心是主密钥 数据密钥两层结构。快照中的a1a0987e82cc400c90582492f814c23c.md是主密钥记录它的content字段同样是 SJCL 密文encryption_method: 4即 SJCL4但参数不同iter: 10000、ks: 256——因为主密钥是全部数据的根必须用更高迭代次数10000 次 PBKDF2来抵抗离线暴力破解。完整链路对应 EncryptionService.ts 的encrypt/decrypt实现用户密码 → 主密钥loadMasterKey(model, getPassword)使用用户密码解密主密钥记录得到 256 字节随机十六进制字符串generateMasterKeyContent_中通过shim.randomBytes(256)生成密钥解密后缓存在decryptedMasterKeys_内存映射中从不落盘主密钥 → 数据密钥实际加密数据时encryptAbstract_通过masterKeyPlainText_(masterKeyId, options)取出主密钥明文作为 SJCLsjcl.json.encrypt(key, ...)的 key 直接加密每个数据块每条数据 → 密文块每条笔记/文件夹独立加密头部记录所用主密钥 IDiv、salt每块独立随机。EncryptionMethod枚举完整定义了全部加密方法源码第 38-49 行编号枚举用途/状态1SJCL已弃用OCB2 不安全2SJCL2已弃用曾用于加密主密钥3SJCL3保留兼容4SJCL4曾用于加密主密钥快照中主密钥即此方法5SJCL1a曾用于加密数据快照中笔记即此方法6Custom自定义加密处理器7SJCL1b2023 年升级为 AES-2568KeyV1当前默认主密钥加密方法AES-256-GCMPBKDF2 220000 次迭代9FileV1当前默认文件加密方法10StringV1当前默认字符串加密方法从源码看当前默认值分别为数据字符串EncryptionMethod.StringV1、文件EncryptionMethod.FileV1、主密钥EncryptionMethod.KeyV1第 74-76 行。KeyV1/FileV1/StringV1 是新一代基于原生加密库node:crypto / react-native-quick-crypto的方案使用 AES-256-GCM并额外引入 256 位随机盐生成独立数据密钥以避免 nonce 重用问题源码注释明确说明其中主密钥 PBKDF2 迭代次数于 2024 年 8 月按 OWASP 建议提升至 220000。四、这些快照文件是怎么来的同步快照生成机制快照不是手写的而是由测试工具生成的。核心逻辑在 packages/lib/testing/syncTargetUtils.tstestData定义了一套标准测试数据3 个文件夹、5 条笔记其中部分笔记附带图片资源resource: true和标签tags: [...]main(syncTargetType)依次执行setupDatabaseAndSynchronizer(1)→createTestData(testData)→ 若为e2ee则setEncryptionEnabled(true)并loadEncryptionMasterKey()→synchronizerStart()完成一次完整同步最后把同步目标目录整体复制到syncTargetSnapshots/${syncVersion}/${syncTargetType}/即快照。因此2/e2ee/目录本质上是一个同步版本 2、开启 E2EE 的同步目标在完成首次同步后的完整磁盘形态其中info.json记录了{version:2}locks/、temp/目录与.sync/version.txt等也一应俱全。五、快照的用途同步迁移与加密数据的回归验证快照目录的实际消费者是packages/lib/services/synchronizer/synchronizer_MigrationHandler.test.ts。该测试的核心思路源码注释是先取版本 n 的同步目标快照再将其升级到 n1。关键流程在testMigrationE2EE测试源码第 99-142 行deploySyncTargetSnapshot(e2ee, migrationVersion - 1)把快照整体复制到本地同步目录模拟一个旧版本客户端同步过的云端设置新的syncVersion调用migrationHandler().upgrade(...)执行迁移迁移逻辑在 packages/lib/services/synchronizer/migrations/ 下如migration2会创建locks、temp目录并写入.sync/version.txt同步后用MasterKey.all()取回主密钥通过encryption.passwordCache注入密码123456loadMasterKeysFromSettings加载主密钥再启动decryptionWorker().start()解密全部数据最后checkTestData(testData)逐项校验文件夹、笔记、资源、标签是否与加密前完全一致——任何一步解密失败或数据被改动都会让测试抛错。值得注意的测试细节在客户端 2场景中未解密前checkTestData必须抛错expectThrow解密后必须成功expectNotThrow以此验证 E2EE 数据在同步后仍处于加密状态、只有掌握密码的客户端才能读取。这正是 Joplin 服务端只存储密文设计的事实印证。六、加密数据在代码中的身份判定实际应用中如何区分一条数据是否已加密EncryptionService提供了两个判定入口源码第 838-849 行itemIsEncrypted(item)同时检查item.encryption_applied为真且encryption_cipher_text以JED\d\d开头isValidHeaderIdentifier正则fileIsEncrypted(path)读取文件前 5 个字符与JED标识符比对。其中isValidHeaderIdentifier定义在 packages/lib/services/e2ee/EncryptionService.ts 第 24 行return /JED\d\d/.test(id);这也解释了为何快照中所有加密条目的encryption_cipher_text都以JED010000...起始——头 5 个字符JED01恰好同时携带标识符与头部版本号解密器据此选择正确的头部模板。七、如何在本地自行验证这份快照如果你希望亲自动手验证上述解析仓库提供了现成的路径用测试直接跑 E2EE 迁移验证在packages/lib目录下运行npx jest services/synchronizer/synchronizer_MigrationHandler.test.ts其中的should apply migration 2 E2EE测试用例会完整走一遍部署快照 → 迁移 → 解密 → 校验数据流程生成新的快照参考syncTargetUtils.ts顶部注释在 test-utils 中将同步目标设为filesystem然后执行node tests/support/createSyncTargetSnapshot.js e2ee即可在本地重新生成一份当前同步版本的 E2EE 快照人工比对打开2/e2ee/目录将笔记密文头部中的 Master Key ID 与a1a0987e82cc400c90582492f814c23c.md的id比对再对照EncryptionMethod枚举解读05对应的加密方法即可复现本文第二节的完整拆解。总结通过一条真实的 sync target 快照我们完整还原了 Joplin 端到端加密的落地形态JED头 加密方法/主密钥元数据 SJCL或原生 AES-256-GCM密文块的存储格式、用户密码→主密钥→数据密钥的两层派生链路、以及快照驱动的同步迁移回归测试机制。理解这层数据格式无论是排查为什么某些数据未加密、分析同步冲突还是二次开发与 Joplin 加密数据打交道的工具都能提供直接的底层依据——核心实现均可回溯至 EncryptionService.ts、syncTargetUtils.ts 与 synchronizer_MigrationHandler.test.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),仅供参考