国密SM2,SM3,SM4在BC上的实现(二)

本节讲的SM2的实现,这个其实是可以参照RSA来写,都是非对称加密。
这里写图片描述

我们通过KeyPairGenerator.getInstance("SM2").generateKeyPair();来获取密钥对,最后调用的就是KeyPairGenSpi的generateKeyPair(),仿照RSA中的KeyPairGeneratorSpi来写,
package org.gk.gm.SM2.spi;

import org.bouncycastle.asn1.x9.X9ECParameters;
import org.bouncycastle.jcajce.provider.asymmetric.ec.KeyPairGeneratorSpi.EC;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.KeyPair;
import java.security.SecureRandom;
import java.security.interfaces.ECPrivateKey;
import java.security.interfaces.ECPublicKey;
import java.security.spec.AlgorithmParameterSpec;

import org.bouncycastle.jce.spec.ECParameterSpec;
import org.gk.gm.SM2.util.ECUtil;
import org.gk.gm.provider.SMProvider;

public class KeyPairGenSpi extends EC {
    public KeyPairGenSpi() {
        try {

            X9ECParameters x9 = ECUtil.getNamedCurveByName("SM2");
            ECParameterSpec parameterSpec =  new ECParameterSpec(x9.getCurve(), x9.getG(), x9.getN());
            super.initialize(parameterSpec, (SecureRandom)null);
        } catch (InvalidAlgorithmParameterException var2) {
            var2.printStackTrace();
        }

    }

    public KeyPair generateKeyPair() {
        KeyPair keyPair = super.generateKeyPair();

        try {
            SM2PrivateKey e = new SM2PrivateKey((ECPrivateKey)keyPair.getPrivate(), SMProvider.CONFIGURATION);
            SM2PublicKey publicKey = new SM2PublicKey((ECPublicKey)keyPair.getPublic(), SMProvider.CONFIGURATION);
            return new KeyPair(publicKey, e);
        } catch (InvalidKeyException var4) {
            return null;
        }
    }

    public void initialize(AlgorithmParameterSpec params) throws InvalidAlgorithmParameterException {
        super.initialize((AlgorithmParameterSpec)null, (SecureRandom)null);
    }

    public void initialize(int keySize) {
        try {
            super.initialize((AlgorithmParameterSpec)null, (SecureRandom)null);
        } catch (InvalidAlgorithmParameterException var3) {
            ;
        }

    }

    public void initialize(AlgorithmParameterSpec params, SecureRandom random) throws InvalidAlgorithmParameterException {
        super.initialize((AlgorithmParameterSpec)null, random);
    }

    public void initialize(int keySize, SecureRandom random) {
        try {
            super.initialize((AlgorithmParameterSpec)null, random);
        } catch (InvalidAlgorithmParameterException var4) {
            ;
        }

    }
}

我们这里继承EC(EC也是继承KeyPairGenerator的),其中构造方法就是对SM2进行初始化,SM2PublicKey,SM2PrivateKey这个都是自定义的,这个可以随个人的爱好,只是加解密的时候,对应的变一下就可以了,这里公钥genEncode返回的是512bit(128byte),私钥256bit(64byte)。
好了,再看加密,加密CipherSpi这个类跟RSA的差不多。

public class CipherSpi extends BaseCipherSpi {
    private int opMode;
    private AsymmetricKeyParameter keyParam = null;
    private SM2Cipher sm2Cipher = null;
    private ECPoint c1 = null;
    private ByteArrayOutputStream bout = null;

    public CipherSpi() {
    }

