加密和解密那些事

在平时的开发中,涉及到的加密、解密功能基本上都是使用语言自带的库函数来完成,或者使用第三方开源的库。最近想探究下这些加密、解密算法的实现机制,下面大概记录了相关的探究过程。

计算机的编码

原码、反码、补码

原码、反码、补码知识详细讲解

  • 原码:符号位+真值的绝对值,即用第一位表示符号,其余位表示值。所以8位二进制数的取值范围为[1111 1111, 0111 1111],即[-127, 127]
  • 反码:正数的反码是其本身,负数的反码是在其原码的基础上,符号位不变,其余各位取反;
  • 补码:正数的补码是其本身,负数的补码是在其反码的基础上加1;

注意:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
String str = "好";
byte[] bytes = str.getBytes(StandardCharsets.UTF_8);
for (byte b:bytes) {
System.out.println(b);
}

代码运行的结果:
-27
-91
-67

为什么上面代码运行的结果是负数呢?
"好"的UTF-8二进制编码为:11100101 10100101 10111101

[11100101]补 = [10011011]原= [-27]十进制(byte)
[10100101]补 = [11011011]原= [-91]十进制(byte)
[10111101]补 = [11000011]原= [-67]十进制(byte)

在Java中,数值类型的数据才使用补码表示;而对于字符来说,它是通过字符集(如UTF-8、GBK等)进行编码的,直接存储字节数即可。

字符集

Java中的常用字符编码ASCII、Unicode和UTF-8

在计算机内部,所有的信息最终都表示为一个二进制的字符串。每一个二进制位(bit)有0和1两种状态,因此八个二进制位就可以组合出256种状态,这被称为一个字节(byte)。也就是说,一个字节一共可以用来表示256种不同的状态,每一个状态对应一个符号,就是256个符号,从0000000到11111111。

ASCII码:上个世纪60年代,美国制定了一套字符编码,对英语字符与二进制位之间的关系,做了统一规定。这被称为ASCII码,一直沿用至今。ASCII码一共规定了128个字符的编码,比如空格“SPACE”是32(二进制00100000),大写的字母A是65(二进制01000001)。这128个符号(包括32个不能打印出来的控制符号),只占用了一个字节的后面7位,最前面的1位统一规定为0。

UTF-8:UTF-8是Unicode的实现方式之一,UTF-8是一种变长的编码方式。它可以使用1~4个字节表示一个符号,根据不同的符号而变化字节长度。UTF-8的编码规则:

  • 单字节的符号,字节的第一位(bit)规定为0,后面7位为这个符号的Unicode码。因此对于英语字母,UTF-8编码和ASCII码是相同的;
  • n字节的符号(n>1),第一个字节的前n位都设为1,第n+1位设为0,后面字节的前两位一律设为10。剩下的没有提及的二进制位,全部为这个符号的Unicode码;

编码方式

Base64编码

Base64笔记

Base64,即选出64个字符—-小写字母a-z、大写字母A-Z、数字0-9、符号”+”、”/“(再加上作为垫字的”=”,实际上是65个字符)—-作为一个基本字符集,然后其他所有符号都转换成这个字符集中的字符。

Base64转换方式:

  1. 将每三个字节作为一组,一共是24个二进制位;
  2. 将这24个二进制位分为四组,每个组有6个二进制位;
  3. 在每组前面加两个00,扩展成32个二进制位,即四个字节;
  4. 根据下表,得到扩展后的每个字节的对应符号,这就是Base64的编码值;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
0 A  17 R   34 i   51 z

1 B  18 S   35 j   52 0

2 C  19 T   36 k   53 1

3 D  20 U   37 l   54 2

4 E  21 V   38 m   55 3

5 F  22 W   39 n   56 4

6 G  23 X   40 o   57 5

7 H  24 Y   41 p   58 6

8 I  25 Z   42 q   59 7

9 J  26 a   43 r   60 8

10 K  27 b   44 s   61 9

11 L  28 c   45 t   62 +

12 M  29 d   46 u   63 /

13 N  30 e   47 v

14 O  31 f   48 w   

15 P  32 g   49 x

16 Q  33 h   50 y

对称加密和非对称加密

对称加密:加密方和解密方使用同一密钥;加密和解密的速度比较快,适用于需要加密的数据量比较大时;密钥传输的过程不安全,且容易被破解,密钥管理也比较麻烦:

  • DES(Data Encryption Standard):数据加密标准,速度较快,适用于加密大量数据的场合;
  • 3DES(Triple DES):是基于DES,对一块数据用三个不同的密钥进行三次加密,强度更高;
  • AES(Advanced Encryption Standard):高级加密标准,是下一代的加密算法标准,速度快,安全级别高;

非对称加密:即公私钥加密,加密和解密使用的是两个不同的密钥,假设两个用户要加密交换数据,双方交换公钥,使用时一方用对方的公钥加密,另一方即可用自己的私钥解密:

  • RSA:由 RSA 公司发明,是一个支持变长密钥的公共密钥算法,需要加密的文件块的长度也是可变;
  • DSA(Digital Signature Algorithm):数字签名算法,是一种标准的 DSS(数字签名标准);
  • ECC(Elliptic Curves Cryptography):椭圆曲线密码编码学;

公钥负责加密,私钥负责解密私钥负责签名,公钥负责验证

散列算法:

散列是信息的提炼,通常其长度要比信息小得多,且为一个固定长度。加密性强的散列一定是不可逆的,这就意味着通过散列结果,无法推出任何部分的原始信息。散列应该是防冲突的,即找不出具有相同散列结果的两条信息。具有这些特性的散列结果就可以用于验证信息是否被修改。

