Joplin 非正式加密与安全审计全解析:从 OCB2 迁移到现代 E2EE 加密体系

Joplin 非正式加密与安全审计全解析:从 OCB2 迁移到现代 E2EE 加密体系 Joplin 非正式加密与安全审计全解析从 OCB2 迁移到现代 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体系。2020 年 4 月安全专家 Isaac Potoczny-JonesTozny 公司 CEO对 Joplin 的加密实现进行了非正式审计指出了 OCB2 分组密码模式、冗余密钥扩展、主密钥校验和以及本地明文密码缓存四类问题。本文以这份审计报告readme/news/20200406-224254.md为主线结合当前仓库中 EncryptionService.ts 的完整实现逐条还原审计结论、修复方案与底层原理帮助读者理解 Joplin E2EE 的演进脉络并掌握主密钥升级与数据重加密的完整操作流程。审计背景与结论概览这次审计针对的是 Joplin 的加密实现尤其是同步过程中使用的端到端加密E2EE系统。审计者 Isaac Potoczny-Jones 的总体结论是我在审阅 Joplin 的加密实现时有一些评论和担忧。我没有看到任何我确认是严重critical的问题但有一些选择和弱点我想给出一些建议。审计共提出四类发现Joplin 团队逐一回应并修复发现风险等级修复方式OCB2 分组密码模式存在已知弱点安全隐患全部客户端迁移到 CCM 模式并提供主密钥升级与数据重加密工具对随机主密钥执行多余的密钥扩展性能问题笔记加密的 PBKDF2 迭代次数从 1,000 次降到 100 次源码实际为 101 次存储不必要的 SHA-256 主密钥校验和潜在安全隐患新加密方法中彻底移除校验和字段本地数据库明文缓存密码本地安全模型引入系统钥匙串Keychain服务保存本地密钥这四项修复在今天的 Joplin 源码中均有完整实现下面逐项展开。发现一从 OCB2 到 CCM 的密码模式迁移审计意见审计者指出Joplin 当时选择的 OCB2Offset Codebook Mode 2多分组密码模式在近些年暴露出了一些弱点且它并非 NIST 批准的模式业界普遍认为它已不再是好的选择。源码层面的证据被标记废弃的 OCB2 分支当前源码 EncryptionService.ts 中EncryptionMethod.SJCL分支保留了这段历史实现并明确标注了废弃原因// 2020-01-23: Deprecated and no longer secure due to the use og OCB2 mode - do not use. [EncryptionMethod.SJCL]: () { return sjcl.json.encrypt(key, plainText, { v: 1, // version iter: 1000, ks: 128, // Key size - 128 bits should be secure enough ts: 64, mode: ocb2, // OCB2 mode is slightly faster and has more features, but CCM mode has wider support because it is not patented. cipher: aes, }); },同样被废弃的还有用于加密主密钥的EncryptionMethod.SJCL2L427-L440它使用 OCB2 模式配合 10,000 次迭代。两个分支的注释都指向同一个结论OCB2 虽然稍快、特性更多但 CCM 模式因为不受专利限制而获得更广泛的支持。修复方案全部客户端迁移到 CCM审计后Joplin 在所有客户端桌面、移动端、CLI完成了向 CCM 模式的迁移并在桌面应用的加密配置界面加入了迁移工具。从源码看这一过程对应引入了两个新加密方法L382-L423// 2020-03-06: Added method to fix ... Also took the opportunity to change number of key derivations, per Isaac Potocznys suggestion [EncryptionMethod.SJCL1a]: () { return sjcl.json.encrypt(key, escape(plainText), { v: 1, iter: 101, // 主密钥已经做过密钥扩展且足够安全笔记加密时不必再额外迭代这样解密会更快。SJCL 强制要求 iter 严格大于 100 ks: 128, ts: 64, mode: ccm, cipher: aes, }); },SJCL1a以 CCM 模式替代 OCB2密钥长度仍为 128 位SJCL1b则在 2023 年进一步将密钥升级到 AES-256L404-L423注释明确写到 256-bit is the golden standard that we should follow。主密钥升级Upgrade the master key桌面端加密配置界面中的升级主密钥操作会把主密钥的加密方式转换为 CCM。其底层逻辑在 reencryptMasterKey 中实现先用旧密码解密主密钥内容再用新的默认主密钥加密方法重新加密public async reencryptMasterKey(model: MasterKeyEntity, decryptionPassword: string, encryptionPassword: string, ...): PromiseMasterKeyEntity { const newEncryptionMethod this.defaultMasterKeyEncryptionMethod_; const plainText await this.decryptMasterKeyContent(model, decryptionPassword, decryptOptions); const newContent await this.encryptMasterKeyContent( newEncryptionMethod, plainText, encryptionPassword, encryptOptions, ); return { ...model, ...newContent }; }判断哪些主密钥需要升级的逻辑同样在服务层masterKeysThatNeedUpgrading()会筛选出加密方法不是当前默认方法的所有主密钥L264-L266桌面端的 EncryptionConfigScreen.tsx 通过upgradeMasterKey函数驱动这一流程。数据重加密Re-encryption重加密操作将使用基于 CCM 的新加密方法重新加密全部数据。桌面端加密配置界面中执行此操作时需注意严格按照界面提示操作该过程耗时较长建议安排在夜间或计划好的空闲时段运行尽管当时尚不完全清楚 OCB2 缺陷如何被实际利用审计团队仍建议尽快升级数据。从实现层面看重加密对每个块执行encrypt()后都会调用crypto.increaseNonce()递增随机数L598-L614且每个块之前会写入一个 6 位十六进制的长度前缀解密时据此逐块切分L627-L642。发现二消除对随机主密钥的多余密钥扩展审计意见审计者注意到Joplin 的encrypt函数使用 1,000 或 10,000 轮密钥派生PBKDF2而其中批量数据加密使用的是随机生成的主密钥master key并不需要这种针对用户口令的暴力破解防护。审计者判断这可能是批量加密原始数据时的重大性能瓶颈加密函数使用了 1k 或 10k 轮密钥派生其目的是降低针对用户自选密码的暴力破解风险。但批量加密使用的是随机生成的主密钥假定来自加密强度足够的随机数生成器并不需要密码扩展。我怀疑这可能是批量加密原始数据的性能问题来源——如果你觉得加密很慢原因可能就在这。修复方案从 1,000 次降到 100 次这更多是性能问题而非安全问题旧方案每次加密一条笔记都执行 1,000 次密钥扩展而主密钥本身已用 10,000 次迭代保护笔记级加密的额外迭代纯属浪费。修复后笔记加密只执行约 100 次迭代桌面端、移动端和 CLI 应用的加解密速度都因此提升。源码中的迭代次数与审计建议完全对应主密钥加密SJCL4CCM 模式iter: 10000因为要抵抗针对用户口令的暴力破解L462-L476笔记加密SJCL1a/SJCL1bCCM 模式iter: 101。源码注释特别说明 SJCL 强制要求迭代次数严格大于 100因此取 101 而非 100L392-L393、L413-L414。EncryptionService.test.tspackages/lib/services/e2ee/EncryptionService.test.ts中对SJCL1a、SJCL1b、KeyV1等新旧方法均有往返加解密测试覆盖确保迁移后数据的可读性。分块chunking与性能的进一步权衡密钥扩展只是性能影响的一部分。源码注释中还记录了移动端分块大小的实测数据L111-L125在 Android 7.1 模拟器上解密一个块所需时间随块大小非线性增长——50KB 约 1000ms5KB 约 10ms块缩小 10 倍耗时反而降低 100 倍。因此笔记级加密的块大小被设为 5,000 字节且块大小会写入加密数据头后续可随时调整。加密流程还会调用shim.waitForFrame()让出执行帧避免移动端界面在加解密大文件时卡死L605-L607。发现三移除不必要且可能不安全的 SHA-256 主密钥校验和审计意见审计者发现Joplin 在加密主密钥之外还额外存储了一份主密码的 SHA-256 校验和除了加密之外你还用 SHA256 生成了主密码的校验和并保存下来。我猜这是为了判断用户密码是否正确。我从未见过这种做法它让我有些担心但我不确定这一定是个问题。至少在使用 CCM 模式我认为 OCB2 也是时密码错误就应该无法成功解密。正如 Cryptography StackExchange 上相关讨论所指出的在哈希函数的标准模型下并不要求哈希输出不具备泄露输入信息的属性——校验和的存在可能削弱主密钥的安全性。同时该校验和也是多余的如果密钥无效解密算法本身就会失败。修复方案让解密失败成为唯一的正确性判据当前源码中校验和字段已经基本消失。在encryptMasterKeyContent中只有历史方法SJCL2才计算校验和且代码注释直接点明了设计原则L288-L295return { // Checksum is not necessary since decryption will already fail if data is invalid checksum: encryptionMethod EncryptionMethod.SJCL2 ? this.sha256(hexaBytes) : , encryption_method: encryptionMethod, content: await this.encrypt(encryptionMethod, password, hexaBytes), };即校验和不再需要因为数据无效时解密自然失败。解密侧对应地只在SJCL2分支校验 checksumL327-L333其余方法均以解密是否抛错作为密码正确性的判据。密码校验逻辑集中在checkMasterKeyPassword()——尝试解密主密钥内容成功即认为密码正确L336-L347。正如审计报告所说这一点也已通过新的主密钥升级工具解决只要执行过升级校验和就会从主密钥中移除。发现四用系统钥匙串服务保存本地机密审计意见审计者注意到Joplin 在本地数据库中缓存了明文密码我注意到你在数据库中缓存了明文密码这有些令人担忧。但我想你的加密安全模型是针对同步过程而非本地所以可以理解。存储机密的通用做法是使用钥匙串服务keychain service这在几乎所有现代平台上都可用。修复方案从本地缓存到系统钥匙串审计报告解释了当时的设计权衡密码在本地缓存是为了避免每次同步加解密笔记时都要重新输入安全模型假设本地设备本身是安全的。报告同时预告未来版本将尽可能使用系统钥匙串。这一承诺已在当前仓库落地为独立的钥匙串服务模块packages/lib/services/keychain/采用驱动Driver模式适配不同平台Electron 驱动KeychainServiceDriver.electron.ts基于 Electron 的safeStorage通过safeStorage.encryptString()加密后存入 KvStore并检查safeStorage.isEncryptionAvailable()判断平台是否支持Node 驱动KeychainServiceDriver.node.ts面向 CLI 等非 Electron 场景Dummy 驱动KeychainServiceDriver.dummy.ts作为不支持钥匙串时的回退实现。主入口 KeychainService.ts 统一暴露setPassword/password等接口移动端的加密配置界面packages/app-mobile/components/screens/encryption-config.tsx同样集成了钥匙串相关能力。这也是审计报告末尾致谢语境的一部分这次审计推动 Joplin 在本地机密保护上迈出了实质一步。超越审计2024 年后的现代加密体系审计报告发表于 2020 年而当前仓库展示了此后数年 Joplin 在加密上的持续演进——这是理解审计如何长期影响项目的关键延伸原生加密方法引入KeyV1主密钥、StringV1笔记文本、FileV1附件文件三种新方法改由原生密码库Node 的node:crypto/ React Native 的react-native-quick-crypto驱动使用AES-256-GCM PBKDF2不再依赖 SJCL 的 JS 实现L478-L517。其底层实现在 crypto.ts96 位 IV、16 字节认证标签、密钥长度 32 字节、摘要算法 SHA-512。主密钥派生强度对齐 OWASPKeyV1的 PBKDF2 迭代次数设为220,000源码注释明确引用 OWASP Password Storage Cheat Sheet 的建议L480-L489而StringV1/FileV1因主密钥已足够强迭代次数仅为 3避免笔记级加解密被密钥派生拖慢。nonce 复用防护新的加密方法中主密钥不会直接用于加密数据而是通过主密钥 256 位随机盐派生数据密钥防止 nonce 复用问题nonce 由随机部分、7 字节时间戳和 8 字节计数器组成cryptoShared.ts计数器用尽时会重新生成随机 nonce。从审计报告提出的 OCB2 迁移、迭代次数优化、校验和移除到钥匙串集成再到如今 AES-256-GCM 原生加密体系Joplin 的 E2EE 演进脉络在 EncryptionService.ts 的每一个EncryptionMethod分支和注释里都有清晰可查的痕迹。对于关注笔记数据安全、或想深入理解端到端加密工程实践密码模式选型、密钥派生参数、nonce 管理的开发者而言这份审计报告配合源码是一份极具参考价值的完整案例。【免费下载链接】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),仅供参考