本文主要谈一下密码学中的加密 和数字签名,以及其在java 中如何进行使用。对密码学有兴趣的伙伴,推荐看Bruce Schneier 的著作:Applied Crypotography 。在jdk1.5 的发行版本中安全性方面有了很大的改进,也提供了对RSA 算法的直接支持,现在我们从实例入手解决问题(本文仅是作为简单介绍): 一、密码学上常用的概念 1 )消息摘要: 这是一种与消息认证码结合使用以确保消息完整性的技术。主要使用单向散列函数算法,可用于检验消息的完整性,和通过散列密码直接以文本形式保存等,目前广泛使用的算法有MD4 、MD5 、SHA-1 ,jdk1.5 对上面都提供了支持,在java 中进行消息摘要很简单, java.security.MessageDigest 提供了一个简易的操作方法:
/** *MessageDigestExample.java *Copyright 2005-2-16 */ import java.security.MessageDigest; /** * 单一的消息摘要算法,不使用密码. 可以用来对明文消息(如:密码)隐藏保存 */ public class MessageDigestExample{ public static void main(String[] args) throws Exception{ if(args.length!=1){ System.err.println("Usage:java MessageDigestExample text"); System.exit(1); } byte[] plainText=args[0].getBytes("UTF8"); // 使用getInstance(" 算法") 来获得消息摘要, 这里使用SHA-1 的160 位算法 MessageDigest messageDigest=MessageDigest.getInstance("SHA-1"); System.out.println("/n"+messageDigest.getProvider().getInfo()); // 开始使用算法 messageDigest.update(plainText); System.out.println("/nDigest:"); // 输出算法运算结果 System.out.println(new String(messageDigest.digest(),"UTF8")); } }
还可以通过消息认证码来进行加密实现,javax.crypto.Mac 提供了一个解决方案,有兴趣者可以参考相关API 文档,本文只是简单介绍什么是摘要算法。 2 )私钥加密: 消息摘要只能检查消息的完整性,但是单向的,对明文消息并不能加密,要加密明文的消息的话,就要使用其他的算法,要确保机密性,我们需要使用私钥密码术来交换私有消息。 这种最好理解,使用对称算法。比如:A 用一个密钥对一个文件加密,而B 读取这个文件的话,则需要和A 一样的密钥,双方共享一个私钥(而在web 环境下,私钥在传递时容易被侦听): 使用私钥加密的话,首先需要一个密钥,可用javax.crypto.KeyGenerator 产生一个密钥(java.security.Key), 然后传递给一个加密工具(javax.crypto.Cipher), 该工具再使用相应的算法来进行加密,主要对称算法有:DES (实际密钥只用到56 位),AES (支持三种密钥长度:128 、192 、256 位),通常首先128 位,其他的还有DESede 等,jdk1.5 种也提供了对对称算法的支持,以下例子使用AES 算法来加密:
/** *PrivateExmaple.java *Copyright 2005-2-16 */ import javax.crypto.Cipher; import javax.crypto.KeyGenerator; import java.security.Key; /** * 私? 加密,保证消息机密性 */ public class PrivateExample{ public static void main(String[] args) throws Exception{ if(args.length!=1){ System.err.println("Usage:java PrivateExample "); System.exit(1); } byte[] plainText=args[0].getBytes("UTF8"); // 通过KeyGenerator 形成一个key System.out.println("/nStart generate AES key"); KeyGenerator keyGen=KeyGenerator.getInstance("AES"); keyGen.init(128); Key key=keyGen.generateKey(); System.out.println("Finish generating DES key"); // 获得一个私? 加密类Cipher ,ECB 是加密方式,PKCS5Padding 是填充方法 Cipher cipher=Cipher.getInstance("AES/ECB/PKCS5Padding"); System.out.println("/n"+cipher.getProvider().getInfo()); // 使用私? 加密 System.out.println("/nStart encryption:"); cipher.init(Cipher.ENCRYPT_MODE,key); byte[] cipherText=cipher.doFinal(plainText); System.out.println("Finish encryption:"); System.out.println(new String(cipherText,"UTF8")); System.out.println("/nStart decryption:"); cipher.init(Cipher.DECRYPT_MODE,key); byte[] newPlainText=cipher.doFinal(cipherText); System.out.println("Finish decryption:"); System.out.println(new String(newPlainText,"UTF8")); } }
3 )公钥加密: 上面提到,私钥加密需要一个共享的密钥,那么如何传递密钥呢?web 环境下,直接传递的话很容易被侦听到,幸好有了公钥加密的出现。公钥加密也叫不对称加密,不对称算法使用一对密钥对,一个公钥,一个私钥,使用公钥加密的数据,只有私钥能解开(可用于加密);同时,使用私钥加密的数据,只有公钥能解开(签名)。但是速度很慢(比私钥加密慢100 到1000 倍),公钥的主要算法有RSA ,还包括Blowfish,Diffie-Helman 等,jdk1.5 种提供了对RSA 的支持,是一个改进的地方:
/** *PublicExample.java *Copyright 2005-2-16 */ import java.security.Key; import javax.crypto.Cipher; import java.security.KeyPairGenerator; import java.security.KeyPair; /** * 一个简单的公? 加密例子,Cipher 类使用KeyPairGenerator 生成的公? 和私? */ public class PublicExample{ public static void main(String[] args) throws Exception{ if(args.length!=1){ System.err.println("Usage:java PublicExample "); System.exit(1); } byte[] plainText=args[0].getBytes("UTF8"); // 构成一个RSA 密钥 System.out.println("/nStart generating RSA key"); KeyPairGenerator keyGen=KeyPairGenerator.getInstance("RSA"); keyGen.initialize(1024); KeyPair key=keyGen.generateKeyPair(); System.out.println("Finish generating RSA key"); // 获得一个RSA 的Cipher 类,使用公? 加密 Cipher cipher=Cipher.getInstance("RSA/ECB/PKCS1Padding"); System.out.println("/n"+cipher.getProvider().getInfo()); System.out.println("/nStart encryption"); cipher.init(Cipher.ENCRYPT_MODE,key.getPublic()); byte[] cipherText=cipher.doFinal(plainText); System.out.println("Finish encryption:"); System.out.println(new String(cipherText,"UTF8")); // 使用私? 解密 System.out.println("/nStart decryption"); cipher.init(Cipher.DECRYPT_MODE,key.getPrivate()); byte[] newPlainText=cipher.doFinal(cipherText); System.out.println("Finish decryption:"); System.out.println(new String(newPlainText,"UTF8")); } }
4 )数字签名: 数字签名,它是确定交换消息的通信方身份的第一个级别。上面A 通过使用公钥加密数据后发给B ,B 利用私钥解密就得到了需要的数据,问题来了,由于都是使用公钥加密,那么如何检验是A 发过来的消息呢?上面也提到了一点,私钥是唯一的,那么A 就可以利用A 自己的私钥进行加密,然后B 再利用A 的公钥来解密,就可以了;数字签名的原理就基于此,而通常为了证明发送数据的真实性,通过利用消息摘要获得简短的消息内容,然后再利用私钥进行加密散列数据和消息一起发送。java 中为数字签名提供了良好的支持,java.security.Signature 类提供了消息签名:
/** *DigitalSignature2Example.java *Copyright 2005-2-16 */ import java.security.Signature; import java.security.KeyPairGenerator; import java.security.KeyPair; import java.security.SignatureException; /** * 数字签名,使用RSA 私钥对对消息摘要签名,然后使用公? 验证 测试 */ public class DigitalSignature2Example{ public static void main(String[] args) throws Exception{ if(args.length!=1){ System.err.println("Usage:java DigitalSignature2Example "); System.exit(1); } byte[] plainText=args[0].getBytes("UTF8"); // 形成RSA 公钥对 System.out.println("/nStart generating RSA key"); KeyPairGenerator keyGen=KeyPairGenerator.getInstance("RSA"); keyGen.initialize(1024); KeyPair key=keyGen.generateKeyPair(); System.out.println("Finish generating RSA key"); // 使用私? 签名 Signature sig=Signature.getInstance("SHA1WithRSA"); sig.initSign(key.getPrivate()); sig.update(plainText); byte[] signature=sig.sign(); System.out.println(sig.getProvider().getInfo()); System.out.println("/nSignature:"); System.out.println(new String(signature,"UTF8")); // 使用公? 验证 System.out.println("/nStart signature verification"); sig.initVerify(key.getPublic()); sig.update(plainText); try{ if(sig.verify(signature)){ System.out.println("Signature verified"); }else System.out.println("Signature failed"); }catch(SignatureException e){ System.out.println("Signature failed"); } } }
5) 数字证书。 还有个问题,就是公钥问题,A 用私钥加密了,那么B 接受到消息后,用A 提供的公钥解密;那么现在有个讨厌的C ,他把消息拦截了,然后用自己的私钥加密,同时把自己的公钥发给B ,并告诉B ,那是A 的公钥,结果.... ,这时候就需要一个中间机构出来说话了(相信权威,我是正确的),就出现了Certificate Authority( 也即CA ),有名的CA 机构有Verisign 等,目前数字认证的工业标准是:CCITT 的X.509 : 数字证书:它将一个身份标识连同公钥一起进行封装,并由称为认证中心或 CA 的第三方进行数字签名。 密钥库:java 平台为你提供了密钥库,用作密钥和证书的资源库。从物理上讲,密钥库是缺省名称为 .keystore 的文件(有一个选项使它成为加密文件)。密钥和证书可以拥有名称(称为别名),每个别名都由唯一的密码保护。密钥库本身也受密码保护;您可以选择让每个别名密码与主密钥库密码匹配。 使用工具keytool ,我们来做一件自我认证的事情吧(相信我的认证): 1 、创建密钥库keytool -genkey -v -alias feiUserKey -keyalg RSA 默认在自己的home 目录下(windows 系统是c:/documents and settings/< 你的用户名> 目录下的.keystore 文件),创建我们用 RSA 算法生成别名为 feiUserKey 的自签名的证书, 如果使用了-keystore mm 就在当前目录下创建一个密钥库mm 文件来保存密钥和证书。 2 、查看证书:keytool -list 列举了密钥库的所有的证书 也可以在dos 下输入 keytool -help 查看帮助。
二、 JAR 的签名 我们已经学会了怎样创建自己的证书了,现在可以开始了解怎样对 JAR 文件签名, JAR 文件在 Java 中相当于 ZIP 文件,允许将多个 Java 类文件打包到一个具有 .jar 扩展名的文件中,然后可以对这个 jar 文件进行数字签名,以证实其来源和真实性。该 JAR 文件的接收方可以根据发送方的签名决定是否信任该代码,并可以确信该内容在接收之前没有被篡改过。同时在部署中,可以通过在策略文件中放置访问控制语句根据签名者的身份分配对机器资源的访问权。这样,有些 Apple t 的安全检验访问就得以进行。 使用 jarsigner 工具可以对 jar 文件进行签名: 现在假设我们有个 Test.jar 文件(可以使用 jar 命令行工具生成): jarsigner Test.jar feiUserKey ( 这里我们上面创建了该别名的证书 ) ,详细信息可以输入 jarsigner 查看帮助 验证其真实性: jarsigner -verify Test.jar( 注意,验证的是 jar 是否被修改了,但不检验减少的,如果增加了新的内容,也提示,但减少的不会提示。) 使用 Applet 中: 然后 浏览器 就会提示你:准许这个会话 - 拒绝 - 始终准许 - 查看证书等。 三、安全套接字层( SSL Secure Sockets Layer )和传输层安全性( TLS Transport Layer Security ) 安全套接字层和传输层安全性是用于在 客户机 和服务器之间构建安全的通信通道的协议。它也用来为客户机认证服务器,以及(不太常用的)为服务器认证客户机。该协议在浏览器应用程序中比较常见,浏览器窗口底部的锁表明 SSL/TLS 有效: 1 )当使用 SSL/TLS (通常使用 https:// URL )向站点进行请求时,从服务器向客户机发送一个证书。客户机使用已安装的公共 CA 证书通过这个证书验证服务器的身份,然后检查 IP 名称(机器名)与客户机连接的机器是否匹配。 2 )客户机生成一些可以用来生成对话的私钥(称为会话密钥)的随机信息,然后用服务器的公钥对它加密并将它发送到服务器。服务器用自己的私钥解密消息,然后用该随机信息派生出和客户机一样的私有会话密钥。通常在这个阶段使用 RSA 公钥算法。 3 )客户机和服务器使用私有会话密钥和私钥算法(通常是 RC4 )进行通信。使用另一个密钥的消息认证码来确保消息的完整性。 java 中 javax.net.ssl.SSLServerSocketFactory 类提供了一个很好的 SSLServerSocker 的工厂类,熟悉 Socket 编程的读者可以去练习。当编写完服务器端之后,在浏览器上输入 https:// 主机 名 : 端口 就会通过 SSL/TLS 进行通话了。注意:运行服务端的时候要带系统环境变量运行: javax.net.ssl.keyStore= 密钥库 ( 创建证书时,名字应该为主机名,比如 localhost) 和 javax.net.ssl.keyStorePassword= 你的密码。