Skip to content

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)
ContentEncryptionProviderJWE 内容加密与解密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-jdk18on1.78.1核心密码学提供者SM2Signer, SM3Digest, SM4Engine
bcpkix-jdk18on1.78.1PKI/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 的 KeyProvider SPI 提供了与 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 必须满足以下要求:

  1. 不可预测性:IV 必须使用密码学安全的随机数生成器(CSPRNG)生成
  2. 唯一性:同一密钥下,每个加密操作的 IV 都应该不同
  3. 不保密: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("加密数据格式无效");
        }
    }
}

安全注意事项:

  1. SM4 密钥必须使用安全的密钥派生函数(KDF)生成,禁止使用硬编码密钥
  2. 每次加密必须使用不同的随机 IV,严禁重复使用 IV
  3. 密钥和 IV 必须通过安全通道传输,密钥必须保密而 IV 可以公开
  4. 加密失败时应抛出异常并记录日志,但不应泄露密钥或明文信息

第四章 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

SMContentEncryptionProviderFactorySMContentEncryptionProvider 的工厂类,负责 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.SMContentEncryptionProviderFactory

4.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.SMSignatureProviderFactory

5.3 SignatureSignerContext 与 VerifierContext

Keycloak 的签名操作通过 SignatureSignerContextSignatureVerifierContext 两个上下文类完成。它们分别封装了签名和验签的具体逻辑。

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功能
SMContentEncryptionProviderContentEncryptionProviderbima-sm-content-encryptionSM4-CBC JWE 内容加密
SMSignatureProviderSignatureProviderbima-sm-signatureSM2 数字签名与验签
SMHashProviderHashProviderbima-sm-hashSM3 哈希计算
SMKeyProviderKeyProvidersm-generatedSM4 密钥生成与管理

技术架构亮点:

  1. 分层设计:底层算法工具类(SM2Util/SM3Util/SM4Util)与上层 SPI 实现完全解耦,便于独立测试和替换
  2. 零侵入集成:通过标准 SPI 机制扩展,不修改 Keycloak 核心代码,升级 Keycloak 版本时无需修改扩展
  3. BouncyCastle 底层:利用成熟的 BouncyCastle 密码学库,确保算法实现的正确性和安全性
  4. IV 前置存储:SM4 CBC 模式采用 IV 与密文合并存储方案,简化了密文传输和存储

未来展望:

  1. SM4-GCM 模式支持:GCM(Galois/Counter Mode)模式同时提供加密和认证功能,是 JWE 推荐的内容加密模式。待 BouncyCastle 完善 SM4-GCM 支持后,应优先切换到 GCM 模式以获得更好的安全性和性能。

  2. 国密 TLS 集成:在传输层使用国密 TLS(GMTLS)协议,实现端到端的国密通信。这需要集成国密 SSL/TLS 库(如 GmSSL),并在 Keycloak 的 HTTP 层进行适配。

  3. HSM/KMS 集成:通过 Keycloak 的 KeyProvider SPI 接入硬件安全模块(HSM)或密钥管理服务(KMS),确保密钥的全生命周期安全。

  4. 性能优化:在高并发场景下,可以通过 SM4 硬件加速(如 Intel AES-NI 指令集的国密扩展)和连接池技术提升加解密性能。

  5. 国密算法套件标准化:推动 SM2/SM3/SM4 算法标识符在 JWK/JWA/JWE 标准中的注册,实现与其他国密系统的互操作。

国密算法的集成不仅是技术实现问题,更是一项涉及合规、安全、性能和互操作性的系统工程。本文提供的方案为 Keycloak 国密集成奠定了坚实的技术基础,读者可以根据实际业务需求和安全策略进行裁剪和扩展。在国家信息安全战略持续推进的大背景下,掌握国密算法的集成技术,对于每一位从事身份认证和信息安全领域的工程师来说,都将成为越来越重要的核心竞争力。


版权声明: 本文为必码(bima.cc)原创技术文章,仅供学习交流。

本文内容基于实际项目源码解析整理,代码示例均为教学简化版本,仅供学习参考。

如需获取完整项目代码或技术支持,请访问 bima.cc