JavaEE鸿蒙应用开发HTML&JS+前端Python+大数据开发人工智能开发电商视觉设计软件测试新媒体+短视频直播运营产品经理集成电路应用开发(含嵌入式)Linux云计算+运维开发C/C++拍摄剪辑+短视频制作PMP项目管理认证电商运营Go语言与区块链大数据PHP工程师Android+物联网iOS.NET

【Java教程】看懂这篇文章-你就懂了信息安全的密码学

来源:黑马程序员

浏览2364人

2022.08.05

一、前言

       一个信息系统缺少不了信息安全模块,今天就带着大家全面了解并学习一下信息安全中的密码学知识,本文将会通过案例展示让你了解抽象的密码学知识,阅读本文你将会有如下收获:

  • 熟悉现代密码学体系包含的主流密码技术

  • 掌握Base64和Hex编码技术的特性与使用案例

  • 掌握对称密码和非对称密码的特性与使用案例

  • 掌握混合密码系统和随机数的特征与使用案例

二、关于密码

       提到密码,你的第一印象是什么?我们平时登录微信、邮箱都需要输入用户名和密码,或者用手机支付也需要输入支付密码,大部分人想到的可能就是这些情形中涉及到的密码。然而本文即将讨论的密码与此密码完全是不同的概念。实际上无论是微信还是支付宝或者其他系统要求输入的密码都只是一种身份验证的凭证,也就正确的密码是可以证明你是这个账号的主人的证据。这种密码准确来讲叫做口令比较合适,对应英文中的password、pin。

       本文中提到的密码是什么呢?实际上,密码(cryptography)是一个极其庞大复杂的信息处理体系,涉及到信息的机密性、完整性、认证、不可否认性等众多方面,由此衍生出的很多保护我们信息安全的技术,这些技术我们一般统称为密码技术。密码技术是密码学的核心。

       数学与密码技术的关系:数学是密码技术的基础,复杂的密码技术往往都会涉及到复杂的数学公式。

       密码学:密码学是网络安全、信息安全、区块链等领域的基础,常见的对称密码、公钥密钥、散列函数等,都属于密码学范畴。密码学中的密码技术比如“密码”可以让窃听者无法解读窃取的信息,“单项散列函数”可以检测出消息是否被篡改,“数字签名”可以确定消息是否来源于合法的发送者。

三、信息安全

1. 概念  

       信息安全是指信息网络的硬件、软件及其系统中的数据受到保护,不受偶然的或者恶意的原因而遭到破坏 、 更改 、泄露、否认等,系统连续可靠正常地运行,信息服务不中断。 信息安全安全是建立在以密码技术为基础的计算机安全领域,辅以通信技术、计算机技术与网络技术等方面的内容。

2. 与密码学的关系

  • 密码学是保障信息安全的核心技术 ,但不是提供信息安全的唯一方式 。

  • 信息安全是密码学研究与发展的目的 。

  • 信息安全的理论基础是密码学,信息安全的问题根本解决往往依靠密码学理论 。

3. 密码学与信息安全常识

  • 不要使用保密的密码算法

  • 使用低强度的密码比不进行加密更危险

  • 任何密码总有一天会被破解

  • 密码只是信息安全的一部分

四、现代密码学体系

       信息安全及密码学技术,是整个信息技术的基石。在西方语文中,密码学一词源于希腊语,krypto意思是隐藏,graphene是书写的意思。密码学的发展总共经历了四个阶段:远古密码、古典密码、近代密码和现代密码,这四个阶段的发展历史详细介绍可参见文章最后的附录部分,接下来本文内容主要介绍的是现代密码学所涉及到的密码知识。

1. 信息安全威胁与密码技术

信息安全威胁与密码技术.png

       该图完整的展示了信息安全面临的威胁与解决方案中会用到的密码技术,本文侧重于介绍有关于保证数据机密性的密码技术。

2. 密码算法及重要概念

密码系统组成.png

       将明文通过处理变换为密文的规则称为加密算法,将密文恢复成明文的规则称为解密算法,加密解密一起称为密码算法。

  • 明文 (Plaintext): 信息的原始数据

  • 密文(Ciphertext):明文经过编码变换所生成的数据

  • 加密(Encryption):对明文进行编码变换生成密文的过程

  • 解密(Decryption):将密文恢复成明文的过程

  • 密钥:密码算法需要用到密钥的,密钥就相当于保险库大门的钥匙,重要性不言而喻,所以切记不要泄露密码的密钥。    密钥 (Key):控制明文与密文之间相互变换的,分为加密密钥和解密密钥。

3. ASCII编码

       ASCII码 是现今最通用的单字节编码系统,并等同于国际标准ISO/IEC 646 。在这个页面,你可以找到8位的256个字符、ASCII码表和Windows-1252 (code page 1252,它是国际标准ISO 8859-1的一个扩展字符集) 标准保持一致;

       ASCII码American Standard Code for Information Interchange 的缩写,而不是ASCⅡ(罗马数字2),有很多人在这个地方产生误解;

       ASCII码 规范于1967年第一次发布,最后一次更新是在1986年,它包含了33个控制字符(具有某些特殊功能但是无法显示的字符)和95个可显示字符;

       ASCII码大致可以分作三部分组成。

       第一部分是:ASCII非打印控制字符

       第二部分是:ASCII打印字符

       第三部分是:扩展ASCII打印字符

ASCII表.jpg

ASCII扩展.jpg

3.1 第一部分:ASCII非打印控制字符表

       ASCII表上的数字0–31分配给了控制字符,用于控制像打印机等一些外围设备。例如,12代表换页/新页功能。此命令指示打印机跳到下一页的开头。(参详ASCII码表中0-31)

3.2 第二部分:ASCII打印字符

       数字 32–126 分配给了能在键盘上找到的字符,当您查看或打印文档时就会出现。数字127代表 DELETE 命令。(参详ASCII码表中32-127)

3.3 第三部分:扩展ASCII打印字符

       扩展的ASCII字符满足了对更多字符的需求。扩展的ASCII包含ASCII中已有的128个字符(数字0–32显示在下图中),又增加了128个字符,总共是256个。即使有了这些更多的字符,许多语言还是包含无法压缩到256个字符中的符号。因此,出现了一些ASCII的变体来囊括地区性字符和符号。例如,许多软件程序把ASCII表(又称作ISO8859-1)用于北美、西欧、澳大利亚和非洲的语言。

4. 字符串的ASCII码与二进制位

public class ASCIITest {

 @Test
   public void test01() {
       String str = "heima";
       byte[] bytes = str.getBytes();
       for (byte b : bytes) {
           //打印ascii码
           System.out.println(b);

           //获取二进制位
           String s = Integer.toBinaryString(b);
           System.out.println(s);
       }
   }
   
   
   @Test
   public void test02() throws UnsupportedEncodingException {
       String str = "黑马"; //中文UTF-8编码一个汉字占3个字节,中文GBK编码一个汉字占2个字节。
       byte[] bytes = str.getBytes("UTF-8");
       System.out.println("字节个数:"+ bytes.length);
       char[] chars = str.toCharArray();

       for (char c : chars) {
           //打印字符
           System.out.println(c);
           //字符类型会自动提升为int类型,获取二进制值(10进制转二进制)
           String s = Integer.toBinaryString(c);
           System.out.println(s);
       }
   }

}  

5. Hex编码与Base64编码

       我们知道在计算机中的字节共有256个组合,对应就是ascii码,而ascii码的128~255之间的值是不可见字符。而在网络上交换数据时,比如说从A地传到B地,往往要经过多个路由设备,由于不同的设备对字符的处理方式有一些不同,这样那些不可见字符就有可能被处理错误,这是不利于传输的。所以我们需要将字节转为正确的课可见字符,需要对字节数组进行进一步编码,常用编码格式用Hex编码和Base64编码。

5.1 Hex编码

1字节=8位2进制,比如小写字母a,ASCII表对应十进制为97,二进制表示为01100001

1字节=2位16进制,比如小写字母a,ASCII表对应十进制为97, 十六进制表示为61

进制转换.png

  • 因为一个字节中存在8个 bit可以表示256个字符,而非扩展 ASCII 码只能表示0-127种字符,为了能完整地表示一个字节,可以将二进制数据转换为十六进制数据的方式来实现。所以 Hex 编码也被称作为 Base16 编码,相比于原先8位表示一个字节,Hex 编码能够只用2位表示一个字节。Hex 编码最常用于二进制文件查看时展示的编码,如010Editor 就可以支持查看二进制文件。

  • 使用16个可见字符来表示一个二进制数组,编码后数据大小将x2

  • 1个字符需要用2个可见字符来表示

5.1.1 代码示例

引入依赖

 <dependencies>
       <dependency>
           <groupId>commons-codec</groupId>
           <artifactId>commons-codec</artifactId>
           <version>1.14</version>
       </dependency>
       <dependency>
           <groupId>commons-io</groupId>
           <artifactId>commons-io</artifactId>
           <version>2.6</version>
       </dependency>
       <dependency>
           <groupId>junit</groupId>
           <artifactId>junit</artifactId>
           <version>4.5</version>
           <scope>test</scope>
       </dependency>
 </dependencies>

单元测试:

public class HexDemoTest {

 @Test
   public void test01() {
       String data = "itcast" ;
       byte[] bytes = data.getBytes() ;
       //测试hex
       String encoded = Hex.encodeHexString(bytes) ;
       System.out.println(encoded);
   }

}  

5.2 base64编码

  • Base64编码要求把3个8位字节(3乘8=24)转化为4个6位的字节(4乘6=24),之后在6位的前面补两个0,形成8位一个字节的形式。 如果剩下的字符不足3个字节,则用0填充,输出字符使用'=',因此编码后输出的文本末尾可能会出现1或2个'='。为了保证所输出的编码位可读字符,Base64制定了一个编码表,以便进行统一转换。编码表的大小为2^6=64,这也是Base64名称的由来。标准base64只有64个字符(大写A到Z、小写a到z、数字0到9、“+”和“/”)以及用作后缀等号;

  • Base64是网络上最常见的用于传输8Bit字节码的可读性编码算法之一

  • 以每 3 个 字符(1Byte=8bit)为一组,然后针对每组,首先获取每个字符的 ASCII 编码(字符'a'=97=01100001),然后将 ASCII 编码转换成 8 bit 的二进制,得到一组 3 * 8=24 bit 的字节。然后再将这 24 bit 划分为 4 个 6 bit 的字节,并在每个 6 bit 的字节前面都填两个高位 0,得到 4 个 8 bit 的字节,然后将这 4 个 8 bit 的字节转换成十进制,对照 BASE64 编码表 (下表),得到对应编码后的字符。

  • 使用64个可见字符来表示一个二进制数组,编码后数据大小变成原来的4/3

  • 3个字符用4个可见字符来表示

