
Joplin 同步目标快照与 E2EE 加密数据格式深度解析从syncTargetSnapshots/3/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 在端到端加密E2EE同步中会把笔记、文件夹、标签等所有同步项加密后写入同步目标。本文以仓库内测试夹具文件 packages/app-cli/tests/support/syncTargetSnapshots/3/e2ee/4a754e4afb6147d1a70114596d02184f.md 为切入点完整讲解 Joplin 同步目标快照的目录结构、加密条目Item的磁盘格式JED 头 SJCL 密文、快照的生成工具以及它们如何支撑同步版本迁移测试。读完本文你将掌握如何从一份加密快照文件中还原出同步项的真实元数据结构并理解 Joplin 同步版本迁移Migration的完整工作流。一、什么是同步目标快照Sync Target Snapshot在 Joplin 的测试体系中同步目标快照是指把某个历史版本的同步目标Sync Target的完整磁盘内容原样保存下来的静态文件集合。它们存放在packages/app-cli/tests/support/syncTargetSnapshots/ ├── 1/ │ ├── e2ee/ # 启用端到端加密的 v1 快照 │ └── normal/ # 未加密的 v1 快照 ├── 2/ │ ├── e2ee/ │ └── normal/ └── 3/ ├── e2ee/ └── normal/顶层数字1/2/3表示同步目标版本号syncVersion对应 Joplin 历史上引入的同步格式演进每个版本下分normal明文与e2ee加密两类快照e2ee目录下的每个.md文件对应同步目标中的一个加密同步项另有locks/锁目录与info.json同步目标元信息。这些快照被 synchronizer_MigrationHandler.test.ts 用作历史遗留数据测试新版同步器能否把旧版本数据无损升级到新版本。测试文件头部注释说明了快照的创建方式在 test-utils 中把syncTargetName_设为filesystem再执行node tests/support/createSyncTargetSnapshot.js normal node tests/support/createSyncTargetSnapshot.js e2ee而本文关注的3/e2ee/4a754e4afb6147d1a70114596d02184f.md正是 v3 加密快照中一个典型的笔记-标签关联项NoteTag。二、目标文件逐字段解析一份加密的 NoteTag 同步项该文件完整内容如下加密字段已省略密文细节id: 4a754e4afb6147d1a70114596d02184f note_id: 6865d0c2562e4d8ba88464e75006d443 tag_id: 11e9256883664948970fc5f78d0556bb created_time: updated_time: 2021-08-07T17:03:37.152Z user_created_time: user_updated_time: encryption_cipher_text: JED01000022051f3b6b71948c4f5d909d1af6588c78bb0002d8{...} encryption_applied: 1 is_shared: type_: 6结合仓库中同类快照文件如 6865d0c2562e4d8ba88464e75006d443.md 为type_: 1的笔记、376c1a3fe5ce4fc885e344b52b9f37b8.md 为type_: 2的文件夹、11e9256883664948970fc5f78d0556bb.md 为type_: 5的标签可以确定字段含义id同步项自身的主键UUID32 位十六进制note_id/tag_idNoteTag 关联的两端笔记6865d0c2...与标签11e92568...type_: 6数据库模型类型 ID1笔记2文件夹5标签6笔记-标签关联NoteTagupdated_time该项最近更新时间为 2021-08-07T17:03:37.152Zencryption_applied: 1标记该项已应用 E2EE 加密encryption_cipher_text加密后的正文即 JED 头 密文快照目录中还有1692f8857934461d8c24c8bf68c3fedf.md同样是note_id: 6865d0c2...的 NoteTag但tag_id指向另一个标签07cd0925...。这与 syncTargetUtils.ts 中定义的标准测试数据集一致——笔记subFolder2/note1被同时打上了tag1、tag2两个标签每条关联在同步目标里都对应一个独立的加密 NoteTag 文件。三、E2EE 密文格式详解JED 头 SJCL 载荷encryption_cipher_text字段并非普通 Base64而是一个结构化字符串其骨架为JED01 000022 05 1f3b6b71948c4f5d909d1af6588c78bb 0002d8 {JSON} │ │ │ └─ 主密钥 ID32 位十六进制 │ │ └──── encryptionMethod2 位十六进制 │ └────────── 元数据区长度6 位十六进制22 字节 └──────────────── 标识符 JED 头版本号 01该格式的编码实现在 EncryptionService.ts 的encodeHeader_方法第 775-784 行public encodeHeader_(header: { encryptionMethod: number; masterKeyId: string }) { 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}; }头部模板在headerTemplates_中定义第 80-86 行版本 1 的字段为[encryptionMethod(2B int), masterKeyId(32B hex)]。解码时先读 5 字节标识符用正则/JED\d\d/校验isValidHeaderIdentifier第 21-25 行再读 6 位十六进制的元数据长度随后按模板逐字段解析。SJCL 密文载荷JED 头之后紧跟密文段本文件中为{iv:/eUAJAOvToqE2IUjYsqAMw,v:1,iter:101,ks:128,ts:64, mode:ccm,adata:,cipher:aes,salt:tVgmTCWSasM, ct:CtDJBXr0lQRwJcVZk3Wq...}这是 Stanford JavaScript Crypto LibrarySJCL的标准 JSON 序列化格式各参数含义参数取值含义v1SJCL JSON 格式版本iter101PBKDF2 口令派生迭代次数master key 派生时的实际迭代数记录在info.json为 10000此处是数据加密派生参数ks128AES 密钥长度bitts64CCM 认证标签长度bitmodeccmAES-CCM 认证加密模式cipheraes对称加密算法salt16 字节 Base64口令派生盐值iv16 字节 Base64初始化向量随机生成ctBase64密文含认证标签adata空附加认证数据EncryptionMethod枚举第 38-49 行定义了 1~10 共 10 种加密方法SJCL 系列、Custom、KeyV1、FileV1、StringV1并据此决定分块大小文本类条目默认 64KB/块StringV1、文件类 128KB/块FileV1小分块是为了兼顾移动端解密性能源码注释给出了 Android 模拟器上 50KB→1000ms、5KB→10ms 的实测对比。四、info.json同步目标版本的身份证与加密条目文件同级的info.json记录了整个同步目标的加密状态与主密钥信息3/e2ee/info.json 的内容为{version:3,e2ee:{value:true,updatedTime:1628355817270}, activeMasterKeyId:{value:1f3b6b71948c4f5d909d1af6588c78bb,updatedTime:1628355817333}, masterKeys:[{checksum:,encryption_method:4,content:{...},...}]}其结构对应 syncInfoUtils.ts 中的SyncInfo类version同步目标格式版本号此处为 3e2ee布尔值 时间戳标记该目标是否启用端到端加密activeMasterKeyId当前激活的主密钥 IDmasterKeys主密钥数组。其中encryption_method: 4对应EncryptionMethod.SJCL3content字段是主密钥自身被口令加密后的 SJCL 密文迭代 10000 次、AES-CCMchecksum为空表示新格式主密钥不依赖校验和见 EncryptionService.test.ts 中 should not require a checksum for new master keys 用例。SyncInfo还承载ppk公/私钥对用于共享笔记、appMinVersion要求客户端最低版本、revisionServiceEnabled等字段并通过mergeSyncInfos在本地与远端之间按时间戳合并。五、快照的生成与部署工具快照的创建、校验与部署逻辑全部集中在 syncTargetUtils.tscreateTestData(data)递归遍历测试数据结构凡名称含folder的节点创建文件夹其余创建笔记resource: true时调用shim.attachFileToNote附加图片tags数组通过Tag.addNoteTagByTitle打标签main(syncTargetType)初始化数据库与同步器 → 创建测试数据 → 若为e2ee则执行setEncryptionEnabled(true)并加载主密钥 → 启动一次完整同步 → 把同步目录整体复制到syncTargetSnapshots/syncVersion/type/deploySyncTargetSnapshot(syncTargetType, syncVersion)测试时清空同步目录把snapshotBaseDir/syncVersion/type整目录复制为当前同步目标checkTestData(data)反向校验——按标题加载文件夹/笔记从笔记正文提取资源图片 URL 并加载资源逐一验证标签关联存在。测试数据testData定义了 3 个文件夹、5 条笔记、2 个标签、2 个附带图片资源的笔记photo.jpg。本文目标文件正是subFolder2/note1note_id6865d0c2...与tag1tag_id11e92568...之间的关联记录。六、快照在同步版本迁移测试中的作用6.1 迁移测试主流程synchronizer_MigrationHandler.test.ts 的核心逻辑是把版本 n 的快照升级到 n1async function testMigrationE2EE(migrationVersion: number, maxSyncVersion: number) { await deploySyncTargetSnapshot(e2ee, migrationVersion - 1); // 部署旧版加密快照 Setting.setConstant(syncVersion, migrationVersion); await migrationHandler().upgrade(migrationVersion); // 执行迁移 const newInfo await fetchSyncInfo(fileApi()); expect(newInfo.version).toBe(migrationVersion); // 校验版本已提升 await migrationTests[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)); }针对 E2EE 快照测试还验证了一个重要场景切换到第二个客户端后未解密前checkTestData应当抛错数据仍是密文输入口令并启动decryptionWorker解密后才能读取——这从测试层面印证了 Joplin离开主密钥即无法读取数据的 E2EE 设计。6.2 迁移的具体动作MigrationHandlerMigrationHandler.ts 是迁移的执行者其关键逻辑版本判定fetchSyncTargetInfo()读取info.json缺失时回退读取旧格式的.sync/version.txt存在则视为 v1两者都缺失则视为空目标v0兼容性检查checkCanSync()对比远端版本与客户端支持的syncVersion——远端更高抛outdatedClient请升级客户端更低抛outdatedSyncTarget请升级同步目标锁保护upgrade()先为 v0/v1 目标创建locks/与temp/目录然后获取排他锁LockType.Exclusive30 秒超时并启动自动续锁刷新整个迁移过程处于加锁状态防止多客户端并发写入逐步迁移从syncTargetInfo.version 1开始逐个执行migrations[]数组migration1/2/3位于packages/lib/services/synchronizer/migrations/v1、v2 迁移完成后写回info.json更新版本号。迁移测试对 v2/v3 的断言包括同步目标根目录必须存在.resource、locks、temp三个目录与info.json文件且旧版客户端兼容文件.sync/version.txt内容保持为2。6.3 普通normal与加密e2ee快照的差异对比3/normal/与3/e2ee/目录可以发现normal 快照中每个.md文件以明文形式直接包含title、body、parent_id等业务字段e2ee 快照中所有条目包括 NoteTag 这种关联表记录的业务字段全部消失只保留id、type_、updated_time等非敏感元数据正文被替换为encryption_cipher_textencryption_applied: 1e2ee 快照额外携带info.json中的主密钥信息供测试解密使用测试口令固定为123456。这正是 Joplin E2EE 的核心承诺在文件系统层面的直接体现同步目标上不落任何明文正文。七、如何查看与复现要验证本文对快照格式的分析可以对照密文头解析将encryption_cipher_text的前 5 字符与正则/JED\d\d/比对再按6 位十六进制长度 2 位十六进制加密方法 32 位十六进制主密钥 ID手工拆解头部剩余部分即 SJCL JSON阅读编码实现EncryptionService.ts 第 775-832 行的encodeHeader_/decodeHeaderBytes_与第 21-25 行的isValidHeaderIdentifier运行迁移测试在仓库根目录执行相关 Jest 测试需先安装依赖观察should apply migration 3 E2EE等用例如何部署快照、升级版本并解密校验数据。需要注意的适用前提快照中加密条目由历史版本2021 年、同步版本 3、StringV1文本加密生成迭代参数、加密方法与当前版本实现细节可能存在差异但 JED 头部协议与同步版本迁移框架在仓库当前代码中依然保持兼容这也是快照测试体系至今有效的原因。八、小结一份看似不起眼的4a754e4afb6147d1a70114596d02184f.md串联起了 Joplin 同步体系中最重要的几条链路同步项模型NoteTag、type_类型体系、E2EE 加密格式JED 头 SJCL 载荷、同步目标元信息info.json与SyncInfo、快照生成工具syncTargetUtils.ts以及同步版本迁移机制MigrationHandler与配套测试。理解这一条链路就理解了 Joplin本地数据库加密、同步目标零明文、跨版本无损升级的端到端加密同步全貌。【免费下载链接】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),仅供参考