java加密与解密(一)

一、密码学的发展历史简介

       纵观密码学的发展史,它共经历了三个阶段,分别是手工加密阶段、机械加密阶段和计算机加密阶段。手工加密阶段最为漫长,期间孕育了古典密码,这为后期密码学的发展奠定了基础。机械工业革命发展的同时促进着各种科学技术的进步,密码学也不例外。加之两次世界大战,更加促进了密码学的飞速发展,密码学由此进入现代密码学阶段。尽管如此,在这一阶段的密码学仍旧未能摆脱古典密码学的影子,加密与解密操作均依赖于语言学的支持,转轮密码机Enigma的发明与破解更是将这一特点发挥到了极致。随着数据理论逐步介入,密码学逐渐成为一门学科,而非一门艺术。进入计算机加密阶段后,密码学应用不再局限于军事、政治和外交领域,逐步扩大到商务、金融和社会的其他领域。密码学的研究和应用已大规模扩展到了民用方面。

      密码学主要包含两个分支:密码编码学和密码分析学。密码编码学针对于信息如何隐藏;密码分析学针对于信息如何破译。编码学与分析学相互影响,共同促进密码学的发展。
       古典密码是现代密码的基础,移位和替代是古典密码最常用、最核心的两种加密技巧。由此,古典密码主要分为移位密码和替代密码。例如,凯撒密码就是替代密码的典范。替代密码其分支众多,包含单表替代密码、同音替代密码、多表替代密码和多字母替代密码。移位和替代技巧仍是现代密码学最常用的两种加密手段。

二、现代密码学中的柯克霍夫原则

  1. 即使非数学上不可破解,系统也应在实质(应用)程度上无法破解。
  2. 系统内不应含任何机密物,即使落入敌人手中也不会造成困扰。
  3. 密钥必须易于沟通和记忆,而无需写下,且双方可以很容易地改变密钥。
  4. 系统应可以用于电讯。
  5. 系统应可以携带,不应需要两个人或两个人以上才能使用(应只要一个人就能使用)。
  6. 系统应容易使用,不致让使用者的脑力过分操劳,也无须记得长串的规则。

三、密码体制划分

       从密码体制上划分,现代密码学工分为两种密码体制:堆成密码体制和非对称密码体制。对称与非对称的差别源于加密密钥和解密密钥是否对称,即加密密钥与解密密钥是否相同(对称)。

      在对称密码体制中,加密与解密操作使用相同的密钥,我们把这个密钥称为秘密密钥。DES、AES算法都是常用的对称密码算法。流密码实现简单,对环境要求低,适用于手机平台的加密,广泛应用于军事、外交领域。RC4算法就是典型的流密码算法。流密码的理论、算法受限于国家安全因素未能公布。分组密码在这一点上与流密码恰恰相反,其理论,算法公开,分类众多。DES、AES算法主要的对称密码算法均属于分组密码。分组密码共有5中工作模式:电子密码本模式(ECB)、密文链接模式(CBC)、密文反馈模式(CFB)、输出反馈模式(OFB)、计数器模式(CTR)。分组密码会产生短块,关于短块的处理方法有填充法、流密码加密法、密文挪用技术。
        在非对称密码体制中,加密与解密操作使用不同的密钥。对外公开的密钥,称为公钥;对外保密的密钥,称为私钥。用公钥加密的数据,只能用私钥解密;反之,用私钥加密的数据,只能用公钥解密。RSA算法是常用的非对称密码算法。非对称密码体制同时支持数字签名技术,如RSA、DSA都是常用的数字签名算法。
       散列函数可以有效地确保数据完整性,其是一项消息认证技术。常用的散列函数算法有MD5、SHA、Mac。散列函数也是数字签名技术中最重要的技术环节。数字签名离不开非对称密码体制,其私钥用于签名,公钥用于验证。基于数字签名的不可伪造性,数字签名技术成为5种安全服务中数据完整性服务、认证性服务和抗否认性服务的核心技术。通信双方只有一方提供数字签名的认证方式称为单向认证,通信双方都提供数字签名的认证方式称为双向认证。一般网银系统多采用单向认证方式,而要求较高的网银交易则都采用双向认证方式。
       PKI和PGP是现代网络安全技术领域的两把锁。目前电子商务、电子政务使用PKI技术来确保平台安全性。PGP则多用于电子邮件、文件等的数据签名与加密。

四、电子邮件传输算法——Base64

4.1 Base64算法的由来:

      Base64算法并不是加密算法,仅仅是加密算法的近亲。Base64算法的转换方式很像古典加密算法中的单表置换算法。

     64算法最早应用于解决电子邮件传输的问题。在早期,由于“历史问题”,电子邮件只允许ASCII码字符。如果要传输一封带有非ASCII码字符的电子邮件,当它通过有“历史问题”的网关时就可能出现问题。这个网关很可能会对这个非ASCII码字符的二进制位做调整,即将这个非ASCII码的8位二进制码的最高位置为0。此时用户收到的邮件就会是一封纯粹的乱码邮件了。基于这个原因产生了Base64算法。

4.2 Base64算法定义:

       Base64是一种基于64个字符的编码算法,根据RFC 2045(http://www.ietf.org/rfc/rfc2045.txt)的定义:“Base64内容传送编码是一种以任意8位字节序列组合的描述形式,这种形式不易被人直接识别(The Base64 Content-Transfer-Encoding is designed to represent arbitrary sequences of octets in a form that need not be humanly readable.)”。经过Base64编码后的数据会比原始数据略长,为原来的4/3倍。经Base64编码后的字符串的字符数是以4为单位的整数倍。
       RFC 2045还规定,在电子邮件中,每行为76个字符,每行末需添加一个回车换行符("\r\n"),不论每行是否够76个字符,都要添加一个回车换行符。但在实际应用中,往往根据实际需要忽略了这一要求。
        在RFC 2045文件中给出了表5-1所示的字符映射表:

这张字符映射表中,Value指的是十进制编码,Encoding指的是字符,共映射了64个字符,这也是Base64算法命名的由来。映射表的最后一个字符是等号,它是用来补位的。也难怪有经验的读者朋友一看到字符串末尾有个等号,就会联想到Base64算法了。
其实,Base64算法还有几个同胞兄弟,如Base32和Base16算法。为了能在http请求中以Get方式传递二进制数据,由Base64算法衍生出了Url Base64算法。
       Url Base64算法主要是替换了Base64 字符映射表中的第62和63个字符,也就是将“+”和“/”符号替换成了“-”和“_”符号。但对于补位符“=”,一种建议是使用“~”符号,另一种建议是使用“.”符号。其中,由于“~”符号与文件系统冲突,不建议使用;而对于“.”符号,如果出现连续两次,则认为是错误。对于补位符的问题,Bouncy Castle和Commons Codec有差别:Bouncy Castle使用“.”作为补位符,而Commons Codec则完全杜绝使用补位符。
       有关Base16、Base32和Url Base64算法的详细内容,读者朋友可以参考RFC 4648(http://www.ietf.org/rfc/rfc4648.txt),该文档提交于2006年10月,是一份建议标准(Proposed Standard)。 

4.3 Base64算法与加密算法的关系
       Base64算法有编码和解码操作可充当加密和解密操作,还有一张字符映射表充当了密钥。本书第2章中曾讲到过单表置换算法,Base64算法正是运用了这一思想,将原文经二进制转换后与字符映射表相对应,得到“密文”。Base64算法经常用做一个简单的“加密”来保护某些数据。
       尽管如此,Base64算法仍不能称为加密算法。Base64算法公开,这一点与柯克霍夫原则并无违背,但充当密钥的字符映射表公开,直接违反了柯克霍夫原则,而且Base64算法的加密强度并不够高。因此,不能将Base64算法看做我们所认可的现代加密算法。
       Base64算法虽不能称为加密算法,但其变换法则遵从了单表置换算法。也正因如此,Base64算法成为加密算法学习最好的范例。尤其是在自定义加密算法研制方面,Base64算法是一个很不错的参考,这也是本书将其纳入的缘由之一。如果我们将Base64算法做少许改造,并将字符映射表调整、保密,改造后的Base64算法就具备了加密算法的意义。除此之外,Base64算法常作为密钥、密文和证书的一种通用存储编码格式,与加密算法形影不离。

4.4 实现原理
Base64算法主要是对给定的字符以与字符编码(如ASCII码,UTF-8码)对应的十进制数为基准,做编码操作:
1)将给定的字符串以字符为单位转换为对应的字符编码(如ASCII码)。
2)将获得的字符编码转换为二进制码。
3)对获得的二进制码做分组转换操作,每3个8位二进制码为一组,转换为每4个6位二进制码为一组(不足6位时低位补0)。这是一个分组变化的过程,3个8位二进制码和4个6位二进制码的长度都是24位(3×8 = 4×6 = 24)。
4)对获得的4个6位二进制码补位,向6位二进制码添加2位高位0,组成4个8位二进制码。
5)将获得的4个8位二进制码转换为十进制码。
6)将获得的十进制码转换为Base64字符表中对应的字符。

4.4.1ASCII码字符编码
我们对字符串“A”进行Base64编码,如下所示:
字符 A
ASCII码 65
二进制码  01000001
4-6二进制码 010000 010000
4-8二进制码 00010000    00010000
十进制码  16   16
字符表映射码 Q   Q   =   =

      由此,字符串“A”经过Base64编码[3]后就得到了“QQ==”这样一个字符串。作者有意选择了这样一个字符串作为Base64编码,它经过Base64编码后的字符串末尾带着2个等号。很显然,当原文的二进制码长度不足24位,最终转换为十进制码时也不足4项,这时就需要用等号补位。如果原文只有一个字符,那么经过Base64编码后的字符串末尾会有2个等号。