    protected void engineInit(int opmode, Key key, SecureRandom random) throws InvalidKeyException {
        if(opmode != 1 && opmode != 2) {
            throw new InvalidKeyException("Only supports ENCRYPT_MODE|DECRYPT_MODE.");
        } else if(key == null || !(key instanceof ECPublicKey) && !(key instanceof ECPrivateKey)) {
            throw new InvalidKeyException("No useful Key.");
        } else if(opmode == 2 && key instanceof ECPublicKey) {
            throw new InvalidKeyException("DECRYPT_MODE needs a Private Key.");
        } else {
            if(this.sm2Cipher == null) {
                this.sm2Cipher = new SM2Cipher();
            }

            this.keyParam = key instanceof ECPublicKey?ECUtil.generatePublicKeyParameter((ECPublicKey)key):ECUtil.generatePrivateKeyParameter((ECPrivateKey)key);
            if(opmode == 1) {
                ECPoint q = null;
                if(this.keyParam.isPrivate()) {
                    ECDomainParameters edp = ((ECKeyParameters)this.keyParam).getParameters();
                    if(key instanceof SM2PrivateKey) {
                        DERBitString dps = ((SM2PrivateKey)key).getPubKey();
                        if(dps != null) {
                            try {
                                q = edp.getCurve().decodePoint(dps.getBytes());
                            } catch (RuntimeException var8) {
                                ;
                            }
                        }
                    }

                    if(q == null) {
                        q = edp.getG().multiply(((ECPrivateKeyParameters)this.keyParam).getD());
                    }
                } else {
                    q = ((ECPublicKeyParameters)this.keyParam).getQ();
                }

                this.c1 = this.sm2Cipher.Init_enc(q, random);
            }

            this.opMode = opmode;
            if(this.bout == null) {
                this.bout = new ByteArrayOutputStream();
            } else {
                this.bout.reset();
            }

        }
    }

    protected void engineInit(int opmode, Key key, AlgorithmParameterSpec params, SecureRandom random) throws InvalidKeyException {
        this.engineInit(opmode, key, random);
    }

    protected void engineInit(int opmode, Key key, AlgorithmParameters params, SecureRandom random) throws InvalidKeyException {
        this.engineInit(opmode, key, random);
    }

    protected byte[] engineUpdate(byte[] input, int inputOffset, int inputLen) {
        if(input != null && inputOffset >= 0 && inputOffset + inputLen <= input.length) {
            this.bout.write(input, inputOffset, inputLen);
        }

        return null;
    }

    protected int engineUpdate(byte[] input, int inputOffset, int inputLen, byte[] output, int outputOffset) throws ShortBufferException {
        if(input != null && inputOffset >= 0 && inputOffset + inputLen <= input.length) {
            this.bout.write(input, inputOffset, inputLen);
        }

        return this.bout.size() + 1 + 64 + 32;
    }

    protected byte[] engineDoFinal(byte[] input, int inputOffset, int inputLen) throws IllegalBlockSizeException, BadPaddingException {
        if(input != null && inputOffset >= 0 && inputOffset + inputLen <= input.length) {
            this.bout.write(input, inputOffset, inputLen);
        }

        if(this.bout.size() == 0) {
            return null;
        } else {
            byte[] c3 = new byte[32];
            byte[] tmp = this.bout.toByteArray();
            byte[] c2;
            byte[] c1bytes;
            if(this.opMode == 1) {
                c2 = tmp;
                this.sm2Cipher.Encrypt(tmp);
                this.sm2Cipher.Dofinal(c3);
                byte[] bins1 = Pack.BNto32Bytes(this.c1.getX().toBigInteger());
                c1bytes = Pack.BNto32Bytes(this.c1.getY().toBigInteger());
                ByteArrayOutputStream bu1 = new ByteArrayOutputStream(65 + tmp.length + 32);
                byte[] res = null;
                bu1.write(4);

                try {
                    bu1.write(bins1);
                    bu1.write(c1bytes);
                    bu1.write(c2);
                    bu1.write(c3);
                    res = bu1.toByteArray();
                    bu1.close();
                } catch (IOException var12) {
                    ;
                }

                return res;
            } else if(tmp[0] != 4 && tmp.length < 98) {
                throw new IllegalBlockSizeException("Invalid data.");
            } else {
                ByteArrayInputStream bins = new ByteArrayInputStream(tmp);
                c1bytes = new byte[65];
                c2 = new byte[tmp.length - 32 - 65];

                try {
                    bins.read(c1bytes);
                    bins.read(c2);
                    bins.read(c3);
                    bins.close();
                    this.c1 = ((ECKeyParameters)this.keyParam).getParameters().getCurve().decodePoint(c1bytes);
                    if(this.c1.isInfinity()) {
                        throw new IllegalBlockSizeException("ECPoint is Infinity.");
                    }
                } catch (IOException var13) {
                    ;
                } catch (RuntimeException var14) {
                    throw new BadPaddingException("Invalid data.");
                }

                this.sm2Cipher.Init_dec(((ECPrivateKeyParameters)this.keyParam).getD(), this.c1);
                this.sm2Cipher.Decrypt(c2);
                byte[] bu = new byte[32];
                this.sm2Cipher.Dofinal(bu);
                if(Arrays.equals(bu, c3)) {
                    return c2;
                } else {
                    throw new IllegalBlockSizeException("Decrypt Error.");
                }
            }
        }
    }

