Joplin E2EE 同步快照深度解析:从一条加密 Note-Tag 关联记录看端到端加密数据格式与迁移测试机制
2026/9/10 21:18:36 网站建设 项目流程

Joplin E2EE 同步快照深度解析:从一条加密 Note-Tag 关联记录看端到端加密数据格式与迁移测试机制

【免费下载链接】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 仓库中的一条 E2EE 同步快照记录(packages/app-cli/tests/support/syncTargetSnapshots/2/e2ee/f58c1af04627410da55d9c771b28bece.md)为切入点,深入拆解 Joplin 端到端加密(E2EE)同步数据的落盘格式:包括快照目录的组织结构、JED加密文本的字段语义、主密钥文件与 AES-CCM 密文的参数细节,并结合仓库源码还原快照的生成、部署与迁移校验的完整链路。读完本文,你将能够读懂任意一条 Joplin 加密同步项文件,并理解 Joplin 如何用快照体系保障同步协议跨版本升级时的数据兼容性。

一、什么是同步目标快照(Sync Target Snapshot)

Joplin 的同步协议会随版本演进,而协议升级必须保证"旧数据可迁移、迁移后数据不被破坏"。为此,仓库维护了一套同步目标快照:即在某一特定协议版本下,用一个固定数据集真实执行一次同步后,把同步目标(远端存储)上的目录与文件原样保存下来,作为后续测试的基准输入。

快照根目录位于 packages/app-cli/tests/support/syncTargetSnapshots,其目录约定为:

syncTargetSnapshots/ ├── 1/ # 同步协议版本 1 │ ├── e2ee/ # 开启端到端加密的快照 │ └── normal/ # 未加密的快照 ├── 2/ # 同步协议版本 2(info.json 中 version: 2) │ ├── e2ee/ │ └── normal/ └── 3/ # 同步协议版本 3 ├── e2ee/ └── normal/

其中2/e2ee/info.json的内容为{"version":2},用于标识该快照对应的同步协议版本;locks/子目录保存同步锁文件。而数据本身以"一项一个 Markdown 文件"的方式存储,文件名即该项的id。每个文件与本地数据库中的一条记录一一对应。

二、逐字段解析一条加密同步项文件

本文主角f58c1af04627410da55d9c771b28bece.md是快照2/e2ee/中的一条记录,完整内容如下:

id: f58c1af04627410da55d9c771b28bece note_id: 04c4e932fe3c4c4a9450c09208bd6c21 tag_id: 8a17074d4ec24de7b5a1aa666f7d8b38 created_time: updated_time: 2020-07-25T10:55:20.803Z user_created_time: user_updated_time: encryption_cipher_text: JED0100002205a1a0987e82cc400c90582492f814c23c0002d8{"iv":"j4Au5F1MRKUgSevr56iisw==","v":1,"iter":101,"ks":128,"ts":64,"mode":"ccm","adata":"","cipher":"aes","salt":"Gyo7bQeqz2w=","ct":"HvRLpScsoN1juxw18ostkjkjcxO+VWaprzz15PzE+3LU0KfoPqo0g2GgVPpbmp9dNyuyv+akfj5u5/buMZipiyGteEOa5WxoJ16KJ9qIYjv+kxu9kcfteDhHP6gTz1Mc0DfQRlLfZ5EbcpYQwkcNvdF71t8JsH5QwqA27P/wk5TKZDM/gd641zL92tNViAM4dZws2FDeWvgb4xRU3L7tfSBVoR5DXBKPl5syNrr8m2prdolydWms+RZuQQnFWIMn0jIKucSx56YEwcmCsAdsOLpNd8/MLqHfUOafrCQYDd9QIWEbNz509wWoXiu/Cjl49B+xx0ACa5z/Ey0yBQAwibPLMBq2yAQgxo6SWG00reOkGKQmxYIkD7mQa87zUtsCRWPebFohDV2LfAbbPFScsqNsH2wNhVXJZJ4JcOKMUR1R4hx66P158wOn9VY1Rf3zyJujiqzAhGivdvMQ2qp1TyAoiA8ibPizZIEPh0oyYf7EoZf1mIO/hyFMfs31xSbfdfXhUn57BCgVkndA7NRT2xoEdqBgymLMp2z7ll7F8xjMEG+8wUlAWNXzNDjhzPza2D2s8Co9pqgs9g=="} encryption_applied: 1 is_shared: type_: 6

各字段的语义如下:

字段含义
idf58c1af04627410da55d9c771b28bece该项在同步目标中的唯一标识(也是文件名)
note_id/tag_id04c4e9.../8a1707...关联的两条实体 id,构成"笔记—标签"关系
created_time/updated_time空 /2020-07-25T10:55:20.803Z创建与更新时间(由同步器写入,快照抓取时部分字段为空)
encryption_cipher_textJED0100002205...加密后的内容主体,见下一节详解
encryption_applied1标记该字段已被加密
type_6实体类型编码,见下文类型映射