经Base64编码后的字符串最多只会有2个等号,这是因为:余数 = 原文字节数 MOD 3
       这是一个简单的算术问题,余数的值只可能是0、1或2。余数为0时,则原文字节数恰好是3的倍数,没有等号这个尾巴;余数为1时,则为了让Base64编码后的字符数是4的倍数,要补2个等号;同理,余数为2时,则要补1个等号。所以,通常判别一个字符串是不是Base64编码的第一步操作就是判断这个字符串末尾是不是有等号。
       这同时也说明,Base64编码后的字符串是以4个字符为单位,其长度只能是4个字符的整数倍。本章开篇对“Java加密与解密的艺术”这个字符串做了Base64编码后获得了一个长度为40个字符的字符串“SmF2YeWKoOWvhuS4juino+WvhueahOiJuuacrw==”,正好说明了这一点。

 4.4.2 非ASCII码字符编码
        Base64算法很好地解决了非ASCII码字符的传输问题,譬如中文字符的传输问题。
        ASCII码可以表示十进制范围为0~127的字符,对应二进制范围是0000 0000 ~ 0111 1111。ASCII码包括阿拉伯数字、大小写英文字母和一些控制符,但却没有包含双字节编码的字符,如中文字符。因此有了GB2312、GBK和UTF-8等编码。GB2312、GBK用2个字节表示一个汉字,UTF-8编码则用3个字节表示一个汉字。Base64算法实现时,对6位二进制码添加高2位0,恰恰保护了非ASCII码字符在通过有问题的网关时不发生问题。
       我们以字符串“密”为例,字符串“密”对应的UTF-8编码就是-27,-81,-122。我们对其做如下Base64编码:
       字符串“密”经过Base64编码后得到字符串“5a+G”。
       当然,如果使用GBK编码就不是这个结果了!字符串“密”对应的GBK编码是-61,-36,经过Base64编码后就是字符串“w9w=”了。

4.5 Base64在java中的应用

       Bouncy Castle和Commons Codec这两个jar包都提供了Base64算法实现,但是否遵循RFC 2045定义,即在编码后的字符串末尾是否添加回车换行符,是两种实现的唯一差别!
       Bouncy Castle遵循了一般Base64算法编码。
        Commons Codec提供了Base64算法的两种实现标准:一种是遵循一般Base64算法实现;另一种是遵循RFC 2045定义。此外,Commons Codec还提供了Base64算法的定制实现,可以自定每行字符数和行末符号。同时,Commons Codec还提供了基于Base64算法的输入输出流实现

五、验证数据完整性——消息摘要算法

5.1 消息摘要算法简述
消息摘要算法包含MD、SHA和MAC共3大系列,常用于验证数据的完整性,是数字签名算法的核心算法。

5.1.1 消息摘要算法的由来
      相信大家都有从网上下载软件的经历,偶尔也有从网上下载到破损文件的经历。情况严重时,还可能从某软件的官网上下载到被篡改的软件。如何来验证下载到的文件和官方提供的文件是否一致?这就引入了数据完整性验证的问题。
       该如何验证其一致性?肉眼比较?大小比较?均无可取之处!我们需要一种方便快捷、安全有效的算法。
       先不说如何比较文件是否相同的问题,我们说说如何比较两个对象是否相同。
      相信大家都有使用equals()方法来比较对象的经历。但很多人不知道,实际上equals()方法比较的是两个对象的散列值,即比较两个对象hashCode ()方法的值是否相同,这说明hashCode可以作为辨别对象的唯一标识。
      什么是hashCode呢?顾名思义,hashCode就是散列值。任何消息经过散列函数处理后,都会获得唯一的散列值,这一过程称为“消息摘要”,其散列值称为“数字指纹”,其算法自然就是“消息摘要算法”了。换句话说,如果其数字指纹唯一,就说明其消息是一致的。
       由此,消息摘要算法成了校验数据完整性的主要手段。各大软件厂商提供软件下载的同时总要附带数字签名,这一做法也就不足为奇了。为了能够更加方便、有效地验证数据的完整性,有的软件厂商还提供了不同的消息摘要算法的数字指纹,如MD5和SHA算法,甚至是HMAC算法的数字指纹。此外,用于校验数据完整性的算法还有CRC32算法等。为了方便人们识别和阅读,数字指纹常以十六进制字符串的形式出现。
       消息摘要算法最初是用来构建数字签名的。数字签名操作中,签名操作其实是变相地使用消息摘要算法获得的数字指纹,而验证操作则是验证其数字指纹是否相符。这也是为什么当山东大学王小云教授使用碰撞算法破解了MD5和SHA算法后,使得数字签名在理论上被伪造成为可能。
      消息摘要算法一直是非对称加密算法中一项举足轻重的关键性算法。

5.1.2 消息摘要算法的家谱
       消息摘要算法又称散列算法,其核心在于散列函数的单向性。即通过散列函数可获得对应的散列值,但不可通过该散列值反推其原始信息。这是消息摘要算法的安全性的根本所在。
        消息摘要算法主要分为三大类:MD(Message Digest,消息摘要算法)、SHA-1(Secure Hash Algorithm,安全散列算法)和Hmacmd5(Message Authentication Code,消息认证码算法)。
        如前文所述,MD5、SHA和HMAC都属于消息摘要算法,它们是三大消息摘要算法的主要代表。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算法。
         我们知道,科学技术的发展是不以人的意志为转移的。尽管上述内容列举了各种消息摘要算法,但仍不能满足应用需要。基于这些消息摘要算法,又衍生出了RipeMD系列(包含RipeMD128、RipeMD160、RipeMD256、RipeMD320)、Tiger、GOST3411和Whirlpool算法。

5.2 MD算法家族
       每当人们一提到消息摘要算法,就很自然地会联想到MD5和SHA算法。MD5算法是典型的消息摘要算法,是计算机广泛使用的杂凑算法之一(又译摘要算法、散列算法),更是消息摘要算法的首要代表。

5.2.1 简述
         MD5算法是典型的消息摘要算法,其前身有MD2、MD3和MD4算法,它由MD4、MD3、MD2算法改进而来。不论是哪一种MD算法,它们都需要获得一个随机长度的信息并产生一个128位的信息摘要。如果将这个128位的二进制摘要信息换算成十六进制,可以得到一个32位(每4位二进制数转换为1位十六进制数)的字符串,故我们见到的大部分MD5算法的数字指纹都是32位十六进制的字符串,如本章开篇中,MySQL下载页上的数字指纹(MD5: 5a077abefee447cbb271e2aa7f6d5a47)就是32位的十六进制字符串。现在,各大主流计算机语言均支持MD5算法。
      虽然,MD5算法漏洞越来越多,已不再安全,但至今我们仍没有看到它的下一版本——MD6算法的出现。或许,同样基于MD4算法改进而来的SHA算法将会是MD系列算法的主要替代者。
