首页 >  文章 >  java教程

HKDF-SHA256 密钥派生实战:salt、info 与 AES-256 会话密钥隔离

来源:17golang原创

时间:2026-08-16 20:06:20 147浏览 收藏

服务端准备给每个会话派生一把临时密钥时,最容易踩的坑是直接把原始密钥材料截取几段用,或是直接拿一次 SHA-256 的输出当所有场景的子密钥。Java 25 提供的密钥派生函数(KDF)API 把这件事做成了标准的 Extract/Expand 流程,HKDF-SHA256 还能通过 salt 和 info 把密钥用途直接绑定到协议上下文,避免不同场景密钥混用。

要点速览
  • Java 25 通过 KDF.getInstance("HKDF-SHA256") 获取 HKDF 实现,KDF 只负责密钥派生,不承担加密或者随机数生成的职责。
  • salt 作用于 Extract 阶段,info 作用于 Expand 阶段;把会话 ID、租户 ID 和业务用途写进 info 字段,就能避免不同用途复用同一派生结果。
  • 只有原始密钥材料不适合直接作为业务密钥的场景才需要用 KDF,KDF 的输入本身必须先有足够的熵,来自安全可信的来源。
  • 验收派生逻辑时要固定算法、输出长度、salt、info 和输入材料,验证重复调用派生结果完全一致、上下文变化时派生结果必然不同,还要覆盖非法长度的输入场景。

先把 KDF 和加密、哈希分开

KDF 的核心职责是:拿到一段已有的安全密钥材料和上下文信息,输出指定长度、绑定指定用途的派生子密钥。它不会输出密文,也不会帮你生成高质量的随机种子。

能力输入输出不该拿来做什么
KDF密钥材料、salt、info、输出长度用途隔离的子密钥直接用来加密业务数据
哈希任意消息内容固定长度摘要替代带上下文绑定的密钥派生
AEAD密钥、nonce、明文、AAD密文与认证标签直接从低熵口令生成安全密钥
SecureRandom系统级熵源随机字节流把已有固定密钥材料生成多用途子密钥

Java 25 由 JEP 510 引入标准 KDF 服务接口,具体算法由对应的加密提供者实现。下面示例用的是 HKDF-SHA256,生产环境使用前要先确认运行环境已经装好对应加密提供者,走完项目要求的密码学方案评审流程。

Java 25 HKDF-SHA256 分层派生路径:输入密钥材料经过 Extract 和 Expand 输出会话子密钥

用 HKDF-SHA256 派生一把会话密钥

Java 25 API 的最小调用逻辑可以围绕 KDFHKDFParameterSpecSecretKey 这几个核心对象组织。示例里的输入只是用来演示 API 调用方式,真实线上服务必须从密钥管理系统或者安全握手协议获取合法密钥材料,绝对不能把口令字面量直接当成生产密钥来用。

import javax.crypto.KDF;
import javax.crypto.SecretKey;
import javax.crypto.spec.HKDFParameterSpec;
import java.nio.charset.StandardCharsets;
import java.util.HexFormat;

public class SessionKeyDemo {
    public static void main(String[] args) throws Exception {
        byte[] keyMaterial = "demo-key-material-with-enough-entropy" 
                .getBytes(StandardCharsets.UTF_8);
        byte[] salt = HexFormat.of().parseHex("00112233445566778899aabbccddeeff");
        byte[] info = "checkout/session/v1/tenant-17".getBytes(StandardCharsets.UTF_8);

        KDF kdf = KDF.getInstance("HKDF-SHA256");
        HKDFParameterSpec params = HKDFParameterSpec.ofExtract()
                .addIKM(keyMaterial)
                .addSalt(salt)
                .thenExpand(info, 32);
        SecretKey sessionKey = kdf.deriveKey("AES", params);

        System.out.println(HexFormat.of().formatHex(sessionKey.getEncoded()));
    }
}

代码编译时需要指定 Java 25 作为目标版本:

javac --release 25 SessionKeyDemo.java
java SessionKeyDemo

输出的 32 字节内容可以直接作为 AES-256 的密钥输入,但真正做加密操作时还是要由 AEAD 算法独立生成并管理 nonce,同时把需要的附加认证数据传给加密接口,KDF 的输出本身并不是已经加密完成的业务数据。

salt 和 info 应该分别绑定什么

HKDF 的两个阶段解决的是完全不同的问题。Extract 阶段把输入密钥材料和 salt 汇聚成统一的伪随机密钥,Expand 阶段再根据 info 字段和要求的长度,派生出绑定具体用途的结果。实际开发里可以按这个逻辑分配职责:

  • salt:由协议或者会话生成的非秘密随机值,作用是改变 Extract 阶段的输出结果;它可以和握手记录一起存储,不能用固定字符串直接冒充随机 salt。
  • info:描述派生用途的上下文标识,例如 checkout/session/v1/tenant-17、协议版本号、租户 ID 和通信方向。
  • 输出长度:由下游使用的算法决定,AES-256 对应 32 字节密钥,HMAC-SHA256 的密钥长度按照协议约定取值,不要随意对结果做截取或者补零操作。

