关于加解密、加签验签的那些事

作者:微信小助手

发布时间:2021-01-26T14:00:52

(给ImportNew加星标,提高Java技能)

转自:不学无数的程序员

链接:jianshu.com/p/5e9fe1fff6a3

面对MD5、SHA、DES、AES、RSA等等这些名词你是否有很多问号?这些名词都是什么?还有什么公钥加密、私钥解密、私钥加签、公钥验签。这些都什么鬼?或许在你日常工作没有听说过这些名词,但是一旦你要设计一个对外访问的接口,或者安全性要求高的系统,那么必然会接触到这些名词。所以加解密、加签验签对于一个合格的程序员来说是必须要掌握的一个概念。那么加解密相关的密码学真的离我们很遥远吗?其实生活中有很多常见的场景其实都用到了密码学的相关知识,我们不要把它想得太难,例如在《睡在我上铺的兄弟》这一段中作弊绕口令中,小瘪三代表A,小赤佬代表B,唉呀妈呀代表C,坑爹呀是D,这一段绕口令其实也是密码学的一种。有兴趣的小伙伴可以看一下这一片段绕口令片段。所以其实密码学与我们生活息息相关,接下来我们就一文彻底搞懂这些概念。


《睡在我上铺的兄弟》作弊绕口令片段:

https://www.bilibili.com/video/av3696396/


没有硝烟的战场——浅谈密码技术


没有根基也许可以建一座小屋,但绝对不能造一座坚固的大厦。


密码这个词有很多种的解释,在现代社会如果不接触编程的话,那么普遍的认为是我们设置的登录密码、或者是去银行取钱时输入的数字。都是我们在注册时实现给提供服务的一方存储一组数字,以后我们登录的时候就用这组数字相当于就证明了我们的身份。这个数字通常来说就是叫做密码。


而我们需要了解的不是上面说的密码,而是一种“密码术”,就是对于要传递的信息按照某种规则进行转换,从而隐藏信息的内容。这种方法可以使机密信息得以在公开的渠道传递而不泄密。使用这种方法,要经过加密过程。在加密过程中我们需要知道下面的这些概念:


  • 原文:或者叫明文,就是被隐藏的文字。

  • 加密法:指隐藏原文的法则。

  • 密文:或者叫伪文,指对原文按照加密法处理过后生成的可公开传递的文字。

  • 密钥:在加密法中起决定性的因素,可能是数字、词汇,也可能是一些字母,或者这些东西的组合。


加密的结果生成了密文,要想让接受者能够读懂这些密文,那么就要把加密法以及密钥告诉接受者,否者接受者无法对密文解密,也就无法读懂原文。


从历史的角度来看,密码学大概可以分为古典密码学和近现代密码学两个阶段。两者以现代信息技术的诞生为分界点,现在所讨论的密码学多指的是后者,建立在信息论和数学成果基础之上的。


古典密码学


古典密码学源自于数千年前,最早在公元前 1900 年左右的古埃及,就出现了通过使用特殊字符和简单替换式密码来保护信息。美索不达米亚平原上曾经出土一个公元前 1500 年左右的泥板,其上记录了加密描述的陶瓷器上釉的工艺配方。古希腊时期(公元前 800 ﹣前 146 年)还发明了通过物理手段来隐藏信息的“隐写术”,例如使用牛奶书写、用蜡覆盖文字等。后来在古罗马时期还出现了基于替换加密的凯撒密码,据称凯撒曾用此方法与其部下通信而得以命名。这些手段多数是采用简单的机械工具来保护秘密,在今天看来毫无疑问是十分简陋,很容易猜出来的。严格来看,可能都很难称为密码科学。


凯撒密码是当偏移量是 3 的时候,所有的字母都 A 都将被替换成 D,B 变成 E,以此类推。



凯撒密码


近代密码学


近代密码学的研究来自于第一、二次世界大战中对于军事通信进行保护和猜出来的需求。1901年12月,意大利的工程师 Guglielmo Marconi(奎里亚摩•马可尼)成功完成了跨越大西洋的无线电通信的实验,在全球范围内引发轰动,推动了无线电通信时代的到来。无线电大大提高了远程通信的能力,但是它有一个天然的缺陷——很难限制接收方,这就意味着你所传的信息有可能被拦截,因此就催生了加密技术的发展。


对于无线电信息进行加密和解密也直接促进了近现代密码学和计算机技术的出现。反过来这些科技进步也影响了时代的发展。一战时期德国外交部长 Arthur Zimmermann(阿瑟•齐默尔曼)拉拢墨西哥构成抗美军事同盟的电报(1917年1月16日)被英国情报机构— 40 号办公室破译,直接导致了美国的参战;二战时期德国使用的恩尼格玛(Enigma)密码机(当时最先进的加密设备)被盟军成功破译(1939年到1941年),导致大西洋战役德国失败。据称,二战时期光英国从事密码学研究的人员就达到 7000 人,而他们的成果使二战结束的时间至少提前了一到两年时间。


