ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Joplin 端到端加密(E2EE)深度解析:从 syncTargetSnapshots 快照文件解读 JED01 密文格式与同步迁移测试

Joplin 端到端加密(E2EE)深度解析:从 syncTargetSnapshots 快照文件解读 JED01 密文格式与同步迁移测试 Joplin 端到端加密E2EE深度解析从 syncTargetSnapshots 快照文件解读 JED01 密文格式与同步迁移测试【免费下载链接】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 仓库中的同步目标快照packages/app-cli/tests/support/syncTargetSnapshots为切入点逐字段拆解一条被端到端加密E2EE保护的真实笔记快照文件并结合加密服务、快照生成脚本与迁移测试源码完整还原 Joplin 的「笔记序列化格式 → JED01 加密载荷 → SJCL 密文容器」整条链路。读完本文你将能够读懂任意一条 JED01 加密数据的内部结构、理解 E2EE 快照如何被生成与消费并掌握 Joplin 同步版本迁移测试的运作原理。快照文件在 Joplin 测试体系中的定位Joplin 把「同步目标sync target在不同同步版本syncVersion下应有的数据形态」固化为测试快照存放于 packages/app-cli/tests/support/syncTargetSnapshots其目录结构为syncTargetSnapshots/ ├── 1/ │ ├── e2ee/ # 开启端到端加密后的同步目标快照 │ └── normal/ # 未加密的同步目标快照 ├── 2/ │ ├── e2ee/ │ └── normal/ └── 3/ ├── e2ee/ └── normal/每个版本目录下除若干.md数据文件外还包含info.json记录同步目标元信息与locks/、.sync/等同步运行时目录。本文聚焦的快照文件packages/app-cli/tests/support/syncTargetSnapshots/1/e2ee/f0aabe7de88d47c2ad4ce26d8d5ce70c.md正是syncVersion 1、开启 E2EE时同步目标上的一个真实笔记文件。这些快照有两大用途见 packages/lib/services/synchronizer/synchronizer_MigrationHandler.test.ts 顶部注释回归验证验证当前代码仍能正确读写历史版本的同步目标迁移测试将旧版本快照「升级」到新版本验证同步版本迁移没有破坏任何数据。由于普通文件如未加密笔记内容可直接阅读而 E2EE 快照中的数据全部是密文因此解析该快照文件实质上就是解析 Joplin 的端到端加密格式本身。笔记序列化文件元数据字段逐一解读快照中的.md文件并非传统意义的 Markdown 文档而是Joplin 笔记的序列化表示正文JEX 格式中笔记的body在最顶部其后是完整的元数据字段。本快照文件的字段如下id: f0aabe7de88d47c2ad4ce26d8d5ce70c parent_id: 77c94e3da5d44db28eb485162d1b3f41 created_time: updated_time: 2020-07-25T10:37:00.288Z 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: JED0100002205c24138199f5b403fa3e9b8b4f22685c5... encryption_applied: 1 markup_language: is_shared: type_: 1其中关键字段的含义字段值本例含义idf0aabe7de88d47c2ad4ce26d8d5ce70c32 位十六进制全局唯一 IDparent_id77c94e3da5d44db28eb485162d1b3f41父条目 ID对照同目录77c94e3da5d44db28eb485162d1b3f41.md可知其type_: 2即一个文件夹Folderupdated_time2020-07-25T10:37:00.288Z最后更新时间UTC快照固定于 2020 年 7 月 25 日生成is_conflict空是否为冲突副本encryption_cipher_textJED01...加密后的正文与完整元数据见下文encryption_applied1标记本条已应用加密type_1条目类型1 Note2 Folder3 Setting9 MasterKey 等可以注意到凡是敏感字段正文、标题、标签、地理位置、TODO 信息等在加密模式下全部被置空真实内容整体收拢进encryption_cipher_text。这与未加密快照形成鲜明对比——例如 packages/app-cli/tests/support/syncTargetSnapshots/1/normal/922bf9d7bfbd4493b54202b25fcb9305.md 中同类的笔记note5直接以明文呈现正文与元数据note5 [![photo.jpg](https://gitcode.com/GitHub_Trending/jo/joplin/blob/71d4b09d48d78d1dc71d1d04dcea2f64d3c0aaee/packages/app-cli/tests/support/syncTargetSnapshots/1/normal/.resource/b50d9136b45e44fd9d40ef1ac5e7250a?utm_sourcegitcode_repo_files)](https://link.gitcode.com/i/7825844a4dc4b7d5f1f8dbb0e56f0807) id: 922bf9d7bfbd4493b54202b25fcb9305 parent_id: ef9861f30a724a1491f08656764d64c5 ... encryption_cipher_text: encryption_applied: 0 ... type_: 1两条快照的id规律一致文件夹77c94e...下挂载笔记说明normal与e2ee快照由同一份测试数据生成只是后者额外开启了加密——这正是 E2EE 前后数据形态最直观的对照实验。JED01 载荷加密数据的一级容器encryption_cipher_text的值以JED01开头这是 Joplin 加密数据的统一标识头。其编解码逻辑在 packages/lib/services/e2ee/EncryptionService.ts 中实现。JED01头部的完整结构由encodeHeader_生成、decodeHeaderBytes_解析JED01 # 5 字节固定标识符JED 0101 为模板版本号 000022 # 6 位十六进制后续元数据的总长度34 字节 05 # 2 位十六进制encryptionMethod本例为 5 SJCL1a c24138199f5b403fa3e9b8b4f22685c5 # 32 字节十六进制masterKeyId对应到本快照的密文头JED01 000022 05 c24138199f5b403fa3e9b8b4f22685c5字段模板定义于 EncryptionService.tsheaderTemplates_ { // Template version 1 1: { // Fields are defined as [name, valueSize, valueType] fields: [[encryptionMethod, 2, int], [masterKeyId, 32, hex]], }, };头部后面的整段数据是 SJCL 加密后的密文。头部中的masterKeyId与仓库中主密钥快照 packages/app-cli/tests/support/syncTargetSnapshots/1/e2ee/c24138199f5b403fa3e9b8b4f22685c5.md 完全一致——该文件type_: 9MasterKey记录着加密后的主密钥内容id: c24138199f5b403fa3e9b8b4f22685c5 ... encryption_method: 4 checksum: content: {iv:qukPmj886S4Y8nyT9z/WFA,v:1,iter:10000,ks:256,ts:64,mode:ccm,adata:,cipher:aes,salt:FTTpwryRSrM,ct:ShoeEpKzYWDzkZa2k0QRX2FN8ucIedwm...} type_: 9注意此处encryption_method: 4对应EncryptionMethod.SJCL4iter: 10000, ks: 256, ts: 64, mode: ccm, cipher: aes即主密钥使用高迭代次数的 AES-256-CCM 保护。校验加密标识isValidHeaderIdentifierEncryptionService.ts 通过isValidHeaderIdentifier判断一段数据是否已加密export function isValidHeaderIdentifier(id: string, ignoreTooLongLength false) { if (!id) return false; if (!ignoreTooLongLength id.length ! 5) return false; return /JED\d\d/.test(id); }而 itemIsEncrypted 用「encryption_applied为真 头部标识合法」双重条件判定条目是否已加密——与快照文件中encryption_applied: 1的字段语义严格对应。SJCL 密文容器AES-CCM 与密钥派生参数JED01头部之后的主体是SJCLStanford JavaScript Crypto LibraryJSON 格式的密文。Joplin 将 SJCL 库内置于 packages/lib/vendor/sjcl.jsReact Native 下使用 packages/lib/vendor/sjcl-rn.js并在EncryptionService.encrypt()中调用sjcl.json.encrypt(...)。SJCL 载荷包含如下字段以本快照为例{ iv: OVD9Kpe4iZRhEgnooBGUjA, // 16 字节随机初始化向量Base64 v: 1, // SJCL JSON 版本 iter: 101, // PBKDF2 迭代次数 ks: 128, // AES 密钥长度位 ts: 64, // 认证标签长度位 mode: ccm, // AES-CCM 认证加密模式 adata:, // 关联数据留空 cipher:aes, // 底层分组算法 salt: O2duAuTVjV4, // 随机盐值Base64 ct: pxkDBw02CTRMxtwZ7m3k9ovoWh49gg... // 密文Base64 }本例头部05表明加密方法为SJCL1a。查看 EncryptionService.ts 中 SJCL1a 的加密参数与上表逐项吻合iter: 101、ks: 128、ts: 64、mode: ccm、cipher: aes。加密方法族与安全演进EncryptionMethod枚举定义于 EncryptionService.ts各方法差异可从源码注释与参数配置中确认方法值用途/要点关键参数SJCL1早期方法因使用 OCB2 模式已于 2020-01-23 弃用iter: 1000, ks: 128, mode: ocb2SJCL1a52020-03-06 起用于笔记等字符串加密改用 CCM 模式并转义非 UTF-8 数据iter: 101, ks: 128, mode: ccmSJCL1b72023-06-10 起替代 SJCL1a升级为 AES-256iter: 101, ks: 256, mode: ccmSJCL22曾用于加密主密钥OCB2 模式已弃用iter: 10000, ks: 256, mode: ocb2SJCL44主密钥加密本仓库主密钥快照即此方法iter: 10000, ks: 256, mode: ccmKeyV18基于原生加密库node:crypto / react-native-quick-crypto的主密钥加密PBKDF2 迭代 220000OWASP 建议值AES-256-GCM, digest: sha512, keyLength: 32FileV19文件内容加密Base64 解码后再加密以降低体积块大小 128 KBAES-256-GCM, iterationCount: 3StringV110字符串加密utf16le 编码当前默认方法块大小 64 KBAES-256-GCM, iterationCount: 3Custom6自定义加密处理器EncryptionCustomHandler由外部 handler 实现快照生成于 2020 年故使用当时的 SJCL1a / SJCL42024 年 8 月之后新写入的数据则使用基于 AES-256-GCM 的 KeyV1 / StringV1 / FileV1 系列见 EncryptionService.ts 注释。分块加密块大小与头部追加EncryptionService对长内容采用分块加密encryptAbstract_按chunkSize()EncryptionService.ts读取数据每块独立加密后以「6 位十六进制长度前缀 密文」的形式追加写入块大小随方法变化SJCL 系列 5000 字节、StringV1 65536 字节、FileV1 131072 字节。移动端每次加密后shim.waitForFrame()让出帧避免界面卡顿。解密时decryptAbstract_依据头部记录的encryptionMethod与masterKeyId用对应主密钥逐块还原。E2EE 快照是如何生成的E2EE 快照并非手工构造而是由测试工具脚本生成。核心逻辑在 packages/lib/testing/syncTargetUtils.ts 的main()与createTestData()export const testData { folder1: { subFolder1: {}, subFolder2: { note1: { resource: true, tags: [tag1] }, note2: {}, }, note3: { tags: [tag1, tag2] }, note4: { tags: [tag2] }, }, folder2: {}, folder3: { note5: { resource: true, tags: [tag2] } }, };生成流程syncTargetUtils.tssetupDatabaseAndSynchronizer(1)switchClient(1)初始化测试环境createTestData(testData)递归创建文件夹、笔记并通过shim.attachFileToNote为resource: true的笔记附加photo.jpg图片资源、通过Tag.addNoteTagByTitle打标签若为 e2ee 快照则setEncryptionEnabled(true)并loadEncryptionMasterKey()生成主密钥synchronizerStart()synchronizer().start()执行首次同步将数据推送到测试同步目录将syncDir整体拷贝为syncTargetSnapshots/{syncVersion}/{normal|e2ee}。由此可推断快照文件包括本文的加密笔记实际上是同步目标目录的真实持久化形态即“同步后云端/网盘上存放的文件长什么样”的黄金样本。main(syncTargetType)中的validSyncTargetTypes明确限定[normal, e2ee]与快照目录命名一一对应。迁移测试快照如何被消费与验证快照最核心的消费者是synchronizer_MigrationHandler.test.ts。该测试的思路是以版本 n 的快照为起点升级到 n1验证数据完好。明文场景testMigrationsynchronizer_MigrationHandler.test.ts先部署normal快照await deploySyncTargetSnapshot(normal, migrationVersion - 1); const info await fetchSyncInfo(fileApi()); expect(info.version).toBe(migrationVersion - 1); // 升级到新版本 Setting.setConstant(syncVersion, migrationVersion); await migrationHandler().upgrade(migrationVersion); // 验证版本已升级 const newInfo await fetchSyncInfo(fileApi()); expect(newInfo.version).toBe(migrationVersion);随后检查迁移后的同步目标目录结构.resource、locks、temp、info.json、.sync/version.txt等再调用synchronizer().start()同步最后用checkTestData(testData)逐条校验——每个文件夹、笔记、附件、标签都必须在同步后仍然存在且关联正确syncTargetUtils.ts。加密场景testMigrationE2EEsynchronizer_MigrationHandler.test.ts流程相同但部署的是e2ee快照——也就是本文这条加密笔记所在的目录。迁移完成后checkTestData同样校验数据区别在于所有条目需经解密才能读取。由于快照正文与元数据都是密文测试链路中必然涉及主密钥的加载与解密。相关配套在migrationTests之后的流程与loadMasterKeysFromSettings../e2ee/utils等辅助函数协同验证「加密数据经版本迁移后依然可解密、内容无变化」。deploySyncTargetSnapshot的实现syncTargetUtils.ts非常简单export async function deploySyncTargetSnapshot(syncTargetType: string, syncVersion: number) { const sourceDir ${snapshotBaseDir}/${syncVersion}/${syncTargetType}; await fs.remove(syncDir); await fs.copy(sourceDir, syncDir); }即把选定版本的快照整体铺到同步目录中模拟「一个旧版本客户端留下的同步目标」。从快照反推完整 E2EE 读写链路综合以上源码证据可以串出 Joplin E2EE 的完整读写链路以本文快照文件为例写入侧加密Note.save()之后同步/解密工作流判定encryption_applied为真时将条目的正文与全部敏感元数据序列化为字符串 → 读取当前激活主密钥activeMasterKeyId()EncryptionService.ts→encryptAbstract_写入JED01头部并分块加密 → 结果写入encryption_cipher_text字段。读取侧解密itemIsEncrypted()校验标识 →decryptString()读取头部解析出encryptionMethod与masterKeyId→ 用对应主密钥逐块解密 → 还原序列化文本并解析回元数据与正文。DecryptionWorker见 packages/lib/services/DecryptionWorker.test.ts负责在同步后批量执行这一过程。密钥侧主密钥本身以type_: 9的条目独立存储如快照中的c24138199f5b403fa3e9b8b4f22685c5.md其content用更高迭代次数的 SJCL4 / KeyV1 加密loadMasterKey通过用户密码解锁后缓存在decryptedMasterKeys_映射中EncryptionService.ts。总结与延伸阅读快照即规范syncTargetSnapshots/{1,2,3}/{normal,e2ee}用真实文件形态固化了 Joplin 各同步版本的目标数据结构加密与非加密双版本并存本身就是「E2EE 前后对比」的教科书级样本。JED01 是解析入口任何 Joplin 加密数据都以JED01 元数据长度 方法号 主密钥 ID 开头其后才是 SJCL 密文容器看懂头部即看懂加密数据的“路由信息”。加密方法可演进从 OCB2 的 SJCL → CCM 的 SJCL1a/SJCL1b → 原生 AES-256-GCM 的 StringV1/FileV1/KeyV1头部中的方法号保证了历史数据可被兼容解密这也是快照中保留旧格式样本的意义所在。若希望继续深入可重点阅读以下文件加密核心实现packages/lib/services/e2ee/EncryptionService.ts快照生成与校验packages/lib/testing/syncTargetUtils.ts同步版本迁移测试packages/lib/services/synchronizer/synchronizer_MigrationHandler.test.ts加密服务单元测试packages/lib/services/e2ee/EncryptionService.test.ts解密工作流测试packages/lib/services/DecryptionWorker.test.ts同步器 E2EE 集成测试packages/lib/services/synchronizer/Synchronizer.e2ee.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),仅供参考
返回列表