Java代码实现SM2算法以及注意点总结(踩坑记录)

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档


前言

最近公司的一些项目的安全认证算法在逐渐转为使用国密实现,所以最近学习了一些比如SM2、SM4算法的相关知识,以及代码实现。
因为工作之后忘了很多,而且本身对数学也不是很好(比较菜)所以做的时候找了很多资料,过程中也是踩了很多坑,所以整理出来,希望对一些初学者有所帮助,也作为自己的一个学习资料记录下来。


一、SM2是什么?

SM2是国家密码管理局于2010年12月17日发布的椭圆曲线公钥密码算法。形式类似于RSA这种非对称加密,有公钥和私钥两组密钥。
SM2算法和RSA算法都是公钥密码算法,SM2算法与RSA算法不同的是,SM2算法是基于椭圆曲线上点群离散对数难题,相对于RSA算法,256位的SM2密码强度已经比2048位的RSA密码强度要高。
国密 SM2 算法是一种 ECC 算法。

二、Java实现

最开始的时候在网上找了相当多的实现代码,但是因为JAVA的代码实际对加密上的封装是比较好的,如果只是简单使用也比较简单,所以第一次写的时候并没有发现什么太大的问题。但是最后与公司另一个部门写C的同事做了互通测试,结果加密结果是不通用的,对了之后发现很多不了解的点。

1.实现

先说实现,先看看代码,之后再解释里边的一些参数,如果只是想跑起来,那么看完代码就可以了,但是我还是建议大家往下看完,不然真的会有很多坑。

1.1 引入依赖

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

1.2 生成密钥

    private final static String CRYPTO_NAME_SM2 = "sm2p256v1";

    /**
     * 生成国密公私钥对
     *
     * @return Sm2国密对
     * @throws Exception
     */
    public static Sm2KeyEntity generateSmKey() throws Exception {
        KeyPairGenerator keyPairGenerator = null;
        SecureRandom secureRandom = new SecureRandom();
        ECGenParameterSpec sm2Spec = new ECGenParameterSpec(CRYPTO_NAME_SM2);
        keyPairGenerator = KeyPairGenerator.getInstance("EC", new BouncyCastleProvider());
        keyPairGenerator.initialize(sm2Spec);
        keyPairGenerator.initialize(sm2Spec, secureRandom);
        KeyPair keyPair = keyPairGenerator.generateKeyPair();
        PrivateKey privateKey = keyPair.getPrivate();
        PublicKey publicKey = keyPair.getPublic();
        return new Sm2KeyEntity(privateKey, publicKey);
    }

