9f710fcc6b4118466703459df73c30d8.jpeg

一、明文与密文

1、sdvvzrug fdhvdu 是什么?

🍬
**密文字母表
D E F G H I J K L M N O P Q R S T U V W X Y Z A B C
明文字母表
A B C D E F G H I J K L M N O P Q R S T U V W X Y Z

2、凯撒密码

在密码学中,恺撒密码(英语:Caesar cipher),或称恺撒加密、恺撒变换、变换加密,是一种最简单且最广为人知的加密技术。
image.png

它是一种替换加密的技术,明文中的所有字母都在字母表上向后(或向前)按照一个固定数目进行偏移后被替换成密文。例如,当偏移量是3的时候,所有的字母A将被替换成D,B变成E,以此类推。
凯撒密码最早由古罗马军事统帅盖乌斯·尤利乌斯·凯撒在军队中用来传递加密信息,故称凯撒密码。

3、明文与密文有何区别?

  • 形式不同

image.png
🍬
1.jpg

  • 有意义/无意义

{"password" : "e10adc3949ba59abbe56e057f20f883e"}

**token: **9bf4cb94-7350-45bc-bd59-ded3ed6be244

  • 可理解/不可理解

image.png

猪圈密码(亦称朱高密码、共济会暗号、共济会密码或共济会员密码),是一种以格子为基础的简单替代式密码,曾经是美国内战时盟军使用的密码,目前仅在密码教学、各种竞赛中使用。

image.png
从程序的角度来解释:明文到密文的过程就是函数变换

二、密码学的历史

密码学早在公元前400多年就已经产生,人类使用密码的历史几乎与使用文字的时间一样长。

1、古典密码学

  • 姜子牙的“阴符”和“阴书”

中国最早的密码可追溯到商周时期的“阴符”和“阴书”,其发明者是赫赫有名的姜子牙。
据《太公六韬》记载,一次姜子牙被敌军围困,欲向周文王请求援兵。
为了让文王相信事情的真实性,将自己心爱的鱼竿截成不同长度的竹段,装入匣子,作为信物交给信使,向周文王派兵求援。
文王见到使者后,将竹段拼接起来一看,果然是姜太公随身之物,立马派兵救援,成功化解了这次危机。
后来,姜子牙受“鱼竿传信”的启发,发明了“阴符”。阴符由八个长短不一的竹片组成。
前四个比较长的用于传递不同的战争结果;后四截短的则用于代表各种紧急军情。
image.pngimage.png

  • 斯巴达人的“密码棒”

古希腊的斯巴达由于地理位置特殊,人口较少,经常受到北部游牧民族的攻击和波斯帝国的威胁。这种险恶的环境造就了斯巴达人好战的性格和“全民皆兵”的社会体制。
为保证战争中信息频繁传递的安全性,公元前404年,斯巴达人发明了“塞塔式密码”,俗称“密码棒”:
发件人将皮条缠绕在一定大小、粗细的木棒上书写信息;送信者将皮条缠在腰上以方便携带;收件人拿到皮条后,用同样规格的木棒缠绕布条,以解读加密信息。
image.png

  • 凯撒密码

当时有人专门做了方便译码的硬币,中间的小字母圆盘可以转动,以快速找到对应的字母。
image.png

2、近代密码学

密码形成一门新的学科是在20世纪70年代,这是受计算机科学蓬勃发展刺激和推动的结果。

Arthur Scherbius于1919年设计出了历史上最著名的密码机—德国的Enigma机,,在二次世界大战期间, Enigma曾作为德国陆、海、空三军最高级密码机。Enigma机使用了3个正规轮和1个反射轮。这使得英军从1942年2月到12月都没能解读出德国潜艇发出的信号。
Enigma转轮组的加密原理,正是多表替代——它通过不断改变明文和密文的字母映射关系,对明文字母们进行着连续不断的换表加密操作。
image.png

  • 三个转子不同的方向组成了262626=17576种不同可能性;
  • 三个转子间不同的相对位置为6种可能性;
  • 连接板上两两交换6对字母的可能性数目非常巨大,有100391791500种;于是一共有175766100391791500,大约为10000000000000000,即一亿亿种可能性。

