对称加密与非对称加密详解
文章主要是对加密算法这一块的梳理,包含了对称加密、非对称加密、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(); } }
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通