RSA加密文件


import org.bouncycastle.jce.provider.BouncyCastleProvider;

import javax.crypto.Cipher;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.*;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Scanner;

/**
 * <h3>Title:RSA 加密|解密 文件 </h3><hr/>
 *
 * 1978年,由三位麻省理工学院的学者提出
 * - 于其他算法而言,RSA效率较慢
 * - 私钥与公钥,可互为加解密
 *
 * 本例使用了公钥加密,私钥解密
 *
 * 密钥长度: 默认值 1024,范围 512 ~ 65536
 * 填充方式:
 *      - NoPadding
 *      - PKCS1Padding
 *      - ...略
 * 工作模式: ECB\NONE
 * ( 参考: Java加密与解密的艺术 (第2版) )
 *
 * 密钥的产生:
 * 1. 选择两个保密的大素数 p 和 q(p 跟 q 相差要很大,p-q<n的4次方根)
 * 2. 计算 n=pq,r=(p-1)(q-1),找一个互质的数 e
 * 3. 组成公钥 n、e
 * 4. 找一个数作为私钥,要求跟 e 的乘积除以 r 余数必须为 1
 * 5. 销毁 p、q、r 三个数
 * ( 参考: BV1h5411m78h )
 *
 * TODO 签名与验签
 *
 * @author juner(夜路沏茶人)
 * @date 2022年04月24日 08:35 周日
 * @since JDK_1.8
 * @see
 *      - 参考: RSA实现对文件的加密解密
 *          https://blog.csdn.net/LevenLa/article/details/116705456
 *          - 使用了 私钥加密、公钥解密
 *
 *      - 参考: javac、java命令调用jar包
 *          https://blog.csdn.net/qq_20906903/article/details/107124602
 *          - 测试环境为,单个 class 运行指定 jar
 * @version 0.5.3
 */
@SuppressWarnings("all")
public class RSA5 {

    /** 读取文件,以 1M 为单位 */
    private static final Integer READER_1M = (2<<9) * 1;
    /** 密钥单位大小为,512 */
    private static final Integer UNIT_SIZE = 2<<8;
    /** 默认显示时间格式 */
    private static final String DEFAULT_TIME_SHOW = "yyyy-MM-dd hh:mm:ss";
    /** 默认时间线格式,为保障时间线唯一,此处使用了 SSS 标注唯一 */
    private static final String DEFAULT_TIME_LINE = "yyyyMMdd_hhmm_ss_SSS";

    /** RSA 算法 */
    private static final String RSA = "RSA";
    /** BC 提供 */
    private static final String BC = "BC";
    /** UTF-8 编码 */
    private static final String UTF8 = "UTF-8";

    /** 公钥默认读取路径 */
    private static final String PUBLIC_KEY_FILE = getClassPath(RSA5.class) + "request.key";
    /** 私钥默认读取路径 */
    private static final String PRIVATE_KEY_FILE = getClassPath(RSA5.class) + "response.key";

    /** 私钥 */
    private String prky;
    /** 公钥 */
    private String puky;

    // 加密等级制度
    // - 等级越高,加密越严格(前提是保护要私钥且不能丢失)
    // - 个人电脑加密等级,不要超出 S3072
    // - 加密等级越高,速度越慢。
    // - 推荐使用 S1024(速度较快) 或 S2048(相比较而言更安全)
    // - S3072(速度太慢),其余更高等级加密不推荐

    private static Size S1024;
    private static Size S2048;
    private static Size S3072;
    private static Size S7680;
    private static Size S15360;

    /** 当前加密等级 */
    private static Size thisSize;

    static {

        // 初始化加密等级
        S1024 = Size.create(2 * (2 << 8));
        S2048 = Size.create(4 * (2 << 8));
        S3072 = Size.create(6 * (2 << 8));
        S7680 = Size.create(15 * (2 << 8));
        S15360 = Size.create(30 * (2 << 8));

        // 加密等级通过修改 thisSize 来指定
        thisSize = S1024 ;
    }

    /**
     * 实例化
     */
    public RSA5() {
        this.readKeyPair();
    }