1.3 将各种字符串保存的公钥转为公钥对象


    /**
     * 将Base64转码的公钥串,转化为公钥对象(全量密钥)
     *
     * @param base64PublicKey 公钥对象base64字符串
     * @return 公钥对象 {@link BCECPublicKey}
     */
    public static PublicKey createPublicKeyByFullBase64Str(String base64PublicKey) throws NoSuchAlgorithmException, InvalidKeySpecException {

        X509EncodedKeySpec publicKeySpec = new X509EncodedKeySpec(Base64.getDecoder().decode(base64PublicKey));
        KeyFactory keyFactory = KeyFactory.getInstance("EC", new BouncyCastleProvider());

        return keyFactory.generatePublic(publicKeySpec);
    }
    
    /**
     * 根据Q点数值的数组创建一个公钥对象(Hex)
     *
     * @param publicKeyQPointBase64 公钥Q点值(Hex)
     * @return 公钥对象
     * @throws NoSuchAlgorithmException 没有对应点算法
     * @throws InvalidKeySpecException  非法密钥
     */
    private static PublicKey createPublicKeyByQPointBase64(String publicKeyQPointBase64) throws NoSuchAlgorithmException, InvalidKeySpecException {
        return createPublicKeyByQPoint(Base64.getDecoder().decode(publicKeyQPointBase64));
    }

    /**
     * 根据Q点数值的数组创建一个公钥对象(Hex)
     *
     * @param publicKeyQPointHex 公钥Q点值(Hex)
     * @return 公钥对象
     * @throws NoSuchAlgorithmException 没有对应点算法
     * @throws InvalidKeySpecException  非法密钥
     */
    private static PublicKey createPublicKeyByQPoint(String publicKeyQPointHex) throws NoSuchAlgorithmException, InvalidKeySpecException {
        return createPublicKeyByQPoint(Hex.decode(publicKeyQPointHex));
    }

    /**
     * 根据Q点数值的数组创建一个公钥对象
     *
     * @param publicKeyQPointBytes 公钥Q点值(数组)
     * @return 公钥对象
     * @throws NoSuchAlgorithmException 没有对应点算法
     * @throws InvalidKeySpecException  非法密钥
     */
    public static PublicKey createPublicKeyByQPoint(byte[] publicKeyQPointBytes) throws NoSuchAlgorithmException, InvalidKeySpecException {

        // 获取SM2相关参数
        X9ECParameters parameters = GMNamedCurves.getByName(CRYPTO_NAME_SM2);
        // 椭圆曲线参数规格
        ECParameterSpec ecParameterSpec = new ECParameterSpec(parameters.getCurve(), parameters.getG(), parameters.getN(), parameters.getH());
        // 将公钥HEX字符串转换为椭圆曲线对应的点
        ECPoint ecPoint = parameters.getCurve().decodePoint(publicKeyQPointBytes);
        // 获取椭圆曲线KEY生成器
        KeyFactory keyFactory = KeyFactory.getInstance("EC", new BouncyCastleProvider());
        return keyFactory.generatePublic(new ECPublicKeySpec(ecPoint, ecParameterSpec));
    }

1.4 将各种字符串保存的私钥转为私钥对象

    /**
     * 将Base64转码的私钥串,转化为私钥对象(全量密钥)
     *
     * @param base64PrivateKey 私钥对象base64字符串
     * @return 私钥对象 {@link BCECPrivateKey}
     */
    public static PrivateKey createPrivateKeyByFullBase64Str(String base64PrivateKey) throws NoSuchAlgorithmException, InvalidKeySpecException {

        PKCS8EncodedKeySpec pkcs8EncodedKeySpec = new PKCS8EncodedKeySpec(Base64.getDecoder().decode(base64PrivateKey));
        KeyFactory keyFactory = KeyFactory.getInstance("EC", new BouncyCastleProvider());

        return keyFactory.generatePrivate(pkcs8EncodedKeySpec);
    }

    /**
     * 根据D值(Base64格式的 {@link String })创建一个私钥对象
     *
     * @param dBase64 D值(Base64字符串)
     * @return 私钥对象
     * @throws NoSuchAlgorithmException 没有对应的算法
     * @throws InvalidKeySpecException  非法密钥
     */
    public static PrivateKey createPrivateKeyByDBase64(String dBase64) throws NoSuchAlgorithmException, InvalidKeySpecException {
        return createPrivateKeyByD(Base64.getDecoder().decode(dBase64));
    }

    /**
     * 根据D值({@link  Byte} 数组)创建一个私钥对象
     *
     * @param dBytes D值(Byte数组)
     * @return 私钥对象
     * @throws NoSuchAlgorithmException 没有对应的算法
     * @throws InvalidKeySpecException  非法密钥
     */
    public static PrivateKey createPrivateKeyByD(byte[] dBytes) throws NoSuchAlgorithmException, InvalidKeySpecException {
        String dHex = Hex.toHexString(dBytes);
        return createPrivateKeyByD(dHex);
    }

    /**
     * 根据D值(HEX格式的 {@link String })创建一个私钥对象
     *
     * @param dHex D值(HEX字符串)
     * @return 私钥对象
     * @throws NoSuchAlgorithmException 没有对应的算法
     * @throws InvalidKeySpecException  非法密钥
     */
    public static PrivateKey createPrivateKeyByD(String dHex) throws NoSuchAlgorithmException, InvalidKeySpecException {
        return createPrivateKeyByD(new BigInteger(dHex, 16));
    }

    /**
     * 根据D值({@link BigInteger})创建一个私钥对象
     *
     * @param d D值
     * @return 私钥对象
     * @throws NoSuchAlgorithmException 没有对应的算法
     * @throws InvalidKeySpecException  非法密钥
     */
    public static PrivateKey createPrivateKeyByD(BigInteger d) throws NoSuchAlgorithmException, InvalidKeySpecException {

        X9ECParameters parameters = GMNamedCurves.getByName(CRYPTO_NAME_SM2);
        ECParameterSpec ecParameterSpec = new ECParameterSpec(parameters.getCurve(),
                parameters.getG(), parameters.getN(), parameters.getH());
        KeyFactory keyFactory = KeyFactory.getInstance("EC", new BouncyCastleProvider());

        return keyFactory.generatePrivate(new ECPrivateKeySpec(d, ecParameterSpec));
    }