让我们一同回顾一下MD算法家族的发展历史。
1. MD2算法
       1989年,著名的非对称算法RSA发明人之一——麻省理工学院教授罗纳德·李维斯特(Ronald L. Rivest)开发了MD2算法。这个算法首先对信息进行数据补位,使信息的字节长度是16的倍数。再以一个16位的检验和作为补充信息追加到原信息的末尾。最后根据这个新产生的信息计算出一个128位的散列值,MD2算法由此诞生。
       有关MD2算法详情请参见RFC 1319(http://www.ietf.org/rfc/rfc1319.txt)。
2. MD4算法
        1990年,罗纳德·李维斯特教授开发出较之MD2算法有着更高安全性的MD4算法。在这个算法中,我们仍需对信息进行数据补位。不同的是,这种补位使其信息的字节长度加上448个字节后能成为512的倍数(信息字节长度mod 512 = 448)。此外,关于MD4算法的处理与MD2又有很大差别。但最终仍旧是会获得一个128位的散列值。MD4算法对后续消息摘要算法起到了推动作用,许多比较有名的消息摘要算法都是在MD4算法的基础上发展而来的,如MD5、SHA-1、RIPE-MD和HAVAL算法等。
       有关MD4算法的详情请参见RFC 1320(http://www.ietf.org/rfc/rfc1320.txt)。
       著名开源P2P(Peer-To-Peer,点对点)下载软件EMule(http://www.emule.com)所使用的消息摘要算法正是经过改良后的MD4算法。该算法用于对文件分块后做消息摘要,以验证其文件的完整性。
3. MD5算法
        1991年,继MD4算法后,罗纳德·李维斯特教授开发了MD5算法,将MD算法推向成熟。MD5算法经MD2、MD3和MD4算法发展而来,算法复杂程度和安全强度大大提高。但不管是MD2、MD4还是MD5算法,其算法的最终结果均是产生一个128位的消息摘要,这也是MD系列算法的特点。MD5算法执行效率略次于MD4算法,但在安全性方面,MD5算法更胜一筹。随着计算机技术的发展和计算水平的不断提高,MD5算法暴露出来的漏洞也越来越多。MD5算法已不再适合安全要求较高的场合使用。
有关MD5算法的详情请参见RFC 1321(http://www.ietf.org/rfc/rfc1321.txt),其中包含了MD2、MD4和MD5三种算法的C语言版实现。

5.3 SHA算法家族
        SHA算法是基于MD4算法实现的,作为MD算法的继任者,成为了新一代的消息摘要算法的代表。SHA与MD算法不同之处主要在于摘要长度,SHA算法的摘要更长,安全性更高。

5.3.1 简述
       SHA(Secure Hash Algorithm,安全散列算法)是消息摘要算法的一种,被广泛认可为MD5算法的继任者。它是由美国国家安全局(NSA,National Security Agency)设计,经美国国家标准与技术研究院(NIST,National Institute of Standards and Technology)发布的一系列密码散列函数。SHA算法家族目前共有SHA-1、SHA-224、SHA-256、SHA-384和SHA-512五种算法,通常将后四种算法并称为SHA-2算法。除上述五种算法外,还有发布不久就夭折的SHA-0算法。
        SHA算法是在MD4算法的基础上演进而来的,通过SHA算法同样能够获得一个固定长度的摘要信息。与MD系列算法不同的是:若输入的消息不同,则与其相对应的摘要信息的差异概率很高。SHA算法是FIPS所认证的五种安全杂凑算法。
       这些算法中的“安全”字眼是基于以下两点(根据官方标准的描述):
❑由消息摘要反推原输入讯息,从计算理论上来说是很困难的。
❑想要找到两组不同的消息对应到相同的消息摘要,从计算理论上来说也是很困难的。任何对输入消息的变动,都有很高的概率导致其产生的消息摘要迥异。
随着时间的推移,安全算法已不再安全。继山东大学王小云教授顺利破解MD5算法后,SHA-1算法也难逃此劫,终被王小云教授破解。两大著名消息摘要算法被破解,预示着数字签名在理论上可被伪造,B2B和B2C系统将存在安全隐患。
让我们简要回顾一下SHA算法的发展历史:
(1)SHA-0算法
       1993年,NIST公布了SHA算法家族的第一个版本,FIPS PUB 180。为避免混淆,现在我们称之为SHA-0算法。但SHA-0算法在公布不久后就被NSA撤回,原因是NSA发现SHA-0算法中含有会降低密码安全性的错误。由此,SHA-0算法还未正式推广就已夭折。
(2)SHA-1算法
        1995年,继SHA-0算法夭折后,NIST发布了FIPS PUB 180的修订版本 FIPS PUB 180-1,用于取代FIPS PUB 180,称为SHA-1算法,通常我们也把SHA1算法简称为SHA算法。SHA-1算法在许多安全协定中广为使用,包括TLS/SSL、PGP、SSH、S/MIME 和IPsec,曾被视为是 MD5算法的后继者。SHA-0和SHA-1算法可对最大长度为264的字节信息做摘要处理,得到一个160位的摘要信息,其设计原理相似于MD4和MD5算法。如果将得到160位的摘要信息换算成十六进制,可以得到一个40位(每4位二进制数转换为1位十六进制数)的字符串。
有关SHA-1算法详情请参见RFC 3174(http://www.ietf.org/rfc/rfc3174.txt)。
我们常使用的数字证书就有SHA-1算法的影子,如图6-4所示。图中指纹值正是一个40位的十六进制字符串(51ee11819f269a961671a5bd77cba3f0815103c8)。

(3)SHA-2算法
       SHA算法家族除了其代表SHA-1算法以外,还有SHA-224、SHA-256、SHA-384和SHA-512四种SHA算法的变体,以其摘要信息字节长度命名,通常将这组算法并称为SHA-2算法。摘要信息字节长度的差异是SHA-2和SHA-1算法的最大差异。
       2001年,在FIPS PUB 180-2草稿中包含了SHA-256、SHA-384和SHA-512算法,随即通过了审查和评论,于2002年以官方标准发布。
       2004年2月,在FIPS PUB 180-2变更通知中加入了一个额外的变种“SHA-224”,这是为了符合双金钥3DES(三重DES算法)所需的金钥长度而定义的。

5.4 MAC算法家族
MAC算法结合了MD5和SHA算法的优势,并加入密钥的支持,是一种更为安全的消息摘要算法。

5.4.1 简述
       MAC(Message Authentication Code,消息认证码算法)是含有密钥散列函数算法,兼容了MD和SHA算法的特性,并在此基础上加入了密钥。因为MAC算法融合了密钥散列函数(keyed-Hash),通常我们也把MAC称为HMAC(keyed-Hash Message Authentication Code)。MAC算法和HMAC算法基本上可等同对待。
       MAC算法主要集合了MD和SHA两大系列消息摘要算法。MD系列算法有HmacMD2、HmacMD4和HmacMD5三种算法;SHA系列算法有HmacSHA1、HmacSHA224、HmacSHA256、HmacSHA384和HmacSHA512五种算法。
        经MAC算法得到的摘要值也可以使用十六进制编码表示,其摘要值长度与参与实现的算法摘要值长度相同。例如,HmacSHA1算法得到的摘要长度就是SHA1算法得到的摘要长度,都是160位二进制数,换算成十六进制编码为40位。
       有关HMAC算法详情请参见RFC 2104(http://www.ietf.org/rfc/rfc2104.txt),其中包含了HmacMD5算法的C语言版实现。
        基于SSH(Secure Shell,安全外壳)协议的一些软件,也使用了AES算法。图6-6中展示了SecureCRT软件中如何配置加密算法。

 

                                                                  图为SecureCRT配置界面中的加密算法

5.5 其他消息摘要算法
       除了MD、SHA和MAC这三大主流消息摘要算法外,还有许多我们不了解的消息摘要算法,包括RipeMD系列(包含RipeMD128、RipeMD160、RipeMD256和RipeMD320)、Tiger、Whirlpool和GOST3411算法。
       RipeMD系列算法与MAC系列算法相结合,又产生了HmacRipeMD128和HmacRipeMD160两种算法。
       作为了解,我们在这里做简要介绍。也许有一天,我们会需要这些非主流消息摘要算法。

Bouncy Castle包实现了这些非主流信息摘要算法。

六、初等数据加密-------对称加密算法

       我们都有使用密码保护私密信息的经历,甚至可以说是习惯。我们往往不希望无关的人窥探我们的隐私,从孩童时代就知道用“密码日记本”记录自己的一些隐私。密码日记本无非是一个带锁的日记本。不管是读日记还是写日记都离不开这个密码。
       上述情形就好比我们应用黑匣子,需要读写操作,需要同一套密钥。写操作伴随加密,读操作伴随解密。加密和解密操作使用同一套密钥,这就是对称加密算法的核心。
 6.1 对称加密算法简述
       对称加密算法是当今应用范围最广,使用频率最高的加密算法。它不仅应用于软件行业,在硬件行业同样流行。各种基础设施但凡涉及安全需求,都会优先考虑对称加密算法。    

 6.1.1 对称加密算法的由来
      对于大多数对称加密算法而言,解密算法是加密算法的逆运算,加密密钥和解密密钥相同。如果我们把Base64算法改良,将其字符映射表作为密钥保存,就可以把这个改良后的Base64算法作为一种对称加密算法来看。当然,加密算法这样改良后的安全强度还远远不够,但足以让我们认识对称加密算法的特点。
      对称加密算法易于理解,便于实现,根据加密方式又分为密码和分组密码,其分组密码工作模式又可分为ECB、CBC、CFB、OFB和CTR等,密钥长度决定了加密算法的安全性。有关对称加密算法相关理论知识,请阅读第2章相关内容。
       对称加密算法发展至今已相当完备。以DES算法为例,由于密钥长度的不满足,衍生出了DESede算法(又称TripleDES或3DES算法,翻译成中文是“三重DES”算法)。为了替代DES算法又有了AES(Rijndael)算法。此外,还有RC系列算法,包含RC2、RC4以及针对32位/64位计算机设计的RC5算法(细分为RC5-32和RC5-64,分别对应32位和64位计算机)。

       除了上述算法,我们还常常会用到Blowfish、Twofish、Serpent、IDEA和PBE等对称加密算法。 

6.1.2 对称加密算法的家谱
        目前已知的可通过Java语言实现的对称加密算法大约有20多种,Java 7仅提供了部分算法实现,如DES、DESede、AES、Blowfish,以及RC2和RC4算法等。其他算法(如IDEA算法)需要通过第三方加密软件包Bouncy Castle提供实现。
        在对称加密算法中,DES算法最具有代表性,堪称典范;DESede是DES算法的变种;AES算法则作为DES算法的替代者;而IDEA算法作为一种强加密算法,成为电子邮件加密软件PGP(Pretty Good Privacy)的核心算法之一。
       在Java实现层面上,DES、DESede、AES和IDEA这4种算法略有不同。
       DES和DESede算法在使用密钥材料还原密钥时,建议使用各自相应的密钥材料实现类(DES算法对应DESKeySpec类,DESede算法对应DESedeKeySpec类)完成相应转换操作。
       AES算法在使用密钥材料还原密钥时,则需要使用一般密钥材料实现类(SecretKeySpec类)完成相应转换操作。其他对称加密算法可参照该方式 实现,如RC2、RC4、Blowfish以及IDEA等算法均可参照AES算法实现方式做相应实现。
       IDEA算法实现Java 7未能提供,需要依赖第三方加密组件包Bouncy Castle提供支持,其他由Bouncy Castle提供支持的对称加密算法可参照该算法实现方式做相应实现。

6.2 数据加密标准——DES
        DES算法和DESede算法统称DES系列算法。DES算法是对称加密算法领域中的典型算法,为后续对称加密算法的发展奠定了坚实的基础。DESede算法基于DES算法进行三重迭代,增加了算法安全性。经过一番筛选,Rijndeal算法最终成为了AES算法。这期间,对称加密算法发展迅速,与Rijndeal算法竞争的算法包括Blowfish、Serpent等。IDEA算法也源于增加算法的安全性替代DES算法,诸多对称加密算法的发展均源于DES算法的研究而来。

6.2.1 简述
       1973年,美国国家标准局(National Bureau of Standards,NBS,即现在的美国国家标准技术研究所(National Institute of Standards and Technology,NIST))征求国家密码标准方案,IBM公司提交了自己研制的算法(Luciffer算法,于1971年末提出)。1977年7月15日,该算法被正式采纳,作为美国联邦信息处理标准生效,并很快应用到国际商用数据加密领域,成为事实标准,即数据加密标准(Data Encryption Standard,DES),DES算法由此诞生。
        DES算法作为现代密码学领域中第一个官方授权的加密算法受到全球各大密码学机构的关注。DES算法密钥偏短,仅有56位,迭代次数偏少,受到诸如查分密码分析和线性密码分析等各种攻击威胁,安全性受到严重威胁。不仅如此,由于DES算法具有半公开性质,被怀疑存在美国国家安全局(National Security Agency,NSA)安置的后门,受到各大密码学机构的强烈质疑。
1998年后,实用化DES算法破译机的出现彻底宣告DES算法已不具备安全性。1999年NIST颁布新标准,规定DES算法只能用于遗留加密系统,但不限制使用DESede算法。以当今计算机技术能力,经DES算法加密的数据在24小时内可能被破解。由此,DES算法正式退出历史舞台,AES算法成为它的替代者。
        即便如此,DES算法对于密码学领域的贡献确实是巨大的。各种对称加密算法均由研究DES算法发展而来,对后续对称加密算法的发展起到奠基作用。DES算法实现不仅遍布软件行业,甚至很多硬件芯片本身也具备DES加密实现。同时,作为一款较易实现的加密算法,DES算法也成为最应学习的对称加密算法,其地位堪比C语言在计算机语言中的地位。
       基于CBC工作模式的DES算法相关文档可参考RFC 1829(http://www.ietf.org/rfc/rfc1829.txt)。
       历经20年发展,DES算法不仅应用在软件行业,成为电子商务必不可少的加密算法,同时也逐步渗透到硬件行业。芯片级DES算法生产工艺相当完备,完全可以支持底层加密需求。

6.2.2 实现
我们知道,密钥长度与安全性成正比,但Java 7仅支持56位密钥长度,作为补充,Bouncy Castle提供64位密钥长度支持。在此基础上配合不同的填充方式(如PKCS5Padding,PKCS7Padding),可显著提高加密系统的安全性。
有关DES算法的Java 7实现与Bouncy Castle实现细节如表所示。

                                                                                         图表为DES算法 

其他对称加密算法与该算法实现相类似。
密钥的构建主要需要密钥生成器(KeyGenerator)完成生成操作,如下代码所示:

// 实例化密钥生成器
KeyGenerator kg = KeyGenerator.getInstance("DES");
// 初始化
kg.init(56);
// 生成秘密密钥
SecretKey secretKey = kg.generateKey();
// 获得密钥的二进制编码形式
byte[] b = secretKey.getEncoded();

 字节数组b就是我们需要的秘密密钥字节数组形式。这便于我们将其存储在文件中,或以数据流的形式在网络中传输。
把密钥转化为二进制字节数组形式便于保存,但若我们要使用它需要将其转换为密钥对象,首先需要将二进制密钥转换为密钥材料对象(这里是DESKeySpec对象dks),再使用密钥工厂(SecretKeyFactory)生成密钥。实现代码如下:

// 实例化DES密钥材料
DESKeySpec dks = new DESKeySpec(b);
// 实例化秘密密钥工厂
SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("DES");
// 生成秘密密钥
SecretKey secretKey = keyFactory.generateSecret(dks);

 对于DESede算法,则需要相应的DESedeKeySpec类替换DESKeySpec类来完成上述操作。
得到密钥对象后,我们就可以对数据做加密/解密处理,加密处理代码如下:

//实例化
Cipher cipher = Cipher.getInstance("DES");
// 初始化,设置为加密模式
cipher.init(Cipher.ENCRYPT_MODE, secretKey);
// 执行操作
byte[] data = cipher.doFinal(data);

 如果将上述初始化方法(init()方法)中的模式参数由“Cipher.ENCRYPT_MODE”改为“Cipher.DECRYPT_MODE”,则可作为解密处理。
Java 7提供了DES算法支持,但仅支持56位的密钥长度。我们知道密钥长度与加密强度成正比。我们可以使用Boucy Calstle提供密钥长度,由56位提高至64位。
接下来我们完成一套基于DES算法的密钥构建和加密/解密操作

import java.security.Key;

import javax.crypto.Cipher;

import javax.crypto.KeyGenerator;

import javax.crypto.SecretKey;

import javax.crypto.SecretKeyFactory;

import javax.crypto.spec.DESKeySpec;

/**

*DES安全编码组件

*/

public abstract class DESCoder{

/**

*密钥算法<br>

*Java 6只支持56位密钥<br>

*Bouncy Castle支持64位密钥

*/

public static final String KEY_ALGORITHM="DES";

/**

*加密/解密算法/工作模式/填充方式

*/

public static final String CIPHER_ALGORITHM="DES/ECB/PKCS5Padding";

/**

*转换密钥

*@param key二进制密钥

*@return Key密钥

*@throws Exception

*/

private static Key toKey(byte[]key)throws Exception{

//实例化DES密钥材料

DESKeySpec dks=new DESKeySpec(key);

//实例化秘密密钥工厂

SecretKeyFactory keyFactory=SecretKeyFactory.getInstance(KEY_ALGORITHM);

//生成秘密密钥

SecretKey secretKey=keyFactory.generateSecret(dks);

return secretKey;

}

/**

*解密

*@param data待解密数据

*@param key密钥

*@return byte[]解密数据
*@throws Exception

*/

public static byte[]decrypt(byte[]data, byte[]key)throws Exception{

//还原密钥

Key k=toKey(key);

//实例化

Cipher cipher=Cipher.getInstance(CIPHER_ALGORITHM);

//初始化,设置为解密模式

cipher.init(Cipher.DECRYPT_MODE, k);

//执行操作

return cipher.doFinal(data);

}

/**

*加密

*@param data待加密数据

*@param key密钥

*@return byte[]加密数据

*@throws Exception

*/

public static byte[]encrypt(byte[]data, byte[]key)throws Exception{

//还原密钥

Key k=toKey(key);

//实例化

Cipher cipher=Cipher.getInstance(CIPHER_ALGORITHM);

//初始化,设置为加密模式

cipher.init(Cipher.ENCRYPT_MODE, k);

//执行操作

return cipher.doFinal(data);

}

/**

*生成密钥<br>

*Java 6只支持56位密钥<br>

*Bouncy Castle支持64位密钥<br>

*@return byte[]二进制密钥

*@throws Exception

*/

public static byte[]initKey()throws Exception{

/*

*实例化密钥生成器

*若要使用64位密钥注意替换

*将下述代码中的

*KeyGenerator.getInstance(CIPHER_ALGORITHM);

*替换为

*KeyGenerator.getInstance(CIPHER_ALGORITHM,"BC");

*/

KeyGenerator kg=KeyGenerator.getInstance(KEY_ALGORITHM);

/*

*初始化密钥生成器

*若要使用64位密钥注意替换

*将下述代码kg.init(56);

*替换为kg.init(64);

*/

kg.init(56);

//生成秘密密钥

SecretKey secretKey=kg.generateKey();

//获得密钥的二进制编码形式

return secretKey.getEncoded();

}

}

 如果我们初始化密钥生成器时按如下方式实现,将获得一个默认长度的密钥:


kg.init(new SecureRandom());


Java 7仅仅提供了56位长度的密钥,因此上述方法将产生一个56位长度的密钥。

若我们想要构建一个64位密钥的DES算法,则需要按如下代码做替换实现:


KeyGenerator kg=KeyGenerator.getInstance(CIPHER_ALGORITHM,"BC");


在上述代码中,“BC”是Boucy Calstle安全提供者的缩写。

当然,你也可以使用如下方式替代上述代码:


import java.security.Security;

import org.bouncycastle.jce.provider.BouncyCastleProvider;

//省略

//加入BouncyCastleProvider支持

Security.addProvider(new BouncyCastleProvider());

KeyGenerator kg=KeyGenerator.getInstance(CIPHER_ALGORITHM);


完成密钥生成器实例化操作后,需要注意密钥生成器初始化操作,如以下代码所示:


kg.init(64);


按上述代码实现方式,我们即可获得Boucy Calstle安全提供者提供的64位的DES算法密钥。

注意 密钥生成和加密/解密所使用的算法很可能是不同的。

6.3 三重DES——DESede
       作为DES算法的一种改良,DESede算法针对其密钥长度偏短和迭代次数偏少等问题做了相应改进,提高了安全强度。但这仍不是终点,DESede算法的出现仅为DES算法的改良提供了一种参考。DESede算法处理速度较慢,密钥计算时间较长,加密效率不高等问题使得对称加密算法的发展仍不容乐观。

6.3.1 简述
       DES算法被广大密码学机构质疑的原因主要在于DES算法的半公开性,这违反了柯克霍夫原则,各大密码学结构怀疑美国国家安全局在未公开的算法实现内安置后门。
       DES算法有3点安全隐患:密钥太短、迭代偏少和半公开性。这使得淘汰DES算法成为一种必然,但要淘汰DES算法必须找到合适的替代方案。
        针对密钥太短和迭代偏少问题,有人提出了多重DES的方式来克服这些缺陷。比较典型的有两重DES(2DES)、三重DES(3DES)和四重DES(4DES)等几种形式,但在实际应用中一般采用3DES方案,它还有两个别名Triple DES和DESede。在Java中,我们通常称其为DESede算法。当然,其他两种名称在使用时同样可以获得支持。
        DESede算法将密钥长度增至112位或168位,抗穷举攻击的的能力显著增强,但核心仍是DES算法,虽然通过增加迭代次数提高了安全性,但同时也造成处理速度较慢、密钥计算时间加长、加密效率不高的问题。

6.3.2 实现
        对DES算法实现有一定了解后,DESede算法的实现就容易许多。除了将密钥材料实现类由DESKeySpec类转换为DESedeKeySpec类外,DESede算法与DES算法实现的主要差别在于算法、密钥长度两个方面:
❑算法:自然不用说,这是基本差别,只是DESede还有很多别名,如TripleDES和3DES指的都是DESede算法。
❑密钥长度:Java 7提供的DES算法实现支持56位密钥长度,加上Bouncy Castle相应实现可以支持到64位密钥长度。Java 7提供的DESede算法实现所支持的密钥长度支持为112位和168位,通过Bouncy Castle相应实现可支持密钥长度为128位和192位。DESede算法密钥长度恰恰是DES算法密钥长度的2倍或3倍。
有关DESede算法的Java 7实现与Bouncy Castle实现细节如表所示

                                                                                      DESede算法

对于DESede算法的填充方式,Java 7提供了NoPadding、PKCS5Padding和ISO10126 Padding共3种填充方式。

 Bouncy Castle不仅支持PKCS7Padding这一种填充方式,还支持ISO10126d2Padding、X932Padding、ISO7816d4Padding和ZeroBytePadding共4种填充方式。

对于如何使用PKCS7Padding填充方式完成DESede算法构建的问题,我们在代码清单6-3中详述。
代码清单6-3 DESede算法实现

import java.security.Key;
import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.DESedeKeySpec;
/**
 * DESede安全编码组件
 */
public abstract class DESedeCoder {
  /**
  * 密钥算法
  * Java 7支持密钥长度为112位和168位
  * Bouncy Castle支持密钥长度为128位和192位
*/
  public static final String KEY_ALGORITHM = "DESede";
  /**
  * 加密/解密算法 / 工作模式 / 填充方式
  * Java 7支持PKCS5Padding填充方式
  * Bouncy Castle支持PKCS7Padding填充方式
  */
  public static final String CIPHER_ALGORITHM = "DESede/ECB/PKCS5Padding";
  /**
  * 转换密钥
  * @param key 二进制密钥
  * @return Key 密钥
  * @throws Exception
  */
  private static Key toKey(byte[] key) throws Exception {
      // 实例化DES密钥材料
      DESedeKeySpec dks = new DESedeKeySpec(key);
      // 实例化秘密密钥工厂
      SecretKeyFactory keyFactory = SecretKeyFactory.getInstance(KEY_ALGORITHM);
      // 生成秘密密钥
      return keyFactory.generateSecret(dks);
  }
  /**
  * 解密
  * @param data 待解密数据
 @param key 密钥
  * @return byte[] 解密数据
  * @throws Exception
  */
  public static byte[] decrypt(byte[] data, byte[] key) throws Exception {
      // 还原密钥
      Key k = toKey(key);
      /* 
      * 实例化
      * 使用PKCS7Padding填充方式,按如下代码实现
      * Cipher.getInstance(CIPHER_ALGORITHM, "BC");
      */
      Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM);
      // 初始化,设置为解密模式
      cipher.init(Cipher.DECRYPT_MODE, k);
      // 执行操作
      return cipher.doFinal(data);
  }
  /**
  * 加密
  * @param data 待加密数据
  * @param key 密钥
  * @return byte[] 加密数据
  * @throws Exception
  */
  public static byte[] encrypt(byte[] data, byte[] key) throws Exception {
// 还原密钥
      Key k = toKey(key);
      /* 
      * 实例化
      * 使用PKCS7Padding填充方式,按如下代码实现
      * Cipher.getInstance(CIPHER_ALGORITHM, "BC");
      */
      Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM);
      // 初始化,设置为加密模式
      cipher.init(Cipher.ENCRYPT_MODE, k);
      // 执行操作
      return cipher.doFinal(data);
  }
  /**
  * 生成密钥 <br>
  * @return byte[] 二进制密钥
  * @throws Exception
  */
  public static byte[] initKey() throws Exception {
      /*
      * 实例化
      * 使用128位或192位长度密钥,按如下代码实现
      * KeyGenerator.getInstance(KEY_ALGORITHM, "BC");
      */
      KeyGenerator kg = KeyGenerator.getInstance(KEY_ALGORITHM);
      /*
* 初始化
      * Java 7支持密钥长度为 112位和168位
      * 若使用128位或192位长度密钥,按如下代码实现
      * kg.init(128);
      * 或
      * kg.init(192);
      */
      kg.init(168);
      // 生成秘密密钥
      SecretKey secretKey = kg.generateKey();
      // 获得密钥的二进制编码形式
      return secretKey.getEncoded();
  }
}

       上述代码与DES算法实现如出一辙。这要感谢Sun提供的JCE架构,它提供了统一的加密算法调用模式。我们注意到,这里的密钥材料实现类由DESKeySpec类改为DESedeKeySpec类,这是为DESede算法量身定做的密钥材料实现类。
除了密钥材料实现类的变化,还要注意密钥长度的区别。
     Java 7支持112位和168位密钥长度,注意初始化时显式调用如下代码:
      kg.init(168);
      DESede算法密钥生成器的密钥长度初始化默认值为168位,若使用无参初始化方法,一样会产生一个168位的DESede算法密钥。
      如果要使用128位或192位长度的密钥,需要在实例化密钥生成器对象时就指定Bouncy Castle作为该算法的提供者。我们以 初始化192位长度密钥为例,按如下代码实现:
KeyGenerator kg = KeyGenerator.getInstance(KEY_ALGORITHM, "BC");
kg.init(192);
上述密钥长度初始化实现与DES算法实现无差别,这里要注意的是填充方式扩展。
注意代码中的变量“CIPHER_ALGORITHM”,当前指定的填充方式是PKCS5Padding,若我们使用PKCS7Padding填充方式除了对该变量做调整外,还需要调整Cipher对象cipher实例化代码,按如下方式实现:
Cipher.getInstance(CIPHER_ALGORITHM, "BC");
这样我们就能获得相应填充方式下的加密/解密实现了。
关于DESede算法相应的测试用例与DES算法的测试用例基本没有差别,如代码清单6-4所示。
代码清单6-4 DESede算法实现测试用例

import static org.junit.Assert.*;
import org.apache.commons.codec.binary.Base64;
import org.junit.Test;
/**
 * DESede安全编码组件校验
 * @version 1.0
 */
public class DESedeCoderTest {
  /**
  * 测试
* @throws Exception
  */
  @Test
  public final void test() throws Exception {
      String inputStr = "DESede";
      byte[] inputData = inputStr.getBytes();
      System.err.println("原文:\t" + inputStr);
      // 初始化密钥
      byte[] key = DESedeCoder.initKey();
      System.err.println("密钥:\t" + Base64.encodeBase64String(key));
      // 加密
      inputData = DESedeCoder.encrypt(inputData, key);
      System.err.println("加密后:\t" + Base64.encodeBase64String(inputData));
      // 解密
      byte[] outputData = DESedeCoder.decrypt(inputData, key);
      String outputStr = new String(outputData);
      System.err.println("解密后:\t" + outputStr);
      // 校验
      assertEquals(inputStr, outputStr);
  }
}

 我们可以从控制台中获得信息中明显感受到密钥长度的增加。控制台输出信息如下:
原文:    DESede
密钥:  N8jTp6RuZxkjJea2XVvquV5YegHQ31cV
加密后:  touXuJw8vrc=

解密后:  DESede

      仔细研究过Java API的也许会对上述代码中的密钥材料实现类的变化敏感一些,Java API中仅提供了DES、DESede和PBE共3种对称加密算法密钥材料实现类。那么,其他算法如何还原密钥呢?AES算法实现就是一个不错的示例!

6.4 高级数据加密标准——AES
      DES算法漏洞的发现加速了对称加密算法的改进,通过对DES算法的简单改造得到的DESede算法虽然在一定程度上提升了算法安全强度。但DESede算法低效的加密实现和较慢的处理速度仍不能满足我们对安全的要求。AES算法正是基于这些缘由而诞生。

6.4.1 简述
       1997年,NIST发起了征集DES替代算法——AES(Advanced Encryption Standard,高级数据加密标准)算法的活动。1997年9月12日,NIST发布了征集算法的正式公告和具体细节,要求AES算法要比DESede算法快,至少与DESede算法一样安全,具有128位的分组长度,支持128位、192位和256位的密钥长度,同时要求AES要能够在世界范围内免费使用。
       1998年8月20日,NIST在“第一次AES候选大会”上公布了满足条件的15个AES的候选算法,继而又从中筛选出5个候选算法,包括MARS、RC6、Rijndael、Serpent和Twofish。
       2000年10月2日,由Daemen和Rijmen两位比利时人提出的Rijndael算法,以其密钥设置快、存储要求低,在硬件实现和限制存储的条件下性能优异当选AES算法。
       经过验证,目前采用的AES算法能够有效抵御已知的针对DES算法的所有攻击方法,如部分差分攻击、相关密钥攻击等。至今,还没有AES破译的官方报道。
        AES算法因密钥建立时间短、灵敏性好、内存需求低等优点,在各个领域得到广泛的研究与应用。
目前,AES常用于UMTS(Universal Mobile Telecommunications System,通用移动通信系统)。基于SSH(Secure Shell,安全外壳)协议的一些软件也使用了AES算法。下图展示了SecureCRT软件如何配置加密算法。

 除了常用的SSH软件外,在一些无线路由器中也使用AES算法构建加密协议。

6.4.2 实现
          AES算法成为DES算法的替代者,其实现也成为其他对称加密算法实现的参考模型。无论是Java 7所支持的对称加密算法(如RC2、RC4和Blowfish等算法),还是通过第三方加密组件提供的对称加密算法(如Bouncy Castle提供的IDEA算法实现),都可以通过对AES算法实现做少许改动完成相应实现。本节以AES算法实现为例,读者也可参照实现其他算法,如RC2、RC4和Blowfish等算法。
有关AES算法的Java 7实现细节如表所示。

AES算法实现基于DES算法实现做了修改,具体实现如代码清单6-5所示。
代码清单6-5 AES算法实现 

import java.security.Key;
import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
/**
 * AES安全编码组件
 */
public abstract class AESCoder {
  /**
  * 密钥算法
  */
  public static final String KEY_ALGORITHM = "AES";
  /**
  * 加密/解密算法 / 工作模式 / 填充方式 
  * Java 7支持PKCS5PADDING填充方式 
  * Bouncy Castle支持PKCS7Padding填充方式
  */
  public static final String CIPHER_ALGORITHM = "AES/ECB/PKCS5Padding";
  /**
  * 转换密钥
  * @param key 二进制密钥
  * @return Key 密钥
 @throws Exception
  */
  private static Key toKey(byte[] key) throws Exception {
      // 实例化DES密钥材料
      SecretKey secretKey = new SecretKeySpec(key, KEY_ALGORITHM);
      return secretKey;
  }
  /**
  * 解密
  * @param data 待解密数据
  * @param key 密钥
  * @return byte[] 解密数据
  * @throws Exception
  */
  public static byte[] decrypt(byte[] data, byte[] key) throws Exception {
      // 还原密钥
      Key k = toKey(key);
      /*
      * 实例化 
      * 使用PKCS7Padding填充方式,按如下方式实现 
      * Cipher.getInstance(CIPHER_ALGORITHM, "BC");
      */
      Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM);
 // 初始化,设置为解密模式
      cipher.init(Cipher.DECRYPT_MODE, k);
      // 执行操作
      return cipher.doFinal(data);
  }
  /**
  * 加密
  * @param data 待加密数据
  * @param key 密钥
  * @return byte[] 加密数据
  * @throws Exception
  */
  public static byte[] encrypt(byte[] data, byte[] key) throws Exception {
      // 还原密钥
      Key k = toKey(key);
      /*
      * 实例化 
      * 使用PKCS7Padding填充方式,按如下方式实现
      * Cipher.getInstance(CIPHER_ALGORITHM, "BC");
      */
      Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM);
      // 初始化,设置为加密模式
      cipher.init(Cipher.ENCRYPT_MODE, k);
// 执行操作
      return cipher.doFinal(data);
  }
  /**
  * 生成密钥 <br>
  * @return byte[] 二进制密钥
  * @throws Exception
  */
  public static byte[] initKey() throws Exception {
      // 实例化
      KeyGenerator kg = KeyGenerator.getInstance(KEY_ALGORITHM);
      // AES 要求密钥长度为128位、192位或256位
      kg.init(256);
      // 生成秘密密钥
      SecretKey secretKey = kg.generateKey();
      // 获得密钥的二进制编码形式
      return secretKey.getEncoded();
  }
}

       上述代码实现当通用的,不仅RC2、RC4和Blowfish算法可以使用上述代码方式实现,包括已经实现的DES和DESede算法都可以参照上述代码实现方式,无须考虑密钥材料实现类,只要对算法名称稍作调整即可!

上述代码测试用例与上文提到的DES和DESede算法的测试用例代码差别不大,如代码清单6-6所示。
代码清单6-6 AES算法实现测试用例

import static org.junit.Assert.*;
import org.apache.commons.codec.binary.Base64;
import org.junit.Test;
/**
 * AES安全编码组件校验
 */
public class AESCoderTest {
  /**
  * 测试
  * @throws Exception
  */
  @Test
  public final void test() throws Exception {
      String inputStr = "AES";
      byte[] inputData = inputStr.getBytes();
      System.err.println("原文:\t" + inputStr);
      // 初始化密钥
byte[] key = AESCoder.initKey();
      System.err.println("密钥:\t" + Base64.encodeBase64String(key));
      // 加密
      inputData = AESCoder.encrypt(inputData, key);
      System.err.println("加密后:\t" + Base64.encodeBase64String(inputData));
      // 解密
      byte[] outputData = AESCoder.decrypt(inputData, key);
      String outputStr = new String(outputData);
      System.err.println("解密后:\t" + outputStr);
      // 校验
      assertEquals(inputStr, outputStr);
  }
}

 观察控制台输出的信息,如下所示:
原文:    AES
密钥:  4qrBlCmeHHyEAyoNRY2djo1HWx8LLCH2NQHG9c0ahi4=
加密后:  XhWaN6Am1T3NVSFYs1OMVg==
解密后:  AES

6.5 国际数据加密标准——IDEA
       早在NIST发布征集AES算法以前,就已经有人在找寻DES算法的替代算法了。IDEA算法的提出者未像DESede算法那样在原有DES算法的基础上做改进,而是独辟蹊径地寻求了突破性解决方案。IDEA算法早于AES算法作为DES算法的可选替代算法出现。

6.5.1 简述
       IDEA(International Data Encryption Algorithm,国际数据加密标准)算法是由旅居瑞士的中国青年学者来学嘉和著名密码专家James Massey于1990年提出的一种对称分组密码,并于1992年修改完成。
      IDEA算法使用长度为128位的密钥,数据块大小为64位。从理论上讲,IDEA属于“强”加密算法,至今还没有出现对该算法的有效攻击算法(以目前计算机水平,破译一个IDEA密钥至少需要1013年)。
     IDEA算法在美国之外提出并发展起来,避开了美国法律上对加密技术的诸多限制。因此,有关IDEA算法和实现技术的书籍均可自由出版和交流,极大地促进了IDEA算法的发展和完善。
      IDEA算法是目前较为常用的电子邮件加密算法之一。电子邮件加密软件PGP(Pretty Good Privacy)使用了具有商业版权的IDEA算法,实现邮件加密/解密工作。

6.5.2 实现
         Java 7没有提供IDEA算法的相应实现,若需要使用该算法我们可以通过Bouncy Castle来完成。Bouncy Castle不仅提供了IDEA算法实现,包括其他Java 7未能支持的对称加密算法也可通过Bouncy Castele实现,如AES候选算法Rijndael、Serpent和Twofish等。本节以IDEA算法实现为例,读者朋友可参照其他算法的实现,如Rijndael、Serpent和Twofish等。
有关IDEA算法的Bouncy Castle实现细节如表所示。

 IDEA算法的实现需要通过Bouncy Castle来完成,参考如下代码:
import java.security.Security;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
// 省略
// 加入BouncyCastleProvider支持
Security.addProvider(new BouncyCastleProvider());

本节代码实现对于其他Bouncy Castle支持的算法实现有代表性意义,实现如代码清单6-7所示。
代码清单6-7 IDEA算法实现

import java.security.Key; 
import java.security.Security;
import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
/**
 * IDEA安全编码组件
 * @version 1.0
 * @since 1.0
 */
public abstract class IDEACoder {
  /**
  * 密钥算法
  */
  public static final String KEY_ALGORITHM = "IDEA";
  /**
  * 加密/解密算法 / 工作模式 / 填充方式
  */
  public static final String CIPHER_ALGORITHM = "IDEA/ECB/ISO10126Padding";
  /**
* 转换密钥
  * @param key 二进制密钥
  * @return Key 密钥
  * @throws Exception
  */
  private static Key toKey(byte[] key) throws Exception {
      // 生成秘密密钥
      SecretKey secretKey = new SecretKeySpec(key, KEY_ALGORITHM);
      return secretKey;
  }
  /**
  * 解密
  * @param data 待解密数据
  * @param key 密钥
  * @return byte[] 解密数据
  * @throws Exception
  */
  public static byte[] decrypt(byte[] data, byte[] key) throws Exception {
      // 加入BouncyCastleProvider支持
      Security.addProvider(new BouncyCastleProvider());
      // 还原密钥
      Key k = toKey(key);
      // 实例化
Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM);
      // 初始化,设置为解密模式
      cipher.init(Cipher.DECRYPT_MODE, k);
      // 执行操作
      return cipher.doFinal(data);
  }
  /**
  * 加密
  * @param data 待加密数据
  * @param key 密钥
  * @return byte[] 加密数据
  * @throws Exception
  */
  public static byte[] encrypt(byte[] data, byte[] key) throws Exception {
      // 加入BouncyCastleProvider支持
      Security.addProvider(new BouncyCastleProvider());
      // 还原密钥
      Key k = toKey(key);
      // 实例化
      Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM);
      // 初始化,设置为加密模式
      cipher.init(Cipher.ENCRYPT_MODE, k);
      // 执行操作
return cipher.doFinal(data);
  }
  /**
  * 生成密钥 <br>
  * @return byte[] 二进制密钥
  * @throws Exception
  */
  public static byte[] initKey() throws Exception {
      // 加入BouncyCastleProvider支持
      Security.addProvider(new BouncyCastleProvider());
      // 实例化
      KeyGenerator kg = KeyGenerator.getInstance(KEY_ALGORITHM);
      // 初始化
      kg.init(128);
      // 生成秘密密钥
      SecretKey secretKey = kg.generateKey();
      // 获得密钥的二进制编码形式
      return secretKey.getEncoded();
  }
}

 IDEA算法本身也可以通过Bouncy Castle的API来实现,甚至包括其他Java 7未能支持的算法也可以通过Bouncy Castle的API来实现,包括Serpent和Twofish等。但是,如果这样做就跳出了JCE框架,增加了学习难度。既然JCE框架给我们提供了便捷、灵活的第三方加密算法调用方式,我们为什么不能加以运用呢?
对上述代码做测试,如代码清单6-8所示。
代码清单6-8 IDEA算法实现测试用例

import static org.junit.Assert.*;
import org.apache.commons.codec.binary.Base64;
import org.junit.Test;
/**
 * IDEA安全编码组件校验
 * @version 1.0
 */
public class IDEACoderTest {
  /**
  * 测试
  * @throws Exception
  */
  @Test
  public final void test() throws Exception {
      String inputStr = "IDEA";
      byte[] inputData = inputStr.getBytes();
      System.err.println("原文:\t" + inputStr);
// 初始化密钥
      byte[] key = IDEACoder.initKey();
      System.err.println("密钥:\t" + Base64.encodeBase64String(key));
      // 加密
      inputData = IDEACoder.encrypt(inputData, key);
      System.err.println("加密后:\t" + Base64.encodeBase64String(inputData));
      // 解密
      byte[] outputData = IDEACoder.decrypt(inputData, key);
      String outputStr = new String(outputData);
      System.err.println("解密后:\t" + outputStr);
      // 校验
      assertEquals(inputStr, outputStr);
  }
}

控制台输出信息如下所示:
原文:    IDEA
密钥:  W1a1IFCKIbXKdnejfT5b2A==
加密后:  er43StU2Txk=
解密后:  IDEA

6.6 基于口令加密——PBE
      前文阐述了几种常用的对称加密算法,这些算法的应用模型(甚至包括实现)几乎同出一辙。但并不是所有的对称加密算法都是如此,PBE算法综合了上述对称加密算法和消息摘要算法的优势,形成了对称加密算法的一个特例。

6.6.1 简述
      PBE(Password Based Encryption,基于口令加密)算法是一种基于口令的加密算法,其特点在于口令由用户自己掌管,采用随机数(这里我们叫做盐)杂凑多重加密等方法保证数据的安全性。
PBE算法没有密钥的概念,密钥在其他对称加密算法中是经过算法计算得出的,PBE算法中则使用口令替代了密钥。
       密钥的长短直接影响了算法的安全性,但不方便记忆。即便是我们将密钥通过Base64编码转换为可见字符,长密钥一样不容易记忆。因此,在这种情况下密钥是需要存储的,口令则不然。作者天天都要开关电脑,进入操作系统的唯一途径就是输入口令。口令是我们便于记忆的一种凭证,基于这一点,PBE算法使用口令代替了密钥。
       PBE算法并没有真正构建新的加密/解密算法,而是对我们已知的对称加密算法(如DES算法)做了包装。使用PBE算法对数据做加密/解密操作时,其实是使用了DES或AES等其他对称加密算法做了相应的操作。
       既然PBE算法使用了我们较为常用的对称加密算法,那就无法回避密钥的问题。口令并不能替代密钥,密钥是经过加密算法计算得出的,但口令本身不可能很长,单纯的口令很容易通过穷举攻击方式破译,这就引入了“盐”。盐能够阻止字典攻击或预先计算的攻击,它本身是一个随机信息,相同的随机信息极不可能使用两次。将盐附加在口令上,通过消息摘要算法经过迭代计算  获得构建密钥/初始化向量的基本材料,使得破译加密信息的难度加大。
        PBE算法是对称加密算法的综合性算法,常见算法如PBEWithMD5AndDES,该算法使用了MD5和DES算法构建PBE算法。

6.6.2 实现
       Java 7和Bouncy Castle都提供了PBE系列算法的相关实现,差别在于对各种消息摘要算法和对称加密算法的组合。常用的消息摘要算法包括MD5和SHA算法,常用的对称加密算法包括DES、RC2等。PBE系列算法就是将这些算法进行合理组合,其密钥长度均以PBE具体算法中的对称加密算法密钥长度为准,其工作模式基本上为CBC模式。
       有关PBE算法的Java 7和Bouncy Castle实现细节如表所示。

                                                                   图表所示为PBE算法

PEB算法是实现过程中需要关注的环节,包括盐的初始化、密钥材料的转换以及加密/解密

实现。
在初始化盐时,必须使用随机的方式构建盐,最终要得到一个8字节的字节数组。鉴于安全性要求,这里的随机数生成器只能使用SecureRandom类,如下所示:
// 实例化安全随机数
SecureRandom random = new SecureRandom();
// 产出盐
byte[] b = random.generateSeed(8);

字节数组b[]就是我们要的盐。
密钥材料转换部分不同于其他对称加密算法,这里使用的是PBEKeySpec类,如下所示:
// 密钥材料转换
PBEKeySpec keySpec = new PBEKeySpec(password.toCharArray());

其他对称加密算法的密钥材料实现类的构造方法要求输入字节数组形式的变量,而PBEKeySpec类构造方法则要求输入字符数组变量。
为什么不是字符串(String)而是字符数组(char[])呢?这是因为字符串是可序列化的封装类,可在程序调用时被序列化到文件中,而字符数组只能以内存变量的形式保留在内存中。
在加密/解密实现时,需要注意使用参数材料PBEParameterSpec类,同时注意迭代次数。构建PBE参数材料后就可以转交给Cipher类完成加密/解密操作,如下所示:

// 实例化PBE参数材料
PBEParameterSpec paramSpec = new PBEParameterSpec(salt, ITERATION_COUNT);
// 实例化
Cipher cipher = Cipher.getInstance(ALGORITHM);
// 初始化
cipher.init(Cipher.DECRYPT_MODE, key, paramSpec);

本节以Java 7提供的PBEWITHMD5andDES算法为例,如代码清单6-9所示。
代码清单6-9 PBE算法实现
import java.security.Key;
import java.security.SecureRandom;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.PBEParameterSpec;
/**
 * PBE安全编码组件
 * @version 1.0
 */
public abstract class PBECoder {
  /**
  * Java 7 支持以下任意一种算法
    *  PBEWithMD5AndDES 
  * PBEWithMD5AndTripleDES 
  * PBEWithSHA1AndDESede
  * PBEWithSHA1AndRC2_40
  * PBKDF2WithHmacSHA1
  */
  public static final String ALGORITHM = "PBEWITHMD5andDES";
  /**
  * 迭代次数
  */
  public static final int ITERATION_COUNT = 100;
  /**
  * 盐初始化<br>
  * 盐长度必须为8字节
  * @return byte[] 盐
  * @throws Exception
  */
  public static byte[] initSalt() throws Exception {
      // 实例化安全随机数
      SecureRandom random = new SecureRandom();
      // 产出盐
      return random.generateSeed(8);
  }
/**
  * 转换密钥 
  * @param password 密码
  * @return Key 密钥
  * @throws Exception
  */
  private static Key toKey(String password) throws Exception {
      // 密钥材料转换
      PBEKeySpec keySpec = new PBEKeySpec(password.toCharArray());
      // 实例化
      SecretKeyFactory keyFactory = SecretKeyFactory.getInstance(ALGORITHM);
      // 生成密钥
      SecretKey secretKey = keyFactory.generateSecret(keySpec);
      return secretKey;
  }
  /**
  * 加密
  * @param data 数据
  * @param password 密码
  * @param salt 盐
  * @return byte[] 加密数据
  * @throws Exception
*/
  public static byte[] encrypt(byte[] data, String password, byte[] salt) throws Exception {
      // 转换密钥
      Key key = toKey(password);
      // 实例化PBE参数材料
      PBEParameterSpec paramSpec = new PBEParameterSpec(salt, ITERATION_COUNT);
      // 实例化
      Cipher cipher = Cipher.getInstance(ALGORITHM);
      // 初始化
      cipher.init(Cipher.ENCRYPT_MODE, key, paramSpec);
      // 执行操作
      return cipher.doFinal(data);
  }
  /**
  * 解密
  * @param data 数据
  * @param password 密码
  * @param salt 盐
  * @return byte[] 解密数据
  * @throws Exception
  */
public static byte[] decrypt(byte[] data, String password, byte[] salt) throws Exception {
      // 转换密钥
      Key key = toKey(password);
      // 实例化PBE参数材料
      PBEParameterSpec paramSpec = new PBEParameterSpec(salt, ITERATION_COUNT);
      // 实例化
      Cipher cipher = Cipher.getInstance(ALGORITHM);
      // 初始化
      cipher.init(Cipher.DECRYPT_MODE, key, paramSpec);
      // 执行操作
      return cipher.doFinal(data);
  }
}

 

上述代码的测试用例如代码清单6-10所示。
代码清单7-10 PBE算法实现测试用例
import static org.junit.Assert.*;
import org.apache.commons.codec.binary.Base64;
import org.junit.Test;
/**
 * PBE校验
 * @version 1.0
 */
public class PBECoderTest {
  /**
  * 测试
  * @throws Exception
  */
  @Test
  public void test() throws Exception {
      String inputStr = "PBE";
      System.err.println("原文:" + inputStr);
      byte[] input = inputStr.getBytes();
      String pwd = "snowolf@zlex.org";
      System.err.println("密码:\t" + pwd);
      // 初始化盐
      byte[] salt = PBECoder.initSalt();
      System.err.println("盐:\t" + Base64.encodeBase64String(salt));
      // 加密
      byte[] data = PBECoder.encrypt(input, pwd, salt);
      System.err.println("加密后:\t" + Base64.encodeBase64String(data));
      // 解密
      byte[] output = PBECoder.decrypt(data, pwd, salt);
String outputStr = new String(output);
      System.err.println("解密后:\t" + outputStr);
      // 校验
      assertEquals(inputStr, outputStr);
  }
}

我们在控制台中得到如下信息:
原文:    PBE
密码:  snowolf@zlex.org
盐:   qbjve/LfGIM=
加密后:  NZQG0WfqAg4=
解密后:  PBE

6.7 对称加密网络应用
      加密技术与网络应用密不可分,在开放的网络中进行机密数据传输少不了使用加密算法。在实际应用中,我们常常需要向合作伙伴发送XML格式的数据包。例如,数据报表、货仓清单等。这些都是机密信息,如不对其加密必将公司机密泄露无疑。
本文将使用AES算法,并配合第6章提到的SHA算法构建简单的基于对称加密算法的数据传输网络应用——DataServer。
本实例将用到开源组件包Commons Codec来协助完成本实例的构建。请读者朋友将该相关jar包部署到该应用的/WEB-INF/lib目录下。
       首先,我们来构建一个用于HTTP请求的工具类——HttpUtils。当然,读者朋友可以选用成熟的开源框架——Apache Commons HttpClient替代下述代码。相信大多数读者朋友对于如何构建HTTP请求都了如指掌,为避免拖沓作者在此不对该类做详细介绍,请读者朋友参考Java API相关内容。代码实现如代码清单6-11所示。

代码清单6-11 HttpUtils
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.Map;
import java.util.Properties;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
 * Http 工具
 * @version 1.0
 * @since 1.0
 */
public abstract class HttpUtils {
  public static final String CHARACTER_ENCODING = "UTF-8";
  public static final String METHOD_POST = "POST";
  public static final String CONTENT_TYPE = "Content-Type";
  /**
  * 打印数据
  * @param response HttpServletResponse
  * @param data 待打印的数据
  */
  public static void responseWrite(HttpServletResponse response, byte[] data) throws IOException {
    if (data != null) {
      response.setContentLength(data.length);
      DataOutputStream out = new DataOutputStream(response.getOutputStream());
      out.write(data);
      out.flush();
      out.close();
    }
  }
  /**
  * 从请求中读字节流
  * @param request HttpServletRequest
  * @return byte[] 数据
  * @throws IOException
  */
  public static byte[] requestRead(HttpServletRequest request)
      throws IOException {
    int contentLength = request.getContentLength();
    byte[] data = null;
    if (contentLength > 0) {
      data = new byte[contentLength];
      InputStream is = request.getInputStream();
      DataInputStream dis = new DataInputStream(is);
      dis.readFully(data);
      dis.close();
    }
return data;
  }
  /**
  * 以POST方式向指定地址发送数据包请求,并取得返回的数据包
  * @param urlString 请求地址
  * @param requestData 请求数据
  * @return byte[] 数据包
  * @throws IOException
  */
  public static byte[] postRequest(String urlString, byte[] requestData) throws Exception {
    Properties requestProperties = new Properties();
    requestProperties.setProperty(CONTENT_TYPE, "application/octet-stream; charset=" + CHARACTER_ENCODING);
    return postRequest(urlString, requestData, requestProperties);
  }
  /**
  * 以POST方式向指定地址发送数据包请求,并取得返回的数据包
  * @param urlString 请求地址
  * @param requestData 请求数据
  * @param requestProperties 请求包体
  * @return byte[] 数据包
  * @throws IOException
*/
  public static byte[] postRequest(String urlString, byte[] requestData, Properties requestProperties) throws Exception {
    byte[] responseData = null;
    HttpURLConnection con = null;
    try {
      URL url = new URL(urlString);
      con = (HttpURLConnection) url.openConnection();
      if ((requestProperties != null) && (requestProperties.size() > 0)) {
        for (Map.Entry<Object, Object> entry : requestProperties.entrySet()) {
          String key = String.valueOf(entry.getKey());
          String value = String.valueOf(entry.getValue());
          con.setRequestProperty(key, value);
        }
      }
      con.setRequestMethod(METHOD_POST);
      con.setDoOutput(true);
      con.setDoInput(true);
      DataOutputStream dos = new DataOutputStream(con.getOutputStream());
      if (requestData != null) {
        dos.write(requestData);
}
      dos.flush();
      dos.close();
      DataInputStream dis = new DataInputStream(con.getInputStream());
      int length = con.getContentLength();
      if (length > 0) {
        responseData = new byte[length];
        dis.readFully(responseData);
      }
      dis.close();
    } finally {
      // 关闭流
      if (con != null) {
        con.disconnect();
        con = null;
      }
    }
    return responseData;
  }
}

接下来,我们将构建用于加密/解密的工具类——AESCoder。此处,我们将用到第三方开源组件包——Commons Codec。

对于AESCoder类,我们参考代码清单7-5稍作调整,对密钥稍作包装以方便在存储和使用,如代码清单6-12所示。

import org.apache.commons.codec.binary.Base64;
// 省略
/**
 * 初始化密钥
 * @return String Base64编码密钥
 * @throws Exception
 */
public static String initKeyString() throws Exception {
  return Base64.encodeBase64String(initKey());
}
/**
 * 获取密钥
 * @param key 密钥
 * @return byte[] 密钥
 * @throws Exception
 */
public static byte[] getKey(String key) throws Exception {
  return Base64.decodeBase64(key);
}
/**
 * 解密
 * @param data 待解密数据
 * @param key 密钥
 * @return byte[] 解密数据
 * @throws Exception
 */
public static byte[] decrypt(byte[] data, String key) throws Exception {
  return decrypt(data, getKey(key));
}
/**
 * 加密
 * @param data 待加密数据
 * @param key 密钥
 * @return byte[] 加密数据
 * @throws Exception
 */
public static byte[] encrypt(byte[] data, String key) throws Exception {
  return encrypt(data, getKey(key));
}

      这里我们使用Commons Codec提供Base64算法对密钥进行封装/解包。通过initKeyString()方法我们将得到密钥字符串:“Zk6tc8Gg3NVi+m6X2UmV7rd1XRRiF1sUNtxFbiFqjMs=”。我们将在后续操作中用到该内容。
为防止传递的机密数据在网络传递过程中被篡改,我们可以对数据进行消息摘要,并对该摘要进行验证。这里使用SHA算法对数据进行摘要/验证。完整实现如代码清单6-13所示。

代码清单6-13 摘要/验证
import org.apache.commons.codec.digest.DigestUtils;
// 省略
/**
 * 摘要处理
 * @param data 待摘要数据
 * @return String 摘要字符串
 */
public static String shaHex(byte[] data) {
  return DigestUtils.md5Hex(data);
}
/**
 * 验证
 * @param data 待摘要数据
 * @param messageDigest 摘要字符串
 * @return boolean 验证结果
 */
public static boolean validate(byte[] data, String messageDigest) {
  return messageDigest.equals(shaHex(data));
}

AESCoder类完整实现如代码清单6-14所示。
代码清单6-14 AESCoder
import java.security.Key;
import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.codec.digest.DigestUtils;
/**
 * AES安全编码组件
 * @version 1.0
 */
public abstract class AESCoder {
  /**
  * 密钥算法
  */
  public static final String ALGORITHM = "AES";
  /**
  * 转换密钥
 @param key 二进制密钥
  * @return Key 密钥
  * @throws Exception
  */
  private static Key toKey(byte[] key) throws Exception {
     // 实例化AES密钥材料
     SecretKey secretKey = new SecretKeySpec(key, ALGORITHM);
     return secretKey;
  }
  /**
  * 解密
  * @param data 待解密数据
  * @param key 密钥
  * @return byte[] 解密数据
  * @throws Exception
  */
  public static byte[] decrypt(byte[] data, byte[] key) throws Exception {
     // 还原密钥
     Key k = toKey(key);
     // 实例化
     Cipher cipher = Cipher.getInstance(ALGORITHM);
     // 初始化,设置为解密模式
     cipher.init(Cipher.DECRYPT_MODE, k);
// 执行操作
     return cipher.doFinal(data);
  }
  /**
  * 解密
  * @param data 待解密数据
  * @param key 密钥
  * @return byte[] 解密数据
  * @throws Exception
  */
  public static byte[] decrypt(byte[] data, String key) throws Exception {
     return decrypt(data, getKey(key));
  }
  /**
  * 加密
  * @param data 待加密数据
  * @param key 密钥
  * @return byte[] 加密数据
  * @throws Exception
  */
  public static byte[] encrypt(byte[] data, byte[] key) throws Exception {
     // 还原密钥
     Key k = toKey(key);
// 实例化
     Cipher cipher = Cipher.getInstance(ALGORITHM);
     // 初始化,设置为加密模式
     cipher.init(Cipher.ENCRYPT_MODE, k);
     // 执行操作
     return cipher.doFinal(data);
  }
  /**
  * 加密
  * @param data 待加密数据
  * @param key 密钥
  * @return byte[] 加密数据
  * @throws Exception
  */
  public static byte[] encrypt(byte[] data, String key) throws Exception {
     return encrypt(data, getKey(key));
  }
  /**
  * 生成密钥
  * @return byte[] 二进制密钥
  * @throws Exception
  */
  public static byte[] initKey() throws Exception {
// 实例化
     KeyGenerator kg = KeyGenerator.getInstance(ALGORITHM);
     // 初始化256位密钥
     kg.init(256);
     // 生成秘密密钥
     SecretKey secretKey = kg.generateKey();
     // 获得密钥的二进制编码形式
     return secretKey.getEncoded();
  }
  /**
  * 初始化密钥
  * @return String Base64编码密钥
  * @throws Exception
  */
  public static String initKeyString() throws Exception {
     return Base64.encodeBase64String(initKey());
  }
  /**
  * 获取密钥
  * @param key 密钥
  * @return byte[] 密钥
  * @throws Exception
  */
public static byte[] getKey(String key) throws Exception {
     return Base64.decodeBase64(key);
  }
  /**
  * 摘要处理
  * @param data 待摘要数据
  * @return String 摘要字符串
  */
  public static String shaHex(byte[] data) {
     return DigestUtils.md5Hex(data);
  }
  /**
  * 验证
  * @param data 待摘要数据
  * @param messageDigest 摘要字符串
  * @return boolean 验证结果
  */
  public static boolean validate(byte[] data, String messageDigest) {
     return messageDigest.equals(shaHex(data));
  }
}

 

接下来,我们将构建用于提供服务的Servlet—DataServlet类。这里,DataServlet类继承了HttpServlet类,重写了init()方法并实现了doPost()方法。
我们希望将密钥可以通过web.xml文件进行配置,在DataServlet启动时加载该密钥。我们通过重写init()方法获得密钥信息。相关实现如代码清单6-15所示。

代码清单6-15 DataServlet——初始化
// 省略
// 密钥
private static String key;
// Servlet初始化参数--密钥
private static final String KEY_PARAM = "key";
// 省略
// 初始化
@Override
public void init() throws ServletException {
  super.init();
  // 初始化密钥
  key = getInitParameter(KEY_PARAM);
}

我们可以调用HttpUtils类的requestRead()方法获得请求内容,调用AESCoder类的decrypt()方法对请求内容解密,并将解密后的内容输出在控制台中。同时,我们将从HTTP Header中获得此次交互数据的摘要信息,并对此验证。如果验证通过,则回复“OK”。相关实现如代码清单6-16所示。

代码清单6-16 DataServlet——处理POST请求
// HTTP Header 摘要参数名
private static final String HEAD_MD = "messageDigest";
// 省略
// 处理POST请求
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
  try {
      byte[] input = HttpUtils.requestRead(request);
      // 对数据解密
      byte[] data = AESCoder.decrypt(input, key);
      System.err.println(new String(data));
      // 默认回复内容
      byte[] output = "".getBytes();
      // 获得此次交互数据的摘要信息
      String messageDigest = request.getHeader(HEAD_MD);
      // 如果验证成功则回复OK
      if (AESCoder.validate(data, messageDigest)) {
        // 如果正常接收到数据则回复OK
        output = "OK".getBytes();
      }
      // 加密回复
HttpUtils.responseWrite(response, AESCoder.encrypt(output, key));
    } catch (Exception e) {
      throw new ServletException(e);
    }
}

