SM2加密

所需要的jar包:百度网盘提取连接点我

提取码:k208

1:SM2KeyPair.java

package utils;

import java.math.BigInteger;
import org.bouncycastle.math.ec.ECPoint;


public class SM2KeyPair {
    /** 公钥 */
    private ECPoint publicKey;
    /** 私钥 */
    private BigInteger privateKey;

    SM2KeyPair(ECPoint publicKey, BigInteger privateKey) {
        this.publicKey = publicKey;
        this.privateKey = privateKey;
    }

    public ECPoint getPublicKey() {
        return publicKey;
    }

    public BigInteger getPrivateKey() {
        return privateKey;
    }
}
 

2:SM2Util.java

package utils;

import java.math.BigInteger;
import java.security.SecureRandom;
import java.util.Arrays;
import org.bouncycastle.crypto.DerivationFunction;
import org.bouncycastle.crypto.digests.SHA256Digest;
import org.bouncycastle.crypto.digests.ShortenedDigest;
import org.bouncycastle.crypto.generators.KDF1BytesGenerator;
import org.bouncycastle.crypto.params.ISO18033KDFParameters;
import org.bouncycastle.math.ec.ECCurve;
import org.bouncycastle.math.ec.ECPoint;


public class SM2Util {
    /** 素数p */
    private static final BigInteger p = new BigInteger(
            "FFFFFFFE" + "FFFFFFFF" + "FFFFFFFF" + "FFFFFFFF" + "FFFFFFFF" + "00000000" + "FFFFFFFF" + "FFFFFFFF", 16);
    /** 系数a */
    private static final BigInteger a = new BigInteger(
            "FFFFFFFE" + "FFFFFFFF" + "FFFFFFFF" + "FFFFFFFF" + "FFFFFFFF" + "00000000" + "FFFFFFFF" + "FFFFFFFC", 16);
    /** 系数b */
    private static final BigInteger b = new BigInteger(
            "28E9FA9E" + "9D9F5E34" + "4D5A9E4B" + "CF6509A7" + "F39789F5" + "15AB8F92" + "DDBCBD41" + "4D940E93", 16);
    /** 坐标x */
    private static final BigInteger xg = new BigInteger(
            "32C4AE2C" + "1F198119" + "5F990446" + "6A39C994" + "8FE30BBF" + "F2660BE1" + "715A4589" + "334C74C7", 16);
    /** 坐标y */
    private static final BigInteger yg = new BigInteger(
            "BC3736A2" + "F4F6779C" + "59BDCEE3" + "6B692153" + "D0A9877C" + "C62A4740" + "02DF32E5" + "2139F0A0", 16);
    /** 基点G, G=(xg,yg),其介记为n */
    private static final BigInteger n = new BigInteger(
            "FFFFFFFE" + "FFFFFFFF" + "FFFFFFFF" + "FFFFFFFF" + "7203DF6B" + "21C6052B" + "53BBF409" + "39D54123", 16);
    private static SecureRandom random = new SecureRandom();
    private ECCurve.Fp curve;
    private ECPoint G;

    public static String printHexString(byte[] b) {
        StringBuilder builder = new StringBuilder();
        for (int i = 0; i < b.length; i++) {
            String hex = Integer.toHexString(b[i] & 0xFF);
            if (hex.length() == 1) {
                builder.append('0' + hex);
                hex = '0' + hex;
            }
            System.out.print(hex.toUpperCase());
            builder.append(hex);
        }
        System.out.println();
        return builder.toString();
    }

    public BigInteger random(BigInteger max) {
        BigInteger r = new BigInteger(256, random); // int count = 1;
        while (r.compareTo(max) >= 0) {
            r = new BigInteger(128, random);
        }
        return r;
    }

    private boolean allZero(byte[] buffer) {
        for (int i = 0; i < buffer.length; i++) {
            if (buffer[i] != 0)
                return false;
        }
        return true;
    }

