- 前端
- 图形学
【免费下载链接】drawio
draw.io is a JavaScript, client-side editor for general diagramming.
本指南围绕 drawio 仓库中 src/main/webapp/js/cryptojs/aes.min.js 及其 README 展开,说明这份"裁剪版 CryptoJS 4.2.0 rollup"的来龙去脉:如何从 npm 包手工打包、drawio 用它加密实时协作(realtime collaboration)消息的哪些环节、为什么从 3.1.2 升级到 4.2.0,以及升级后对线上格式兼容性、安全边界和构建流程的影响。读完你将掌握该文件的构建命令、DrawioFileSync中 AES/MD5 的调用链、CSPRNG 探测与失败关闭(fail-closed)机制,以及替换/升级该库时需要注意的验收清单。
1. aes.min.js 是什么:一份被"裁剪"的 CryptoJS 4.2.0
aes.min.js并不是 CryptoJS 的完整发行版,而是一份Trimmed CryptoJS 4.2.0 rollup,只包含六个模块,按依赖顺序拼接后压缩:
core + enc-base64 + md5 + evpkdf + cipher-core + aes这个模块清单并非 drawio 自己拍脑袋定的,而是CryptoJS 上游为aes.js声明的依赖列表。由于 CryptoJS 4.x 移除了 3.x 时代内置的rollups/目录,drawio 只能从 npm 包手工拼接出对应 rollup,再交给 Closure Compiler 做压缩。
值得强调的是,这份裁剪是"克制的"——README 明确列出没有包含的内容:
- 没有 SHA / HMAC / PBKDF2 / RC4 / Rabbit / DES 等其余算法;
- 没有 CBC 之外的密码模式;
- 没有 Pkcs7 之外的填充方式。
也就是说,这个文件只为满足 drawio 实时协作场景下"一个 AES-CBC + OpenSSL 口令 KDF + MD5"的最小需求而存在,避免了把整个 CryptoJS 全家桶塞进前端包体。
2. 从 npm 包手工构建 rollup:完整命令
README 给出了可复现的构建流程(在本地临时目录执行,需要 JDK 环境运行 Closure Compiler):
# 1) 拉取指定版本的 crypto-js npm 包并解包 npm pack crypto-js@<version> && tar xzf crypto-js-<version>.tgz # 2) 按上游 aes.js 的依赖顺序拼接六个模块文件,生成 rollup.js for f in core enc-base64 md5 evpkdf cipher-core aes; do cat package/$f.js; echo; done > rollup.js # 3) 用 Closure Compiler(SIMPLE_OPTIMIZATIONS、ES5 输入输出)压缩 java -jar etc/build/compiler.jar --compilation_level SIMPLE_OPTIMIZATIONS \ --language_in ECMASCRIPT5 --language_out ECMASCRIPT5 \ --js rollup.js --js_output_file rollup.min.js产出rollup.min.js之后,再从当前仓库的aes.min.js文件头复制署名头(attribution header)并更新版本号即可。README 特别提醒:如果未来 CryptoJS 上游改变了aes.js的依赖列表,应当跟随新的依赖清单重新裁剪,而不是沿用本文件这份清单——这说明该 rollup 的模块集合是与上游声明强绑定的。
从产物内容可以印证上述过程:aes.min.js文件头即为CryptoJS v4.2.0、MIT 许可及"Trimmed rollup: core + enc-base64 + md5 + evpkdf + cipher-core + aes"的说明,随后是各模块以 UMD 包装形式依次出现的压缩代码,包括enc.Hex、enc.Latin1、enc.Utf8、WordArray.random(RNG 探测逻辑)、MD5、EvpKDF、CBC模式、Pkcs7填充、OpenSSL格式/KDF,以及最后注册的v.AES。
3. drawio 用它做什么:AES 加密实时协作消息 + MD5 生成通道密钥
3.1 AES 加解密:DrawioFileSync.objectToString / stringToObject
drawio 对 CryptoJS 的主要使用点是DrawioFileSync的序列化/反序列化方法:
DrawioFileSync.prototype.objectToString(DrawioFileSync.js):- 先把消息对象
JSON.stringify; - 自 PROTOCOL 7 起,JSON 直接交给 pako
deflateRaw压缩再 Base64(旧版本先 URI-encode 再压缩,JSON 引号和非 ASCII 字符会被膨胀最多 3 倍,编码耗时占大头;文件格式Graph.compress不受影响); - 若指定了
maxLength且压缩后数据超限,返回null让调用方丢弃超大载荷(加密只会让数据变大,且 CryptoJS 的 base64 编码器会为超大载荷逐字符建数组而抛RangeError); - 当存在通道密钥
this.key且CryptoJS可用时,调用CryptoJS.AES.encrypt(data, this.key).toString()完成加密。
- 先把消息对象
DrawioFileSync.prototype.stringToObject(DrawioFileSync.js):- 先
CryptoJS.AES.decrypt(data, this.key).toString(CryptoJS.enc.Utf8)解密; - 再 pako
inflateRaw解压,并兼容旧客户端写入的 URI-encoded 载荷(以%开头则decodeURIComponent); - 最后
JSON.parse。
- 先
这两个方法同时被P2P 协作路径复用:在 P2PCollab.js 中,发送加密消息时直接构造msg = {bytes: sync.objectToString(msg), data: 'aes'},data: 'aes'是为了让旧服务器不会丢弃消息;接收侧则用sync.stringToObject(msg.bytes)还原(P2PCollab.js)。
3.2 字符串密钥触发 OpenSSL KDF(EvpKDF)
关键机制是:objectToString传入的this.key是字符串(而非 32 字节的二进制密钥)。CryptoJS 检测到字符串密钥时,会走口令派生路径——用 OpenSSL KDF(EvpKDF,默认 MD5 迭代 1 轮,见 rollup 内v.kdf.OpenSSL实现)从口令派生出 AES 密钥与 IV,其中8 字节 salt 来自WordArray.random。
由此引出本文后面要讲的安全边界:AES 密钥实际派生自通道密钥(channel key,即this.key,一个共享秘密),salt 只是公开的、按 OpenSSLSalted__格式明文前置在密文上的随机数。
3.3 MD5 用途:通道密钥派生
README 列出的 MD5 使用点与源码一一对应:
OneDriveFile.prototype.getChannelKey(OneDriveFile.js):以"创建时间戳 + 创建者用户 ID"拼接后取CryptoJS.MD5(...).toString()作为通道密钥;nextcloud插件(plugins/nextcloud.js):以instanceId + id拼接取CryptoJS.MD5(...).toString();- README 提到的
monday插件同样依赖该库的 MD5。
对照之下,Google Drive 的DriveFile.prototype.getChannelKey(DriveFile.js)不做哈希,而是直接读取存储的自定义属性key——README 特意点出这一差异,说明并非所有存储后端都走 MD5 派生。
4. 为什么是 4.2.0:一次由安全公告驱动的升级
4.1 3.1.2 的两个问题
升级前 drawio 用的是 CryptoJS 3.1.2,其问题有两个:
- 弱随机源:3.1.2 的
WordArray.random用Math.random()播种(seed),被 Dependabot 标记为GHSA-rg76-677x-56q9/CVE-2026-71851(alert #310); - 弱 KDF 默认值:3.1.2 存在弱 PBKDF2 默认值问题(issue #162),4.2.0 一并修复。
4.x 系列改用crypto.getRandomValues作为熵源,4.2.0 同时清掉了 #162。
4.2 真实暴露面:salt 碰撞而非密钥恢复
README 对风险边界做了精确界定,不夸大、也不轻描淡写:
- 在 3.1.2 下,不安全的 RNG 只用于生成KDF salt;
- 而 salt 在 OpenSSL
Salted__格式下以明文形式前置在密文中,本来就是要公开的; - 因此实际风险是salt 碰撞导致同一通道密钥下 (key, IV) 复用,而不是 AES 密钥被恢复——因为 AES 密钥派生自通道密钥(共享秘密),与 RNG 无关。
换言之,升级 4.2.0 消除的是"同一共享密钥 + 弱随机 salt 造成 (key, IV) 复用"这类密码学使用风险。
4.3 确定性输出逐字节比对
在升级前,仓库对库产生的所有确定性输出都做了与 3.1.2 的逐字节比对,覆盖:
- MD5;
- Utf8 / Latin1 / Hex / Base64 编码器;
- EvpKDF;
- 固定 salt 下的 AES;
- 双向跨版本解密(3.1.2 加密 → 4.2.0 解密,反之亦然)。
这保证了升级不是"换了把锁":任何依赖这些确定性输出的存量数据与在线会话都保持可读。
5. 行为差异与失败关闭(fail-closed)机制
5.1 没有原生 crypto 时:从静默降级到显式抛错
- 3.1.2:在无原生加密(native crypto)的环境下,静默回退到
Math.random(); - 4.2.0:直接抛出
Native crypto module could not be used to get secure random number.
需要澄清的是,只有加密受此影响——AES.decrypt和MD5从不触碰 RNG,解密与哈希在无 CSPRNG 环境下依然可用。
5.2 RNG 探测链与不安全上下文
4.2.0 的WordArray.random按以下顺序探测可用熵源(见aes.min.js中core模块的探测代码):
self.crypto → globalThis.crypto → window.msCrypto → global.crypto → require('crypto')并且getRandomValues在不安全上下文(insecure contexts,即普通 HTTP 部署)下也可用,所以纯 HTTP 部署的 drawio 实例不受影响。
5.3 可被覆盖的window.crypto与isEncryptionAvailable兜底
虽然探测链覆盖广,README 仍指出一个可攻击点:window.crypto是一个可配置(configurable)的访问器属性,嵌入页面或插件可以用Object.defineProperty替换它,从而让前两步探测失效(进而使整个探测链失败)。
drawio 对此的兜底实现是DrawioFileSync.prototype.isEncryptionAvailable(DrawioFileSync.js):
- 若没有通道密钥或没有 CryptoJS,直接返回
true(本就不加密,谈不上风险); - 否则做一次
CryptoJS.lib.WordArray.random(8)探测:成功则缓存encryptionAvailable = true,抛错则缓存false并通过EditorUi.logError记录(文件 ID 经哈希后才进日志,避免原始文件 ID 泄漏); - 结果每会话只探测一次,缓存于静态属性
DrawioFileSync.encryptionAvailable,不重载页面不会改变。
探测结果为假时,objectToString会抛出No CSPRNG for realtime encryption(DrawioFileSync.js),实时同步被保持关闭。README 解释了为什么不能回退到明文:接收方仍持有通道密钥,会尝试解密而得到乱码,且明文载荷会落入缓存——因此"发不出去"比"发出去但谁都读不了"更安全。这就是典型的失败关闭设计:宁可让带密钥通道的实时协作不可用,也不发一条接收方读不了、缓存却能读的消息。
6. 构建集成与替换方式
6.1 逐字拼接,永不重编译
构建阶段,aes.min.js会被<concat>逐字拼进base.min.js,且不会再经过 Closure 编译。这意味着:
- 文件内的 UMD 包装和
require('crypto')引用不会经过 Closure 处理(避免被优化器改写破坏); - 替换该库就是一次直接的文件替换(straight file swap),不需要改动构建链。
开发模式下,Devel.js 通过mxscript(drawDevUrl + 'js/cryptojs/aes.min.js')直接按开发 URL 加载该文件;生产构建则把内容拼入base.min.js。此外该文件也出现在 service-worker.js 的预缓存清单中("js/cryptojs/aes.min.js"),说明其作为运行时静态资源参与 Service Worker 预缓存。
6.2 线上格式兼容:4.2.0 与 3.1.2 可互通
由于 EvpKDF / OpenSSL 格式在版本间未变,4.2.0 的 rollup 在线上是升级兼容的:同一协作会话里,一个 3.1.2 客户端和一个 4.2.0 客户端依然能互相读取对方的消息。这是第 4.3 节双向跨版本解密验证在真实场景中的意义。
6.3 升级/替换验收清单(可操作建议)
结合 README 与源码,替换或升级aes.min.js时应至少验证:
- 模块集合:确认新版本
aes.js的依赖声明是否仍是core, enc-base64, md5, evpkdf, cipher-core, aes,有变化就跟随新清单; - 确定性输出:MD5、四种编码器(Utf8/Latin1/Hex/Base64)、EvpKDF、固定 salt 的 AES 结果与旧版本逐字节一致;
- 双向互操作:旧版本加密 → 新版本解密、新版本加密 → 旧版本解密均通过;
- CSPRNG 行为:确认无原生 crypto 时抛错、有
getRandomValues(含 HTTP 不安全上下文)时可用,并确认isEncryptionAvailable探测/缓存逻辑仍能覆盖; - 构建产物:替换后检查
base.min.js中拼接内容、service-worker.js预缓存清单与开发模式Devel.js加载路径是否同步。
7. 小结
aes.min.js是 drawio 实时协作加密链路中一个"小而关键"的组件:它以裁剪 rollup 的方式只引入 CryptoJS 的 AES-CBC(OpenSSL KDF 口令派生)与 MD5,为DrawioFileSync的消息加密和OneDriveFile/nextcloud等后端的通道密钥派生提供基础;升级到 4.2.0 修复了 3.1.2 弱随机源(Math.random())与弱 KDF 默认值问题,同时通过逐字节比对保证线上格式兼容;而isEncryptionAvailable的失败关闭设计则确保在 CSPRNG 被嵌入页或插件破坏时,实时协作宁可不启动也不发送任何接收方无法解密的消息。理解这份文件的构建命令、使用点与安全边界,是排查 drawio 实时同步加密问题、或在其基础上做定制升级的前提。
- 前端
- 图形学
【免费下载链接】drawio
draw.io is a JavaScript, client-side editor for general diagramming.
相关推荐
N_m3u8DL-RE 流媒体下载教程:10 分钟装好并跑通第一条 m3u8/MPD 下载命令
N_m3u8DL RE 流媒体下载教程:10 分钟装好并跑通第一条 m3u8/MPD 下载命令 N_m3u8DL RE 是一款免费开源的跨平台流媒体下载工具,专
CLI音视频LangChain Go 30 分钟入门:从 0 到 1 构建带记忆的 AI 对话助手
LangChain Go 30 分钟入门:从 0 到 1 构建带记忆的 AI 对话助手 LangChain Go(langchaingo)是一个用 Go 编写的
人工智能大模型AI AgentRAG后端解密前端加密黑盒:从CryptoJS源码看AES算法实现
解密前端加密黑盒:从CryptoJS源码看AES算法实现 你是否曾在开发中调用 CryptoJS.AES.encrypt 时疑惑:明明传入的是字符串,为何能输出
密码学
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考