完整实现如代码清单6-17所示。 


代码清单6-17 DataServlet
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
 * 数据服务DataServlet
 * @since 1.0
 */
public class DataServlet extends HttpServlet {
  private static final long serialVersionUID = -6219906900195793155L;
  // 密钥
  private static String key;
  // Servlet初始化参数--密钥
private static final String KEY_PARAM = "key";
  // HTTP Header 摘要参数名
  private static final String HEAD_MD = "messageDigest";
  // 初始化
  @Override
  public void init() throws ServletException {
      super.init();
      // 初始化密钥
      key = getInitParameter(KEY_PARAM);
  }
  // 处理POST请求
  protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
      try {
          // 获得此次交互数据的摘要信息
          String messageDigest = request.getHeader(HEAD_MD);
          byte[] input = HttpUtils.requestRead(request);
          // 对数据解密
          byte[] data = AESCoder.decrypt(input, key);
          System.err.println(new String(data));
          // 默认回复内容
          byte[] output = "".getBytes();
          // 如果验证成功则回复OK
if (AESCoder.validate(data, messageDigest)) {
          // 如果正常接收到数据则回复OK
          output = "OK".getBytes();
      }
      // 加密回复
      HttpUtils.responseWrite(response, AESCoder.encrypt(output, key));
    } catch (Exception e) {
      throw new ServletException(e);
    }
  }
}

 此处,我们将密钥(“Zk6tc8Gg3NVi+m6X2UmV7rd1XRRiF1sUNtxFbiFqjMs=”)配置在web.xml文件中,完整代码如代码清单6-18所示。