    /**
     * * 加密 * @param input 待加密消息M * @param publicKey 公钥 * @return byte[] 加密后的字节数组
     */
    public byte[] encrypt(String input, ECPoint publicKey) {
        System.out.println("publicKey is: " + publicKey);
        byte[] inputBuffer = input.getBytes();
        printHexString(inputBuffer);
        /* 1 产生随机数k,k属于[1, n-1] */
        BigInteger k = random(n);
        System.out.print("k: ");
        printHexString(k.toByteArray());
        /* 2 计算椭圆曲线点C1 = [k]G = (x1, y1) */
        ECPoint C1 = G.multiply(k);
        byte[] C1Buffer = C1.getEncoded(false);
        System.out.print("C1: ");
        printHexString(C1Buffer);
        // 3 计算椭圆曲线点 S = [h]Pb * curve没有指定余因子,h为空
        // BigInteger h = curve.getCofactor(); System.out.print("h: ");
        // printHexString(h.toByteArray()); if (publicKey != null) { ECPoint
        // result = publicKey.multiply(h); if (!result.isInfinity()) {
        // System.out.println("pass"); } else {
        // System.err.println("计算椭圆曲线点 S = [h]Pb失败"); return null; } }
        /* 4 计算 [k]PB = (x2, y2) */
        ECPoint kpb = publicKey.multiply(k).normalize();
        /* 5 计算 t = KDF(x2||y2, klen) */
        byte[] kpbBytes = kpb.getEncoded(false);
        DerivationFunction kdf = new KDF1BytesGenerator(new ShortenedDigest(new SHA256Digest(), 20));
        byte[] t = new byte[inputBuffer.length];
        kdf.init(new ISO18033KDFParameters(kpbBytes));
        kdf.generateBytes(t, 0, t.length);
        if (allZero(t)) {
            System.err.println("all zero");
        }
        /* 6 计算C2=M^t */

        byte[] C2 = new byte[inputBuffer.length];
        for (int i = 0; i < inputBuffer.length; i++) {
            C2[i] = (byte) (inputBuffer[i] ^ t[i]);
        }
        /* 7 计算C3 = Hash(x2 || M || y2) */
        byte[] C3 = calculateHash(kpb.getXCoord().toBigInteger(), inputBuffer, kpb.getYCoord().toBigInteger());

        /* 8 输出密文 C=C1 || C2 || C3 */
        byte[] encryptResult = new byte[C1Buffer.length + C2.length + C3.length];
        System.arraycopy(C1Buffer, 0, encryptResult, 0, C1Buffer.length);
        System.arraycopy(C2, 0, encryptResult, C1Buffer.length, C2.length);
        System.arraycopy(C3, 0, encryptResult, C1Buffer.length + C2.length, C3.length);
        System.out.print("密文: ");
        printHexString(encryptResult);
        return encryptResult;
    }

    public void decrypt(byte[] encryptData, BigInteger privateKey) {
        System.out.println("privateKey is: " + privateKey);
        System.out.println("encryptData length: " + encryptData.length);
        byte[] C1Byte = new byte[65];
        System.arraycopy(encryptData, 0, C1Byte, 0, C1Byte.length);
        ECPoint C1 = curve.decodePoint(C1Byte).normalize();
        /* 计算[dB]C1 = (x2, y2) */
        ECPoint dBC1 = C1.multiply(privateKey).normalize();
        /* 计算t = KDF(x2 || y2, klen) */
        byte[] dBC1Bytes = dBC1.getEncoded(false);
        DerivationFunction kdf = new KDF1BytesGenerator(new ShortenedDigest(new SHA256Digest(), 20));
        int klen = encryptData.length - 65 - 20;
        System.out.println("klen = " + klen);
        byte[] t = new byte[klen];
        kdf.init(new ISO18033KDFParameters(dBC1Bytes));
        kdf.generateBytes(t, 0, t.length);
        if (allZero(t)) {
            System.err.println("all zero");
        }
        /* 5 计算M'=C2^t */
        byte[] M = new byte[klen];
        for (int i = 0; i < M.length; i++) {
            M[i] = (byte) (encryptData[C1Byte.length + i] ^ t[i]);
        }
        /* 6 计算 u = Hash(x2 || M' || y2) 判断 u == C3是否成立 */
        byte[] C3 = new byte[20];
        System.arraycopy(encryptData, encryptData.length - 20, C3, 0, 20);
        byte[] u = calculateHash(dBC1.getXCoord().toBigInteger(), M, dBC1.getYCoord().toBigInteger());
        if (Arrays.equals(u, C3)) {
            System.out.println("解密成功");
            System.out.println("M' = " + new String(M));
        } else {
            System.out.print("u = ");
            printHexString(u);
            System.out.print("C3 = ");
            printHexString(C3);
            System.err.println("解密验证失败");
        }
    }