《模仿游戏》 海报
“计算机科学之父”艾伦·图灵,协助盟军破译德国密码系统“英格玛”,从而扭转二战战局。
image.png

3、现代密码学

1976 年,美国密码学家提出“公钥密码”概念。此类密码中加密和解密使用不同的密钥,其中,用于加密的叫做公钥,用于解密的为私钥。

1977年,美国麻省理工学院提出第一个公钥加密算法RSA算法,之后ElGamal、椭圆曲线、双线性对等公钥密码相继被提出,密码学真正进入了一个新的发展时期。

一般来说,公钥密码的安全性由相应数学问题在计算机上的难解性来保证,以广为使用的RSA算法为例,它的安全性是建立在大整数素因子分解在计算机上的困难性,如,对于整数22,我们易于发现它可以分解为2和11两个素数相乘,但对于一个500位的整数,即使采用相应算法,也要很长时间才能完成分解。

但随着计算能力的不断增强和因子分解算法的不断改进,特别是量子计算机的发展,公钥密码安全性也渐渐受到威胁。

4、量子密码学

  • 测不准原理(量子世界粒子运动规律)

微观世界的粒子有许多共轭量(位置和速度)。在某一时刻,人们只能对一个共轭量之一进行测量,不能同时测量另一个与之共轭的量。更加奇怪的是,量子态叫做「只能测一次」。只要对任何一个量子态做一次测量,那个量子态就会坍塌,彻底变成另外一个状态;那么不会有第二次机会。

  • 不可克隆定理

量子的世界不同于经典的世界,经典的世界复印机可以复印相同的一份文件;但是在量子世界却不存在,复制后将破坏原来的量子态。

  • 量子纠缠特性

两个纠缠的量子,无论相隔多远,改变其中一个量子状态时,另一个量子会在瞬间发生状态变化。

科学家可以用光子形成的量子态,传输一组任意长的随机密钥。这个密钥非常安全,发送者和接收者可以放心地用它来加密或解密一段信息。不用担心窃听,不用担心伪造,因为量子物理学中的“测不准原理”和“不可克隆定律”,保证了它的完全性。这个理论就是后来支撑了量子密码学半边天的量子密钥分发。

1984年,Bennett(贝内特)和Brassard(布拉萨德)最早提出了量子密码协议-BB84协议。该密码术与经典密码最大区别是它能抵挡任何破译技术和计算工具的攻击,原因在于它的安全性是由物理定律来保证,而不是靠某种高复杂的运算。

经过30多年的发展,量子密码学已经发展成为一门理论与实验交相呼应的成熟学科。1989年,科学家在32.5厘米的距离上,第一次验证了BB84协议的设想。在2016年8月,中国发射了世界首颗量子科学实验卫星“墨子号”,成功地将这一距离拓展到了1200千米。
image.png

5、密码学的未来

公钥密码体制的安全性均依赖于数学难题(大整数分解难题和离散对数求解难题)的困难性.然而这些问题在量子计算情形下经过 Shor 算法均可变为易解问题——P问题(即计算问题),因而我们可以断言量子计算机出现之日,便是现今密码寿终正寝之日。因此,研究抗量子的计算的密码算法是未来密码学的新的研究方向。

三、哈希算法

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

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

  • 相同的输入一定得到相同的输出;
  • 不同的输入大概率得到不同的输出。

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

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

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

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

1、哈希碰撞

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

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

那么问题来了:碰撞能不能避免?🍬

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

  • 碰撞概率低;
  • 不能猜测输出。

不能猜测输出是指,输入的任意一个bit的变化会造成输出完全不同,这样就很难从输出反推输入(只能依靠暴力穷举)。假设一种哈希算法有如下规律:

  1. hashA("java001") = "123456"
  2. hashA("java002") = "123457"
  3. hashA("java003") = "123458"

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

  1. hashB("java001") = "123456"
  2. hashB("java002") = "580271"
  3. hashB("java003") = ???

常用的哈希算法有:

算法 输出长度(位) 输出长度(字节)
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

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

2、哈希算法的用途

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