    protected int engineGetOutputSize(int inputLen) {
        return 97 + inputLen;
    }

    protected int engineDoFinal(byte[] input, int inputOffset, int inputLen, byte[] output, int outputOffset) throws ShortBufferException, IllegalBlockSizeException, BadPaddingException {
        byte[] res = this.engineDoFinal(input, inputOffset, inputLen);
        if(res == null) {
            return 0;
        } else if(output.length - outputOffset < res.length) {
            throw new ShortBufferException("Output Length:" + res.length);
        } else {
            System.arraycopy(res, 0, output, outputOffset, res.length);
            return res.length;
        }
    }

    protected byte[] engineWrap(Key key) throws IllegalBlockSizeException, InvalidKeyException {
        throw new InvalidKeyException("Unsupported.");
    }

    protected Key engineUnwrap(byte[] wrappedKey, String wrappedKeyAlgorithm, int wrappedKeyType) throws InvalidKeyException {
        throw new InvalidKeyException("Unsupported.");
    }

    public static class SM2 extends CipherSpi {
        public SM2() {
        }
    }
}

Clipher 的doFinal方法,最后调用的就是engineDoFinal,真正实现加解密算法的是在

public class SM2Cipher {
    private int ct = 1;
    private ECPoint p2;
    private SM3Digest sm3keybase;
    private SM3Digest sm3c3;
    private byte[] key = new byte[32];
    private byte keyOff = 0;

    public SM2Cipher() {
    }

    private void Reset() {
        this.sm3keybase = new SM3Digest();
        this.sm3c3 = new SM3Digest();
        byte[] p = Pack.BNto32Bytes(this.p2.getX().toBigInteger());
        this.sm3keybase.update(p, 0, p.length);
        this.sm3c3.update(p, 0, p.length);
        p = Pack.BNto32Bytes(this.p2.getY().toBigInteger());
        this.sm3keybase.update(p, 0, p.length);
        this.ct = 1;
        this.NextKey();
    }

    private void NextKey() {
        SM3Digest sm3keycur = new SM3Digest(this.sm3keybase);
        sm3keycur.update((byte)(this.ct >> 24 & 255));
        sm3keycur.update((byte)(this.ct >> 16 & 255));
        sm3keycur.update((byte)(this.ct >> 8 & 255));
        sm3keycur.update((byte)(this.ct & 255));
        sm3keycur.doFinal(this.key, 0);
        this.keyOff = 0;
        ++this.ct;
    }

    public ECPoint Init_enc(ECPoint userKey, SecureRandom random) {
        SM2CipherKey cpkey = SM2CipherKeyGen.getInstance(random).generateKey();
        BigInteger k = cpkey.K;
        ECPoint c1 = cpkey.Q;
        this.p2 = userKey.multiply(k);
        this.Reset();
        return c1;
    }

    public void Encrypt(byte[] data) {
        this.sm3c3.update(data, 0, data.length);

        for(int i = 0; i < data.length; ++i) {
            if(this.keyOff == this.key.length) {
                this.NextKey();
            }

            data[i] ^= this.key[this.keyOff++];
        }

    }

    public void Init_dec(BigInteger userD, ECPoint c1) {
        this.p2 = c1.multiply(userD);
        this.Reset();
    }