散列算法是一种单向算法,用户可以通过Hash算法对目标信息生成一段特定长度的、唯一的Hash值,却不能通过这个Hash值重新获得目标信息。因此Hash算法常用在不可还原的密码存储、信息完整性校验等。

常见的Hash算法:

  • MD5(Message Digest Algorithm 5):是RSA数据安全公司开发的一种单向散列算法,非可逆,相同的明文产生相同的密文
  • SHA(Secure Hash Algorithm):可以对任意长度的数据运算生成一个160位的数值

HTTPS涉及到的加密、解密

你知道,HTTPS用的是对称加密还是非对称加密

HTTPS 在内容传输的加密上使用的是对称加密,非对称加密只作用在证书验证阶段

HTTPS的整体过程分为证书验证和数据传输阶段,具体的交互过程如下:

① 证书验证阶段:

1)浏览器发起 HTTPS 请求;
2)服务端返回 HTTPS 证书;
3)客户端验证证书是否合法,如果不合法则提示告警。

② 数据传输阶段:

1)当证书验证合法后,在本地生成随机数;
2)通过公钥加密随机数,并把加密后的随机数传输到服务端;
3)服务端通过私钥对随机数进行解密;
4)服务端通过客户端传入的随机数构造对称加密算法,对返回结果内容进行加密后传输。

Java实现

非对称加密、解密

生成公钥、私钥:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// KeyPairGenerator类用于生成公钥和私钥对,基于RSA算法生成对象
KeyPairGenerator keyPairGen = KeyPairGenerator.getInstance("RSA");

// 初始化密钥对生成器,密钥大小为96-1024位
keyPairGen.initialize(1024, new SecureRandom());

// 生成一个密钥对,保存在keyPair中
KeyPair keyPair = keyPairGen.generateKeyPair();

RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();

// 得到公钥字符串
String publicKeyStr = new String(Base64.getEncoder().encode(publicKey.getEncoded()));

// 得到私钥字符串
String privateKeyStr = new String(Base64.getEncoder().encode(privateKey.getEncoded()));

使用公钥进行加密:

1
2
3
4
5
6
7
8
String expr = "i love you";

RSAPublicKey pubKey = (RSAPublicKey) KeyFactory.getInstance("RSA").generatePublic(new X509EncodedKeySpec(Base64.getDecoder().decode(publicKeyStr)));

Cipher cipherEcry = Cipher.getInstance("RSA");
cipherEcry.init(Cipher.ENCRYPT_MODE, pubKey);

String encryStr = Base64.getEncoder().encodeToString(cipherEcry.doFinal(expr.getBytes()));

使用私钥进行解密:

1
2
3
4
5
6
7
8
byte[] inputByte = Base64.getDecoder().decode(encryStr.getBytes());

RSAPrivateKey priKey = (RSAPrivateKey)KeyFactory.getInstance("RSA").generatePrivate(new PKCS8EncodedKeySpec(Base64.getDecoder().decode(privateKeyStr)));

Cipher cipherDecry = Cipher.getInstance("RSA");
cipherDecry.init(Cipher.DECRYPT_MODE, priKey);

String decryStr = new String(cipherDecry.doFinal(inputByte));

消息摘要

1
2
3
4
5
6
String message = "i love you";
// MessageDigest md = MessageDigest.getInstance("SHA-1");
MessageDigest md = MessageDigest.getInstance("MD5");
md.update(message.getBytes());
byte[] digest = md.digest();
System.out.println("消息摘要:" + byte2hex(digest));

数字签名与验证

生成公钥、私钥文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java.security.KeyPairGenerator keygen =
java.security.KeyPairGenerator.getInstance("DSA");
keygen.initialize(512);
// 生成密钥组
KeyPair keys = keygen.genKeyPair();
PublicKey pubkey = keys.getPublic();
PrivateKey prikey = keys.getPrivate();
// 将私钥写入文件中
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("private.key"));
out.writeObject(prikey);
out.close();

// 将公钥写入文件中
out = new ObjectOutputStream(new FileOutputStream("public.key"));
out.writeObject(pubkey);
out.close();

生成数字签名:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 从文件中读取私钥,将一个字符串以及其签名保存在一个文件中(字符串和其签名也可以放在不同文件中/‘)
ObjectInputStream in = new ObjectInputStream(new FileInputStream("private.key"));
PrivateKey priKey = (PrivateKey) in.readObject();
in.close();
String message = "i love you";
// 用私钥对信息生成数字签名
Signature signature = Signature.getInstance("DSA");
signature.initSign(priKey);
signature.update(message.getBytes());
// 对信息的数字签名
byte[] signed = signature.sign();
// 把信息和数字签名保存在一个文件中
ObjectOutputStream outTarget = new ObjectOutputStream(new FileOutputStream("signed"));
outTarget.writeObject(message);
outTarget.writeObject(signed);
out.close();

对数字签名进行验证:

1
2
3
4
5
6
7
8
9
10
11
12
ObjectInputStream targetIn = new ObjectInputStream(new FileInputStream("public.key"));
PublicKey pubKey = (PublicKey)targetIn.readObject();
targetIn.close();
System.out.println("-----format---" + pubKey.getFormat());
targetIn = new ObjectInputStream(new FileInputStream("signed"));
String msg = (String)targetIn.readObject();
byte[] sign = (byte[])targetIn.readObject();
targetIn.close();
Signature check = Signature.getInstance("DSA");
check.initVerify(pubKey);
check.update(msg.getBytes());
System.out.println(check.verify(sign));