加密与安全

发布于:2025-08-02 ⋅ 阅读:(11) ⋅ 点赞:(0)

目录

一、编码算法

1.什么是编码?

2.URL编码

3.Base64编码

4.小结

二、哈希算法

1.概述

2.哈希碰撞

3.常用哈希算法

4.哈希算法的用途

4.1 校验下载文件

4.2 存储用户密码

 5.SHA-1

6.小结

三、Hmac算法

1.概述

2.小结

四、BouncyCastle

1.概述

2.用法

3.小结

五、对称加密算法

1.概述

2.使用AES加密

2.1 ECB模式

2.2 CBC模式

3.小结


一、编码算法

1.什么是编码?

ASCII码就是一种编码,字母A的编码是十六进制的0x41,字母B0x42,以此类推:

字母

ASCII编码

A

0x41

B

0x42

C

0x43

D

0x44

因为ASCII编码最多只能有127个字符,要想对更多的文字进行编码,就需要用占用2个字节的Unicode或者3个字节的UTF-8。例如:中文的"中"字使用Unicode编码就是0x4e2dUTF-8编码是0xe4b8ad

汉字

Unicode编码

UTF-8编码

0x4e2d

0xe4b8ad

0x6587

0xe69687

0x7f16

0xe7bc96

0x7801

0xe7a081

 因此,最简单的编码是直接给每个字符指定一个若干字节表示的整数,复杂一点的编码就需要根据一个已有的编码推算出来。

2.URL编码

URL编码是浏览器发送数据给服务器时使用的编码,它通常附加在URL的参数部分,例如:

百度安全验证

之所以需要URL编码,是因为出于兼容性考虑,很多服务器只识别ASCII字符。但如果URL中包含中文日文这些非ASCII字符怎么办?不要紧,URL编码有一套规则:

        如果字符是A~Za~z0~9以及-_.*,则保持不变;

        如果是其他字符,先转换为UTF-8编码,然后对每个字节以%XX表示。

例如:字符"中"UTF-8编码是0xe4b8ad,因此,它的URL编码是%E4%B8%ADURL编码总是大写。

Java标准库提供了一个URLEncoder类来对任意字符串进行URL编码。

        如果服务器收到URL编码的字符串,就可以对其进行解码,还原成原始字符串。Java标准库的URLDecoder就可以解码:

import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.util.Scanner;

public class Demo01 {
    public static void main(String[] args) throws UnsupportedEncodingException {
        //编码
        System.out.println("请输入:");
        String input= new Scanner(System.in).next();
        String encodeString=URLEncoder.encode(input,"UTF-8");
        System.out.println("编码后的结果为:"+encodeString);

        //解码
        String decodeString=URLDecoder.decode(encodeString,"UTF-8");
        System.out.println("解码后的结果为:"+decodeString);
    }
}
//运行结果
//请输入:
//中文!
//编码后的结果为:%E4%B8%AD%E6%96%87%21
//解码后的结果为:中文!

上述代码如果在控制台输入的是"中文!"的运行结果是%E4%B8%AD%E6%96%87%21"中"URL编码是%E4%B8%AD"文"URL编码是%E6%96%87!虽然是ASCII字符,也要对其编码为%21

要特别注意:URL编码是编码算法,不是加密算法。URL编码的目的是把任意文本数据编码为%前缀表示的文本,编码后的文本仅包含A~Za~z0~9-_.*%,便于浏览器和服务器处理。

3.Base64编码

URL编码是对字符进行编码,表示成%xx的形式,而Base64编码是对二进制数据进行编码,表示成文本格式。

Base64编码可以把任意长度的二进制数据变为纯文本,并且纯文本内容中且只包含指定字符内容:A~Za~z0~9+/=。它的原理是把3字节的二进制数据按6bit一组,用4个整数表示,然后查表,把整数用索引对应到字符,得到编码后的字符串。

6位整数的范围总是0~63,所以,能用64个字符表示:字符A~Z对应索引0~25,字符a~z对应索引26~51,字符0~9对应索引52~61,最后两个索引6263分别用字符+/表示。

举个例子3byte数据分别是e4b8ad,按6bit分组得到十六进制390b222d,分别对应十进制57113445,通过索引计算结果为5Lit4。