    private byte[] calculateHash(BigInteger x2, byte[] M, BigInteger y2) {
        ShortenedDigest digest = new ShortenedDigest(new SHA256Digest(), 20);
        byte[] buf = x2.toByteArray();
        digest.update(buf, 0, buf.length);
        digest.update(M, 0, M.length);
        buf = y2.toByteArray();
        digest.update(buf, 0, buf.length);
        buf = new byte[20];
        digest.doFinal(buf, 0);
        return buf;
    }

    private boolean between(BigInteger param, BigInteger min, BigInteger max) {
        if (param.compareTo(min) >= 0 && param.compareTo(max) < 0) {
            return true;
        } else {
            return false;
        }
    }

    /** * 公钥校验 * @param publicKey 公钥 * @return boolean true或false */
    private boolean checkPublicKey(ECPoint publicKey) {
        if (!publicKey.isInfinity()) {
            BigInteger x = publicKey.getXCoord().toBigInteger();
            BigInteger y = publicKey.getYCoord().toBigInteger();
            if (between(x, new BigInteger("0"), p) && between(y, new BigInteger("0"), p)) {
                BigInteger xResult = x.pow(3).add(a.multiply(x)).add(b).mod(p);
                System.out.println("xResult: " + xResult.toString());
                BigInteger yResult = y.pow(2).mod(p);
                System.out.println("yResult: " + yResult.toString());
                if (yResult.equals(xResult) && publicKey.multiply(n).isInfinity()) {
                    return true;
                }
            }
            return false;
        } else {
            return false;
        }
    }

    /** * 获得公私钥对 * @return */
    public SM2KeyPair generateKeyPair() {
        BigInteger d = random(n.subtract(new BigInteger("1")));
        SM2KeyPair keyPair = new SM2KeyPair(G.multiply(d).normalize(), d);
        if (checkPublicKey(keyPair.getPublicKey())) {
            System.out.println("generate key successfully");
            return keyPair;
        } else {
            System.err.println("generate key failed");
            return null;
        }
    }

    public SM2Util() 
    {        curve = new ECCurve.Fp(p, // q    
            a, // a
            b); // b        
    G = curve.createPoint(xg, yg);    
    }    }

3:SM2UtilTest.java

package utils;

import java.util.Arrays;


public class SM2UtilTest {
    /** 元消息串 */
    
    private static String M="a=1|b=33|c=韩";
    public static void main(String[] args) {
        SM2Util sm2 = new SM2Util();
        SM2KeyPair keyPair = sm2.generateKeyPair();
        byte[] data = sm2.encrypt(M,keyPair.getPublicKey());
        System.out.println("data is:"+Arrays.toString(data));
        
        System.out.println("keyPair.getPublicKey() is:"+keyPair.getPublicKey());
        System.out.println("keyPair.getPrivateKey() is:"+keyPair.getPrivateKey());
        
        sm2.decrypt(data, keyPair.getPrivateKey());
        }
    
    }

 