1.5 加密、解密、签名、验签

import org.bouncycastle.crypto.CipherParameters;
import org.bouncycastle.crypto.InvalidCipherTextException;
import org.bouncycastle.crypto.digests.SM3Digest;
import org.bouncycastle.crypto.engines.SM2Engine;
import org.bouncycastle.crypto.params.*;
import org.bouncycastle.crypto.signers.SM2Signer;
import org.bouncycastle.crypto.signers.StandardDSAEncoding;
import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPrivateKey;
import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey;
import org.bouncycastle.jcajce.provider.asymmetric.util.ECUtil;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.jce.spec.ECParameterSpec;
import org.bouncycastle.util.encoders.Hex;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.SecureRandom;
import java.security.Security;


public class BcSm2Util {

    private final static Logger LOGGER = LoggerFactory.getLogger(BcSm2Util.class);

    static {
        Security.addProvider(new BouncyCastleProvider());
    }

    /**
     * 根据publicKey对原始数据data,使用SM2加密
     */
    public static byte[] encrypt(byte[] data, PublicKey publicKey) throws InvalidCipherTextException {

        BCECPublicKey localECPublicKey = (BCECPublicKey) publicKey;
        ECParameterSpec localECParameterSpec = localECPublicKey.getParameters();
        ECDomainParameters localECDomainParameters = new ECDomainParameters(
                localECParameterSpec.getCurve(), localECParameterSpec.getG(), localECParameterSpec.getN()
        );
        ECPublicKeyParameters localECPublicKeyParameters = new ECPublicKeyParameters(localECPublicKey.getQ(), localECDomainParameters);

        SM2Engine localSM2Engine = new SM2Engine(new SM3Digest(), SM2Engine.Mode.C1C3C2);
        localSM2Engine.init(true, new ParametersWithRandom(localECPublicKeyParameters, new SecureRandom()));

        byte[] arrayOfByte2;
        arrayOfByte2 = localSM2Engine.processBlock(data, 0, data.length);
        return arrayOfByte2;
    }

    /**
     * 根据privateKey对加密数据encodedata,使用SM2解密
     */
    public static byte[] decrypt(byte[] encodedata, PrivateKey privateKey) throws InvalidCipherTextException {
        SM2Engine localSM2Engine = new SM2Engine(new SM3Digest(), SM2Engine.Mode.C1C3C2);
        BCECPrivateKey sm2PriK = (BCECPrivateKey) privateKey;
        ECParameterSpec localECParameterSpec = sm2PriK.getParameters();
        ECDomainParameters localECDomainParameters = new ECDomainParameters(
                localECParameterSpec.getCurve(), localECParameterSpec.getG(), localECParameterSpec.getN()
        );
        ECPrivateKeyParameters localECPrivateKeyParameters = new ECPrivateKeyParameters(sm2PriK.getD(),
                localECDomainParameters);
        localSM2Engine.init(false, localECPrivateKeyParameters);

        return localSM2Engine.processBlock(encodedata, 0, encodedata.length);

    }

