对称加密与非对称加密详解

文章主要是对加密算法这一块的梳理,包含了对称加密、非对称加密、DH密钥交换和消息摘要算法的关键参数和代码实现,所有代码经过实际测试,可以正常使用。

1、加密算法分类

一般的对称加密可以从加密算法、加密模式(根据模式不同,可能会有初始化向量,用于第一块明文的加密)、填充方式和密钥长度这四个方面确定,非对称加密从加密算法、填充方式和密钥模长三个方面确定。

如下图,其实加密算法大致分为以下几类,对称加密和非对称加密的

2、加密模式分类

加密模式是指在加密过程中,对数据进行分组和加密的方式,下面按对称加密和非对称加密分开说明:

对称加密的加密模式有:ECB、CBC、CFB、OFB、CTR等等,除了ECB模式,其他的模式都需要初始化向量。

  • ECB模式(电子密码本模式):将明文分割为固定长度的块来进行加密,每个块独立使用相同的密钥进行加密生成密文,不存在数据关联,安全性较低。
  • CBC模式(密文分组链接模式):使用前一块的密文与当前块异或,然后加密生成密文,以此产生关联性,提高数据加密的安全性。
  • CFB模式(密码反馈模式):将前一块的密文作为输入加密后,与当前块进行异或运算,产生新的密文块。
  • OFB模式(输出反馈模式):将初始化向量加密,然后与当前块进行异或运算,产生新的密文块,然后将初始化向量加密后的输出再次加密,与下一块进行异或运算,产生下一个密文快。
  • CTR模式(计数器模式):使用一个计数器和一个随机数初始化向量来加密输入的数据,然后按位与或操作将密钥流与明文数据相位异或,以生成密文数据。

非对称加密没有加密模式,允许加密的数据长度和加密的密钥模长有关系,例如1024位,2048位等等;1024位对应128字节,为了防止数据长度不够,引入了PKCS#1填充方式,固定占据了11个字节,所以真实可用的长度是117位

3、填充方式

加密算法使用块加密的方式对明文进行加密,需要将明文划分为固定大小的块进行加密操作。如果明文的长度不足一个块大小,就需要用填充方式将其扩充至一个完整的块大小,否则加密过程会出错。填充方式的目的是使明文长度能够被块大小整除,同时还可以防止攻击者通过分析加密结果来推断明文的长度和内容,增强了加密算法的可靠性和安全性,下面列举一下常见的填充方式。

3.1、PKCS#1_PADDING

指定了RSA 密码加密和签名算法的标准,其定义了一种填充方式来确保输入长度对于RSA算法适合。

  • PKCS1Padding_V1.5是RSA默认的填充方式。

该填充方式会将原数据D(Data)转换为加密块EB(Encryption Block),其组成如下:EB = 00 + BT + PS + 00 + D

00:最开头的00为保留位;

BT:表示标志字节,有三个值0x00、0x01、0x02,其中00和01用于私钥的操作,02用于公钥的加密。

PS:表示填充数据,如果BT是00,则填充数据全是0x00,如果BT是01,填充数据全是0xFF,如果BT是02,填充数据为随机生成的非零字节(这意味着相同的公钥和原数据,每次加密出来的数据也不相同);

00:是原数据开头的字节。

由此可以知道00、BT和00就需要三个字节,而PKCS#1标准建议PS的最小长度为8,所以填充数据至少要占到3+8 = 11个字节,1024模长的密钥一次最多能加密的明文长度为 1024/8 - 11 = 128 - 11 = 117个字节

  • RSA_PKCS1_OAEP_PADDING填充模式是PKCS#1推出的新填充方式。

RSA_PKCS1_PADDING(V1.5)的缺点是无法验证解密的结果的正确性,为了解决这个问题,在密文的计算中加入了原文的hash值,在RAS的填充方案中属于安全性最高的一种,代价则是该填充方式最少需要占据41个字节。

3.2、RSA_NO_PADDING

是一种在RSA加密、解密过程中不进行数据填充的方式。仅支持固定长度的原始数据块,建议使用场景较为有限,并且数据长度不够时,加密时需要手动的补充数据,解密时需要手动裁剪掉补充的数据。

3.3、PKCS#7_PADDING

每个填充字节的值都等于填充的字节数,例如,若需要填充5个字节,则填充的字节为0x05。这种填充方式可用于块大小在1-255的加密算法。

3.4、PKCS#5_PADDING