Base64编码表.jpg

5.2.1 原理示例1-足够三字节

base64示例1.png第一步:"jay"、“a”、"n"对应的ASCII码值分别为106,97,121,对应的二进制值是01101010、01100001、01111001。如图第二三行所示,由此组成一个24位的二进制字符串。

第二步:如图第四行,将24位每6位二进制位一组分成四组。

第三步:在上面每一组前面补两个0(红色背景),扩展成32个二进制位,此时变为四个字节:00011010、00100110、00000101、00111001。分别对应的值(Base64编码索引)为:26、38、5、57。

第四步:用上面的值在Base64编码表中进行查找,分别对应:a、m、F、5。因此“jay”Base64编码之后就变为:amF5。

5.2.2 代码示例1-足够三字节

单元测试:

public class Base64DemoTest {

   @Test
   public void test01() {
       //jay正好三个字节,编码后输出的结果没有=
       System.out.println(Base64.encodeBase64String("jay".getBytes()));
   }
}
5.2.3 原理示例2-不够三字节

如果字节不足三个怎么办,分组的时候不组8位的都补0,计算没结果的=号代替

base64示例2.png5.2.4 代码示例2-不够三字节

单元测试:

public class Base64Demo {

   @Test
   public void test02() {
       //ja不够三个字节,编码后一定会有=
       System.out.println(Base64.encodeBase64String("ja".getBytes()));
   }
}

5.3 代码示例-编码与解码

单元测试:

/**
* hex编码与base64编码测试
*/
public class HexAndBase64Test {

   @Test
   public void test() throws DecoderException {
       String data = "黑马程序员" ;
       byte[] bytes = data.getBytes() ;
       //测试hex
       String encryStr = Hex.encodeHexString(bytes) ;
       String decryStr = new String(Hex.decodeHex(encryStr.toCharArray())) ;
       System.out.println("Hex编码解码:"+ encryStr  + " | " + decryStr) ;

       //测试base64
       encryStr = Base64.encodeBase64String(bytes) ;
       decryStr = new String(Base64.decodeBase64(encryStr.getBytes()) );
       System.out.println("Base64编码解码:"+ encryStr  + " | " + decryStr) ;
   }
}

上面我们已经看到了Base64就是用6位(2的6次幂就是64)表示字符,因此成为Base64。同理,Base32就是用5位,Base16就是用4位。

对比: hex编码速度快,体积大;base64编码速度慢,体积小

6. 密码分类

6.1 对称密码

       加密密钥和解密密钥相同,又称传统密码体制、共享密钥密码体制、秘密密钥体制或单密钥体制。从密钥使用方式上分为分组密码和序列密码 ,这点后文会有介绍。

对称加密算法的优点:算法公开、计算量小、加密速度快、加密效率高。

对称加密算法的缺点:交易双方都使用同样钥匙,安全性得不到保证。此外,每对用户每次使用对称加密算法时,都需要使用其他人不知道的惟一钥匙,这会使得发收信双方所拥有的钥匙数量呈几何级数增长,密钥管理成为用户的负担。对称加密算法在分布式网络系统上使用较为困难,主要是因为密钥管理困难,使用成本较高。

对称加密通常使用的是相对较小的密钥,一般小于256 bit。因为密钥越大,加密越强,但加密与解密的过程越慢。如果你只用1 bit来做这个密钥,那黑客们可以先试着用0来解密,不行的话就再用1解;但如果你的密钥有1 MB大,黑客们可能永远也无法破解,但加密和解密的过程要花费很长的时间。密钥的大小既要照顾到安全性,也要照顾到效率,是一个trade-off。

常用对称加密算法

  1. DES(Data Encryption Standard):数据加密标准,速度较快,适用于加密大量数据的场合。

  2. 3DES(Triple DES):是基于DES,对一块数据用三个不同的密钥进行三次加密,强度更高。

  3. AES(Advanced Encryption Standard):高级加密标准,是下一代的加密算法标准,速度快,安全级别高,支持128、192、256、512位密钥的加密。

算法特征

  1. 加密方和解密方使用同一个密钥,一旦密钥文件泄漏, 就会导致数据暴露

  2. 加密解密的速度比较快,适合数据比较长时的使用,可以加密大文件

  3. 密钥传输的过程不安全,且容易被破解,密钥管理也比较麻烦。

  4. 加密后编码表找不到对应字符, 出现乱码

  5. 一般结合Base64使用

6.1.1 DES
  • DES是1997年美国联邦信息处理标准中所采用的一种对称密码算法,一直以来被美国以及其他国家的政府和银行等广泛采用。随着计算机的快速发展,DES已经被暴力破解,1997年用时96天破译密钥,1998年41天破译密钥,到了1999年只用22小时15分钟就可以破译。

  • DES技术是一种将64比特的明文加密成64比特的密文的对称密码算法,因此理论上来讲,他的密钥长度也是64位,但因为在DES的密钥中每隔7比特,就会设置一个用于错误检查的比特,所以实际上DES的密钥的长度只有56比特。

  • DES是以64比特的明文(比特序列)为一个单位进行加密,这64比特的单位成为分组,一般来说,以分组为单位进行处理的密码算法称为分组密码。

  • DES每次每次只能加密64比特的数据,如果要加密的明文比较长,就需要对DES加密进行迭代(反复),而迭代的具体方案就称为模式。

Java中有关对称和非对称加密的核心类:javax.crypto.Cipher

代码示例

/**
* DES加密算法测试
*/
public class DesTest {

   /**
    * 测试DES加密
    */
   @Test
   public void testEncrypt() throws Exception {
       //明文
       String text = "黑马程序员";
       //密钥,长度必须为8个字节(字符)
       byte[] secretKeyBytes = "12345678".getBytes();
       //secretKeyBytes  = generateSecretKey("DES", 56);
       // Cipher:获取密码对象,参数按"算法/模式/填充模式"
       Cipher cipher = Cipher.getInstance("DES");

       // 参数1:密钥,key的字节数组,参数2:加密算法
       SecretKeySpec sks = new SecretKeySpec(secretKeyBytes, "DES");
       //加密对象初始化数据,参数1:模式,有加密模式和解密模式,参数2:密钥规则
       cipher.init(Cipher.ENCRYPT_MODE,sks);
       //执行加密,得到加密结果
       byte[] bytes = cipher.doFinal(text.getBytes());
       //输出字节,因为ascii码有负数,解析不出来,所以乱码
       //将byte数组转成ASCII编码,必须确保byte数组的值在ASCII的可视字符范围,否则会出现乱码,
       //因为ASCII的取值范围比byte小,byte的取值范围是-128到127
       for (byte b : bytes) {
           System.out.println(b);
       }
       // 打印密文
       System.out.println(new String(bytes));

       //将byte数组转成Base64编码。
       String result = Base64.encodeBase64String(bytes);
       System.out.println("加密后的值:" + result);
   }

   /**
    * 测试DES解密
    */
   @Test
   public void testDecrypt() throws Exception {
       //密文
       String crpyt = "+rBmhkThnKQf8IJTM/qmMA==";

       //密钥,长度必须为8个字节(字符)
       byte[] secretKeyBytes = "12345678".getBytes();
       //获取Cipher对象
       Cipher cipher = Cipher.getInstance("DES");
       // 指定密钥规则
       SecretKeySpec sks = new SecretKeySpec(secretKeyBytes, "DES");
       cipher.init(Cipher.DECRYPT_MODE, sks);
       //解密,上面使用的base64编码,下面直接用密文
       byte[] bytes = cipher.doFinal(Base64.decodeBase64(crpyt));
       //  因为是明文,所以直接返回
       String text = new String(bytes);
       System.out.println("解密后的值:"+ text) ;
   }

   /**
    * 生成密钥
    * @param algorithm 算法
    * @param len  密钥长度
    */
   public static byte[] generateSecretKey(String algorithm, int len) throws NoSuchAlgorithmException {
       KeyGenerator keyGenerator = KeyGenerator.getInstance(algorithm);//密钥生成器
       keyGenerator.init(len);//密钥长度
       SecretKey secretKey = keyGenerator.generateKey();//生成密钥
       return secretKey.getEncoded(); //密钥字节数组转字符串
   }
}
6.1.2 3DES
  • 三重DES,是为了加强DES的强度,将DES重复3次所得到的一种密码算法。明文需经过3次DES处理才能得到最后密文,由于DES密钥实际长度为56比特,因此3DES的密钥密钥实际长度就是56*3=168比特。通过增加迭代次数提高安全性,常应用在银行等金融机构。

  • DES密钥长度是8字节(64比特),3DES密钥长度是24字节(192比特)

  • 三重DES不是进行三次DES加密(加密-加密-加密),而是加密-解密-加密的过程。

  • 加密过程:用第一支密钥对原文进行加密,再使用第二支密钥对第一步操作后的信息进行解密,最后使用第三支密钥对第二步操作后的信息进行加密得到最终密文。

    解密过程:用第三支密钥对密文进行解密,再采用第二支密钥进行加密,最后采用第一支密钥解密得到原文。

  • 三重DES中所有密钥都相同时,三重DES等同于普通DES,因为前两步加密解密后得到的是原来的明文。

  • EDE:表示加密(Encryption)-> 解密(Decryption)->加密(Encryption)这个流程。

  • 缺点:处理速度较慢、密钥计算时间较长、加密效率不高。

/**
* 3DES加密算法测试
*/
public class Des3Test {

   /**
    * 测试3DES加密
    */
   @Test
   public void testEncrypt() throws Exception {
       //明文
       String text = "黑马程序员";
       //密钥,长度必须24个字节(字符)
       byte[] secretKeyBytes = "123456781234567812345678".getBytes();
       //可指定密钥实际长度是168
       //secretKeyBytes  = generateSecretKey("DESede", 168);
       // Cipher:获取密码对象,参数按"算法/模式/填充模式"
       Cipher cipher = Cipher.getInstance("DESede");

       // 参数1:密钥,key的字节数组,参数2:加密算法
       SecretKeySpec sks = new SecretKeySpec(secretKeyBytes, "DESede");
       //加密对象初始化数据,参数1:模式,有加密模式和解密模式,参数2:密钥规则
       cipher.init(Cipher.ENCRYPT_MODE,sks);
       //执行加密,得到加密结果
       byte[] bytes = cipher.doFinal(text.getBytes());

       //将byte数组转成Base64编码。
       String result = Base64.encodeBase64String(bytes);
       System.out.println("加密后的值:" + result);
   }

