常见的加解密算法
6576字约22分钟
2024-08-08
Base64
介绍
Base64
是一种二进制到文本的编码方式,是为了解决各系统以及传输协议中二进制不兼容问题而产生的。需要注意的是 Base64
不是加密算法,其仅仅是一种编码方式,算法也是公开的,所以不能依赖它进行加密
Base64 使用场景
任何需要将二进制数据转为可读字符的场景,比如:系统生成验证码图片,会将文件二进制流转换为 Base64
编码字符串返回
编码/解码
使用 JDK8
中自带的 Base64
编码/解码工具类
public static void main(String[] args) {
Charset CHARSET_UTF_8 = StandardCharsets.UTF_8;
// 字符串编码
Base64.getEncoder().encodeToString("https://maruinotes.com/".getBytes(CHARSET_UTF_8));
// 字符串解码
Base64.getDecoder().decode("aHR0cHM6Ly9tYXJ1aW5vdGVzLmNvbS8=".getBytes(CHARSET_UTF_8));
// URL 编码,URL 对反斜线 / 有特殊的意义,因此 URL 编码需要替换掉它,使用下划线替换
Base64.getUrlEncoder().encodeToString("https://maruinotes.com/?".getBytes(CHARSET_UTF_8));
// URL 解码
Base64.getUrlDecoder().decode("aHR0cHM6Ly9tYXJ1aW5vdGVzLmNvbS8_".getBytes(CHARSET_UTF_8));
// 流文件编码
Base64.getEncoder().wrap(outputStream);
// 流文件解码
Base64.getDecoder().wrap(inputStream);
// MIME 编码,每一行输出不超过76个字符,而且每行以“\r\n”符结束
Base64.getMimeEncoder().encodeToString("https://maruinotes.com/".getBytes(CHARSET_UTF_8));
// MIME 解码
Base64.getMimeDecoder().decode("aHR0cHM6Ly9tYXJ1aW5vdGVzLmNvbS8=".getBytes(CHARSET_UTF_8));
}
工具类
/**
* @ClassName Base64Util
* @Desciption Base64 工具类
* @Author MaRui
* @Date 2022/9/26 16:02
* @Version 1.0
*/
public class Base64Util {
public static final Charset CHARSET_UTF_8 = StandardCharsets.UTF_8;
/**
* base64 编码
* @param source 被编码的 base64 字符串
* @return 编码后的字符串
*/
public static String encode(String source) {
return encode(source, CHARSET_UTF_8);
}
/**
* base64 编码,指定字符集
* @param source 被编码的 base64 字符串
* @param charset 字符集
* @return 编码后的字符串
*/
public static String encode(String source, Charset charset) {
if (source == null) {
return null;
}
byte[] bytes = source.getBytes(charset);
return Base64.getEncoder().encodeToString(bytes);
}
/**
* base64 解码
* @param source 被解码的 base64 字符串
* @return 被编码前的字符串
*/
public static String decodeStr(String source) {
return decodeStr(source, CHARSET_UTF_8);
}
/**
* base64 解码
* @param source 被解码的 base64 字符串
* @param charset 字符集
* @return 被编码前的字符串
*/
public static String decodeStr(String source, Charset charset) {
byte[] bytes = source.getBytes(charset);
byte[] decode = Base64.getDecoder().decode(bytes);
return new String(decode, charset);
}
}
Base64 原理分析
Base64 编码原理
1、将原始数据每三个字节作为一组,每个字节是
8
个bit,一共是24
个 bit2、将
24
个bit
分为四组,每组6
个bit
3、在每组前面加补
00
,将其补全成四组8
个bit
(后续会介绍Base64
字符串末尾的=
怎么来的)4、根据
Base64
码表得到扩展后每个字节的对应符号
| 二进制 | 十进制 | 名称/意义 | 二进制 | 十进制 | 名称/意义 | | --------- | ------ | ----------------------------------- | --------- | ------ | ----------- | --------- | ------ | --------- | | 0000 0000 | 0 | 空字符(Null) | 0100 0000 | 64 | @ | | 0000 0001 | 1 | 标题开始 | 0100 0001 | 65 | A | | 0000 0010 | 2 | 本文开始 | 0100 0010 | 66 | B | | 0000 0011 | 3 | 本文结束 | 0100 0011 | 67 | C | | 0000 0100 | 4 | 传输结束 | 0100 0100 | 68 | D | | 0000 0101 | 5 | 请求 | 0100 0101 | 69 | E | | 0000 0110 | 6 | 确认回应 | 0100 0110 | 70 | F | | 0000 0111 | 7 | 响铃 | 0100 0111 | 71 | G | | 0000 1000 | 8 | 退格 | 0100 1000 | 72 | H | | 0000 1001 | 9 | 水平定位符号 | 0100 1001 | 73 | I | | 0000 1010 | 10 | 换行键 | 0100 1010 | 74 | J | | 0000 1011 | 11 | 垂直定位符号 | 0100 1011 | 75 | K | | 0000 1100 | 12 | 换页键 | 0100 1100 | 76 | L | | 0000 1101 | 13 | 归位键 | 0100 1101 | 77 | M | | 0000 1110 | 14 | 取消变换(Shift out) | 0100 1110 | 78 | N | | 0000 1111 | 15 | 启用变换(Shift in) | 0100 1111 | 79 | O | | 0001 0000 | 16 | 跳出数据通讯 | 0101 0000 | 80 | P | | 0001 0001 | 17 | 设备控制一(XON 启用软件速度控制) | 0101 0001 | 81 | Q | | 0001 0010 | 18 | 设备控制二 | 0101 0010 | 82 | R | | 0001 0011 | 19 | 设备控制三(XOFF 停用软件速度控制) | 0101 0011 | 83 | S | | 0001 0100 | 20 | 设备控制四 | 0101 0100 | 84 | T | | 0001 0101 | 21 | 确认失败回应 | 0101 0101 | 85 | U | | 0001 0110 | 22 | 同步用暂停 | 0101 0110 | 86 | V | | 0001 0111 | 23 | 区块传输结束 | 0101 0111 | 87 | W | | 0001 1000 | 24 | 取消 | 0101 1000 | 88 | X | | 0001 1001 | 25 | 连接介质中断 | 0101 1001 | 89 | Y | | 0001 1010 | 26 | 替换 | 0101 1010 | 90 | Z | | 0001 1011 | 27 | 跳出 | 0101 1011 | 91 | [ | | 0001 1100 | 28 | 文件分割符 | 0101 1100 | 92 | \ | | 0001 1101 | 29 | 组群分隔符 | 0101 1101 | 93 | ] | | 0001 1110 | 30 | 记录分隔符 | 0101 1110 | 94 | ^ | | 0001 1111 | 31 | 单元分隔符 | 0101 1111 | 95 | _ | | 0010 0000 | 32 | (空格)(␠) | 0110 0000 | 96 | ` | | 0010 0001 | 33 | ! | 0110 0001 | 97 | a | | 0010 0010 | 34 | " | 0110 0010 | 98 | b | | 0010 0011 | 35 | # | 0110 0011 | 99 | c | | 0010 0100 | 36 | $ | 0110 0100 | 100 | d | | 0010 0101 | 37 | % | 0110 0101 | 101 | e | | 0010 0110 | 38 | & | 0110 0110 | 102 | f | | 0010 0111 | 39 | ' | 0110 0111 | 103 | g | | 0010 1000 | 40 | ( | 0110 1000 | 104 | h | | 0010 1001 | 41 | ) | 0110 1001 | 105 | i | | 0010 1010 | 42 | * | 0110 1010 | 106 | j | | 0010 1011 | 43 | + | 0110 1011 | 107 | k | | 0010 1100 | 44 | , | 0110 1100 | 108 | l | | 0010 1101 | 45 | - | 0110 1101 | 109 | m | | 0010 1110 | 46 | . | 0110 1110 | 110 | n | | 0010 1111 | 47 | / | 0110 1111 | 111 | o | | 0011 0000 | 48 | 0 | 0111 0000 | 112 | p | | 0011 0001 | 49 | 1 | 0111 0001 | 113 | q | | 0011 0010 | 50 | 2 | 0111 0010 | 114 | r | | 0011 0011 | 51 | 3 | 0111 0011 | 115 | s | | 0011 0100 | 52 | 4 | 0111 0100 | 116 | t | | 0011 0101 | 53 | 5 | 0111 0101 | 117 | u | | 0011 0110 | 54 | 6 | 0111 0110 | 118 | v | | 0011 0111 | 55 | 7 | 0111 0111 | 119 | w | | 0011 1000 | 56 | 8 | 0111 1000 | 120 | x | | 0011 1001 | 57 | 9 | 0111 1001 | 121 | y | | 0011 1010 | 58 | : | 0111 1010 | 122 | z | | 0011 1011 | 59 | ; | 0111 1011 | 123 | { | | 0011 1100 | 60 | < | 0111 1100 | 124 | | | | 0011 1101 | 61 | = | 0111 1101 | 125 | } | | 0011 1110 | 62 | > | 0111 1110 | 126 | ~ | | 0011 1111 | 63 | ? | 0111 1111 | 127 | 删除 |
二进制 | 十进制 | 对应字符 | 二进制 | 十进制 | 对应字符 |
---|---|---|---|---|---|
0000 0000 | 0 | A | 0010 0000 | 32 | g |
0000 0001 | 1 | B | 0010 0001 | 33 | h |
0000 0010 | 2 | C | 0010 0010 | 34 | i |
0000 0011 | 3 | D | 0010 0011 | 35 | j |
0000 0100 | 4 | E | 0010 0100 | 36 | k |
0000 0101 | 5 | F | 0010 0101 | 37 | l |
0000 0110 | 6 | G | 0010 0110 | 38 | m |
0000 0111 | 7 | H | 0010 0111 | 39 | n |
0000 1000 | 8 | I | 0010 1000 | 40 | o |
0000 1001 | 9 | J | 0010 1001 | 41 | p |
0000 1010 | 10 | K | 0010 1010 | 42 | q |
0000 1011 | 11 | L | 0010 1011 | 43 | r |
0000 1100 | 12 | M | 0010 1100 | 44 | s |
0000 1101 | 13 | N | 0010 1101 | 45 | t |
0000 1110 | 14 | O | 0010 1110 | 46 | u |
0000 1111 | 15 | P | 0010 1111 | 47 | v |
0001 0000 | 16 | Q | 0011 0000 | 48 | w |
0001 0001 | 17 | R | 0011 0001 | 49 | x |
0001 0010 | 18 | S | 0011 0010 | 50 | y |
0001 0011 | 19 | T | 0011 0011 | 51 | z |
0001 0100 | 20 | U | 0011 0100 | 52 | 0 |
0001 0101 | 21 | V | 0011 0101 | 53 | 1 |
0001 0110 | 22 | W | 0011 0110 | 54 | 2 |
0001 0111 | 23 | X | 0011 0111 | 55 | 3 |
0001 1000 | 24 | Y | 0011 1000 | 56 | 4 |
0001 1001 | 25 | Z | 0011 1001 | 57 | 5 |
0001 1010 | 26 | a | 0011 1010 | 58 | 6 |
0001 1011 | 27 | b | 0011 1011 | 59 | 7 |
0001 1100 | 28 | c | 0011 1100 | 60 | 8 |
0001 1101 | 29 | d | 0011 1101 | 61 | 9 |
0001 1110 | 30 | e | 0011 1110 | 62 | + |
0001 1111 | 31 | f | 0011 1111 | 63 | / |
下面我们试着将 bug
进行一下 Base64
编码(需要注意的是图中未画出来的内容:分组之后在每组前面补 00
,补全成 8
个 bit
)
Base64 编码时是将原始数据每三个字节作为一组,那如果最后只有一个字节了,将补 4
个 0
位,编码成 2
个 Base64
字符,然后补两个 =
Base64 编码时是将原始数据每三个字节作为一组,那如果最后只有两个字节了,将补 2
个 0
位,编码成 3
个 Base64
字符,然后补一个 =
Base64 Url
如果编码后的数据放置在 url
上,建议使用 Base64URL
,因为 +、/、=
这三个字符在 url
有特殊意义,Base64Url
编码会将 +、/
替换成 -、_
,并且将 =
去掉(PS:=
会去掉这个点未测试出来,可能与 Base64Url
实现不同有关)
URLEncode
URLEncode
是一种将特殊字符转换成百分号编码的方法,以便浏览器和服务器之间能够正确地处理它们
该方法会将某些字符替换为由 '%' 和其后面的两个十六进制数字所组成的编码。这些字符包括字母、数字、下划线、连字符、句点以及某些保留字符
特殊字符 | 编码结果 |
---|---|
+ | %2B |
= | %3D |
# | %23 |
... | ... |
踩坑点:系统生成的一些令牌数据,前端获取放置在浏览器中访问没有问题,手动调用接口获取令牌数据放置在浏览器中访问出错,可能刚好令牌数据中有的字符需要
URLEncode
导致的
MD5 算法
介绍
MD5
的全称是信息-摘要算法5(Message-Digest Algorithm 5
),用于确保信息传输完整一致。一种被广泛使用的密码散列函数,可以产生一个 128
位的散列值(常见的是用 32
位的 16
进制表示,比如:a36388424eb9e988e9064e06dafda0d5
)
MD5
算法具有以下特点:
1、压缩性:任意长度的数据,算出的
MD5
值长度都是固定的2、容易计算:从原数据计算出
MD5
值很容易3、抗修改性:对原数据进行任何改动,哪怕只修改
1
个字节,所得到的MD5
值都有很大区别4、强抗碰撞:已知原数据和其
MD5
值,想找到一个具有相同MD5
值的数据(即伪造数据)是非常困难的
虽说
MD5
不可逆,但我们在使用的时候,需要加盐,不然就有很低级的安全问题
某些MD5
破解网站,把常用的密码先MD5
处理,并将数据存储起来,然后跟需要查询的MD5
结果匹配,这时就有可能通过匹配的MD5
得到明文,所以有些简单的MD5
码是可以反查到加密前的原文。为了让MD5
码更加安全,涌现了很多其他方法,如加盐。 盐要足够长足够乱,得到的MD5
码就很难查到
使用场景
密码加密存储:服务端只记录用户密码的
MD5
值,验证用户身份时,只需要将用户输入的密码做一下MD5
,然后与记录的MD5
的值作对比即可验证其密码的合法性数字签名:比如为了防止第三方进行篡改,发布程序时公开这个程序文件的
MD5
码,别人任何地方下载的程序文件,只需要对比一下文件的MD5
是否与公开的一致,就可以判断是否被第三方修改过文件完整性验证:比如下载一个文件时,服务器返回的信息中包括这个文件的
MD5
码,在本地下载完毕时进行MD5
,将两个MD5
值进行比较,如果一致则说明文件完整没有丢包文件上传:比如百度云实现的秒传,就是对比你上传的文件
MD5
在百度服务器是否已经存在了
工具类
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>1.15</version>
</dependency>
import org.apache.commons.codec.digest.DigestUtils;
/** MD5盐值(盐值自己随机生成,相同的用途就固定用一个盐值) */
String salt = "GWONIiFDpT3iDvOqI5bjA3aW";
// 待 MD5 字符串
String source = "https://maruinotes.com/";
String source = salt + url;
System.out.println(DigestUtils.md5Hex(source));
// a36388424eb9e988e9064e06dafda0d5
SHA
介绍
SHA
的全称是安全散列算法(Secure Hash Algorithm
)。与 MD5
算法类似,SHA
算法也是一种信息摘要生成算法
SHA-1
是第一代 SHA
算法标准;SHA-2
包括 SHA-224、SHA-256、SHA-384、SHA-512、SHA-512/224、SHA-512/256
被统称为 SHA-2
;SHA-3
包括 SHA3-224、SHA3-256、SHA3-384、SHA3-512
SHA-256
:可以生成长度256bit
的信息摘要SHA-224
:SHA-256
的“阉割版”,可以生成长度224bit
的信息摘要SHA-512
:可以生成长度512bit
的信息摘要SHA-384
:SHA-512
的“阉割版”,可以生成长度384bit
的信息摘要
信息摘要越长,发生碰撞的几率就越低,破解的难度就越大。但同时,耗费的性能和占用的空间也就越高
MD5 与 SHA-1 算法已被攻破,不应该被用于新的用途;SHA-2 与 SHA-3 还是安全的,可以使用
SHA-1 与 MD5 对比
MD5
的摘要的长度尽量是128bit
,SHA-1
摘要长度160bit
。多出32bit
就意味着不同明文的碰撞几率降低了2^32 = 324294967296
倍。由于
SHA-1
摘要比MD5
摘要长,因而SHA-1
生成摘要的性能比MD5
略低
工具类
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>1.15</version>
</dependency>
import org.apache.commons.codec.digest.DigestUtils;
// 待 sha 字符串
String source = "https://maruinotes.com/";
// SHA-1
System.out.println(DigestUtils.sha1Hex(source));
// e3d153976b9cdb44b0c3c3bb198ad00fb0cd16fe
// SHA-256
System.out.println(DigestUtils.sha256Hex(source));
// b879e41efcc9872dfbc755c495af93861d4b816bf8f761bf438fe8ac87ba54b2
// SHA-512
System.out.println(DigestUtils.sha512Hex(source));
// 775d5c934affdeee2aefc18ae979634cc7982f21e5e416ae4765ebcd3aee3be2b53fae6efe29a944f964f2c87999a1b29188e48ecc072162a16b27219d0f623c
// SHA-384
System.out.println(DigestUtils.sha384Hex(source));
// dde653e348b93c34d30ba9e3b59409dba9a554e6c6614007ef57f8839531f03fc293041ad93f42cb96e577442fdb3a0e
DES
介绍
DES
是一个分组加密算法,就是将明文分组进行加密,每次按顺序取明文一部分,一个典型的 DES
以 64
位为分组,加密解密用算法相同。它的密钥长度为 56
位,因为每组第 8
位是用来做奇偶校验,密钥可以是任意 56
位的数,保密性依赖于密钥
DES
算法的有效密钥长度为56
位,实际上,DES
使用了64
位的密钥,其中有8
位用于奇偶校验
工具类
import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.Key;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* @ClassName AESUtil
* @Desciption AES 加解密工具类
* @Author MaRui
* @Date 2023/12/8 11:30
* @Version 1.0
*/
public class DESUtil {
private static final String ALGORITHM = "DES";
private static final String DES_CBC_PADDING = "DES/CBC/PKCS5Padding";
/**
* Aes加密(CBC工作模式)
* @param content 待加密内容
* @param key 秘钥
* @param offset 偏移量
* @return
*/
public static String encodeByCBC(String content, String key, String offset) {
try {
//获取SecretKey对象,也可以使用getSecretKey()方法
Key secretKey = new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), ALGORITHM);
//获取指定转换的密码对象Cipher(参数:算法/工作模式/填充模式)
Cipher cipher = Cipher.getInstance(DES_CBC_PADDING);
//创建向量参数规范也就是初始化向量
IvParameterSpec ips = new IvParameterSpec(offset.getBytes(StandardCharsets.UTF_8));
//用密钥和一组算法参数规范初始化此Cipher对象(加密模式)
cipher.init(Cipher.ENCRYPT_MODE, secretKey, ips);
//执行加密操作
return Base64.getEncoder().encodeToString(cipher.doFinal(content.getBytes(StandardCharsets.UTF_8)));
} catch (Exception e) {
Logger.getLogger(AESUtil.class.getName()).log(Level.SEVERE, null, e);
}
return null;
}
/**
* Aes解密(CBC工作模式)
* @param content 待解密内容
* @param key 秘钥
* @param offset 偏移量
* @return
*/
public static String decodeByCBC(String content, String key, String offset) {
try {
// 偏移量
IvParameterSpec ips = new IvParameterSpec(offset.getBytes(StandardCharsets.UTF_8));
// 获取SecretKey对象,也可以使用getSecretKey()方法
SecretKey secretKey = new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), ALGORITHM);
// 获取指定转换的密码对象Cipher(参数:算法/工作模式/填充模式)
Cipher cipher = Cipher.getInstance(DES_CBC_PADDING);
// 用密钥和一组算法参数规范初始化此Cipher对象(加密模式)
cipher.init(Cipher.DECRYPT_MODE, secretKey, ips);
// 执行解密操作
byte[] bytes = cipher.doFinal(Base64.getDecoder().decode(content));
return new String(bytes, StandardCharsets.UTF_8);
} catch (Exception e) {
Logger.getLogger(AESUtil.class.getName()).log(Level.SEVERE, null, e);
}
return null;
}
public static void main(String[] args) throws NoSuchAlgorithmException {
// 密钥生成器
KeyGenerator keyGen = KeyGenerator.getInstance(ALGORITHM);
// 初始化密钥生成器
keyGen.init(56);
// 生成密钥
SecretKey secretKey = keyGen.generateKey();
// 密钥字节数组
byte[] keyByte = secretKey.getEncoded();
// 秘钥 TJsm7JLy
String key = Base64.getEncoder().encodeToString(keyByte).substring(0, 8);
// 生成密钥
SecretKey secretKey2 = keyGen.generateKey();
// 密钥字节数组
byte[] keyByte2 = secretKey2.getEncoded();
// 偏移量 +7X3NCbl
String offset = Base64.getEncoder().encodeToString(keyByte2).substring(0, 8);
System.out.println("秘钥:" + key);
System.out.println("偏移量:" + offset);
String content = "https://maruinotes.com/";
// 加密
String encodeStr = encodeByCBC(content, key, offset);
System.out.println(encodeStr);
// 8yaX5XQjv/xADc2NpJKgKRwTswn6lPfP
// 解密
String decodeStr = decodeByCBC(encodeStr, key, offset);
System.out.println(decodeStr);
// https://maruinotes.com/
}
}
AES
介绍
AES(Advanced Encryption Standard)
又称 Rijndael
加密法。这个标准用来替代原先的 DES(Data Encryption Standard)
,比 DES
有更高的安全性
AES
算法采用固定长度的密钥(128 bits、192 bits 或256 bits
)来加密和解密数据块,加密和解密过程都是基于矩阵运算和字节替换等操作进行的。加密时会将明文数据按 16
字节(128 bits
) 进行分组,不足 16
字节时将用特定的 Padding(如PCKS7)
字符进填充,所以不同的 Padding
方式密文最后一段可能不一样
CBC(Cipher Block Chaining)
:密码块链,明文被分成固定大小的块,并按顺序进行加密,每一个块(分组)要先和前一个分组加密后的数据进行XOR
异或操作,然后再进行加密。 这样每个密文块依赖该块之前的所有明文块,为了保持每条消息都具有唯一性,第一个数据块进行加密之前需要用初始化向量IV
进行异或操作。CBC
模式是一种最常用的加密模式,它主要缺点是加密是连续的,不能并行处理,并且与ECB
一样消息块必须填充到块大小的整倍数ECB(Electronic Codebook)
:电子密码本,是最简单的加密模式,明文消息被分成固定大小的块(分组),并且每个块被单独加密。 每个块的加密和解密都是独立的,且使用相同的方法进行加密,所以可以进行并行计算,但是这种方法一旦有一个块被破解,使用相同的方法可以解密所有的明文数据,安全性比较差。 适用于数据较少的情形,加密前需要把明文数据填充到块大小的整倍数CFB(Cipher Feedback)
:密码反馈,将数据分成位(bit
)而不是块,提供了流式加密(stream cipher
)的特性,可以对任意长度的数据进行加密。它允许逐位或逐字节地加密和解密,并且不需要填充(padding
)。 和CBC
模式比较相似,前一个分组的密文加密后和当前分组的明文XOR
异或操作生成当前分组的密文。因此,CFB
模式对于传输错误和数据丢失比较敏感,因为一个错误位会影响后续的加密结果
对于现代密码学来说,如果从秘钥的数量划分可以分为对称密码学和非对称密码学,对称加密只使用一把秘钥加解密,非对称加密则通过公钥和私钥两个秘钥加解密。对称加密由于使用同一把秘钥加解密,因此速度比较快,适合于数据量比较大的加解密
工具类
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.Key;
import java.util.Base64;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* @ClassName AESUtil
* @Desciption AES 加解密工具类
* @Author MaRui
* @Date 2023/12/8 11:30
* @Version 1.0
*/
public class AESUtil {
private static final String ALGORITHM = "AES";
private static final String AES_CBC_PADDING = "AES/CBC/PKCS5Padding";
/**
* Aes加密(CBC工作模式)
* @param content 待加密内容
* @param key 秘钥
* @param offset 偏移量
* @return
*/
public static String encodeByCBC(String content, String key, String offset) {
try {
//获取SecretKey对象,也可以使用getSecretKey()方法
Key secretKey = new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), ALGORITHM);
//获取指定转换的密码对象Cipher(参数:算法/工作模式/填充模式)
Cipher cipher = Cipher.getInstance(AES_CBC_PADDING);
//创建向量参数规范也就是初始化向量
IvParameterSpec ips = new IvParameterSpec(offset.getBytes(StandardCharsets.UTF_8));
//用密钥和一组算法参数规范初始化此Cipher对象(加密模式)
cipher.init(Cipher.ENCRYPT_MODE, secretKey, ips);
//执行加密操作
return Base64.getEncoder().encodeToString(cipher.doFinal(content.getBytes(StandardCharsets.UTF_8)));
} catch (Exception e) {
Logger.getLogger(AESUtil.class.getName()).log(Level.SEVERE, null, e);
}
return null;
}
/**
* Aes解密(CBC工作模式)
* @param content 待解密内容
* @param key 秘钥
* @param offset 偏移量
* @return
*/
public static String decodeByCBC(String content, String key, String offset) {
try {
// 偏移量
IvParameterSpec ips = new IvParameterSpec(offset.getBytes(StandardCharsets.UTF_8));
// 获取SecretKey对象,也可以使用getSecretKey()方法
SecretKey secretKey = new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), ALGORITHM);
// 获取指定转换的密码对象Cipher(参数:算法/工作模式/填充模式)
Cipher cipher = Cipher.getInstance(AES_CBC_PADDING);
// 用密钥和一组算法参数规范初始化此Cipher对象(加密模式)
cipher.init(Cipher.DECRYPT_MODE, secretKey, ips);
// 执行解密操作
byte[] bytes = cipher.doFinal(Base64.getDecoder().decode(content));
return new String(bytes, StandardCharsets.UTF_8);
} catch (Exception e) {
Logger.getLogger(AESUtil.class.getName()).log(Level.SEVERE, null, e);
}
return null;
}
public static void main(String[] args) throws NoSuchAlgorithmException {
// 密钥生成器
KeyGenerator keyGen = KeyGenerator.getInstance(ALGORITHM);
// 初始化密钥生成器
keyGen.init(128);
// 生成密钥
SecretKey secretKey = keyGen.generateKey();
// 密钥字节数组
byte[] keyByte = secretKey.getEncoded();
// 密钥 A0XFAl7XNjhUbKtN
String key = Base64.getEncoder().encodeToString(keyByte).substring(0, 16);
// 生成密钥
SecretKey secretKey2 = keyGen.generateKey();
// 密钥字节数组
byte[] keyByte2 = secretKey2.getEncoded();
// 偏移量 qOgxQPHcKkrCnteo
String offset = Base64.getEncoder().encodeToString(keyByte2).substring(0, 16);
System.out.println("密钥:" + key);
System.out.println("偏移量:" + offset);
String content = "https://maruinotes.com/";
// 加密
String encodeStr = encodeByCBC(content, key, offset);
System.out.println(encodeStr);
// jhIiAGfUzdUvyamr6hXxvAjTUtC9zUrXJhW+tCJbsl8=
// 解密
String decodeStr = decodeByCBC(encodeStr, key, offset);
System.out.println(decodeStr);
// https://maruinotes.com/
}
}
RSA
介绍
RSA
算法是一种非对称密码算法,它的名字由三位开发者,即 Ron Rivest
、Adi Shamir
和 Leonard Adleman
的姓氏的首字母组成的。RSA
算法基于数论,将两个大素数相乘十分容易,但想要对其乘积进行因式分解却极其困难,因此可以将乘积公开作为加密密钥
RSA
的安全性依赖于大数分解,因此RSA
算法加密安全性较高。但是,RSA
算法为保证安全性,会大大提升密钥长度,导致运算速度变慢。这导致它在大量数据加密时并不适用
工具类
正常流程都是公钥加密,私钥解密。也就是说,加入 A
与 B
互相传递数据,那就有两套 RAS
公密钥:
A
给B
传数据,用B
给的公钥加密,B
用私钥解密B
给A
传数据,用A
给的公钥加密,A
用私钥解密
import javax.crypto.Cipher;
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.util.Base64;
/**
* @ClassName RSAUtil
* @Desciption RSA 工具类
* @Author MaRui
* @Date 2023/12/13 17:56
* @Version 1.0
*/
public class RSAUtil {
private static final String RSA_KEY_ALGORITHM = "RSA";
// 标准签名算法名称
private static final String RSA_SIGNATURE_ALGORITHM = "SHA1withRSA";
private static final String RSA2_SIGNATURE_ALGORITHM = "SHA256withRSA";
/**
* 公钥加密(用于数据加密)
*
* @param data 加密前的字符串
* @param publicKeyStr base64编码后的公钥
* @return base64编码后的字符串
* @throws Exception
*/
public static String encryptByPublicKey(String data, String publicKeyStr) throws Exception {
// Java原生base64解码
byte[] pubKey = Base64.getDecoder().decode(publicKeyStr);
// 创建X509编码密钥规范
X509EncodedKeySpec x509KeySpec = new X509EncodedKeySpec(pubKey);
// 返回转换指定算法的KeyFactory对象
KeyFactory keyFactory = KeyFactory.getInstance(RSA_KEY_ALGORITHM);
// 根据X509编码密钥规范产生公钥对象
PublicKey publicKey = keyFactory.generatePublic(x509KeySpec);
// 根据转换的名称获取密码对象Cipher(转换的名称:算法/工作模式/填充模式)
Cipher cipher = Cipher.getInstance(keyFactory.getAlgorithm());
// 用公钥初始化此Cipher对象(加密模式)
cipher.init(Cipher.ENCRYPT_MODE, publicKey);
// 对数据加密
byte[] encrypt = cipher.doFinal(data.getBytes());
// 返回base64编码后的字符串
return Base64.getEncoder().encodeToString(encrypt);
}
/**
* 私钥解密(用于数据解密)
*
* @param data 解密前的字符串
* @param privateKeyStr 私钥
* @return 解密后的字符串
* @throws Exception
*/
public static String decryptByPrivateKey(String data, String privateKeyStr) throws Exception {
// Java原生base64解码
byte[] priKey = Base64.getDecoder().decode(privateKeyStr);
// 创建PKCS8编码密钥规范
PKCS8EncodedKeySpec pkcs8KeySpec = new PKCS8EncodedKeySpec(priKey);
// 返回转换指定算法的KeyFactory对象
KeyFactory keyFactory = KeyFactory.getInstance(RSA_KEY_ALGORITHM);
// 根据PKCS8编码密钥规范产生私钥对象
PrivateKey privateKey = keyFactory.generatePrivate(pkcs8KeySpec);
// 根据转换的名称获取密码对象Cipher(转换的名称:算法/工作模式/填充模式)
Cipher cipher = Cipher.getInstance(keyFactory.getAlgorithm());
// 用私钥初始化此Cipher对象(解密模式)
cipher.init(Cipher.DECRYPT_MODE, privateKey);
// 对数据解密
byte[] decrypt = cipher.doFinal(Base64.getDecoder().decode(data));
// 返回字符串
return new String(decrypt);
}
/**
* 私钥加密(用于数据签名)
*
* @param data 加密前的字符串
* @param privateKeyStr base64编码后的私钥
* @return base64编码后后的字符串
* @throws Exception
*/
public static String encryptByPrivateKey(String data, String privateKeyStr) throws Exception {
// Java原生base64解码
byte[] priKey = Base64.getDecoder().decode(privateKeyStr);
// 创建PKCS8编码密钥规范
PKCS8EncodedKeySpec pkcs8KeySpec = new PKCS8EncodedKeySpec(priKey);
// 返回转换指定算法的KeyFactory对象
KeyFactory keyFactory = KeyFactory.getInstance(RSA_KEY_ALGORITHM);
// 根据PKCS8编码密钥规范产生私钥对象
PrivateKey privateKey = keyFactory.generatePrivate(pkcs8KeySpec);
// 根据转换的名称获取密码对象Cipher(转换的名称:算法/工作模式/填充模式)
Cipher cipher = Cipher.getInstance(keyFactory.getAlgorithm());
// 用私钥初始化此Cipher对象(加密模式)
cipher.init(Cipher.ENCRYPT_MODE, privateKey);
// 对数据加密
byte[] encrypt = cipher.doFinal(data.getBytes());
// 返回base64编码后的字符串
return Base64.getEncoder().encodeToString(encrypt);
}
/**
* 公钥解密(用于数据验签)
*
* @param data 解密前的字符串
* @param publicKeyStr base64编码后的公钥
* @return 解密后的字符串
* @throws Exception
*/
public static String decryptByPublicKey(String data, String publicKeyStr) throws Exception {
// Java原生base64解码
byte[] pubKey = Base64.getDecoder().decode(publicKeyStr);
// 创建X509编码密钥规范
X509EncodedKeySpec x509KeySpec = new X509EncodedKeySpec(pubKey);
// 返回转换指定算法的KeyFactory对象
KeyFactory keyFactory = KeyFactory.getInstance(RSA_KEY_ALGORITHM);
// 根据X509编码密钥规范产生公钥对象
PublicKey publicKey = keyFactory.generatePublic(x509KeySpec);
// 根据转换的名称获取密码对象Cipher(转换的名称:算法/工作模式/填充模式)
Cipher cipher = Cipher.getInstance(keyFactory.getAlgorithm());
// 用公钥初始化此Cipher对象(解密模式)
cipher.init(Cipher.DECRYPT_MODE, publicKey);
// 对数据解密
byte[] decrypt = cipher.doFinal(Base64.getDecoder().decode(data));
// 返回字符串
return new String(decrypt);
}
/**
* RSA签名
*
* @param data 待签名数据
* @param priKey 私钥
* @param signType RSA或RSA2
* @return 签名
* @throws Exception
*/
public static String sign(byte[] data, byte[] priKey, String signType) throws Exception {
// 创建PKCS8编码密钥规范
PKCS8EncodedKeySpec pkcs8KeySpec = new PKCS8EncodedKeySpec(priKey);
// 返回转换指定算法的KeyFactory对象
KeyFactory keyFactory = KeyFactory.getInstance(RSA_KEY_ALGORITHM);
// 根据PKCS8编码密钥规范产生私钥对象
PrivateKey privateKey = keyFactory.generatePrivate(pkcs8KeySpec);
// 标准签名算法名称(RSA还是RSA2)
String algorithm = RSA_KEY_ALGORITHM.equals(signType) ? RSA_SIGNATURE_ALGORITHM : RSA2_SIGNATURE_ALGORITHM;
// 用指定算法产生签名对象Signature
Signature signature = Signature.getInstance(algorithm);
// 用私钥初始化签名对象Signature
signature.initSign(privateKey);
// 将待签名的数据传送给签名对象(须在初始化之后)
signature.update(data);
// 返回签名结果字节数组
byte[] sign = signature.sign();
// 返回Base64编码后的字符串
return Base64.getEncoder().encodeToString(sign);
}
/**
* RSA校验数字签名
*
* @param data 待校验数据
* @param sign 数字签名
* @param pubKey 公钥
* @param signType RSA或RSA2
* @return boolean 校验成功返回true,失败返回false
*/
public static boolean verify(byte[] data, byte[] sign, byte[] pubKey, String signType) throws Exception {
// 返回转换指定算法的KeyFactory对象
KeyFactory keyFactory = KeyFactory.getInstance(RSA_KEY_ALGORITHM);
// 创建X509编码密钥规范
X509EncodedKeySpec x509KeySpec = new X509EncodedKeySpec(pubKey);
// 根据X509编码密钥规范产生公钥对象
PublicKey publicKey = keyFactory.generatePublic(x509KeySpec);
// 标准签名算法名称(RSA还是RSA2)
String algorithm = RSA_KEY_ALGORITHM.equals(signType) ? RSA_SIGNATURE_ALGORITHM : RSA2_SIGNATURE_ALGORITHM;
// 用指定算法产生签名对象Signature
Signature signature = Signature.getInstance(algorithm);
// 用公钥初始化签名对象,用于验证签名
signature.initVerify(publicKey);
// 更新签名内容
signature.update(data);
// 得到验证结果
return signature.verify(sign);
}
public static void main(String[] args) throws Exception {
// RAS 公钥、密钥生成
KeyPairGenerator keyPairGen = KeyPairGenerator.getInstance(RSA_KEY_ALGORITHM);
keyPairGen.initialize(1024);
KeyPair keyPair = keyPairGen.generateKeyPair();
RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
String publicStr = Base64.getEncoder().encodeToString(publicKey.getEncoded());
String privateStr = Base64.getEncoder().encodeToString(privateKey.getEncoded());
System.out.println("publicKey:" + publicStr);
System.out.println("privateKey:" + privateStr);
// 明文
String data = "https://maruinotes.com/";
// 公钥加密,私钥解密
String publicEncryptData = encryptByPublicKey(data, publicStr);
System.out.println("公钥加密结果:" + publicEncryptData);
String privateDecryptData = decryptByPrivateKey(publicEncryptData, privateStr);
System.out.println("私钥解密结果:" + privateDecryptData);
// 私钥加密,公钥解密(用作签名)
String privateEncryptData = encryptByPrivateKey(data, privateStr);
System.out.println("私钥加密结果:" + privateEncryptData);
String publicDecryptData = decryptByPublicKey(privateEncryptData, publicStr);
System.out.println("公钥解开密结果:" + publicDecryptData);
}
}