┌───────────────┬───────────────┬───────────────┐
│      e4       │      b8       │      ad       │
└───────────────┴───────────────┴───────────────┘
┌─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┐
│1│1│1│0│0│1│0│0│1│0│1│1│1│0│0│0│1│0│1│0│1│1│0│1│二进制
└─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┘
┌───────────┬───────────┬───────────┬───────────┐
│    39     │    0b     │    22     │    2d     │十六进制
└───────────┴───────────┴───────────┴───────────┘
┌───────────┬───────────┬───────────┬───────────┐
│    57     │    11     │    34     │    45     │十进制
└───────────┴───────────┴───────────┴───────────┘
┌───────────┬───────────┬───────────┬───────────┐
│    5      │    L      │    i      │    t      │十进制
└───────────┴───────────┴───────────┴───────────┘

 在Java中,二进制数据就是byte[]数组。Java标准库提供了Base64来对byte[]数组进行编码:

public class Main {
    public static void main(String[] args) {
        byte[] input = new byte[] { (byte) 0xe4, (byte) 0xb8, (byte) 0xad };
        String b64encoded = Base64.getEncoder().encodeToString(input);
        System.out.println(b64encoded);
    }
}

编码后得到字符串结果:5Lit。要对这个字符使用Base64解码,仍然用Base64这个类:

public class Main {
    public static void main(String[] args) {
        byte[] output = Base64.getDecoder().decode("5Lit");
        System.out.println(Arrays.toString(output)); // [-28, -72, -83]
    }
}

因为标准的Base64编码会出现+/=,所以不适合把Base64编码后的字符串放到URL中。一种针对URLBase64编码可以在URL中使用的Base64编码,它仅仅是把+变成-/变成_

import java.util.Arrays;
import java.util.Base64;

public class Demo03 {
    public static void main(String[] args) {
        //原始字节数组
        byte[] input=new byte[]{0x01,0x02,0x7f,0x00};
        //编码
        String baseEncode=Base64.getEncoder().encodeToString(input);
        System.out.println("base64进行编码:"+baseEncode);
        String baseEncode1=Base64.getUrlEncoder().encodeToString(input);
        System.out.println("base64url进行编码:"+baseEncode1);

        //解码
        byte[] bytes=Base64.getDecoder().decode(baseEncode);
        System.out.println("base64解码后的内容:"+ Arrays.toString(bytes));
        byte[] bytes1=Base64.getUrlDecoder().decode(baseEncode1);
        System.out.println("base64url解码后的内容:"+ Arrays.toString(bytes1));
    }
}

Base64编码的目的是把二进制数据变成文本格式,这样在很多文本中就可以处理二进制数据。例如,电子邮件协议就是文本协议,如果要在电子邮件中添加一个二进制文件,就可以用Base64编码,然后以文本的形式传送。

Base64编码的缺点是传输效率会降低,因为它把原始数据的长度增加了1/3。和URL编码一样,Base64编码是一种编码算法,不是加密算法。

如果把Base6464个字符编码表换成32个、48个或者58个,就可以使用Base32编码,Base48编码和Base58编码。字符越少,编码的效率就会越低。

4.小结

(1)URL编码和Base64编码都是编码算法,它们不是加密算法;

(2)URL编码的目的是把任意文本数据编码为%前缀表示的文本,便于浏览器和服务器处理;

(3)Base64编码的目的是把任意二进制数据编码为文本,但编码后数据量会增加1/3

二、哈希算法

1.概述

哈希算法(Hash)又称摘要算法(Digest),它的作用是:对任意一组输入数据进行计算,得到一个固定长度的输出摘要。

哈希算法最重要的特点就是:

        相同的输入一定得到相同的输出;

        不同的输入大概率得到不同的输出。

所以,哈希算法的目的:为了验证原始数据是否被篡改。

Java字符串的hashCode()就是一个哈希算法,它的输入是任意字符串,输出是固定的4字节int整数:

"hello".hashCode(); // 0x5e918d2
"hello, java".hashCode(); // 0x7a9d88e8
"hello, bob".hashCode(); // 0xa0dbae2f

两个相同的字符串永远会计算出相同的hashCode,否则基于hashCode定位的HashMap就无法正常工作。这也是为什么当我们自定义一个class时,覆写equals()方法时我们必须正确覆写hashCode()方法。

2.哈希碰撞

哈希碰撞是指:两个不同的输入得到了相同的输出。

例如:

"AaAaAa".hashCode(); // 0x7460e8c0
"BBAaBB".hashCode(); // 0x7460e8c0

"通话".hashCode(); // 0x11ff03
"重地".hashCode(); // 0x11ff03

碰撞能不能避免?答案是不能。碰撞是一定会出现的,因为输出的字节长度是固定的,StringhashCode()输出是4字节整数,最多只有4294967296种输出,但输入的数据长度是不固定的,有无数种输入。所以,哈希算法是把一个无限的输入集合映射到一个有限的输出集合,必然会产生碰撞。碰撞不可怕,我们担心的不是碰撞,而是碰撞的概率,因为碰撞概率的高低关系到哈希算法的安全性。一个安全的哈希算法必须满足:

        碰撞概率低;

        不能猜测输出。

不能猜测输出是指:输入的任意一个bit的变化会造成输出完全不同,这样就很难从输出反推输入(只能依靠暴力穷举)。

假设一种哈希算法有如下规律:

hashA("java001") = "123456"
hashA("java002") = "123457"
hashA("java003") = "123458"

那么很容易从输出123459反推输入,这种哈希算法就不安全。安全的哈希算法从输出是看不出任何规律的:

hashB("java001") = "123456"
hashB("java002") = "580271"
hashB("java003") = ???

3.常用哈希算法

哈希算法,根据碰撞概率,哈希算法的输出长度越长,就越难产生碰撞,也就越安全。

常用的哈希算法有:

算法

输出长度(位)

输出长度(字节)

MD5

128 bits

16 bytes

SHA-1

160 bits

20 bytes

RipeMD-160

160 bits

20 bytes

SHA-256

256 bits

32 bytes

SHA-512

512 bits

64 bytes

Java标准库提供了常用的哈希算法,通过统一的接口进行调用。以MD5算法为例,看看如何对输入内容计算哈希:

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;

public class Demo02 {
    public static void main(String[] args) throws NoSuchAlgorithmException {
        //1.创建MessageDigest实例
        MessageDigest md=MessageDigest.getInstance("MD5");
        //2.使用MD5进行信息加密
        byte[] message="我本将心向明月".getBytes();
        md.update(message);
        //3.进行加密,加密后长度不受原字节数组长度影响,一直是16
        // 是单向加密,无法反推
        byte[] bytes=md.digest();
        System.out.println("加密前的字节数组为:"+Arrays.toString(message));
        System.out.println("加密后的长度为:"+message.length);
        System.out.println("加密后的字节数组为:"+Arrays.toString(bytes));
        System.out.println("加密后的长度为:"+bytes.length);
        String digMessage=byteToHex(bytes);
        System.out.println("加密后的字符串为:"+digMessage);
        System.out.println("加密后的长度为:"+digMessage.length());
    }
    public static String byteToHex(byte[] bytes){
        StringBuilder sb = new StringBuilder();
        for (byte b:bytes) {
            //将一个字节数字转成2位的16进制的字符串数字
            sb.append(String.format("%02x",b));
        }
        return  sb.toString();
    }
}
//运行结果:
//加密前的字节数组为:[-26, -120, -111, -26, -100, -84, -27, -80, //-122, -27, -65, -125, -27, -112, -111, -26, -104, -114, -26, //-100, -120]
//加密后的长度为:21
//加密后的字节数组为:[-99, 85, -59, -110, 118, -116, 36, 49, 127, //57, 43, 33, 17, 19, 83, -21]
//加密后的长度为:16
//加密后的字符串为:9d55c592768c24317f392b21111353eb
//加密后的长度为:32

使用MessageDigest时,我们首先根据哈希算法获取一个MessageDigest实例,然后,反复调用update(byte[])输入数据。当输入结束后,调用digest()方法获得byte[]数组表示的摘要,最后,把它转换为十六进制的字符串。

4.哈希算法的用途

4.1 校验下载文件

因为相同的输入永远会得到相同的输出,因此,如果输入被修改了,得到的输出就会不同。我们在网站上下载软件的时候,经常看到下载页显示的MD5哈希值:

 如何判断下载到本地的软件是原始的、未经篡改的文件?我们只需要自己计算一下本地文件的哈希值,再与官网公开的哈希值对比,如果相同,说明文件下载正确,否则,说明文件已被篡改。

4.2 存储用户密码