   /**
    * 测试3DES解密
    */
   @Test
   public void testDecrypt() throws Exception {
       //密文
       String crpyt = "+rBmhkThnKQf8IJTM/qmMA==";

       //密钥,长度必须24个字节(字符)
       byte[] secretKeyBytes = "123456781234567812345678".getBytes();
       //获取Cipher对象
       Cipher cipher = Cipher.getInstance("DESede");
       // 指定密钥规则
       SecretKeySpec sks = new SecretKeySpec(secretKeyBytes, "DESede");
       cipher.init(Cipher.DECRYPT_MODE, sks);
       //解密,上面使用的base64编码,下面直接用密文
       byte[] bytes = cipher.doFinal(Base64.decodeBase64(crpyt));
       //  因为是明文,所以直接返回
       String text = new String(bytes);
       System.out.println("解密后的值:"+ text) ;
   }

   /**
    * 生成密钥
    * @param algorithm 算法
    * @param len  密钥长度
    */
   public static byte[] generateSecretKey(String algorithm, int len) throws NoSuchAlgorithmException {
       KeyGenerator keyGenerator = KeyGenerator.getInstance(algorithm);//密钥生成器
       keyGenerator.init(len);//密钥长度
       SecretKey secretKey = keyGenerator.generateKey();//生成密钥
       return secretKey.getEncoded(); //密钥字节数组转字符串
   }
}
6.1.3 AES
  • AES(Advanced Encryption Standard)是取代其前任标准(DES)而称为新标准的一种对称算法。

  • AES分组长度为128比特,密钥长度有128、192、256比特三种,AES-128、AES192和AES-256。

  • 至今还没有有效破解AES的方式

还是之前的代码,替换密钥值和加密算法即可

/**
* AES加密算法测试
*/
public class AesTest {

   /**
    * 测试AES加密
    */
   @Test
   public void testEncrypt() throws Exception {
       //明文
       String text = "黑马程序员";
       //密钥,长度必须为16个字节(字符)
       byte[] secretKeyBytes = "1234567812345678".getBytes();
       //密钥实际长度128比特
       //secretKeyBytes  = generateSecretKey("AES", 128);
       // Cipher:获取密码对象,参数按"算法/模式/填充模式"
       Cipher cipher = Cipher.getInstance("AES");

       // 参数1:密钥,key的字节数组,参数2:加密算法
       SecretKeySpec sks = new SecretKeySpec(secretKeyBytes, "AES");
       //加密对象初始化数据,参数1:模式,有加密模式和解密模式,参数2:密钥规则
       cipher.init(Cipher.ENCRYPT_MODE,sks);
       //执行加密,得到加密结果
       byte[] bytes = cipher.doFinal(text.getBytes());

       //将byte数组转成Base64编码。
       String result = Base64.encodeBase64String(bytes);
       System.out.println("加密后的值:" + result);
   }

   /**
    * 测试AES解密
    */
   @Test
   public void testDecrypt() throws Exception {
       //密文
       String crpyt = "j9qMqmunoPEtMRpNYPWfCw==";

       //密钥,长度必须为16个字节(字符)
       byte[] secretKeyBytes = "1234567812345678".getBytes();
       //获取Cipher对象
       Cipher cipher = Cipher.getInstance("AES");
       //指定密钥规则
       SecretKeySpec sks = new SecretKeySpec(secretKeyBytes, "AES");
       cipher.init(Cipher.DECRYPT_MODE, sks);
       //解密,上面使用的base64编码,下面直接用密文
       byte[] bytes = cipher.doFinal(Base64.decodeBase64(crpyt));
       //因为是明文,所以直接返回
       String text = new String(bytes);
       System.out.println("解密后的值:"+ text) ;
   }

   /**
    * 生成密钥
    * @param algorithm 算法
    * @param len  密钥长度
    */
   public static byte[] generateSecretKey(String algorithm, int len) throws NoSuchAlgorithmException {
       KeyGenerator keyGenerator = KeyGenerator.getInstance(algorithm);//密钥生成器
       keyGenerator.init(len);//密钥长度
       SecretKey secretKey = keyGenerator.generateKey();//生成密钥
       return secretKey.getEncoded(); //密钥字节数组转字符串
   }
}
6.1.4 破解AES的难度

据统计,完全破解要花费时长为2104亿年,消耗电量1.1201 * 10^22 kWh,电费1.368 * 10^13 亿美元

6.1.5 选用哪一种?

DES已被破解不要再使用,3DES在部分金融机构内还有在使用将来会被AES取代,推荐使用AES。

6.2 分组密码

6.2.1 概念

按对明文的处理方式,密码算法可以分为分组密码( Blok cipher)和流密码(Stream cipher)。

6.2.2 分组密码:也叫块加密(block cyphers),每次只能处理特定长度的一块数据的密码算法,“一块”称为分组,一个分组的比特数就是分组长度。一次加密明文中的一个块,将明文按一定的位长分组,明文组经过加密运算得到密文组,密文组经过解密运算(加密运算的逆运算),还原成明文组。比如:DES和3DES的分组长度都是64比特,一次性只能加密64比特的明文并生成64比特的密文。

6.2.3 序列密码:也叫流加密(stream cyphers),对数据流进行连续处理的密码算法,是指利用少量的密钥(制乱元素)通过某种复杂的运算(密码算法)产生大量的伪随机位流,用于对明文位流的加密。解密是指用同样的密钥和密码算法及与加密相同的伪随机位流,用以还原明文位流。流密码中一般以1比特、8比特或32比特等为单位进行加解密。

6.2.4 对比:分组密码处理一个分组就结束,无需通过内部状态记录加密进度;流密码是对一串数据流进行连续处理,需要保持内部状态。

前文所提到的DES、3DES、AES等大部分对称加密算法都属于分组密码。流密码的典型例子有一次性密码本。

本文内容主要讲解的是分组密码。

6.2.2 加密模式

分组算法只能加密固定长度的分组,但有时加密的明文长度会超过分组密码的分组长度,此时就需要对分组密码进行迭代,以便将一段很长的明文全部加密。迭代的方法就称为分组密码的加密模式(model)。

加密模式的种类:常见的有ECB模式(电子密码本模式)、CBC模式(密码分组链接模式)、CTR模式(计数器模式)等,本课中重点说明ECB模式和CBC模式。

6.2.3 明文分组与密文分组

明文分组: 分组密码算法中作为加密对象的明文。明文分组的长度与分组密码算法的分组长度相等。

密文分组:分组加密算法中对明文分组加密之后产生的密文。

明文分组与密文分组.png

6.2.4 ECB与CBC
1) ECB

ECB加密模式:需要加密的明文按照块密码的块大小被分为数个块(组),并对每个块进行独立加密。明文分组与密文分组是一一对应的关系,每个明文分组都各自独立地进行加密和解密。如果明文中存在多个相同的明文分组,那么明文分组将被转换为相同的密文分组,这样的话可以通过观察密文来推测出明文的组合,从而可能破译密码,所以ECB模式存在一定风险。

ECB模式.png

ECB的一个非常大的弱点就是可以在不破译密文的情况下操作明文,比如银行转账例子中:付款人、收款人、转账金额分别可以对应三个明文分组,分别加密对应三个密文分组,如果将收款人密文分组和付款人密文分组顺序调换,那么整个银行转行的操作就是完全相反的。也就是说在ECB模式中,只要对任意密文分组进行替换,对应的明文分组也会被替,如果直接将密文分组删除,那么明文分组也会被删除,如果对密文分组负责,那么明文分组也会被复制。

由于ECB模式的巨大漏洞,作为程序员,切记千万不能使用ECB模式进行加解密。

2) CBC

CBC加密模式:密文分组像链条一样拼接在一起,具体过程是首先将明文分组与前一个密文分组进行XOR运行,然后在进行加密。

CBC模式.png

对比ECB模式和CBC模式其中一个分组的加密过程,发现他们的区别在于ECB模式只进行了加密,而CBC模式在加密之前进行了一次XOR运算。

CBC模式在加密一个明文分组时,由于不存在前一个密文分组,所以需要提前准备好一个长度为一个分组的比特序列来当做前一个密文分组,这个比特序列称为初始化向量(initialization vector),缩写是IV。每次加密时都会随机产生一个不同的比特序列当做初始化向量。

CBC模式就算是明文分组中的某两个组比如分组1和明文分组2值相等,由于有XOR运算。密文分组1和2的值也不一定相等。所以CBC模式不存在ECB模式中的缺陷。

由于CBC模式的安全性,CBC被应用在在互联网安全通信协议IPsec中,在此协议中主要使用了3DES-CBC以及AES-CBC,CBC模式还被应用在Kerberos verson 5的认证系统中。

6.2.5 填充模式

大多数密码算法都是块密码算法,需要将明文消息切成固定大小的块,一块一块地进行加密。例如DES就需要将消息切割成一个个64位的块。如果消息长度不是64的整数倍,最后一个消息块就不够64位,这时就要对最后一个消息块进行填充。填充方式有很多种,如果加密时以某种方式填充,解密时就得使用这种填充方式并去除填充内容。

NoPadding

不填充,在DES加密算法下, 要求原文长度必须是8byte的整数倍,在AES加密算法下, 要求原文长度必须是16byte的整数倍

PKCS5Padding

数据块的大小为8位, 不够就补足

6.2.6 Java的应用