如何判断下载到本地的软件是原始的、未经篡改的文件?我们只需要自己计算一下本地文件的哈希值,再与官网公开的哈希值对比,如果相同,说明文件下载正确,否则,说明文件已被篡改。
哈希算法的另一个重要用途是存储用户口令。如果直接将用户的原始口令存放到数据库中,会产生极大的安全风险:

  • 数据库管理员能够看到用户明文口令;
  • 数据库数据一旦泄漏,黑客即可获取用户明文口令。

不存储用户的原始口令,那么如何对用户进行认证?
方法是存储用户口令的哈希,例如,MD5。
在用户输入原始口令后,系统计算用户输入的原始口令的MD5并与数据库存储的MD5对比,如果一致,说明口令正确,否则,口令错误。
因此,数据库存储用户名和口令的表内容应该像下面这样:

username password
bob f30aa7a662c728b7407c54ae6bfd27d1
alice 25d55ad283aa400af464c76d713c07ad
tim bed128365216c019988915ed3add75fb

这样一来,数据库管理员看不到用户的原始口令。即使数据库泄漏,黑客也无法拿到用户的原始口令。想要拿到用户的原始口令,必须用暴力穷举的方法,一个口令一个口令地试,直到某个口令计算的MD5恰好等于指定值。
使用哈希口令时,还要注意防止 彩虹表攻击
什么是彩虹表呢?上面讲到了,如果只拿到MD5,从MD5反推明文口令,只能使用暴力穷举的方法。
然而黑客并不笨,暴力穷举会消耗大量的算力和时间。但是,如果有一个预先计算好的常用口令和它们的MD5的对照表:

常用口令 MD5
hello123 f30aa7a662c728b7407c54ae6bfd27d1
12345678 25d55ad283aa400af464c76d713c07ad
passw0rd bed128365216c019988915ed3add75fb
19700101 570da6d5277a646f6552b8832012f5dc
20201231 6879c0ae9117b50074ce0a0d4c843060

这个表就是彩虹表。如果用户使用了常用口令,黑客从MD5一下就能反查到原始口令:
bob的MD5:f30aa7a662c728b7407c54ae6bfd27d1,原始口令:hello123;
alice的MD5:25d55ad283aa400af464c76d713c07ad,原始口令:12345678;
tim的MD5:bed128365216c019988915ed3add75fb,原始口令:passw0rd。
这就是为什么不要使用常用密码,以及不要使用生日作为密码的原因。
即使用户使用了常用口令,我们也可以采取措施来抵御彩虹表攻击,方法是对每个口令额外添加随机数,这个方法称之为加盐(salt):

  1. digest = md5(salt+inputPassword)

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

username salt password
bob H1r0a a5022319ff4c56955e22a74abcc2c210
alice 7$p2w e5de688c99e961ed6e560b972dab8b6a
tim z5Sk9 1eee304b92dc0d105904e7ab58fd2f64

加盐的目的在于使黑客的彩虹表失效,即使用户使用常用口令,也无法从MD5反推原始口令。

3、SHA-1

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

注意:MD5因为输出长度较短,短时间内破解是可能的,目前已经不推荐使用。

四、多种加密方式

1、对称加密

对称加密算法就是传统的用一个密码进行加密和解密。例如,我们常用的WinZIP和WinRAR对压缩包的加密和解密,就是使用对称加密算法:
[算法] 加密算法漫谈 - 图15
从程序的角度看,所谓加密,就是这样一个函数,它接收密码和明文,然后输出密文:
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算法由于密钥过短,可以在短时间内被暴力破解,所以现在已经不安全了。

使用AES加密

