数据编码、数字签名、信息加密 是前后端开发都经常需要使用到的技术,应用场景包括了用户登入、交易、信息通讯、OAuth 等等,不同的应用场景也会需要使用到不同的签名加密算法,或者需要搭配不一样的 签名加密算法 来达到业务目标。这里简单的给大家介绍几种常见的签名加密算法和一些典型场景下的应用。主要包括:
数据编码:Base64
散列算法(消息摘要、签名算法):MD5/SHA/MAC
对称加密算法:DES/AES/RC2/RC4
非对称加密算法:RSA
数据编码
Base64算法并不是加密算法,它的出现是为了解决ASCII码在传输过程中可能出现乱码的问题。Base64是网络上最常见的用于传输8bit字节码的可读性编码算法之一。可读性编码算法不是为了保护数据的安全性,而是为了可读性。可读性编码不改变信息内容,只改变信息内容的表现形式。Base64使用了64种字符:大写A到Z、小写a到z、数字0到9、“+”和“/”,故得此名。
储备知识Byte和bit
Byte:字节,数据存储的基本单位;
bit:比特,也叫位,一个位只能存储0或者1。
关系:1Byte = 8bit。
一个英文字符占1个字节,8位:
程序输出:
一个中文字符在不同编码下所占的字节数不同:
程序输出:
所以在UTF-8编码下,一个中文占3个字节;在GBK编码下,一个中文占2个字节。
Base64编码原理
Base64编码表:
索引 | 对应字符 | 索引 | 对应字符 | 索引 | 对应字符 | 索引 | 对应字符 |
---|---|---|---|---|---|---|---|
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 |
Base64编码的过程:
- 将字符串转换为字符数组;
- 将每个字符转换为ASCII码;
- 将ASCII码转换为8bit二进制码;
- 然后每3个字节为一组(一个字节为8个bit,所以每组24个bit);
- 将每组的24个bit分为4份,每份6个bit;
- 在每6个bit前补0,补齐8bit(前面补0不影响数值大小);
- 然后将每8bit转换为10进制数,根据上面的Base64编码表进行转换。
上面步骤中,为什么要将每组24个bit分为4份,每份6个bit呢?因为6bit的最大值为111111,转换为十进制为63,所以6bit的取值范围为0~63,这和base64编码表长度一致。
根据上面的过程,我们来举个例子:现要对hello这个字符串进行Base64编码,过程如下:
- hello转换为字符数组:h e l l o;
- 对应的ASCII码为:104 101 108 108 111;
- 转换为8bit二进制数:01101000 01100101 01101100 01101100 01101111
- 分组,每组24个bit(不足24个bit的用00000000补齐): 011010000110010101101100 011011000110111100000000;
- 每组24bit分为4份,每份6bit:011010 000110 010101 101100 011011 000110 111100 000000;
- 在每6个bit前补0,补齐8bit:00011010 00000110 00010101 00101100 00011011 00000110 00111100 00000000;
- 将每8bit转换为10进制数:26 6 21 44 27 6 60 0
- 从上面Base64编码表中找到十进制数对应的字符(末尾的0并不是A,而是用=等号补位):a G V s b G 8 =
所以hello经过Base64编码的结果为aGVsbG8=
我们可以用代码验证下(JDK8开始已经提供了Base64的实现):
程序输出也是aGVsbG8=
URL Base64算法
Base64编码值通过URL传输会出现问题,因为Base64编码中的“+”和“/”符号是不允许出现在URL中的。同样,符号“=”用做参数分隔符,也不允许出现在URL中,根据RFC 4648中的建议,“~”和“.”符都有可能替代“=”符号。但“~”符号与文件系统相冲突,不能使用;如果使用“.”符号,某些文件系统认为该符号连续出现两次则为错误。
所以common codec包下的URL Base64算法舍弃了填充符,使用了不定长URL Base64编码。
引入common codec依赖包:
举个例子:
输出如下:
散列算法
散列算法(消息摘要算法、签名算法)是单向不可逆的,无法通过加密后的散列值反推原始值,相同的内容用同样的摘要算法获得的散列值是一样的,所以常用于验证数据的完整性。
该算法主要分为三大类:MD(Message Digest,消息摘要算法)、SHA(Secure HashAlgorithm,安全散列算法)和MAC(Message Authentication Code,消息认证码算法)。MD系列算法包括MD2、MD4和MD5共3种算法;SHA算法主要包括其代表算法SHA-1和SHA-1算法的变种SHA-2系列算法(包含SHA-224、SHA-256、SHA-384和SHA-512);MAC算法综合了上述两种算法,主要包括HmacMD5、HmacSHA1、HmacSHA256、HmacSHA384和HmacSHA512算法。这节主要记录下JDK8对这些算法的支持情况。
消息摘要算法的结果我们一般将其转换为16进制字符串,方便阅读传输。
MD系列算法
MD5算法是MD系列算法的代表,由MD2、MD4等算法演变而来。无论采用哪种MD算法,结果都是32字节的16进制字符串。JDK8只支持MD2和MD5两种MD算法。
新建一个maven项目,引入common-codec依赖(包含一些加解密工具类,用于增强JDK或者简化JDK相关加解密API):
JDK8中,MD系列算法的实现是通过MessageDigest类来完成的,下面演示下使用JDK8原生API实现MD2和MD5加密(算法名称不区分大小写)。
MD2:
运行结果:
MD5(只需要将算法改为md5即可):
运行结果:
common-codec的DigestUtils也提供了MD2和MD5算法相关方法:
common-codec并没有自己实现相关算法,而是对JDK原生API进行封装,使用起来更方便。
SHA系列算法
SHA算法是基于MD4算法实现的,作为MD算法的继任者,成为了新一代的消息摘要算法的代表。SHA与MD算法不同之处主要在于摘要长度,SHA算法的摘要更长,安全性更高。
SHA算法家族目前共有SHA-1、SHA-224、SHA-256、SHA-384和SHA-512五种算法,通常将后四种算法并称为SHA-2算法。
JDK8支持SHA-1、SHA-256、SHA-384、SHA-224和SHA-512五种算法,其中SHA的写法等价于SHA-1。JDK8中,SHA系列算法的实现也是通过MessageDigest类来完成的:
运行结果:
剩下的几种算法可以自己尝试。
common-codec同样也提供了SHA相关算法的API:
MAC系列算法
MAC算法结合了MD和SHA算法的优势,并加入秘钥的支持,是一种更为安全的消息摘要算法。因为MAC算法融合了秘钥散列函数(keyed-Hash),通常我们也把MAC称为HMAC(keyed-Hash Message Authentication Code)。
MAC算法主要集合了MD和SHA两大系列消息摘要算法。MD系列算法有HmacMD2、HmacMD4和HmacMD5三种算法;SHA系列算法有HmacSHA1、HmacSHA224、HmacSHA256、HmacSHA384和HmacSHA512五种算法。
JDK8支持了HmacMD5、HmacSHA1、HmacSHA224、HmacSHA256、HmacSHA384和HmacSHA512这六种MAC算法,通过Mac类实现。
输出结果:
实际应用
在Tomcat下载页面:https://tomcat.apache.org/download-70.cgi中,我们可以查看相关文件的摘要:
我们将32-bit Windows zip这个文件下载下来,计算出这个文件的sha512值:
结果:
这和页面https://downloads.apache.org/tomcat/tomcat-7/v7.0.105/bin/apache-tomcat-7.0.105-windows-x86.zip.sha512上显示的一致,说明我们下载的文件在传输过程中没有被篡改。
对称加密算法
对称加密算法加密和解密使用的是同一份秘钥,解密是加密的逆运算。对称加密算法加密速度快,密文可逆,一旦秘钥文件泄露,就会导致原始数据暴露。对称加密的结果一般使用Base64算法编码,便于阅读和传输。JDK8支持的对称加密算法主要有DES、DESede、AES、Blowfish,以及RC2和RC4等。不同的算法秘钥长度不同,秘钥长度越长,加密安全性越高。
DES
DES(Data Encryption Standard,数据加密标准)算法是对称加密算法领域中的典型算法,DES算法秘钥较短,以现在计算机的计算能力,DES算法加密的数据在24小时内可能被破解。所以DES算法已经被淘汰,建议使用AES算法,不过这里还是简单了解下。
JDK8仅支持56位长度的DES秘钥,下面举个JDK8实现DES加密的例子:
转换模式transformation这里先设置为和加密算法一样,下面介绍了加密模式和填充模式后再作说明。
上面步骤看着挺多,其实可以总结为如下几步:
- 生成加密秘钥;
1.1. 通过KeyGenerator生成一个指定位数的秘钥;
1.2. 通过上面生成的秘钥实例化算法对应的秘钥材料KeySpec;
1.3. 使用秘钥材料通过秘钥工厂SecretKeyFactory生成算法秘钥SecretKey。
- 通过转换模式实例化Cipher;
- 指定Cipher模式和秘钥,进行加解密操作。
运行结果如下:
如果在生成秘钥的时候,不指定为56位,则会抛出java.security.InvalidParameterException: Wrong keysize: must be equal to 56异常。
DESede
作为DES算法的一种改良,DESede算法(也称为3DES,三重DES)针对其秘钥长度偏短和迭代次数偏少等问题做了相应改进,提高了安全强度,但同时也造成处理速度较慢、秘钥计算时间加长、加密效率不高的问题。所以这里还是简单了解下,实际还是推荐用AES。
JDK8支持112位或168位长度的DESede秘钥,下面举个JDK8实现DESede加密的例子:
过程和DES一致,区别仅在于使用的加密算法为DESede,秘钥长度指定为112,秘钥材料对象为DESedeKeySpec类。
上面程序输出如下:
如果指定不合法的秘钥长度,程序将抛出java.security.InvalidParameterException: Wrong keysize: must be equal to 112 or 168异常。
AES
AES(AdvancedEncryption Standard,高级数据加密标准)算法支持128位、192位和256位的秘钥长度,加密速度比DES和DESede都快,至今还没有被破解的报道。经过验证,目前采用的AES算法能够有效抵御已知的针对DES算法的所有攻击方法,如部分差分攻击、相关秘钥攻击等。AES算法因秘钥建立时间短、灵敏性好、内存需求低等优点,在各个领域得到广泛的研究与应用。
JDK8支持128位、192位和256位长度的AES秘钥,下面举个JDK8实现AES加密的例子:
过程和上面例子大体一致,唯一区别是使用AES算法无需指定特定类型的秘钥材料,直接用SecretKeySpec对象即可。
程序输出如下:
如果指定不合法的秘钥长度,程序将抛出java.security.InvalidParameterException: Wrong keysize: must be equal to 128, 192 or 256异常。
RC2、RC4
RC2和RC4算法也可以用于替换DES算法,特点是秘钥长度较为灵活,RC2和RC4的秘钥长度范围为40到1024位。
JDK8支持RC2和RC4算法:
程序输出如下:
如果指定不合法的秘钥长度,程序将抛出java.security.InvalidParameterException: Key length for RC2 must be between 40 and 1024 bits异常。
RC4读者可以自己玩一玩。
Blowfish
Blowfish算法也可以用于替换DES,Blowfish算法的秘钥长度范围为32到448位,并且必须为8的倍数。
JDK8支持Blowfish算法:
程序运行结果:
如果指定不合法的秘钥长度,程序将抛出java.security.InvalidParameterException: Keysize must be multiple of 8, and can only range from 32 to 448 (inclusive)异常。
PBE
PBE(Password Based Encryption,基于口令加密)算法是一种基于口令的加密算法,特点是没有秘钥的概念,信息交互双方事先拟定好口令即可。单纯的口令很容易通过穷举攻击方式破译,所以PBE也加入了“盐”的概念。
PBE算法是对称加密算法的综合性算法,常见算法如PBEWithMD5AndDES,该算法使用了MD5和DES算法构建PBE算法。JDK8支持PBEWithMD5AndDES、PBEWithMD5AndTripleDES、PBEWithSHA1AndDESede、PBEWithSHA1AndRC2_40、PBEWithSHA1AndRC2_128、PBEWithSHA1AndRC4_40、PBEWithSHA1AndRC4_128、PBEWithHmacSHA1AndAES_128、PBEWithHmacSHA224AndAES_128、PBEWithHmacSHA256AndAES_128、PBEWithHmacSHA384AndAES_128、PBEWithHmacSHA512AndAES_128、PBEWithHmacSHA1AndAES_256、PBEWithHmacSHA224AndAES_256、PBEWithHmacSHA256AndAES_256、PBEWithHmacSHA384AndAES_256和PBEWithHmacSHA512AndAES_256,其中算法名称中包含Hmac的需要配和初始化向量使用。
不需要指定初始化向量的PBE算法族(PBEWithMD5AndDES、PBEWithMD5AndTripleDES、PBEWithSHA1AndDESede、PBEWithSHA1AndRC2_40、PBEWithSHA1AndRC2_128、PBEWithSHA1AndRC4_40、PBEWithSHA1AndRC4_128)例子:
程序输出如下:
算法名称包含Hmac的PBE算法需要指定初始化向量,比如PBEWithHmacSHA1AndAES_128:
通过IvParameterSpec类创建初始化向量,创建初始化向量的秘钥必须为16字节,这里为123456789abcdefg,实例化PBE参数材料的时候通过构造参数传入初始化向量。
程序输出如下:
加密模式
对称加密算法分为:序列密码(流密码)加密,分组密码(块密码)加密两种。流密码是对信息流中的每一个元素(一个字母或一个比特)作为基本的处理单元进行加密,块密码是先对信息流分块,再对每一块分别加密。
上面介绍的这些都属于块密码加密。不同的算法侧重点不同,有的强调效率,有的强调安全,有的强调容错性。根据数据加密时每个加密区块间的关联方式来区分,可以分为4种加密模式:电子密码本模式(Electronic Code Book,ECB)、密文链接模式(Cipher Book Chaining,CBC)、密文反馈模式(Cipher Feed Back,CFB)、输出反馈模式(Output Feed Back,OFB)。AES标准除了推荐上述4种工作模式外,还推荐了一种新的工作模式—计数器模式(Counter,CTR)。这些工作模式可适用于各种分组密码算法。
ECB
ECB模式加解密过程如下图所示:
明文分为若干块,每次加密均产生独立的密文分组,每组的加密结果不会对其他分组产生影响,相同的明文加密后对应产生相同的密文。
- 优点:可并行操作,没有误差传递(因为每个密文都是独立加密来的);
- 缺点:如果明文重复,则对应的密文也会重复,对明文进行主动攻击的可能性较高;
- 用途:适合加密秘钥、随机数等短数据。例如,安全地传递DES秘钥,ECB是最合适的模式。
CBC
CBC模式加解密过程如下图所示:
明文分为若干块,每次加密前,明文块都和前一个明文块加密后的内容进行异或处理,然后再用秘钥加密。因为第一个明文块没有可以用来异或处理的密文块,所以我们需要提供一个初始化向量来替代。
- 优点:密文链接模式加密后的密文上下文关联,对明文的主动攻击的可能性较低;
- 缺点:不能并行加密,如果在加密过程中发生错误,则错误将被无限放大,导致加密失败。并且需要提供初始化向量;
- 用途:可加密任意长度的数据;适用于计算产生检测数据完整性的消息认证码Mac。
CFB
CFB模式加解密过程如下图所示:
明文分为若干块,每次加密前,先将前一个密文块使用秘钥加密,加密结果和当前明文块异或处理得到密文块。同样的,需要为第一个明文块加密提供初始化向量。
- 优点:和CBC类似;
- 缺点:和CBC类似;
- 用途:因错误传播无界,可用于检查发现明文密文的篡改。
OFB
OFB模式加解密过程如下图所示:
过程和CFB类似,区别在于OFB第一次使用秘钥对初始化向量进行加密(结果为A),加密结果和明文块异或处理得到密文块,下一次操作时候,不是使用秘钥加密前一个密文块,而是使用秘钥加密A的结果再和明文块异或处理,得到当前密文块。
- 优点:和CFB类似;
- 缺点:不利于并行计算;对明文的主动攻击是可能的,安全性较CFB差;
- 用途:适用于加密冗余性较大的数据,比如语音和图像数据。
CTR
CTR模式加解密过程如下图所示:
CTR含义是计数器模式,所以它维护了一个递增的计数器。秘钥加密计数器,结果和明文块异或得到密文块,依次类推。
- 优点:可以并行操作,安全性和CBC一样好;
- 缺点:没有错误传播,因此不易确保数据完整性;
- 用途:适用于各种加密应用。
填充模式
当需要按块处理的数据, 数据长度不符合块处理需求时, 按照一定的方法填充满块长的规则。如果不填充,待加密的数据块长度不符合要求时程序会抛出异常。
JDK8中主要支持NoPadding和PKCS5Padding填充模式。
- NoPadding:不填充;
- PKCS5Padding:数据块的大小为8位, 不够就补足。
加密、填充模式实战
在了解了加密模式和填充模式后,我们回头看前面代码中的transformation参数,实例化Cipher对象的时候需要指定transformation转换模式,转换模式主要有两种格式:
- 算法;
- 算法/加密模式/填充模式。
下面就AES算法来实践不同的加密、填充模式。
当转换模式为AES/ECB/PKCS5Padding时:
程序运行解果:
将转换模式改为AES/CBC/PKCS5Padding时,程序输出抛出java.security.InvalidKeyException: Parameters missing异常。
因为该模式需要指定初始化向量,将代码修改为:
程序输出如下:
将转换模式改为AES/CBC/NoPadding时,程序抛出javax.crypto.IllegalBlockSizeException: Input length not multiple of 16 bytes异常。因为rawchen不是16字节的倍数。
更多对称加密算法和加密模式填充模式的组合可以自己尝试下,或者参考下面的算法总结。
手动指定秘钥
在使用对称加密算法加解密的时候,秘钥一般是双方事先约定好的,假如现在通过转换模式为AES/CTR/PKCS5Padding加密得到AES秘钥xaBsoZIBs1Dz6veBfwfzpPwzrmMsu8mKqu4Lljk+zZo=,AES密文VKOtjAOQJQ==,初始化向量秘钥为123456789abcdefg,如何通过秘钥和密文进行解密呢,可以参考下面的代码(即演示如何手动指定秘钥并解密):
输出如下:
算法总结
对上面这些算法进行总结:
算法 | 秘钥长度(位) | 工作模式 | 填充模式 | 初始化向量秘钥长度(字节) |
---|---|---|---|---|
DES | 56 | ECB、CBC、CFB、OFB、CTR等 | NoPadding、 PKCS5Padding、 ISO10126Padding | 8 |
DESede | 112、168 | ECB、CBC、CFB、OFB、CTR等 | NoPadding、 PKCS5Padding、 ISO10126Padding | 8 |
AES | 128、192、256 | ECB、CBC、CFB、OFB、CTR等 | NoPadding、 PKCS5Padding、 ISO10126Padding | 16 |
RC2 | 40~1024 | ECB、CBC、CFB、OFB、CTR等 | NoPadding、 PKCS5Padding、 ISO10126Padding | 8 |
RC4 | 40~1024 | ECB | NoPadding | 无 |
Blowfish | 32~448,8的倍数 | ECB、CBC、CFB、OFB、CTR等 | NoPadding、 PKCS5Padding、 ISO10126Padding | 8 |
PBE | 无 | CBC | PKCS5Padding | 16(带Hmac) |
非对称加密算法
非对称加密和对称加密算法相比,多了一把秘钥,为双秘钥模式,一个公开称为公钥,一个保密称为私钥。遵循公钥加密私钥解密,或者私钥加密公钥解密。非对称加密算法源于DH算法,后又有基于椭圆曲线加密算法的密钥交换算法ECDH,不过目前最为流行的非对称加密算法是RSA,本文简单记录下RSA的使用。
RSA算法
RSA算法是最为典型的非对称加密算法,该算法由美国麻省理工学院(MIT)的Ron Rivest、Adi Shamir和Leonard Adleman三位学者提出,并以这三位学者的姓氏开头字母命名,称为RSA算法。
RSA算法的数据交换过程分为如下几步:
- A构建RSA秘钥对;
- A向B发布公钥;
- A用私钥加密数据发给B;
- B用公钥解密数据;
- B用公钥加密数据发给A;
- A用私钥解密数据。
JDK8支持RSA算法:
算法 | 秘钥长度 | 加密模式 | 填充模式 |
---|---|---|---|
RSA | 512~16384位,64倍数 | ECB | NoPadding PKCS1Padding OAEPWithMD5AndMGF1Padding OAEPWithSHA1AndMGF1Padding OAEPWithSHA-1AndMGF1Padding OAEPWithSHA-224AndMGF1Padding OAEPWithSHA-256AndMGF1Padding OAEPWithSHA-384AndMGF1Padding OAEPWithSHA-512AndMGF1Padding OAEPWithSHA-512/224AndMGF1Padding OAEPWithSHA-512/2256ndMGF1Padding |
代码例子:
程序输出如下:
可以看到,公钥加密私钥解密和私钥加密公钥解密的模式都可行。
公私钥获取
假如现在有RSA公钥:
RSA私钥:
需要将它们还原为PublicKey和PrivateKey对象,可以参考如下代码:
程序输出如下:
分段加解密
RSA加解密中必须考虑到的密钥长度、明文长度和密文长度问题。明文长度需要小于密钥长度,而密文长度则等于密钥长度。因此当加密内容长度大于密钥长度时,有效的RSA加解密就需要对内容进行分段。
这是因为,RSA算法本身要求加密内容也就是明文长度m必须满足0<m<密钥长度n
。如果小于这个长度就需要进行padding,因为如果没有padding,就无法确定解密后内容的真实长度,字符串之类的内容问题还不大,以0作为结束符,但对二进制数据就很难,因为不确定后面的0是内容还是内容结束符。而只要用到padding,那么就要占用实际的明文长度,于是实际明文长度需要减去padding字节长度。我们一般使用的padding标准有NoPPadding、OAEPPadding、PKCS1Padding等,其中PKCS#1建议的padding就占用了11个字节。
以秘钥长度为1024bits为例:
程序会抛出如下异常:
对于1024长度的密钥。128字节(1024bits/8)减去PKCS#1建议的padding就占用了11个字节正好是117字节。所以加密的明文长度120字节大于117字节,程序抛出了异常。
要解决这个问题,可以采用分段加密的手段。编写一个分段加解密的工具类:
测试:
程序输出如下:
建议
- 公钥是通过A发送给B的,其在传递过程中很有可能被截获,也就是说窃听者很有可能获得公钥。如果窃听者获得了公钥,向A发送数据,A是无法辨别消息的真伪的。因此,虽然可以使用公钥对数据加密,但这种方式还是会有存在一定的安全隐患。如果要建立更安全的加密消息传递模型,就需要AB双方构建两套非对称加密算法密钥,仅遵循“私钥加密,公钥解密”的方式进行加密消息传递;
- RSA不适合加密过长的数据,虽然可以通过分段加密手段解决,但过长的数据加解密耗时较长,在响应速度要求较高的情况下慎用。一般推荐使用非对称加密算法传输对称加密秘钥,双方数据加密用对称加密算法加解密。