哈希算法的另一个重要用途是存储用户口令。如果直接将用户的原始口令存放到数据库中,会产生极大的安全风险:

        数据库管理员能够看到用户明文口令;

        数据库数据一旦泄漏,黑客即可获取用户明文口令。

username

password

bob

123456789

alice

sdfsdfsdf

tim

justdoit

不存储用户的原始口令,那么如何对用户进行认证?方法是存储用户口令的哈希,例如,MD5。在用户输入原始口令后,系统计算用户输入的原始口令的MD5并与数据库存储的MD5对比,如果一致,说明口令正确,否则,口令错误。

因此,数据库存储用户名和口令的表内容应该像下面这样:

username

password

bob

25f9e794323b453885f5181f1b624d0b

alice

73a90acaae2b1ccc0e969709665bc62f

tim

19f9f30bd097d4c066d758fb01b75032

 

这样一来,数据库管理员看不到用户的原始口令。即使数据库泄漏,黑客也无法拿到用户的原始口令。想要拿到用户的原始口令,必须用暴力穷举的方法,一个口令一个口令地试,直到某个口令计算的MD5恰好等于指定值。

使用哈希口令时,还要注意防止彩虹表攻击

什么是彩虹表呢?上面讲到了,如果只拿到MD5,从MD5反推明文口令,只能使用暴力穷举的方法。然而黑客并不笨,暴力穷举会消耗大量的算力和时间。但是,如果有一个预先计算好的常用口令和它们的MD5的对照表,这个表就是彩虹表。如果用户使用了常用口令,黑客从MD5一下就能反查到原始口令:

常用口令

MD5

hello123

f30aa7a662c728b7407c54ae6bfd27d1

12345678

25d55ad283aa400af464c76d713c07ad

passw0rd

bed128365216c019988915ed3add75fb

19700101

570da6d5277a646f6552b8832012f5dc

wbjxxmy

11d7a82f45f6a176fd9d5c100ccab40a

这就是为什么不要使用常用密码,以及不要使用生日作为密码的原因。

当然,我们也可以采取特殊措施来抵御彩虹表攻击:对每个口令额外添加随机数,这个方法称之为加盐salt):

digest = md5(salt + inputPassword)

经过加盐处理的数据库表,内容如下:

username

salt

password

bob

H1r0a

a5022319ff4c56955e22a74abcc2c210

alice

7$p2w

e5de688c99e961ed6e560b972dab8b6a

tim

z5Sk9

1eee304b92dc0d105904e7ab58fd2f64

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.UUID;

/**
 * 此次的盐值信息为:afe39a
 * 加密后的结果为:[85, -38, -11, -44, 76, 97, 3, -117, 117, 91, 38, -117, 69, -110, 1, 70]
 * 加密后的字符串为:55daf5d44c61038b755b268b45920146
 * 加密后的长度为:16
 */
public class Demo03 {
    public static void main(String[] args) throws NoSuchAlgorithmException {
        //1.创建MessageDigest实例
        MessageDigest md=MessageDigest.getInstance("MD5");
        //2.使用MD5进行信息加密
        byte[] message="我本将心向明月".getBytes();
        md.update(message);
        //3.添加盐值,随机值
//        String uuid= UUID.randomUUID().toString().substring(0,6);
//        System.out.println("此次的盐值信息为:"+uuid);
//        md.update(uuid.getBytes());
        //用上面刚获取的盐值查看一下加密后的字符串是否一致
        md.update("afe39a".getBytes());
        //4.进行加密
        byte[] bytes=md.digest();
        System.out.println("加密后的结果为:"+Arrays.toString(bytes));
        System.out.println("加密后的字符串为:"+Demo02.byteToHex(bytes));
        System.out.println("加密后的长度为:"+bytes.length);
    }
}

 5.SHA-1

SHA-1也是一种哈希算法,它的输出是160 bits,即20字节。SHA-1是由美国国家安全局开发的,SHA算法实际上是一个系列,包括SHA-0(已废弃)、SHA-1SHA-256SHA-512等。

Java中使用SHA-1,和MD5完全一样,只需要把算法名称改为"SHA-1":

// 创建一个MessageDigest实例:
        MessageDigest md = MessageDigest.getInstance("SHA-1");

类似的,计算SHA-256,我们需要传入名称"SHA-256",计算SHA-512,我们需要传入名称"SHA-512"。Java标准库支持的所有哈希算法可以在这里查到。