    public void Decrypt(byte[] data) {
        for(int i = 0; i < data.length; ++i) {
            if(this.keyOff == this.key.length) {
                this.NextKey();
            }

            data[i] ^= this.key[this.keyOff++];
        }

        this.sm3c3.update(data, 0, data.length);
    }

    public void Dofinal(byte[] c3) {
        byte[] p = Pack.BNto32Bytes(this.p2.getY().toBigInteger());
        this.sm3c3.update(p, 0, p.length);
        this.sm3c3.doFinal(c3, 0);
        this.Reset();
    }
}

看到这里,这个和之前网络上单独加解密的一样了吧。

签名部分SignatureSpi

public class SignatureSpi extends DSABase {
    SignatureSpi(SignatureSpi.SM2B digest, DSA signer, DSAEncoder encoder) {
        super(digest, signer, encoder);
    }

    protected void engineInitVerify(PublicKey publicKey) throws InvalidKeyException {
        ECPublicKeyParameters param = (ECPublicKeyParameters)ECUtil.generatePublicKeyParameter(publicKey);
        ((SignatureSpi.SM2B)this.digest).setPublicKey(param.getQ());
        this.signer.init(false, param);
    }

    protected void engineInitSign(PrivateKey privateKey) throws InvalidKeyException {
        ECPoint pubKey = null;
        ECPrivateKeyParameters param = (ECPrivateKeyParameters)ECUtil.generatePrivateKeyParameter(privateKey);
        ECDomainParameters edp = param.getParameters();
        DERBitString dps = null;
        if(privateKey instanceof SM2PrivateKey) {
            dps = ((SM2PrivateKey)privateKey).getPubKey();
        }

        if(dps != null) {
            try {
                pubKey = edp.getCurve().decodePoint(dps.getBytes());
            } catch (RuntimeException var7) {
                ;
            }
        }

        if(pubKey == null) {
            pubKey = edp.getG().multiply(param.getD());
        }

        ((SignatureSpi.SM2B)this.digest).setPublicKey(pubKey);
        if(this.appRandom != null) {
            this.signer.init(true, new ParametersWithRandom(param, this.appRandom));
        } else {
            this.signer.init(true, param);
        }

    }

    protected void engineSetParameter(AlgorithmParameterSpec params) {
        if(params instanceof SigParamSpec) {
            DEROctetString dos = ((SigParamSpec)params).string;
            if(dos != null) {
                ((SignatureSpi.SM2B)this.digest).setUserID(dos.getOctets());
            }

            SecureRandom random = ((SigParamSpec)params).random;
            if(random != null) {
                this.appRandom = random;
            }
        }

    }

    /** @deprecated */
    @Deprecated
    protected void engineSetParameter(String param, Object value) throws InvalidParameterException {
        if(param.equalsIgnoreCase("USER_ID")) {
            if(value instanceof String) {
                String uid = (String)value;
                if(uid.length() > 0) {
                    ((SignatureSpi.SM2B)this.digest).setUserID(uid.getBytes());
                }
            } else {
                if(!(value instanceof byte[])) {
                    throw new InvalidParameterException("Bad User ID.");
                }

                ((SignatureSpi.SM2B)this.digest).setUserID((byte[])((byte[])value));
            }
        } else {
            if(!param.equalsIgnoreCase("RANDOM") || !(value instanceof SecureRandom)) {
                throw new InvalidParameterException("Unknown parameter.");
            }

            this.appRandom = (SecureRandom)value;
        }

    }

    protected void engineUpdate(byte b) throws SignatureException {
        ((SignatureSpi.SM2B)this.digest).prepareZ();
        this.digest.update(b);
    }

    protected void engineUpdate(byte[] b, int off, int len) throws SignatureException {
        ((SignatureSpi.SM2B)this.digest).prepareZ();
        this.digest.update(b, off, len);
    }

    private static class SM2B extends SM3Digest {
        private static final byte[] def_id = "SM2 = \'Saabee Meet 2b\'.".getBytes();
        private final byte[][] zConst = new byte[4][32];
        private final X9ECParameters x9cp = ECUtil.getNamedCurveByName("SM2");
        private byte[] user_ID;
        private ECPoint pubKey;