AES算法是目前应用最广泛的加密算法。我们先用ECB模式加密并解密。

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

  1. 根据算法名称/工作模式/填充模式获取Cipher实例;
  2. 根据算法名称初始化一个SecretKey实例,密钥必须是指定长度;
  3. 使用SerectKey初始化Cipher实例,并设置加密或解密模式;
  4. 传入明文或密文,获得密文或明文。

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

  1. import java.security.*;
  2. import java.util.Base64;
  3. import javax.crypto.*;
  4. import javax.crypto.spec.*;
  5. public class Main {
  6. public static void main(String[] args) throws Exception {
  7. // 原文:
  8. String message = "Hello, world!";
  9. System.out.println("Message: " + message);
  10. // 128位密钥 = 16 bytes Key:
  11. byte[] key = "1234567890abcdef".getBytes("UTF-8");
  12. // 加密:
  13. byte[] data = message.getBytes("UTF-8");
  14. byte[] encrypted = encrypt(key, data);
  15. System.out.println("Encrypted: " + Base64.getEncoder().encodeToString(encrypted));
  16. // 解密:
  17. byte[] decrypted = decrypt(key, encrypted);
  18. System.out.println("Decrypted: " + new String(decrypted, "UTF-8"));
  19. }
  20. // 加密:
  21. public static byte[] encrypt(byte[] key, byte[] input) throws GeneralSecurityException {
  22. Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
  23. SecretKey keySpec = new SecretKeySpec(key, "AES");
  24. cipher.init(Cipher.ENCRYPT_MODE, keySpec);
  25. return cipher.doFinal(input);
  26. }
  27. // 解密:
  28. public static byte[] decrypt(byte[] key, byte[] input) throws GeneralSecurityException {
  29. Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
  30. SecretKey keySpec = new SecretKeySpec(key, "AES");
  31. cipher.init(Cipher.DECRYPT_MODE, keySpec);
  32. return cipher.doFinal(input);
  33. }
  34. }

密钥长度由算法设计决定,AES的密钥长度是128/192/256位;
使用对称加密算法需要指定算法名称、工作模式和填充模式。

2、口令加密

AES加密,密钥长度是固定的128/192/256位,而不是我们用WinZip/WinRAR那样,随便输入几位都可以。
这是因为对称加密算法决定了口令必须是固定长度,然后对明文进行分块加密。又因为安全需求,口令长度往往都是128位以上,即至少16个字符。
但是我们平时使用的加密软件,输入6位、8位都可以,难道加密方式不一样?
实际上用户输入的口令并不能直接作为AES的密钥进行加密(除非长度恰好是128/192/256位),并且用户输入的口令一般都有规律,安全性远远不如安全随机数产生的随机口令。因此,用户输入的口令,通常还需要使用PBE算法,采用随机数杂凑计算出真正的密钥,再进行加密。
PBE就是Password Based Encryption的缩写,它的作用如下:

  1. key = generate(userPassword, secureRandomPassword);

PBE的作用就是把用户输入的口令和一个安全随机的口令采用杂凑后计算出真正的密钥。以AES密钥为例,我们让用户输入一个口令,然后生成一个随机数,通过PBE算法计算出真正的AES口令,再进行加密,代码如下:

  1. public class Main {
  2. public static void main(String[] args) throws Exception {
  3. // 把BouncyCastle作为Provider添加到java.security:
  4. Security.addProvider(new BouncyCastleProvider());
  5. // 原文:
  6. String message = "Hello, world!";
  7. // 加密口令:
  8. String password = "hello12345";
  9. // 16 bytes随机Salt:
  10. byte[] salt = SecureRandom.getInstanceStrong().generateSeed(16);
  11. System.out.printf("salt: %032x\n", new BigInteger(1, salt));
  12. // 加密:
  13. byte[] data = message.getBytes("UTF-8");
  14. byte[] encrypted = encrypt(password, salt, data);
  15. System.out.println("encrypted: " + Base64.getEncoder().encodeToString(encrypted));
  16. // 解密:
  17. byte[] decrypted = decrypt(password, salt, encrypted);
  18. System.out.println("decrypted: " + new String(decrypted, "UTF-8"));
  19. }
  20. // 加密:
  21. public static byte[] encrypt(String password, byte[] salt, byte[] input) throws GeneralSecurityException {
  22. PBEKeySpec keySpec = new PBEKeySpec(password.toCharArray());
  23. SecretKeyFactory skeyFactory = SecretKeyFactory.getInstance("PBEwithSHA1and128bitAES-CBC-BC");
  24. SecretKey skey = skeyFactory.generateSecret(keySpec);
  25. PBEParameterSpec pbeps = new PBEParameterSpec(salt, 1000);
  26. Cipher cipher = Cipher.getInstance("PBEwithSHA1and128bitAES-CBC-BC");
  27. cipher.init(Cipher.ENCRYPT_MODE, skey, pbeps);
  28. return cipher.doFinal(input);
  29. }
  30. // 解密:
  31. public static byte[] decrypt(String password, byte[] salt, byte[] input) throws GeneralSecurityException {
  32. PBEKeySpec keySpec = new PBEKeySpec(password.toCharArray());
  33. SecretKeyFactory skeyFactory = SecretKeyFactory.getInstance("PBEwithSHA1and128bitAES-CBC-BC");
  34. SecretKey skey = skeyFactory.generateSecret(keySpec);
  35. PBEParameterSpec pbeps = new PBEParameterSpec(salt, 1000);
  36. Cipher cipher = Cipher.getInstance("PBEwithSHA1and128bitAES-CBC-BC");
  37. cipher.init(Cipher.DECRYPT_MODE, skey, pbeps);
  38. return cipher.doFinal(input);
  39. }
  40. }