接下来就是可以称之为是密码学发展史上里程碑的事件了。1945年9月1日,Claude Elwood Shannon(克劳德•艾尔伍德•香农)完成了划时代的内部报告《A Mathematical Theory of Cryptography(密码术的一个数学理论)》,1949 年 10 月,该报告以《Communication Theory of Secrecy Systems(保密系统的通信理论)》为题在 Bell System Technical Journal(贝尔系统技术期刊)上正式发表。这篇论文首次将密码学和信息论联系到一起,为对称密码技术提供了数学基础。这也标志着近现代密码学的正式建立。这也是密码学发展史上的第一座里程碑性事件。


密码学发展史上的第二个里程碑性事件是 DES 的出现。DES 全称为 Data Encryption Standard,即数据加密标准,是一种使用密钥加密的分组密码算法,1977年被美国联邦政府的国家标准局确定为联邦资料处理标准(FIPS),并授权在非密级政府通信中使用,随后该算法在国际上广泛流传开来。


密码学发展史上的第三个里程碑性事件就是我们区块链中广泛应用的公钥密码,也就是非对称密码算法的出现。1976年11月,Whitfield Diffie 和 Martin E.Hellman 在 IEEE Transactions on Information Theory 上发表了论文《New Directions in Cryptography(密码学的新方向)》,探讨了无需传输密钥的保密通信和签名认证体系问题,正式开创了现代公钥密码学体系的研究。在公钥密码发现以前,如果需要保密通信,通信双方事先要对加解密的算法以及要使用的密钥进行事先协商,包括送鸡毛信,实际上是在传送密钥。但自从有了公钥密码,需要进行秘密通信的双方不再需要进行事前的密钥协商了。公钥密码在理论上是不保密的,在实际上是保密的。也就是说,公钥密码是可以猜出来的,但需要极长的时间,等到猜出来了,这个秘密也没有保密的必要了。


上面我们说到了关于近现代的密码学相关的东西,基本上总结下来我们现在常用的就两个,一个是对称加密算法,一个是非对称加密算法。那么接下来我们就以介绍这两个概念为主线引出开题中我们提到的概念。


程序实现


对称加密算法


对称加密指的就是加密和解密使用同一个秘钥,所以叫做对称加密。对称加密只有一个秘钥,作为私钥。具体的算法有:DES、3DES、TDEA、Blowfish、RC5、IDEA。但是我们常见的有:DES、AES 等等。


那么对称加密的优点是什么呢?算法公开、计算量小、加密速度快、加密效率高。缺点就是秘钥的管理和分发是非常困难的,不够安全。在数据传送前,发送方和接收方必须商定好秘钥,然后双方都必须要保存好秘钥,如果一方的秘钥被泄露了,那么加密的信息也就不安全了。另外,每对用户每次使用对称加密算法时,都需要使用其他人不知道的唯一秘钥,这会使得收、发双方所拥有的的钥匙数量巨大,秘钥管理也会成为双方的负担。


加密的过程我们可以理解为如下:


  • 加密:原文+秘钥 = 密文

  • 解密:密文-秘钥 = 原文


可以看到两次过程使用的都是一个秘钥。用图简单表示如下:



实战演练


既然我们知道关于对称加密算法的相关知识,那么我们日常用 Java 如何实现对称加密的加密和解密动作呢?常见的对称加密算法有:DES、AES 等。


DES


DES 加密算法是一种分组密码,以 64 位为分组对数据加密,它的密钥长度是 56 位,加密解密用同一算法。DES 加密算法是对密钥进行保密,而公开算法,包括加密和解密算法。这样,只有掌握了和发送方相同密钥的人才能解读由 DES 加密算法加密的密文数据。因此,破译 DES 加密算法实际上就是搜索密钥的编码。对于 56 位长度的密钥来说,如果用穷举法来进行搜索的话,其运算次数为 2 的 56 次方。


接下来用 Java 实现 DES 加密


private final static String DES = "DES";
public static void main(String[] args) throws Exception { String data = "123 456"; String key = "wang!@#$"; System.err.println(encrypt(data, key)); System.err.println(decrypt(encrypt(data, key), key));
}
/** * Description 根据键值进行加密 * @param data * @param key 加密键byte数组 * @return * @throws Exception */public static String encrypt(String data, String key) throws Exception { byte[] bt = encrypt(data.getBytes(), key.getBytes()); String strs = new BASE64Encoder().encode(bt); return strs;}
/** * Description 根据键值进行解密 * @param data * @param key 加密键byte数组 * @return * @throws IOException * @throws Exception */public static String decrypt(String data, String key) throws IOException, Exception { if (data == null) return null; BASE64Decoder decoder = new BASE64Decoder(); byte[] buf = decoder.decodeBuffer(data); byte[] bt = decrypt(buf,key.getBytes()); return new String(bt);}
/** * Description 根据键值进行加密 * @param data * @param key 加密键byte数组 * @return * @throws Exception */private static byte[] encrypt(byte[] data, byte[] key) throws Exception { // 生成一个可信任的随机数源 SecureRandom sr = new SecureRandom();
// 从原始密钥数据创建DESKeySpec对象 DESKeySpec dks = new DESKeySpec(key);
// 创建一个密钥工厂,然后用它把DESKeySpec转换成SecretKey对象 SecretKeyFactory keyFactory = SecretKeyFactory.getInstance(DES); SecretKey securekey = keyFactory.generateSecret(dks);
// Cipher对象实际完成加密操作 Cipher cipher = Cipher.getInstance(DES);
// 用密钥初始化Cipher对象 cipher.init(Cipher.ENCRYPT_MODE, securekey, sr);
return cipher.doFinal(data);}