2.1type_字段的实体类型映射

type_是理解快照内容的关键索引。Joplin 在 BaseModel.ts 中通过ModelType枚举维护了类型常量,其中与本快照直接相关的是:

  • TYPE_NOTE = 1(笔记)
  • TYPE_FOLDER = 2(笔记本/文件夹)
  • TYPE_TAG = 5(标签)
  • TYPE_NOTE_TAG = 6(笔记-标签关联)

因此本文件的type_: 6表示它是一条笔记与标签的多对多关联记录,与note_idtag_id两个外键字段互相印证。同目录下的 8a17074d4ec24de7b5a1aa666f7d8b38.md(type_: 5)正是被关联的标签本体,同样以密文形式存储。

2.2 明文记录与加密记录的差异

对比同版本的normal/快照可以发现,加密模式下的同步项:

  1. 不直接保存titlebody等业务字段,而是将所有内容折叠进encryption_cipher_text
  2. 保留idnote_idtag_id建立索引所需的最小字段,使同步器无需解密即可完成差异比较;
  3. 附加encryption_applied: 1标记,便于客户端识别加密项并触发解密流程。

这种设计保证了 Joplin 在不解密的情况下也能高效地增量同步——只有真正读取内容时才需要解密。

三、JED加密文本格式:加密内容的核心载体

encryption_cipher_text的值以JED0100002205a1a0987e82cc400c90582492f814c23c0002d8开头,随后紧跟一段 SJCL JSON 密文。其结构可以拆解为:

JED01 00002205 a1a0987e82cc400c90582492f814c23c 0002d8 {SJCL JSON} │ │ │ │ │ │ │ │ │ └─ 密文负载长度(十六进制) │ │ │ └─ masterKeyId(32 位十六进制) │ │ └─ 加密元数据长度(十六进制) │ └─ 格式版本号 01 └─ Joplin Encryption Data 标识前缀

3.1 头部元数据与主密钥绑定

Joplin 的加密实现位于 packages/lib/services/e2ee/EncryptionService.ts。从源码看,现代版本使用显式编码的头部来携带加密元数据,其模板定义于 EncryptionService.ts:

fields: [['encryptionMethod', 2, 'int'], ['masterKeyId', 32, 'hex']],

而 encodeHeader_ 的编码逻辑为:encryptionMetadata += padLeft(header.encryptionMethod.toString(16), 2, '0') + header.masterKeyId。将上述头部与快照文本对照,a1a0987e82cc400c90582492f814c23c正是本快照主密钥(Master Key)的 id,说明这条 NoteTag 记录是使用该主密钥派生的会话密钥加密的。

3.2 SJCL JSON 参数逐项解读

紧随头部的是标准 SJCL(Stanford JavaScript Crypto Library)密文对象,本快照中的参数为:

参数含义
ivj4Au5F1MRKUgSevr56iisw==16 字节随机初始化向量(Base64)
v1SJCL JSON 结构版本
iter101PBKDF2 迭代次数
ks128密钥长度 128 位(AES-128)
ts64GCM/CCM 认证标签长度 64 位
modeccm认证加密模式:AES-CCM
adata附加认证数据
cipheraes底层分组密码算法
saltGyo7bQeqz2w=用于密钥派生的随机盐
ct长 Base64 串密文(含认证标签)

其中iter: 101是一个容易引起疑问的细节。源码注释给出了明确解释(EncryptionService.ts):主密钥本身已经通过强密钥派生函数保护,因此用主密钥解密得到的"会话密钥"已经足够安全,对每条记录再做高迭代派生只会拖慢加解密速度;而 SJCL 强制要求iter严格大于 100,因此 Joplin 取最小值101

3.3 加密方法版本与主密钥文件

快照目录中的 a1a0987e82cc400c90582492f814c23c.md 即上文头部引用的主密钥文件,其关键字段为:

source_application: net.cozic.joplintest-cli encryption_method: 4 checksum: content: {"iv":"...","iter":10000,"ks":256,"ts":64,"mode":"ccm",...,"salt":"...","ct":"..."} type_: 9
  • encryption_method: 4对应 EncryptionMethod 枚举中的SJCL4(该枚举还包括SJCL=1SJCL2=2SJCL3=3SJCL1a=5SJCL1b=7等历史版本,体现加密方案的演进脉络);
  • 与普通记录不同,主密钥使用iter: 10000ks: 256高成本 PBKDF2 派生,因为主密钥直接由用户密码保护,需要更高的暴力破解成本;
  • type_: 9标记其为主密钥实体,与普通业务项(笔记、标签、关联)区分开;
  • source_application: net.cozic.joplintest-cli表明该快照由测试专用的 CLI 客户端生成,这也是理解快照来源的重要线索。

四、快照如何生成:从测试数据到加密同步目标

快照并非手工伪造,而是由脚本真实执行"建数据 → 开加密 → 同步 → 拷贝远端目录"流程生成的。生成逻辑位于 packages/lib/testing/syncTargetUtils.ts:

4.1 测试数据集定义

testData(syncTargetUtils.ts)定义了一个固定结构的数据集:3 个笔记本(folder1含 2 个子文件夹、folder2folder3),5 条笔记,其中若干笔记附带资源(resource: true,来自supportDir/photo.jpg)和标签(tags: ['tag1', 'tag2'])。createTestData(syncTargetUtils.ts)递归遍历该结构,通过Folder.saveNote.saveshim.attachFileToNoteTag.addNoteTagByTitle在本地数据库建立完整数据。本文主角f58c1af...正是note104c4e9...)与tag18a1707...)之间关联记录的加密形态。

4.2 加密模式与快照落盘

main函数(syncTargetUtils.ts)的执行流程为:

  1. 校验快照类型必须是normale2ee
  2. setupDatabaseAndSynchronizer(1)+switchClient(1)初始化测试环境;
  3. createTestData(testData)创建数据集;
  4. 若为e2ee类型,则调用setEncryptionEnabled(true)开启端到端加密,并loadEncryptionMasterKey()加载测试主密钥;
  5. synchronizerStart()后执行一次完整同步,把全部数据推送到同步目标;
  6. 读取Setting.value('syncVersion')确定当前协议版本,将同步目录整体复制到snapshotBaseDir/{version}/{type},并打印输出路径。

也就是说,2/e2ee/下的每一个.md文件,都是同步器在协议版本 2 下真实序列化输出的结果,这也保证了快照与真实线上数据格式的严格一致。

五、快照的实战用途:同步协议迁移测试

快照最主要的使用场景是同步协议版本迁移测试,测试代码位于 synchronizer_MigrationHandler.test.ts。

5.1 部署旧版本快照

deploySyncTargetSnapshot(syncTargetType, syncVersion)(syncTargetUtils.ts)负责把指定版本的快照复制为"当前同步目标":

export async function deploySyncTargetSnapshot(syncTargetType: string, syncVersion: number) { const sourceDir = `${snapshotBaseDir}/${syncVersion}/${syncTargetType}`; await fs.remove(syncDir); await fs.copy(sourceDir, syncDir); }

测试中(synchronizer_MigrationHandler.test.ts)的模式是:先部署migrationVersion - 1的快照,读取并断言旧版本号(fetchSyncInfo返回migrationVersion - 1),再调用migrationHandler().upgrade(migrationVersion)执行协议升级,最后验证info.json/version.txt已更新、新增目录(.resourcelockstempinfo.json)符合预期。

5.2 加密数据的解密校验

对于e2ee快照(synchronizer_MigrationHandler.test.ts),迁移完成后还须验证数据未被迁移过程破坏

  1. 从快照读取主密钥:const masterKey = (await MasterKey.all())[0]
  2. 注入测试密码Setting.setObjectValue('encryption.passwordCache', masterKey.id, '123456')
  3. 加载主密钥并启动decryptionWorker().start()解密全部数据;
  4. 调用checkTestData(testData)逐项断言——每个笔记本、笔记、资源、标签及其关联都必须完好存在(syncTargetUtils.ts 中通过loadByTitleextractImageUrlsTag.hasNote等做反向校验);
  5. switchClient(2)用第二个客户端同步一遍,验证多客户端场景下数据依旧一致。

正是这条"生成快照 → 部署旧版 → 升级 → 解密 → 校验数据完整性"的闭环,保证了 Joplin 每次同步协议升级都不会让用户已有的加密数据(包括本文这类细粒度的 Note-Tag 关联记录)发生丢失或损坏。

六、总结与延伸阅读

通过本文,你可以掌握三条核心知识:

  1. 快照即事实syncTargetSnapshots/{version}/{mode}/下的每个 Markdown 文件都是某协议版本下同步目标的真实截影,type_字段决定了实体类型,encryption_cipher_text承载全部业务内容;
  2. 加密格式可读JED前缀 + 头部元数据(加密方法 + 主密钥 id)+ SJCL JSON(AES-CCM、PBKDF2、随机盐/IV)构成了 Joplin E2EE 的完整密文结构;主密钥用高成本派生保护,业务记录用低成本迭代换取速度;
  3. 快照驱动兼容性:借助deploySyncTargetSnapshot与迁移测试,仓库能对每一个历史协议版本持续回归验证。

如需继续深入,推荐阅读以下源码文件:

  • 快照生成与校验:packages/lib/testing/syncTargetUtils.ts
  • 协议迁移测试:packages/lib/services/synchronizer/synchronizer_MigrationHandler.test.ts
  • 加密算法与头部格式:packages/lib/services/e2ee/EncryptionService.ts
  • 实体类型常量映射:packages/lib/BaseModel.ts
  • 主密钥快照样例:packages/app-cli/tests/support/syncTargetSnapshots/2/e2ee/a1a0987e82cc400c90582492f814c23c.md

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询