有关加密模式与填充模式,在Java中的应用,有如下几点需要注意

  • javax.crypto.Cipher类是Java中提供加密和解密功能的核心类,Cipher对象创建的方式有如下两种:

    Cipher c = Cipher.getInstance("算法/模式/填充");或Cipher c = Cipher.getInstance("算法")

  • 默认情况下, 加密模式和填充模式为 : ECB/PKCS5Padding

  • 如果使用CBC模式, 在初始化Cipher对象时, 需要增加参数, 初始化向量IV : IvParameterSpec iv = new IvParameterSpec(key.getBytes());

  • Cipher创建的第一种方式中,参数一般称为转换名,转换名由"算法/模式/填充"构成。Java平台的每个实现都需要支持以下标准Cipher转换:

    - `AES/CBC/NoPadding` (128) 
    - `AES/CBC/PKCS5Padding` (128)
    - `AES/ECB/NoPadding` (128)
    - `AES/ECB/PKCS5Padding` (128)
    - `DES/CBC/NoPadding` (56)
    - `DES/CBC/PKCS5Padding(56)`
    - `DES/ECB/NoPadding(56)`
    - `DES/ECB/PKCS5Padding` (56)
    - `DESede/CBC/NoPadding` (168)
    - `DESede/CBC/PKCS5Padding` (168)
    - `DESede/ECB/NoPadding` (168)
    - `DESede/ECB/PKCS5Padding` (168)
    - `RSA/ECB/PKCS1Padding` `1024,2048`
    - `RSA/ECB/OAEPWithSHA-1AndMGF1Padding` `1024,2048`  
    - `RSA/ECB/OAEPWithSHA-256AndMGF1Padding` `1024,2048`  
6.2.7 改进DES示例

       将DesTest类中,加密测试用例里只改动一行,改动获取Cipher的方式改为:

Cipher cipher = Cipher.getInstance("DES/CBC/PKCS5Padding");

       运行加密测试用例多次,发现每次加密结果都不一样,是因为初始向量IV由于加密时我们没有指定,所以会每次加密的时候系统随机生成IV,将每次加密后的值都拿去解密,发现全部解密失败,因为解密时需要IV值却不知道IV值是什么。所以程序需要进一步改进为加密时指定IV值,以方便解密时使用同样的IV值来解密。

再次改进加密和解密都是用相同的IV值:

/**
* DES加密算法测试
*/
public class DesTest {

   /**
    * 测试DES加密
    */
   @Test
   public void testEncrypt() throws Exception {
       //明文
       String text = "黑马程序员";
       //密钥,长度必须为8个字节(字符)
       byte[] secretKeyBytes = "12345678".getBytes();
       //secretKeyBytes  = generateSecretKey("DES", 56);
       // Cipher:获取密码对象,参数按"算法/模式/填充模式"
       Cipher cipher = Cipher.getInstance("DES/CBC/PKCS5Padding");

       // 参数1:密钥,key的字节数组,参数2:加密算法
       SecretKeySpec sks = new SecretKeySpec(secretKeyBytes, "DES");
       //加密对象初始化数据,参数1:模式,有加密模式和解密模式,参数2:密钥规则,参数3:IV初始向量值
       IvParameterSpec iv = new IvParameterSpec("22446688".getBytes());
       cipher.init(Cipher.ENCRYPT_MODE, sks, iv);
       //执行加密,得到加密结果
       byte[] bytes = cipher.doFinal(text.getBytes());

       //将byte数组转成Base64编码。
       String result = Base64.encodeBase64String(bytes);
       System.out.println("加密后的值:" + result);
   }

   /**
    * 测试DES解密
    */
   @Test
   public void testDecrypt() throws Exception {
       //密文
       String crpyt = "FeHL4fKM/N1RSYOKJJ6ZZQ==";

       //密钥,长度必须为8个字节(字符)
       byte[] secretKeyBytes = "12345678".getBytes();
       //获取Cipher对象
       Cipher cipher = Cipher.getInstance("DES/CBC/PKCS5Padding");
       // 指定密钥规则
       SecretKeySpec sks = new SecretKeySpec(secretKeyBytes, "DES");
       //加密对象初始化数据,参数1:模式,有加密模式和解密模式,参数2:密钥规则,参数3:IV初始向量值
       IvParameterSpec iv = new IvParameterSpec("22446688".getBytes());
       cipher.init(Cipher.DECRYPT_MODE, sks, iv);
       //解密,上面使用的base64编码,下面直接用密文
       byte[] bytes = cipher.doFinal(Base64.decodeBase64(crpyt));
       //  因为是明文,所以直接返回
       String text = new String(bytes);
       System.out.println("解密后的值:"+ text) ;
   }

   /**
    * 生成密钥
    * @param algorithm 算法
    * @param len  密钥长度
    */
   public static byte[] generateSecretKey(String algorithm, int len) throws NoSuchAlgorithmException {
       KeyGenerator keyGenerator = KeyGenerator.getInstance(algorithm);//密钥生成器
       keyGenerator.init(len);//密钥长度
       SecretKey secretKey = keyGenerator.generateKey();//生成密钥
       return secretKey.getEncoded(); //密钥字节数组转字符串
   }
}

3DS、AES的改进思路类似:第一步修改转换为DESede/CBC/PKCS5Padding、AES/CBC/PKCS5Padding,第二步在加密和解密的时候设置初始化向量为同一值。

3DES最终代码:

/**
* 3DES加密算法测试
*/
public class Des3Test {

   /**
    * 测试3DES加密
    */
   @Test
   public void testEncrypt() throws Exception {
       //明文
       String text = "黑马程序员";
       //密钥,长度必须24个字节(字符)
       byte[] secretKeyBytes = "123456781234567812345678".getBytes();
       //可指定密钥长度是168
       //secretKeyBytes  = generateSecretKey("DESede", 168);
       // Cipher:获取密码对象,参数按"算法/模式/填充模式"
       Cipher cipher = Cipher.getInstance("DESede/CBC/PKCS5Padding");

       //参数1:密钥,key的字节数组,参数2:加密算法
       SecretKeySpec sks = new SecretKeySpec(secretKeyBytes, "DESede");
       //加密对象初始化数据,参数1:模式,有加密模式和解密模式,参数2:密钥规则,参数3:IV初始向量值
       IvParameterSpec iv = new IvParameterSpec("22446688".getBytes());
       cipher.init(Cipher.ENCRYPT_MODE, sks, iv);
       //执行加密,得到加密结果
       byte[] bytes = cipher.doFinal(text.getBytes());

       //将byte数组转成Base64编码。
       String result = Base64.encodeBase64String(bytes);
       System.out.println("加密后的值:" + result);
   }

   /**
    * 测试3DES解密
    */
   @Test
   public void testDecrypt() throws Exception {
       //密文
       String crpyt = "FeHL4fKM/N1RSYOKJJ6ZZQ==";

       //密钥,长度必须24个字节(字符)
       byte[] secretKeyBytes = "123456781234567812345678".getBytes();
       // 获取Cipher对象
       Cipher cipher = Cipher.getInstance("DESede/CBC/PKCS5Padding");
       // 指定密钥规则
       SecretKeySpec sks = new SecretKeySpec(secretKeyBytes, "DESede");
       //加密对象初始化数据,参数1:模式,有加密模式和解密模式,参数2:密钥规则,参数3:IV初始向量值
       IvParameterSpec iv = new IvParameterSpec("22446688".getBytes());
       cipher.init(Cipher.DECRYPT_MODE, sks, iv);
       // 解密,上面使用的base64编码,下面直接用密文
       byte[] bytes = cipher.doFinal(Base64.decodeBase64(crpyt));
       //  因为是明文,所以直接返回
       String text = new String(bytes);
       System.out.println("解密后的值:"+ text) ;
   }

   /**
    * 生成密钥
    * @param algorithm 算法
    * @param len  密钥长度
    */
   public static byte[] generateSecretKey(String algorithm, int len) throws NoSuchAlgorithmException {
       KeyGenerator keyGenerator = KeyGenerator.getInstance(algorithm);//密钥生成器
       keyGenerator.init(len);//密钥长度
       SecretKey secretKey = keyGenerator.generateKey();//生成密钥
       return secretKey.getEncoded(); //密钥字节数组转字符串
   }
}

AES最终代码

/**
* AES加密算法测试
*/
public class AesTest {

   /**
    * 测试AES加密
    */
   @Test
   public void testEncrypt() throws Exception {
       //明文
       String text = "黑马程序员";
       //密钥,长度必须为16个字节(字符)
       byte[] secretKeyBytes = "1234567812345678".getBytes();
       //secretKeyBytes  = generateSecretKey("AES", 128);
       // Cipher:获取密码对象,参数按"算法/模式/填充模式"
       Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");

       // 参数1:密钥,key的字节数组,参数2:加密算法
       SecretKeySpec sks = new SecretKeySpec(secretKeyBytes, "AES");
       //加密对象初始化数据,参数1:模式,有加密模式和解密模式,参数2:密钥规则,参数3:IV初始向量值
       IvParameterSpec iv = new IvParameterSpec("1122334455667788".getBytes());
       cipher.init(Cipher.ENCRYPT_MODE, sks, iv);
       //执行加密,得到加密结果
       byte[] bytes = cipher.doFinal(text.getBytes());

       //将byte数组转成Base64编码。
       String result = Base64.encodeBase64String(bytes);
       System.out.println("加密后的值:" + result);
   }

   /**
    * 测试AES解密
    */
   @Test
   public void testDecrypt() throws Exception {
       //密文
       String crpyt = "VG+sz7FleJ5QMMe+elTkpg==";
       //密钥,长度必须为16个字节(字符)
       byte[] secretKeyBytes = "1234567812345678".getBytes();
       //获取Cipher对象
       Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
       //指定密钥规则
       SecretKeySpec sks = new SecretKeySpec(secretKeyBytes, "AES");
       //加密对象初始化数据,参数1:模式,有加密模式和解密模式,参数2:密钥规则,参数3:IV初始向量值
       IvParameterSpec iv = new IvParameterSpec("1122334455667788".getBytes());
       cipher.init(Cipher.DECRYPT_MODE, sks, iv);
       //解密,上面使用的base64编码,下面直接用密文
       byte[] bytes = cipher.doFinal(Base64.decodeBase64(crpyt));
       //  因为是明文,所以直接返回
       String text = new String(bytes);
       System.out.println("解密后的值:"+ text) ;
   }

   /**
    * 生成密钥
    * @param algorithm 算法
    * @param len  密钥长度
    */
   public static byte[] generateSecretKey(String algorithm, int len) throws NoSuchAlgorithmException {
       KeyGenerator keyGenerator = KeyGenerator.getInstance(algorithm);//密钥生成器
       keyGenerator.init(len);//密钥长度
       SecretKey secretKey = keyGenerator.generateKey();//生成密钥
       return secretKey.getEncoded(); //密钥字节数组转字符串
   }
}