同一份输入密钥材料只要更换 info 内容,就必须得到不同的派生结果。这样“会话数据加密”和“回调请求签名”就能拿到完全独立的密钥,就算某一把密钥泄露,影响范围也只会限制在对应场景下,不会波及其他业务逻辑。

Java 25 KDF 上下文绑定对比:相同输入更换会话用途 info 后得到不同派生密钥

用测试锁住重复性、隔离性和边界条件

KDF 的测试不能只写断言“返回了 32 个字节”这种弱校验,至少要覆盖三类核心特性:

static byte[] derive(byte[] material, byte[] salt, String purpose)
        throws Exception {
    KDF kdf = KDF.getInstance("HKDF-SHA256");
    HKDFParameterSpec params = HKDFParameterSpec.ofExtract()
            .addIKM(material)
            .addSalt(salt)
            .thenExpand(purpose.getBytes(StandardCharsets.UTF_8), 32);
    return kdf.deriveData(params);
}

// 同样的输入和上下文,应得到同样的 32 字节结果
assertArrayEquals(first, derive(material, salt, "checkout/session/v1"));

// 用途变化后,不应继续复用同一派生结果
assertFalse(Arrays.equals(first, derive(material, salt, "webhook/sign/v1")));
assertEquals(32, first.length);

测试用例可以用固定公开向量,但绝对不能把测试向量、演示用默认 salt 或者示例口令带进生产配置。还要补充一条提供者兼容性检查逻辑:服务启动时主动获取 HKDF-SHA256,如果对应算法不可用就直接快速失败抛出告警,不要悄悄回退到自定义哈希拼接的野路子实现。

几个容易误用的地方

  1. 把 KDF 当成密码存储方案。用户口令属于低熵输入,应该用专门面向密码存储的慢哈希方案,配置合适的工作因子;KDF 不是口令哈希的替代方案。
  2. 忽略用途和版本信息。info 只写 session 实在太宽泛,至少要把协议版本、通信方向或者具体业务用途放到 info 里做区分。
  3. 把 salt 当成秘密凭据。salt 的作用是改变派生过程的输出,本身不需要保密;它可以跟着协议元数据一起传输,只要保证生成逻辑和关联关系可靠就行。
  4. 忽略输出长度的约束。派生结果的输出长度必须由下游算法和协议明确定义,调用方不能拿到一段任意长度的字节后凭自己的感觉随便截取。
  5. 没有回归固定测试向量。算法名称、加密提供者、salt、info 或者输出长度发生变更时,固定测试向量能第一时间发现协议不兼容问题。

上线前的 Java 25 KDF 检查表

  • 确认构建环境和线上运行环境都是支持 JEP 510 的 Java 25 版本,记录对应加密提供者的来源。
  • 明确密钥材料的来源、存储周期和访问权限,禁止把低熵口令直接当做 KDF 的输入。
  • 给每一类不同的业务用途定义独立的 info 前缀和对应的协议版本号。
  • 对 salt、info、派生长度和算法名做全量参数校验,非法输入直接返回错误,不要向下传递。
  • 准备好固定向量用例和上下文隔离测试用例,升级 JDK 或者加密提供者之后重新跑一遍所有用例。

如果项目只是需要一把全新的随机 AES 密钥,直接用 KeyGenerator 生成会更简单;只有当你有一份已有的密钥材料,需要按照协议派生出多把用途隔离的子密钥时,KDF 才能发挥它的价值。

相关问题

Java 25 KDF API 支持哪些算法?

JEP 510 只定义了标准服务接口,实际可用的算法列表取决于 JDK 安装的加密提供者。本文示例用的是 HKDF-SHA256,部署前要在目标运行环境里提前查询并验证算法名称,不要默认所有提供者都提供完全一致的实现。

HKDF 的 salt 必须保密吗?

通常不需要保密。salt 主要作用于 Extract 阶段,核心要求是生成方式安全、关联关系正确、和协议约定保持一致;它不能代替密钥材料本身,也不能单独承担认证的职责。

info 可以为空吗?

底层 API 可能允许传入空的 info 字段,但业务协议层面通常不应该省略用途上下文。把用途、版本、租户或者通信方向编码到 info 里,能大幅降低不同场景误用同一派生结果的风险。

KDF 输出可以直接当登录密码吗?

不建议这么做。登录密码属于低熵、容易被暴力猜测的输入,应该用专门的密码存储方案处理;KDF 更适合处理本身就有足够高熵的密钥材料,完成协议层面的派生操作。

把派生规则当成协议的一部分

Java 25 的 KDF API 只解决了“怎么调用标准派生服务”的问题,不会替团队决定密钥生命周期管理、协议字段定义和权限控制模型。把派生算法、salt 规则、info 字段规范、输出长度和加密提供者要求都写进正式协议文档和测试向量里,业务代码只负责传入经过校验的合法参数,后续版本升级时才不会因为一行 API 替换就意外改变密钥的语义。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>