例题:我们自己提供一个HashTools工具类:

import org.bouncycastle.jce.provider.BouncyCastleProvider;

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.Security;
import java.util.UUID;

public class HashTools {
    //创建信息摘要对象--成员变量
    private static MessageDigest md;

    private HashTools() {

    }

    public static String messageMd5(String str) throws NoSuchAlgorithmException {
        //创建信息摘要对象md5
        md = MessageDigest.getInstance("MD5");
        return handle(str);
    }

    public static String messageSha1(String str) throws NoSuchAlgorithmException {
        //创建信息摘要对象md5
        md = MessageDigest.getInstance("SHA-1");
        return handle(str);
    }

    public static String handle(String str) {
        //获取要加密的信息的字节数组并添加
        byte[] message = str.getBytes();
        md.update(message);
        //添加盐值
        md.update(UUID.randomUUID().toString().substring(0, 6).getBytes());
        //加密操作
        byte[] bytes = md.digest();
        //转字符串
        return byteToHex(bytes);
    }

    public static String byteToHex(byte[] bytes) {
        StringBuilder sb = new StringBuilder();
        for (byte b : bytes) {
            //将一个字节数字转成2位的16进制的字符串数字
            sb.append(String.format("%02x", b));
        }
        return sb.toString();
    }
}

用的时候直接调用:

import java.security.NoSuchAlgorithmException;

public class Demo05 {
    public static void main(String[] args) throws NoSuchAlgorithmException {
        String message="我本将心向明月";
        String md5Dig=HashTools.messageMd5(message);
        System.out.println("md5加密后的字符串为:"+md5Dig);
        System.out.println(md5Dig.length());//16*2

        String sha1Dig=HashTools.messageSha1(message);
        System.out.println("sha1加密后的字符串为:"+sha1Dig);
        System.out.println(sha1Dig.length());//20*2
    }
}

6.小结

(1)哈希算法可用于验证数据完整性,具有防篡改检测的功能;

(2)常用的哈希算法有MD5SHA-1等;

(3)用哈希存储口令时要考虑彩虹表攻击。

三、Hmac算法

1.概述

在前面讲到哈希算法时,我们说,存储用户的哈希口令时,要加盐存储,目的就在于抵御彩虹表攻击。我们回顾一下哈希算法:digest = hash(input)

正是因为相同的输入会产生相同的输出,我们加盐的目的就在于,使得输入有所变化:

digest = hash(salt + input)

这个salt可以看作是一个额外的“认证码”,同样的输入,不同的认证码,会产生不同的输出。因此,要验证输出的哈希,必须同时提供“认证码”。

Hmac算法就是一种基于密钥的消息认证码算法,它的全称是Hash-based Message Authentication Code,是一种更安全的消息摘要算法。

Hmac算法总是和某种哈希算法配合起来用的。例如,我们使用MD5算法,对应的就是Hmac MD5算法,它相当于“加盐”的MD5HmacMD5 ≈ md5(secure_random_key, input)

因此,HmacMD5可以看作带有一个安全的keyMD5。使用HmacMD5而不是用MD5salt,有如下好处:

    HmacMD5使用的key长度是64字节,更安全;

    Hmac是标准算法,同样适用于SHA-1等其他哈希算法;

    Hmac输出和原有的哈希算法长度一致。

可见,Hmac本质上就是把key混入摘要的算法。验证此哈希时,除了原始的输入数据,还要提供key。为了保证安全,我们不会自己指定key,而是通过Java标准库的KeyGenerator生成一个安全的随机的key

下面是使用HmacMD5的参考代码:

import javax.crypto.KeyGenerator;
import javax.crypto.Mac;
import javax.crypto.SecretKey;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;

public class Demo06 {
    public static void main(String[] args) throws NoSuchAlgorithmException, InvalidKeyException {
        //产生密钥生成器对象
        KeyGenerator generator = KeyGenerator.getInstance("HmacMD5");
        //通过密钥生成器生成密钥
        SecretKey key = generator.generateKey();
        //获取密钥字节信息
        byte[] bytes = key.getEncoded();

        System.out.println("密钥字节信息:" + Arrays.toString(bytes));
        System.out.println("密钥字符串:" + HashTools.byteToHex(bytes));
        System.out.println("密钥长度:" + bytes.length);

        //获取加密对象
        Mac mac = Mac.getInstance("HmacMD5");
        //初始化key值
        mac.init(key);
        //提供需要进行加密的信息添加进来
        mac.update("我本将心向明月".getBytes());
        //加密操作
        byte[] md5Message = mac.doFinal();

        System.out.println("加密字节信息:" + Arrays.toString(md5Message));
        System.out.println("加密字符串:" + HashTools.byteToHex(md5Message));
        System.out.println("加密长度:" + md5Message.length);
    }
}