使用PBE时,我们还需要引入BouncyCastle,并指定算法是PBEwithSHA1and128bitAES-CBC-BC。观察代码,实际上真正的AES密钥是调用Cipher的init()方法时同时传入SecretKey和PBEParameterSpec实现的。在创建PBEParameterSpec的时候,我们还指定了循环次数1000,循环次数越多,暴力破解需要的计算量就越大。
如果我们把salt和循环次数固定,就得到了一个通用的“口令”加密软件。如果我们把随机生成的salt存储在U盘,就得到了一个“口令”加USB Key的加密软件,它的好处在于,即使用户使用了一个非常弱的口令,没有USB Key仍然无法解密,因为USB Key存储的随机数密钥安全性非常高。

3、密钥交换算法

对称加密算法解决了数据加密的问题。我们以AES加密为例,在现实世界中,小明要向路人甲发送一个加密文件,他可以先生成一个AES密钥,对文件进行加密,然后把加密文件发送给对方。因为对方要解密,就必须需要小明生成的密钥。

现在问题来了:如何传递密钥?

在不安全的信道上传递加密文件是没有问题的,因为黑客拿到加密文件没有用。但是,如何如何在不安全的信道上安全地传输密钥?
要解决这个问题,密钥交换算法即DH算法:Diffie-Hellman算法应运而生。
DH算法解决了密钥在双方不直接传递密钥的情况下完成密钥交换,这个神奇的交换原理完全由数学理论支持。
我们来看DH算法交换密钥的步骤。假设甲乙双方需要传递密钥,他们之间可以这么做:

  1. 首首先选择一个素数p,例如509,底数g,任选,例如5,随机数a,例如123,然后计算A=g^a mod p,结果是215,然后,发送p=509,g=5,A=215给

  2. 方收到后,也选择一个随机数b,例如,456,然后计算B=g^b mod p,结果是181,再同时计算s=A^b mod p,结果是121;

  3. 把计算的B=181发给计算s=B^a mod p的余数,计算结果与算出的结果一样,都是121。

所以最终双方协商出的密钥s是121。注意到这个密钥s并没有在网络上传输。而通过网络传输的p,g,A和B是无法推算出s的,因为实际算法选择的素数是非常大的。
所以,更确切地说,DH算法是一个密钥协商算法,双方最终协商出一个共同的密钥,而这个密钥不会通过网络传输。
如果我们把a看成甲的私钥,A看成甲的公钥,b看成乙的私钥,B看成乙的公钥,DH算法的本质就是双方各自生成自己的私钥和公钥,私钥仅对自己可见,然后交换公钥,并根据自己的私钥和对方的公钥,生成最终的密钥secretKey,DH算法通过数学定律保证了双方各自计算出的secretKey是相同的。

但是DH算法并未解决中间人攻击,即甲乙双方并不能确保与自己通信的是否真的是对方。消除中间人攻击需要其他方法。

4、非对称加密

