前文的教程中,我们使用加盐的 Bcrypt 算法计算用户密码的摘要,使用 RSA256 非对称加密算法对 JWT 执行签名(形成 JWS)。尽管这些算法已经足够强壮,但对于要求符合政务安全规范以及信息安全等级保护规范的应用系统来说,在使用密码的时候要求使用国密算法。本章将在简单讨论国密算法的基础上, 依据 Spring Security 的规范,用国密算法代替 Bcrypt 和 RSA256。

34.1 国密算法概述

国密算法即国家密码局认定的国产密码算法。主要有SM1,SM2,SM3,SM4。

34.1.1 SM1算法

SM1 为对称加密。其加密强度与AES相当。该算法不公开,调用该算法时,需要通过加密芯片的接口进行调用。

34.1.2 SM2算法

SM2为非对称加密的公钥密码算法,是我国自主设计的椭圆曲线公钥密码算法,该算法基于256位比特(32字节)ECC,安全强度比RSA 2048位高,签名速度与秘钥生成速度都快于RSA。该算法已公开

在实际使用非对称加密时,一般是后端生成密钥对,将公钥交给前端,前端用公钥加密数据,后端用私钥对数据解密。在有些项目中,没有使用HTTPS的时候,登录的口令(用户名/密码),需要进行加密传输的需求,这时候我们就需要采用非对称加密来实现。

34.1.3 SM3算法

SM3为消息摘要算法,可用于数字签名、消息认证、验证数据完整性等,可以用MD5 或 SHA-1 算法作为对比理解,但SM3安全度更高,校验结果为256位比特(32字节)。该算法已公开

34.1.4 SM4算法

SM4是对称加密算法,类似于 DES 或 AES。它的密钥长度和分组长度均为128位,若消息长度过长,需要进行分组,在同一密钥控制下逐组进行加密,若消息长度不足,则要进行填充。该算法已公开

34.2 摘要加盐与消息认证算法

34.2.1 摘要算法与碰撞

摘要算法(Digest)又称哈希算法(Hash),它的作用是对任意一组输入数据进行计算,得到一个固定长度的输出摘要。

下面是摘要算法最重要的特点:

  1. 相同的输入一定得到相同的输出;
  2. 不同的输入大概率得到不同的输出。
  3. 算法不可逆(无法根据结果计算出原文)

摘要算法的目的是为了验证原始数据是否被篡改:两个相同的内容永远会计算出相同的 hashCode,只要其中一个做了微小的修改,结果就会差异很大。

摘要算法的一个问题是会发生碰撞,即两个不相关的内容的输入可能会得到相同的输出。那么摘要算法的碰撞能不能避免?答案是不能。碰撞是一定会出现的,因为摘要结果的长度是固定的。摘要算法相当于把一个无限的输入集合映射到一个有限的输出集合,所以必然会产生碰撞。

碰撞并不可怕,我们担心的不是碰撞而是发生碰撞的概率,因为碰撞概率的高低关系到哈希算法的安全性。一个安全的哈希算法必须满足碰撞概率低和不能猜测输出两个条件。不能猜测输出是指,输入的任意一个 bit 的变化会造成输出完全不同,这样就很难从输出反推输入(只能依靠暴力穷举)。显然,根据碰撞概率,哈希算法的输出长度越长,就越难产生碰撞,也就越安全。

常用的哈希算法有:

算法 输出长度(位) 输出长度(字节)
SM3 256 bits 32 bytes
MD5 128 bits 16 bytes
SHA-1 160 bits 20 bytes
RipeMD-160 160 bits 20 bytes
SHA-256 256 bits 32 bytes
SHA-512 512 bits 64 bytes

34.2.2 摘要加盐抵御攻击

相同的输入永远会得到相同的输出、结果比较短、速度又非常快,摘要算法的这些优点使得他一个重要用途是存储用户密码。如果直接将用户的原始口令存放到数据库中,会产生极大的安全风险,所以通常的方法是存储用户密码的摘要结果。

使用摘要算法保护密码时,要重点防止彩虹表攻击。

如果只拿到摘要结果,想要反推明文密码只能使用暴力穷举的方法,这个过程的计算量非常巨大。所以攻击者并不会真的去暴力穷举,他们的作法寻找与摘要结果相同的“碰撞”。彩虹表就是一个常见寻找碰撞的手段。简单说来,就是做一个预先计算好的常用密码和摘要结果的对照表。如果用户使用了常用口令(或者计算结果和表上其它某个的结果相同,即发生了碰撞),攻击者就能反查到可用的密码。这个表就是彩虹表

在有彩虹表威胁的情况下,简单地直接使用摘要算法显然是不合适的。为了抵御彩虹表攻击,标准的作法是对每个原文额外添加固定或随机内容之后再计算摘要,这个方法称之为加盐(salt)

加盐的目的在于使攻击者的彩虹表失效,即使用户使用常用口令,也无法使用彩虹表反推原始口令。

注意:不管是否加盐,MD5因为输出长度较短,已被证明可以在短时间内用算法寻找碰撞而破解,目前已经不应该使用

34.2.3 消息认证算法

摘要算法加盐(salt)的目的在于抵御彩虹表攻击。这个salt可以看作是一个额外的“认证码”,同样的输入,不同的认证码,会产生不同的输出。因此,要验证输出的哈希,必须同时提供“认证码”。尽管我们可以自己设计这一过程,但可靠的作法是使用消息认证码算法(MAC,Message Authentication Code)。消息认证算法是含有密钥的摘要算法,因此也常被称为HMAC(Hash-based Message Authentication Code)。需要说明的是,HMAC 严格说来与具体的摘要算法无关,它必须和某种哈希算法配合起来用。例如,如果在 HMAC 中使用MD5算法,对应的就是HMAC-MD5算法(它相当于“加盐”的MD5),同样的,还有使用HMAC-SHA1、HMAC-SHA-224、HMAC-SHA-384、HMAC-SHA-512、HMAC-SM2。

为了保证安全,在具体使用 HMAC 算法的时候我们通常不会自己指定key,而是通过Java标准库的KeyGenerator生成一个安全的随机的key。

34.3 选择Bouncy Castle库

在网络上有很多实现国密算法的开源代码,但绝大多数都仅仅是算法本身。经过评估,我们选择 Bouncy Castle 加密算法库提供的国密算法API。Bouncy Castle 是符合 Java 安全算法标准的开放源代码的加密算法库,是各种加密算法的Java实现。它在基于 MIT X联盟许可证的许可下分发。

国密算法的入口在下面三个类:

  • org.bouncycastle.crypto.engines.SM2Engine
  • org.bouncycastle.crypto.digests.SM3Digest
  • org.bouncycastle.crypto.engines.SM4Engine


使用 Bouncy Castle 可以直接添加它的依赖

  1. <dependency>
  2. <groupId>org.bouncycastle</groupId>
  3. <artifactId>bcprov-jdk15on</artifactId>
  4. <version>1.69</version>
  5. </dependency>

34.4 用HMAC-SM3处理密码

使用 HMAC-SM3处理用户密码的过程非常简单,主要是封装 Bouncy Castle SM3 API 和按照 Spring Security 的标准定制密码编码器两项工作。

34.4.1 封装SM3 算法工具具类

尽管可以直接调用 Bouncy Castle SM3 API,但封装以后会不仅更加方便使用,还会让代码的逻辑机构更加清晰。下面是完整的 SM3 工具类代码:

  1. package com.longser.union.cloud.crypto;
  2. import org.bouncycastle.crypto.digests.SM3Digest;
  3. import org.bouncycastle.crypto.macs.HMac;
  4. import org.bouncycastle.crypto.params.KeyParameter;
  5. import org.bouncycastle.jce.provider.BouncyCastleProvider;
  6. import org.springframework.util.Assert;
  7. import javax.crypto.KeyGenerator;
  8. import javax.crypto.SecretKey;
  9. import javax.validation.constraints.NotBlank;
  10. import javax.validation.constraints.NotEmpty;
  11. import java.security.NoSuchAlgorithmException;
  12. import java.security.Security;
  13. import java.util.Arrays;
  14. import java.util.Base64;
  15. import java.util.Random;
  16. /**
  17. * 这是一个封装 Bouncy Castle SM3 算法库的工具类。 分别实现应用 SM3 和 HMAC-SM3 算法计算摘要和比较明
  18. * 文的摘要结果是否与给定的一致。在使用 HMAC-SM3 的时候,把随机生成的 Key 串和摘要结果拼在一起,以实现动
  19. * 态加盐。
  20. * @author David Jia
  21. */
  22. public class SM3 {
  23. private final static String BOUNCY_CASTLE_PROVIDER_NAME = "BC";
  24. public static byte[] getHash(@NotBlank String srcData) {
  25. return getHash(srcData.getBytes());
  26. }
  27. public static byte[] getHash(@NotEmpty byte[] srcData) {
  28. SM3Digest digest = new SM3Digest();
  29. digest.update(srcData, 0, srcData.length);
  30. byte[] hash = new byte[digest.getDigestSize()];
  31. digest.doFinal(hash, 0);
  32. return hash;
  33. }
  34. public static boolean matches(@NotBlank String srcData, @NotBlank String sm3Hash) {
  35. return matches(srcData.getBytes(), sm3Hash.getBytes());
  36. }
  37. public static boolean matches(@NotEmpty byte[] srcData, @NotEmpty byte[] sm3Hash) {
  38. byte[] newHash = getHash(srcData);
  39. return Arrays.equals(newHash, sm3Hash);
  40. }
  41. public static boolean hmacMatches(@NotBlank String srcData, @NotBlank String sm3HmacHash) {
  42. String[] stringArray = sm3HmacHash.split("=");
  43. Assert.isTrue( stringArray.length == 3,sm3HmacHash + " does not look like SM3 HMAC code.");
  44. String realHash = stringArray[1];
  45. String keyString = stringArray[0] + stringArray[2];
  46. byte[] skey = Base64.getDecoder().decode(keyString);
  47. String newHash = hmacHash(srcData.getBytes(), skey);
  48. return realHash.equals( newHash);
  49. }
  50. private static String hmacHash(@NotEmpty byte[] srcData, @NotEmpty byte[] skey) {
  51. SM3Digest digest = new SM3Digest();
  52. KeyParameter keyParameter = new KeyParameter(skey);
  53. HMac mac = new HMac(digest);
  54. mac.init(keyParameter);
  55. mac.update(srcData, 0, srcData.length);
  56. byte[] result = new byte[mac.getMacSize()];
  57. mac.doFinal(result, 0);
  58. return Base64.getEncoder().withoutPadding().encodeToString(result);
  59. }
  60. public static String getHmacHash(@NotBlank String srcData) throws NoSuchAlgorithmException {
  61. return getHmacHash(srcData.getBytes());
  62. }
  63. public static String getHmacHash(@NotEmpty byte[] srcData) throws NoSuchAlgorithmException {
  64. // 这个方法中给 HMAC 的盐(key)是 Java KeyGenerator 生成的
  65. // 为了让 getInstance("HMACSM3") 工作,必须注册 BouncyCastleProvider
  66. // 这也是选择使用 Bouncy Castle 库的原因,因为它符合 Java 规范
  67. if( Security.getProvider(BOUNCY_CASTLE_PROVIDER_NAME) == null ) {
  68. Security.addProvider(new BouncyCastleProvider());
  69. }
  70. KeyGenerator keyGen = KeyGenerator.getInstance("HMACSM3");
  71. SecretKey key = keyGen.generateKey();
  72. byte[] skey = Arrays.copyOf(key.getEncoded(),32);
  73. String realHash = hmacHash(srcData, skey);
  74. // 得到真实的哈希值之后,我们把随机得到的盐(key)随机分成两个部分分别夹在哈希结果
  75. // 的两端(用 = 标记)之后保存,这样结果就自己携带了验证所需的盐(key)
  76. int splicer = (new Random()).nextInt(32);
  77. String keyString = Base64.getEncoder().withoutPadding().encodeToString(skey);
  78. return keyString.substring(0, splicer) + "=" + realHash + "=" + keyString.substring(splicer);
  79. }
  80. }