MD5相比,使用HmacMD5的步骤是:

  1. 通过名称HmacMD5获取KeyGenerator实例;
  2. 通过KeyGenerator创建一个SecretKey实例;
  3. 通过名称HmacMD5获取Mac实例;
  4. SecretKey初始化Mac实例;
  5. Mac实例反复调用update(byte[])输入数据;
  6. 调用Mac实例的doFinal()获取最终的哈希值。

我们可以用Hmac算法取代原有的自定义的加盐算法,因此,存储用户名和口令的数据库结构如下:

username

secret_key (64 bytes)

password

bob

a8c06e05f92e...5e16

7e0387872a57c85ef6dddbaa12f376de

alice

e6a343693985...f4be

c1f929ac2552642b302e739bc0cdbaac

tim

f27a973dfdc0...6003

af57651c3a8a73303515804d4af43790

 有了Hmac计算的哈希和SecretKey,我们想要验证怎么办?这时,SecretKey不能从KeyGenerator生成,而是从一个byte[]数组恢复:

首先在HashTools工具类中添加一个stringTobyte()方法:

    public static byte[] stringTobyte(String str){
        byte[] bytes=new byte[str.length()/2];
        for (int i = 0; i <str.length() ; i=i+2) {
            String subString =str.substring(i,i+2);
            bytes[i/2] =(byte) Integer.parseInt(subString,16);
        }
        return bytes;
    }
import javax.crypto.Mac;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;

/**
 * 密钥字节信息:[-6, 15, -26, 90, -100, 18, 105, 25, 104, 50, 98, 74, -101, 50, 2, 42, 77, -99, -14, 60, 37, 114, -47, -20, -76, -7, -41, -1, -26, -91, 68, -7, -41, 115, -87, 21, -57, -110, -28, -64, 38, -99, -62, 82, 50, -95, -60, -103, -49, 115, 21, -39, -4, -60, 117, 6, -97, 20, 69, 116, 55, -128, 80, 51]
 * 密钥字符串:fa0fe65a9c1269196832624a9b32022a4d9df23c2572d1ecb4f9d7ffe6a544f9d773a915c792e4c0269dc25232a1c499cf7315d9fcc475069f14457437805033
 * 密钥长度:64
 * 加密字节信息:[-112, 60, 66, -40, 89, -83, -110, 40, 121, 54, -93, 118, 1, 94, 28, 119]
 * 加密字符串:903c42d859ad92287936a376015e1c77
 * 加密长度:16
 */
public class Demo07 {
    public static void main(String[] args) throws NoSuchAlgorithmException, InvalidKeyException {
        //1.密钥字符串转字节数组操作
        String str="fa0fe65a9c1269196832624a9b32022a4d9df23c2572d1ecb4f9d7ffe6a544f9d773a915c792e4c0269dc25232a1c499cf7315d9fcc475069f14457437805033";
        byte[] bytes=HashTools.stringTobyte(str);
        System.out.println(Arrays.toString(bytes));

        //2.还原密钥key
        SecretKey key=new SecretKeySpec(bytes,"HmacMD5");

        //3.再次加密
        Mac mac = Mac.getInstance("HmacMD5");
        //初始化key值
        mac.init(key);
        //提供需要进行加密的信息添加进来
        mac.update("我本将心向明月".getBytes());
        //加密操作
        byte[] md5Message = mac.doFinal();

        System.out.println("加密字节信息:" + Arrays.toString(md5Message));
        System.out.println("加密字符串:" + HashTools.byteToHex(md5Message));
        System.out.println("加密长度:" + md5Message.length);
    }
}

2.小结

Hmac算法是一种标准的基于密钥的哈希算法,可以配合MD5SHA-1等哈希算法,计算的摘要长度和原摘要算法长度相同。

四、BouncyCastle

1.概述

Java标准库提供了一系列常用的哈希算法。但如果我们要用的某种算法,Java标准库没有提供怎么办?