    /**
     * 私钥签名
     */
    public static byte[] signByPrivateKey(byte[] data, PrivateKey privateKey) throws Exception {

        SM2Signer signer = new SM2Signer(StandardDSAEncoding.INSTANCE, new SM3Digest());
        CipherParameters param = new ParametersWithRandom(ECUtil.generatePrivateKeyParameter(privateKey));
        ParametersWithID parametersWithID = new ParametersWithID(
                param, Hex.decodeStrict("31323334353637383132333435363738")
        );

        signer.init(true, parametersWithID);
        signer.update(data, 0, data.length);
        return signer.generateSignature();
    }

    /**
     * 公钥验签
     */
    public static boolean verifyByPublicKey(byte[] data, PublicKey publicKey, byte[] signature) throws Exception {
        SM2Signer signer = new SM2Signer(StandardDSAEncoding.INSTANCE, new SM3Digest());
        ParametersWithID parametersWithID = new ParametersWithID(
                ECUtil.generatePublicKeyParameter(publicKey), Hex.decodeStrict("31323334353637383132333435363738")
        );

        signer.init(false, parametersWithID);
        signer.update(data, 0, data.length);
        return signer.verifySignature(signature);
    }
    
    /**
     * 计算签名(将签名的结果转为R和S值返回)
     *
     * @param data       原始数据
     * @param privateKey 私钥
     * @return 签名结果
     * @throws Exception 异常
     */
    public static Sm2SignRS signByPrivateKeyToRS(byte[] data, PrivateKey privateKey) throws Exception {
        byte[] signResultBytes = signByPrivateKey(data, privateKey);
        StandardDSAEncoding standardDSAEncoding = new StandardDSAEncoding();
        ParametersWithRandom param = new ParametersWithRandom(ECUtil.generatePrivateKeyParameter(privateKey));
        BigInteger[] decode = standardDSAEncoding.decode(
                ((ECKeyParameters) param.getParameters()).getParameters().getN(), signResultBytes
        );

        byte[] resultR;
        byte[] sumR = decode[0].toByteArray();
        if (sumR[0] == 0x00 && sumR.length == 33) {
            resultR = new byte[32];
            System.arraycopy(sumR, 1, resultR, 0, 32);
        } else {
            resultR = sumR;
        }

        byte[] resultS;
        byte[] sumS = decode[1].toByteArray();
        if (sumS[0] == 0x00 && sumS.length == 33) {
            resultS = new byte[32];
            System.arraycopy(sumS, 1, resultS, 0, 32);
        } else {
            resultS = sumS;
        }

        return new Sm2SignRS(
                resultR,
                resultS
        );
    }

    /**
     * 根据R和S的值验证签名
     *
     * @param data      原始数据
     * @param publicKey 公钥
     * @param sm2SignRS 签名数据对象
     * @return 成功返回true
     * @throws Exception 异常
     */
    public static boolean verifyByPublicKeyByRS(byte[] data, PublicKey publicKey, Sm2SignRS sm2SignRS) throws Exception {
        StandardDSAEncoding standardDSAEncoding = new StandardDSAEncoding();
        ECKeyParameters ecKeyParameters = (ECKeyParameters) ECUtil.generatePublicKeyParameter(publicKey);
        byte[] encode = standardDSAEncoding.encode(
                ecKeyParameters.getParameters().getN(),
                byteConvertInteger(sm2SignRS.getR()),
                byteConvertInteger(sm2SignRS.getS())
        );

        return verifyByPublicKey(data, publicKey, encode);
    }

    /**
     * 换字节流(字节数组)型数据转大数字(主要是做了在首个数组为负数的时候将数组前边拼接为 0x00)
     *
     * @param b 字节数组
     * @return 大数字
     */
    public static BigInteger byteConvertInteger(byte[] b) {
        if (b[0] < 0) {
            byte[] temp = new byte[b.length + 1];
            temp[0] = 0;
            System.arraycopy(b, 0, temp, 1, b.length);
            return new BigInteger(temp);
        }
        return new BigInteger(b);
    }
}

2.坑

2.1 加密中的坑

2.1.1 加密方式

