- 后端
- Web框架
【免费下载链接】playframework
The Community Maintained High Velocity Web Framework For Java and Scala.
本文是 Play Framework 2.5 系列迁移文档的核心章节,系统讲解旧版Crypto工具对象被废弃的原因、其背后消息认证(MAC)与对称加密(AES)设计上的安全隐患,以及迁移到 Kalium、Tink 或纯 JCA 的三条推荐路径。读完本文,你将理解 Play 为何将密码学能力拆分为CookieSigner、CSRFTokenSigner等专用 trait,并能为自己的应用选择安全、合适的替代实现。
背景:从Crypto单例到专用 Signer Trait
从 Play 1.x 起,Play 框架就内置了一个名为Crypto的对象,提供若干密码学操作,并被 Play 内部使用。虽然该对象从未在官方文档中被正式介绍,但它的 scaladoc 中将其描述为"密码学工具"(cryptographic utilities),并附带了一段重要警告:
"这些工具旨在提供便利,但使用该类之前,务必阅读每个方法的文档并理解加密背后的概念。安全加密是困难的,任何东西都无法替代对密码学的充分理解。这些方法并不适合所有加密需求。"
然而,经过实践检验,"以便利工具的形式提供密码学能力"这一思路被证明并不可行。因此从2.5.x开始,Play 将原本属于Crypto的框架专用功能拆分成了三个独立的 trait,并正式将Crypto单例对象标记为deprecated(废弃):
| 新组件 | 职责 |
|---|---|
CookieSigner | 为 Cookie(尤其是 Session Cookie)提供消息认证码(MAC)签名 |
CSRFTokenSigner | 生成与校验 CSRF 令牌,防止 BREACH 攻击 |
AESSigner | 对称加密相关的签名辅助(AES 相关能力) |
从当前仓库源码可以验证这一拆分结果:CookieSigner与CSRFTokenSigner分别定义在 core/play/src/main/scala/play/api/libs/crypto/CookieSigner.scala 与 core/play/src/main/scala/play/api/libs/crypto/CSRFTokenSigner.scala,并提供对应的 Java 接口版本 core/play/src/main/java/play/libs/crypto/CookieSigner.java。依赖注入时,可通过 CryptoComponents 获取cookieSigner()与csrfTokenSigner()两个组件。
关联阅读:Play 2.5 总迁移指南 Migration25.md 中" Crypto Deprecated"一节也明确指出:加密迁移方案取决于你的使用场景,尤其是是否涉及不安全的密码学原语构造,推荐顺序是"能上 Kalium 就上 Kalium,否则用 Tink 或直接用 JCA"。
消息认证(Message Authentication):为什么不应滥用Crypto.sign
Play 使用Crypto.sign方法为 Session Cookie 提供消息认证。该方法在内部用于对 Session Cookie 进行签名,但官方明确建议:不应在签名 Cookie 之外的场景使用Crypto.sign,理由有以下三点。
MAC 算法独立性:Play 需要保留升级 HMAC 的空间
Play 当前使用HMAC-SHA1对 Session Cookie 进行签名与校验。HMAC(基于哈希的消息认证码)是一种利用密钥(即play.crypto.secret配置的应用密钥)配合消息摘要函数,认证数据未被篡改的密码学函数,此处采用的摘要函数是 SHA-1。
虽然 SHA-1 近年遭受了若干攻击,但当它与 HMAC 配合用于"消息真实性"验证时,仍然是安全的。关键在于:Play 需要保留"按需切换到其他 HMAC 函数"的灵活性,因此 HMAC 的具体选择不应成为公共 API 的一部分。如果Crypto.sign被大量外部代码直接依赖,Play 内部升级 HMAC 算法的自由度就会受限,这正是它不应作为公共 API 的理由之一。
从仓库源码看,当前DefaultCookieSigner的默认实现确实仍固定使用"HmacSHA1"算法(见 CookieSigner.scala),并通过Mac.getInstance(HmacSHA1)获取 JCE 提供的 MAC 实例,最后用Codecs.toHexString输出十六进制签名字符串:
def sign(message: String): String = { sign(message, secretConfiguration.secret.getBytes(StandardCharsets.UTF_8)) }其 Scala 与 Java 接口均提供了两个重载:一个使用应用密钥签名,一个显式传入自定义key字节数组签名。单元测试 CookieSignerSpec.scala 验证了这两种签名路径——例如使用密钥"0123456789abcdef"对文本"Play Framework 2.0"签名,得到的 HMAC-SHA1 十六进制摘要为94f63b1470ee74e15dc15fd704e26b0df36ef848。
潜在的新功能需求:Session 缺少过期时间
Play 目前只对 Session Cookie 进行签名,但没有为其附加任何会话超时或过期时间。这意味着,只要攻击者能接触到请求(active attacker),就可以把一个 Session Cookie 替换成另一个 Session Cookie(cookie swapping)。要解决这类问题,Play 可能需要给 Session 引入时间戳等额外机制——而这类演进同样要求签名逻辑不能过早固化到公共 API 中。
误用为密码哈希:MAC 不是密码存储方案
请勿将Crypto.sign或任何形式的 HMAC 用于密码哈希!这是文档中特别强调的告诫。原因是两者的设计目标截然相反:
- MAC 追求快与廉价:签名/验签需要高频执行,设计上要求计算开销小;
- 密码哈希追求慢与昂贵:为了抵抗暴力破解与 GPU 加速攻击,密码哈希需要刻意引入计算成本。
因此正确的密码存储应使用scrypt、bcrypt 或 PBKDF2等专用算法。在 Java 生态中,jBCrypt是最广为人知的 bcrypt 实现之一。
对称加密(Symmetric Encryption):Crypto.encryptAES的安全隐患
Crypto还包含两个对称加密方法:Crypto.encryptAES与Crypto.decryptAES。这两个方法从未被 Play 内部使用,但社区投入了大量精力对其安全性进行审查。这两个方法同样将被废弃,并可能在未来的版本中移除。
与前面的警告一致,这两个方法"并非普遍安全"——存在若干常见的工作模式(mode of operation)在使用它们时是不安全的。下面逐一分析Crypto.encryptAES存在的密码学问题。
再次强调:
Crypto.encryptAES从未在 Play 内部被直接使用,因此这不是Play 自身的安全漏洞。
无认证的流密码:AES-CTR 的可塑性(Malleability)
Crypto.encryptAES默认使用AES-CTR模式。CTR 模式属于流密码类工作模式,它只提供加密(confidentiality),不提供认证(authentication)——也就是说,活跃攻击者可以把密文内容替换成其他内容而不被发现。这种"密文可被篡改"的性质被称为可塑性(malleability),在特定条件下甚至可能让攻击者恢复出明文,或任意改写消息内容。
业界有两种缓解该问题的标准构造:
- Encrypt Then MAC(先加密后签名):对密文再施加一个 MAC;
- 认证加密(Authenticated Encryption):直接使用同时提供认证与加密的算法,例如AES-GCM。
Play 内部用Crypto.encryptAES加密 Session Cookie 的内容(该内容本身已附加了 MAC)。由于 Play 采用的是Encrypt Then MAC构造,因此它并不易受此类攻击——但那些在没有 MAC 保护的情况下使用对称加密的用户,则存在潜在风险。
文档同时建议:不要自行实现 Encrypt-Then-MAC 构造,正确做法是直接使用认证加密,让认证与加密在同一算法中完成(详见下文迁移章节)。
违反密钥分离原则(Key Separation Principle)
虽然"AES + HMAC"被认为是安全构造,但 Play 使用密钥的方式存在一个设计问题:密钥分离原则要求一把密钥只服务于一个目的。而在Crypto时代,play.crypto.secret不仅用于签名,还被Crypto.encryptAES用于加密——同一把密钥被复用于两个不同用途。
文档引用密码学社区的分析指出:对于"HMAC vs AES"这一组合,目前没有已知的密钥混用干扰,密码学家的普遍共识是 AES 与 SHA-1/SHA-256 "足够不同",同一密钥同时用于 AES 与 HMAC 不应产生实际问题。因此对于正在使用Crypto.encryptAES的用户,密钥混用并不会立刻带来安全漏洞。
但随着应用规模增大,密钥分离原则可能在另一个维度被违反:如果Crypto.encryptAES被用于多种不同用途,同样建议为不同用途分配独立密钥。
工作模式的全局配置问题
如前所述,AES 支持多种工作模式,而不同模式可能需要不同的附加安全措施。Play 却通过play.crypto.aes.transformation这一配置项提供全局的工作模式配置——该配置会影响整个应用,包括所有使用Crypto.encryptAES的第三方库。这样一来,改动这一配置项对整个应用的实际影响范围很难预估,这也成为其被废弃的另一个理由。
迁移路径(Migration):从Crypto平滑过渡
从Crypto功能迁移有以下几条路径,按推荐优先级排序:Kalium、Tink、纯 JCA。
Kalium(首选):基于 libsodium 的高级密码学库
如果你的生产环境可以控制二进制依赖,且没有必须使用 NIST 批准算法的外部要求,推荐使用Kalium——它是 libsodium 库的封装。
- 需要
Crypto.sign的 MAC 替代品:使用org.abstractj.kalium.keys.AuthenticationKey,它实现了HMAC-SHA512/256; - 需要
Crypto.encryptAES的对称加密替代品:使用org.abstractj.kalium.crypto.SecretBox,它实现了secret-key authenticated encryption(密钥认证加密),天然同时提供加密与认证。
注意:Kalium 要求环境中安装 libsodium 二进制库,官方建议安装经你亲自验证过的源码构建版本。
Tink:纯 Java 且支持 NIST 批准算法的选择
如果你需要纯 Java 方案,或必须依赖 NIST 批准的算法,可以使用Tink——一个构建在 JCA 之上的高层密码学库。文档特别说明:Tink 的成熟度与支持力度不如 libsodium / Kalium,因此Kalium 仍然优先。
- 需要
Crypto.sign的 MAC 替代品:使用com.google.crypto.tink.mac.MacKeyTemplates; - 需要
Crypto.encryptAES的对称加密替代品:使用com.google.crypto.tink.aead.AeadKeyTemplates。
JCA:保持原算法不变的最低改动方案
Kalium 与 Tink 使用的密码学原语都与Crypto不同。如果希望在不改变底层算法(如继续使用 AES 与 HMAC)的前提下完成迁移,最合适的做法是把 Crypto 库中的相关代码抽取到用户自己的类中,由应用层自行维护,再按安全最佳实践(如使用认证加密、隔离密钥用途)加以改造。
迁移决策速查
| 你的需求 | 推荐方案 |
|---|---|
替代Crypto.sign(MAC)且环境可控 | KaliumAuthenticationKey(HMAC-SHA512/256) |
替代Crypto.sign(MAC)且需纯 Java/NIST | TinkMacKeyTemplates |
替代Crypto.encryptAES(认证加密)且环境可控 | KaliumSecretBox |
替代Crypto.encryptAES(认证加密)且需纯 Java/NIST | TinkAeadKeyTemplates |
| 必须保持 AES/HMAC 原算法不变 | 抽取 Crypto 源码为用户级类,自行加固 |
当前仓库中的替代实现:Signer 是如何工作的
迁移到 2.5 之后,Play 自身的 Session Cookie 签名链路已经完整建立在CookieSigner之上,可以在仓库源码中直接印证:
- Session.scala 中,
DefaultSessionCookieBaker与LegacySessionCookieBaker均通过构造器注入CookieSigner,其中LegacySessionCookieBaker的注释明确写着"以 Play 2.5.x 风格签名 Session Cookie",cookieSigner参数标注为"通常是 HMAC-SHA1"; CookieSignerProvider(见 CookieSigner.scala)从SecretConfiguration构建DefaultCookieSigner,而SecretConfiguration定义在 HttpConfiguration.scala,其默认密钥为"changeme",并支持通过配置指定 JCE Provider;- 密钥与 Provider 的配置项为
play.http.secret.provider(旧配置名play.crypto.provider仍可通过 deprecated 机制读取,见 HttpConfiguration.scala),DefaultCookieSigner在实例化 MAC 时若指定了 Provider 则优先使用Mac.getInstance(HmacSHA1, p),否则使用平台默认 JCE Provider。
CSRFTokenSigner 的防 BREACH 设计
CSRFTokenSigner的默认实现(CSRFTokenSigner.scala)值得专门一提,它展示了 Play 在"签名"场景下的实战密码学设计:
signToken会将当前时间戳作为 nonce 与原始 token 拼接后签名,输出格式为签名-时间戳-原始token。其核心目的正如源码注释所述:"主要为了抵御BREACH 漏洞——在不改变 token 实际值的前提下,让 token 每次请求看起来都是随机的";extractSignedToken按-分割并校验签名,校验时使用MessageDigest.isEqual进行常数时间比较,避免时序侧信道攻击;generateToken使用SecureRandom生成 12 字节随机数并转为十六进制;默认实现还会在初始化时调用random.nextBytes(new ArrayByte)进行 440 位的自播种(对应 NIST SP800-90A 建议),以兼容旧版 Windows 上 SHA1PRNG 的弱自播种问题。
这些实现细节表明:即使是"为 Cookie 签名"这一看似简单的需求,Play 也内建了对抗活跃攻击者(nonce 化、常数时间比较)与熵不足(SecureRandom 自播种)的多重防护——这正是"密码学 API 应由框架谨慎设计、而非暴露给用户随意拼装"这一迁移主旨的最佳注解。
进一步阅读
密码学 API 的设计远比表面看起来复杂。文档推荐了以下关于密码学设计与 API 复杂性的公开资料,供深入研究:
- The Long Journey from Papers to Software: Crypto APIs
- What's Wrong with Crypto API Design
- Real World Crypto 2015: Error-prone cryptographic designs (djb)及其配套幻灯片
此外,OWASP 的Cryptographic Storage Cheatsheet是学习安全加密存储实践的重要参考资料。理解这些资料,有助于在迁移到 Kalium / Tink / JCA 时做出正确的算法选择与构造决策。
- 后端
- Web框架
【免费下载链接】playframework
The Community Maintained High Velocity Web Framework For Java and Scala.
相关推荐
Youku-mPLUG中文数据集:首个大规模中文视频文本对在生成任务中的应用
Youku mPLUG中文数据集:首个大规模中文视频文本对在生成任务中的应用 随着AIGC技术的飞速发展,视频生成领域对高质量中文数据的需求日益迫切。然而长期以
密码学后端如何在 ESP-IDF 中快速获取 WiFi TSF 时间戳:一份完整指南
如何在 ESP IDF 中快速获取 WiFi TSF 时间戳:一份完整指南 想在 ESP IDF 项目里拿到微秒级精度的 WiFi 时间参考?本文带你用 esp
物联网嵌入式商品图加一条差评,分类模型会更聪明吗?PyTorch 多模态学习快速上手指南
商品图加一条差评,分类模型会更聪明吗?PyTorch 多模态学习快速上手指南 pytorch deep learning 是一套从零到一讲透 PyTorch 的
示例工程教程
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考