PKCS#5_PADDING与PKCS#7_PADDING是一致的,支持的块大小不一样。v2.0以及更早版本,说明只能填充8字节块大小的数据。但是在PKCS#5_PADDING v2.1中,将AES加入进来了,所以PKCS#5_PADDING与PKCS#7_PADDING在AES中是一致的,没有区别,都是都支持16字节的块填充。

3.5、ZeroPadding

在原始数据后面填充0,保证数据块长度一致,简单易用,但是存在缺点,比如数据块中可能有本来就是0的数据,会导致导致填充异常,无法区分是原始数据还是填充数据,实际中比较少用。

3.6、ISO10126Padding

该填充方式,随机数填充到块长度的整数倍,填充数为需要填充的字节数,最后一个字节填充填充数。因为填充随机数不可预测,提高了数据的安全性,常用于AES和DES加密。

4、对称加密算法-AES

明确:算法名称、加密模式和填充方式即可,部分加密模式有的需要初始化向量,密钥长度按实际情况确定。

java.crypto包下是一些加解密常用的类,Cipher类里面的注释如下。可以看到,java标准库支持部分的加解密算法、加密模式和填充方式。其不支持CFB的加密模式,也不支持PKC7Padding的填充方式,等等。

如果需要扩展,则需要引入额外的第三方jar包,BouncyCastle就是一个提供了很多哈希算法和加密算法的第三方库,它提供了Java标准库没有的一些算法,依赖如下:

4.1、依赖引入

bcprov-jdk18on,此处的18表示支持jdk1.8及其以上版本,如果jdk是低版本可以自行选择对应版本的依赖。

<dependency>
     <groupId>org.bouncycastle</groupId>
     <artifactId>bcprov-jdk18on</artifactId>
     <version>1.75</version>
</dependency>

4.2、示例代码

下面是AES/CBC/PKCS7Padding的示例代码,可以根据需要修改算法名称例如AES/CFB/PKCS5Padding等等

package org.example;

import org.bouncycastle.jce.provider.BouncyCastleProvider;

import javax.crypto.Cipher;
import javax.crypto.CipherInputStream;
import javax.crypto.CipherOutputStream;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.File;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.StandardOpenOption;
import java.security.Security;

public class AESEncryptFile {

    private static final String key = "aUIJE89aw3no93jl";

    private static final String iv = "1234567890123456";

    static {
        try {
            //增加BouncyCastle
            Security.addProvider(new BouncyCastleProvider());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

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

        //测试密钥长度为16位(128bit)
        String source = "E:\\testFileEncrypt\\对比测试.zip";
        String dest = "E:\\testFileEncrypt\\16_encrypt_file" + ".zip";
        //加密
        encryptFile(source, dest, key.getBytes(StandardCharsets.UTF_8));
        //解密
        String s = "E:\\testFileEncrypt\\16_encrypt_file" + ".zip";
        String d = "E:\\testFileEncrypt\\16_decrypt_file" + ".zip";
        decryptFile(s, d, key.getBytes(StandardCharsets.UTF_8));

    }

    public static void encryptFile(String sourceFilePath,
                                   String destFilePath,
                                   byte[] key) throws Exception {
        File file = new File(sourceFilePath);
        if (file.exists() && file.isFile()) {
            File dest = new File(destFilePath);
            if (dest.exists() || dest.createNewFile()) {
                //指定加密算法
                SecretKeySpec spec = new SecretKeySpec(key, "AES");
                //指定加密模式和填充方式
                Cipher cipher = Cipher.getInstance("AES/CBC/PKCS7Padding");
                byte[] ivBytes = iv.getBytes(StandardCharsets.UTF_8);
                cipher.init(Cipher.ENCRYPT_MODE, spec, new IvParameterSpec(ivBytes));
                try (InputStream in = Files.newInputStream(file.toPath(), StandardOpenOption.READ);
                     OutputStream o = Files.newOutputStream(dest.toPath(), StandardOpenOption.WRITE);
                     CipherInputStream cin = new CipherInputStream(in, cipher)) {
                    byte[] buffer = new byte[2048];
                    int readNum;
                    while ((readNum = cin.read(buffer)) != -1) {
                        o.write(buffer, 0, readNum);
                    }
                }
            }
        }
    }

    public static void decryptFile(String sourceFilePath,
                                   String destFilePath,
                                   byte[] key) throws Exception {
        File file = new File(sourceFilePath);
        if (file.exists() && file.isFile()) {
            File dest = new File(destFilePath);
            if (dest.exists() || dest.createNewFile()) {
                //指定加密算法
                SecretKeySpec spec = new SecretKeySpec(key, "AES");
                //指定加密模式和填充方式
                Cipher cipher = Cipher.getInstance("AES/CBC/PKCS7Padding");
                //调用不同的Cipher的init方法,区分是否需要初化向量(初始化向量的字符串长度,需要和密钥长度相同)
                byte[] ivBytes = iv.getBytes(StandardCharsets.UTF_8);
                cipher.init(Cipher.DECRYPT_MODE, spec, new IvParameterSpec(ivBytes));
                try (InputStream in = Files.newInputStream(file.toPath(), StandardOpenOption.READ);
                     OutputStream o = Files.newOutputStream(dest.toPath(), StandardOpenOption.WRITE);
                     CipherOutputStream cOut = new CipherOutputStream(o, cipher)) {
                    byte[] buffer = new byte[2048];
                    int readNum;
                    while ((readNum = in.read(buffer)) != -1) {
                        cOut.write(buffer, 0, readNum);
                    }
                }
            }
        }
    }
}

5、非对称加密算法-RSA

RAS属于非对称加密,加解密的密钥不相同,分为公私钥,公私钥是配对的。私钥用来解密和签名,公钥用来加密和验签。一般的使用场景是,自己生成公私钥对,自己保存私钥,给别人提供公钥(可以提供公钥base64编码的字符串),别人公钥加密数据,自己私钥解密数据。签名一般用作身份认证,自己提供出去了公钥,调用别人接口时,需要私钥加密(签名),用户通过公钥解密(验签),解密成功验证身份。

5.1、示例代码

package org.example;


import javax.crypto.Cipher;
import java.security.Key;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;

public class SecurityRSA {

