Java—bouncycastle支持国密SM2的公钥加密算法

java代码是依赖 BouncyCastle 类库,经修改此类库中的 SM2Engine 类的原码而来,用于支持 SM2 公钥加密算法,符合《GB/T 35276-2017: 信息安全技术 SM2密码算法使用规范》。

SM4 国标《GB/T 32907-2016 信息安全技术 SM4分组密码算法》。

可以使用 gmssl 工具进行交互测试(http://gmssl.org)

引入jar:

<dependency>
  <groupId>org.bouncycastle</groupId>
  <artifactId>bcprov-jdk15on</artifactId>
  <version>1.68</version>
</dependency>

加解密工具类:

import java.io.IOException;
import java.math.BigInteger;
import java.security.SecureRandom;

import org.bouncycastle.asn1.ASN1EncodableVector;
import org.bouncycastle.asn1.ASN1InputStream;
import org.bouncycastle.asn1.ASN1Integer;
import org.bouncycastle.asn1.ASN1OctetString;
import org.bouncycastle.asn1.ASN1Sequence;
import org.bouncycastle.asn1.DEROctetString;
import org.bouncycastle.asn1.DERSequence;
import org.bouncycastle.crypto.CipherParameters;
import org.bouncycastle.crypto.Digest;
import org.bouncycastle.crypto.InvalidCipherTextException;
import org.bouncycastle.crypto.digests.SM3Digest;
import org.bouncycastle.crypto.params.ECDomainParameters;
import org.bouncycastle.crypto.params.ECKeyParameters;
import org.bouncycastle.crypto.params.ECPrivateKeyParameters;
import org.bouncycastle.crypto.params.ECPublicKeyParameters;
import org.bouncycastle.crypto.params.ParametersWithRandom;
import org.bouncycastle.math.ec.ECFieldElement;
import org.bouncycastle.math.ec.ECMultiplier;
import org.bouncycastle.math.ec.ECPoint;
import org.bouncycastle.math.ec.FixedPointCombMultiplier;
import org.bouncycastle.util.Arrays;
import org.bouncycastle.util.BigIntegers;
import org.bouncycastle.util.Memoable;
import org.bouncycastle.util.Pack;


/**
 * 自义的 SM2 公钥加密、私钥解密引擎, 用于替换  BouncyCastle 中的 SM2Engine 的实现,
 * 可用于非 java 开发的系统之间交换数据时的公钥加密、私钥解密,完全符合 GM/T 以下两个标准:
 * <br/>
 * <li>《GM/T 0003.4-2012: SM2椭圆曲线公钥密钥算法:第4部分:公钥加密算法》</li>
 * <li>《GM/T 0009-2012: SM2密码算法使用规范》</li>
 * <br/>
 * <p/>
 * <li>1.加密密文默认为 C1||C3||C2, 输出内容为 ASN.1 编码。符合 《GM/T 0009-2012: SM2密码算法使用规范》 标准
 * </li>
 * <li>2.加密密文设置为 C1||C2||C3,则输出内容不是 ASN.1 编码。与 《GM/T 0009-2012 ...》 标准不兼容。</li>
 * <br/>
 * 建议可以使用 GmSSL 工具进行交互测试,请参考 {@code http://gmssl.org}
 * <p>
 * SM2 public key encryption engine - based on https://tools.ietf.org/html/draft-shen-sm2-ecdsa-02.
 *
 *
 * @author YangHongFeng
 * @since 2020/5/28 creation
 */
public class MySm2Engine {

    public enum Mode {
        C1C2C3, C1C3C2;
    }

    private final Digest digest;
    private final Mode mode;

    private boolean forEncryption;
    private ECKeyParameters ecKey;
    private ECDomainParameters ecParams;
    private int curveLength;
    private SecureRandom random;

    /***
     * 默认采用国标:C1||C3||C2
     */
    public MySm2Engine() {
        this(new SM3Digest(), Mode.C1C3C2);
    }

    public MySm2Engine(Mode mode) {
        this(new SM3Digest(), mode);
    }

    public MySm2Engine(Digest digest) {
        this(digest, Mode.C1C2C3);
    }

    public MySm2Engine(Digest digest, Mode mode) {
        if (mode == null) {
            throw new IllegalArgumentException("mode cannot be NULL");
        }
        this.digest = digest;
        this.mode = mode;
    }

    /**
     * 初始化
     * @param forEncryption true-公钥加密, false-私钥解密
     * @param param 密码参数,从中获取公或私钥、及椭圆曲线相关参数
     */
    public void init(boolean forEncryption, CipherParameters param) {
        this.forEncryption = forEncryption;

        if (forEncryption) {
            ParametersWithRandom rParam = (ParametersWithRandom) param;

            ecKey = (ECKeyParameters) rParam.getParameters();
            ecParams = ecKey.getParameters();

            ECPoint s = ((ECPublicKeyParameters) ecKey).getQ().multiply(ecParams.getH());
            if (s.isInfinity()) {
                throw new IllegalArgumentException("invalid key: [h]Q at infinity");
            }

            random = rParam.getRandom();
        } else {
            ecKey = (ECKeyParameters) param;
            ecParams = ecKey.getParameters();
        }

        curveLength = (ecParams.getCurve().getFieldSize() + 7) / 8;
    }

    /**
     * 进行加密、或解密
     * @param in
     * @param inOff
     * @param inLen
     * @return
     * @throws InvalidCipherTextException
     */
    public byte[] processBlock(
            byte[] in,
            int inOff,
            int inLen)
            throws IOException, InvalidCipherTextException {
        if (forEncryption) {
            return encrypt(in, inOff, inLen);
        } else {
            return decrypt(in, inOff, inLen);
        }
    }

    public int getOutputSize(int inputLen) {
        return (1 + 2 * curveLength) + inputLen + digest.getDigestSize();
    }

    protected ECMultiplier createBasePointMultiplier() {
        return new FixedPointCombMultiplier();
    }

    private byte[] encrypt(byte[] in, int inOff, int inLen)
            throws IOException {
        byte[] c2 = new byte[inLen];

        System.arraycopy(in, inOff, c2, 0, c2.length);

        ECMultiplier multiplier = createBasePointMultiplier();

        ECPoint c1P;
        ECPoint kPB;
        do {
            BigInteger k = nextK();

            c1P = multiplier.multiply(ecParams.getG(), k).normalize();
            // c1 = c1P.getEncoded(false);

            kPB = ((ECPublicKeyParameters) ecKey).getQ().multiply(k).normalize();

            kdf(digest, kPB, c2);
        }

        while (notEncrypted(c2, in, inOff));

        byte[] c3 = new byte[digest.getDigestSize()];

        addFieldElement(digest, kPB.getAffineXCoord());
        digest.update(in, inOff, inLen);
        addFieldElement(digest, kPB.getAffineYCoord());

        digest.doFinal(c3, 0);

        switch (mode) {
        case C1C3C2:
            // 2020/06/01 按国标组装为 ANS.1 编码
            final ASN1EncodableVector vector = new ASN1EncodableVector();
            vector.add(new ASN1Integer(c1P.getXCoord().toBigInteger()));
            vector.add(new ASN1Integer(c1P.getYCoord().toBigInteger()));
            vector.add(new DEROctetString(c3));
            vector.add(new DEROctetString(c2));
            return new DERSequence(vector).getEncoded();
        default:
            byte[] c1 = c1P.getEncoded(false);
            return Arrays.concatenate(c1, c2, c3);
        }
    }

    private byte[] decrypt(byte[] in, int inOff, int inLen)
            throws InvalidCipherTextException, IOException {

        ECPoint c1P; // = ecParams.getCurve().decodePoint(c1);
        byte[] inHash ;
        byte[] inCipherData;

        if (mode == Mode.C1C3C2) {
            // 2020/06/01 按国标 ANS.1 编码 进行解码
            ASN1InputStream inputStream = new ASN1InputStream(in);
            ASN1Sequence seq = (ASN1Sequence) inputStream.readObject();
            if (seq.size() != 4){
                throw new InvalidCipherTextException("invalid cipher text");
            }
            int index = 0;

            // C1 == XCoordinate 、YCoordinate
            BigInteger x = ((ASN1Integer) seq.getObjectAt(index ++)).getPositiveValue();
            // YCoordinate
            BigInteger y = ((ASN1Integer) seq.getObjectAt(index ++)).getPositiveValue();

            // XCoord 、YCoord ==> CEPoint (C1)
            c1P = ecParams.getCurve().createPoint(x, y);

            // HASH (C3)
            inHash = ((ASN1OctetString)seq.getObjectAt(index ++)).getOctets();

            // CipherText (C2)
            inCipherData = ((ASN1OctetString)seq.getObjectAt(index)).getOctets();
        } else {
            // C1
            byte[] c1 = new byte[curveLength * 2 + 1];
            System.arraycopy(in, inOff, c1, 0, c1.length);
            c1P = ecParams.getCurve().decodePoint(c1);

            // C2 == inCipherData
            int digestSize = this.digest.getDigestSize();
            inCipherData = new byte[inLen - c1.length - digestSize];
            System.arraycopy(in, inOff + c1.length, inCipherData, 0, inCipherData.length);

            // C3 == Hash
            inHash = new byte[digestSize];
            System.arraycopy(in, inOff + c1.length + inCipherData.length, inHash, 0, inHash.length);
        }

        // 解密 ==> inCipherData;
        ECPoint s = c1P.multiply(ecParams.getH());
        if (s.isInfinity())
        {
            throw new InvalidCipherTextException("[h]C1 at infinity");
        }
        c1P = c1P.multiply(((ECPrivateKeyParameters)ecKey).getD()).normalize();
        kdf(digest, c1P, inCipherData);

        // 动态计算已解密的明文的摘要并比较
        byte[] cipherDigest = new byte[digest.getDigestSize()];

        addFieldElement(digest, c1P.getAffineXCoord());
        digest.update(inCipherData, 0, inCipherData.length);
        addFieldElement(digest, c1P.getAffineYCoord());

        digest.doFinal(cipherDigest, 0);

        int check = 0;
        if (mode == Mode.C1C3C2)
        {
            for (int i = 0; i != cipherDigest.length; i++) {
                check |= cipherDigest[i] ^ inHash[i];
            }
        } else {
            for (int i = 0; i != cipherDigest.length; i++) {
                check |= cipherDigest[i] ^ inHash[i];
            }
        }

        // Arrays.fill(c1, (byte)0);
        Arrays.fill(cipherDigest, (byte)0);

        if (check != 0)
        {
            Arrays.fill(inCipherData, (byte)0);
            throw new InvalidCipherTextException("invalid cipher text");
        }

        // return c2;
        return inCipherData;
    }

    private boolean notEncrypted(byte[] encData, byte[] in, int inOff) {
        for (int i = 0; i != encData.length; i++) {
            if (encData[i] != in[inOff + i]) {
                return false;
            }
        }

        return true;
    }

    private void kdf(Digest digest, ECPoint c1, byte[] encData) {
        int digestSize = digest.getDigestSize();
        byte[] buf = new byte[Math.max(4, digestSize)];
        int off = 0;

        Memoable memo = null;
        Memoable copy = null;

        if (digest instanceof Memoable) {
            addFieldElement(digest, c1.getAffineXCoord());
            addFieldElement(digest, c1.getAffineYCoord());
            memo = (Memoable) digest;
            copy = memo.copy();
        }

        int ct = 0;

        while (off < encData.length) {
            if (memo != null) {
                memo.reset(copy);
            } else {
                addFieldElement(digest, c1.getAffineXCoord());
                addFieldElement(digest, c1.getAffineYCoord());
            }

            Pack.intToBigEndian(++ct, buf, 0);
            digest.update(buf, 0, 4);
            digest.doFinal(buf, 0);

            int xorLen = Math.min(digestSize, encData.length - off);
            xor(encData, buf, off, xorLen);
            off += xorLen;
        }
    }

    private void xor(byte[] data, byte[] kdfOut, int dOff, int dRemaining) {
        for (int i = 0; i != dRemaining; i++) {
            data[dOff + i] ^= kdfOut[i];
        }
    }

    private BigInteger nextK() {
        int qBitLength = ecParams.getN().bitLength();

        BigInteger k;
        do {
            k = BigIntegers.createRandomBigInteger(qBitLength, random);
        }
        while (k.equals(BigIntegers.ZERO) || k.compareTo(ecParams.getN()) >= 0);

        return k;
    }

    private void addFieldElement(Digest digest, ECFieldElement v) {
        byte[] p = BigIntegers.asUnsignedByteArray(curveLength, v.toBigInteger());

        digest.update(p, 0, p.length);
    }
}

工具类:

import org.bouncycastle.asn1.gm.GMNamedCurves;
import org.bouncycastle.asn1.x9.X9ECParameters;
import org.bouncycastle.crypto.InvalidCipherTextException;
import org.bouncycastle.crypto.params.ECDomainParameters;
import org.bouncycastle.crypto.params.ECPrivateKeyParameters;
import org.bouncycastle.crypto.params.ECPublicKeyParameters;
import org.bouncycastle.crypto.params.ParametersWithRandom;
import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPrivateKey;
import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.jce.spec.ECParameterSpec;
import org.bouncycastle.jce.spec.ECPrivateKeySpec;
import org.bouncycastle.jce.spec.ECPublicKeySpec;
import org.bouncycastle.util.encoders.Hex;

import java.io.IOException;
import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.SecureRandom;


public class SM2Util {
    //椭圆曲线ECParameters ASN.1 结构
    private static final X9ECParameters x9ECParameters = GMNamedCurves.getByName("sm2p256v1");
    //椭圆曲线公钥或私钥的基本域参数。
    private static final ECParameterSpec ecDomainParameters = new ECParameterSpec(x9ECParameters.getCurve(), x9ECParameters.getG(), x9ECParameters.getN());

    /**
     * 公钥字符串转换为 BCECPublicKey 公钥对象
     *
     * @param pubKeyHex 64字节十六进制公钥字符串(如果公钥字符串为65字节首个字节为0x04:表示该公钥为非压缩格式,操作时需要删除)
     * @return BCECPublicKey SM2公钥对象
     */
    public static BCECPublicKey getECPublicKeyByPublicKeyHex(String pubKeyHex) {
        //截取64字节有效的SM2公钥(如果公钥首个字节为0x04)
        if (pubKeyHex.length() > 128) {
            pubKeyHex = pubKeyHex.substring(pubKeyHex.length() - 128);
        }
        //将公钥拆分为x,y分量(各32字节)
        String stringX = pubKeyHex.substring(0, 64);
        String stringY = pubKeyHex.substring(stringX.length());
        //将公钥x、y分量转换为BigInteger类型
        BigInteger x = new BigInteger(stringX, 16);
        BigInteger y = new BigInteger(stringY, 16);
        //通过公钥x、y分量创建椭圆曲线公钥规范
        ECPublicKeySpec ecPublicKeySpec = new ECPublicKeySpec(x9ECParameters.getCurve().createPoint(x, y), ecDomainParameters);
        //通过椭圆曲线公钥规范,创建出椭圆曲线公钥对象(可用于SM2加密及验签)
        return new BCECPublicKey("EC", ecPublicKeySpec, BouncyCastleProvider.CONFIGURATION);
    }


    /**
     * 私钥字符串转换为 BCECPrivateKey 私钥对象
     *
     * @param privateKeyHex: 32字节十六进制私钥字符串
     * @return BCECPrivateKey:SM2私钥对象
     */
    public static BCECPrivateKey getBCECPrivateKeyByPrivateKeyHex(String privateKeyHex) {
        //将十六进制私钥字符串转换为BigInteger对象
        BigInteger d = new BigInteger(privateKeyHex, 16);
        //通过私钥和私钥域参数集创建椭圆曲线私钥规范
        ECPrivateKeySpec ecPrivateKeySpec = new ECPrivateKeySpec(d, ecDomainParameters);
        //通过椭圆曲线私钥规范,创建出椭圆曲线私钥对象(可用于SM2解密和签名)
        return new BCECPrivateKey("EC", ecPrivateKeySpec, BouncyCastleProvider.CONFIGURATION);
    }

    /**
     * SM2加密算法
     *
     * @param parameters 公钥参数
     * @param data       明文数据
     */
    public static String encrypt(ECPublicKeyParameters parameters, String data) throws InvalidCipherTextException, IOException {
        MySm2Engine sm2Engine = new MySm2Engine();
        sm2Engine.init(true, new ParametersWithRandom(parameters, new SecureRandom()));

        byte[] in = data.getBytes(StandardCharsets.UTF_8);
        return Hex.toHexString(sm2Engine.processBlock(in, 0, in.length));
    }

    /**
     * SM2加密算法
     *
     * @param publicKey 公钥
     * @param data      明文数据
     */
    public static String encrypt(PublicKey publicKey, String data) throws InvalidCipherTextException, IOException {
        ECPublicKeyParameters ecPublicKeyParameters = null;
        if (publicKey instanceof BCECPublicKey) {
            BCECPublicKey bcecPublicKey = (BCECPublicKey) publicKey;
            ECParameterSpec ecParameterSpec = bcecPublicKey.getParameters();
            ECDomainParameters ecDomainParameters = new ECDomainParameters(ecParameterSpec.getCurve(),
                    ecParameterSpec.getG(), ecParameterSpec.getN());
            ecPublicKeyParameters = new ECPublicKeyParameters(bcecPublicKey.getQ(), ecDomainParameters);
        }

        return encrypt(ecPublicKeyParameters, data);
    }

    /**
     * SM2解密算法
     *
     * @param privateKey 私钥
     * @param cipherData 密文数据
     */
    public static String decrypt(PrivateKey privateKey, String cipherData) throws InvalidCipherTextException, IOException {
        byte[] cipherDataByte = Hex.decode(cipherData);

        BCECPrivateKey bcecPrivateKey = (BCECPrivateKey) privateKey;
        ECParameterSpec ecParameterSpec = bcecPrivateKey.getParameters();

        ECDomainParameters ecDomainParameters = new ECDomainParameters(ecParameterSpec.getCurve(),
                ecParameterSpec.getG(), ecParameterSpec.getN());

        ECPrivateKeyParameters ecPrivateKeyParameters = new ECPrivateKeyParameters(bcecPrivateKey.getD(),
                ecDomainParameters);

        MySm2Engine sm2Engine = new MySm2Engine();
        sm2Engine.init(false, ecPrivateKeyParameters);

        byte[] arrayOfBytes = sm2Engine.processBlock(cipherDataByte, 0, cipherDataByte.length);
        return new String(arrayOfBytes, StandardCharsets.UTF_8);
    }

}

测试例子:

import org.bouncycastle.crypto.InvalidCipherTextException;
import org.bouncycastle.jce.provider.BouncyCastleProvider;

import java.io.IOException;
import java.security.InvalidAlgorithmParameterException;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.spec.ECGenParameterSpec;

public class sm2_demo {

    public static void main(String[] args) throws NoSuchAlgorithmException, InvalidAlgorithmParameterException, InvalidCipherTextException, IOException {

        final ECGenParameterSpec sm2Spec = new ECGenParameterSpec("sm2p256v1");
        // 获取一个椭圆曲线类型的密钥对生成器
        final KeyPairGenerator kpg = KeyPairGenerator.getInstance("EC", new BouncyCastleProvider());
        // 使用SM2参数初始化生成器
        kpg.initialize(sm2Spec);
        // 获取密钥对
        KeyPair keyPair = kpg.generateKeyPair();
        PublicKey publicKey = keyPair.getPublic();
        PrivateKey privateKey = keyPair.getPrivate();
        String m = "lzc_SM2_demo";
        String data = SM2Util.encrypt(publicKey, m);
        System.out.println(data);
        String text = SM2Util.decrypt(privateKey, data);
        System.out.println(text);
    }
}

参考链接


发布者

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注