/** * Description 根据键值进行解密 * @param data * @param key 加密键byte数组 * @return * @throws Exception */private static byte[] decrypt(byte[] data, byte[] key) throws Exception { // 生成一个可信任的随机数源 SecureRandom sr = new SecureRandom();
// 从原始密钥数据创建DESKeySpec对象 DESKeySpec dks = new DESKeySpec(key);
// 创建一个密钥工厂,然后用它把DESKeySpec转换成SecretKey对象 SecretKeyFactory keyFactory = SecretKeyFactory.getInstance(DES); SecretKey securekey = keyFactory.generateSecret(dks);
// Cipher对象实际完成解密操作 Cipher cipher = Cipher.getInstance(DES);
// 用密钥初始化Cipher对象 cipher.init(Cipher.DECRYPT_MODE, securekey, sr);
return cipher.doFinal(data);}


输出以后可以看到数据被加密了:


5fiw/XhRJ0E=123 456


在 Java 中用 DES 加密有一个特殊的地方


  1. 秘钥设置的长度必须大于等于 8。

  2. 秘钥设置的长度如果大于 8 的话,那么只会取前 8 个字节作为秘钥。


为什么呢,我们可以看到在初始化 DESKeySpec 类的时候有下面一段,其中 var1 是我们传的秘钥。可以看到它进行了截取,只截取前八个字节。


public DESKeySpec(byte[] var1, int var2) throws InvalidKeyException {    if (var1.length - var2 < 8) {        throw new InvalidKeyException("Wrong key size");    } else {        this.key = new byte[8];        System.arraycopy(var1, var2, this.key, 0, 8);    }}


AES


AES 加密算法是密码学中的高级加密标准,该加密算法采用对称分组密码体制,密钥长度的最少支持为 128、192、256,分组长度 128 位,算法应易于各种硬件和软件实现。这种加密算法是美国联邦政府采用的区块加密标准,AES 标准用来替代原先的 DES,已经被多方分析且广为全世界所使用。


JCE(Java Cryptography Extension)在早期JDK版本中,由于受美国的密码出口条例约束,Java 中涉及加解密功能的 API 被限制出口,所以 Java 中安全组件被分成了两部分: 不含加密功能的 JCA(Java Cryptography Architecture )和含加密功能的 JCE(Java Cryptography Extension)。


JCE 的 API 都在 javax.crypto 包下,核心功能包括:加解密、密钥生成(对称)、MAC 生成、密钥协商。


加解密功能由 Cipher 组件提供,其也是 JCE 中最核心的组件。


在设置 Cipher 类的时候有几个注意点


  1. Cipher 在使用时需以参数方式指定 transformation。

  2. transformation 的格式为 algorithm/mode/padding,其中 algorithm 为必输项,如: AES/DES/CBC/PKCS5Padding,具体有哪些可看下表。

  3. 缺省的 mode 为 ECB,缺省的 padding 为 PKCS5Padding。

  4. 在 block 算法与流加密模式组合时, 需在 mode 后面指定每次处理的 bit 数, 如 DES/CFB8/NoPadding, 如未指定则使用缺省值, SunJCE 缺省值为 64bits。

  5. Cipher 有 4 种操作模式:ENCRYPT_MODE(加密)、DECRYPT_MODE(解密)、WRAP_MODE(导出Key)、UNWRAP_MODE(导入Key),初始化时需指定某种操作模式。


算法/模式/填充 16字节加密后数据长度 不满16字节加密后长度
AES/CBC/NoPadding 16 不支持
AES/CBC/PKCS5Padding 32 16
AES/CBC/ISO10126Padding 32 16
AES/CFB/NoPadding 16 原始数据长度
AES/CFB/PKCS5Padding 32 16
AES/CFB/ISO10126Padding 32 16
AES/ECB/NoPadding 16 不支持
AES/ECB/PKCS5Padding 32 16
AES/ECB/ISO10126Padding 32 16
AES/OFB/NoPadding 16 原始数据长度
AES/OFB/PKCS5Padding 32 16
AES/OFB/ISO10126Padding 32 16
AES/PCBC/NoPadding 16 不支持
AES/PCBC/PKCS5Padding 32 16
AES/PCBC/ISO10126Padding 32 16


秘钥的可以由我们自己定义,也可以是由AES自己生成,当自己定义是需要是要注意:


  1. 根据 AES 规范,可以是 16 字节、24 字节和32 字节长,分别对应 128 位、192 位和 256 位;