6.3 非对称密码

       加密密钥和解密密钥不相同,并且从一个很难(实际上不可能实现)推出另一个,又称公钥密码体制(public-key cryptography system) 。公钥密码体制用一个密钥进行加密( (验证 ),而用另一个进行解密( 签名)。其中一个密钥可以公开,成为公开密钥(pulic key),简称公钥;另一个密钥需要秘密保存,称为私有密钥(private key),简称私钥 。

  • 公钥和私钥是一对

  • 如果用公开密钥对数据进行加密,只有用对应的私有密钥才能解密

  • 如果用私有密钥对数据进行加密,只有用对应的公开密钥才能解密

非对称加密算法的优点:安全性更高,公钥是公开的,密钥是自己保存的,不需要将私钥给别人。

非对称加密算法的缺点:加密和解密花费时间长、速度慢,只适合对少量数据进行加密。

非对称加密算法:  RSA、Elgamal、背包算法、Rabin、D-H、ECC(椭圆曲线加密算法),常见的有RSA、ECC。

现代计算机和互联网安全体系,很大程度上都依赖于公钥密码,但公钥密钥也存在风险,比如中间人攻击。

6.3.1 中间人攻击

中间人攻击指的是主动攻击者混入发送者和接收者中间,对发送者伪装成接收者,对接收者伪装成发送者的攻击方式。如果要防御中间人攻击,我们需要一种手段来确认接收者收到的公钥是否真的属于发送者,这种手段称为认证,可以利用公钥的证书来实现。

中间人攻击.png

6.3.2 生成密钥对
 public class RSATest {

       /**
    * 测试生成密钥对 公钥和私钥
    */
   @Test
   public void testCreatePubKeyPriKey() throws NoSuchAlgorithmException {
       //创建密钥对生成器对象
       KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
       //生成密钥对
       KeyPair keyPair = keyPairGenerator.generateKeyPair();
       //生成私钥
       PrivateKey privateKey = keyPair.getPrivate();
       //生成公钥
       PublicKey publicKey = keyPair.getPublic();
       //获取私钥字节数组
       byte[] privateKeyEncoded = privateKey.getEncoded();
       //获取公钥字节数组
       byte[] publicKeyEncoded = publicKey.getEncoded();
       //进行base64编码
       String privateKeyString = Base64.encodeBase64String(privateKeyEncoded);
       String publicKeyString = Base64.encodeBase64String(publicKeyEncoded);
       //输出私钥
       System.out.println(privateKeyString);
       //输出公钥
       System.out.println(publicKeyString);
   }
}
6.3.3 私钥加密公钥解密
    private  String privateKeyString = "私钥的值";
   private  String publicKeyString = "公钥的值";
   private  String algorithm = "RSA";

/**
    * 测试私钥加密
    * @throws Exception
    */
   @Test
   public void testPrikKeyEncrypt() throws Exception {
       String text = "传智播客";
       PrivateKey privateKey = getPrivateKey();
       //创建加密对象
       Cipher cipher = Cipher.getInstance(algorithm);
       //初始化加密,参数1:加密模式,参数2:使用私钥进行加密
       cipher.init(Cipher.ENCRYPT_MODE,privateKey);
       //对明文进行加密
       byte[] bytes = cipher.doFinal(text.getBytes());
       System.out.println(Base64.encodeBase64String(bytes));
   }

   /**
    * 测试公钥解密
    */
   @Test
   public void testPubKeyDecrypt() throws Exception {
       String encrypt = "加密的值";
       PublicKey publicKey = getPublicKey();
       //创建加密对象
       Cipher cipher = Cipher.getInstance(algorithm);
       //初始化加密,参数1:解密模式,参数2:使用公钥进行解密
       cipher.init(Cipher.DECRYPT_MODE,publicKey);
       //对密文进行解密
       byte[] bytes = cipher.doFinal(Base64.decodeBase64(encrypt));
       System.out.println(new String(bytes));
   }


   /**
    * 获取私钥对象
    */
   private PrivateKey getPrivateKey() throws Exception{
       //获取密钥工厂
       KeyFactory keyFactory = KeyFactory.getInstance(algorithm);
       //构建密钥规范 进行Base64解码
       PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(Base64.decodeBase64(privateKeyString));
       //生成私钥
       return keyFactory.generatePrivate(spec);
   }

   /**
    * 获取公钥对象
    */
   private PublicKey getPublicKey() throws Exception{
       //获取密钥工厂
       KeyFactory keyFactory = KeyFactory.getInstance(algorithm);
       //构建密钥规范 进行Base64解码
       X509EncodedKeySpec spec = new X509EncodedKeySpec(Base64.decodeBase64(publicKeyString));
       //生成公钥
       return keyFactory.generatePublic(spec);
   }
6.3.4 公钥加密私钥解密
        /**
    * 测试公钥加密
    * @throws Exception
    */
   @Test
   public void testPubKeyEncrypt() throws Exception {
       String text = "传智播客";
       PublicKey publicKey = getPublicKey();
       //创建加密对象
       Cipher cipher = Cipher.getInstance(algorithm);
       //初始化加密,参数1:加密模式,参数2:使用公钥进行加密
       cipher.init(Cipher.ENCRYPT_MODE,publicKey);
       //对明文进行加密
       byte[] bytes = cipher.doFinal(text.getBytes());
       System.out.println(Base64.encodeBase64String(bytes));
   }

   /**
    * 测试私钥解密
    */
   @Test
   public void testPriKeyDecrypt() throws Exception {
       String encrypt = "加密的值";
       //公钥进行解密
       PrivateKey publicKey = getPrivateKey();
       //初始化加密,参数1:解密模式,参数2:使用私钥进行解密
       Cipher cipher = Cipher.getInstance(algorithm);
       cipher.init(Cipher.DECRYPT_MODE,publicKey);
       //对密文进行解密
       byte[] bytes = cipher.doFinal(Base64.decodeBase64(encrypt));
       System.out.println(new String(bytes));
   }
6.3.5 代码重构测试加解密
    /**
    * 测试加密解密
    * @throws Exception
    */
   @Test
   public void testEncryptDecrypt() throws Exception {
       PrivateKey privateKey = getPrivateKey();
       PublicKey publicKey = getPublicKey();
       //1.1私钥加密
       String  priEnrypt = encrypt(privateKey, "传智播客");
       //1.1公钥解密
       decrypt(publicKey,priEnrypt);

       //2.1公钥加密
       String  pubEnrypt = encrypt(publicKey, "传智播客");
       //2.1私钥解密
       decrypt(privateKey,pubEnrypt);
   }



   /**
    * 加密数据
    */
   private String encrypt(Key key, String input) throws Exception{
       //创建加密对象
       Cipher cipher = Cipher.getInstance(algorithm);
       //初始化加密,参数1:加密模式,参数2:使用key进行加密
       cipher.init(Cipher.ENCRYPT_MODE, key);
       //私钥加密
       byte[] bytes = cipher.doFinal(input.getBytes());
       //对密文进行Base64编码
       System.out.println("加密结果:"+ Base64.encodeBase64String(bytes));
       return Base64.encodeBase64String(bytes);
   }


   /**
    * 解密数据
    */
   private String decrypt(Key key, String encrypted) throws Exception{
       //创建加密对象
       Cipher cipher = Cipher.getInstance(algorithm);
       //初始化加密,参数1:解密模式,参数2:使用key进行解密
       cipher.init(Cipher.DECRYPT_MODE,key);
       //由于密文进行了Base64编码, 在这里需要进行解码
       byte[] decode = Base64.decodeBase64(encrypted);
       //对密文进行解密,不需要使用base64,因为原文不会乱码
       byte[] bytes = cipher.doFinal(decode);
       System.out.println("解密结果:"+ new String(bytes));
       return new String(bytes);
   }
6.3.6 密钥对保存及读取
  1. 保存公钥和私钥到文件

private String pubKeyPath = "test.pub";
   private String priKeyPath = "test.pri";

   /**
    * 测试生成密钥对 公钥和私钥
    */
   @Test
   public void testCreatePubKeyPriKey() throws NoSuchAlgorithmException, IOException {
       //创建密钥对生成器对象
       KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
       //生成密钥对
       KeyPair keyPair = keyPairGenerator.generateKeyPair();
       //生成私钥
       PrivateKey privateKey = keyPair.getPrivate();
       //生成公钥
       PublicKey publicKey = keyPair.getPublic();
       //获取私钥字节数组
       byte[] privateKeyEncoded = privateKey.getEncoded();
       //获取公钥字节数组
       byte[] publicKeyEncoded = publicKey.getEncoded();
       //进行base64编码
       String privateKeyString = Base64.encodeBase64String(privateKeyEncoded);
       String publicKeyString = Base64.encodeBase64String(publicKeyEncoded);
       //输出私钥
       System.out.println(privateKeyString);
       //输出公钥
       System.out.println(publicKeyString);

       // 保存文件
       FileUtils.writeStringToFile(new File(pubKeyPath), publicKeyString, Charset.forName("UTF-8"));
       FileUtils.writeStringToFile(new File(priKeyPath), privateKeyString, Charset.forName("UTF-8"));
   }
  1. 从文件读取公钥和私钥

 /**
    * 获取私钥对象
    */
   private PrivateKey getPrivateKey() throws Exception{
       String privateKeyString = FileUtils.readFileToString(new File(priKeyPath), Charset.defaultCharset());
       //获取密钥工厂
       KeyFactory keyFactory = KeyFactory.getInstance(algorithm);
       //构建密钥规范 进行Base64解码
       PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(Base64.decodeBase64(privateKeyString));
       //生成私钥
       return keyFactory.generatePrivate(spec);
   }

   /**
    * 获取公钥对象
    */
   private PublicKey getPublicKey() throws Exception{
       String publicKeyString  = FileUtils.readFileToString(new File(pubKeyPath), Charset.defaultCharset());
       //获取密钥工厂
       KeyFactory keyFactory = KeyFactory.getInstance(algorithm);
       //构建密钥规范 进行Base64解码
       X509EncodedKeySpec spec = new X509EncodedKeySpec(Base64.decodeBase64(publicKeyString));
       //生成公钥
       return keyFactory.generatePublic(spec);
   }

6.4 随机数

6.4.1 概念

       对于密码技术来说,算法的强度再高,只要攻击者知道了密钥,那么安全就形同虚设。我们需要使用一种技术来随机成成密钥,使得攻击者无法看穿,那么这项技术就是随机数技术。

随机数技术的应用场景:

  • 生成密钥:用于对称密码和消息认证码。

  • 生成密钥对:用于公钥密码和数字签名

  • 生成初始向量(IV):用于分组密码的CBC、CFB和OFB模式。

  • 生成nonce:用于防御重放共计及分组密码的CTR模式

  • 生成盐:用于基于口令的密码(PBE)

随机数技术分类:

  • 随机性:完全杂乱的数列

  • 不可预测性:不能从过去的数列推测出下一个出现的数

  • 不可重现性:除非将数列本身保存下来,佛则不能重现相同的数列

       由上到下越来越严格,密码技术使用的随机数仅仅靠随机性是不够的,至少还要具备不可预测性才行。具备不可预测性的随机数,一定具备随机性。具备不可重现性的随机数,一定具随机性和不可预测性。


随机性不可预测性不可重现性说明
弱随机数具备

只具备随机属性,不可用与密码技术
强伪随机数具备具备
具备不可预测性,可用于密码技术
真随机数具备具备具备具备不可重现性,可用于密码技术

严格程度,从低到高:随机性->不可预测性->不可重现性

6.4.2 伪随机数生成器

随机数可以通过硬件来生成,也可以通过软件生成。

生成随机数的硬件设备称为随机数生成器,生成随机数的软件称为伪随机数生成器。

1) 具体的伪随机数生成器

  • 线性同余法

    不具备不可预测性,不可用于密码技术,比如Java中的java.util.Random。核心是将当前随机数乘以A再加上C,然后除以M得到的余数作为下一个伪随机数。

  • 单项散列函数法

    具备不可预测性,可用于密码技术。核心在于单项散列函数的单向性。

  • 密码法

    具备不可预测性,可用于密码技术。何在在于密码的机密性。

  • ASNI X9.17

    具备不可预测性,可用于密码技术。