        public SM2B() {
            Pack.BNto32Bytes(this.x9cp.getCurve().getA().toBigInteger(), this.zConst[0]);
            Pack.BNto32Bytes(this.x9cp.getCurve().getB().toBigInteger(), this.zConst[1]);
            Pack.BNto32Bytes(this.x9cp.getG().getX().toBigInteger(), this.zConst[2]);
            Pack.BNto32Bytes(this.x9cp.getG().getY().toBigInteger(), this.zConst[3]);
            this.user_ID = null;
            this.pubKey = null;
        }

        public void setUserID(byte[] userID) {
            this.user_ID = userID;
        }

        public void setPublicKey(ECPoint pubKey) {
            this.pubKey = pubKey;
        }

        public void prepareZ() {
            if(this.pubKey != null) {
                this.reset();
                byte[] p = this.user_ID;
                if(p == null || p.length == 0) {
                    p = EC5Util.getSM2_UID();
                    if(p == null) {
                        p = def_id;
                    }
                }

                int len = p.length * 8;
                this.update((byte)(len >> 8 & 255));
                this.update((byte)(len & 255));
                this.update(p, 0, p.length);
                p = this.zConst[0];
                this.update(p, 0, p.length);
                p = this.zConst[1];
                this.update(p, 0, p.length);
                p = this.zConst[2];
                this.update(p, 0, p.length);
                p = this.zConst[3];
                this.update(p, 0, p.length);
                p = Pack.BNto32Bytes(this.pubKey.getX().toBigInteger());
                this.update(p, 0, p.length);
                p = Pack.BNto32Bytes(this.pubKey.getY().toBigInteger());
                this.update(p, 0, p.length);
                this.pubKey = null;
                byte[] zVal = new byte[super.getDigestSize()];
                this.doFinal(zVal, 0);
                this.reset();
                this.update(zVal, 0, zVal.length);
            }
        }
    }

    private static class StdDSAEncoder implements DSAEncoder {
        private StdDSAEncoder() {
        }

        public byte[] encode(BigInteger r, BigInteger s) throws IOException {
            ASN1EncodableVector v = new ASN1EncodableVector();
            v.add(new ASN1Integer(r));
            v.add(new ASN1Integer(s));
            return (new DERSequence(v)).getEncoded("DER");
        }

        public BigInteger[] decode(byte[] encoding) throws IOException {
            ASN1Sequence s = (ASN1Sequence)ASN1Primitive.fromByteArray(encoding);
            BigInteger[] sig = new BigInteger[]{ASN1Integer.getInstance(s.getObjectAt(0)).getValue(), ASN1Integer.getInstance(s.getObjectAt(1)).getValue()};
            return sig;
        }
    }

    public static class SM3SM2 extends SignatureSpi {
        public SM3SM2() {
            super(new SignatureSpi.SM2B(), new SM2Signer(), new SignatureSpi.StdDSAEncoder());
        }
    }
}

代码比较多,但是基本逻辑照着RSA的来,稍后我会整理一下,把全部代码发出来的。

  • 4
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 14
    评论