Android SM2、SM3、SM4 算法支持 Service Provider 及证书制作软件包 国密算法 JCAJCE Service Provider,适应版本 Android 4.2.2~7.0 支持 SM2 的 KeyFactory、KeyPairGenerator、Cipher、Signature、X.509 CertificateFactory 接口 支持 SM3 的 MessageDigest 接口、SM3withSM2 混合算法 支持 SM4 的 Cipher、KeyFactory、KeyGenerator、SecretKey 接口、相关算法 CMAC-SM4、Poly1305-SM4 增加 java.security.PublicKey 的子类 SM2PublicKey 增加 java.security.PrivateKey 的子类 SM2PrivateKey 全功能支持 SM3withSM2 算法的 X.509 证书结构体解释与密码运算 支持 BKS、PKCS#12 KeyStore 生成、解释、验算 X.509v1/v3 证书,签名算法支持 SM3withSM2、主流 RSA、DSA、ECDSA.... 生成、解释、验算 PKCS#10 证书申请,签名算法支持 SM3withSM2、主流 RSA、DSA、ECDSA.... *** 无须打包 BouncyCastle 支持库,体积小、节约内存 *** 请参阅 testSM.java、testCERT.java 文件列表: 1、AndroidSM.jar -- SM2、SM3、SM4 算法/证书支持的 JCA/JCE Service Provider 类库 2、AndroidCRT.jar -- X.509 数字证书/PKCS#10 证书申请相关类库 3、bc422.jar -- BouncyCastle 加密库,Android 4.2.2 内置版本(由真机导出dex文件转换而得,仅用于编译时选用,勿打包到apk文件中) 4、testSM.java -- SM2、SM3、SM4 算法相关类引用范例 5、testCERT.java -- X.509 数字证书/PKCS#10 证书申请相关类引用范例 6、readme.txt -- 本文 因条件及精力限制,各类、方法的实现未经严格彻底的测试,不宜用于商业用途软件的开发。 如欲将本开发包发布、上传、拷贝、共享等,务必保持其内容完整性(包括本文) 如有需要帮助或者索取源码,请联系 suntongo@qq.com, suntongo@hotmail.com
<think>我们被要求使用Swift实现SM2加密算法的封装。根据引用[1]中Go语言的实现,我们可以参考其流程: 1. 将待加密的字符串转换为字节数组(Data) 2. 使用公钥进行加密加密结果通常为ASN.1编码的字节序列) 3. 将加密结果转换为十六进制字符串或Base64字符串 在Swift中,我们可以使用第三方库来实现SM2算法。目前,一个常用的选择是`GMObjC`(如引用[2]和[3]所述),它是用Objective-C编写的,但可以在Swift项目中使用。 步骤: 1. 集成`GMObjC`到项目中(通过CocoaPods或手动导入)。 2. 在Swift中,通过桥接文件使用Objective-C的类和方法。 3. 封装SM2加密方法。 注意:由于`GMObjC`是Objective-C库,我们需要在桥接文件中引入头文件。 实现思路: 1. 将字符串转换为Data(如果已经是Data则跳过)。 2. 使用`GMSm2Cryptor`类进行加密(注意:`GMObjC`提供了这个类)。 3. 将加密结果转换为Base64或十六进制字符串(根据需求)。 具体代码封装如下: 首先,确保项目已经集成了`GMObjC`。然后创建桥接文件,并在其中添加: ```objective-c #import <GMObjC/GMSm2Cryptor.h> ``` 在Swift文件中,我们可以这样封装: ```swift import Foundation class SM2Crypto { /// SM2加密(公钥加密) /// - Parameters: /// - data: 需要加密的数据(Data类型) /// - publicKey: 公钥(Base64编码的字符串,需要去除头尾的标识和换行) /// - Returns: 加密后的Base64字符串(也可以选择16进制,这里使用Base64) static func encrypt(data: Data, publicKey: String) -> String? { // 将Base64格式的公钥字符串转换为Data(如果公钥是PEM格式,需要先去掉头尾标识和换行) // 注意:GMObjC要求公钥是裸的Base64字符串(即不包含-----BEGIN PUBLIC KEY-----等) guard let pubKeyData = Data(base64Encoded: publicKey) else { print("公钥Base64解码失败") return nil } // 使用GMObjC进行加密,注意:这里公钥是Data类型,而GMObjC提供的方法需要NSData let cipherData = GMSm2Cryptor.encrypt(data, publicKey: pubKeyData as NSData) // 将加密结果转换为Base64字符串 return cipherData.base64EncodedString() } /// 另一种封装:如果输入是字符串,则先转为Data static func encrypt(plainText: String, publicKey: String) -> String? { guard let data = plainText.data(using: .utf8) else { print("字符串转Data失败") return nil } return encrypt(data: data, publicKey: publicKey) } } ``` 注意:上述代码假设传入的公钥已经是去掉了PEM头尾的Base64字符串。如果公钥是PEM格式,我们需要先进行转换。 例如,PEM格式的公钥: ``` -----BEGIN PUBLIC KEY----- MFkwEwYHKoZIzj0CAQYIKoEcz1UBgi0DQgAExfCxYVjD1h0XgFd5t+5qDvWQ ...(其余内容) -----END PUBLIC KEY----- ``` 需要去除头尾和换行,得到连续的Base64字符串。 另外,我们也可以提供从PEM格式提取Base64字符串的方法: ```swift extension String { /// 从PEM格式的公钥字符串中提取Base64内容 func extractBase64FromPEM() -> String? { // 去掉-----BEGIN PUBLIC KEY-----和-----END PUBLIC KEY-----以及换行 let lines = self.components(separatedBy: .newlines) let base64Lines = lines.filter { !$0.hasPrefix("-----") } return base64Lines.joined() } } ``` 这样,在调用加密方法前,可以先将PEM公钥转换为裸的Base64字符串。 但是,请注意:`GMObjC`的`GMSm2Cryptor.encrypt(_:publicKey:)`方法要求传入的公钥是裸的公钥数据(即04||X||Y格式的65字节,或者压缩格式),而通常我们得到的PEM文件是DER编码的,再经过Base64编码。因此,实际上我们可能需要先将PEM文件转换为裸公钥的Data。 然而,`GMObjC`库中有一个方法可以处理PEM格式的公钥字符串:`GMSm2Cryptor.createPublicKey(byPem:)`。我们可以使用这个方法将PEM格式的公钥字符串转换为裸的公钥(Data)。因此,我们修改一下: ```swift static func encrypt(data: Data, publicKeyPem: String) -> String? { // 使用GMObjC的方法将PEM公钥字符串转换为裸的公钥Data guard let pubKeyData = GMSm2Cryptor.createPublicKey(byPem: publicKeyPem) else { print("公钥PEM格式转换失败") return nil } // 现在pubKeyData是裸的公钥(65字节,04开头) let cipherData = GMSm2Cryptor.encrypt(data, publicKey: pubKeyData as NSData) return cipherData.base64EncodedString() } ``` 这样,我们就可以直接传入PEM格式的公钥字符串(包含-----BEGIN PUBLIC KEY-----等)进行加密了。 综上所述,我们提供了两种方式: 1. 直接传入裸公钥的Base64字符串(需要自己提前转换好)。 2. 传入PEM格式的公钥字符串,在函数内部转换。 根据实际需求选择。 注意:由于`GMObjC`是Objective-C库,在Swift中使用时,需要确保桥接文件正确设置,并且项目已经正确链接该库。 另外,加密结果我们返回了Base64编码的字符串。如果需要十六进制,可以修改为: ```swift return cipherData.hexEncodedString() ``` 其中,`hexEncodedString`是一个扩展方法,将Data转换为十六进制字符串。 下面给出这个扩展: ```swift extension Data { func hexEncodedString() -> String { return map { String(format: "%02hhx", $0) }.joined() } } ``` 因此,我们可以根据需求返回Base64或十六进制。 最后,我们还可以提供更多的灵活性,比如让调用者选择输出格式。 考虑到以上因素,我们可以设计一个更通用的封装。 由于时间关系,这里我们只展示一种方式(使用PEM公钥,返回Base64字符串)。其他方式类似。 完整示例: ```swift import Foundation class SM2Crypto { /// SM2加密(公钥加密) /// - Parameters: /// - plainText: 明文(字符串) /// - publicKeyPem: PEM格式的公钥字符串 /// - Returns: Base64编码的加密字符串 static func encrypt(plainText: String, publicKeyPem: String) -> String? { guard let data = plainText.data(using: .utf8) else { print("字符串转Data失败") return nil } return encrypt(data: data, publicKeyPem: publicKeyPem) } /// SM2加密(公钥加密) /// - Parameters: /// - data: 明文数据 /// - publicKeyPem: PEM格式的公钥字符串 /// - Returns: Base64编码的加密字符串 static func encrypt(data: Data, publicKeyPem: String) -> String? { guard let pubKeyData = GMSm2Cryptor.createPublicKey(byPem: publicKeyPem) else { print("公钥PEM格式转换失败") return nil } let cipherData = GMSm2Cryptor.encrypt(data, publicKey: pubKeyData as NSData) return cipherData?.base64EncodedString() } } ``` 注意:上述代码中,`GMSm2Cryptor.encrypt`方法返回的是`Data?`,因此我们使用了可选链。 另外,在实际使用中,可能还需要处理错误,这里只是简单打印。 使用示例: ```swift let pemPublicKey = """ -----BEGIN PUBLIC KEY----- MFkwEwYHKoZIzj0CAQYIKoEcz1UBgi0DQgAEu/L2VdQ1BdX7uq0jF7SX8uU5u4qU 3l+6z5Zm5Yq1ZfQ7X7j7X7j7X7j7X7j7X7j7X7j7X7j7X7j7X7j7X7j7X7j7 -----END PUBLIC KEY----- """ let plainText = "Hello, SM2!" if let encrypted = SM2Crypto.encrypt(plainText: plainText, publicKeyPem: pemPublicKey) { print("加密结果(Base64): \(encrypted)") } else { print("加密失败") } ``` 请确保在项目中正确配置了`GMObjC`,并且桥接文件正确。 如果项目是纯Swift,并且不想使用桥接文件,也可以考虑使用Swift封装SM2算法的其他库,但是目前`GMObjC`是比较成熟的选择。 以上就是在Swift中封装SM2加密算法的基本方法。 §§相关问题§§ 1. 如何在Swift项目中集成GMObjC库? 2. SM2加密算法中,公钥的格式有哪些?如何转换? 3. 除了加密SM2算法还支持哪些功能(如签名、验签)? 4. 在Swift中如何封装SM2的解密算法? 5. 如何处理SM2加密过程中的错误和异常?</think>### Swift实现SM2加密封装方法 在Swift中实现SM2加密算法,推荐使用**GMObjC**库(iOS平台的国密算法封装库)[^2]。以下是完整的封装实现: ```swift import Foundation import GMObjC struct SM2Crypto { /// SM2加密方法 /// - Parameters: /// - plainText: 需要加密的文本 /// - publicKey: PEM格式的公钥字符串 /// - Returns: Base64编码的加密结果 static func encrypt(plainText: String, publicKey: String) -> String? { // 1. 将文本转为Data guard let plainData = plainText.data(using: .utf8) else { print("文本转Data失败") return nil } // 2. 创建公钥对象 guard let pubKey = GMSm2Cryptor.createPublicKey(byPem: publicKey) else { print("公钥创建失败") return nil } // 3. 执行SM2加密 guard let cipherData = GMSm2Cryptor.encrypt(plainData, publicKey: pubKey) else { print("加密过程失败") return nil } // 4. 返回Base64编码结果 return cipherData.base64EncodedString() } } ``` ### 使用示例 ```swift // PEM格式的公钥(实际使用时替换为你的公钥) let publicKeyPEM = """ -----BEGIN PUBLIC KEY----- MFkwEwYHKoZIzj0CAQYIKoEcz1UBgi0DQgAExfCxYVjD1h0XgFd5t+5qDvWQ ... -----END PUBLIC KEY----- """ // 需要加密的文本 let plainText = "敏感数据123ABC" // 执行加密 if let encrypted = SM2Crypto.encrypt(plainText: plainText, publicKey: publicKeyPEM) { print("加密结果: \(encrypted)") } else { print("加密失败") } ``` ### 关键点说明 1. **公钥格式要求**: - 必须使用PEM格式的公钥 - 包含标准的`-----BEGIN PUBLIC KEY-----`头[^3] 2. **加密流程**: ```mermaid graph TD A[输入文本] --> B{转Data} B --> C[创建公钥对象] C --> D[执行SM2加密] D --> E{转Base64} E --> F[输出结果] ``` 3. **依赖管理**: ```ruby # Podfile配置 pod 'GMObjC', '~> 3.2.0' # 国密算法封装库 ``` 4. **错误处理**: - 文本转Data失败 - 公钥格式错误 - 加密过程异常 - 结果编码失败 ### 注意事项 1. **密钥管理**: - 公钥应从安全渠道获取 - 推荐使用硬件安全模块(HSM)存储密钥[^2] 2. **性能优化**: ```swift // 异步加密示例 DispatchQueue.global(qos: .userInitiated).async { let result = SM2Crypto.encrypt(plainText: "...", publicKey: "...") DispatchQueue.main.async { // 更新UI } } ``` 3. **加密结果处理**: - 结果包含ASN.1编码结构 - 如需十六进制输出,替换为: ```swift return cipherData.map { String(format: "%02hhx", $0) }.joined() ``` [^1]: Go语言的SM2加密实现展示了核心加密流程 [^2]: GMObjC提供了iOS平台的国密算法完整实现 [^3]: SM2_sign方法在国密算法中用于数字签名
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值