34.4.2 测试封装的 SM3 工具类

下面这个测试类主要测试 HMAC-SM3 的编码和验证:

package com.longser.union.cloud.security;

import com.longser.crypto.SM3;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.util.Assert;

@SpringBootTest
public class CryptoTest {

    @Test
    public void KeyGeneratorTest() throws Exception {

        String plaintext = "Hello World";
        String result = SM3.getHmacHash(plaintext);

        System.out.println("The length of result is " + result.length());
        System.out.println(result);

        Assert.isTrue(SM3.hmacMatches(plaintext, result), "Why? It's not matched.");
    }
}

34.4.3 使用 SM3的密码编码器

Spring Security 的密码编码器非常简单,就是实现 PasswordEncoder 接口要求的 encode 和 matches 两个方法:

package com.longser.union.cloud.crypto;

import lombok.SneakyThrows;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.security.crypto.password.PasswordEncoder;

/**
 * 给 Spring Security 应用程序使用 SM3 算法加密和比较密码的 PasswordEncoder 类。可以按照 Spring
 * Security 的规范直接使用。
 */
public class SM3PasswordEncoder implements PasswordEncoder {

    private final Log logger = LogFactory.getLog(this.getClass());

    @SneakyThrows
    @Override
    public String encode(CharSequence rawPassword) {
        if (rawPassword == null) {
            throw new IllegalArgumentException("rawPassword cannot be null");
        } else {
            return SM3.getHmacHash(rawPassword.toString());
        }
    }

    @Override
    public boolean matches(CharSequence rawPassword, String encodedPassword) {
        if (rawPassword == null) {
            throw new IllegalArgumentException("rawPassword cannot be null");
        } else if (encodedPassword != null && encodedPassword.length() != 0) {
            return SM3.hmacMatches(rawPassword.toString(), encodedPassword);
        } else {
            this.logger.warn("Empty encoded password");
            return false;
        }
    }
}

34.4.4 修改安全配置

定义好的密码编码器可以在 SecurityConfig 中直接替换掉当前的 BCryptPasswordEncoder:

    @Bean
    public PasswordEncoder passwordEncoder() {
-       return new BCryptPasswordEncoder();
+       return new SM3PasswordEncoder();
    }

因为 Spring Security 良好的设计和我们规范的自定义,更换新的 PasswordEncoder 只需要修改这一个地方。

34.5 SM2算法用于JWT签名

34.5.1 封装 SM2 算法工具类

封装 SM2 算法的工具类比较复杂,“篇幅”较长,为便于阅读全部内容放到了本章教程的尾部。这些代码全部来自ZZMarquis 的 gmhelper

34.5.2 测试封装的 SM2 工具类

下面的测试代码有三个方法:

  • 生成 Base 64格式的密钥对
  • 展示加密、解密、签名、验签的不同方法
  • 生成密钥、展示各种格式密钥的转换方式,以各种不同格式保存密钥

其中用到的 FileUtil 类的源代码也在本章教程的后面。

注意:sm2KeyPairGeneration 输出的结果需要保存起来,在下文会用到。

    @Test
    public void sm2KeyPairGeneration() {
        try {
            AsymmetricCipherKeyPair keyPair = SM2.generateKeyPairParameter();
            ECPrivateKeyParameters privateKey = (ECPrivateKeyParameters) keyPair.getPrivate();
            ECPublicKeyParameters publicKey = (ECPublicKeyParameters) keyPair.getPublic();

            byte[] privateKeyPkcs1Der = BCECUtil.convertECPrivateKeyToSEC1(privateKey, publicKey);
            System.out.println(Base64.getEncoder().encodeToString(privateKeyPkcs1Der));

            byte[] publicKeyX509Der = BCECUtil.convertECPublicKeyToX509(publicKey);
            System.out.println(Base64.getEncoder().encodeToString(publicKeyX509Der));

        } catch (Exception ex) {

        }
    }

    @Test
    public void sm2EncryptSignTest() {
        try {
            // Generate Key Pair
            KeyPair keyPair = SM2.generateKeyPair();
            ECPrivateKeyParameters privateKey = BCECUtil.convertPrivateKeyToParameters((BCECPrivateKey) keyPair.getPrivate());
            ECPublicKeyParameters publicKey = BCECUtil.convertPublicKeyToParameters((BCECPublicKey) keyPair.getPublic());

            final byte[] WITH_ID = "Longser Technologies".getBytes();
            byte[] plainMessage = "Hello World".getBytes();

            // Encrypt (with public key) and Decrypt (with private key)
            byte[] encryptedData = SM2.encrypt(publicKey, plainMessage);
            byte[] decryptedData = SM2.decrypt(privateKey, encryptedData);
            System.out.println(new String(decryptedData));
            Assert.isTrue(Arrays.equals(decryptedData, plainMessage),"");

            // sign <==>  ( (decodeDERSM2Sign -> encodeSM2SignToDER) -> verify )
            // Sign (with private key) and Verify (with public key), use special ID
            byte[] sign = SM2.sign(privateKey, WITH_ID, plainMessage);

            byte[] rawSign = SM2.decodeDERSM2Sign(sign);
            sign = SM2.encodeSM2SignToDER(rawSign);

            boolean flag = SM2.verify(publicKey, WITH_ID, plainMessage, sign);
            Assert.isTrue(flag, "verify failed\"");

            // sign <==> verify
            // Sign (with private key) and Verify (with public key), use default ID
            sign = SM2.sign(privateKey, plainMessage);
            flag = SM2.verify(publicKey, plainMessage, sign);
            Assert.isTrue(flag, "verify failed");

        } catch (Exception ex) {
            ex.printStackTrace();
            Assert.isTrue(false, "");
        }
    }

    @Test
    public void sm2KeyPairEncodingTest() {
        try {
            AsymmetricCipherKeyPair keyPair = SM2.generateKeyPairParameter();
            ECPrivateKeyParameters privateKey = (ECPrivateKeyParameters) keyPair.getPrivate();
            ECPublicKeyParameters publicKey = (ECPublicKeyParameters) keyPair.getPublic();

            // EC => PKCS#1 DER
            byte[] privateKeyPkcs1Der = BCECUtil.convertECPrivateKeyToSEC1(privateKey, publicKey);
            System.out.println("Private Key PKCS#1 DER length:" + privateKeyPkcs1Der.length);
            System.out.println("Private Key PKCS#1 DER\n:" + ByteUtils.toHexString(privateKeyPkcs1Der));
            FileUtil.writeFile("target/ec.pkcs1.pri", privateKeyPkcs1Der);

            // EC => PKCS#8 DER
            byte[] privateKeyPkcs8Der = BCECUtil.convertECPrivateKeyToPKCS8(privateKey, publicKey);

            System.out.println("Private Key PKCS#8 DER length:" + privateKeyPkcs8Der.length);
            System.out.println("Private Key PKCS#8 DER:\n" + ByteUtils.toHexString(privateKeyPkcs8Der));
            FileUtil.writeFile("target/ec.pkcs8.pri.der", privateKeyPkcs8Der);

            // PKCS#8 DER => PKCS#8 PEM
            String privateKeyPkcs8Pem = BCECUtil.convertECPrivateKeyPKCS8ToPEM(privateKeyPkcs8Der);
            System.out.println("Private Key PKCS#8 PEM:\n" + privateKeyPkcs8Pem);
            FileUtil.writeFile("target/ec.pkcs8.pri.pem", privateKeyPkcs8Pem.getBytes(StandardCharsets.UTF_8));

            // PKCS#8 PEM => PKCS#8 DER
            byte[] privateKeyFromPem = BCECUtil.convertECPrivateKeyPEMToPKCS8(privateKeyPkcs8Pem);
            if (!Arrays.equals(privateKeyFromPem, privateKeyPkcs8Der)) {
                throw new Exception("privateKeyFromPem != privateKeyPkcs8Der");
            }

            // EC => X.509 DER
            byte[] publicKeyX509Der = BCECUtil.convertECPublicKeyToX509(publicKey);
            System.out.println("Public Key X.509 DEF length:" + publicKeyX509Der.length);
            System.out.println("Public Key X.509 DEF:" + ByteUtils.toHexString(publicKeyX509Der));
            FileUtil.writeFile("target/ec.x509.pub.der", publicKeyX509Der);

            // X.509 DEF => X.509 PEM
            String pubKeyX509Pem = BCECUtil.convertECPublicKeyX509ToPEM(publicKeyX509Der);
            System.out.println("Public Key X.509 PEM:\n" + pubKeyX509Pem);
            FileUtil.writeFile("target/ec.x509.pub.pem", pubKeyX509Pem.getBytes(StandardCharsets.UTF_8));

            // X509 PEM => X.509 DER
            byte[] pubKeyFromPem = BCECUtil.convertECPublicKeyPEMToX509(pubKeyX509Pem);
            if (!Arrays.equals(pubKeyFromPem, publicKeyX509Der)) {
                throw new Exception("pubKeyFromPem != publicKeyX509Der");
            }

            // BCECPrivateKey newPrivateKey = BCECUtil.convertPKCS8ToECPrivateKey(privateKeyPkcs8Der);
        } catch (Exception ex) {
            ex.printStackTrace();
            Assert.isTrue(false, "");
        }
    }

34.5.3 JWS 签名与验签类

JWSJSON Web Signature,是携带了签名的 JWT。我们在前文教程中使用的就是 JWS。Spring Security 的 JWS 认证机制非常复杂,并且没有专门的文档来详细描述。下面教程展示的方法是对其源代码进行静态和动态分析之后的成果

1. SM2JWSProvider

这个类是 JWS 签名和验签的公共基础类,它继承自 BaseJWSProvider,主要定义当前支持的算法名称:

package com.longser.security.jose;

import com.nimbusds.jose.JWSAlgorithm;
import com.nimbusds.jose.crypto.impl.BaseJWSProvider;

import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.Set;

/**
 * JWS 签名和验签的公共基础类,主要定义当前支持的算法名称
 * @author David Jia
 */
public class SM2JWSProvider extends BaseJWSProvider {
    public static final Set<JWSAlgorithm> SUPPORTED_ALGORITHMS;

    protected SM2JWSProvider() {
        super(SUPPORTED_ALGORITHMS);
    }

    static {
        Set<JWSAlgorithm> algs = new LinkedHashSet<>();
        algs.add(JWSAlgorithm.parse("SM2"));
        SUPPORTED_ALGORITHMS = Collections.unmodifiableSet(algs);
    }
}

2. SM2JWSSigner

这是 JWS 签名时用的类。只需要在方法 sign 中计算对signingInput的签名并且以 Base64URL 格式返回:

package com.longser.security.jose;

import com.longser.crypto.SM2;
import com.nimbusds.jose.JWSHeader;
import com.nimbusds.jose.JWSSigner;
import com.nimbusds.jose.util.Base64URL;
import lombok.SneakyThrows;
import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPrivateKey;
import org.springframework.util.Assert;

/**
 * JWS 签名类。在方法 sign 中计算对signingInput的签名并且以 Base64URL 格式返回
 * @author David Jia
 */
public class SM2JWSSigner extends SM2JWSProvider implements JWSSigner {
    private final BCECPrivateKey privateKey;

    public SM2JWSSigner(BCECPrivateKey privateKey) {
        this.privateKey = privateKey;
    }