代码清单6-18 web.xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
id="WebApp_ID"version="2.5">
<display-name>DataServer</display-name>
<servlet>
<servlet-name>DataServlet</servlet-name>
<servlet-class>DataServlet</servlet-class>
<init-param>
<param-name>key</param-name>
<param-value> 
<![CDATA[Zk6tc8Gg3NVi+m6X2UmV7rd1XRRiF1sUNtxFbiFqjMs=]]>
 </param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>DataServlet</servlet-name>
<url-pattern>/DataServlet</url-pattern>
</servlet-mapping>
</web-app>

此处,我们将向服务器发送XML格式的数据包。并在发送数据之前对其进行摘要和加密,并校验获得的信息是否包含“OK”内容。此处,我们将密钥作为变量写入DataServletTest类中。完整实现如代码清单6-19所示。

代码清单6-19 DataServletTest
import static org.junit.Assert.*;
import java.util.Properties;
import org.junit.Test;
/**
 * DataServlet测试用例
 * @since 1.0
 */
public class DataServletTest {
  // 秘密密钥
  private static final String key = "Zk6tc8Gg3NVi+m6X2UmV7rd1XRRiF1sUNtxFbiFqjMs=";
  // 请求地址
  private static final String url = "http://localhost:8080/dataserver/DataServlet";
  @Test
  public final void test() throws Exception {
     // 构造数据包
     StringBuilder sb = new StringBuilder();
     sb.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\r\n");
     sb.append("<dataGroup>\r\n");
     sb.append("\t<dataItem>\r\n");
     sb.append("\t\t<id>");
     sb.append("10201");
     sb.append("</id>\r\n");
sb.append("\t\t<price>");
     sb.append("35.0");
     sb.append("</price>\r\n");
     sb.append("\t\t<time>");
     sb.append("2009-10-30");
     sb.append("</time>\r\n");
     sb.append("\t</dataItem>\r\n");
     sb.append("\t<dataItem>\r\n");
     sb.append("\t\t<id>");
     sb.append("10301");
     sb.append("</id>\r\n");
     sb.append("\t\t<price>");
     sb.append("55.0");
     sb.append("</price>\r\n");
     sb.append("\t\t<time>");
     sb.append("2009-10-31");
     sb.append("</time>\r\n");
     sb.append("\t</dataItem>\r\n");
     sb.append("</dataGroup>\r\n");
     byte[] data = sb.toString().getBytes();
     // 向HTTP Header附加数据摘要信息
     Properties requestProperties = new Properties();
     requestProperties.put("messageDigest", AESCoder.shaHex(data));
/ 加密并发送数据
     byte[] input = HttpUtils.postRequest(url, AESCoder.encrypt(data, key), requestProperties);
     // 解密
     input = AESCoder.decrypt(input, key);
     // 校验
     assertEquals("OK", new String(input));
  }
}

启动DataServer服务,并执行测试用例,我们将在控制台中得到如下信息:
<?xml version="1.0" encoding="UTF-8"?>
<dataGroup>
  <dataItem>
    <id>10201</id>
    <price>35.0</price>
    <time>2009-10-30</time>
  </dataItem>
  <dataItem>
    <id>10301</id>
    <price>55.0</price>
    <time>2009-10-31</time>
  </dataItem>

</dataGroup>

很显然,这是我们发送给服务器的数据内容。
在实际应用中,我们经常需要与合作伙伴进行机密数据交互。通常,我们都会使用对称加密算法,例如AES算法对其进行加密。并使用消息摘要算法对其内容进行摘要/验证,例如SHA算法。
 

  • 11
    点赞
  • 68
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值