从DH算法我们可以看到,公钥-私钥组成的密钥对是非常有用的加密方式,因为公钥是可以公开的,而私钥是完全保密的,由此奠定了非对称加密的基础。
非对称加密就是加密和解密使用的不是相同的密钥:只有同一个公钥-私钥对才能正常加解密。
因此,如果小明要加密一个文件发送给小红,他应该首先向小红索取她的公钥,然后,他用小红的公钥加密,把加密文件发送给小红,此文件只能由小红的私钥解开,因为小红的私钥在她自己手里,所以,除了小红,没有任何人能解开此文件。

RSA算法

非对称加密的典型算法就是RSA算法,它是由Ron Rivest,Adi Shamir,Leonard Adleman这三个哥们一起发明的,所以用他们仨的姓的首字母缩写表示。
非对称加密相比对称加密的显著优点在于,对称加密需要协商密钥,而非对称加密可以安全地公开各自的公钥,在N个人之间通信的时候:使用非对称加密只需要N个密钥对,每个人只管理自己的密钥对。而使用对称加密需要则需要N(N-1)/2个密钥,因此每个人需要管理N-1个密钥,密钥管理难度大,而且非常容易泄漏。
既然非对称加密这么好,那我们抛弃对称加密,完全使用非对称加密行不行?也不行。因为非对称加密的缺点就是*运算速度非常慢,比对称加密要慢很多

所以,在实际应用的时候,非对称加密总是和对称加密一起使用。假设小明需要给小红需要传输加密文件,他俩首先交换了各自的公钥,然后:

  1. 小明生成一个随机的AES口令,然后用小红的公钥通过RSA加密这个口令,并发给小红;
  2. 小红用自己的RSA私钥解密得到AES口令;
  3. 双方使用这个共享的AES口令用AES加密通信。

可见非对称加密实际上应用在第一步,即加密“AES口令”。这也是我们在浏览器中常用的HTTPS协议的做法,即浏览器和服务器先通过RSA交换AES口令,接下来双方通信实际上采用的是速度较快的AES对称加密,而不是缓慢的RSA非对称加密。

以RSA算法为例,它的密钥有256/512/1024/2048/4096等不同的长度。长度越长,密码强度越大,当然计算速度也越慢。
如果修改待加密的byte[]数据的大小,可以发现,使用512bit的RSA加密时,明文长度不能超过53字节,使用1024bit的RSA加密时,明文长度不能超过117字节,这也是为什么使用RSA的时候,总是配合AES一起使用,即用AES加密任意长度的明文,用RSA加密AES口令。
此外,只使用非对称加密算法不能防止中间人攻击。

5、签名算法

我们使用非对称加密算法的时候,对于一个公钥-私钥对,通常是用公钥加密,私钥解密。
如果使用私钥加密,公钥解密是否可行呢?实际上是完全可行的。
不过我们再仔细想一想,私钥是保密的,而公钥是公开的,用私钥加密,那相当于所有人都可以用公钥解密。这个加密有什么意义?
这个加密的意义在于,如果小明用自己的私钥加密了一条消息,比如小明喜欢小红,然后他公开了加密消息,由于任何人都可以用小明的公钥解密,从而使得任何人都可以确认小明喜欢小红这条消息肯定是小明发出的,其他人不能伪造这个消息,小明也不能抵赖这条消息不是自己写的。
因此,私钥加密得到的密文实际上就是数字签名,要验证这个签名是否正确,只能用私钥持有者的公钥进行解密验证。使用数字签名的目的是为了确认某个信息确实是由某个发送方发送的,任何人都不可能伪造消息,并且,发送方也不能抵赖。
在实际应用的时候,签名实际上并不是针对原始消息,而是针对原始消息的哈希进行签名,即:

  1. signature = encrypt(privateKey, sha256(message))

对签名进行验证实际上就是用公钥解密:

  1. hash = decrypt(publicKey, signature)

然后把解密后的哈希与原始消息的哈希进行对比。
因为用户总是使用自己的私钥进行签名,所以,私钥就相当于用户身份。而公钥用来给外部验证用户身份。
常用数字签名算法有:

  • MD5withRSA
  • SHA1withRSA
  • SHA256withRSA