    private static String src = "hello RAS 1024";

    public static void main(String[] args) {

        testRSA();

    }

    public static void testRSA() {

        try {

            //初始化密钥对
            KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
            //指定模长
            keyPairGenerator.initialize(1024);
            KeyPair keyPair = keyPairGenerator.generateKeyPair();
            RSAPublicKey rsaPublicKey = (RSAPublicKey) keyPair.getPublic();
            RSAPrivateKey rsaPrivateKey = (RSAPrivateKey) keyPair.getPrivate();

            //公钥和私钥base64字符串
            String publicKeyBase64Str = Base64.getEncoder().encodeToString(rsaPublicKey.getEncoded());
            String privateKeyBase64Str = Base64.getEncoder().encodeToString(rsaPrivateKey.getEncoded());
            System.out.println("公钥Base64字符串:" + publicKeyBase64Str);
            System.out.println("私钥Base64字符串:" + privateKeyBase64Str);
            System.out.println("原始数据 : " + src);

            byte[] result;
            //私钥签名
            Key privateKey = getRSAPrivateKey(privateKeyBase64Str);
            result = encrypt(privateKey, src);
            System.out.println("私钥加密后的Base64字符串 : " + Base64.getEncoder().encodeToString(result));

            //公钥验签
            Key publicKey = getRASPuplicKey(publicKeyBase64Str);
            System.out.println("公钥解密后的数据:" + decrypt(publicKey, result));

            //公钥加密
            publicKey = getRASPuplicKey(publicKeyBase64Str);
            result = encrypt(publicKey, src);
            System.out.println("公钥加密后的Base64字符串 : " + Base64.getEncoder().encodeToString(result));

            //私钥解密
            privateKey = getRSAPrivateKey(privateKeyBase64Str);
            System.out.println("私钥解密后的数据:" + decrypt(privateKey, result));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 获取私钥
     *
     * @param privateKey 私钥Base64编码字符串
     * @return
     * @throws Exception
     */
    public static Key getRSAPrivateKey(String privateKey) throws Exception {
        KeyFactory keyFactory = KeyFactory.getInstance("RSA");
        byte[] bytes = Base64.getDecoder().decode(privateKey);
        PKCS8EncodedKeySpec pkcs8EncodedKeySpec = new PKCS8EncodedKeySpec(bytes);
        return keyFactory.generatePrivate(pkcs8EncodedKeySpec);
    }

    /**
     * 获取公钥
     *
     * @param publicKey 公钥Base64编码字符串
     * @return
     * @throws Exception
     */
    public static Key getRASPuplicKey(String publicKey) throws Exception {
        KeyFactory keyFactory = KeyFactory.getInstance("RSA");
        byte[] bytes = Base64.getDecoder().decode(publicKey);
        X509EncodedKeySpec x509EncodedKeySpec = new X509EncodedKeySpec(bytes);
        return keyFactory.generatePublic(x509EncodedKeySpec);
    }

    /**
     * 加密数据
     *
     * @param key 加密的key
     * @param str 原始数据字符串
     * @return 加密后的比特数组
     * @throws Exception
     */
    public static byte[] encrypt(Key key, String str) throws Exception {
        Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
        cipher.init(Cipher.ENCRYPT_MODE, key);
        return cipher.doFinal(str.getBytes());
    }

    /**
     * 解密数据
     *
     * @param key         解密的key
     * @param encryptData 加密后的比特数组
     * @return 解密后的字符串
     * @throws Exception
     */
    public static String decrypt(Key key, byte[] encryptData) throws Exception {
        Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
        cipher.init(Cipher.DECRYPT_MODE, key);
        byte[] decryptData = cipher.doFinal(encryptData);
        return new String(decryptData);
    }
}

6、DH密钥交换

DH密钥交换和RSA是不同的,DH算法的目的是解决在通信安全之前的密钥配送问题,常规的密钥配送有三种方式:1、线下的约定;2、公钥配置方式,例如RSA的公钥加密密钥,然后私钥解密,这样两方就安全的拿到了密钥。3、DH密钥交换算法,通过公开的参数,两边就可以计算得到相同的密钥,密钥不在网络中传输。

6.1、DH密钥交换过程

原理就是通过公开的大素数g和原根p生成自己的公私钥,然后交换彼此的公钥,最后根据对方的公钥和自己的私钥,计算得到相同的密钥。

6.1、示例代码

package org.example;

import javax.crypto.KeyAgreement;
import javax.crypto.SecretKey;
import javax.crypto.interfaces.DHPrivateKey;
import javax.crypto.interfaces.DHPublicKey;
import javax.crypto.spec.DHParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.security.*;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;

public class SecurityDH {

    /**
     * 全局变量存储服务端和客户端的公私钥
     */
    public static Map<String, Object> keyMap = new HashMap<>(4);

    public static void main(String[] args) throws NoSuchAlgorithmException {
        //服务端初始化公私钥对
        initServiceKey();

        //客户端通过服务端生成的公钥获取大素数p和原根g,生成自己的公私钥对
        initClientKey(((DHPublicKey) keyMap.get("service_public")).getEncoded());

        //服务端根据自身私钥和客户端公钥计算密钥
        byte[] ASecretBytes = getSecretKey(
                ((DHPublicKey) keyMap.get("client_public")).getEncoded(),
                ((DHPrivateKey) keyMap.get("service_private")).getEncoded()
        );
        System.out.println("服务端计算出来的密钥(base64字符串):" + encodeToString(ASecretBytes));

        //客户端根据自身私钥和服务端公钥计算密钥
        byte[] BSecretBytes = getSecretKey(
                ((DHPublicKey) keyMap.get("service_public")).getEncoded(),
                ((DHPrivateKey) keyMap.get("client_private")).getEncoded()
        );
        System.out.println("客户端计算出来的密钥(base64字符串):" + encodeToString(BSecretBytes));

        //因为协商出来的密钥长度,不一定服务加密算法的要求,此处举例,
        //由于SHA-256计算的长度的256位,所以可以利用这点处理成AES算法的密钥(128、256)
        MessageDigest sha256 = MessageDigest.getInstance("SHA-256");
        byte[] clientAESKeyBytes = sha256.digest(BSecretBytes);
        SecretKey clientKey = new SecretKeySpec(clientAESKeyBytes, "AES");
        System.out.println("客户端计算出来的AES(base64字符串):" + encodeToString(clientKey.getEncoded()));

        byte[] serviceAESKeyBytes = sha256.digest(ASecretBytes);
        SecretKey aesKey = new SecretKeySpec(serviceAESKeyBytes, "AES");
        System.out.println("服务端计算出来的AES(base64字符串):" + encodeToString(aesKey.getEncoded()));

    }

    /**
     * 初始化服务端密钥对
     */
    public static void initServiceKey() {
        try {
            //生成服务端的密钥对
            KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("DH");
            keyPairGenerator.initialize(1024, new SecureRandom());
            KeyPair keyPair = keyPairGenerator.generateKeyPair();

            //公钥和私钥
            DHPublicKey publicKey = (DHPublicKey) keyPair.getPublic();
            DHPrivateKey privateKey = (DHPrivateKey) keyPair.getPrivate();

            //将密钥对存储在Map中
            keyMap.put("service_public", publicKey);
            keyMap.put("service_private", privateKey);
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        }
    }

    /**
     * 根据服务端公钥生成客户端密钥对
     *
     * @param key 甲方公钥
     * @return Map 乙方密钥Map
     * @throws Exception
     */
    public static void initClientKey(byte[] key) {
        try {
            //接收到服务端公钥后,解码为PublicKey对象
            KeyFactory keyFactory = KeyFactory.getInstance("DH");
            X509EncodedKeySpec x509KeySpec = new X509EncodedKeySpec(key);
            PublicKey pubKey = keyFactory.generatePublic(x509KeySpec);

            //从服务端公钥中获取大素数p和原根g
            DHParameterSpec dhParameterSpec = ((DHPublicKey) pubKey).getParams();

            //生成客户端的密钥对
            KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("DH");
            keyPairGenerator.initialize(dhParameterSpec);
            KeyPair keyPair = keyPairGenerator.generateKeyPair();

            //公钥和私钥
            DHPublicKey publicKey = (DHPublicKey) keyPair.getPublic();
            DHPrivateKey privateKey = (DHPrivateKey) keyPair.getPrivate();

            //将密钥对存储在Map中
            keyMap.put("client_public", publicKey);
            keyMap.put("client_private", privateKey);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }


    /**
     * 构建密钥
     *
     * @param publicKey  别人的公钥
     * @param privateKey 自己的私钥
     * @return byte[] 本地密钥
     * @throws Exception
     */
    public static byte[] getSecretKey(byte[] publicKey, byte[] privateKey) {
        try {
            //实例化密钥工厂
            KeyFactory keyFactory = KeyFactory.getInstance("DH");

            //初始化公钥,产生公钥
            X509EncodedKeySpec x509KeySpec = new X509EncodedKeySpec(publicKey);
            PublicKey pubKey = keyFactory.generatePublic(x509KeySpec);

            //初始化私钥,产生私钥
            PKCS8EncodedKeySpec pkcs8KeySpec = new PKCS8EncodedKeySpec(privateKey);
            PrivateKey priKey = keyFactory.generatePrivate(pkcs8KeySpec);

            //生成密钥
            KeyAgreement keyAgree = KeyAgreement.getInstance(keyFactory.getAlgorithm());
            keyAgree.init(priKey);
            keyAgree.doPhase(pubKey, true);
            return keyAgree.generateSecret();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    private static String encodeToString(byte[] bytes) {
        return Base64.getEncoder().encodeToString(bytes);
    }

}

 

7、哈希算法

哈希算法一般都是单向的,无法逆向解密,所以常用作消息摘要,防止数据篡改,常见的哈希算法有MD5、SHA-1、SHA-256,下面的代码示例为计算字符串和文件的MD5值。

7.1、示例代码

package org.example;

import cn.hutool.core.util.HexUtil;

import javax.validation.constraints.NotNull;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.security.MessageDigest;

public class HashDemo {

    public static void main(String[] args) throws Exception {
        //计算消息摘要
        byte[] buffer = encodeStr("test1234561", "MD5");
        byte[] buffer2 = encodeFile("E:\\testFileEncrypt\\对比测试.zip", "MD5");
        //比特数组转16进制字符串
        System.out.println(HexUtil.encodeHexStr(buffer));
        System.out.println(HexUtil.encodeHexStr(buffer2));
    }

    /**
     * 计算字符串的消息摘要
     *
     * @param inStr     字符串
     * @param algorithm 消息摘要算法(MD5、SHA-1、SHA-256)
     * @return
     * @throws Exception
     */
    public static byte[] encodeStr(String inStr, @NotNull String algorithm) throws Exception {
        MessageDigest sha = MessageDigest.getInstance(algorithm);
        byte[] byteArray = inStr.getBytes("UTF-8");
        return sha.digest(byteArray);
    }

    /**
     * 计算文件的消息摘要
     *
     * @param filePath  文件路径
     * @param algorithm 消息摘要算法(MD5、SHA-1、SHA-256)
     * @return
     * @throws Exception
     */
    public static byte[] encodeFile(String filePath, @NotNull String algorithm) throws Exception {
        MessageDigest sha = MessageDigest.getInstance(algorithm);
        InputStream inputStream = Files.newInputStream(Paths.get(filePath), StandardOpenOption.READ);
        byte[] bytes = new byte[2048];
        int i;
        while ((i = inputStream.read(bytes)) != -1) {
            sha.update(bytes, 0, i);
        }
        return sha.digest();
    }
}
posted @ 2023-07-12 18:07  浪迹天涯的派大星  阅读(1620)  评论(0编辑  收藏  举报