2)伪随机数生成器的攻击

       由于伪随机数生成器承担了生成密钥的重任,会经常被当做攻击对象。如果攻击者知道了伪随机数的种子,那么他就能够知道这个伪随机数生成器所生成的全部伪随机数数列。我们需要使用具备不可重现性的真随机数作为种子。

实现方式:事先准备好随机池,随机池内容要保护好,当密码软件需要伪随机数的种子时,可以从随机池中取出来需要的长度的随机比特序列来使用。

随机池:

  • 比如linux中的/dev/random和/dev/urandom文件就是一个根据硬件设备驱动存储真随机数的随机池

  • 比如Windows操作系统的CryptGenRandom接口

6.4.3 Java随机数示例

Java编程中,能保证不可预测的随机算法类是SecureRandom,可用于密码技术

/**
* 随机数生成测试
*/
public class RandomTest {

   @Test
   public void testRandom() throws NoSuchAlgorithmException {
       //linux环境下会读取/dev/urandom非阻塞生成随机数
       SecureRandom random = new SecureRandom();
       for (int i = 0; i < 100; i++) {
           System.out.println(random.nextLong());
       }
   }
}

6.5 混合密码系统

6.5.1 概念
  • 对称密码问题:缺陷在于对于密钥的管理上,以及在非安全信道中通讯时,密钥交换的安全性不能保障,而公钥密码则解决了这个问题(也称密钥配送问题)

  • 公钥密码问题1:处理速度远远低于对称密码

  • 公钥密码问题2:会出现中间人攻击

所以在实际的网络环境中,会将两者混合使用,将对称密码和非对称密码结合起来的方式就是混合密码。混合密码系统可以解决公钥密码速度慢的问题,中间人共计需要用到认证技术。

6.5.2 混合密码系统组成机制
  • 用对称密码加密消息

  • 通过伪随机生成器生成对称密码加密中使用的会话密钥

  • 用公钥密码加密会话密钥

  • 从混合密码系统外部赋予公钥密码加密时使用的密钥

混合密码系统运用了伪随机数生成器、对称密码、公钥密码这三种密码技术,充分利用了对称密码和公钥密码的优势。比如密码软件PGP,SSL/TLS技术都运用了混合密码系统。

1)加密过程
混合密码系统的加密.png
  1. 加密会话密钥

    会话密钥指的是为本次通信生成的临时密钥,一般通过伪随机数生成器产生,产生会话密钥的同时会传给右侧作为对称密码的密钥使用。接着通过公钥对会话密钥进行加密,公钥密码加密所使用的密钥是接收者的公钥

  2. 使用对称密码进行加密,密钥为会话密钥。如果内容很长,对称密钥依然能快速完成加密

  3. 根据第一步和第二部的加密结果,按顺序拼接成混合密码系统的密文

2)解密过程
混合密码系统的解密.png
  1. 分离密文:根据发送者和接收者事先约定好的密文结构,将密文分离成两部分

  2. 解密会话密钥:用公钥密码解密会话密钥,解密密钥是接收者的私钥。解密后的会话密钥将被用作解密消息的密钥

  3. 解密消息:使用解密后的会话密钥利用对称密码进行解密

6.5.3 注意事项
  • 会话密钥通过伪随机数生成器生成,所以要选择高强度的伪随机数生成器

  • 对称密码被用来加密消息,所以要选择高强度对称密码算法确保密钥足够的长度,并且选择分组密码模式。

  • 公钥密码被用来加密会话密钥,所以要选择高强度的公钥密码算法确保密钥足够的长度

  • 公钥密码的强度最好高于对称密码的强度,对称密码的会话密钥如果被破译只影响此次通信,公钥密码如果被破译,会导致从过去到未来(用相同公钥加密的)所有的通信内容都被破译

6.5.4 代码示例

AES工具类:

/**
* AES加解密工具类
*/
public class AESUtil {
/**
 * AES加密
 * @param text 待加密的明文
 * @param secretKey 密钥
 * @param iv 初始向量
 */
public static String encrypt(byte[] text,byte[] secretKey,byte[] iv) throws Exception {

 // Cipher:获取密码对象,参数按"算法/模式/填充模式"
 Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");

 // 参数1:密钥,key的字节数组,参数2:加密算法
 SecretKeySpec sks = new SecretKeySpec(secretKey, "AES");
 //加密对象初始化数据,参数1:模式,有加密模式和解密模式,参数2:密钥规则,参数3:IV初始向量值
 IvParameterSpec ivParam = new IvParameterSpec(iv);
 cipher.init(Cipher.ENCRYPT_MODE, sks, ivParam);
 //执行加密,得到加密结果
 byte[] bytes = cipher.doFinal(text);

 //将byte数组转成Base64编码。
 String result = Base64.encodeBase64String(bytes);
 return result;
}

/**
 * AES加密
 * @param text 待加密的明文
 * @param secretKey 密钥
 */
public static String encrypt(byte[] text,byte[] secretKey) throws Exception {

 // Cipher:获取密码对象,参数按"算法/模式/填充模式"
 Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");

 // 参数1:密钥,key的字节数组,参数2:加密算法
 SecretKeySpec sks = new SecretKeySpec(secretKey, "AES");
 //加密对象初始化数据,参数1:模式,有加密模式和解密模式,参数2:密钥规则
 cipher.init(Cipher.ENCRYPT_MODE, sks);
 //执行加密,得到加密结果
 byte[] bytes = cipher.doFinal(text);

 //将byte数组转成Base64编码。
 String result = Base64.encodeBase64String(bytes);
 return result;
}

/**
 * AES解密
 * @param encrypted 待解密的密文
 * @param secretKey 密钥
 * @param iv 初始向量值
 */
public static String decrypt(String encrypted,byte[] secretKey, byte[] iv) throws Exception {
 //秘钥,长度必须为16个字节(字符)
 //获取Cipher对象
 Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
 //指定密钥规则
 SecretKeySpec sks = new SecretKeySpec(secretKey, "AES");
 //加密对象初始化数据,参数1:模式,有加密模式和解密模式,参数2:密钥规则,参数3:IV初始向量值
 IvParameterSpec ivParam = new IvParameterSpec(iv);
 cipher.init(Cipher.DECRYPT_MODE, sks, ivParam);
 //解密,上面使用的base64编码,下面直接用密文
 byte[] bytes = cipher.doFinal(Base64.decodeBase64(encrypted));
 //  因为是明文,所以直接返回
 String text = new String(bytes);
 return text;
}

/**
 * AES解密
 * @param encrypted 待解密的密文
 * @param secretKey 密钥
 */
public static String decrypt(byte[] encrypted,byte[] secretKey) throws Exception {
 //秘钥,长度必须为16个字节(字符)
 //获取Cipher对象
 Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
 //指定密钥规则
 SecretKeySpec sks = new SecretKeySpec(secretKey, "AES");
 //加密对象初始化数据,参数1:模式,有加密模式和解密模式,参数2:密钥规则
 cipher.init(Cipher.DECRYPT_MODE, sks);
 //解密,上面使用的base64编码,下面直接用密文
 byte[] bytes = cipher.doFinal(encrypted);
 //  因为是明文,所以直接返回
 String text = new String(bytes);
 return text;
}

/**
 * 生成密钥
 * @param algorithm 算法
 * @param len  密钥长度
 */
public static byte[] generateSecretKey(String algorithm, int len) throws NoSuchAlgorithmException {
 KeyGenerator keyGenerator = KeyGenerator.getInstance(algorithm);//密钥生成器
 keyGenerator.init(len);//密钥长度
 SecretKey secretKey = keyGenerator.generateKey();//生成密钥
 return secretKey.getEncoded(); //密钥字节数组转字符串
}
}

RSA 工具类:

/**
* RSA加解密工具类
*/
public class RSAUtil {

private static String algorithm = "RSA";

/**
 * 加密数据
 * @param key 公钥或者私钥
 * @param text 待加密的明文
 */
public static String encrypt(Key key, byte[] text) throws Exception{
 //创建加密对象
 Cipher cipher = Cipher.getInstance(algorithm);
 //初始化加密,参数1:加密模式,参数2:使用key进行加密
 cipher.init(Cipher.ENCRYPT_MODE, key);
 //私钥加密
 byte[] bytes = cipher.doFinal(text);
 //对密文进行Base64编码
 return Base64.encodeBase64String(bytes);
}


/**
 * 解密数据
 * @param key 公钥或者私钥
 * @param encrypted 待解密的密文
 */
public static byte[] decrypt(Key key, String encrypted) throws Exception{
 //创建加密对象
 Cipher cipher = Cipher.getInstance(algorithm);
 //初始化加密,参数1:解密模式,参数2:使用key进行解密
 cipher.init(Cipher.DECRYPT_MODE,key);
 //由于密文进行了Base64编码, 在这里需要进行解码
 byte[] decode = Base64.decodeBase64(encrypted);
 //对密文进行解密,不需要使用base64,因为原文不会乱码
 byte[] bytes = cipher.doFinal(decode);
 return bytes;
}

private static String pubKeyPath = "test.pub";
private static String priKeyPath = "test.pri";
/**
 * 生成密钥对公钥和私钥
 */
public static void createPubKeyPriKey() throws NoSuchAlgorithmException, IOException {
 //创建密钥对生成器对象
 KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
 //生成密钥对
 KeyPair keyPair = keyPairGenerator.generateKeyPair();
 //生成私钥
 PrivateKey privateKey = keyPair.getPrivate();
 //生成公钥
 PublicKey publicKey = keyPair.getPublic();
 //获取私钥字节数组
 byte[] privateKeyEncoded = privateKey.getEncoded();
 //获取公钥字节数组
 byte[] publicKeyEncoded = publicKey.getEncoded();
 //进行base64编码
 String privateKeyString = Base64.encodeBase64String(privateKeyEncoded);
 String publicKeyString = Base64.encodeBase64String(publicKeyEncoded);
 //输出私钥
 System.out.println(privateKeyString);
 //输出公钥
 System.out.println(publicKeyString);

 // 保存文件
 FileUtils.writeStringToFile(new File(pubKeyPath), publicKeyString, Charset.forName("UTF-8"));
 FileUtils.writeStringToFile(new File(priKeyPath), privateKeyString, Charset.forName("UTF-8"));
}

/**
 * 获取私钥对象
 */
public static PrivateKey getPrivateKey() throws Exception{
 String privateKeyString = FileUtils.readFileToString(new File(priKeyPath), Charset.defaultCharset());
 //获取密钥工厂
 KeyFactory keyFactory = KeyFactory.getInstance(algorithm);
 //构建密钥规范 进行Base64解码
 PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(Base64.decodeBase64(privateKeyString));
 //生成私钥
 return keyFactory.generatePrivate(spec);
}

/**
 * 获取公钥对象
 */
public static PublicKey getPublicKey() throws Exception{
 String publicKeyString  = FileUtils.readFileToString(new File(pubKeyPath), Charset.defaultCharset());
 //获取密钥工厂
 KeyFactory keyFactory = KeyFactory.getInstance(algorithm);
 //构建密钥规范 进行Base64解码
 X509EncodedKeySpec spec = new X509EncodedKeySpec(Base64.decodeBase64(publicKeyString));
 //生成公钥
 return keyFactory.generatePublic(spec);
}

public static void main(String[] args) throws Exception {
 PrivateKey privateKey = RSAUtil.getPrivateKey();
 PublicKey publicKey = RSAUtil.getPublicKey();
 String  pubEnrypt = encrypt(publicKey, "传智播客".getBytes());
 System.out.println(pubEnrypt);
 //2.1私钥解密
 byte[] decrypt = decrypt(privateKey, pubEnrypt);
 System.out.println(new String(decrypt));
}
}

混合密码测试类:

/**
* 混合密码测试
*/
public class MixCryptTest {


   @Test
   public void testMxiCrypt() throws Exception {

       //准备好公钥与私钥,生成到文件中
       RSAUtil.createPubKeyPriKey(); //其中公钥给客户端使用,私钥服务端保留,下方代码表示客户端给服务端发送混合加密消息

       // *********** 客户端发送消息开始 *********** //
       //准备消息明文
       byte[] text = "大家好,我来自传智播客大家好,我来自传智播客".getBytes();

       //伪随机数生成器生成会话密钥
       byte[] sessionSecretKey = AESUtil.generateSecretKey("AES", 128);

       //公钥密码算法用公钥加密会话密钥
       String secretKeyEncrypted = RSAUtil.encrypt(RSAUtil.getPublicKey(), sessionSecretKey);
       System.out.println("[客户端]会话密钥加密结果:" + secretKeyEncrypted);

       //为对称加密算法准备加密时候初始向量值
       byte[] iv = AESUtil.generateSecretKey("AES", 128);
       //对称密码算法用会话密钥加密消息
       String textEncrypt = AESUtil.encrypt(text, sessionSecretKey, iv);
       System.out.println("[客户端]消息明文加密结果:" + textEncrypt);


       //拼接两个加密后的值
       byte[] b1 = Base64.decodeBase64(secretKeyEncrypted);
       int len1 = b1.length;
       byte[] b2 = Base64.decodeBase64(textEncrypt);
       int len2 = b2.length;
       //拼接两个加密后的值
       String splitFlag = "==itcast==";
       byte[] flag = splitFlag.getBytes("UTF-8");
       int lenFlag = flag.length;

       byte[] bFinal = new byte[len1+lenFlag+len2];
       System.arraycopy(b1, 0, bFinal, 0, len1);
       System.arraycopy(b2, 0, bFinal, len1, lenFlag);
       System.arraycopy(b2, 0, bFinal, len1+lenFlag, len2);
       String finalStr = Base64.encodeBase64String(bFinal);
       System.out.println("[客户端]拼接后的加密结果:" + finalStr);
       // *********** 客户端发送消息结束 *********** //


       // *********** 服务端解密消息开始 *********** //
       //开始分离数据
       byte[] finalBytes = Base64.decodeBase64(finalStr);
       byte[] sskEncryptedBytes = new byte[len1];
       byte[] textEncryptedBytes = new byte[len2];
       System.arraycopy(finalBytes, 0, sskEncryptedBytes, 0, len1);
       System.arraycopy(finalBytes, len1+lenFlag, textEncryptedBytes, 0, len2);
       String sskEncrypt = Base64.encodeBase64String(sskEncryptedBytes);
       String textEncrypted = Base64.encodeBase64String(textEncryptedBytes);
       //公钥密码用私钥解密会话密钥的密文得到会话密钥明文
       byte[] ssKBytes = RSAUtil.decrypt(RSAUtil.getPrivateKey(), sskEncrypt);

       //对称密码用会话密钥的明文解密消息
       String textPlain = AESUtil.decrypt(textEncrypted, ssKBytes, iv);
       System.out.println("[服务端]密文解密结果:" + textPlain);
       // *********** 服务端解密消息结束 *********** //
   }
}

五、结束语

       在今日文章中,我们体系化的了解到了密码的含义,介绍了现代密码学涉及到的编码技术和密码技术,其中最重要的就是对称密码技术和公钥密码技术,为了保证密码的安全我们还了解到了分组密码的加密模式和填充模式、及随机数和混合密码系统。

       通过今日的学习,相信大家内心中都已经揭开了密码的神秘面纱。当然,有关于密码相关的内容绝不仅仅只是今天这么多,还有非常多密码组合技术,比如下方这些密码技术等着我们进一步探究:

  • 数字签名:单项散列函数和公钥密码的组合

  • 证书:公钥和数字签名的组合

  • 消息认证码:单项散列函数和密码组组合,或者通过对称密码来生成

六、附录.密码发展历史

说明:该部分内容收集整理自网络

1. 第一阶段:远古密码

时间范围:远古到公元前400年左右

密码特征:

       这个时期所谓“密码”本质上是古人根据物理工具特性所作出的改造和伪装,当中并没有涉及到启发式的数学思维或方法,我们称之为远古密码。

1.1 阴符

       世界上最早的密码工具普遍被认为是在公元前1000年由姜子牙发明的军用“阴符”。相传商纣王末年,姜太公辅佐周室。有一次,姜太公带领的周军指挥大营被叛兵包围,情况危急,姜太公令信使突围,回朝搬兵,但又怕信使遗忘机密,或者周文王不认识信使,耽误了军务大事。

       于是其将自己珍爱的鱼竿折成数节,每节长短不一,各代表一件军机,令信使牢记,不得外传。信使几经周折回到朝中,周文王令左右将几节鱼竿合在一起,亲自检验。周文王辨认出是姜太公的心爱之物,于是亲率大军,解救姜太公,“阴符”由此诞生。

当时“阴符”的规格共有8种(即8个不同的尺寸),每种规格分别代表一种状态。比如,长为1尺的“阴符”表示大获全胜、全歼敌军;长为4寸的“阴符”表示军队失败,将领阵亡等。

尺寸状态含义
1尺大获全胜、全歼敌军
9寸击破敌军、擒获敌将
8寸迫使敌军投降、攻占敌人城邑
7寸击退敌人、通报战况
6寸激励军民、坚强守御
5寸请求补给粮草、增加兵力
4寸报告军队失败、将领阵亡
3寸战斗失利、士卒伤亡

1.2 阴书

       阴符虽有简便保密的特点,但却过于简单,无法表达更复杂的含义。为此,姜子牙又进一步发明了"阴书",即将竖写的秘密文书横截成3段,并派出3位信使各执一段,于不同时间、不同路线分别出发,先后送达收件者。任何人只有在收齐3段文件以后才能获悉秘密文书的全部内容。

1.3 塞塔密码

       公元前400年,古希腊的斯巴达人发明了塞塔(scytale)密码,也称密码棒。他们把长条纸螺旋地斜绕在一根多棱棒上,让纸条被多棱棒分割成一个个的类似格子一样的区域,并沿着这个区域,将文字沿棒的水平方向从左到右书写,写一个字旋转一下,换一个区域,写完一行再另起一行从左到右写。

       这样一来,本来完整通顺的一句话,就被机械地分割开了。将信息写完后,解下来的纸条上的文字消息杂乱无章、无法理解,这就将原来的信息明文转换成了密文。在把展开的纸条传递出去以后,解密人只有将纸条斜绕在另一根同等尺寸的多棱棒上才能看到原始消息。

密码棒.png

1.4 隐语

       《左传·宣公十二年》记载了一种用于替换信息的“隐语”法。所谓“隐语”,就是电影里常见的暗语、黑话,用一个完全不相关的词来代表特定含义。

2. 第二阶段:古典密码

时间范围:公元前400年-20世纪初 (classical cryptography),长达几千年

密码特征:

       古典密码编码方法的核心是代替和置换。

  • 代替:代替密码是将明文中的字符替代成其他字符。代替密码是指先建立一个替换表,加密时将需要加密的明文依次通过查表,替换为相应的字符,明文字符被逐个替换后,生成无任何意义的字符串,即密文,替代密码的密钥就是其替换表。

  • 置换:置换密码又称换位密码,是根据一定的规则重新排列明文,以便打破明文的结构特性。置换密码的特点是保持明文的所有字符不变,只是利用置换打乱了明文字符的位置和次序。也就是说,改变了明文的结构,不改变明文的内容。