    @Override
    @SneakyThrows
    public Base64URL sign(JWSHeader header, final byte[] signingInput) {
        Assert.isTrue("SM2".equals(header.getAlgorithm().getName()), "Wrong algorithm used ");

        return Base64URL.encode(SM2.sign(this.privateKey, signingInput));
    }
}

3. SM2JWSVerifier

这是 JWS 验签时用的类。

package com.longser.security.jose;

import com.longser.crypto.SM2;
import com.nimbusds.jose.CriticalHeaderParamsAware;
import com.nimbusds.jose.JWSHeader;
import com.nimbusds.jose.JWSVerifier;
import com.nimbusds.jose.crypto.impl.CriticalHeaderParamsDeferral;
import com.nimbusds.jose.util.Base64URL;
import lombok.SneakyThrows;
import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey;

import javax.annotation.concurrent.ThreadSafe;
import java.util.Set;

/**
 * JWS 验签类。在方法 verify 中比较对 signedContent计算签名的结果是否与 signature 一致。
 * @author David Jia
 */
@ThreadSafe
public class SM2JWSVerifier extends SM2JWSProvider implements JWSVerifier, CriticalHeaderParamsAware {
    private final CriticalHeaderParamsDeferral critPolicy;
    private final BCECPublicKey publicKey;

    public SM2JWSVerifier(BCECPublicKey publicKey) {
        this(publicKey, null);
    }

    public SM2JWSVerifier(BCECPublicKey publicKey, Set<String> defCritHeaders) {
        this.critPolicy = new CriticalHeaderParamsDeferral();
        if (publicKey == null) {
            throw new IllegalArgumentException("The public RSA key must not be null");
        } else {
            this.publicKey = publicKey;
            this.critPolicy.setDeferredCriticalHeaderParams(defCritHeaders);
        }
    }

    public BCECPublicKey getPublicKey() {
        return this.publicKey;
    }

    @Override
    public Set<String> getProcessedCriticalHeaderParams() {
        return this.critPolicy.getProcessedCriticalHeaderParams();
    }

    @Override
    public Set<String> getDeferredCriticalHeaderParams() {
        return this.critPolicy.getDeferredCriticalHeaderParams();
    }

    @Override
    @SneakyThrows
    public boolean verify(JWSHeader header, byte[] signedContent, Base64URL signature) {
        if (!this.critPolicy.headerPasses(header)) {
            return false;
        } else {
            return SM2.verify(publicKey, signedContent, signature.decode());
        }
    }
}

34.5.4 定义解码器与验签工厂

Spring Security 验证 JWS 的过程比签名过程复杂。定义验签器(JWS Verifier)以后,还需要分别定义 JWT 节解码器(JWT Decoder Builder) 和 JWS 验签工厂(JWS Verifier Factory),他们的关系是前者调用后者。

1. EnhancedJWSVerifierFactory

如同它的名字,这是在现有 DefaultJWSVerifierFactory 基础上的增强,在保留原算法的基础上增加了对 SM2 算法的支持。因为 DefaultJWSVerifierFactory 设计的不容易继承,所以这里的作法是复制它的代码后进行改写。

package com.longser.security.jose;

import com.nimbusds.jose.*;
import com.nimbusds.jose.crypto.ECDSAVerifier;
import com.nimbusds.jose.crypto.MACVerifier;
import com.nimbusds.jose.crypto.RSASSAVerifier;
import com.nimbusds.jose.jca.JCAContext;
import com.nimbusds.jose.proc.JWSVerifierFactory;
import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey;

import javax.annotation.concurrent.ThreadSafe;
import javax.crypto.SecretKey;
import java.security.Key;
import java.security.interfaces.ECPublicKey;
import java.security.interfaces.RSAPublicKey;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.Set;

/**
 * 如同它的名字,这是在现有 DefaultJWSVerifierFactory 基础上的增强,在保留原算法的基础上增加了对  SM2
 * 算法的支持。因为 DefaultJWSVerifierFactory 设计的不容易继承,所以这里的作法是复制它的代码后进行改写。
 * @author David Jia
 */
@ThreadSafe
public class EnhancedJWSVerifierFactory implements JWSVerifierFactory {
    public static final Set<JWSAlgorithm> SUPPORTED_ALGORITHMS;
    private final JCAContext jcaContext = new JCAContext();

    public EnhancedJWSVerifierFactory() {
    }

    @Override
    public Set<JWSAlgorithm> supportedJWSAlgorithms() {
        return SUPPORTED_ALGORITHMS;
    }

    @Override
    public JCAContext getJCAContext() {
        return this.jcaContext;
    }

    @Override
    public JWSVerifier createJWSVerifier(JWSHeader header, Key key) throws JOSEException {
        JWSVerifier verifier;

        // 先判断 header 中声明的是否为 SM2 算法
        if (SM2JWSVerifier.SUPPORTED_ALGORITHMS.contains(header.getAlgorithm())) {
            if (!(key instanceof BCECPublicKey)) {
                throw new KeyTypeException(BCECPublicKey.class);
            }

            BCECPublicKey bcecPublicKey = (BCECPublicKey)key;
            verifier = new SM2JWSVerifier(bcecPublicKey);
        } else if (MACVerifier.SUPPORTED_ALGORITHMS.contains(header.getAlgorithm())) {
            if (!(key instanceof SecretKey)) {
                throw new KeyTypeException(SecretKey.class);
            }

            SecretKey macKey = (SecretKey)key;
            verifier = new MACVerifier(macKey);
        } else if (RSASSAVerifier.SUPPORTED_ALGORITHMS.contains(header.getAlgorithm())) {
            if (!(key instanceof RSAPublicKey)) {
                throw new KeyTypeException(RSAPublicKey.class);
            }

            RSAPublicKey rsaPublicKey = (RSAPublicKey)key;
            verifier = new RSASSAVerifier(rsaPublicKey);
        } else {
            if (!ECDSAVerifier.SUPPORTED_ALGORITHMS.contains(header.getAlgorithm())) {
                throw new JOSEException("Unsupported JWS algorithm: " + header.getAlgorithm());
            }

            if (!(key instanceof ECPublicKey)) {
                throw new KeyTypeException(ECPublicKey.class);
            }

            ECPublicKey ecPublicKey = (ECPublicKey)key;
            verifier = new ECDSAVerifier(ecPublicKey);
        }

        verifier.getJCAContext().setProvider(this.jcaContext.getProvider());
        return verifier;
    }

    static {
        Set<JWSAlgorithm> algs = new LinkedHashSet<>();
        algs.addAll(MACVerifier.SUPPORTED_ALGORITHMS);
        algs.addAll(RSASSAVerifier.SUPPORTED_ALGORITHMS);
        algs.addAll(ECDSAVerifier.SUPPORTED_ALGORITHMS);
        algs.add(JWSAlgorithm.parse("SM2"));
        SUPPORTED_ALGORITHMS = Collections.unmodifiableSet(algs);
    }
}

2. SM2JwtDecoderBuilder

这个类基本上和 RSAJwtDecoderBuilder 一样,主要的区别就是声明使用的是 SM2算法,以及改调用 DefaultJWSVerifierFactory 为我们定义的EnhancedJWSVerifierFactory。

package com.longser.security.jose;

import com.nimbusds.jose.JWSAlgorithm;
import com.nimbusds.jose.crypto.factories.DefaultJWEDecrypterFactory;
import com.nimbusds.jose.proc.JWSKeySelector;
import com.nimbusds.jose.proc.SecurityContext;
import com.nimbusds.jose.proc.SingleKeyJWSKeySelector;
import com.nimbusds.jwt.proc.ConfigurableJWTProcessor;
import com.nimbusds.jwt.proc.DefaultJWTProcessor;
import com.nimbusds.jwt.proc.JWTProcessor;
import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey;
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
import org.springframework.util.Assert;

import java.util.function.Consumer;

/**
 * 这个类基本上和 RSAJwtDecoderBuilder 一样,主要的区别就是声明使用的是 SM2算法,以及改调用
 * DefaultJWSVerifierFactory 为我们定义的EnhancedJWSVerifierFactory。
 * @author David Jia
 */
@SuppressWarnings("unused")
public class SM2JwtDecoderBuilder {
    private JWSAlgorithm jwsAlgorithm;
    private final BCECPublicKey key;
    private Consumer<ConfigurableJWTProcessor<SecurityContext>> jwtProcessorCustomizer;

    public SM2JwtDecoderBuilder(BCECPublicKey key) {
        Assert.notNull(key, "key cannot be null");
        // 这里声明使用的是 SM2 算法
        this.jwsAlgorithm = JWSAlgorithm.parse("SM2");
        this.key = key;
        this.jwtProcessorCustomizer = (processor) -> {
        };
    }

    public SM2JwtDecoderBuilder signatureAlgorithm(SignatureAlgorithm signatureAlgorithm) {
        Assert.notNull(signatureAlgorithm, "signatureAlgorithm cannot be null");
        this.jwsAlgorithm = JWSAlgorithm.parse(signatureAlgorithm.getName());
        return this;
    }

    public SM2JwtDecoderBuilder jwtProcessorCustomizer(Consumer<ConfigurableJWTProcessor<SecurityContext>> jwtProcessorCustomizer) {
        Assert.notNull(jwtProcessorCustomizer, "jwtProcessorCustomizer cannot be null");
        this.jwtProcessorCustomizer = jwtProcessorCustomizer;
        return this;
    }

    JWTProcessor<SecurityContext> processor() {

        JWSKeySelector<SecurityContext> jwsKeySelector = new SingleKeyJWSKeySelector<>(this.jwsAlgorithm, this.key);

        DefaultJWTProcessor<SecurityContext> jwtProcessor = new DefaultJWTProcessor<>();
        // DefaultJWTProcessor 默认使用 DefaultJWSVerifierFactory,指定我们的定义
        jwtProcessor.setJWSVerifierFactory(new EnhancedJWSVerifierFactory());

        jwtProcessor.setJWEDecrypterFactory(new DefaultJWEDecrypterFactory());

        jwtProcessor.setJWSKeySelector(jwsKeySelector);
        jwtProcessor.setJWTClaimsSetVerifier((claims, context) -> {
        });
        this.jwtProcessorCustomizer.accept(jwtProcessor);
        return jwtProcessor;
    }

    public NimbusJwtDecoder build() {
        return new NimbusJwtDecoder(this.processor());
    }
}

34.5.5 给JWT工具类增加方法

完成前文各种类定义之后,首先在创建 JWT 的工具类中使用 SM2JWSSigner。

1. JwtBuilder

首先在 JwtBuilder 中增加一个生成 SM2 签名器的方法

    public JwtBuilder signer(BCECPrivateKey privateKey) {
        this.signer = new SM2JWSSigner(privateKey);
        return this;
    }

2. JwtUtils

然后在 JwtUtils 中增加两个使用 BCECPrivateKey 创建 JWT 的方法。

    public static String create(String username,
                                long expiry,
                                BCECPrivateKey privateKey,
                                Authentication authentication) throws MalformedURLException {
        setJwtData(username, expiry);
        jwtBuilder.header(JWSAlgorithm.parse("SM2"));
        jwtBuilder.claim("scope", getCope(authentication));

        return jwtBuilder.signer(privateKey).build();
    }

    public static String create(String username, long expiry, BCECPrivateKey privateKey) throws MalformedURLException {
        setJwtData(username, expiry);
        jwtBuilder.header(JWSAlgorithm.parse("SM2"));

        return jwtBuilder.signer(privateKey).build();
    }

34.5.6 各种组装工作

把我们在测试过程中保存的公钥和私钥字符串配置在 application.yml 中:

application:
  crypto:
    SM2:
      private.key: MIIBUQIBAQQgQyqY+BGIx6bSVRKPquo4VyO54KMtA0P4lWSIq2jA1+uggeMwgeACAQEwLAYHKoZIzj0BAQIhAP////7/////////////////////AAAAAP//////////MEQEIP////7/////////////////////AAAAAP/////////8BCAo6fqenZ9eNE1ankvPZQmn85eJ9RWrj5LdvL1BTZQOkwRBBDLEriwfGYEZX5kERmo5yZSP4wu/8mYL4XFaRYkzTHTHvDc2ovT2d5xZvc7ja2khU9Cph3zGKkdAAt8y5SE58KACIQD////+////////////////cgPfayHGBStTu/QJOdVBIwIBAaFEA0IABJWqmlZ3mDdbVvnZ71UGjbB0GtxLhWIxLmESi0Uv7IKwMvsv17cs2E3ExNpd598xwQzfJyMKUMeqRYUWQqCpU+4=
      public.key: MIIBMzCB7AYHKoZIzj0CATCB4AIBATAsBgcqhkjOPQEBAiEA/////v////////////////////8AAAAA//////////8wRAQg/////v////////////////////8AAAAA//////////wEICjp+p6dn140TVqeS89lCafzl4n1FauPkt28vUFNlA6TBEEEMsSuLB8ZgRlfmQRGajnJlI/jC7/yZgvhcVpFiTNMdMe8Nzai9PZ3nFm9zuNraSFT0KmHfMYqR0AC3zLlITnwoAIhAP////7///////////////9yA99rIcYFK1O79Ak51UEjAgEBA0IABJWqmlZ3mDdbVvnZ71UGjbB0GtxLhWIxLmESi0Uv7IKwMvsv17cs2E3ExNpd598xwQzfJyMKUMeqRYUWQqCpU+4=

然后在 SecurityConfig 中定义两个属性变量:

    @Value("${application.crypto.SM2.public.key}")
    String sm2PublicKeyBase64;

    @Value("${application.crypto.SM2.private.key}")
    String sm2PrivateKeyBase64;

然后在 SecurityConfig 中定义两个 Bean 分别对应公钥和私钥对象

    @Bean
    BCECPublicKey bcecPublicKey() {
        return SM2.getPublicKeyFromBase64(sm2PublicKeyBase64);
    }

    @Bean
    BCECPrivateKey bcecPrivateKey() {
        return SM2.getPrivteKeyFromBase64(sm2PrivateKeyBase64);
    }

现在修改 jwtDecoder 的定义,使用 SM2JwtDecoderBuilder 来生成 JwtDecoder (这里是那一系列验签相关类的入口):

    @Bean
    JwtDecoder jwtDecoder() throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, NoSuchProviderException {
-       return NimbusJwtDecoder.withPublicKey(this.key).build()
+       return (new SM2JwtDecoderBuilder(bcecPublicKey())).build();
    }

当然,如果你要确认前文我们做的和 OAuth 2 Resource Server 有关的设置还存在:

    @Override
    protected void configure(HttpSecurity http) throws Exception {

        ... 省略代码 ...

        http.logout().logoutUrl("/api/logout")

        ... 省略代码 ...

                .and()
                .oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt)
                .sessionManagement((session) -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))

        ... 省略代码 ...

        http.getConfigurer(OAuth2ResourceServerConfigurer.class)
                .authenticationEntryPoint(new BearerTokenUnauthorized());
    }

34.5.7 修改登录 API

现在我们修改 LoginController 控制器的代码,首先注入私钥

    @Autowired
    BCECPrivateKey bcecPrivateKey;

然后在登录成功以后,给前端返回用新的私钥签名的 JWT

-return JwtUtils.create(username, jwtTimeToLive, privateKey, authentication);
+return JwtUtils.create(username, jwtTimeToLive, bcecPrivateKey, authentication);

34.5.8 测试新算法是否工作

如果你是严格按照教程的步骤走过来的,那么用之前讲解 JWT 时的测试方法应该是成功的。

34.6 关于对称加密算法SM4

所谓对称加密算法,指的是加密和解密使用相同的密钥。它一般用于需要高速对较大数据量进行加密、解密的场合。作为优秀的对称加密算法,国密算法中的 SM4 与 AES 算法相似。因为两者的模式也相同,因此经常被放在一起做对比研究。

SM4 作为分组密码算法,它的分组长度和 AES 一样为128比特,这意味着算法本身每次最多只能处理16个字节。网络流量加密和文件加密等场景,均要求处理更长的消息,利用分组密码算法完成信息加密时,必须借助分组密码算法的工作模式。常见的工作模式有ECB、 CBC、CTR、 CFB、 OFB、GCM、CCM、FPE等。所有各种 SM4 衍生的算法模式都基于 SM4 的基础轮函数ECB。CCM 和 GCM 是 SM4与 MAC 的结合的高级模式。其中 CCM 是 CTR 加密模式和 CMAC 认证算法的混合使用,常用在需要同时加密和认证的领域,比如WiFi安全中的WPE协议,它就使用了AES-CCM模式。下面是各种算法模式的简要说明:

  • ECB:Electronic Codebook Mode,也即电子密码本模式
  • CBC:Cipher Block Chaining Mode,也即密文分组链接模式
  • CTR:Counter Mode,也即计数器模式
  • CFB:Cipher Feedback Mode,也即密文反馈模式
  • OFB:Output Feedback Mode,也即输出反馈模式
  • GCM:Galois/Counter Mode,也即伽罗瓦计数器模式
  • CCM:Counter with CBC-MAC,也即带CBC-MAC的技术器模式
  • FPE:Format-Preserving Encryption Mode,也即保留格式加密模式

限于篇幅和教程目标,本文无法详细讨论在面对具体的工程实现任务时如何从中选用合适的工作模式,根据实际分析的结论,建议直接采用GCM

同样的原因本文也不在这里展开讨论 SM4 算法的封装 和使用,如果在项目中需要使用 SM4 算法,请参考如下链接的内容:

@SuppressWarnings(“unused”) public class SM2Cipher { /**

 * ECC密钥
 */
private byte[] c1;

/**
 * 真正的密文
 */
private byte[] c2;

/**
 * 对(c1+c2)的SM3-HASH值
 */
private byte[] c3;

/**
 * SM2标准的密文,即(c1+c2+c3)
 */
private byte[] cipherText;

public byte[] getC1() {
    return c1;
}

public void setC1(byte[] c1) {
    this.c1 = c1;
}

public byte[] getC2() {
    return c2;
}

public void setC2(byte[] c2) {
    this.c2 = c2;
}

public byte[] getC3() {
    return c3;
}

public void setC3(byte[] c3) {
    this.c3 = c3;
}

public byte[] getCipherText() {
    return cipherText;
}

public void setCipherText(byte[] cipherText) {
    this.cipherText = cipherText;
}

}