加密方式里边,有两种 一种是 C1C3C2另一种是C1C2C3,这两种加密方式不同,当时找的资料说是,旧版本的标准上,使用的是C1C2C3,但是后续应该是更新过,使用的是C1C3C2,其他语言,比如Python或者Golang或者C的实现大多直接就是C1C3C2的,但是如果java中使用的bouncycastle的包,默认使用的是C1C2C3,就会发生与其他语言的加密结果不能相互解密的情况,但是可能你跟其他人的Java系统加解密又没有问题。

2.1.2 与其他语言的密钥传输编码问题

Java的密钥一般直接使用 PrivateKey.getEncoded() 或者 PublicKey.getEncoded() 获得密钥,然后直接使用Base64或者Hex将密钥转成可见字符串,但是Sm2算法这样保存的密钥其他语言的工具大多数是解析不了的。

2.1.2.1 导出

所以,Java生成的密钥,在导出的时候,最好使用 PrivateKey.getD().toByteArray() 和 PublicKey.getQ().getEncoded() 导出密钥,然后再转成Base64或者Hex给其他系统;

2.1.2.2 导入

导入的时候,请使用我上边的代码转换成对应的公钥或者私钥的对象,这里有另一个容易出错的地方,在私钥byte[]转私钥对象的时候,有些人会使用new BigInteger(byte[])这个方法将byte[]转为BigInteger,然后调用 keyFactory.generatePrivate(new ECPrivateKeySpec(BigInteger, ECParameterSpec)),但是实测这样在一些情况下会报错,这个时间长了记不清具体原因了,好像是因为第一位为负数的情况下会报错,我的方法是把byte[]转为Hex,然后再使用new BigInteger(hexStr, int)这种方式转为BigInteger,这个需要注意。

2.1.3 公钥压缩

publicKey.getQ().getEncoded()这个方法,有一个boolean参数,控制输出的密钥是否为压缩后的密钥,输出内容转为Hex之后,02和03开头的为压缩后的密钥,04表示未经压缩的密钥。Java里边压缩为压缩的调用同一个方法就能转为公钥对象,但是其他语言的目前不清楚,所以导出的时候,最好标注一下压缩或者未压缩。


2.2 签名验签中的坑

2.2.1 签名

Sm2签名时,有一个userId的概念,这个东西一般直接用CipherParameters对象是带不进去的,如果没有,默认是 Hex.decodeStrict(“31323334353637383132333435363738”) ,也就是1234567812345678的Ascii值,如果要使用自定义的userId,则需要使用ParametersWithID这个对象调用Signer的init,这个对象可以传入一个CipherParameters,然后再传入一个userId,可以把制定的userId带进去。

2.2.2 签名验签RS

我们平常接触的算法,一般我们调用加解密算法只返回一个值,但是Sm2算法,签名其实是有两个值,一个R,一个S,两个值构成一个签名结果,Java中bouncycastle的返回虽然也是一个值,但是大概看了一下算法的实现代码,其实得出的结果也是两个值,一个R,一个S,然后通过一个方法拼接成一个值(不确定这个方法的转换方式是不是有标准的)。在我们与C程序一块测试的时候,他们反馈了这个问题,然后我对着bouncycastle包内的org.bouncycastle.crypto.signers.SM2Signer类,摘出来了R、S和单一返回值的转换代码,已经提取到上边的代码里,可以直接使用。

总结