它们实际上就是指定某种哈希算法进行RSA签名的方式。

使用其他公钥,或者验证签名的时候修改原始信息,都无法验证成功。

DSA签名

除了RSA可以签名外,还可以使用DSA算法进行签名。DSA是Digital Signature Algorithm的缩写,它使用ElGamal数字签名算法。
DSA只能配合SHA使用,常用的算法有:

  • SHA1withDSA
  • SHA256withDSA
  • SHA512withDSA

和RSA数字签名相比,DSA的优点是更快。

ECDSA签名

椭圆曲线签名算法ECDSA:Elliptic Curve Digital Signature Algorithm也是一种常用的签名算法,它的特点是可以从私钥推出公钥。比特币的签名算法就采用了ECDSA算法,使用标准椭圆曲线secp256k1。BouncyCastle提供了ECDSA的完整实现。

五、数字证书

我们知道,摘要算法用来确保数据没有被篡改,非对称加密算法可以对数据进行加解密,签名算法可以确保数据完整性和抗否认性,把这些算法集合到一起,并搞一套完善的标准,这就是 数字证书
因此,数字证书就是集合了多种密码学算法,用于实现数据加解密、身份认证、签名等多种功能的一种安全标准。
数字证书可以防止中间人攻击,因为它采用链式签名认证,即通过根证书(Root CA)去签名下一级证书,这样层层签名,直到最终的用户证书。而Root CA证书内置于操作系统中,所以,任何经过CA认证的数字证书都可以对其本身进行校验,确保证书本身不是伪造的。
我们在上网时常用的HTTPS协议就是数字证书的应用。浏览器会自动验证证书的有效性:
[算法] 加密算法漫谈 - 图16
要使用数字证书,首先需要创建证书。正常情况下,一个合法的数字证书需要经过CA签名,这需要认证域名并支付一定的费用。开发的时候,我们可以使用自签名的证书,这种证书可以正常开发调试,但不能对外作为服务使用,因为其他客户端并不认可未经CA签名的证书。
在Java程序中,数字证书存储在一种Java专用的key store文件中,JDK提供了一系列命令来创建和管理key store。我们用下面的命令创建一个key store,并设定口令123456:

  1. keytool -storepass 123456 -genkeypair -keyalg RSA -keysize 1024 -sigalg SHA1withRSA -validity 3650 -alias mycert -keystore my.keystore -dname "CN=www.sample.com, OU=sample, O=sample, L=BJ, ST=BJ, C=CN"

几个主要的参数是:

  • keyalg:指定RSA加密算法;
  • sigalg:指定SHA1withRSA签名算法;
  • validity:指定证书有效期3650天;
  • alias:指定证书在程序中引用的名称;
  • dname:最重要的CN=www.sample.com指定了Common Name,如果证书用在HTTPS中,这个名称必须与域名完全一致。