以下是使用Java实现国密SM2数字签名的示例代码: ```java import org.bouncycastle.crypto.AsymmetricCipherKeyPair; import org.bouncycastle.crypto.digests.SM3Digest; import org.bouncycastle.crypto.engines.SM2Engine; 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.jce.spec.ECParameterSpec; import org.bouncycastle.util.encoders.Hex; import java.math.BigInteger; import java.security.Security; public class SM2SignDemo { public static void main(String[] args) throws Exception { Security.addProvider(new BouncyCastleProvider()); // 生成密钥对 ECKeyPairGenerator keyPairGenerator = new ECKeyPairGenerator(); ECKeyGenerationParameters keyGenerationParams = new ECKeyGenerationParameters( new ECDomainParameters(SM2Util.SM2_p, SM2Util.SM2_a, SM2Util.SM2_b, SM2Util.SM2_ecparams_G, SM2Util.SM2_n), SM2Util.SM2_random); keyPairGenerator.init(keyGenerationParams); AsymmetricCipherKeyPair keyPair = keyPairGenerator.generateKeyPair(); ECPrivateKeyParameters privateKey = (ECPrivateKeyParameters) keyPair.getPrivate(); ECPublicKeyParameters publicKey = (ECPublicKeyParameters) keyPair.getPublic(); // 签名 SM2Signer signer = new SM2Signer(); signer.init(true, privateKey); byte[] msg = "Hello, world!".getBytes(); byte[] z = SM2Util.getSM2Z("1234567812345678".getBytes(), publicKey.getQ()); signer.update(z, 0, z.length); signer.update(msg, 0, msg.length); BigInteger[] sig = signer.generateSignature(); String signature = Hex.toHexString(sig[0].toByteArray()) + Hex.toHexString(sig[1].toByteArray()); System.out.println("Signature: " + signature); // 验证签名 signer.init(false, publicKey); signer.update(z, 0, z.length); signer.update(msg, 0, msg.length); System.out.println("Signature verification result: " + signer.verifySignature(sig[0], sig[1])); } } class SM2Util { static final BigInteger SM2_p = new BigInteger("FFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000FFFFFFFFFFFFFFFF", 16); static final BigInteger SM2_a = new BigInteger("FFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000FFFFFFFFFFFFFFFC", 16); static final BigInteger SM2_b = new BigInteger("28E9FA9E9D9F5E344D5A9E4BCF6509A7F39789F515AB8F92DDBCBD414D940E93", 16); static final BigInteger SM2_n = new BigInteger("FFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFF7203DF6B21C6052B53BBF40939D54123", 16); static final BigInteger SM2_h = BigInteger.valueOf(1); static final BigInteger SM2_Gx = new BigInteger("32C4AE2C1F1981195F9904466A39C9948FE30BBFF2660BE171F996B8FEEF18EE", 16); static final BigInteger SM2_Gy = new BigInteger("BC3736A2F4F6779C59BDCEE36B692153D0A5D10B213A3D89B0C7C5AE0FD36B88", 16); static final ECDomainParameters SM2_ecparams_G = new ECDomainParameters(SM2_p, SM2_a, SM2_b, new org.bouncycastle.math.ec.ECPoint.SecP256K1(SM2_p, SM2_a, SM2_b, new BigInteger[]{SM2_Gx, SM2_Gy}, SM2_n, SM2_h)); static final SM3Digest SM2_256_DIGEST = new SM3Digest(); static final byte[] SM2_DEFAULT_USER_ID = "1234567812345678".getBytes(); static final SecureRandom SM2_random = new SecureRandom(); /** * 获取SM2签名中的Z值 */ static byte[] getSM2Z(byte[] userId, org.bouncycastle.math.ec.ECPoint userKey) { byte[] userIdDigest = new byte[32]; SM2_256_DIGEST.update(userId, 0, userId.length); SM2_256_DIGEST.doFinal(userIdDigest, 0); byte[] x = userKey.getXCoord().getEncoded(); byte[] y = userKey.getYCoord().getEncoded(); byte[] z = new byte[userIdDigest.length + x.length + y.length]; System.arraycopy(userIdDigest, 0, z, 0, userIdDigest.length); System.arraycopy(x, 0, z, userIdDigest.length, x.length); System.arraycopy(y, 0, z, userIdDigest.length + x.length, y.length); return z; } } ``` 在上面的示例代码中,我们使用了BouncyCastle库来实现SM2数字签名。其中,`SM2Signer`类用于签名和验签,`ECDomainParameters`类用于定义椭圆曲线参数,`SM3Digest`类用于计算摘要,`SM2Util`类用于定义一些常量和工具方法。 在签名过程中,我们需要先计算出SM2签名中的Z值,然后将Z值和待签名的消息传入`SM2Signer`类中进行签名签名结果为两个整数,需要将它们转换为十六进制字符串拼接在一起即可得到最终的签名值。 在验签过程中,我们需要使用相同的Z值和待验签的消息来初始化`SM2Signer`类,然后将签名值传入进行验签。如果验签成功,`verifySignature`方法会返回`true`,否则返回`false`。 需要注意的是,以上示例代码仅供参考,实际使用时需要根据具体需求进行修改和优化。
评论 14
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值