    /**
     * 获取 class 运行路径
     * @param clazz Class类
     * @return 运行路径(由于测试系统为Windows系统,所以此处进行了截取,从1开始截取后面所有内容 )
     */
    public static final String getClassPath(Class clazz) {
        return clazz.getClassLoader()
                    .getResource("")
                    .getPath()
                    .substring(1);
    }

    /**
     * 获取当前时间
     * @return
     */
    private static String getNowDate() {
        Date date = new Date();
        SimpleDateFormat format = new SimpleDateFormat(DEFAULT_TIME_SHOW);
        return format.format(date);
    }

    /**
     * 获取时间线
     * @return
     */
    private static String getLineDate() {
        Date date = new Date();
        SimpleDateFormat format = new SimpleDateFormat(DEFAULT_TIME_LINE);
        return format.format(date);
    }

    /**
     * 生成密钥对
     * @return 密钥对
     */
    public static KeyPair generateKeyPair() {
        try {
            KeyPairGenerator kpg;
            // 添加提供者 BC
            Security.addProvider(new BouncyCastleProvider());
            // 密钥对实例化,指定算法及提供者
            kpg = KeyPairGenerator.getInstance(RSA, BC);
            assert kpg!=null : "key generate error.";
            // 密钥对初始化,指定密钥大小
            kpg.initialize(thisSize.getKeySize());
            // 获取生成的密钥对
            return kpg.generateKeyPair();
        } catch (Exception e) {
            return null;
        }
    }

    /**
     * 判定 密钥文件 是否存在
     * 暂定逻辑为,必须公钥密钥同时存在
     * @return
     */
    private static boolean areKeysPresent() {
        File privateKey = new File(PRIVATE_KEY_FILE);
        File publicKey = new File(PUBLIC_KEY_FILE);
        return privateKey.exists() && publicKey.exists();
    }

    /**
     * 保存密钥对
     * @param keyPair 密钥对
     */
    private static void saveKeyPair(KeyPair keyPair) {
        // 如果密钥对存在,则终止重写
        // - 如果使用其他密钥对,导致密钥不识别,需要更换正确的密钥对或密钥等级
        // - 因此,建议使用固定等级的密钥对
        // - 即,只换密钥对,不变等级
        if(areKeysPresent()) return;
        // 写入密钥对文件
        writeDataBuffer(new File(PUBLIC_KEY_FILE), loadPublicKey(keyPair.getPublic()));
        writeDataBuffer(new File(PRIVATE_KEY_FILE), loadPrivateKey(keyPair.getPrivate()));
    }

    /**
     * 读取密钥对文件,并赋值给相关对象的属性
     */
    private void readKeyPair() {
        this.prky = readDataOnce(PRIVATE_KEY_FILE);
        this.puky = readDataOnce(PUBLIC_KEY_FILE);
    }

    /**
     * 加载私钥
     * @param key 私钥字符串
     * @return 私钥对象
     */
    private static PrivateKey loadPrivateKey(String key) {
        try {
            // 如果读取字符串为空,则直接抛出
            assert key!=null : "private-key is null.";
            // 通过 私钥字节数组,指定 算法,再次生成 私钥对象
            // 私钥 使用 PKCS8EncodedKeySpec
            PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(
                    java.util.Base64.getDecoder().decode((key.getBytes()))
            );
            KeyFactory keyFactory = KeyFactory.getInstance(RSA);
            return keyFactory.generatePrivate(keySpec);
        } catch (Exception e) {
            error(e.getMessage());
            return null;
        }
    }

    /**
     * 加载公钥对象
     * @param key 公钥字符串
     * @return 公钥对象
     */
    private static PublicKey loadPublicKey(String key) {
        try {
            // 如果读取字符串为空,则直接抛出
            if (key == null) {
                throw new RuntimeException("public-key is null.");
            }
            // 通过 公钥字节数组,指定 算法,再次生成 公钥对象
            // 公钥 使用 X509EncodedKeySpec
            X509EncodedKeySpec keySpec = new X509EncodedKeySpec(
                    java.util.Base64.getDecoder().decode((key.getBytes()))
            );
            KeyFactory keyFactory = KeyFactory.getInstance(RSA);
            return keyFactory.generatePublic(keySpec);
        } catch (Exception e) {
            error(e.getMessage());
            return null;
        }
    }