2.1 恺撒密码

       公元前58年左右由凯撒大帝发明该密码应用在军事通信之中,故而得名为恺撒密码(Caesar cipher)。恺撒密码是一种代替密码,其加密方式就是将英文中的每个字母用另外一个字母来代替。恺撒密码是对英文26个字母进行移位代替的密码,属于“移位代替密码”,是最简单的一类代替密码。

       恺撒密码的本质是构建了一张明密代替表,即密码本。明密代替表就像一把钥匙,用这把钥匙可以方便地进行“加密”和“解密”操作。

凯撒密码.png

2.2 棋盘密码

公元前两世纪,古希腊人发明了利用Polybius方阵加密的方法,即棋盘密码。所谓棋盘密码,就是把字母按序或乱序填入表格里,并对应行和列进行加密,比如最简单的棋盘密码:


1234
1ABCD
2EFGH
3IJKL
4MNOP

假设明文是GOD,那么加密后则为23 43 14。棋盘密码虽然简单,但实用性非常好。

2.3 圆盘密码

       15世纪末,佛罗洛萨人Alberti又发明了圆盘密码,也称密码盘。所谓圆盘密码,就是两个同心圆盘上,内盘随机填写字符,而外盘的字符则按照一定顺序来填写。使用者只需转动圆盘就可以找到内外盘字符间的映射方式。

密码盘.png

2.4 维吉尼亚密码

       上述密码都可以统称为单表代替密码,即明文和密文之间均具有一一对应的关系,了打破这种对应关系,1553年,Giovan Battista Bellaso在其著作中发明了通过字母密钥来循环加密明文的“维吉尼亚”密码(也称维热纳尔密码,Vigenere密码)。

       假设要加密的明文是:TO BE OR NOT TO BE THAT IS THE QUESTION,选择某一关键词并重复而得到密钥,如关键词为RELATIONS,即RE LA  TI  ONS  RE LA TION  SR ELA TIONSREL。对于明文的第一个字母T,对应密钥的第一个字母R,于是使用表格中R行字母表进行加密,就可以在一个特定的26*26表格中搜寻第R行、第T列所对应的字母作为T的密文。以此类推,得到密文为:KSMEH ZBBLK SMEMP OGAJX SEJCS FLZSY。

得出对应关系如下:

明文:TO BE OR NOT TO BE THAT IS THE QUESTION

密钥:RE LA TI ONS RE LA TION SR ELA TIONSREL

密文:KS ME HZ BBL KS ME MPOG AJ XSE JCSFLZSY

维吉尼亚密码.jpg

解密:密钥第一个字母R对应的行中,查找密文的第一个字母K,发现K对应的是t列,所以明文是T,以此类推。

2.5 栅栏密码

       古典密码中还有一类著名的密码是“置换密码”,置换密码不是用密文字母代替相对应的明文字母,而是通过打乱明文字母的位置,使有意义的明文信息变换为无意义的密文乱码。栅栏密码是置换密码的典型代表。栅栏密码出现于1861年至1865年的美国南北战争时期。其加密原理是:明文按列写入,密文按行输出。

加密原理:把将要传递的信息中的字母交替排成上下两行。再将下面一行字母排在上面一行的后边,从而形成一段密码。比如下面的示例:

明文:THE LONGEST DAY MUST HAVE AN END

  • 加密:

1、把将要传递的信息中的字母交替排成上下两行。

T E O G S D Y U T A E N N

H L N E T A M S H V A E D

2、 密文:

将下面一行字母排在上面一行的后边。

TEOGSDYUTAENN HLNETAMSHVAED

  • 解密:

先将密文分为两行

T E O G S D Y U T A E N N

H L N E T A M S H V A E D

再按上下上下的顺序组合成一句话

明文:THE LONGEST DAY MUST HAVE AN END

2.6 隐写术

       与密码技术对应还有一种技术,目的不是为了让消息变得无法解读,而是想办法隐藏消息本身,这种技术一般称为隐写术。隐写术一般分为两类:

  • 隐写药水

    在古代的战争中,多见使用隐藏信息的方式保护重要的通信资料。比如先把需要保护的信息用化学药水写到纸上,药水干后,纸上看不出任何的信息,需要使用另外的化学药水涂抹后才可以阅读纸上的信息。

  • 藏头尾诗词

    比如《唐寅诗集》中“我爱秋香”。如下:

    我画兰江水悠悠

    爱晚亭上枫叶稠

    秋月融融照佛寺

    香烟袅袅绕轻楼

总结:古典密码中不管是恺撒密码还是栅栏密码,都有自身的缺点可以被破译者利用,但是如果将置换密码与代替密码二者组合起来,则可以在一定程度上使得变换更复杂。乘积密码就是以某种方式连续执行两个或多个密码变换技术,从而使最后加密结果更随机,从密码编码的安全性角度来看,比单一的代替密码和置换密码都更强。古典密码中的代替密码(主要代表为恺撒密码)与置换密码(主要代表为栅栏密码)可以组合成多种新的密码形式,即乘积密码的具体形式,这也是分组密码的雏形。

3. 第三阶段:近代密码

时间范围:20世纪初到20世纪50年代,包含1战和2战时期

密码特征:密码机的迅速发展,越来越多的数学家加入密码队伍

       在战争中,密码设计者不断地设计出新的密码,这些密码又不断地被密码分析者破译。设计和破译就像矛和盾,此消彼长,密码在战争中不断发展演变,越来越复杂,越来越难以破译。手工作业方式已难以满足复杂密码运算的要求,密码研究者设计出了一些复杂的机械和电动机械设备,实现了信息的加解密操作,近代密码时期宣告到来。

3.1 Playfair密码

       在第一次世界大战前期,英军陆军主要使用是英国物理学家Wheatstone在1854年发明的Playfair密码。Playfair密码的核心是通过使用一个关键词方格来对字符对进行加密,此前曾被广泛用于克里米亚和布尔战争。但在1915年,Playfair密码就被同盟国破解了。

3.2 一次性便笺密码

       1918年,美国数学家Gillbert Vernam发明出一次性便笺密码,它是一种理论上绝对无法破译的加密系统,被誉为密码编码学的圣杯。但由于产生和分发大量随机密钥的过程十分困难,因此这种密码的实际应用受到了很大限制。从另一方面来说,其安全性也更加无法保证。

3.3 恩尼格玛密码

       1919年,德国工程师Arthur Scherbius设计出了历史上最著名的密码机:Enigma机,恩尼格玛密码机是转子机械密码机的统称,它包括了一系列不同的型号,由德国人亚瑟·谢尔比乌斯和理查德·里特发明,在20世纪20年代开始被用于商业,后来被一些国家的军队与政府进行改造并使用,最著名的是掀起第二次世界大战的纳粹德国。

       最先破解早期Enigma密码机的是波兰人,其利用德军电报中前几个字母的重复出现,摸索出了破译方案,并将其告知英军和法军。英军在计算机理论之父Turing的带领下,通过寻找德军在密钥选择上的失误以及借助战争中成功夺取的德军密码本破解出重要的德军情报。

3.4 JN-25密码

       第二次世界大战中,密码攻防战持续升级。除盟军对恩尼格玛密码机破译的典型案例外,在亚洲战场,编码与破译之间的斗争同样是惊心动魄。1943年春天,山本五十六为了控制不断恶化的残局,亲自前往所罗门群岛基地巡视,以鼓舞士气。

       1943年4月13日,日军第八舰队司令将山本一行视察的行程、时间表,用D号密码(美军称为JN-25)加密后发给有关基地。尽管该密码的密码本在4月2日刚刚被换过,但美国破译人员根据过去收集的资料,利用多表代替的统计特性破译了这份密报。经过周密安排,美军飞机于4月18日击落了飞往视察地途中的山本五十六乘坐的飞机。

4. 第四阶段: 现代密码

时间范围:1949年至今

密码特征:数据的安全基于密钥而不是算法的保密

       1949年信息论创始人Shannon香农发表划时代论文《The Communication Theory of Secret Systems》,其中定义了理论安全性,提出扩散和混淆原则。奠定了现代密码学的理论基础。

       1972年,IBM研制出了对称密码体制加密算法。3年以后,美国国家标准局将其颁布为国家标准,即数据加密标准(DES)。这是密码学历史上一个颇具里程碑意义的事件。

       1976年,W.Diffie和M.Hellman发表了《密码学的新方向》一文,文中首次提出了适应网络保密通信需求的公钥密码思想,掀起了公钥密码学的序幕。次年,美国的Ronald Rivest、Adi Shamir和Len Adleman提出了第一个建立在大数因子分解基础上的公钥密码算法,即著名的RSA算法。

       1976年,Victor Miller和Neal Koblitz分别提出了如今家喻户晓的椭圆曲线密码学(ECC)。尽管在当时而言,ECC更接近于数学理想的范畴,不具备实用性,但ECC在安全性上的优势以及实现效率使其一直成为密码学家们乐此不疲的研究课题——相比起其它公钥密码算法,ECC的抗攻击性具有绝对的优势。例如,使用ECC加密的256位密钥所提供的安全性,与使用RSA或DSA加密的3072位密钥相当。这意味着ECC算法所要求的带宽更低、存储空间更小。这些优点在某些对于带宽、处理器能力或存储空间有限制的应用(比如移动设备)中显得尤为重要。

       90年代初,麻省理工学院教授Ronald Rivest提出了MD4信息摘要算法,它是一种用来测试信息完整性的哈希函数。MD4的实现,旋即开启了哈希函数的大门,包括后来Ronald Rivest重新提出的安全性更优的MD5,由NSA和NIST提出的SHA函数家族以及Hans Dobbertin,Antoon Bosselaers 和 Bart Prenee提出的RIPEMD。在这一时期,密码学家来学嘉和JamesMasseey还提出了在软硬件实现上比DES更优的国际数据加密算法,即IDEA。

       随着计算机能力的不断提高,不少密码体系比如主流的DES正逐步面临淘汰。1998年,电子边境基金会(EFF)利用耗资25万美元打造的专用计算机,仅用56个小时就成功破解了DES密钥。随后在1999年,EFF仅用了22小时15分就完成了破解工作。此后,美国国家安全局宣布弃用DES转而启用由比利时密码学家Joan Daemen和Vincent Rijmen所提出的Rijndael加密算法,即高级加密标准AES。

       进入千禧年以后,MD4、MD5、RIPEMD(RIPEMD-160仍然安全)、SHA1以及RSA-768的强抗碰撞性相继被攻破,RSA-1024业已在2012年前后被停用。随着区块链技术的兴起,ECC俨然成为密码学殿堂最亮眼的新星,但依旧难逃量子计算技术的威胁。