总之,可能是之前对加密解密以及签名验签的实际实现并不十分了解,导致现在调试的时候发生了很多问题,借助SM2的学习,补充一下相关知识,也做一个记录,避免之后忘记。这段时间也调了SM4的对称加密算法,等有时间再写一篇总结。

  • 10
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
可以使用Bouncy Castle等第三方库来实现SM2算法的加解密操作,以下是Java代码示例: ``` java import org.bouncycastle.asn1.ASN1ObjectIdentifier; import org.bouncycastle.asn1.gm.GMObjectIdentifiers; import org.bouncycastle.asn1.x9.X9ECParameters; 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.ECPrivateKeyParameters; import org.bouncycastle.crypto.params.ECPublicKeyParameters; import org.bouncycastle.crypto.signers.SM2Signer; import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.bouncycastle.util.encoders.Hex; import javax.crypto.Cipher; import java.security.KeyPair; import java.security.KeyPairGenerator; import java.security.Security; public class SM2Utils { static { Security.addProvider(new BouncyCastleProvider()); } /** * 生成SM2公私钥对 * * @return 公私钥对 * @throws Exception */ public static AsymmetricCipherKeyPair generateKeyPair() throws Exception { X9ECParameters ecParams = GMObjectIdentifiers.sm2p256v1; ECDomainParameters ecDomainParameters = new ECDomainParameters(ecParams.getCurve(), ecParams.getG(), ecParams.getN()); ECKeyPairGenerator keyPairGenerator = new ECKeyPairGenerator(); ECKeyGenerationParameters generationParameters = new ECKeyGenerationParameters(ecDomainParameters, null); keyPairGenerator.init(generationParameters); return keyPairGenerator.generateKeyPair(); } /** * SM2加密 * * @param data 要加密的数据 * @param privateKey 私钥 * @param publicKey 公钥 * @return 加密结果,以16进制字符串表示 * @throws Exception */ public static String encrypt(byte[] data, ECPrivateKeyParameters privateKey, ECPublicKeyParameters publicKey) throws Exception { Cipher cipher = Cipher.getInstance("SM2", BouncyCastleProvider.PROVIDER_NAME); cipher.init(Cipher.ENCRYPT_MODE, publicKey); byte[] encrypted = cipher.doFinal(data); return Hex.toHexString(encrypted); } /** * SM2解密 * * @param encrypted 加密的数据,以16进制字符串表示 * @param privateKey 私钥 * @param publicKey 公钥 * @return 解密结果 * @throws Exception */ public static byte[] decrypt(String encrypted, ECPrivateKeyParameters privateKey, ECPublicKeyParameters publicKey) throws Exception { Cipher cipher = Cipher.getInstance("SM2", BouncyCastleProvider.PROVIDER_NAME); cipher.init(Cipher.DECRYPT_MODE, privateKey); byte[] encryptedBytes = Hex.decode(encrypted); return cipher.doFinal(encryptedBytes); } /** * SM2签名 * * @param data 要签名的数据 * @param privateKey 私钥 * @return 签名结果,以16进制字符串表示 * @throws Exception */ public static String sign(byte[] data, ECPrivateKeyParameters privateKey) throws Exception { SM2Signer signer = new SM2Signer(); signer.init(true, privateKey); signer.update(data, 0, data.length); byte[] signature = signer.generateSignature(); return Hex.toHexString(signature); } /** * SM2验签 * * @param data 被签名的数据 * @param signature 签名结果,以16进制字符串表示 * @param publicKey 公钥 * @return 验签结果 * @throws Exception */ public static boolean verify(byte[] data, String signature, ECPublicKeyParameters publicKey) throws Exception { SM2Signer signer = new SM2Signer(); signer.init(false, publicKey); signer.update(data, 0, data.length); byte[] signatureBytes = Hex.decode(signature); return signer.verifySignature(signatureBytes); } public static void main(String[] args) throws Exception { AsymmetricCipherKeyPair keyPair = generateKeyPair(); ECPrivateKeyParameters privateKey = (ECPrivateKeyParameters) keyPair.getPrivate(); ECPublicKeyParameters publicKey = (ECPublicKeyParameters) keyPair.getPublic(); String plaintext = "Hello SM2"; String encryptedText = encrypt(plaintext.getBytes("UTF-8"), privateKey, publicKey); System.out.println("Encrypted Text: " + encryptedText); byte[] decryptedBytes = decrypt(encryptedText, privateKey, publicKey); String decryptedText = new String(decryptedBytes, "UTF-8"); System.out.println("Decrypted Text: " + decryptedText); String signature = sign(plaintext.getBytes("UTF-8"), privateKey); System.out.println("Signature: " + signature); boolean verified = verify(plaintext.getBytes("UTF-8"), signature, publicKey); System.out.println("Verified: " + verified); } } ``` 如果您有更具体的实现问题可以再提出来,我会尽力解答。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值