    /**
     * 将 Key 对象转换成 字符串
     * @param key Key
     * @return 字符串
     */
    private static String keyToString(Key key) {
        try {
            byte[] keyBytes = key.getEncoded();
            return new String(java.util.Base64.getEncoder().encode(keyBytes), UTF8);
        } catch(Exception e) {
            e.printStackTrace();
            return null;
        }
    }

    /**
     * 加载私钥字符串
     * @param privateKey 私钥对象
     * @return 私钥字符串
     */
    private static String loadPrivateKey(PrivateKey privateKey) {
        return keyToString(privateKey);
    }

    /**
     * 加载公钥字符串
     * @param publicKey 公钥对象
     * @return 公钥字符串
     */
    private static String loadPublicKey(PublicKey publicKey) {
        return keyToString(publicKey);
    }

    /**
     * 给文件中写入数据
     * @param file 文件对象
     * @param data 数据字节数组
     */
    private static void writeData(
            File file,
            byte[] data
    ) {
        try {
            FileOutputStream out = new FileOutputStream(file);
            assert data != null : "write data is null.";
            out.write(data);
            out.flush();
            out.close();
        } catch (Exception e) {
            error(e.getMessage());
        }
    }

    /**
     * 给文件中写入数据
     * @param file 文件对象
     * @param content 字符串对象
     */
    private static void writeDataBuffer(
            File file,
            String content
    ) {
        try {
            if (file.getParentFile() != null) {
                // 建立多级文件夹
                file.getParentFile().mkdirs();
            }
            // 创建新的空文件
            file.createNewFile();

            FileOutputStream stream =  new FileOutputStream(file);
            // 以 UTF-8 编码方式进行写入
            OutputStreamWriter outputStreamWriter =
                    new OutputStreamWriter(stream, UTF8);
            BufferedWriter writer = new BufferedWriter(outputStreamWriter);
            writer.write(content);
            writer.flush();
            writer.close();
        } catch (Exception e) {
            e.printStackTrace();
        }

    }