方法一:自己写一个,难度很大;

方法二:找一个现成的第三方库,直接使用。

BouncyCastle就是一个提供了很多哈希算法和加密算法的第三方开源库。它提供了Java标准库没有的一些算法,例如,RipeMD160哈希算法。

RIPEMD160是一种基于Merkle-Damgård结构的加密哈希函数,它是比特币标准之一。RIPEMD-160RIPEMD算法的增强版本,RIPEMD-160算法可以产生出160位的的哈希摘要。

2.用法

首先,我们必须把BouncyCastle提供的bcprov-jdk15on-1.70.jar添加至classpath。这个jar包可以从官方网站下载。

 其次,Java标准库的java.security包提供了一种标准机制,允许第三方提供商无缝接入。我们要使用BouncyCastle提供的RipeMD160算法,需要先把BouncyCastle注册一下:

可以在HashTools中添加messageRipeMD160方法,然后直接调用:

    public static String messageRipeMD160(String str) throws NoSuchAlgorithmException {
        //创建信息摘要对象md5
        Security.addProvider(new BouncyCastleProvider());
        md = MessageDigest.getInstance("RipeMD160");
        return handle(str);
    }
        String ripemd160Dig=HashTools.messageRipeMD160(message);
        System.out.println("ripemd160加密后的字符串为:"+ripemd160Dig);
        System.out.println(ripemd160Dig.length());//20*2

3.小结

(1)BouncyCastle是一个开源的第三方算法提供商;

(2)BouncyCastle提供了很多Java标准库没有提供的哈希算法和加密算法;

五、对称加密算法

1.概述

对称加密算法就是传统的用一个秘钥进行加密和解密。例如,我们常用的WinZIPWinRAR对压缩包的加密和解密,就是使用对称加密算法:

从程序的角度看,所谓加密,就是这样一个函数,它接收密码和明文,然后输出密文:

    secret = encrypt(key, message);

而解密则相反,它接收密码和密文,然后输出明文:

     plain = decrypt(key, secret);

在软件开发中,常用的对称加密算法有:

算法

密钥长度

工作模式

填充模式

DES

56/64

ECB/CBC/PCBC/CTR/...

NoPadding/PKCS5Padding/...

AES

128/192/256

ECB/CBC/PCBC/CTR/...

NoPadding/PKCS5Padding/PKCS7Padding/...

IDEA

128

ECB

PKCS5Padding/PKCS7Padding/...

密钥长度直接决定加密强度,而工作模式和填充模式可以看成是对称加密算法的参数和格式选择。Java标准库提供的算法实现并不包括所有的工作模式和所有填充模式。

最后,值得注意的是,DES算法由于密钥过短,可以在短时间内被暴力破解,所以现在已经不安全了。

2.使用AES加密

AES算法是目前应用最广泛的加密算法。比较常见的工作模式是ECBCBC

2.1 ECB模式

ECB模式是最简单的AES加密模式,它需要一个固定长度的密钥,固定的明文会生成固定的密文。

我们先用ECB模式加密并解密:

import apesource.knowledge.hashStudy.HashTools;

import javax.crypto.*;
import javax.crypto.spec.SecretKeySpec;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;

public class Demo01 {
    public static void main(String[] args) throws IllegalBlockSizeException, InvalidKeyException, BadPaddingException, NoSuchAlgorithmException, NoSuchPaddingException {
        //加密   加密的字节数组长度随着input的变化而变化
        byte[]  input="我本将心向明月".getBytes();
        //keys是固定长度的密钥,固定的明文会生成固定的密文。
        // 128位密钥 = 16 bytes Key
        byte[] keys="1234567890abcedf".getBytes();
        byte[] encodeBytes=encodeMessage(input,keys);
        System.out.println("加密后的结果:"+ Arrays.toString(encodeBytes));
        System.out.println("加密后的字符串:"+ HashTools.byteToHex(encodeBytes));
        System.out.println("加密后字节数组的长度:"+encodeBytes.length);

        //解密
        byte[] decodeMessage=decodeMessage(encodeBytes,keys);
        System.out.println("解密后的信息:"+new String(decodeMessage));
    }