执行上述命令,JDK会在当前目录创建一个my.keystore文件,并存储创建成功的一个私钥和一个证书,它的别名是mycert。
有了key store存储的证书,我们就可以通过数字证书进行加解密和签名:

  1. import java.io.InputStream;
  2. import java.math.BigInteger;
  3. import java.security.*;
  4. import java.security.cert.*;
  5. import javax.crypto.Cipher;
  6. public class Main {
  7. public static void main(String[] args) throws Exception {
  8. byte[] message = "Hello, use X.509 cert!".getBytes("UTF-8");
  9. // 读取KeyStore:
  10. KeyStore ks = loadKeyStore("/my.keystore", "123456");
  11. // 读取私钥:
  12. PrivateKey privateKey = (PrivateKey) ks.getKey("mycert", "123456".toCharArray());
  13. // 读取证书:
  14. X509Certificate certificate = (X509Certificate) ks.getCertificate("mycert");
  15. // 加密:
  16. byte[] encrypted = encrypt(certificate, message);
  17. System.out.println(String.format("encrypted: %x", new BigInteger(1, encrypted)));
  18. // 解密:
  19. byte[] decrypted = decrypt(privateKey, encrypted);
  20. System.out.println("decrypted: " + new String(decrypted, "UTF-8"));
  21. // 签名:
  22. byte[] sign = sign(privateKey, certificate, message);
  23. System.out.println(String.format("signature: %x", new BigInteger(1, sign)));
  24. // 验证签名:
  25. boolean verified = verify(certificate, message, sign);
  26. System.out.println("verify: " + verified);
  27. }
  28. static KeyStore loadKeyStore(String keyStoreFile, String password) {
  29. try (InputStream input = Main.class.getResourceAsStream(keyStoreFile)) {
  30. if (input == null) {
  31. throw new RuntimeException("file not found in classpath: " + keyStoreFile);
  32. }
  33. KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType());
  34. ks.load(input, password.toCharArray());
  35. return ks;
  36. } catch (Exception e) {
  37. throw new RuntimeException(e);
  38. }
  39. }
  40. static byte[] encrypt(X509Certificate certificate, byte[] message) throws GeneralSecurityException {
  41. Cipher cipher = Cipher.getInstance(certificate.getPublicKey().getAlgorithm());
  42. cipher.init(Cipher.ENCRYPT_MODE, certificate.getPublicKey());
  43. return cipher.doFinal(message);
  44. }
  45. static byte[] decrypt(PrivateKey privateKey, byte[] data) throws GeneralSecurityException {
  46. Cipher cipher = Cipher.getInstance(privateKey.getAlgorithm());
  47. cipher.init(Cipher.DECRYPT_MODE, privateKey);
  48. return cipher.doFinal(data);
  49. }
  50. static byte[] sign(PrivateKey privateKey, X509Certificate certificate, byte[] message)
  51. throws GeneralSecurityException {
  52. Signature signature = Signature.getInstance(certificate.getSigAlgName());
  53. signature.initSign(privateKey);
  54. signature.update(message);
  55. return signature.sign();
  56. }
  57. static boolean verify(X509Certificate certificate, byte[] message, byte[] sig) throws GeneralSecurityException {
  58. Signature signature = Signature.getInstance(certificate.getSigAlgName());
  59. signature.initVerify(certificate);
  60. signature.update(message);
  61. return signature.verify(sig);
  62. }
  63. }

在上述代码中,我们从key store直接读取了私钥-公钥对,私钥以PrivateKey实例表示,公钥以X509Certificate表示,实际上数字证书只包含公钥,因此,读取证书并不需要口令,只有读取私钥才需要。如果部署到Web服务器上,例如Nginx,需要把私钥导出为Private Key格式,把证书导出为X509Certificate格式。
以HTTPS协议为例,浏览器和服务器建立安全连接的步骤如下:

  1. 浏览器向服务器发起请求,服务器向浏览器发送自己的数字证书;
  2. 浏览器用操作系统内置的Root CA来验证服务器的证书是否有效,如果有效,就使用该证书加密一个随机的AES口令并发送给服务器;
  3. 服务器用自己的私钥解密获得AES口令,并在后续通讯中使用AES加密。

上述流程只是一种最常见的单向验证。如果服务器还要验证客户端,那么客户端也需要把自己的证书发送给服务器验证,这种场景常见于网银等。
注意:数字证书存储的是公钥,以及相关的证书链和算法信息。私钥必须严格保密,如果数字证书对应的私钥泄漏,就会造成严重的安全威胁。如果CA证书的私钥泄漏,那么该CA证书签发的所有证书将不可信。数字证书服务商DigiNotar就发生过私钥泄漏导致公司破产的事故。

六、补充

计算机加密技术就是为了实现上述目标,而现代计算机密码学理论是建立在严格的数学理论基础上的,密码学已经逐渐发展成一门科学。对于绝大多数开发者来说,设计一个安全的加密算法非常困难,验证一个加密算法是否安全更加困难,当前被认为安全的加密算法仅仅是迄今为止尚未被攻破。因此,要编写安全的计算机程序,我们要做到:

  • 不要自己设计山寨的加密算法;
  • 不要自己实现已有的加密算法;
  • 不要自己修改已有的加密算法。

参(kao)考(bei)资料: 加密与安全 - 廖雪峰的官方网站 密码学发展简史