Appearance
Keycloak 国密 SM4 对称加密 SPI 实现:CBC 模式与 JWE 内容加密集成的完整方案
作者: 必码 | bima.cc
前言
随着《中华人民共和国密码法》的正式实施以及网络安全等级保护 2.0(等保 2.0)标准的全面推进,国密算法(SM2、SM3、SM4)在政务、金融、能源、交通等关键信息基础设施领域的应用已从"推荐使用"升级为"强制要求"。作为全球领先的开源身份与访问管理(IAM)平台,Keycloak 凭借其丰富的协议支持、灵活的 SPI 扩展机制和完善的联邦认证能力,被广泛应用于各类企业级场景。然而,Keycloak 原生仅支持 RSA、AES、HMAC 等国际标准密码算法,对国密算法的支持完全缺失。这意味着,在等保 2.0 合规场景下,Keycloak 的令牌签名、密码哈希、内容加密等核心安全功能都无法满足国密要求,成为政企客户落地部署的一大障碍。
本文基于 keycloak-sandbox 项目中 spi-sm-crypto-extension 模块的完整实现,系统性地讲解如何通过 Keycloak 的 SPI(Service Provider Interface)机制,将国密 SM4 对称加密算法以 CBC 模式集成到 Keycloak 的 JWE(JSON Web Encryption)内容加密体系中。文章不仅涵盖 SM4 算法的核心原理与实现细节,还将扩展到 SM2 非对称加密、SM3 哈希算法以及密钥管理等完整的国密 SPI 体系,为读者呈现一个可落地、可扩展、可维护的国密集成完整方案。
读者受众:
- 负责政务、金融等领域 Keycloak 部署,需要满足等保 2.0 合规要求的安全工程师
- 对国密算法和密码学 SPI 扩展感兴趣的后端开发者
- 正在进行 Keycloak 深度定制,需要理解 ContentEncryptionProvider、SignatureProvider、HashProvider、KeyProvider 四大 SPI 接口的架构师
- 希望了解 BouncyCastle 国密支持与 Keycloak SPI 集成最佳实践的技术爱好者
第一章 国密算法体系与 Keycloak 集成挑战
1.1 SM2/SM3/SM4 国密算法概述
国密算法是由中国国家密码管理局制定的一系列密码算法标准,旨在保障国家信息安全。目前最核心的三大算法分别是 SM2、SM3 和 SM4,它们分别对应国际标准中的不同密码学功能:
┌─────────────────────────────────────────────────────────────────────┐
│ 国密算法体系总览 │
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ SM2 │ │ SM3 │ │ SM4 │ │
│ │ 非对称加密 │ │ 哈希算法 │ │ 对称加密 │ │
│ │ │ │ │ │ │ │
│ │ · 数字签名 │ │ · 消息摘要 │ │ · 数据加密 │ │
│ │ · 密钥交换 │ │ · 数字签名 │ │ · 数据解密 │ │
│ │ · 公钥加密 │ │ · MAC 生成 │ │ · CBC 模式 │ │
│ │ │ │ │ │ · ECB 模式 │ │
│ │ 基于椭圆曲线│ │ 输出256位 │ │ 密钥长度128位│ │
│ │ sm2p256v1 │ │ │ │ 分组长度128位│ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 对应国际标准 │ │
│ │ SM2 ≈ RSA/ECDSA SM3 ≈ SHA-256 SM4 ≈ AES-128 │ │
│ └─────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘SM2 椭圆曲线公钥密码算法:SM2 是一种基于椭圆曲线密码学(ECC)的公钥密码算法,使用国家密码管理局指定的 sm2p256v1 椭圆曲线。SM2 的密钥长度为 256 位,安全强度相当于 RSA-3072,但密钥长度仅为 RSA 的 1/12,在计算效率和存储效率上具有显著优势。SM2 主要包含三个功能子集:数字签名算法(SM2-2)、密钥交换协议(SM2-3)和公钥加密算法(SM2-4)。在 Keycloak 场景中,SM2 主要用于 JWT 令牌的签名与验签。
SM3 密码杂凑算法:SM3 是一种密码杂凑(哈希)算法,输出 256 位的杂凑值。SM3 的安全性与 SHA-256 相当,但在压缩函数的设计上采用了更加复杂的消息扩展和置换结构,以抵抗已知的密码分析攻击。在 Keycloak 场景中,SM3 可用于密码哈希存储、消息完整性校验和数字签名中的摘要生成。
SM4 分组密码算法:SM4 是一种分组密码算法,分组长度为 128 位,密钥长度为 128 位。SM4 采用非平衡 Feistel 结构,共进行 32 轮迭代运算。SM4 支持多种工作模式,包括 ECB、CBC、CFB、OFB 和 CTR 模式。在 Keycloak 场景中,SM4 主要用于 JWE(JSON Web Encryption)的内容加密,保护令牌中的敏感信息。
1.2 Keycloak 密码学 SPI 架构
Keycloak 的密码学功能通过 SPI 机制实现,其核心架构基于 Provider-Factory 模型。每一个密码学功能都由一个 SPI 接口定义,具体的算法实现通过 Provider 提供,而 Provider 的创建和管理由 ProviderFactory 负责。
┌─────────────────────────────────────────────────────────────────────┐
│ Keycloak 密码学 SPI 架构 │
│ │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ Keycloak Core │ │
│ │ │ │
│ │ ┌─────────────┐ ┌─────────────┐ ┌──────────────────┐ │ │
│ │ │ Token Mgr │ │ Password │ │ JWE / JWS │ │ │
│ │ │ (签名/验签) │ │ (哈希/验证) │ │ (内容加密) │ │ │
│ │ └──────┬──────┘ └──────┬──────┘ └────────┬─────────┘ │ │
│ │ │ │ │ │ │
│ │ ▼ ▼ ▼ │ │
│ │ ┌──────────────────────────────────────────────────┐ │ │
│ │ │ SPI 接口层 │ │ │
│ │ │ SignatureProvider │ HashProvider │ ContentEnc.. │ │ │
│ │ │ KeyProvider │ │ │ │ │
│ │ └──────────────────────┬───────────────────────────┘ │ │
│ │ │ │ │
│ │ ┌───────────────┼───────────────┐ │ │
│ │ ▼ ▼ ▼ │ │
│ │ ┌────────────┐ ┌────────────┐ ┌────────────┐ │ │
│ │ │ Provider │ │ Provider │ │ Provider │ │ │
│ │ │ Factory │ │ Factory │ │ Factory │ │ │
│ │ │ (创建管理) │ │ (创建管理) │ │ (创建管理) │ │ │
│ │ └─────┬──────┘ └─────┬──────┘ └─────┬──────┘ │ │
│ │ │ │ │ │ │
│ └────────┼───────────────┼───────────────┼─────────────────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ 具体算法实现层 │ │
│ │ RSA/ECDSA │ SHA-256 │ AES-GCM │ SM2 │ SM3 │ SM4 │ │
│ └──────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘Keycloak 密码学 SPI 的核心接口包括:
| SPI 接口 | 功能描述 | 国密实现 |
|---|---|---|
SignatureProvider | 数字签名与验签 | SMSignatureProvider(SM2) |
HashProvider | 密码哈希与验证 | SMHashProvider(SM3) |
ContentEncryptionProvider | JWE 内容加密与解密 | SMContentEncryptionProvider(SM4) |
KeyProvider | 密钥生成与管理 | SMKeyProvider(SM4 密钥) |
这种分层架构的优势在于:上层业务逻辑(如令牌管理、密码验证)只依赖 SPI 接口,不关心具体的算法实现;下层算法实现可以独立替换和升级,不影响上层逻辑。这为我们集成国密算法提供了天然的扩展点。
1.3 国密集成的技术路线
在 Keycloak 中集成国密算法,存在多种可能的技术路线。我们需要在兼容性、性能、可维护性和合规性之间做出权衡:
路线一:JCA/JCE Provider 替换 通过注册 BouncyCastle 作为全局 JCA Provider,替换 JDK 默认的密码学实现。这种方式最简单,但会影响所有使用 JCA 的组件,可能引入兼容性风险,且无法针对 Keycloak 的特定场景进行优化。
路线二:Keycloak SPI 扩展(本文采用) 通过实现 Keycloak 的四大密码学 SPI 接口,将国密算法作为独立的 Provider 注册到 Keycloak 中。这种方式对 Keycloak 原有功能零侵入,可以通过管理控制台灵活配置,且能够充分利用 Keycloak 的密钥管理、算法协商等内置能力。
路线三:自定义协议层 在 HTTP 层或协议层实现国密适配,例如通过反向代理进行令牌的加解密转换。这种方式实现复杂度高,且无法覆盖 Keycloak 内部的密码学操作。
本文采用路线二,即 Keycloak SPI 扩展方式。其整体技术架构如下:
┌─────────────────────────────────────────────────────────────────────┐
│ 国密 SPI 扩展整体架构 │
│ │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ spi-sm-crypto-extension │ │
│ │ │ │
│ │ ┌──────────────┐ ┌──────────────┐ ┌───────────────┐ │ │
│ │ │ SM2Util │ │ SM3Util │ │ SM4Util │ │ │
│ │ │ (底层算法) │ │ (底层算法) │ │ (底层算法) │ │ │
│ │ └──────┬───────┘ └──────┬───────┘ └──────┬────────┘ │ │
│ │ │ │ │ │ │
│ │ ▼ ▼ ▼ │ │
│ │ ┌──────────────┐ ┌──────────────┐ ┌───────────────┐ │ │
│ │ │ SMSignature │ │ SMHash │ │ SMContent │ │ │
│ │ │ Provider │ │ Provider │ │ Encryption │ │ │
│ │ │ │ │ │ │ Provider │ │ │
│ │ └──────┬───────┘ └──────┬───────┘ └──────┬────────┘ │ │
│ │ │ │ │ │ │
│ │ ▼ ▼ ▼ │ │
│ │ ┌──────────────┐ ┌──────────────┐ ┌───────────────┐ │ │
│ │ │ SMSignature │ │ SMHash │ │ SMContent │ │ │
│ │ │ Provider │ │ Provider │ │ Encryption │ │ │
│ │ │ Factory │ │ Factory │ │ Provider │ │ │
│ │ │ (bima-sm- │ │ (bima-sm- │ │ Factory │ │ │
│ │ │ signature) │ │ hash) │ │ (bima-sm- │ │ │
│ │ │ │ │ │ │ content- │ │ │
│ │ │ │ │ │ │ encryption) │ │ │
│ │ └──────────────┘ └──────────────┘ └───────┬───────┘ │ │
│ │ │ │ │
│ │ ┌──────────────┐ │ │ │
│ │ │ SMKey │ ◄───────────────────────────┘ │ │
│ │ │ Provider │ │ │
│ │ │ Factory │ │ │
│ │ │ (sm- │ │ │
│ │ │ generated) │ │ │
│ │ └──────────────┘ │ │
│ └───────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ BouncyCastle Provider │ │
│ │ bcprov-jdk18on 1.78.1 + bcpkix-jdk18on 1.78.1 │ │
│ └───────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘第二章 BouncyCastle 国密支持
2.1 BouncyCastle Provider 注册
BouncyCastle 是 Java 生态中最成熟的密码学库之一,它不仅提供了对国际标准密码算法的全面支持,还率先实现了中国国家密码标准(SM2/SM3/SM4)。在 Keycloak 国密 SPI 扩展中,BouncyCastle 作为底层密码学引擎,为上层的 SM2Util、SM3Util、SM4Util 提供算法实现支持。
BouncyCastle 通过 JCA(Java Cryptography Architecture)的 Provider 机制注册到 Java 运行时环境中。注册方式有两种:
方式一:静态注册(推荐用于生产环境)
在 $JAVA_HOME/conf/security/java.security 文件中添加:
properties
# 在 security.provider.1=SunPKCS11-NSS 之后添加
security.provider.11=org.bouncycastle.jce.provider.BouncyCastleProvider静态注册的优点是全局生效,且在 JVM 启动时就完成注册,避免了时序问题。缺点是需要修改 JDK 配置文件,在容器化部署中可能不够灵活。
方式二:动态注册(推荐用于开发和测试)
在代码中通过 Security.addProvider() 方法动态注册:
java
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import java.security.Security;
/**
* BouncyCastle Provider 注册工具
* 注意:在实际项目中,应确保此注册在所有密码学操作之前完成
*/
public class BCProviderRegistrar {
private static final String BC_PROVIDER_NAME = "BC";
/**
* 注册 BouncyCastle Provider(如果尚未注册)
* @return true 表示新注册,false 表示已存在
*/
public static synchronized boolean registerIfAbsent() {
if (Security.getProvider(BC_PROVIDER_NAME) == null) {
Security.addProvider(new BouncyCastleProvider());
return true;
}
return false;
}
/**
* 获取 BouncyCastle Provider
*/
public static BouncyCastleProvider getProvider() {
registerIfAbsent();
return (BouncyCastleProvider) Security.getProvider(BC_PROVIDER_NAME);
}
}在 Keycloak SPI 扩展中,推荐在 ProviderFactory 的 init() 方法中触发注册,确保在 Keycloak 加载 SPI 扩展时 BouncyCastle Provider 已经就绪:
java
public class SMContentEncryptionProviderFactory
implements ContentEncryptionProviderFactory {
@Override
public void init(Config.Scope config) {
// 确保 BouncyCastle Provider 已注册
BCProviderRegistrar.registerIfAbsent();
}
// ... 其他方法
}2.2 bcprov-jdk18on 与 bcpkix-jdk18on
BouncyCastle 提供了两个核心依赖包,它们在国密集成中扮演不同角色:
| 依赖包 | 版本 | 功能 | 国密相关类 |
|---|---|---|---|
bcprov-jdk18on | 1.78.1 | 核心密码学提供者 | SM2Signer, SM3Digest, SM4Engine |
bcpkix-jdk18on | 1.78.1 | PKI/X.509 扩展 | SM2ParameterSpec, ECNamedCurveTable |
Maven 依赖配置:
xml
<dependencies>
<!-- BouncyCastle 核心密码学 Provider -->
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk18on</artifactId>
<version>1.78.1</version>
</dependency>
<!-- BouncyCastle PKI/X.509 扩展(用于 SM2 密钥对生成等) -->
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcpkix-jdk18on</artifactId>
<version>1.78.1</version>
</dependency>
<!-- Keycloak 核心依赖(provided 作用域) -->
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-core</artifactId>
<version>${keycloak.version}</version>
<scope>provided</scope>
</dependency>
</dependencies>bcprov-jdk18on 提供了底层密码学算法的实现,包括:
SM4Engine:SM4 分组密码引擎,支持 ECB 和 CBC 等模式SM3Digest:SM3 杂凑算法的 MessageDigest 实现SM2Signer:SM2 数字签名算法的 Signer 实现PKCS7Padding:PKCS#7 填充方案(用于 SM4 CBC 模式)
bcpkix-jdk18on 提供了 PKI 相关的高级功能,包括:
ECNamedCurveTable:命名椭圆曲线查找表,用于获取 sm2p256v1 曲线参数ECNamedCurveParameterSpec:椭圆曲线参数规范ECKeyPairGenerator:椭圆曲线密钥对生成器
2.3 SM2P256V1 曲线与密钥生成
SM2 算法基于一条特定的 256 位椭圆曲线——sm2p256v1。这条曲线由国家密码管理局指定,其参数与 SEC 标准的 secp256r1(也叫 P-256)不同,因此不能直接使用 JDK 内置的 EC 实现。
BouncyCastle 通过 ECNamedCurveTable 提供了对 sm2p256v1 曲线的支持。密钥生成的核心流程如下:
┌─────────────────────────────────────────────────────────────────────┐
│ SM2 密钥对生成流程 │
│ │
│ 1. 获取曲线参数 │
│ ECNamedCurveTable.getParameterSpec("sm2p256v1") │
│ │ │
│ ▼ │
│ 2. 初始化密钥对生成器 │
│ ECKeyPairGenerator generator = new ECKeyPairGenerator() │
│ generator.init(new ECKeyGenerationParameters(params, random)) │
│ │ │
│ ▼ │
│ 3. 生成密钥对 │
│ AsymmetricCipherKeyPair keyPair = generator.generateKeyPair() │
│ │ │
│ ▼ │
│ 4. 提取公钥和私钥 │
│ ECPublicKeyParameters pubKey = keyPair.getPublic() │
│ ECPrivateKeyParameters privKey = keyPair.getPrivate() │
│ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ sm2p256v1 曲线参数(简化示意) │ │
│ │ · 素数域 p: 256 位大素数 │ │
│ │ · 曲线方程: y² = x³ + ax + b (mod p) │ │
│ │ · 基点 G: 曲线上的一个阶为大素数 n 的点 │ │
│ │ · 阶 n: 基点 G 的阶(256 位大素数) │ │
│ │ · 公钥 Q = d × G(d 为私钥,Q 为公钥) │ │
│ └──────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘以下是密钥生成的教学简化代码:
java
import org.bouncycastle.crypto.AsymmetricCipherKeyPair;
import org.bouncycastle.crypto.generators.ECKeyPairGenerator;
import org.bouncycastle.crypto.params.ECKeyGenerationParameters;
import org.bouncycastle.crypto.params.ECNamedDomainParameters;
import org.bouncycastle.pqc.math.linearalgebra.ByteUtils;
import java.security.SecureRandom;
/**
* SM2 密钥对生成示例(教学简化版)
*/
public class SM2KeyGenerationExample {
/**
* 生成 SM2 密钥对
* @return 包含公钥和私钥的字节数组
*/
public static byte[][] generateKeyPair() {
// 1. 获取 sm2p256v1 曲线参数
ECNamedDomainParameters curveParams = getSM2CurveParams();
// 2. 初始化密钥对生成器
SecureRandom random = new SecureRandom();
ECKeyPairGenerator generator = new ECKeyPairGenerator();
ECKeyGenerationParameters genParams =
new ECKeyGenerationParameters(curveParams, random);
generator.init(genParams);
// 3. 生成密钥对
AsymmetricCipherKeyPair keyPair = generator.generateKeyPair();
// 4. 提取公钥和私钥的编码字节
byte[] publicKeyBytes = encodePublicKey(keyPair.getPublic());
byte[] privateKeyBytes = encodePrivateKey(keyPair.getPrivate());
return new byte[][]{publicKeyBytes, privateKeyBytes};
}
private static ECNamedDomainParameters getSM2CurveParams() {
// 通过 BouncyCastle 获取 sm2p256v1 曲线参数
// 实际实现中使用 ECNamedCurveTable.getParameterSpec("sm2p256v1")
// 此处为教学示意,省略具体参数提取细节
return null; // 教学占位
}
private static byte[] encodePublicKey(Object publicKey) {
// 将公钥参数编码为字节数组(未压缩格式:04 || x || y)
// 教学示意
return null;
}
private static byte[] encodePrivateKey(Object privateKey) {
// 将私钥参数编码为字节数组
// 教学示意
return null;
}
}安全提示: 在生产环境中,SM2 私钥必须使用硬件安全模块(HSM)或密钥管理服务(KMS)进行保护,严禁以明文形式存储在文件系统或数据库中。Keycloak 的
KeyProviderSPI 提供了与 HSM/KMS 集成的扩展点。
第三章 SM4 对称加密核心实现
3.1 SM4 算法原理
SM4 是中国国家密码管理局于 2012 年发布的分组密码算法(原名 SMS4),是我国第一个商用密码算法。SM4 的设计目标是在软件和硬件上都能高效实现,同时提供足够的安全强度。
SM4 算法核心参数:
| 参数 | 值 | 说明 |
|---|---|---|
| 分组长度 | 128 位 | 每次加密/解密处理 16 字节数据 |
| 密钥长度 | 128 位 | 加密密钥为 16 字节 |
| 轮数 | 32 轮 | 迭代 32 次非线性变换 |
| 密钥扩展轮数 | 32 轮 | 从加密密钥生成 32 个轮密钥 |
| S 盒 | 8×8 | 非线性变换的核心组件 |
SM4 加密流程(简化):
┌─────────────────────────────────────────────────────────────────────┐
│ SM4 加密算法流程 │
│ │
│ 输入: 明文 (128 bit) + 密钥 (128 bit) │
│ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ 密钥扩展 │ │
│ │ │ │
│ │ MK (128 bit) │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │
│ │ │ FK 变换 │ → │ 轮函数F │ → ... │ 轮函数F │ │ │
│ │ └─────────┘ └─────────┘ └─────────┘ │ │
│ │ │ │ │ │ │
│ │ └───────────┴───────────────────┘ │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ rk0, rk1, ..., rk31 (32 个轮密钥) │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ 32 轮 Feistel 迭代 │ │
│ │ │ │
│ │ (X0, X1, X2, X3) ← 明文 │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ┌──────────────────────────────────────────┐ │ │
│ │ │ Xi+4 = Xi ⊕ T(Xi+1 ⊕ Xi+2 ⊕ Xi+3 ⊕ rki) │ │
│ │ │ │ │ │
│ │ │ T 变换 = L(τ(A)) │ │ │
│ │ │ · τ: 非线性变换(4 个并行 S 盒替换) │ │ │
│ │ │ · L: 线性变换(循环移位 + 异或) │ │ │
│ │ └──────────────────────────────────────────┘ │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ (X35, X34, X33, X32) → 反序变换 → 密文 (128 bit) │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │
│ 输出: 密文 (128 bit) │
└─────────────────────────────────────────────────────────────────────┘SM4 的核心是轮函数 T,它由非线性变换 τ 和线性变换 L 组成:
- 非线性变换 τ:将 32 位输入分成 4 个字节,每个字节通过一个 8×8 的 S 盒进行替换。S 盒是 SM4 安全性的核心,它提供了算法所需的非线性特性。
- 线性变换 L:对 τ 的输出进行循环移位和异或运算。具体为:
L(B) = B ⊕ (B<<<2) ⊕ (B<<<10) ⊕ (B<<<18) ⊕ (B<<<24),其中<<<表示 32 位循环左移。
SM4 的解密算法与加密算法具有相同的结构,只是轮密钥的使用顺序相反(rk31, rk30, ..., rk0)。这种对称性设计使得 SM4 的硬件实现更加高效。
3.2 CBC 模式与 PKCS7 填充
SM4 作为分组密码算法,本身只能加密固定长度(128 位)的数据块。要加密任意长度的数据,需要使用分组密码的工作模式。本文实现采用 CBC(Cipher Block Chaining,密码分组链接)模式,这是最常用也最安全的分组密码工作模式之一。
CBC 模式原理:
┌─────────────────────────────────────────────────────────────────────┐
│ CBC 加密模式 │
│ │
│ 明文: P1 | P2 | P3 | ... | Pn │
│ 密钥: K (128 bit) │
│ IV: 初始化向量 (128 bit, 随机生成) │
│ │
│ 加密过程: │
│ │
│ IV ──→ ⊕ ──→ [SM4 加密] ──→ C1 │
│ P1 ──→↗ │
│ │
│ C1 ──→ ⊕ ──→ [SM4 加密] ──→ C2 │
│ P2 ──→↗ │
│ │
│ C2 ──→ ⊕ ──→ [SM4 加密] ──→ C3 │
│ P3 ──→↗ │
│ │
│ ... │
│ │
│ Ci-1 → ⊕ ──→ [SM4 加密] ──→ Cn │
│ Pn ──→↗ │
│ │
│ 密文: IV | C1 | C2 | C3 | ... | Cn │
│ │
│ 特点: │
│ · 相同的明文块在不同位置产生不同的密文块 │
│ · 第一个明文块的安全性依赖于 IV 的随机性 │
│ · 无法并行加密(但可以并行解密) │
│ · 密文长度是明文长度的整数倍(需要填充) │
└─────────────────────────────────────────────────────────────────────┘CBC 模式的核心思想是:每个明文块在加密之前,先与前一个密文块进行异或运算。对于第一个明文块,则与初始化向量(IV)进行异或。这样,即使两个明文块完全相同,只要它们的位置不同或 IV 不同,产生的密文块就不同,从而有效防止了 ECB 模式下的模式泄露问题。
PKCS7 填充方案:
由于 CBC 模式要求数据长度必须是分组长度(128 位 = 16 字节)的整数倍,当明文长度不满足这一要求时,需要进行填充。PKCS7(也叫 PKCS#5)是最常用的填充方案:
┌─────────────────────────────────────────────────────────────────────┐
│ PKCS7 填充规则 │
│ │
│ 填充值 = 需要填充的字节数(1~16) │
│ 每个填充字节的值都等于填充字节数 │
│ │
│ 示例 1: 明文长度 = 13 字节 │
│ ┌──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┐ │
│ │D1│D2│D3│D4│D5│D6│D7│D8│D9│D10│D11│D12│D13│03│03│03│ │
│ └──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┘ │
│ ←──────── 明文 ────────→ ←── 填充 3 字节 ──→ │
│ │
│ 示例 2: 明文长度 = 15 字节 │
│ ┌──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┐ │
│ │D1│D2│D3│D4│D5│D6│D7│D8│D9│D10│D11│D12│D13│D14│D15│01│ │
│ └──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┘ │
│ ←──────────── 明文 ──────────→ 填充 1 字节 │
│ │
│ 示例 3: 明文长度 = 16 字节(恰好对齐) │
│ ┌──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┐ │
│ │D1│D2│...│D16│16│16│16│16│16│16│16│16│16│16│16│16│16│16│16│16│16│16│16│16│16│16│16│16│16│16│16│16│ │
│ └──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┘ │
│ ←──── 明文 ────→ ←──────── 填充 16 字节(完整块)─────────→ │
│ │
│ 注意: 即使明文长度已对齐,也必须填充一个完整块 │
│ 这样在解密时可以明确区分数据与填充 │
└─────────────────────────────────────────────────────────────────────┘PKCS7 填充的一个重要特性是:即使明文长度恰好是分组大小的整数倍,也必须添加一个完整的填充块(16 个值为 0x10 的字节)。这保证了在解密时,最后一个字节的值总是可以正确指示填充长度,不会与数据混淆。
3.3 随机 IV 生成策略
在 CBC 模式中,初始化向量(IV)的安全性至关重要。如果 IV 是固定的或可预测的,攻击者可能利用已知的明文-密文对进行密码分析。因此,IV 必须满足以下要求:
- 不可预测性:IV 必须使用密码学安全的随机数生成器(CSPRNG)生成
- 唯一性:同一密钥下,每个加密操作的 IV 都应该不同
- 不保密:IV 不需要保密,但必须与密文一起传输以便解密
在 Java 中,java.security.SecureRandom 是推荐的 CSPRNG 实现:
java
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import java.security.SecureRandom;
/**
* 随机 IV 生成策略(教学简化版)
*/
public class IVGenerationExample {
private static final int IV_LENGTH = 16; // SM4 分组长度 = 128 位 = 16 字节
/**
* 生成随机 IV
* 使用 SecureRandom 确保密码学安全性
*/
public static byte[] generateRandomIV() {
byte[] iv = new byte[IV_LENGTH];
SecureRandom secureRandom = new SecureRandom();
secureRandom.nextBytes(iv);
return iv;
}
/**
* 创建 IvParameterSpec
* 用于初始化 CBC 模式的 Cipher
*/
public static IvParameterSpec createIVSpec() {
return new IvParameterSpec(generateRandomIV());
}
}性能优化提示: 在高并发场景下,频繁创建
SecureRandom实例可能成为性能瓶颈。建议在应用启动时初始化一个SecureRandom实例并复用。在 Keycloak SPI 中,可以在 ProviderFactory 的init()方法中完成初始化,然后在 Provider 实例中共享使用。
3.4 IV 与密文合并存储
由于 CBC 模式需要 IV 才能解密,IV 必须与密文一起存储和传输。常见的存储格式有两种:
方案一:IV 前置(本文采用)
┌─────────────────────────────────────────────────────────────────────┐
│ IV 前置存储格式 │
│ │
│ 存储格式: [IV (16 字节)] [密文 (n 字节)] │
│ │
│ 字节布局: │
│ ┌────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬───┬────┐ │
│ │IV0 │IV1 │... │IV15│C0 │C1 │... │Cn-2│Cn-1│ │ │ │ │ │
│ └────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴───┴────┘ │
│ ←── IV (16 bytes) ──→←────────── 密文 (n bytes) ──────────→ │
│ │
│ 总长度 = 16 + n 字节 │
│ │
│ 优点: │
│ · 解密时直接从头部读取 IV,无需额外参数 │
│ · 格式简洁,适合序列化传输 │
│ · 与 JWE 的 "additional authenticated data" 模型兼容 │
└─────────────────────────────────────────────────────────────────────┘方案二:IV 与密文分开存储
IV 和密文作为独立字段存储,例如在 JWE 的 header 中通过 iv 参数传递 IV。这种方式在 JWE 标准中更为常见,但在 Keycloak 的 ContentEncryptionProvider SPI 中,IV 前置方案更加简洁。
在 spi-sm-crypto-extension 模块中,SM4Util 采用 IV 前置方案,加密时将 IV 拼接到密文前面,解密时先提取前 16 字节作为 IV,剩余部分作为密文:
java
/**
* IV 与密文合并/分离(教学简化版)
*/
public class IVCipherMergeExample {
private static final int IV_LENGTH = 16;
/**
* 加密后合并 IV 和密文
* @param iv 初始化向量(16 字节)
* @param cipherText 密文
* @return IV + 密文的合并字节数组
*/
public static byte[] mergeIVAndCipherText(byte[] iv, byte[] cipherText) {
byte[] result = new byte[IV_LENGTH + cipherText.length];
System.arraycopy(iv, 0, result, 0, IV_LENGTH);
System.arraycopy(cipherText, 0, result, IV_LENGTH, cipherText.length);
return result;
}
/**
* 从合并数据中分离 IV 和密文
* @param merged IV + 密文的合并字节数组
* @return 数组,[0] = IV, [1] = 密文
*/
public static byte[][] separateIVAndCipherText(byte[] merged) {
if (merged.length < IV_LENGTH) {
throw new IllegalArgumentException("数据长度不足以包含 IV");
}
byte[] iv = new byte[IV_LENGTH];
byte[] cipherText = new byte[merged.length - IV_LENGTH];
System.arraycopy(merged, 0, iv, 0, IV_LENGTH);
System.arraycopy(merged, IV_LENGTH, cipherText, 0, cipherText.length);
return new byte[][]{iv, cipherText};
}
}3.5 加密与解密流程
将以上所有组件组合起来,SM4 CBC 模式的完整加密和解密流程如下:
┌─────────────────────────────────────────────────────────────────────┐
│ SM4-CBC 加密流程 │
│ │
│ 输入: 明文 (byte[]), 密钥 (byte[16]) │
│ │
│ Step 1: 生成随机 IV │
│ ┌──────────────────────────────────────────┐ │
│ │ SecureRandom.nextBytes(iv[16]) │ │
│ └──────────────────┬───────────────────────┘ │
│ │ │
│ Step 2: 初始化 Cipher │
│ ┌──────────────────▼───────────────────────┐ │
│ │ Cipher cipher = Cipher.getInstance( │ │
│ │ "SM4/CBC/PKCS7Padding", "BC") │ │
│ │ cipher.init(Cipher.ENCRYPT_MODE, │ │
│ │ new SecretKeySpec(key, "SM4"), │ │
│ │ new IvParameterSpec(iv)) │ │
│ └──────────────────┬───────────────────────┘ │
│ │ │
│ Step 3: 加密 │
│ ┌──────────────────▼───────────────────────┐ │
│ │ byte[] cipherText = cipher.doFinal( │ │
│ │ plainText) │ │
│ └──────────────────┬───────────────────────┘ │
│ │ │
│ Step 4: 合并 IV 和密文 │
│ ┌──────────────────▼───────────────────────┐ │
│ │ byte[] result = IV + cipherText │ │
│ └──────────────────┬───────────────────────┘ │
│ │ │
│ ▼ │
│ 输出: [IV (16 bytes)] [密文 (n bytes)] │
│ │
├─────────────────────────────────────────────────────────────────────┤
│ SM4-CBC 解密流程 │
│ │
│ 输入: [IV (16 bytes)] [密文 (n bytes)], 密钥 (byte[16]) │
│ │
│ Step 1: 分离 IV 和密文 │
│ ┌──────────────────────────────────────────┐ │
│ │ iv = merged[0:16] │ │
│ │ cipherText = merged[16:] │ │
│ └──────────────────┬───────────────────────┘ │
│ │ │
│ Step 2: 初始化 Cipher │
│ ┌──────────────────▼───────────────────────┐ │
│ │ Cipher cipher = Cipher.getInstance( │ │
│ │ "SM4/CBC/PKCS7Padding", "BC") │ │
│ │ cipher.init(Cipher.DECRYPT_MODE, │ │
│ │ new SecretKeySpec(key, "SM4"), │ │
│ │ new IvParameterSpec(iv)) │ │
│ └──────────────────┬───────────────────────┘ │
│ │ │
│ Step 3: 解密 │
│ ┌──────────────────▼───────────────────────┐ │
│ │ byte[] plainText = cipher.doFinal( │ │
│ │ cipherText) │ │
│ │ // PKCS7 填充自动去除 │ │
│ └──────────────────┬───────────────────────┘ │
│ │ │
│ ▼ │
│ 输出: 明文 (byte[]) │
└─────────────────────────────────────────────────────────────────────┘以下是 SM4Util 的教学简化实现:
java
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.security.SecureRandom;
import java.util.Arrays;
/**
* SM4 对称加密工具(教学简化版)
* 实现了 CBC 模式 + PKCS7 填充 + 随机 IV
*
* 注意:此为教学简化版本,省略了异常处理细节和输入校验。
* 生产环境请使用完整的加密工具类。
*/
public class SM4UtilExample {
private static final String ALGORITHM = "SM4";
private static final String TRANSFORMATION = "SM4/CBC/PKCS7Padding";
private static final String PROVIDER = "BC";
private static final int IV_LENGTH = 16;
private static final int KEY_LENGTH = 16;
private final SecureRandom secureRandom;
public SM4UtilExample() {
this.secureRandom = new SecureRandom();
}
/**
* SM4-CBC 加密
*
* @param plainText 明文
* @param key 16 字节密钥
* @return IV + 密文(IV 在前 16 字节)
*/
public byte[] encrypt(byte[] plainText, byte[] key) {
validateKey(key);
// Step 1: 生成随机 IV
byte[] iv = generateIV();
// Step 2: 初始化 Cipher(加密模式)
Cipher cipher = createCipher(Cipher.ENCRYPT_MODE, key, iv);
// Step 3: 执行加密(自动 PKCS7 填充)
byte[] cipherText = cipher.doFinal(plainText);
// Step 4: 合并 IV 和密文
return mergeIVAndCipherText(iv, cipherText);
}
/**
* SM4-CBC 解密
*
* @param mergedData IV + 密文(IV 在前 16 字节)
* @param key 16 字节密钥
* @return 明文
*/
public byte[] decrypt(byte[] mergedData, byte[] key) {
validateKey(key);
validateMergedData(mergedData);
// Step 1: 分离 IV 和密文
byte[] iv = Arrays.copyOfRange(mergedData, 0, IV_LENGTH);
byte[] cipherText = Arrays.copyOfRange(mergedData, IV_LENGTH,
mergedData.length);
// Step 2: 初始化 Cipher(解密模式)
Cipher cipher = createCipher(Cipher.DECRYPT_MODE, key, iv);
// Step 3: 执行解密(自动去除 PKCS7 填充)
return cipher.doFinal(cipherText);
}
/**
* 创建并初始化 Cipher
*/
private Cipher createCipher(int mode, byte[] key, byte[] iv) {
Cipher cipher = Cipher.getInstance(TRANSFORMATION, PROVIDER);
SecretKeySpec keySpec = new SecretKeySpec(key, ALGORITHM);
IvParameterSpec ivSpec = new IvParameterSpec(iv);
cipher.init(mode, keySpec, ivSpec);
return cipher;
}
/**
* 生成随机 IV
*/
private byte[] generateIV() {
byte[] iv = new byte[IV_LENGTH];
secureRandom.nextBytes(iv);
return iv;
}
/**
* 合并 IV 和密文
*/
private byte[] mergeIVAndCipherText(byte[] iv, byte[] cipherText) {
byte[] result = new byte[iv.length + cipherText.length];
System.arraycopy(iv, 0, result, 0, iv.length);
System.arraycopy(cipherText, 0, result, iv.length, cipherText.length);
return result;
}
private void validateKey(byte[] key) {
if (key == null || key.length != KEY_LENGTH) {
throw new IllegalArgumentException(
"密钥长度必须为 " + KEY_LENGTH + " 字节");
}
}
private void validateMergedData(byte[] data) {
if (data == null || data.length <= IV_LENGTH) {
throw new IllegalArgumentException("加密数据格式无效");
}
}
}安全注意事项:
- SM4 密钥必须使用安全的密钥派生函数(KDF)生成,禁止使用硬编码密钥
- 每次加密必须使用不同的随机 IV,严禁重复使用 IV
- 密钥和 IV 必须通过安全通道传输,密钥必须保密而 IV 可以公开
- 加密失败时应抛出异常并记录日志,但不应泄露密钥或明文信息
第四章 SM4ContentEncryptionProvider SPI 集成
4.1 ContentEncryptionProvider 接口
Keycloak 的 ContentEncryptionProvider 是 JWE(JSON Web Encryption)内容加密的 SPI 接口。它定义了加密和解密 JWE payload 的标准方法,是 Keycloak 令牌安全体系的重要组成部分。
┌─────────────────────────────────────────────────────────────────────┐
│ ContentEncryptionProvider 在 JWE 中的位置 │
│ │
│ JWE 结构 (JSON Web Encryption): │
│ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ Header (明文 JSON) │ │
│ │ { │ │
│ │ "alg": "RSA-OAEP", ← 密钥加密算法 │ │
│ │ "enc": "SM4-CBC", ← 内容加密算法 │ │
│ │ "iv": "base64url(iv)", ← 初始化向量 │ │
│ │ "tag": "base64url(tag)" ← 认证标签(可选) │ │
│ │ } │ │
│ ├──────────────────────────────────────────────────────────┤ │
│ │ Encrypted Key (密钥加密后的内容加密密钥) │ │
│ │ base64url(CEK_encrypted) │ │
│ ├──────────────────────────────────────────────────────────┤ │
│ │ Initialization Vector (IV) │ │
│ │ base64url(iv) │ │
│ ├──────────────────────────────────────────────────────────┤ │
│ │ Ciphertext (加密后的 payload) │ │
│ │ base64url(SM4-CBC(plaintext)) │ │
│ │ ↑ │ │
│ │ │ │ │
│ │ ContentEncryptionProvider 负责的部分 │ │
│ │ (使用 CEK 对 payload 进行对称加密) │ │
│ ├──────────────────────────────────────────────────────────┤ │
│ │ Authentication Tag (认证标签,GCM 模式需要) │ │
│ │ base64url(tag) │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │
│ ContentEncryptionProvider 职责: │
│ · 使用内容加密密钥(CEK)对 JWE payload 进行加密 │
│ · 生成随机 IV 并与密文一起返回 │
│ · 在解密时使用 CEK 和 IV 还原明文 │
│ · 支持 JWE 标准中定义的各种内容加密算法 │
└─────────────────────────────────────────────────────────────────────┘Keycloak 的 ContentEncryptionProvider 接口定义了以下核心方法:
java
/**
* ContentEncryptionProvider 接口核心方法(教学简化版)
* 基于 Keycloak 源码抽象
*/
public interface ContentEncryptionProvider {
/**
* 加密 JWE payload
*
* @param content 待加密的明文内容
* @param key 内容加密密钥(CEK)
* @param algorithm 加密算法标识
* @param additionalAuthenticatedData 附加认证数据
* @return 加密结果,包含 IV、密文和认证标签
*/
byte[] encrypt(byte[] content, byte[] key, String algorithm,
byte[] additionalAuthenticatedData);
/**
* 解密 JWE payload
*
* @param encryptedContent 加密的密文
* @param key 内容加密密钥(CEK)
* @param algorithm 加密算法标识
* @param additionalAuthenticatedData 附加认证数据
* @param initializationVector 初始化向量
* @param authenticationTag 认证标签
* @return 解密后的明文
*/
byte[] decrypt(byte[] encryptedContent, byte[] key, String algorithm,
byte[] additionalAuthenticatedData,
byte[] initializationVector,
byte[] authenticationTag);
}4.2 SMContentEncryptionProviderFactory
SMContentEncryptionProviderFactory 是 SMContentEncryptionProvider 的工厂类,负责 Provider 的创建、配置和生命周期管理。在 Keycloak SPI 机制中,每个 Provider 都必须有一个对应的 Factory。
java
import org.keycloak.Config;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.provider.ProviderConfigProperty;
import java.util.List;
/**
* SM4 内容加密 Provider 工厂
* SPI ID: bima-sm-content-encryption
*
* 通过 META-INF/services/org.keycloak.crypto.ContentEncryptionProviderFactory
* 注册到 Keycloak
*/
public class SMContentEncryptionProviderFactory
implements ContentEncryptionProviderFactory {
// Provider 标识符,用于在 Keycloak 管理控制台中选择
public static final String PROVIDER_ID = "bima-sm-content-encryption";
private Config.Scope config;
@Override
public ContentEncryptionProvider create(KeycloakSession session) {
// 每次调用创建新的 Provider 实例
// Keycloak 的 Provider 通常是轻量级的,可以频繁创建
return new SMContentEncryptionProvider(session);
}
@Override
public void init(Config.Scope config) {
this.config = config;
// 确保 BouncyCastle Provider 已注册
BCProviderRegistrar.registerIfAbsent();
}
@Override
public void postInit(KeycloakSessionFactory factory) {
// 可选的后初始化钩子
// 可用于验证配置、注册事件监听器等
}
@Override
public void close() {
// 清理资源
}
@Override
public String getId() {
return PROVIDER_ID;
}
@Override
public List<ProviderConfigProperty> getMetadata() {
// 返回管理控制台中可配置的属性
return List.of(
new ProviderConfigProperty(
"sm4.keySize",
"SM4 密钥长度",
"SM4 密钥长度(固定为 128 位)",
"128",
ProviderConfigProperty.STRING_TYPE
)
);
}
}SPI 服务注册文件:
为了让 Keycloak 能够发现并加载 SMContentEncryptionProviderFactory,需要在 META-INF/services/ 目录下创建服务注册文件:
# 文件路径: META-INF/services/org.keycloak.crypto.ContentEncryptionProviderFactory
# 文件内容:
cc.bima.keycloak.crypto.SMContentEncryptionProviderFactory4.3 JWE 内容加密流程
当 Keycloak 需要对 JWE payload 进行加密时,完整的调用链如下:
┌─────────────────────────────────────────────────────────────────────┐
│ JWE 内容加密完整流程 │
│ │
│ 1. Keycloak Token Manager │
│ │ 需要加密令牌中的敏感信息 │
│ ▼ │
│ 2. JWE Builder │
│ │ 构建 JWE 结构,选择算法 │
│ │ header.enc = "SM4-128-CBC" │
│ ▼ │
│ 3. JweEncryptionProvider │
│ │ 根据 header.enc 查找对应的 ContentEncryptionProvider │
│ │ → SMContentEncryptionProviderFactory.create() │
│ │ → SMContentEncryptionProvider │
│ ▼ │
│ 4. SMContentEncryptionProvider.encrypt() │
│ │ │
│ │ 4.1 生成随机 IV (16 字节) │
│ │ 4.2 初始化 SM4-CBC Cipher │
│ │ 4.3 使用 CEK 加密 payload │
│ │ 4.4 返回 IV + 密文 │
│ │ │
│ ▼ │
│ 5. JWE 序列化 │
│ │ 将 header、encrypted key、IV、密文组装为 JWE │
│ │ Compact Serialization: header.ek.iv.ct.tag │
│ │ JSON Serialization: 完整 JSON 结构 │
│ ▼ │
│ 6. 输出 JWE 令牌 │
│ │
├─────────────────────────────────────────────────────────────────────┤
│ JWE 内容解密完整流程 │
│ │
│ 1. 接收 JWE 令牌 │
│ │ │
│ ▼ │
│ 2. JWE Parser │
│ │ 解析 header,提取 enc 算法标识 │
│ │ header.enc = "SM4-128-CBC" │
│ ▼ │
│ 3. JweDecryptionProvider │
│ │ 根据 header.enc 查找 ContentEncryptionProvider │
│ │ → SMContentEncryptionProvider │
│ ▼ │
│ 4. 密钥解包(KeyWrapper) │
│ │ 使用接收方私钥解密 CEK │
│ │ → 获得明文的 SM4 密钥 │
│ ▼ │
│ 5. SMContentEncryptionProvider.decrypt() │
│ │ │
│ │ 5.1 从 JWE 中提取 IV 和密文 │
│ │ 5.2 初始化 SM4-CBC Cipher(解密模式) │
│ │ 5.3 使用 CEK 解密密文 │
│ │ 5.4 返回明文 payload │
│ │ │
│ ▼ │
│ 6. 输出明文 payload │
└─────────────────────────────────────────────────────────────────────┘以下是 SMContentEncryptionProvider 的教学简化实现:
java
import org.keycloak.crypto.ContentEncryptionProvider;
import org.keycloak.models.KeycloakSession;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.security.SecureRandom;
/**
* SM4 JWE 内容加密 Provider(教学简化版)
*
* 实现 Keycloak 的 ContentEncryptionProvider 接口,
* 使用 SM4-CBC 模式对 JWE payload 进行加密和解密。
*/
public class SMContentEncryptionProvider implements ContentEncryptionProvider {
private static final String ALGORITHM_NAME = "SM4";
private static final String TRANSFORMATION = "SM4/CBC/PKCS7Padding";
private static final String BC_PROVIDER = "BC";
private static final int IV_LENGTH = 16;
private final KeycloakSession session;
private final SecureRandom secureRandom;
public SMContentEncryptionProvider(KeycloakSession session) {
this.session = session;
this.secureRandom = new SecureRandom();
}
@Override
public byte[] encrypt(byte[] content, byte[] key,
String algorithm,
byte[] additionalAuthenticatedData) {
try {
// 1. 生成随机 IV
byte[] iv = new byte[IV_LENGTH];
secureRandom.nextBytes(iv);
// 2. 初始化 Cipher(加密模式)
Cipher cipher = Cipher.getInstance(TRANSFORMATION, BC_PROVIDER);
SecretKeySpec keySpec = new SecretKeySpec(key, ALGORITHM_NAME);
IvParameterSpec ivSpec = new IvParameterSpec(iv);
cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec);
// 3. 加密内容
byte[] cipherText = cipher.doFinal(content);
// 4. 返回 IV + 密文(Keycloak 框架负责将 IV 放入 JWE header)
return mergeIVAndCipherText(iv, cipherText);
} catch (Exception e) {
throw new RuntimeException("SM4 加密失败", e);
}
}
@Override
public byte[] decrypt(byte[] encryptedContent, byte[] key,
String algorithm,
byte[] additionalAuthenticatedData,
byte[] initializationVector,
byte[] authenticationTag) {
try {
// 1. 确定使用传入的 IV 还是内嵌的 IV
byte[] iv = (initializationVector != null)
? initializationVector
: extractIV(encryptedContent);
byte[] cipherText = (initializationVector != null)
? encryptedContent
: extractCipherText(encryptedContent);
// 2. 初始化 Cipher(解密模式)
Cipher cipher = Cipher.getInstance(TRANSFORMATION, BC_PROVIDER);
SecretKeySpec keySpec = new SecretKeySpec(key, ALGORITHM_NAME);
IvParameterSpec ivSpec = new IvParameterSpec(iv);
cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec);
// 3. 解密内容
return cipher.doFinal(cipherText);
} catch (Exception e) {
throw new RuntimeException("SM4 解密失败", e);
}
}
// ... 辅助方法省略
}4.4 加密算法标识与协商
在 JWE 标准中,内容加密算法通过 enc(encryption)头部参数标识。Keycloak 使用算法标识符来查找对应的 ContentEncryptionProvider 实现。
SM4-CBC 的算法标识符映射:
┌─────────────────────────────────────────────────────────────────────┐
│ JWE 内容加密算法标识符映射 │
│ │
│ 标准 JWE 算法标识符: │
│ ┌──────────────────┬──────────────────────────────────────┐ │
│ │ A128CBC-HS256 │ AES-128-CBC + HMAC-SHA-256 │ │
│ │ A192CBC-HS384 │ AES-192-CBC + HMAC-SHA-384 │ │
│ │ A256CBC-HS384 │ AES-256-CBC + HMAC-SHA-384 │ │
│ │ A128GCM │ AES-128-GCM │ │
│ │ A256GCM │ AES-256-GCM │ │
│ └──────────────────┴──────────────────────────────────────┘ │
│ │
│ 国密扩展算法标识符(自定义): │
│ ┌──────────────────┬──────────────────────────────────────┐ │
│ │ SM4-128-CBC │ SM4-CBC + PKCS7Padding │ │
│ │ SM4-128-GCM │ SM4-GCM(预留,待 BouncyCastle 支持)│ │
│ └──────────────────┴──────────────────────────────────────┘ │
│ │
│ 算法协商流程: │
│ │
│ 客户端 ──→ Authorization Request ──→ Keycloak │
│ (client_metadata.enc = "SM4-128-CBC") │
│ │
│ Keycloak ──→ 检查是否支持 SM4-128-CBC │
│ │ │
│ ├─→ 支持 → 使用 SMContentEncryptionProvider │
│ │ │
│ └─→ 不支持 → 返回算法不支持错误 │
│ 或回退到默认算法 │
└─────────────────────────────────────────────────────────────────────┘注意: SM4-CBC 模式不提供内置的消息认证(MAC),而标准的 JWE CBC 模式(如 A128CBC-HS256)要求配合 HMAC 使用以确保密文的完整性。在生产环境中,建议为 SM4-CBC 添加独立的 HMAC-SM3 认证层,或者等待 BouncyCastle 完善 SM4-GCM 模式的支持后切换到 GCM 模式。
第五章 SM2 非对称加密实现
5.1 SM2 签名与验签
SM2 数字签名算法是国家密码管理局发布的椭圆曲线数字签名算法,属于 SM2 算法体系的三个子集之一(SM2-2)。SM2 签名算法在安全性上与 ECDSA(使用 P-256 曲线)相当,但使用了不同的曲线参数和签名生成流程。
SM2 签名算法核心流程:
┌─────────────────────────────────────────────────────────────────────┐
│ SM2 签名生成流程 │
│ │
│ 输入: 消息 M, 私钥 d │
│ │
│ Step 1: 消息预处理 │
│ ┌──────────────────────────────────────────┐ │
│ │ ZA = SM3(ENTL || ID || a || b || Gx || Gy) │ │
│ │ e = SM3(ZA || M) │ 用户身份摘要 │
│ └──────────────────┬───────────────────────┘ │
│ │ │
│ Step 2: 生成随机数 k │
│ ┌──────────────────▼───────────────────────┐ │
│ │ 随机选取 k ∈ [1, n-1] │ │
│ │ 计算点 (x1, y1) = k × G │ │
│ └──────────────────┬───────────────────────┘ │
│ │ │
│ Step 3: 计算签名分量 r │
│ ┌──────────────────▼───────────────────────┐ │
│ │ r = (e + x1) mod n │ │
│ │ 如果 r = 0 或 r + k = n,重新选取 k │ │
│ └──────────────────┬───────────────────────┘ │
│ │ │
│ Step 4: 计算签名分量 s │
│ ┌──────────────────▼───────────────────────┐ │
│ │ s = ((1 + d)^(-1) × (k - r × d)) mod n │ │
│ │ 如果 s = 0,重新选取 k │ │
│ └──────────────────┬───────────────────────┘ │
│ │ │
│ ▼ │
│ 输出: 签名 (r, s) │
│ │
├─────────────────────────────────────────────────────────────────────┤
│ SM2 签名验证流程 │
│ │
│ 输入: 消息 M, 签名 (r, s), 公钥 Q │
│ │
│ Step 1: 验证签名范围 │
│ ┌──────────────────────────────────────────┐ │
│ │ 检查 r ∈ [1, n-1] 且 s ∈ [1, n-1] │ │
│ └──────────────────┬───────────────────────┘ │
│ │ │
│ Step 2: 消息预处理(与签名相同) │
│ ┌──────────────────▼───────────────────────┐ │
│ │ e = SM3(ZA || M) │ │
│ └──────────────────┬───────────────────────┘ │
│ │ │
│ Step 3: 计算验证点 │
│ ┌──────────────────▼───────────────────────┐ │
│ │ t = (r + s) mod n │ │
│ │ (x1, y1) = s × G + t × Q │ │
│ └──────────────────┬───────────────────────┘ │
│ │ │
│ Step 4: 验证签名 │
│ ┌──────────────────▼───────────────────────┐ │
│ │ R = (e + x1) mod n │ │
│ │ 验证通过 ⟺ R == r │ │
│ └──────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘5.2 SMSignatureProvider 实现
SMSignatureProvider 实现了 Keycloak 的 SignatureProvider 接口,使用 SM2 算法进行数字签名和验签。在 Keycloak 中,签名 Provider 主要用于 JWT(JSON Web Token)的签名和验证。
java
import org.keycloak.crypto.SignatureProvider;
import org.keycloak.crypto.SignatureSignerContext;
import org.keycloak.crypto.SignatureVerifierContext;
import org.keycloak.models.KeycloakSession;
/**
* SM2 签名 Provider(教学简化版)
*
* 实现 Keycloak 的 SignatureProvider 接口,
* 使用 SM2 算法(配合 SM3 哈希)进行签名和验签。
*/
public class SMSignatureProvider implements SignatureProvider {
private final KeycloakSession session;
public SMSignatureProvider(KeycloakSession session) {
this.session = session;
}
@Override
public SignatureSignerContext signer() {
// 返回 SM2 签名上下文
// 签名算法: SM2, 哈希算法: SM3
return new SMSignatureSignerContext(session);
}
@Override
public SignatureVerifierContext verifier(String kid) {
// 返回 SM2 验签上下文
// kid: Key ID,用于查找对应的公钥
return new SMSignatureVerifierContext(session, kid);
}
@Override
public void close() {
// 清理资源
}
}SPI 服务注册文件:
# META-INF/services/org.keycloak.crypto.SignatureProviderFactory
cc.bima.keycloak.crypto.SMSignatureProviderFactory5.3 SignatureSignerContext 与 VerifierContext
Keycloak 的签名操作通过 SignatureSignerContext 和 SignatureVerifierContext 两个上下文类完成。它们分别封装了签名和验签的具体逻辑。
SMSignatureSignerContext(签名上下文):
java
import org.bouncycastle.crypto.Signer;
import org.bouncycastle.crypto.signers.SM2Signer;
import org.bouncycastle.crypto.digests.SM3Digest;
import org.keycloak.crypto.SignatureSignerContext;
import java.nio.charset.StandardCharsets;
/**
* SM2 签名上下文(教学简化版)
*
* 签名算法: SM2 (sm2p256v1 曲线)
* 哈希算法: SM3
*/
public class SMSignatureSignerContext implements SignatureSignerContext {
private final byte[] privateKeyBytes;
public SMSignatureSignerContext(byte[] privateKeyBytes) {
this.privateKeyBytes = privateKeyBytes;
}
@Override
public String getAlgorithm() {
// 返回 JWA(JSON Web Algorithm)标识符
// 使用自定义标识符标识 SM2 签名
return "SM2";
}
@Override
public byte[] sign(byte[] data) {
try {
// 1. 初始化 SM2 签名器
SM2Signer signer = new SM2Signer();
// 使用 SM3 作为摘要算法
signer.init(true, derivePrivateKeyParams(privateKeyBytes));
// 2. 输入待签名数据
signer.update(data, 0, data.length);
// 3. 生成签名
return signer.generateSignature();
} catch (Exception e) {
throw new RuntimeException("SM2 签名失败", e);
}
}
// 私钥参数派生(教学示意,省略具体实现)
private Object derivePrivateKeyParams(byte[] keyBytes) {
// 将字节数组转换为 BouncyCastle 的 ECPrivateKeyParameters
return null;
}
}SMSignatureVerifierContext(验签上下文):
java
import org.bouncycastle.crypto.Signer;
import org.bouncycastle.crypto.signers.SM2Signer;
import org.keycloak.crypto.SignatureVerifierContext;
/**
* SM2 验签上下文(教学简化版)
*
* 通过 kid(Key ID)查找对应的 SM2 公钥进行验签
*/
public class SMSignatureVerifierContext implements SignatureVerifierContext {
private final byte[] publicKeyBytes;
public SMSignatureVerifierContext(byte[] publicKeyBytes) {
this.publicKeyBytes = publicKeyBytes;
}
@Override
public String getAlgorithm() {
return "SM2";
}
@Override
public boolean verify(byte[] data, byte[] signature) {
try {
// 1. 初始化 SM2 验签器
SM2Signer verifier = new SM2Signer();
verifier.init(false, derivePublicKeyParams(publicKeyBytes));
// 2. 输入原始数据
verifier.update(data, 0, data.length);
// 3. 验证签名
return verifier.verifySignature(signature);
} catch (Exception e) {
throw new RuntimeException("SM2 验签失败", e);
}
}
// 公钥参数派生(教学示意,省略具体实现)
private Object derivePublicKeyParams(byte[] keyBytes) {
// 将字节数组转换为 BouncyCastle 的 ECPublicKeyParameters
return null;
}
}5.4 KeyWrapper 适配
在 JWE 的完整加密流程中,内容加密密钥(CEK)需要使用接收方的公钥进行加密(Key Wrapping),接收方使用自己的私钥解密(Key Unwrapping)。Keycloak 通过 KeyWrapper SPI 接口提供密钥包装功能。
对于国密场景,SM2 公钥加密算法可以用于密钥包装:
┌─────────────────────────────────────────────────────────────────────┐
│ SM2 密钥包装流程 │
│ │
│ 发送方: │
│ │
│ 1. 生成随机 CEK (SM4 密钥, 16 字节) │
│ 2. 使用接收方 SM2 公钥加密 CEK │
│ encryptedKey = SM2-PublicKeyEncrypt(CEK, receiverPublicKey) │
│ 3. 使用 CEK 加密 payload (SM4-CBC) │
│ cipherText = SM4-CBC-Encrypt(payload, CEK) │
│ 4. 组装 JWE: header.encryptedKey.IV.cipherText │
│ │
│ 接收方: │
│ │
│ 1. 解析 JWE,提取 encryptedKey │
│ 2. 使用自己的 SM2 私钥解密 encryptedKey → CEK │
│ CEK = SM2-PrivateKeyDecrypt(encryptedKey, receiverPrivateKey) │
│ 3. 使用 CEK 解密 cipherText → payload │
│ payload = SM4-CBC-Decrypt(cipherText, CEK) │
│ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ KeyWrapper SPI 适配 │ │
│ │ │ │
│ │ SM2KeyWrapper implements KeyWrapper { │ │
│ │ wrapKey(cek, publicKey) → encryptedKey │ │
│ │ unwrapKey(encryptedKey, privateKey) → cek │ │
│ │ } │ │
│ │ │ │
│ │ Factory ID: bima-sm-key-wrapper │ │
│ └──────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘第六章 SM3 哈希算法实现
6.1 SM3 算法原理
SM3 是中国国家密码管理局于 2010 年发布的密码杂凑算法,输出 256 位(32 字节)的杂凑值。SM3 的设计参考了 SHA-256 的整体结构,但在压缩函数、消息扩展和布尔函数等方面做了改进,以增强安全性和抵抗已知的密码分析攻击。
SM3 算法核心流程:
┌─────────────────────────────────────────────────────────────────────┐
│ SM3 哈希算法流程 │
│ │
│ 输入: 消息 M (任意长度) │
│ 输出: 杂凑值 H (256 位 = 32 字节) │
│ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ Step 1: 消息填充 │ │
│ │ │ │
│ │ M' = M || 0x01 || 0x00...0x00 || L (64 bit) │ │
│ │ ↑ ↑ ↑ │ │
│ │ 消息 1 bit 消息长度(位) │ │
│ │ │ │
│ │ 填充后长度 ≡ 512 (mod 512) bit │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ Step 2: 消息扩展 │ │
│ │ │ │
│ │ 将填充后的消息分成 n 个 512 位分组 │ │
│ │ B(0) || B(1) || ... || B(n-1) │ │
│ │ │ │
│ │ 每个分组 B(i) 扩展为 132 个字 W(0)..W(67), W'(0)..W'(63)│ │
│ │ 扩展使用异或和循环移位操作 │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ Step 3: 压缩函数迭代 │ │
│ │ │ │
│ │ 初始值 V(0) = IV (8 个 32 位字) │ │
│ │ │ │
│ │ for i = 0 to n-1: │ │
│ │ V(i+1) = CF(V(i), B(i)) │ │
│ │ │ │
│ │ CF: 压缩函数 │ │
│ │ ┌────────────────────────────────────────────┐ │ │
│ │ │ 64 轮迭代 │ │ │
│ │ │ 每轮使用布尔函数 FF/GG 和置换 P0/P1 │ │ │
│ │ │ FF(j, X, Y, Z) = X ⊕ Y ⊕ Z (j < 16) │ │ │
│ │ │ FF(j, X, Y, Z) = (X&Y)|(X&Z)|(Y&Z) (j≥16)│ │ │
│ │ │ GG(j, X, Y, Z) = X ⊕ Y ⊕ Z (j < 16) │ │ │
│ │ │ GG(j, X, Y, Z) = (X&Y)|(~X&Z) (j≥16) │ │ │
│ │ └────────────────────────────────────────────┘ │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ Step 4: 输出 │ │
│ │ │ │
│ │ H = V(n) (256 位) │ │
│ └──────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘SM3 的安全性特性包括:
- 抗碰撞性:找到两个不同消息具有相同 SM3 哈希值在计算上是不可行的
- 抗原像性:给定哈希值 h,找到消息 m 使得 SM3(m) = h 在计算上是不可行的
- 抗第二原像性:给定消息 m1,找到不同的消息 m2 使得 SM3(m1) = SM3(m2) 在计算上是不可行的
6.2 SMHashProvider 实现
SMHashProvider 实现了 Keycloak 的 HashProvider 接口,使用 SM3 算法进行密码哈希和验证。
java
import org.bouncycastle.crypto.digests.SM3Digest;
import org.keycloak.crypto.HashProvider;
import java.nio.charset.StandardCharsets;
/**
* SM3 哈希 Provider(教学简化版)
*
* 实现 Keycloak 的 HashProvider 接口,
* 使用 SM3 算法进行消息摘要计算。
*/
public class SMHashProvider implements HashProvider {
@Override
public String hash(String input) {
SM3Digest digest = new SM3Digest();
byte[] inputBytes = input.getBytes(StandardCharsets.UTF_8);
digest.update(inputBytes, 0, inputBytes.length);
byte[] hashBytes = new byte[digest.getDigestSize()];
digest.doFinal(hashBytes, 0);
return bytesToHex(hashBytes);
}
@Override
public boolean verify(String input, String hashValue) {
String computedHash = hash(input);
// 使用恒定时间比较,防止时序攻击
return constantTimeEquals(computedHash, hashValue);
}
/**
* 字节数组转十六进制字符串
*/
private String bytesToHex(byte[] bytes) {
StringBuilder sb = new StringBuilder();
for (byte b : bytes) {
sb.append(String.format("%02x", b));
}
return sb.toString();
}
/**
* 恒定时间字符串比较
* 防止通过比较时间差异推断哈希值
*/
private boolean constantTimeEquals(String a, String b) {
if (a.length() != b.length()) {
return false;
}
int result = 0;
for (int i = 0; i < a.length(); i++) {
result |= a.charAt(i) ^ b.charAt(i);
}
return result == 0;
}
}SMHashProviderFactory:
java
import org.keycloak.Config;
import org.keycloak.crypto.HashProvider;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.provider.ProviderFactory;
/**
* SM3 哈希 Provider 工厂
* SPI ID: bima-sm-hash
*/
public class SMHashProviderFactory implements ProviderFactory<HashProvider> {
public static final String PROVIDER_ID = "bima-sm-hash";
@Override
public HashProvider create(KeycloakSession session) {
return new SMHashProvider();
}
@Override
public void init(Config.Scope config) {
BCProviderRegistrar.registerIfAbsent();
}
@Override
public void postInit(KeycloakSessionFactory factory) {
}
@Override
public void close() {
}
@Override
public String getId() {
return PROVIDER_ID;
}
}6.3 密码哈希场景应用
在 Keycloak 中,密码哈希是 HashProvider 最重要的应用场景。当用户注册或修改密码时,Keycloak 使用 HashProvider 对密码进行哈希处理后存储;当用户登录时,Keycloak 对输入的密码进行同样的哈希处理后与存储的哈希值进行比较。
然而,直接使用 SM3 对密码进行哈希并不安全,因为 SM3 是快速哈希算法,容易受到暴力破解和彩虹表攻击。在生产环境中,必须使用 PBKDF2、bcrypt 或 Argon2 等专门的密码哈希算法,它们通过引入盐值(salt)和迭代次数来增加破解难度。
┌─────────────────────────────────────────────────────────────────────┐
│ 密码哈希安全增强策略 │
│ │
│ 方案一: PBKDF2 + SM3(推荐) │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ hash = PBKDF2(password, salt, iterations, dkLen, SM3) │ │
│ │ │ │
│ │ · salt: 随机盐值(至少 16 字节) │ │
│ │ · iterations: 迭代次数(建议 ≥ 100,000) │ │
│ │ · dkLen: 派生密钥长度(32 字节 = SM3 输出长度) │ │
│ │ · SM3: 作为 PBKDF2 的伪随机函数(PRF) │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │
│ 方案二: HMAC-SM3 + Salt(简化方案) │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ hash = HMAC-SM3(password, salt) │ │
│ │ 存储: salt || hash │ │
│ │ │ │
│ │ · 使用 SM3 作为 HMAC 的底层哈希函数 │ │
│ │ · 每个用户使用不同的随机盐值 │ │
│ │ · 安全性低于 PBKDF2,但实现简单 │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │
│ Keycloak 中的密码哈希流程: │
│ │
│ 用户注册/修改密码: │
│ 1. 生成随机 salt │
│ 2. hash = PBKDF2-HMAC-SM3(password, salt, iterations) │
│ 3. 存储: salt || iterations || hash │
│ │
│ 用户登录: │
│ 1. 从数据库读取 salt || iterations || storedHash │
│ 2. hash = PBKDF2-HMAC-SM3(inputPassword, salt, iterations) │
│ 3. 比较 hash == storedHash │
└─────────────────────────────────────────────────────────────────────┘第七章 SMKeyProvider 密钥管理
7.1 KeyProvider SPI 接口
Keycloak 的 KeyProvider SPI 是密钥管理的核心接口,它负责密钥的生成、存储、检索和轮换。在密码学体系中,密钥管理是安全性的基石——即使使用了最强大的加密算法,如果密钥管理不当,整个安全体系都会崩溃。
┌─────────────────────────────────────────────────────────────────────┐
│ KeyProvider SPI 架构 │
│ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ Keycloak Realm │ │
│ │ │ │
│ │ ┌──────────────────────────────────────────────────┐ │ │
│ │ │ KeyManager │ │ │
│ │ │ │ │ │
│ │ │ ┌──────────┐ ┌──────────┐ ┌──────────────┐ │ │ │
│ │ │ │ RSA Key │ │ EC Key │ │ SM4 Key │ │ │ │
│ │ │ │ Provider │ │ Provider │ │ Provider │ │ │ │
│ │ │ │ │ │ │ │ (sm-generated)│ │ │ │
│ │ │ └──────────┘ └──────────┘ └──────────────┘ │ │ │
│ │ │ │ │ │
│ │ │ 密钥存储: │ │ │
│ │ │ · 数据库(默认) │ │ │
│ │ │ · Java Keystore │ │ │
│ │ │ · HSM/KMS(硬件安全模块) │ │ │
│ │ └──────────────────────────────────────────────────┘ │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │
│ KeyProvider 接口核心方法: │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ getPublicKey() → 公钥(非对称算法) │ │
│ │ getPrivateKey() → 私钥(非对称算法) │ │
│ │ getSecretKey() → 对称密钥(SM4 等) │ │
│ │ getKid() → 密钥 ID │ │
│ │ getAlgorithm() → 算法标识 │ │
│ │ getStatus() → 密钥状态(ACTIVE/PASSIVE/DISABLED)│ │
│ └──────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘7.2 SMKeyProviderFactory
SMKeyProviderFactory 是 SM4 密钥 Provider 的工厂类,负责创建和管理 SM4 对称密钥。
java
import org.keycloak.Config;
import org.keycloak.component.ComponentModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
/**
* SM4 密钥 Provider 工厂
* SPI ID: sm-generated
*
* 负责创建 SM4 对称密钥 Provider 实例
*/
public class SMKeyProviderFactory implements KeyProviderFactory {
public static final String PROVIDER_ID = "sm-generated";
@Override
public SMKeyProvider create(KeycloakSession session,
ComponentModel model) {
return new SMKeyProvider(session, model);
}
@Override
public void init(Config.Scope config) {
BCProviderRegistrar.registerIfAbsent();
}
@Override
public void postInit(org.keycloak.models.KeycloakSessionFactory factory) {
}
@Override
public void close() {
}
@Override
public String getId() {
return PROVIDER_ID;
}
}7.3 密钥生成与存储
SM4 密钥的生成使用 SecureRandom 生成 128 位(16 字节)的随机密钥。密钥的存储方式取决于 Keycloak 的配置,可以存储在数据库、Java Keystore 或外部 HSM/KMS 中。
┌─────────────────────────────────────────────────────────────────────┐
│ SM4 密钥生成与存储流程 │
│ │
│ 密钥生成: │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ 1. SecureRandom 生成 16 字节随机密钥 │ │
│ │ 2. 为密钥分配唯一 kid (Key ID) │ │
│ │ 3. 设置密钥状态为 ACTIVE │ │
│ │ 4. 设置密钥算法为 SM4 │ │
│ │ 5. 记录密钥创建时间 │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │
│ 密钥存储(数据库方式): │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ 表: REALM_KEY │ │
│ │ ┌─────────┬──────────┬──────────┬────────┬───────────┐ │ │
│ │ │ kid │ algorithm│ priority │ status │ secret │ │ │
│ │ ├─────────┼──────────┼──────────┼────────┼───────────┤ │ │
│ │ │ sm4-001 │ SM4 │ 100 │ ACTIVE │ [加密存储] │ │ │
│ │ │ sm4-002 │ SM4 │ 50 │ PASSIVE│ [加密存储] │ │ │
│ │ └─────────┴──────────┴──────────┴────────┴───────────┘ │ │
│ │ │ │
│ │ 注意: secret 字段应使用 Realm 的主密钥加密后存储 │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │
│ 密钥检索: │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ 1. 根据 kid 查找密钥 │ │
│ │ 2. 使用 Realm 主密钥解密 secret │ │
│ │ 3. 返回 SecretKeySpec 对象 │ │
│ └──────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘以下是 SMKeyProvider 的教学简化实现:
java
import org.keycloak.component.ComponentModel;
import org.keycloak.keys.KeyProvider;
import org.keycloak.models.KeycloakSession;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.security.SecureRandom;
/**
* SM4 密钥 Provider(教学简化版)
*
* 负责生成、存储和提供 SM4 对称密钥
*/
public class SMKeyProvider implements KeyProvider {
private static final String ALGORITHM = "SM4";
private static final int KEY_SIZE = 16; // 128 位 = 16 字节
private final KeycloakSession session;
private final ComponentModel model;
private final SecretKey secretKey;
private final String kid;
public SMKeyProvider(KeycloakSession session, ComponentModel model) {
this.session = session;
this.model = model;
// 从配置中获取或生成密钥
String existingKey = model.get("secret");
if (existingKey != null && !existingKey.isEmpty()) {
this.secretKey = new SecretKeySpec(
decodeBase64(existingKey), ALGORITHM);
this.kid = model.get("kid");
} else {
// 生成新密钥
byte[] keyBytes = new byte[KEY_SIZE];
new SecureRandom().nextBytes(keyBytes);
this.secretKey = new SecretKeySpec(keyBytes, ALGORITHM);
this.kid = generateKid();
}
}
@Override
public SecretKey getSecretKey() {
return secretKey;
}
@Override
public String getKid() {
return kid;
}
@Override
public String getAlgorithm() {
return ALGORITHM;
}
private String generateKid() {
// 生成唯一的密钥标识符
return "sm4-" + java.util.UUID.randomUUID().toString()
.substring(0, 8);
}
private byte[] decodeBase64(String encoded) {
return java.util.Base64.getDecoder().decode(encoded);
}
}7.4 密钥轮换策略
密钥轮换(Key Rotation)是密钥管理的核心安全实践。定期更换加密密钥可以限制单个密钥泄露的影响范围,是纵深防御策略的重要组成部分。
┌─────────────────────────────────────────────────────────────────────┐
│ 密钥轮换策略 │
│ │
│ 轮换场景: │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ 1. 定期轮换: 每隔 N 天自动轮换(如 90 天) │ │
│ │ 2. 紧急轮换: 发现密钥可能泄露时立即轮换 │ │
│ │ 3. 合规轮换: 满足等保 2.0 等合规要求的轮换周期 │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │
│ 轮换流程: │
│ │
│ ┌──────────┐ 生成新密钥 ┌──────────┐ │
│ │ 旧密钥 │ ──────────────→ │ 新密钥 │ │
│ │ (ACTIVE) │ │ (PASSIVE) │ │
│ └──────────┘ └──────────┘ │
│ │ │ │
│ │ 切换状态 │ 切换状态 │
│ ▼ ▼ │
│ ┌──────────┐ ┌──────────┐ │
│ │ 旧密钥 │ │ 新密钥 │ │
│ │ (PASSIVE)│ │ (ACTIVE) │ │
│ └──────────┘ └──────────┘ │
│ │ │ │
│ │ 保留(解密旧数据) │ 用于新数据加密 │
│ │ │ │
│ ▼ ▼ │
│ ┌──────────┐ ┌──────────┐ │
│ │ 旧密钥 │ │ 新密钥 │ │
│ │ (DISABLED) │ (ACTIVE) │ │
│ │ (归档删除) │ │ │
│ └──────────┘ └──────────┘ │
│ │
│ 轮换期间的数据处理策略: │
│ · 加密: 始终使用 ACTIVE 状态的密钥 │
│ · 解密: 依次尝试 ACTIVE → PASSIVE 状态的密钥 │
│ · 过渡期: 旧密钥保持 PASSIVE 状态,直到所有旧数据被重新加密 │
│ · 清理: 旧密钥过渡期结束后设为 DISABLED,最终归档或删除 │
│ │
│ Keycloak 中的密钥轮换: │
│ · 通过管理控制台或 Admin REST API 触发 │
│ · 支持自动轮换(配置轮换周期) │
│ · 轮换后旧密钥自动降级为 PASSIVE │
│ · 令牌验证时自动尝试所有 PASSIVE 密钥 │
└─────────────────────────────────────────────────────────────────────┘在 Keycloak 中,密钥轮换可以通过管理控制台手动触发,也可以通过配置自动执行。对于 SM4 密钥,建议的轮换策略如下:
- 轮换周期:90 天(可根据安全策略调整)
- 过渡期:30 天(旧密钥保持 PASSIVE 状态的时间)
- 最小密钥数:2(确保轮换期间至少有一个 ACTIVE 密钥和一个 PASSIVE 密钥)
- 密钥归档:轮换后的旧密钥应安全归档,以备审计和旧数据解密需要
总结与展望
本文基于 keycloak-sandbox 项目中的 spi-sm-crypto-extension 模块,系统性地讲解了如何通过 Keycloak SPI 机制集成国密算法,形成了一套完整的国密密码学解决方案。以下是本文的核心成果总结:
已实现的核心组件:
| 组件 | SPI 接口 | Provider ID | 功能 |
|---|---|---|---|
| SMContentEncryptionProvider | ContentEncryptionProvider | bima-sm-content-encryption | SM4-CBC JWE 内容加密 |
| SMSignatureProvider | SignatureProvider | bima-sm-signature | SM2 数字签名与验签 |
| SMHashProvider | HashProvider | bima-sm-hash | SM3 哈希计算 |
| SMKeyProvider | KeyProvider | sm-generated | SM4 密钥生成与管理 |
技术架构亮点:
- 分层设计:底层算法工具类(SM2Util/SM3Util/SM4Util)与上层 SPI 实现完全解耦,便于独立测试和替换
- 零侵入集成:通过标准 SPI 机制扩展,不修改 Keycloak 核心代码,升级 Keycloak 版本时无需修改扩展
- BouncyCastle 底层:利用成熟的 BouncyCastle 密码学库,确保算法实现的正确性和安全性
- IV 前置存储:SM4 CBC 模式采用 IV 与密文合并存储方案,简化了密文传输和存储
未来展望:
SM4-GCM 模式支持:GCM(Galois/Counter Mode)模式同时提供加密和认证功能,是 JWE 推荐的内容加密模式。待 BouncyCastle 完善 SM4-GCM 支持后,应优先切换到 GCM 模式以获得更好的安全性和性能。
国密 TLS 集成:在传输层使用国密 TLS(GMTLS)协议,实现端到端的国密通信。这需要集成国密 SSL/TLS 库(如 GmSSL),并在 Keycloak 的 HTTP 层进行适配。
HSM/KMS 集成:通过 Keycloak 的
KeyProviderSPI 接入硬件安全模块(HSM)或密钥管理服务(KMS),确保密钥的全生命周期安全。性能优化:在高并发场景下,可以通过 SM4 硬件加速(如 Intel AES-NI 指令集的国密扩展)和连接池技术提升加解密性能。
国密算法套件标准化:推动 SM2/SM3/SM4 算法标识符在 JWK/JWA/JWE 标准中的注册,实现与其他国密系统的互操作。
国密算法的集成不仅是技术实现问题,更是一项涉及合规、安全、性能和互操作性的系统工程。本文提供的方案为 Keycloak 国密集成奠定了坚实的技术基础,读者可以根据实际业务需求和安全策略进行裁剪和扩展。在国家信息安全战略持续推进的大背景下,掌握国密算法的集成技术,对于每一位从事身份认证和信息安全领域的工程师来说,都将成为越来越重要的核心竞争力。
版权声明: 本文为必码(bima.cc)原创技术文章,仅供学习交流。
本文内容基于实际项目源码解析整理,代码示例均为教学简化版本,仅供学习参考。
如需获取完整项目代码或技术支持,请访问 bima.cc。