    private static byte[] encodeMessage(byte[] message, byte[] key) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException {
        //1.获取对称加密对象,设置加密算法,工作模式,填充模式
        Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
        //2.加密准备密钥信息
        SecretKey keys = new SecretKeySpec(key, "AES");
        //3.初始化加密对象
        cipher.init(Cipher.ENCRYPT_MODE, keys);
        //4.加密操作
        byte[] bytes = cipher.doFinal(message);

        return bytes;
    }

    private static byte[] decodeMessage(byte[] encode,byte[] key) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException {
        //1.获取对称加密对象,设置加密算法,工作模式,填充模式
        Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
        //2.加密准备密钥信息
        SecretKey keys = new SecretKeySpec(key, "AES");
        //3.初始化解密对象
        cipher.init(Cipher.DECRYPT_MODE, keys);
        //4.执行解密操作
        return cipher.doFinal(encode);
    }
}

Java标准库提供的对称加密接口非常简单,使用时按以下步骤编写代码:

1.根据算法名称/工作模式/填充模式获取Cipher实例;

2.根据算法名称初始化一个SecretKey实例,密钥必须是指定长度;

3.使用SerectKey初始化Cipher实例,并设置加密或解密模式;

4.传入明文或密文,获得密文或明文。

2.2 CBC模式

ECB模式是最简单的AES加密模式,这种一对一的加密方式会导致安全性降低。所以,更好的方式是通过CBC模式,它需要一个随机数作为IV参数,这样对于同一份明文,每次生成的密文都不同:

import javax.crypto.*;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.Arrays;

public class Demo02 {
    public static void main(String[] args) throws Exception {
        byte[] message="我本将心向明月".getBytes();
        byte[] keys="1234567890abcdef".getBytes();
        byte[] encode=encodeMessage(message,keys);
        System.out.println("加密后的信息为:"+Arrays.toString(encode));

        byte[] decode=decodeMessage(encode,keys);
        System.out.println(new String(decode));
    }
    private static byte[] encodeMessage(byte[] message, byte[] key) throws Exception{
        //1.获取Cipher对象
        Cipher cipher=Cipher.getInstance("AES/CBC/PKCS5Padding");

        //2.还原key
        SecretKey keys=new SecretKeySpec(key,"AES");

        //3.准备IV偏移   SecureRandom安全随机数
        SecureRandom sr=SecureRandom.getInstanceStrong();
        byte[] bytes=sr.generateSeed(16);
//        System.out.println("iv偏移值"+Arrays.toString(bytes));
        IvParameterSpec iv=new IvParameterSpec(bytes);

        //4.初始化操作,设置加密模式,设置key,设置偏移量
        cipher.init(Cipher.ENCRYPT_MODE,keys,iv);

        //5.加密
        byte[] endcode=cipher.doFinal(message);

        return addByte(endcode,bytes);
    }
    private static byte[] decodeMessage(byte[] decodeMessage, byte[] key) throws Exception{
        //1.获取Cipher对象
        Cipher cipher=Cipher.getInstance("AES/CBC/PKCS5Padding");

        //2.还原key
        SecretKey keys=new SecretKeySpec(key,"AES");

        //3.还原iv值
        byte[] bytes=Arrays.copyOfRange(decodeMessage,decodeMessage.length-16,decodeMessage.length);
        IvParameterSpec iv=new IvParameterSpec(bytes);

        //4.初始化操作,设置解密模式,设置key,设置偏移量
        cipher.init(Cipher.DECRYPT_MODE,keys,iv);

        //5.加密
        byte[] endcode=cipher.doFinal(Arrays.copyOf(decodeMessage,decodeMessage.length-16));

        return endcode;
    }
    public static byte[] addByte(byte[] endcode,byte[] iv){
        byte[] bytes=new byte[endcode.length+iv.length];
        System.arraycopy(endcode,0,bytes,0,endcode.length);
        System.arraycopy(iv,0,bytes,endcode.length,iv.length);
        return bytes;
    }
}

CBC模式下,需要一个随机生成的16字节IV参数,必须使用SecureRandom生成。因为多了一个IvParameterSpec实例,因此,初始化方法需要调用Cipher的一个重载方法并传入IvParameterSpec

观察输出,可以发现每次生成的IV不同,密文也不同。

3.小结

(1)对称加密算法使用同一个密钥进行加密和解密,常用算法有DESAESIDEA等;

(2)密钥长度由算法设计决定,AES的密钥长度是128/192/256位;

(3)使用对称加密算法需要指定算法名称、工作模式和填充模式。