<a name="wAaDD"></a>
### 34.7.2 BC椭圆曲线工具类
这是一个封装了 Bouncy Castle 各种椭圆曲线(Eclipse Curve)相关的数据类型转换的工具类,在主要用于各种格式密钥的转换。
```javascript
package com.longser.crypto;

import org.bouncycastle.asn1.ASN1Encodable;
import org.bouncycastle.asn1.ASN1EncodableVector;
import org.bouncycastle.asn1.ASN1Encoding;
import org.bouncycastle.asn1.ASN1Integer;
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
import org.bouncycastle.asn1.ASN1OctetString;
import org.bouncycastle.asn1.ASN1Primitive;
import org.bouncycastle.asn1.DERNull;
import org.bouncycastle.asn1.DEROctetString;
import org.bouncycastle.asn1.DERSequence;
import org.bouncycastle.asn1.pkcs.PrivateKeyInfo;
import org.bouncycastle.asn1.x509.AlgorithmIdentifier;
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo;
import org.bouncycastle.asn1.x9.X962Parameters;
import org.bouncycastle.asn1.x9.X9ECParameters;
import org.bouncycastle.asn1.x9.X9ECPoint;
import org.bouncycastle.asn1.x9.X9ObjectIdentifiers;
import org.bouncycastle.crypto.AsymmetricCipherKeyPair;
import org.bouncycastle.crypto.generators.ECKeyPairGenerator;
import org.bouncycastle.crypto.params.ECDomainParameters;
import org.bouncycastle.crypto.params.ECKeyGenerationParameters;
import org.bouncycastle.crypto.params.ECKeyParameters;
import org.bouncycastle.crypto.params.ECPrivateKeyParameters;
import org.bouncycastle.crypto.params.ECPublicKeyParameters;
import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPrivateKey;
import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey;
import org.bouncycastle.jcajce.provider.asymmetric.util.EC5Util;
import org.bouncycastle.jcajce.provider.asymmetric.util.ECUtil;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.jce.spec.ECNamedCurveSpec;
import org.bouncycastle.jce.spec.ECParameterSpec;
import org.bouncycastle.math.ec.ECCurve;
import org.bouncycastle.math.ec.ECPoint;
import org.bouncycastle.math.ec.FixedPointCombMultiplier;
import org.bouncycastle.pqc.math.linearalgebra.ByteUtils;
import org.bouncycastle.util.io.pem.PemObject;
import org.bouncycastle.util.io.pem.PemReader;
import org.bouncycastle.util.io.pem.PemWriter;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.math.BigInteger;
import java.security.InvalidAlgorithmParameterException;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.SecureRandom;
import java.security.spec.ECGenParameterSpec;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;

/**
 * 这个工具类的方法,也适用于其他基于BC库的ECC算法
 */
@SuppressWarnings("unused")
public class BCECUtil {
    private static final String ALGO_NAME_EC = "EC";
    private static final String PEM_STRING_PUBLIC = "PUBLIC KEY";
    private static final String PEM_STRING_ECPRIVATEKEY = "EC PRIVATE KEY";

    /**
     * 生成ECC密钥对
     *
     * @return ECC密钥对
     */
    public static AsymmetricCipherKeyPair generateKeyPairParameter(
            ECDomainParameters domainParameters, SecureRandom random) {
        ECKeyGenerationParameters keyGenerationParams = new ECKeyGenerationParameters(domainParameters,
                random);
        ECKeyPairGenerator keyGen = new ECKeyPairGenerator();
        keyGen.init(keyGenerationParams);
        return keyGen.generateKeyPair();
    }

    public static KeyPair generateKeyPair(ECDomainParameters domainParameters, SecureRandom random)
            throws NoSuchProviderException, NoSuchAlgorithmException,
            InvalidAlgorithmParameterException {
        KeyPairGenerator kpg = KeyPairGenerator.getInstance(ALGO_NAME_EC, BouncyCastleProvider.PROVIDER_NAME);
        ECParameterSpec parameterSpec = new ECParameterSpec(domainParameters.getCurve(), domainParameters.getG(),
                domainParameters.getN(), domainParameters.getH());
        kpg.initialize(parameterSpec, random);
        return kpg.generateKeyPair();
    }

    public static int getCurveLength(ECKeyParameters ecKey) {
        return getCurveLength(ecKey.getParameters());
    }

    public static int getCurveLength(ECDomainParameters domainParams) {
        return (domainParams.getCurve().getFieldSize() + 7) / 8;
    }

    public static byte[] fixToCurveLengthBytes(int curveLength, byte[] src) {
        if (src.length == curveLength) {
            return src;
        }

        byte[] result = new byte[curveLength];
        if (src.length > curveLength) {
            System.arraycopy(src, src.length - result.length, result, 0, result.length);
        } else {
            System.arraycopy(src, 0, result, result.length - src.length, src.length);
        }
        return result;
    }

    /**
     * @param dHex             十六进制字符串形式的私钥d值,如果是SM2算法,Hex字符串长度应该是64(即32字节)
     * @param domainParameters EC Domain参数,一般是固定的,如果是SM2算法的可参考{@link SM2#DOMAIN_PARAMS}
     */
    public static ECPrivateKeyParameters createECPrivateKeyParameters(
            String dHex, ECDomainParameters domainParameters) {
        return createECPrivateKeyParameters(ByteUtils.fromHexString(dHex), domainParameters);
    }

    /**
     * @param dBytes           字节数组形式的私钥d值,如果是SM2算法,应该是32字节
     * @param domainParameters EC Domain参数,一般是固定的,如果是SM2算法的可参考{@link SM2#DOMAIN_PARAMS}
     */
    public static ECPrivateKeyParameters createECPrivateKeyParameters(
            byte[] dBytes, ECDomainParameters domainParameters) {
        return createECPrivateKeyParameters(new BigInteger(1, dBytes), domainParameters);
    }

    /**
     * @param d                大数形式的私钥d值
     * @param domainParameters EC Domain参数,一般是固定的,如果是SM2算法的可参考{@link SM2#DOMAIN_PARAMS}
     */
    public static ECPrivateKeyParameters createECPrivateKeyParameters(
            BigInteger d, ECDomainParameters domainParameters) {
        return new ECPrivateKeyParameters(d, domainParameters);
    }

    /**
     * 根据EC私钥构造EC公钥
     *
     * @param priKey ECC私钥参数对象
     */
    public static ECPublicKeyParameters buildECPublicKeyByPrivateKey(ECPrivateKeyParameters priKey) {
        ECDomainParameters domainParameters = priKey.getParameters();
        ECPoint q = new FixedPointCombMultiplier().multiply(domainParameters.getG(), priKey.getD());
        return new ECPublicKeyParameters(q, domainParameters);
    }

    /**
     * @param x                大数形式的公钥x分量
     * @param y                大数形式的公钥y分量
     * @param curve            EC曲线参数,一般是固定的,如果是SM2算法的可参考{@link SM2#CURVE}
     * @param domainParameters EC Domain参数,一般是固定的,如果是SM2算法的可参考{@link SM2#DOMAIN_PARAMS}
     */
    public static ECPublicKeyParameters createECPublicKeyParameters(
            BigInteger x, BigInteger y, ECCurve curve, ECDomainParameters domainParameters) {
        return createECPublicKeyParameters(x.toByteArray(), y.toByteArray(), curve, domainParameters);
    }

    /**
     * @param xHex             十六进制形式的公钥x分量,如果是SM2算法,Hex字符串长度应该是64(即32字节)
     * @param yHex             十六进制形式的公钥y分量,如果是SM2算法,Hex字符串长度应该是64(即32字节)
     * @param curve            EC曲线参数,一般是固定的,如果是SM2算法的可参考{@link SM2#CURVE}
     * @param domainParameters EC Domain参数,一般是固定的,如果是SM2算法的可参考{@link SM2#DOMAIN_PARAMS}
     */
    public static ECPublicKeyParameters createECPublicKeyParameters(
            String xHex, String yHex, ECCurve curve, ECDomainParameters domainParameters) {
        return createECPublicKeyParameters(ByteUtils.fromHexString(xHex), ByteUtils.fromHexString(yHex),
                curve, domainParameters);
    }

    /**
     * @param xBytes           十六进制形式的公钥x分量,如果是SM2算法,应该是32字节
     * @param yBytes           十六进制形式的公钥y分量,如果是SM2算法,应该是32字节
     * @param curve            EC曲线参数,一般是固定的,如果是SM2算法的可参考{@link SM2#CURVE}
     * @param domainParameters EC Domain参数,一般是固定的,如果是SM2算法的可参考{@link SM2#DOMAIN_PARAMS}
     */
    public static ECPublicKeyParameters createECPublicKeyParameters(
            byte[] xBytes, byte[] yBytes, ECCurve curve, ECDomainParameters domainParameters) {
        final byte uncompressedFlag = 0x04;
        int curveLength = getCurveLength(domainParameters);
        xBytes = fixToCurveLengthBytes(curveLength, xBytes);
        yBytes = fixToCurveLengthBytes(curveLength, yBytes);
        byte[] encodedPubKey = new byte[1 + xBytes.length + yBytes.length];
        encodedPubKey[0] = uncompressedFlag;
        System.arraycopy(xBytes, 0, encodedPubKey, 1, xBytes.length);
        System.arraycopy(yBytes, 0, encodedPubKey, 1 + xBytes.length, yBytes.length);
        return new ECPublicKeyParameters(curve.decodePoint(encodedPubKey), domainParameters);
    }

    public static ECPrivateKeyParameters convertPrivateKeyToParameters(BCECPrivateKey ecPriKey) {
        ECParameterSpec parameterSpec = ecPriKey.getParameters();
        ECDomainParameters domainParameters = new ECDomainParameters(parameterSpec.getCurve(), parameterSpec.getG(),
                parameterSpec.getN(), parameterSpec.getH());
        return new ECPrivateKeyParameters(ecPriKey.getD(), domainParameters);
    }

    public static ECPublicKeyParameters convertPublicKeyToParameters(BCECPublicKey ecPubKey) {
        ECParameterSpec parameterSpec = ecPubKey.getParameters();
        ECDomainParameters domainParameters = new ECDomainParameters(parameterSpec.getCurve(), parameterSpec.getG(),
                parameterSpec.getN(), parameterSpec.getH());
        return new ECPublicKeyParameters(ecPubKey.getQ(), domainParameters);
    }

    public static BCECPublicKey createPublicKeyFromSubjectPublicKeyInfo(SubjectPublicKeyInfo subPubInfo)
            throws NoSuchProviderException,
            NoSuchAlgorithmException, InvalidKeySpecException, IOException {
        return BCECUtil.convertX509ToECPublicKey(subPubInfo.toASN1Primitive().getEncoded(ASN1Encoding.DER));
    }

    /**
     * 将ECC私钥转换为PKCS8标准的字节流
     *
     * @param priKey 私钥
     * @param pubKey 可以为空,但是如果为空的话得到的结果OpenSSL可能解析不了
     */
    public static byte[] convertECPrivateKeyToPKCS8(
            ECPrivateKeyParameters priKey, ECPublicKeyParameters pubKey) {
        ECDomainParameters domainParams = priKey.getParameters();
        ECParameterSpec spec = new ECParameterSpec(domainParams.getCurve(), domainParams.getG(),
                domainParams.getN(), domainParams.getH());
        BCECPublicKey publicKey = null;
        if (pubKey != null) {
            publicKey = new BCECPublicKey(ALGO_NAME_EC, pubKey, spec,
                    BouncyCastleProvider.CONFIGURATION);
        }
        BCECPrivateKey privateKey = new BCECPrivateKey(ALGO_NAME_EC, priKey, publicKey,
                spec, BouncyCastleProvider.CONFIGURATION);
        return privateKey.getEncoded();
    }

    /**
     * 将PKCS8标准的私钥字节流转换为私钥对象
     */
    public static BCECPrivateKey convertPKCS8ToECPrivateKey(byte[] pkcs8Key)
            throws NoSuchAlgorithmException, NoSuchProviderException, InvalidKeySpecException {
        PKCS8EncodedKeySpec peks = new PKCS8EncodedKeySpec(pkcs8Key);
        KeyFactory kf = KeyFactory.getInstance(ALGO_NAME_EC, BouncyCastleProvider.PROVIDER_NAME);
        return (BCECPrivateKey) kf.generatePrivate(peks);
    }

    /**
     * 将PKCS8标准的私钥字节流转换为PEM
     */
    public static String convertECPrivateKeyPKCS8ToPEM(byte[] encodedKey) throws IOException {
        return convertEncodedDataToPEM(PEM_STRING_ECPRIVATEKEY, encodedKey);
    }

    /**
     * 将PEM格式的私钥转换为PKCS8标准字节流
     */
    public static byte[] convertECPrivateKeyPEMToPKCS8(String pemString) throws IOException {
        return convertPEMToEncodedData(pemString);
    }

    /**
     * 将ECC私钥转换为SEC1标准的字节流
     * openssl d2i_ECPrivateKey函数要求的DER编码的私钥也是SEC1标准的,
     * 这个工具函数的主要目的就是为了能生成一个openssl可以直接“识别”的ECC私钥.
     * 相对RSA私钥的PKCS1标准,ECC私钥的标准为SEC1
     */
    public static byte[] convertECPrivateKeyToSEC1(
            ECPrivateKeyParameters priKey, ECPublicKeyParameters pubKey) throws IOException {
        byte[] pkcs8Bytes = convertECPrivateKeyToPKCS8(priKey, pubKey);
        PrivateKeyInfo pki = PrivateKeyInfo.getInstance(pkcs8Bytes);
        ASN1Encodable encodable = pki.parsePrivateKey();
        ASN1Primitive primitive = encodable.toASN1Primitive();
        return primitive.getEncoded();
    }

    /**
     * 将SEC1标准的私钥字节流恢复为PKCS8标准的字节流
     */
    public static byte[] convertECPrivateKeySEC1ToPKCS8(byte[] sec1Key) throws IOException {
        /*
         * 参考org.bouncycastle.asn1.pkcs.PrivateKeyInfo和
         * org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPrivateKey,逆向拼装
         */
        X962Parameters params = getDomainParametersFromName(SM2.JDK_EC_SPEC, false);
        ASN1OctetString privKey = new DEROctetString(sec1Key);
        ASN1EncodableVector v = new ASN1EncodableVector();
        v.add(new ASN1Integer(0)); //版本号
        v.add(new AlgorithmIdentifier(X9ObjectIdentifiers.id_ecPublicKey, params)); //算法标识
        v.add(privKey);
        DERSequence ds = new DERSequence(v);
        return ds.getEncoded(ASN1Encoding.DER);
    }

    /**
     * 将SEC1标准的私钥字节流转为BCECPrivateKey对象
     */
    public static BCECPrivateKey convertSEC1ToBCECPrivateKey(byte[] sec1Key)
            throws NoSuchAlgorithmException, NoSuchProviderException, InvalidKeySpecException, IOException {
        PKCS8EncodedKeySpec peks = new PKCS8EncodedKeySpec(convertECPrivateKeySEC1ToPKCS8(sec1Key));
        KeyFactory kf = KeyFactory.getInstance(ALGO_NAME_EC, BouncyCastleProvider.PROVIDER_NAME);
        return (BCECPrivateKey) kf.generatePrivate(peks);
    }

    /**
     * 将SEC1标准的私钥字节流转为ECPrivateKeyParameters对象
     * openssl i2d_ECPrivateKey函数生成的DER编码的ecc私钥是:SEC1标准的、带有EC_GROUP、带有公钥的,
     * 这个工具函数的主要目的就是为了使Java程序能够“识别”openssl生成的ECC私钥
     */
    public static ECPrivateKeyParameters convertSEC1ToECPrivateKey(byte[] sec1Key)
            throws NoSuchAlgorithmException, NoSuchProviderException, InvalidKeySpecException, IOException {
        BCECPrivateKey privateKey = convertSEC1ToBCECPrivateKey(sec1Key);
        return convertPrivateKeyToParameters(privateKey);
    }

    /**
     * 将ECC公钥对象转换为X509标准的字节流
     */
    public static byte[] convertECPublicKeyToX509(ECPublicKeyParameters pubKey) {
        ECDomainParameters domainParams = pubKey.getParameters();
        ECParameterSpec spec = new ECParameterSpec(domainParams.getCurve(), domainParams.getG(),
                domainParams.getN(), domainParams.getH());
        BCECPublicKey publicKey = new BCECPublicKey(ALGO_NAME_EC, pubKey, spec,
                BouncyCastleProvider.CONFIGURATION);
        return publicKey.getEncoded();
    }

    /**
     * 将X509标准的公钥字节流转为公钥对象
     */
    public static BCECPublicKey convertX509ToECPublicKey(byte[] x509Bytes) throws NoSuchProviderException,
            NoSuchAlgorithmException, InvalidKeySpecException {
        X509EncodedKeySpec eks = new X509EncodedKeySpec(x509Bytes);
        KeyFactory kf = KeyFactory.getInstance("EC", BouncyCastleProvider.PROVIDER_NAME);
        return (BCECPublicKey) kf.generatePublic(eks);
    }

    /**
     * 将X509标准的公钥字节流转为PEM
     */
    public static String convertECPublicKeyX509ToPEM(byte[] encodedKey) throws IOException {
        return convertEncodedDataToPEM(PEM_STRING_PUBLIC, encodedKey);
    }

    /**
     * 将PEM格式的公钥转为X509标准的字节流
     */
    public static byte[] convertECPublicKeyPEMToX509(String pemString) throws IOException {
        return convertPEMToEncodedData(pemString);
    }

    /**
     * copy from BC
     */
    public static X9ECParameters getDomainParametersFromGenSpec(ECGenParameterSpec genSpec) {
        return getDomainParametersFromName(genSpec.getName());
    }

    /**
     * copy from BC
     */
    public static X9ECParameters getDomainParametersFromName(String curveName) {
        X9ECParameters domainParameters;
        try {
            if (curveName.charAt(0) >= '0' && curveName.charAt(0) <= '2') {
                ASN1ObjectIdentifier oidID = new ASN1ObjectIdentifier(curveName);
                domainParameters = ECUtil.getNamedCurveByOid(oidID);
            } else {
                if (curveName.indexOf(' ') > 0) {
                    curveName = curveName.substring(curveName.indexOf(' ') + 1);
                }
                domainParameters = ECUtil.getNamedCurveByName(curveName);
            }
        } catch (IllegalArgumentException ex) {
            domainParameters = ECUtil.getNamedCurveByName(curveName);
        }
        return domainParameters;
    }

    /**
     * copy from BC
     */
    public static X962Parameters getDomainParametersFromName(
            java.security.spec.ECParameterSpec ecSpec, boolean withCompression) {
        X962Parameters params;

        if (ecSpec instanceof ECNamedCurveSpec) {
            ASN1ObjectIdentifier curveOid = ECUtil.getNamedCurveOid(((ECNamedCurveSpec) ecSpec).getName());
            if (curveOid == null) {
                curveOid = new ASN1ObjectIdentifier(((ECNamedCurveSpec) ecSpec).getName());
            }
            params = new X962Parameters(curveOid);
        } else if (ecSpec == null) {
            params = new X962Parameters(DERNull.INSTANCE);
        } else {
            ECCurve curve = EC5Util.convertCurve(ecSpec.getCurve());

            X9ECParameters ecP = new X9ECParameters(
                    curve,
                    new X9ECPoint(EC5Util.convertPoint(curve, ecSpec.getGenerator()), withCompression),
                    ecSpec.getOrder(),
                    BigInteger.valueOf(ecSpec.getCofactor()),
                    ecSpec.getCurve().getSeed());

            params = new X962Parameters(ecP);
        }

        return params;
    }

    private static String convertEncodedDataToPEM(String type, byte[] encodedData) throws IOException {
        ByteArrayOutputStream bOut = new ByteArrayOutputStream();
        try (PemWriter pWrt = new PemWriter(new OutputStreamWriter(bOut))) {
            PemObject pemObj = new PemObject(type, encodedData);
            pWrt.writeObject(pemObj);
        }
        return bOut.toString();
    }

    private static byte[] convertPEMToEncodedData(String pemString) throws IOException {
        ByteArrayInputStream bIn = new ByteArrayInputStream(pemString.getBytes());
        try (PemReader pRdr = new PemReader(new InputStreamReader(bIn))) {
            PemObject pemObject = pRdr.readPemObject();
            return pemObject.getContent();
        }
    }
}

34.7.3 SM2 算法封装工具类

这里把类名 SM2Utils 修改成了 SM2

package com.longser.crypto;

import org.bouncycastle.asn1.ASN1Encodable;
import org.bouncycastle.asn1.ASN1EncodableVector;
import org.bouncycastle.asn1.ASN1Encoding;
import org.bouncycastle.asn1.ASN1Integer;
import org.bouncycastle.asn1.ASN1Sequence;
import org.bouncycastle.asn1.DEROctetString;
import org.bouncycastle.asn1.DERSequence;
import org.bouncycastle.crypto.AsymmetricCipherKeyPair;
import org.bouncycastle.crypto.CipherParameters;
import org.bouncycastle.crypto.CryptoException;
import org.bouncycastle.crypto.InvalidCipherTextException;
import org.bouncycastle.crypto.engines.SM2Engine;
import org.bouncycastle.crypto.engines.SM2Engine.Mode;
import org.bouncycastle.crypto.params.ECDomainParameters;
import org.bouncycastle.crypto.params.ECPrivateKeyParameters;
import org.bouncycastle.crypto.params.ECPublicKeyParameters;
import org.bouncycastle.crypto.params.ParametersWithID;
import org.bouncycastle.crypto.params.ParametersWithRandom;
import org.bouncycastle.crypto.signers.SM2Signer;
import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPrivateKey;
import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey;
import org.bouncycastle.math.ec.ECPoint;
import org.bouncycastle.math.ec.custom.gm.SM2P256V1Curve;

import java.io.IOException;
import java.math.BigInteger;
import java.security.InvalidAlgorithmParameterException;
import java.security.KeyPair;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.SecureRandom;
import java.security.spec.ECFieldFp;
import java.security.spec.EllipticCurve;

@SuppressWarnings("unused")
public class SM2 {
    /*
     * 以下为SM2推荐曲线参数
     */
    public static final SM2P256V1Curve CURVE = new SM2P256V1Curve();
    public final static BigInteger SM2_ECC_P = CURVE.getQ();
    public final static BigInteger SM2_ECC_A = CURVE.getA().toBigInteger();
    public final static BigInteger SM2_ECC_B = CURVE.getB().toBigInteger();
    public final static BigInteger SM2_ECC_N = CURVE.getOrder();
    public final static BigInteger SM2_ECC_H = CURVE.getCofactor();
    public final static BigInteger SM2_ECC_GX = new BigInteger(
            "32C4AE2C1F1981195F9904466A39C9948FE30BBFF2660BE1715A4589334C74C7", 16);
    public final static BigInteger SM2_ECC_GY = new BigInteger(
            "BC3736A2F4F6779C59BDCEE36B692153D0A9877CC62A474002DF32E52139F0A0", 16);
    public static final ECPoint G_POINT = CURVE.createPoint(SM2_ECC_GX, SM2_ECC_GY);
    public static final ECDomainParameters DOMAIN_PARAMS = new ECDomainParameters(CURVE, G_POINT,
            SM2_ECC_N, SM2_ECC_H);
    public static final int CURVE_LEN = BCECUtil.getCurveLength(DOMAIN_PARAMS);

    public static final EllipticCurve JDK_CURVE = new EllipticCurve(new ECFieldFp(SM2_ECC_P), SM2_ECC_A, SM2_ECC_B);
    public static final java.security.spec.ECPoint JDK_G_POINT = new java.security.spec.ECPoint(
            G_POINT.getAffineXCoord().toBigInteger(), G_POINT.getAffineYCoord().toBigInteger());
    public static final java.security.spec.ECParameterSpec JDK_EC_SPEC = new java.security.spec.ECParameterSpec(
            JDK_CURVE, JDK_G_POINT, SM2_ECC_N, SM2_ECC_H.intValue());

    //////////////////////////////////////////////////////////////////////////////////////

    public static final int SM3_DIGEST_LENGTH = 32;

    /**
     * 生成ECC密钥对
     *
     * @return ECC密钥对
     */
    public static AsymmetricCipherKeyPair generateKeyPairParameter() {
        SecureRandom random = new SecureRandom();
        return BCECUtil.generateKeyPairParameter(DOMAIN_PARAMS, random);
    }

    /**
     * 生成ECC密钥对
     *
     * @return 密钥对
     */
    public static KeyPair generateKeyPair() throws NoSuchProviderException, NoSuchAlgorithmException,
            InvalidAlgorithmParameterException {
        SecureRandom random = new SecureRandom();
        return BCECUtil.generateKeyPair(DOMAIN_PARAMS, random);
    }

    /**
     * 只获取私钥里的d值,32字节
     *
     * @param privateKey BCECPublicKey 类型的公钥
     * @return 32字节数组
     */
    public static byte[] getRawPrivateKey(BCECPrivateKey privateKey) {
        return fixToCurveLengthBytes(privateKey.getD().toByteArray());
    }

    /**
     * 只获取公钥里的XY分量,64字节
     *
     * @param publicKey BCECPublicKey 类型的公钥
     * @return 64字节数组
     */
    public static byte[] getRawPublicKey(BCECPublicKey publicKey) {
        byte[] src65 = publicKey.getQ().getEncoded(false);
        byte[] rawXY = new byte[CURVE_LEN * 2];//SM2的话这里应该是64字节
        System.arraycopy(src65, 1, rawXY, 0, rawXY.length);
        return rawXY;
    }

    /**
     * @param pubKey  公钥
     * @param srcData 原文
     * @return 默认输出C1C3C2顺序的密文。C1为65字节第1字节为压缩标识,这里固定为0x04,后面64字节为xy分量各32字节。C3为32字节。C2长度与原文一致。
     */
    public static byte[] encrypt(BCECPublicKey pubKey, byte[] srcData) throws InvalidCipherTextException {
        ECPublicKeyParameters pubKeyParameters = BCECUtil.convertPublicKeyToParameters(pubKey);
        return encrypt(Mode.C1C3C2, pubKeyParameters, srcData);
    }

    /**
     * @param mode    指定密文结构,旧标准的为C1C2C3,新的[《SM2密码算法使用规范》 GM/T 0009-2012]标准为C1C3C2
     * @param pubKey  公钥
     * @param srcData 原文
     * @return 根据mode不同,输出的密文C1C2C3排列顺序不同。C1为65字节第1字节为压缩标识,这里固定为0x04,后面64字节为xy分量各32字节。C3为32字节。C2长度与原文一致。
     */
    public static byte[] encrypt(Mode mode, BCECPublicKey pubKey, byte[] srcData) throws InvalidCipherTextException {
        ECPublicKeyParameters pubKeyParameters = BCECUtil.convertPublicKeyToParameters(pubKey);
        return encrypt(mode, pubKeyParameters, srcData);
    }

    /**
     * @param pubKeyParameters 公钥
     * @param srcData          原文
     * @return 默认输出C1C3C2顺序的密文。C1为65字节第1字节为压缩标识,这里固定为0x04,后面64字节为xy分量各32字节。C3为32字节。C2长度与原文一致。
     */
    public static byte[] encrypt(ECPublicKeyParameters pubKeyParameters, byte[] srcData)
            throws InvalidCipherTextException {
        return encrypt(Mode.C1C3C2, pubKeyParameters, srcData);
    }

    /**
     * @param mode             指定密文结构,旧标准的为C1C2C3,新的[《SM2密码算法使用规范》 GM/T 0009-2012]标准为C1C3C2
     * @param pubKeyParameters 公钥
     * @param srcData          原文
     * @return 根据mode不同,输出的密文C1C2C3排列顺序不同。C1为65字节第1字节为压缩标识,这里固定为0x04,后面64字节为xy分量各32字节。C3为32字节。C2长度与原文一致。
     */
    public static byte[] encrypt(Mode mode, ECPublicKeyParameters pubKeyParameters, byte[] srcData)
            throws InvalidCipherTextException {
        SM2Engine engine = new SM2Engine(mode);
        ParametersWithRandom pwr = new ParametersWithRandom(pubKeyParameters, new SecureRandom());
        engine.init(true, pwr);
        return engine.processBlock(srcData, 0, srcData.length);
    }

    /**
     * @param priKey    私钥
     * @param sm2Cipher 默认输入C1C3C2顺序的密文。C1为65字节第1字节为压缩标识,这里固定为0x04,后面64字节为xy分量各32字节。C3为32字节。C2长度与原文一致。
     * @return 原文。SM2解密返回了数据则一定是原文,因为SM2自带校验,如果密文被篡改或者密钥对不上,都是会直接报异常的。
     */
    public static byte[] decrypt(BCECPrivateKey priKey, byte[] sm2Cipher) throws InvalidCipherTextException {
        ECPrivateKeyParameters priKeyParameters = BCECUtil.convertPrivateKeyToParameters(priKey);
        return decrypt(Mode.C1C3C2, priKeyParameters, sm2Cipher);
    }

    /**
     * @param mode      指定密文结构,旧标准的为C1C2C3,新的[《SM2密码算法使用规范》 GM/T 0009-2012]标准为C1C3C2
     * @param priKey    私钥
     * @param sm2Cipher 根据mode不同,需要输入的密文C1C2C3排列顺序不同。C1为65字节第1字节为压缩标识,这里固定为0x04,后面64字节为xy分量各32字节。C3为32字节。C2长度与原文一致。
     * @return 原文。SM2解密返回了数据则一定是原文,因为SM2自带校验,如果密文被篡改或者密钥对不上,都是会直接报异常的。
     */
    public static byte[] decrypt(Mode mode, BCECPrivateKey priKey, byte[] sm2Cipher) throws InvalidCipherTextException {
        ECPrivateKeyParameters priKeyParameters = BCECUtil.convertPrivateKeyToParameters(priKey);
        return decrypt(mode, priKeyParameters, sm2Cipher);
    }

    /**
     * @param priKeyParameters 私钥
     * @param sm2Cipher        默认输入C1C3C2顺序的密文。C1为65字节第1字节为压缩标识,这里固定为0x04,后面64字节为xy分量各32字节。C3为32字节。C2长度与原文一致。
     * @return 原文。SM2解密返回了数据则一定是原文,因为SM2自带校验,如果密文被篡改或者密钥对不上,都是会直接报异常的。
     */
    public static byte[] decrypt(ECPrivateKeyParameters priKeyParameters, byte[] sm2Cipher)
            throws InvalidCipherTextException {
        return decrypt(Mode.C1C3C2, priKeyParameters, sm2Cipher);
    }

    /**
     * @param mode             指定密文结构,旧标准的为C1C2C3,新的[《SM2密码算法使用规范》 GM/T 0009-2012]标准为C1C3C2
     * @param priKeyParameters 私钥
     * @param sm2Cipher        根据mode不同,需要输入的密文C1C2C3排列顺序不同。C1为65字节第1字节为压缩标识,这里固定为0x04,后面64字节为xy分量各32字节。C3为32字节。C2长度与原文一致。
     * @return 原文。SM2解密返回了数据则一定是原文,因为SM2自带校验,如果密文被篡改或者密钥对不上,都是会直接报异常的。
     */
    public static byte[] decrypt(Mode mode, ECPrivateKeyParameters priKeyParameters, byte[] sm2Cipher)
            throws InvalidCipherTextException {
        SM2Engine engine = new SM2Engine(mode);
        engine.init(false, priKeyParameters);
        return engine.processBlock(sm2Cipher, 0, sm2Cipher.length);
    }

    /**
     * 分解SM2密文
     *
     * @param cipherText 默认输入C1C3C2顺序的密文。C1为65字节第1字节为压缩标识,这里固定为0x04,后面64字节为xy分量各32字节。C3为32字节。C2长度与原文一致。
     */
    public static SM2Cipher parseSM2Cipher(byte[] cipherText) throws Exception {
        int curveLength = BCECUtil.getCurveLength(DOMAIN_PARAMS);
        return parseSM2Cipher(Mode.C1C3C2, curveLength, SM3_DIGEST_LENGTH, cipherText);
    }

    /**
     * 分解SM2密文
     *
     * @param mode       指定密文结构,旧标准的为C1C2C3,新的[《SM2密码算法使用规范》 GM/T 0009-2012]标准为C1C3C2
     * @param cipherText 根据mode不同,需要输入的密文C1C2C3排列顺序不同。C1为65字节第1字节为压缩标识,这里固定为0x04,后面64字节为xy分量各32字节。C3为32字节。C2长度与原文一致。
     */
    public static SM2Cipher parseSM2Cipher(Mode mode, byte[] cipherText) throws Exception {
        int curveLength = BCECUtil.getCurveLength(DOMAIN_PARAMS);
        return parseSM2Cipher(mode, curveLength, SM3_DIGEST_LENGTH, cipherText);
    }

    /**
     * @param curveLength  曲线长度,SM2的话就是256位。
     * @param digestLength 摘要长度,如果是SM2的话因为默认使用SM3摘要,SM3摘要长度为32字节。
     * @param cipherText   默认输入C1C3C2顺序的密文。C1为65字节第1字节为压缩标识,这里固定为0x04,后面64字节为xy分量各32字节。C3为32字节。C2长度与原文一致。
     */
    public static SM2Cipher parseSM2Cipher(
            int curveLength, int digestLength, byte[] cipherText) throws Exception {
        return parseSM2Cipher(Mode.C1C3C2, curveLength, digestLength, cipherText);
    }

    /**
     * 分解SM2密文
     *
     * @param mode         指定密文结构,旧标准的为C1C2C3,新的[《SM2密码算法使用规范》 GM/T 0009-2012]标准为C1C3C2
     * @param curveLength  曲线长度,SM2的话就是256位。
     * @param digestLength 摘要长度,如果是SM2的话因为默认使用SM3摘要,SM3摘要长度为32字节。
     * @param cipherText   根据mode不同,需要输入的密文C1C2C3排列顺序不同。C1为65字节第1字节为压缩标识,这里固定为0x04,后面64字节为xy分量各32字节。C3为32字节。C2长度与原文一致。
     */
    public static SM2Cipher parseSM2Cipher(Mode mode, int curveLength, int digestLength,
                                           byte[] cipherText) throws Exception {
        byte[] c1 = new byte[curveLength * 2 + 1];
        byte[] c2 = new byte[cipherText.length - c1.length - digestLength];
        byte[] c3 = new byte[digestLength];

        System.arraycopy(cipherText, 0, c1, 0, c1.length);
        if (mode == Mode.C1C2C3) {
            System.arraycopy(cipherText, c1.length, c2, 0, c2.length);
            System.arraycopy(cipherText, c1.length + c2.length, c3, 0, c3.length);
        } else if (mode == Mode.C1C3C2) {
            System.arraycopy(cipherText, c1.length, c3, 0, c3.length);
            System.arraycopy(cipherText, c1.length + c3.length, c2, 0, c2.length);
        } else {
            throw new Exception("Unsupported mode:" + mode);
        }

        SM2Cipher result = new SM2Cipher();
        result.setC1(c1);
        result.setC2(c2);
        result.setC3(c3);
        result.setCipherText(cipherText);
        return result;
    }

    /**
     * DER编码密文
     *
     * @param cipher 默认输入C1C3C2顺序的密文。C1为65字节第1字节为压缩标识,这里固定为0x04,后面64字节为xy分量各32字节。C3为32字节。C2长度与原文一致。
     * @return DER编码后的密文
     */
    public static byte[] encodeSM2CipherToDER(byte[] cipher) throws Exception {
        int curveLength = BCECUtil.getCurveLength(DOMAIN_PARAMS);
        return encodeSM2CipherToDER(Mode.C1C3C2, curveLength, SM3_DIGEST_LENGTH, cipher);
    }

    /**
     * DER编码密文
     *
     * @param mode   指定密文结构,旧标准的为C1C2C3,新的[《SM2密码算法使用规范》 GM/T 0009-2012]标准为C1C3C2
     * @param cipher 根据mode不同,需要输入的密文C1C2C3排列顺序不同。C1为65字节第1字节为压缩标识,这里固定为0x04,后面64字节为xy分量各32字节。C3为32字节。C2长度与原文一致。
     * @return 按指定mode DER编码后的密文
     */
    public static byte[] encodeSM2CipherToDER(Mode mode, byte[] cipher) throws Exception {
        int curveLength = BCECUtil.getCurveLength(DOMAIN_PARAMS);
        return encodeSM2CipherToDER(mode, curveLength, SM3_DIGEST_LENGTH, cipher);
    }

    /**
     * DER编码密文
     *
     * @param curveLength  曲线长度,SM2的话就是256位。
     * @param digestLength 摘要长度,如果是SM2的话因为默认使用SM3摘要,SM3摘要长度为32字节。
     * @param cipher       默认输入C1C3C2顺序的密文。C1为65字节第1字节为压缩标识,这里固定为0x04,后面64字节为xy分量各32字节。C3为32字节。C2长度与原文一致。
     * @return 默认输出按C1C3C2编码的结果
     */
    public static byte[] encodeSM2CipherToDER(int curveLength, int digestLength, byte[] cipher)
            throws Exception {
        return encodeSM2CipherToDER(Mode.C1C3C2, curveLength, digestLength, cipher);
    }

    /**
     * @param mode         指定密文结构,旧标准的为C1C2C3,新的[《SM2密码算法使用规范》 GM/T 0009-2012]标准为C1C3C2
     * @param curveLength  曲线长度,SM2的话就是256位。
     * @param digestLength 摘要长度,如果是SM2的话因为默认使用SM3摘要,SM3摘要长度为32字节。
     * @param cipher       根据mode不同,需要输入的密文C1C2C3排列顺序不同。C1为65字节第1字节为压缩标识,这里固定为0x04,后面64字节为xy分量各32字节。C3为32字节。C2长度与原文一致。
     * @return 按指定mode DER编码后的密文
     */
    public static byte[] encodeSM2CipherToDER(Mode mode, int curveLength, int digestLength, byte[] cipher)
            throws Exception {

        byte[] c1x = new byte[curveLength];
        byte[] c1y = new byte[curveLength];
        byte[] c2 = new byte[cipher.length - c1x.length - c1y.length - 1 - digestLength];
        byte[] c3 = new byte[digestLength];

        int startPos = 1;
        System.arraycopy(cipher, startPos, c1x, 0, c1x.length);
        startPos += c1x.length;
        System.arraycopy(cipher, startPos, c1y, 0, c1y.length);
        startPos += c1y.length;
        if (mode == Mode.C1C2C3) {
            System.arraycopy(cipher, startPos, c2, 0, c2.length);
            startPos += c2.length;
            System.arraycopy(cipher, startPos, c3, 0, c3.length);
        } else if (mode == Mode.C1C3C2) {
            System.arraycopy(cipher, startPos, c3, 0, c3.length);
            startPos += c3.length;
            System.arraycopy(cipher, startPos, c2, 0, c2.length);
        } else {
            throw new Exception("Unsupported mode:" + mode);
        }

        ASN1Encodable[] arr = new ASN1Encodable[4];
        // c1x,c1y的第一个bit可能为1,这个时候要确保他们表示的大数一定是正数,所以new BigInteger符号强制设为正。
        arr[0] = new ASN1Integer(new BigInteger(1, c1x));
        arr[1] = new ASN1Integer(new BigInteger(1, c1y));
        if (mode == Mode.C1C2C3) {
            arr[2] = new DEROctetString(c2);
            arr[3] = new DEROctetString(c3);
        } else {
            arr[2] = new DEROctetString(c3);
            arr[3] = new DEROctetString(c2);
        }
        DERSequence ds = new DERSequence(arr);
        return ds.getEncoded(ASN1Encoding.DER);
    }

    /**
     * 解码DER密文
     *
     * @param derCipher 默认输入按C1C3C2顺序DER编码的密文
     * @return 输出按C1C3C2排列的字节数组,C1为65字节第1字节为压缩标识,这里固定为0x04,后面64字节为xy分量各32字节。C3为32字节。C2长度与原文一致。
     */
    public static byte[] decodeDERSM2Cipher(byte[] derCipher) throws Exception {
        return decodeDERSM2Cipher(Mode.C1C3C2, derCipher);
    }

    /**
     * @param mode      指定密文结构,旧标准的为C1C2C3,新的[《SM2密码算法使用规范》 GM/T 0009-2012]标准为C1C3C2
     * @param derCipher 根据mode输入C1C2C3或C1C3C2顺序DER编码后的密文
     * @return 根据mode不同,输出的密文C1C2C3排列顺序不同。C1为65字节第1字节为压缩标识,这里固定为0x04,后面64字节为xy分量各32字节。C3为32字节。C2长度与原文一致。
     */
    public static byte[] decodeDERSM2Cipher(Mode mode, byte[] derCipher) throws Exception {
        ASN1Sequence as = DERSequence.getInstance(derCipher);
        byte[] c1x = ((ASN1Integer) as.getObjectAt(0)).getValue().toByteArray();
        byte[] c1y = ((ASN1Integer) as.getObjectAt(1)).getValue().toByteArray();
        // c1x,c1y可能因为大正数的补0规则在第一个有效字节前面插了一个(byte)0,变成33个字节,在这里要修正回32个字节去
        c1x = fixToCurveLengthBytes(c1x);
        c1y = fixToCurveLengthBytes(c1y);
        byte[] c3;
        byte[] c2;
        if (mode == Mode.C1C2C3) {
            c2 = ((DEROctetString) as.getObjectAt(2)).getOctets();
            c3 = ((DEROctetString) as.getObjectAt(3)).getOctets();
        } else if (mode == Mode.C1C3C2) {
            c3 = ((DEROctetString) as.getObjectAt(2)).getOctets();
            c2 = ((DEROctetString) as.getObjectAt(3)).getOctets();
        } else {
            throw new Exception("Unsupported mode:" + mode);
        }

        int pos = 0;
        byte[] cipherText = new byte[1 + c1x.length + c1y.length + c2.length + c3.length];
        final byte uncompressedFlag = 0x04;
        cipherText[0] = uncompressedFlag;
        pos += 1;
        System.arraycopy(c1x, 0, cipherText, pos, c1x.length);
        pos += c1x.length;
        System.arraycopy(c1y, 0, cipherText, pos, c1y.length);
        pos += c1y.length;
        if (mode == Mode.C1C2C3) {
            System.arraycopy(c2, 0, cipherText, pos, c2.length);
            pos += c2.length;
            System.arraycopy(c3, 0, cipherText, pos, c3.length);
        } else {
            System.arraycopy(c3, 0, cipherText, pos, c3.length);
            pos += c3.length;
            System.arraycopy(c2, 0, cipherText, pos, c2.length);
        }
        return cipherText;
    }

    /**
     * 签名
     *
     * @param priKey  私钥
     * @param srcData 原文
     * @return DER编码后的签名值
     */
    public static byte[] sign(BCECPrivateKey priKey, byte[] srcData) throws CryptoException {
        ECPrivateKeyParameters priKeyParameters = BCECUtil.convertPrivateKeyToParameters(priKey);
        return sign(priKeyParameters, null, srcData);
    }

    /**
     * 签名
     * 不指定withId,则默认withId为字节数组:"1234567812345678".getBytes()
     *
     * @param priKeyParameters 私钥
     * @param srcData          原文
     * @return DER编码后的签名值
     */
    public static byte[] sign(ECPrivateKeyParameters priKeyParameters, byte[] srcData) throws CryptoException {
        return sign(priKeyParameters, null, srcData);
    }

    /**
     * 私钥签名
     *
     * @param priKey  私钥
     * @param withId  可以为null,若为null,则默认withId为字节数组:"1234567812345678".getBytes()
     * @param srcData 原文
     * @return DER编码后的签名值
     */
    public static byte[] sign(BCECPrivateKey priKey, byte[] withId, byte[] srcData) throws CryptoException {
        ECPrivateKeyParameters priKeyParameters = BCECUtil.convertPrivateKeyToParameters(priKey);
        return sign(priKeyParameters, withId, srcData);
    }

    /**
     * 签名
     *
     * @param priKeyParameters 私钥
     * @param withId           可以为null,若为null,则默认withId为字节数组:"1234567812345678".getBytes()
     * @param srcData          源数据
     * @return DER编码后的签名值
     */
    public static byte[] sign(ECPrivateKeyParameters priKeyParameters, byte[] withId, byte[] srcData)
            throws CryptoException {
        SM2Signer signer = new SM2Signer();
        CipherParameters param;
        ParametersWithRandom pwr = new ParametersWithRandom(priKeyParameters, new SecureRandom());
        if (withId != null) {
            param = new ParametersWithID(pwr, withId);
        } else {
            param = pwr;
        }
        signer.init(true, param);
        signer.update(srcData, 0, srcData.length);
        return signer.generateSignature();
    }

    /**
     * 将DER编码的SM2签名解码成64字节的纯R+S字节流
     *
     * @return 64字节数组,前32字节为R,后32字节为S
     */
    public static byte[] decodeDERSM2Sign(byte[] derSign) {
        ASN1Sequence as = DERSequence.getInstance(derSign);
        byte[] rBytes = ((ASN1Integer) as.getObjectAt(0)).getValue().toByteArray();
        byte[] sBytes = ((ASN1Integer) as.getObjectAt(1)).getValue().toByteArray();
        //由于大数的补0规则,所以可能会出现33个字节的情况,要修正回32个字节
        rBytes = fixToCurveLengthBytes(rBytes);
        sBytes = fixToCurveLengthBytes(sBytes);
        byte[] rawSign = new byte[rBytes.length + sBytes.length];
        System.arraycopy(rBytes, 0, rawSign, 0, rBytes.length);
        System.arraycopy(sBytes, 0, rawSign, rBytes.length, sBytes.length);
        return rawSign;
    }

    /**
     * 把64字节的纯R+S字节数组编码成DER编码
     *
     * @param rawSign 64字节数组形式的SM2签名值,前32字节为R,后32字节为S
     * @return DER编码后的SM2签名值
     */
    public static byte[] encodeSM2SignToDER(byte[] rawSign) throws IOException {
        //要保证大数是正数
        BigInteger r = new BigInteger(1, extractBytes(rawSign, 0));
        BigInteger s = new BigInteger(1, extractBytes(rawSign, 32));
        ASN1EncodableVector v = new ASN1EncodableVector();
        v.add(new ASN1Integer(r));
        v.add(new ASN1Integer(s));
        return new DERSequence(v).getEncoded(ASN1Encoding.DER);
    }

    /**
     * 验签
     *
     * @param pubKey  公钥
     * @param srcData 原文
     * @param sign    DER编码的签名值
     */
    public static boolean verify(BCECPublicKey pubKey, byte[] srcData, byte[] sign) {
        ECPublicKeyParameters pubKeyParameters = BCECUtil.convertPublicKeyToParameters(pubKey);
        return verify(pubKeyParameters, null, srcData, sign);
    }

    /**
     * 验签
     * 不指定withId,则默认withId为字节数组:"1234567812345678".getBytes()
     *
     * @param pubKeyParameters 公钥
     * @param srcData          原文
     * @param sign             DER编码的签名值
     * @return 验签成功返回true,失败返回false
     */
    public static boolean verify(ECPublicKeyParameters pubKeyParameters, byte[] srcData, byte[] sign) {
        return verify(pubKeyParameters, null, srcData, sign);
    }

    /**
     * 验签
     *
     * @param pubKey  公钥
     * @param withId  可以为null,若为null,则默认withId为字节数组:"1234567812345678".getBytes()
     * @param srcData 原文
     * @param sign    DER编码的签名值
     * @return 验签成功返回true,失败返回false
     */
    public static boolean verify(BCECPublicKey pubKey, byte[] withId, byte[] srcData, byte[] sign) {
        ECPublicKeyParameters pubKeyParameters = BCECUtil.convertPublicKeyToParameters(pubKey);
        return verify(pubKeyParameters, withId, srcData, sign);
    }

    /**
     * 验签
     *
     * @param pubKeyParameters 公钥
     * @param withId           可以为null,若为null,则默认withId为字节数组:"1234567812345678".getBytes()
     * @param srcData          原文
     * @param sign             DER编码的签名值
     * @return 验签成功返回true,失败返回false
     */
    public static boolean verify(ECPublicKeyParameters pubKeyParameters, byte[] withId, byte[] srcData, byte[] sign) {
        SM2Signer signer = new SM2Signer();
        CipherParameters param;
        if (withId != null) {
            param = new ParametersWithID(pubKeyParameters, withId);
        } else {
            param = pubKeyParameters;
        }
        signer.init(false, param);
        signer.update(srcData, 0, srcData.length);
        return signer.verifySignature(sign);
    }

    /**
     * 把Base 64 编码的 SM2 私钥转换成 BCECPrivateKey 格式
     * @param sm2PrivateKeyBase64 Base 64 编码的 SM2 私钥
     * @return BCECPrivateKey
     */
    public static BCECPrivateKey getPrivteKeyFromBase64(String sm2PrivateKeyBase64) {
        if( Security.getProvider("BC") == null ) {
            Security.addProvider(new BouncyCastleProvider());
        }

        try {
            byte[] privateKeyPkcs1Der = Base64.getDecoder().decode(sm2PrivateKeyBase64);

            return BCECUtil.convertSEC1ToBCECPrivateKey(privateKeyPkcs1Der);
        } catch (Exception ex) {
            return null;
        }
    }

    /**
     * 把Base 64 编码的 SM2 公钥转换成 BCECPublicKey 格式
     * @param sm2PublicKeyBase64 Base 64 编码的 SM2 公钥
     * @return BCECPublicKey
     */
    public static BCECPublicKey getPublicKeyFromBase64(String sm2PublicKeyBase64) {
        if( Security.getProvider("BC") == null ) {
            Security.addProvider(new BouncyCastleProvider());
        }

        try {
            byte[] publicKeyX509Der = Base64.getDecoder().decode(sm2PublicKeyBase64);

            return BCECUtil.convertX509ToECPublicKey(publicKeyX509Der);
        } catch (Exception ex) {
            return null;
        }
    }

    private static byte[] extractBytes(byte[] src, int offset) {
        byte[] result = new byte[SM3_DIGEST_LENGTH];
        System.arraycopy(src, offset, result, 0, result.length);
        return result;
    }

    private static byte[] fixToCurveLengthBytes(byte[] src) {
        if (src.length == CURVE_LEN) {
            return src;
        }

        byte[] result = new byte[CURVE_LEN];
        if (src.length > CURVE_LEN) {
            System.arraycopy(src, src.length - result.length, result, 0, result.length);
        } else {
            System.arraycopy(src, 0, result, result.length - src.length, src.length);
        }
        return result;
    }
}

版权说明:本文由北京朗思云网科技股份有限公司原创,向互联网开放全部内容但保留所有权力。