    /**
     * 以 UTF-8 读码方式,一次性读取全部数据
     * - 适用于小型文件
     * - 此处使用在读取完整的密钥
     * @param filePath 文件路径
     * @return 文件内容
     */
    private static String readDataOnce(String filePath) {
        try {
            byte[] bytes = Files.readAllBytes(Paths.get(filePath));
            return new String(bytes, StandardCharsets.UTF_8);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

    /**
     * 加密
     *
     * @param source 指定需要加密的文件
     * @param publicKey 公钥字符串
     *                  使用公钥加密,因为私钥比公钥破解难度更高
     * @param filePath 新文件路径及文件名
     */
    public void encrypt(
            String source,
            String publicKey,
            String filePath
    ) {
        try {
            // 将 公钥字符串 加载为 公钥对象
            RSAPublicKey rsaPublicKey = (RSAPublicKey) loadPublicKey(publicKey);
            assert rsaPublicKey != null : "rsa-publik-key is null";

            // 读取需要加密的文件,并将其转换为 字节数组
            File file = new File(source);
            byte[] sourceData = handleReadData(file);

            // 使用公钥进行加密数据
            byte[] data = encryptByPublicKey(sourceData, rsaPublicKey.getEncoded());

            // 将加密后的数据信息写入到文件
            assert data != null: "data is null";
            file = new File(filePath);
            writeData(file, data);
        } catch (Exception e) {
            error(e.getMessage());
        }
    }

    /**
     * 解密
     * @param source 需要解密的文件
     * @param privateKey 私钥
     * @param extend 解密后的新文件
     *               解密时将文件解码后,重新生成新文件
     */
    public void decrypt(
            String source,
            String privateKey,
            String extend
    ) {
        try {
            // 加载私钥对象
            RSAPrivateKey rsaPrivateKey = (RSAPrivateKey) loadPrivateKey(privateKey);

            // 读取文件数据
            File file = new File(source);
            byte[] sourceDataByte = handleReadData(file);

            // 进行解密操作
            assert rsaPrivateKey != null : "rsa-private-key is null";
            assert sourceDataByte != null : "source-data-byte is null";
            byte[] data = decryptByPrivateKey(sourceDataByte, rsaPrivateKey.getEncoded());

            // 写入到新文件
            file = new File(extend);
            writeData(file, data);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 读取数据(核心)
     * @param file 预读取文件
     * @return 文件相关内容的字节
     * @throws IOException 文件访问异常、权限异常
     */
    private byte[] handleReadData(
            File file
    ) throws IOException {

        try {
            // 以 1M 为单位,进行分段读取文件,并重组数据,返回数据相关字节数组
            FileInputStream in = new FileInputStream(file);
            ByteArrayOutputStream byteOut = new ByteArrayOutputStream();
            byte[] tmpBuffer = new byte[READER_1M];
            int count;
            while ((count = in.read(tmpBuffer)) != -1) {
                byteOut.write(tmpBuffer, 0, count);
                tmpBuffer = new byte[READER_1M];
            }
            in.close();
            return byteOut.toByteArray();
        } catch (Exception e) {
            error(e.getMessage());
            return null;
        }
    }

    /**
     * 処理数据(核心)
     * @param cipher cipher配置对象
     * @param fileByte 文件字节数组
     * @param modeIndex 处理模式
     *                  选项一、Cipher.ENCRYPT_MODE
     *                  选项二、Cipher.DECRYPT_MODE
     * @return
     */
    private byte[] handleData(
            Cipher cipher,
            byte[] fileByte,
            int modeIndex
    ) {
        try {
            // 进行模式选择
            // - 根据密钥大小等级,进行模式选择的最值
            int singleMax = modeIndex == Cipher.DECRYPT_MODE
                    ? thisSize.getDecryptSize() : thisSize.getEncryptSize();

            // 获取数据最大长度,并作其余初始化
            int inputLen = fileByte.length;
            ByteArrayOutputStream out = new ByteArrayOutputStream();
            int offSet = 0;
            byte[] cache;
            int i = 0;

            // 分段処理进行 加密/解密 操作
            while (inputLen - offSet > 0) {
                if (inputLen - offSet > singleMax) {
                    cache = cipher.doFinal(fileByte, offSet, singleMax);
                } else {
                    cache = cipher.doFinal(fileByte, offSet, inputLen - offSet);
                }
                out.write(cache, 0, cache.length);
                i++;
                offSet = i * singleMax;
            }

            // 数据转换、关闭资源、并返回已处理数据
            byte[] data = out.toByteArray();
            out.close();
            return data;
        } catch (Exception e) {
            error(e.getMessage());
            return null;
        }
    }

    /**
     * 通过 公钥 进行加密操作
     *
     * @param fileByte 文件字节数组
     * @param publicKeyByte 公钥字节数组
     * @return 加密文件字节数组
     * @throws Exception 异常
     */
    private byte[] encryptByPublicKey(
            byte[] fileByte,
            byte[] publicKeyByte
    ) throws Exception {

        // 指定加密算法
        KeyFactory keyFactory = KeyFactory.getInstance(RSA);

        // 生成公钥
        X509EncodedKeySpec x509KeySpec = new X509EncodedKeySpec(publicKeyByte);
        Key publicKey = keyFactory.generatePublic(x509KeySpec);

        // 指定配置并初始化
        Cipher cipher = Cipher.getInstance(keyFactory.getAlgorithm());
        cipher.init(Cipher.ENCRYPT_MODE, publicKey);

        // 进行加密処理
        return handleData(cipher, fileByte, Cipher.ENCRYPT_MODE);
    }

    /**
     * 通过 私钥 进行加密操作
     *
     * @param dataStr 加密文件字节数组
     * @param privateKeyByte 私钥字节数组
     * @return 文件字节数组
     * @throws Exception 异常
     */
    private byte[] decryptByPrivateKey(
            byte[] dataStr,
            byte[] privateKeyByte
    ) throws Exception {

        // 指定加密算法
        KeyFactory keyFactory = KeyFactory.getInstance(RSA);

        // 生成私钥
        PKCS8EncodedKeySpec pkcs8KeySpec = new PKCS8EncodedKeySpec(privateKeyByte);
        Key privateKey = keyFactory.generatePrivate(pkcs8KeySpec);

        // 指定配置并初始化
        Cipher cipher = Cipher.getInstance(keyFactory.getAlgorithm());
        cipher.init(Cipher.DECRYPT_MODE, privateKey);

        // 进行解密処理
        return handleData(cipher, dataStr, Cipher.DECRYPT_MODE);
    }

    // TODO 临时,错误提示
    private static void error(String message) {
        System.err.println(" ");
        System.err.println(" - error: " + message + " (" + getNowDate() +")");
        System.err.println(" ");
    }

    // TODO 临时,消息输出
    private static void message(String message) {
        System.out.println(" ");
        System.out.println(" - info: " + message + " (" + getNowDate() +")");
        System.out.println(" ");
    }

    /**
     * TODO 标记,临时
     * 操作
     * @param sc 输入操作
     */
    private static void options(Scanner sc) {

        System.out.println("---------------------------");
        System.out.println("input options number: ");
        System.out.println("1: encrypt file ");
        System.out.println("2: decrypt file");
        System.out.println("x: exit");
        System.out.println("ls: show path");
        System.out.println("---------------------------");

        String content = sc.nextLine();
        switch (content) {
            case "1": opt1(sc); break;
            case "2": opt2(sc); break;
            case "x": sc.close(); break;

            case "ls":
                System.out.println(getClassPath(RSA5.class));
                options(sc);
                break;
            default:
                error("invalid options");
                options(sc);
                break;
        }

    }

    /**
     * TODO 标记,临时
     * 操作一,进行加密
     * @param sc 输入操作
     */
    private static void opt1(Scanner sc) {

        System.out.println("---------------------------");
        System.out.println("input file name: ");
        System.out.println("---------------------------");

        String content = sc.nextLine().trim();
        if(!"".equals(content)) {

            if("=x".equalsIgnoreCase(content)) {
                options(sc);
            }

            if(new File(getClassPath(RSA5.class) + content).exists()) {
                RSA5 service = new RSA5();
                service.encrypt(getClassPath(RSA5.class) + content, service.puky,getClassPath(RSA5.class) + content + ".sign");
                message("encrypt final '" + content + "' to " + content + ".sign ");
            } else {
                error("not found '" + content + "'. ");
                opt1(sc);
            }
        } else {
            opt1(sc);
        }

    }

    /**
     * TODO 标记,临时
     * 操作二,进行解密
     * @param sc 输入操作
     */
    private static void opt2(Scanner sc) {

        System.out.println("---------------------------");
        System.out.println("input file name (.sign): ");
        System.out.println("---------------------------");

        String content = sc.nextLine().trim();
        if(!"".equals(content)) {
            if("=x".equalsIgnoreCase(content)) {
                options(sc);
            }

            if(new File(getClassPath(RSA5.class) + content).exists()) {
                String lineDate = getLineDate();
                RSA5 service = new RSA5();
                service.decrypt(getClassPath(RSA5.class) + content, service.prky, getClassPath(RSA5.class) + lineDate + ".json");

                message(content + "' to " + lineDate + ".json ");
            } else {
                error("not found '" + content + "'.");
                opt2(sc);
            }
        } else {
            opt2(sc);
        }

    }

    public static void main(String[] args) throws Exception {

        // 测试说明
        // - 平级文件: bcprov-jdk15on-1.60.jar
        // - 加密操作: 会在平级目录下生成 密钥对文件
        //                            PUBLIC_KEY_FILE
        //                            PRIVATE_KEY_FILE
        // - 解密操作: 平级目录下必须存在可用的 密钥对文件,且密钥对文件等级与配置等级一致

        // 测试
        // - 生成二进制文件
        //      javac -encoding UTF-8 -classpath .;bcprov-jdk15on-1.60.jar RSA5.java
        // - 执行
        //      java -ea -classpath .;bcprov-jdk15on-1.60.jar RSA5

        // 生成密钥对文件,并保存;如果存在,则不生成
        KeyPair keyPair = generateKeyPair();
        saveKeyPair(keyPair);

        // 实例化并读取密钥文件
        RSA5 service = new RSA5();
        service.readKeyPair();
        message(" - lv " + thisSize.getKeySize());

        // 操作
        Scanner sc = new Scanner(System.in);
        options(sc);

    }

}

/**
 * 密钥大小等级
 */
@SuppressWarnings("all")
class Size {

    /** 密钥大小 */
    private Integer keySize;
    /** 最大加密大小 */
    private Integer encryptSize;
    /** 最大解密大小 */
    private Integer decryptSize;

    // --------------------------------------- 直接创建并配置

    /**
     * 创建配置 密钥大小等级
     * @param keySize 密钥大小
     * @return 密钥配置
     */
    public static Size create(Integer keySize) {
        // 大于等于 512
        assert keySize>=512;
        // 小于等于 65535
        assert keySize<=65535;
        // 必须是 64 的整数倍
        assert keySize%64==0;

        return new Size(keySize)
                .setEncryptSize(keySize/8 - 11)
                .setDecryptSize(keySize/8);
    }

    // --------------------------------------- 禁止通过 new 实例化对象

    private Size(Integer keySize) {
        this.keySize = keySize;
    }

    // --------------------------------------- 获取密钥大小等级配置

    public Integer getKeySize() {
        return keySize;
    }

    public Integer getEncryptSize() {
        return encryptSize;
    }

    public Integer getDecryptSize() {
        return decryptSize;
    }

    // --------------------------------------- 禁止通过 set 方法改变配置

    private Size setKeySize(Integer keySize) {
        this.keySize = keySize;
        return this;
    }

    private Size setEncryptSize(Integer encryptSize) {
        this.encryptSize = encryptSize;
        return this;
    }

    private Size setDecryptSize(Integer decryptSize) {
        this.decryptSize = decryptSize;
        return this;
    }

}
  • 2
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
RSA加密算法的实现涉及到大数运算和加密算法的细节,比较复杂。下面是一个简单的RSA加密文件的C语言实现示例: ```c #include <stdio.h> #include <stdlib.h> #include <string.h> #include <openssl/rsa.h> #include <openssl/pem.h> #define KEY_LENGTH 2048 #define PUB_EXP 3 #define FILENAME "rsa_key.pem" int main() { RSA *rsa = NULL; FILE *fp = NULL; char plaintext[256]; char ciphertext[256]; // 生成RSA密钥对 rsa = RSA_generate_key(KEY_LENGTH, PUB_EXP, NULL, NULL); if (rsa == NULL) { printf("Failed to generate RSA key pair.\n"); return 1; } // 保存RSA公钥到文件 fp = fopen(FILENAME, "w"); if (fp == NULL) { printf("Failed to open file %s.\n", FILENAME); return 1; } PEM_write_RSAPublicKey(fp, rsa); fclose(fp); // 加载RSA私钥 fp = fopen(FILENAME, "r"); if (fp == NULL) { printf("Failed to open file %s.\n", FILENAME); return 1; } rsa = PEM_read_RSAPublicKey(fp, NULL, NULL, NULL); fclose(fp); // 获取待加密的明文 printf("请输入待加密的明文:"); fgets(plaintext, sizeof(plaintext), stdin); // RSA加密 int len = RSA_public_encrypt(strlen(plaintext), (unsigned char*)plaintext, (unsigned char*)ciphertext, rsa, RSA_PKCS1_PADDING); if (len == -1) { printf("Failed to encrypt plaintext.\n"); return 1; } // 输出密文 printf("密文为:"); for (int i = 0; i < len; i++) { printf("%02X", (unsigned char)ciphertext[i]); } printf("\n"); RSA_free(rsa); return 0; } ``` 这段代码使用了OpenSSL库来进行RSA加密操作。首先,它生成一个RSA密钥对,然后将公钥保存到文件中。接下来,它加载公钥,并获取待加密的明文。最后,使用RSA公钥对明文进行加密,并输出加密后的密文。 你可以根据需要修改代码中的KEY_LENGTH和FILENAME参数,以及根据实际情况进行错误处理和其他操作。 希望这个示例对你有所帮助!如果有任何疑问,请随时提问。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值