Java 安全编程加密了解2,java编程加密了解
Java 安全编程加密了解2,java编程加密了解
Java 安全编程
1.1 J2SE 的主要工具
基本工具:
javac Java 编程语言的编译器。本书各章的程序都是在 DOS 窗口中通过执行 "javac 文件名 ” 来编译 Java 程序的。文件名必须以 .java 为后缀,编译以后生成 .class 为后缀的字节码文件。
java 用于执行 Java 应用程序。本书各章的程序大都通过在 DOS 窗口输入 “java 字节码文件名称 ” 来运行 javac 编译好的程序。输入命令时,字节码文件名称的后缀不输入。
javadoc 用于生成 API 文档。在编写程序时将注释语句写在 “/**” 和 “*/” 之间,则其内容便可被 javadoc 识别,执行 “javadoc *.java” ,自动生成 API 文档。
appletviewer 没有 Web 浏览器时可用来运行和调试 Java Applet 程序。
jar 管理 jar 文件。本书多次使用该工具将 Java 程序打包成为一个文件,并进而进行进一步的处理。
jdb Java 调试器
javah C 头文件和存根的生成器,用于编写本地文件。
javap 类分解器。可显示字节码文件的包、标记为 public 及 protected 的变量和方法等信息。
extcheck
检测 jar 文件的版本冲突
RMI 工具:
rmic 生成远程对象的架构和存根。执行后可根据给定的字节码文件 XX.class 可生成 XX__Stub.class 和 XX_Skel.class 文件部署在 RMI 系统中。
rmiregistry 提供远程对象的注册服务。 RMI 客户程序可通过该服务找到远程对象。
rmid 启动激活系统后台程序。
serialver 返回类的 serialVersionUID
国际化工具:
native2ascii 将本地编码的文本转换为 Unicode 编码
安全工具
keytool 管理密钥库和证书。本书自第 5 章起大量使用该工具。
Jarsigner 对 jar 文件进行签名,并验证 jar 文件的签名。
policytool 管理策略文件的图形界面工具。
Java IDL and RMI-IIOP 工具
tnameserv 提供访问名字服务
idlj 根据给定的 IDL 文件生成 Java 绑定,使 Java 程序可以使用 CORBA 功能
orbd 在 CORBA 环境中使客户透明地定位和执行服务器上 persistent 对象
servertool 应用程序编写者注册、取消注册、启动、关闭 persistent 服务器的命令行工具。
Java Plug-in
工具:
unregbean 用于取消 Java Bean 组件的注册
HtmlConverter 修改调用 Applet 的 HTML 网页,将其中的 <applet> 标记按照一定格式转
换为 <Object> 标记,以便让浏览器使用 Java Plug-in 运行 Java Applet 程序。
1.2 反编译器的安装
Jad 是 Java 著名的反编译器,下载地址: http://www.varaneckas.com/jad
下载后将其放到 java/bin 的目录下,即可使用了。
( 1 ) 最简单的用法
JAD 的最简单用法是直接输入“ Jad 字节码文件名称”,如
Jad example.class
或 Jad example
则将 example.class 反编译,反编译得到的源代码保存为 example.jad 。如果 example.jad 已经
存在,会提示是否覆盖。使用命令选项 -o 可跳过提示。
( 2 ) 使用通配符
JAD 支持通配符,如可输入:
Jad *.class
则将当前目录所有字节码文件进行反编译。
( 3 ) 指定输出形式
如果希望反编译结果以 .java 为后缀,可输入
jad -sjavaexample.class
但此时要注意不要覆盖了原有的源代码,尤其加上 -o 选项后,覆盖原有文件不会出现提示,
更应小心。类似地,可以指定任意后缀。
使用 -p 选项可以将反编译结果输出到屏幕,如
jad –p example.class
进一步可将其重定向到任意文件名,如
jad –p example.class >my.java
使用“ -d 目录名”选项可指定反编译后的输出文件的目录,若没有目录会自动创建。
如
jad -o -dtest -sjava*.class
或
jad -o -dtest -s java *.class
将当前目录所有字节码文件反编译到 test 目录。
( 4 ) 设置输出内容
使用 -a 选项则可以使用 JVM 字节码注释输出结果。使用 -stat 选项可以最后统计类、方法、成员变量等的数量。
<!-- /* Font Definitions */ @font-face {font-family:宋体; panose-1:2 1 6 0 3 1 1 1 1 1; mso-font-alt:SimSun; mso-font-charset:134; mso-generic-font-family:auto; mso-font-pitch:variable; mso-font-signature:3 680460288 22 0 262145 0;} @font-face {font-family:楷体_GB2312; mso-font-alt:微软雅黑; mso-font-charset:134; mso-generic-font-family:modern; mso-font-pitch:fixed; mso-font-signature:1 135135232 16 0 262144 0;} @font-face {font-family:幼圆; mso-font-alt:微软雅黑; mso-font-charset:134; mso-generic-font-family:modern; mso-font-pitch:fixed; mso-font-signature:1 135135232 16 0 262144 0;} @font-face {font-family:"/@宋体"; panose-1:2 1 6 0 3 1 1 1 1 1; mso-font-charset:134; mso-generic-font-family:auto; mso-font-pitch:variable; mso-font-signature:3 680460288 22 0 262145 0;} @font-face {font-family:"/@幼圆"; mso-font-charset:134; mso-generic-font-family:modern; mso-font-pitch:fixed; mso-font-signature:1 135135232 16 0 262144 0;} @font-face {font-family:"/@楷体_GB2312"; mso-font-charset:134; mso-generic-font-family:modern; mso-font-pitch:fixed; mso-font-signature:1 135135232 16 0 262144 0;} /* Style Definitions */ p.MsoNormal, li.MsoNormal, div.MsoNormal {mso-style-parent:""; margin:0in; margin-bottom:.0001pt; text-align:justify; text-justify:inter-ideograph; mso-pagination:none; font-size:10.5pt; mso-bidi-font-size:10.0pt; font-family:"Times New Roman"; mso-fareast-font-family:宋体; mso-font-kerning:1.0pt;} h2 {mso-style-next:Normal; margin-top:24.0pt; margin-right:0in; margin-bottom:24.0pt; margin-left:0in; text-align:justify; text-justify:inter-ideograph; text-indent:21.0pt; mso-line-height-alt:15.7pt; mso-pagination:lines-together; page-break-after:avoid; mso-outline-level:2; mso-layout-grid-align:none; font-size:18.0pt; mso-bidi-font-size:10.0pt; font-family:"Times New Roman"; mso-fareast-font-family:幼圆; mso-bidi-font-weight:normal;} p.MsoBodyTextIndent, li.MsoBodyTextIndent, div.MsoBodyTextIndent {margin-top:0in; margin-right:0in; margin-bottom:0in; margin-left:42.0pt; margin-bottom:.0001pt; mso-para-margin-top:0in; mso-para-margin-right:0in; mso-para-margin-bottom:0in; mso-para-margin-left:4.0gd; mso-para-margin-bottom:.0001pt; text-align:justify; text-justify:inter-ideograph; mso-pagination:none; font-size:10.5pt; mso-bidi-font-size:10.0pt; font-family:"Times New Roman"; mso-fareast-font-family:宋体; mso-font-kerning:1.0pt;} @page Section1 {size:8.5in 11.0in; margin:1.0in 1.25in 1.0in 1.25in; mso-header-margin:.5in; mso-footer-margin:.5in; mso-paper-source:0;} div.Section1 {page:Section1;} -->
1.3混淆器的安装
本书使用的混淆器为 Marvin Obfuscator 1.2b ,可在 http://www.drjava.de/obfuscator/下载,将其直接解压缩到某个目录即可。
该混淆器使用 Java开发,提供了一个 jar文件。为了便于运行程序, Marvin Obfuscator 1.2b提供了一个批处理文件 obfuscate.bat。其内容如下:
@echo off
set JAVALIB=c:/java/jdk1.3/jre/lib/rt.jar
java -cp marvinobfuscator.jar;%JAVALIB% drjava.marvin.Obfuscate %1 %2 %3 %4 %5 %6 %7 %8 %9
该批处理针对的是 JDK1.3,对于本书的环境需要将 JAVALIB环境变量作些修改,将新增的类库加入 JAVALIB,如可改为:
set JAVALIB=C:/j2sdk1.4.0/jre/lib/rt.jar;C:/j2sdk1.4.0/jre/lib/jce.jar
混淆器使用时的配置放在 marvinobfuscator1_2b安装目录的 dummyproject子目录下的 config.txt文件中。需要混淆的类要先用 jar打包,如打包为 My.jar,然后需要将 config.txt开头几行的 "somelibrary.jar"改为需要进行混淆操作的打包文件名称“ My.jar”。此外,需要将 config.txt中“ mainClasses=(("myapp.Main"))”改为 My.jar文件中包含 main( )方法的字节码文件,如改为“ mainClasses=(("xx.class"))”。然后将该 config.txt文件保存在 My.jar所在目录中。
以后就可以执行
obfuscate 目标目录 混淆以后的文件名
进行混淆操作了。
加密是保护重要数据以及程序之间进行秘密通信的重要方法,随着加密的广泛应用,已发展成为一门单独的学科:密码( Cryptography )。密码学这一单词来自希腊语 Kryptus (隐藏)和 Gr á phein (写),可见通过加密将信息隐藏起来是密码学的重要内容。 Java 中提供了常用的加密和解密算法,该部分将介绍如何使用这些已有的算法。
该部分主要内容:
1. 通过编写凯撒密码了解加密和解密的基本过程
2. 创建对称密钥,使用对称密钥进行加密和解密
3. 创建非对称密钥,使用非对称密钥进行加密和解密
4. 使用密钥协定分发密钥
2.1 一个简单的加密和解密程序——凯撒密码
说明:
凯撒密码是罗马扩张时期朱利斯• 凯撒( Julius Caesar )创造的,用于加密通过信使传递的作战命令。它将字母表中的字母移动一定位置而实现加密。例如如果向右移动 2 位,则字母 A 将变为 C ,字母 B 将变为 D , … ,字母 X 变成 Z ,字母 Y 则变为 A ,字母 Z 变为 B 。
因此,假如有个明文字符串“ Hello ”用这种方法加密的话,将变为密文:“ Jgnnq ”。而如果要解密,则只要将字母向相反方向移动同样位数即可。如密文“ Jgnnq ”每个字母左移两位变为“ Hello ”。这里,移动的位数“ 2 ”是加密和解密所用的密钥。本实例通过 Java 实现了这一过程,由此可以了解与加密和解密相关的概念。
编程思路:
首先获取要加密的内容以及密钥,凯撒密码的密钥即字符移动的位数。由于凯撒密码器的移位是针对字符的,因此需要将待加密的内容中每个字符取出,然后针对每个字符分别加以移位。
//文件 Caesar.java
public class Caesar
{//凯撒密码 ,移动的位数 2就是加密和解密所用的密钥 !!
public static void main(String args[]) throws Exception{
String s=args[0];//读取要加密的字符串 (明文 )
int key=Integer.parseInt(args[1]);//读取密钥 (移动的位数 ,负数表示向左移动,正数表示向右移动 )
String es="";
for(int i=0;i<s.length( );i++)
{//取出字符串中每个字符
char c=s.charAt(i);
if(c>='a' && c<='z')
{//对每个字符进行移位,是小写字母
c+=key%26;//移动 key%26位
if(c<'a') c+=26;//向左越界
if(c>'z') c-=26;//向右越界
}
else if(c>='A' && c<='Z')
{//大写字母
c+=key%26;
if(c<'A') c+=26;
if(c>'Z') c-=26;
}
es+=c;
}
System.out.println(es);//输出密文
}
}
该程序既可用于加密又可用于解密。
编译: javac Caesar.java
只要执行:
java Caesar 明文(要加密的字符串) 密钥(移动的位数)
即可加密。在密钥前面加上负号,将运行
java Caesar 明文(要加密的字符串) - 密钥(移动的位数)即可解密。
如为了加密字符串“ Hello World! ”,可随意取一个密钥如 4 ,运行:
java Caesar "Hello World!" 4
将输出“ Lipps Asvph! ”。这里“ Hello World! ”是明文,“ Lipps Asvph! ”是密文。如果密钥大于 26 ,程序中移位前会和 26 取模而将其调整到 26 以下。因此运行:
java Caesar "Hello World!" 30
同样将输出“ Lipps Asvph! ”。
为了将密文“ Lipps Asvph! ”解密,需要知道加密该密文所用的密钥 4 ,这样,执行:
java Caesar "Lipps Asvph!" -4
将得到明文“ Hello World! ”。
如果密钥和加密时所用的不同,则解密时将得到无意义的输出,如运行
java Caesar "Lipps Asvph!" –3
程序将输出“ Ifmmp Xpsme! ”。这样,只有知道密钥才能得到原来的密文。
2.2 对称密钥的生成和保存
上节的凯撒密码是很脆弱的,密钥总共只有 26 个,攻击者得到密文后即使不知道密钥,也可一个一个地试过去,最多试 26 次就可以得到明文。现代密码算法的过程要复杂得多,其中一类和凯撒密码类似,加密和解密使用相同的密钥,称为对称密钥算法;另一类则在加密时使用一种密钥,在解密时使用另一种密钥,称为非对称密钥算法。这些算法的密钥也不再是简单的整数,而是很长的二进制数。这样,一个 56 位的密钥有 2^(56)( 即 72,057,594,037,927,936 )个不同的可能取值,这需要耗费超级计算机约一天的时间尝试每一个密钥。当密钥长度达到 128 位,则密钥数量达到 2^(128) 个,需要的时间增加到 272 倍,约 1.29 × 1019 年。 Java 中已经提供了常用的加密算法,我们不需要了解算法的细节而可以直接使用这些算法实现加密。各种算法所用的密钥各有不同,本节将学习 Java 中创建对称密钥的方法。
2.2.1 对称密钥的生成及以对象序列化方式保存
实例说明
本实例给出 Java 中创建对称密钥的步骤,并通过对象序列化方式保存在文件中。
编程思路:
( 1 ) 获取密钥生成器
KeyGenerator kg=KeyGenerator.getInstance("DESede");
分析: Java 中 KeyGenerator 类中提供了创建对称密钥的方法。 Java 中的类一般使用 new 操作符通过构造器创建对象,但 KeyGenerator 类不是这样,它预定义了一个静态方法 getInstance( ),通过它获得 KeyGenerator 类型的对象。这种类成为工厂类或工厂。方法 getInstance( )的参数为字符串类型,指定加密算法的名称。可以是“ Blowfish ” 、“ DES”、“ DESede”、“ HmacMD5”或“ HmacSHA1”等。这些算法都可以实现加密,这里我们不关心这些算法的细节,只要知道其使用上的特点即可。其中“ DES”是目前最常用的对称加密算法,但安全性较差。针对 DES 安全性的改进产生了能满足当前安全需要的 TripleDES 算法,即“ DESede”。“ Blowfish”的密钥长度可达 448 位,安全性很好。“ AES”是一种替代 DES 算法的新算法,可提供很好的安全性。
( 2 ) 初始化密钥生成器
kg.init(168);
分析:该步骤一般指定密钥的长度。如果该步骤省略的话,会根据算法自动使用默认的密钥长度。指定长度时,若第一步密钥生成器使用的是“ DES”算法,则密钥长度必须是 56 位;若是“ DESede”,则可以是 112 或 168 位,其中 112 位有效;若是“ AES”,可以是 128, 192 或 256 位;若是“ Blowfish”,则可以是 32 至 448 之间可以被 8 整除
的数;“ HmacMD5”和“ HmacSHA1”默认的密钥长度都是 64 个字节。
( 3 ) 生成密钥
SecretKey k=kg.generateKey( );
分析:使用第一步获得的 KeyGenerator 类型的对象中 generateKey( )方法可以获得密钥。其类型为 SecretKey 类型,可用于以后的加密和解密。
( 4 ) 通过对象序列化方式将密钥保存在文件中
FileOutputStream f=new FileOutputStream("key1.dat");
ObjectOutputStream b=new ObjectOutputStream(f);
b.writeObject(k);
分析: ObjectOutputStream 类中提供的 writeObject 方法可以将对象序列化,以流的方式进行处理。这里将文件输出流作为参数传递给 ObjectOutputStream 类的构造器,这样创建好的密钥将保存在文件 key1.data 中。
//文件: Skey_DES.java
import java.io.*;
import javax.crypto.*;
public class Skey_DES
{//对称密钥的生成,并通过对象序列化方式保存在文件中
public static void main(String args[]) throws Exception
{
KeyGenerator kg=KeyGenerator.getInstance("DESede");//创建密钥生成器
kg.init(168);//初始化密钥生成器
SecretKey k=kg.generateKey( );//生成密钥
//通过对象序列化方式将密钥保存在文件中
FileOutputStream f=new FileOutputStream("key1.dat");
ObjectOutputStream b=new ObjectOutputStream(f);
b.writeObject(k);
}
}
编译 javac Skey_DES.java 运行 java Skey_DES ,在当前目录下将生成文件 key1.dat ,其中包含的密钥可以用于使
用 Triple DES 算法的加密和解密。
2.2.2 以字节保存对称密钥
实例说明
2.2.1 小节的实例将密钥通过对象序列化方式保存在文件中,在文件中保存的是对象,本实例以另一种方式保存在文件中,即以字节保存在文件中。
编程思路:
Java 中所有的密钥类都有一个 getEncoded( ) 方法,通过它可以从密钥对象中获取主要编码格式,其返回值是字节数组。其主要步骤为:
( 1 ) 获取密钥
FileInputStream f=new FileInputStream("key1.dat");
ObjectInputStream b=new ObjectInputStream(f);
Key k=(Key)b.readObject( );
分析:该步骤与 2.2.1 小节的第 4 步是相对应的, 2.2.1 小节的第 4 步将密钥对象以对象流的方式存入文件,而这一步则将文件中保存的对象读取出来以便使用。首先创建文件输入流,然后将其作为参数传递给对象输入流,最后执行对象输入流的 readObject( )方法读取密钥对象。由于 readObject( )返回的是 Object 类型,因此需要强制转换成 Key 类型。这里使用的是已有的密钥,也可以不使用这里的三行代码,而使用 2.1.1 小节中的前三步的代码生成新的密钥再继续下面的步骤。
( 2 ) 获取主要编码格式
byte[ ] kb=k.getEncoded( );
分析:执行 SecretKey 类型的对象 k 的 getEncoded( )方法,返回的编码放在 byte类型的数组中。
( 3 ) 保存密钥编码格式
FileOutputStream f2=new FileOutputStream("keykb1.dat");
f2.write(kb);
分析:先创建文件输出流对象,在其参数中指定文件名,如 keykb1.dat。然后执行文件输出流的 write( )方法将第 2 步中得到的字节数组中的内容写入文件。
//文件: Skey_kb.java
import java.io.*;
import java.security.*;
public class Skey_kb
{// 以字节方式保存密钥。程序中在保存了密钥编码后,又使用循环语句将字节数组中的内容
// 打印出来。这样就可以较为直观地看到密钥编码的内容了。
public static void main(String args[]) throws Exception
{
FileInputStream f=new FileInputStream("key1.dat");// 首先获取密钥
ObjectInputStream b=new ObjectInputStream(f);
Key k=(Key)b.readObject( );
byte[ ] kb=k.getEncoded( );// 获取主要编码格式
// 保存密钥编码格式
FileOutputStream f2=new FileOutputStream("keykb1.dat");
f2.write(kb);
// 打印密钥编码中的内容
for(int i=0;i<kb.length;i++)
{
System.out.print(kb[i]+",");
}
}
}
程序中在保存了密钥编码后,又使用循环语句将字节数组中的内容打印出来。这样可以较为直观地看到密钥编码的内容。
运行程序:
编译: javac Skey_kb.java 输入 java Skey_kb 运行程序,在程序的当前目录中将产生文件名为 keykb1.dat 的文件,屏幕输出如下:
11,-105,-119,50,4,-105,16,38,-14,-111,21,-95,70,-15,76,-74,67,-88,59,-71,55,-125,104,42,
此即程序中创建的密钥的编码内容,如果用文本编辑器打开 keykb1.dat ,看到的不是上面的数字而是类似下面的字符:
棄 2 ?& 驊 馤禖 ?? 僪 *
这是因为 keykb1.dat 是一个二进制文件,存放的是任意二进制数。读者运行时肯定结果和上面会有所不同,实际上 2.2.1 小节的程序每次运行时生成的密钥都不会相同,这就保证了密钥的唯一性。作为对称密钥,只要保证若加密某段文字用的是某个密钥,则解密这段密文时用同样的密钥即可。
2.3 使用对称密钥进行加密和解密
在 2.2 节学习了如何创建对称密钥,本节介绍如何使用创建好的对称密钥进行加密和解密。
2.3.1 使用对称密钥进行加密
实例说明
本实例的输入是 2.2.1 小节中生成并以对象方式保存在文件 key1.dat 中的密钥,以及需要加密的一段最简单的字符串 "Hello World!" ,使用密钥对 "Hello World!" 进行加密,加密后的信息保存在文件中。在此基础上读者可以举一反三加密各种信息。
编程思路:
首先要从文件中获取已经生成的密钥,然后考虑如何使用密钥进行加密。这涉及到各种算法。 Java 中已经提供了常用的加密算法,我们执行 Java 中 Cipher 类的各个方法就可以完成加密过程,其主要步骤为:
( 1 ) 从文件中获取密钥
FileInputStream f=new FileInputStream("key1.dat");
ObjectInputStream b=new ObjectInputStream(f);
Key k=(Key)b.readObject( );
分析:该步骤与 2.2.2 小节的第 1 步相同。
( 2 ) 创建密码器( Cipher 对象)
Cipher cp=Cipher.getInstance("DESede");
分析:和 2.2.1 小节的第 1 步中介绍的 KeyGenerator 类一样, Cipher 类是一个工厂类,它不是通过 new 方法创建对象,而是通过其中预定义的一个静态方法 getInstance()获取 Cipher 对象。 getInstance( )方法的参数是一个字符串,该字符串给出 Cipher 对象应该执行
哪些操作,因此把传入的字符串称为转换( transformation)。通常通过它指定加密算法或解密所用的算法的名字,如本例的 "DESede"。此外还可以同时指定反馈模式及填充方案等,如 "DESede/ECB/PKCS5Padding"。反馈模式及填充方案的概念和用途将在后面介绍。
( 3 ) 初始化密码器
cp.init(Cipher.ENCRYPT_MODE, k);
分析:该步骤执行 Cipher 对象的 init()方法对 Cipher 对象进行初始化。该方法包括两个参数, 第一个参数指定密码器准备进行加密还是解密,若传入 Cipher.ENCRYPT_MODE 则进入加密模式。第二个参数则传入加密或解密所使用的密钥,即第 1 步从文件中读取的密钥对象 k。
( 4 ) 获取等待加密的明文
String s="Hello World!";
byte ptext[]=s.getBytes("UTF8");
分析: Cipher 对象所作的操作是针对字节数组的,因此需要将要加密的内容转换成字节数组。本例中要加密的是一个字符串 s,可以使用字符串的 getBytes( )方法获得对应的字节数组。 getBytes( )方法中必须使用参数 "UTF8"指定 … ,否则 …
( 5 ) 执行加密
byte ctext[]=cp.doFinal(ptext);
分析:执行 Cipher 对象的 doFinal( )方法,该方法的参数中传入待加密的明文,从而按照前面几步设置的算法及各种模式对所传入的明文进行加密操作,该方法返回加密的结果。
( 6 ) 处理加密结果
FileOutputStream f2=new FileOutputStream("SEnc.dat");
f2.write(ctext);
分析:第 5 步得到的加密结果是字节数组,对其可作各种处理,如在网上传递、保存在文件中等。这里将其保存在文件 Senc.dat 中。
//文件: SEnc.java
import java.io.*;
import java.security.*;
import javax.crypto.*;
public class SEnc
{//使用对称密钥进行加密。输入是以对象方式保存在文件 key1.dat中的密钥 .对字符串 "Hello World!"
//进行加密,将加密后的信息 (密文 )保存在文件 SEnc.dat中。
public static void main(String args[]) throws Exception
{
String s="Hello World!";
//从文件中获取密钥
FileInputStream f=new FileInputStream("key1.dat");
ObjectInputStream b=new ObjectInputStream(f);
Key k=(Key)b.readObject( );
//创建密码器 (Cipher对象 )
Cipher cp=Cipher.getInstance("DESede");
cp.init(Cipher.ENCRYPT_MODE, k);//初始化密码器
byte ptext[]=s.getBytes("UTF8");//获取等待加密的明文
for(int i=0;i<ptext.length;i++)
{//输出明文
System.out.print(ptext[i]+",");
}
System.out.println("");
byte ctext[]=cp.doFinal(ptext);//执行加密
for(int i=0;i<ctext.length;i++)
{//输出加密结果
System.out.print(ctext[i] +",");
}
FileOutputStream f2=new FileOutputStream("SEnc.dat");//保存加密结果
f2.write(ctext);
}
}
程序中使用两个循环语句将字节数组加密前后加密后的内容打印出来,可作为对比。
运行程序
当前目录下必须有 2.2.1 小节中生成的密钥文件 key1.dat ,输入 java SEnc 运行程序,在程序的当前目录中将产生文件名为 SEnc.dat 的文件,屏幕输出如下:
72,101,108,108,111,32,87,111,114,108,100,33,
-57,119,0,-45,-9,23,37,-56,-60,-34,-99,105,99,113,-17,76,
其中第一行为字符串 "Hello World!" 的字节数组编码方式,第二行为加密后的内容,第二行的内容会随着密钥的不同而不同。第一行的内容没有加过密,任何人若得到第一行数据,只要将其用二进制方式写入文本文件,用文本编辑器打开文件就可以看到对应的字符串“ Hello World! ”。而第二行的内容由于是加密过的,没有密钥的人即使得到第二行的内容也无法知道其内容。密文同时保存在 SEnc.dat 文件中,将其提供给需要的人时,需要同时提供加密时使用的密钥( key1.dat ,或 keykb1.dat ),这样收到 SEnc.dat 中密文的人才能够解密文件中的内容 .
2.3.2 使用对称密钥进行解密
实例说明
有了 2.3.1 小节加密后的密文 SEnc.dat ,以及加密时所使用的密钥 key1.dat 或 keykb1.dat ,本实例对 SEnc.dat 中的密文进行解密,得到明文。
编程思路:
首先要从文件中获取加密时使用的密钥,然后考虑如何使用密钥进行解密。其主要步骤为:
( 1 ) 获取密文
FileInputStream f=new FileInputStream("SEnc.dat");
int num=f.available();
byte[ ] ctext=new byte[num];
f.read(ctext);
分析:密文存放在文件 SEnc.dat 中,由于解密是针对字节数组进行操作的,因此,要先将密文从文件中读入字节数组。首先创建文件输入流,然后使用文件输入流的 available( )方法判断密文将占用多少字节,从而创建相应大小的字节数组 ctext,最后使用文件输入流的 read( )方法一次性读入数组 ctext。如果不考虑通用性,也可将要加密的内容直接在程序中向数组赋值。如可将 2.3.1小节的第二行输出的密文用如下语句直接赋值:
byte ctext[ ]={-57,119,0,-45,-9,23,37,-56,-60,-34,-99,105,99,113,-17,76};
该句可替代上面的四条语句,只是通用性差了,只能加密这一条密文
( 2 ) 获取密钥
FileInputStream f2=new FileInputStream("keykb1.dat");
int num2=f2.available();
byte[ ] keykb=new byte[num2];
f2.read(keykb);
SecretKeySpec k=new SecretKeySpec(keykb,"DESede");
分析:获取可以和 2.3.1 小节第 1 步一样直接获取密钥,本实例使用另外一种方式获取密钥,即使用 2.2.2 小节以字节方式保存在文件 keykb1.dat 中的密钥。首先要将 keykb1.dat 中的内容读入字节数组 keykb,这里使用了和第 1 步类似的四条语句。如果不考虑通用性,也可以将 2.2.2 小节输出的信息如下直接赋值:
byte [] keykb ={11,-105,-119,50,4,-105,16,38,-14,-111,21,-95,
70,-15,76,-74,67,-88,59,-71,55,-125,104,42};
最后,使用将其作为参数传递给 SecretKeySpec 类的构造器而生成密钥。 SecretKeySpec 类的构造器中第 2 个参数则指定加密算法。由于 keykb1.dat 中的密钥原来使用的是 DESede 算法,因此这里仍旧使用字符串“ DESede”作为参数。
( 3 ) 创建密码器( Cipher 对象)
Cipher cp=Cipher.getInstance("DESede");
分析:该步骤同 2.3.1 小节的第 2 步。
( 4 ) 初始化密码器
cp.init(Cipher.DECRYPT_MODE, k);
分析:该步骤和 2.3.1 的第 3 步类似,对 Cipher 对象进行初始化。该方法包括两个参数,第一个参数传入 Cipher.ENCRYPT_MODE 进入解密模式,第二个参数则传入解密所使用的密钥。
( 5 ) 执行解密
byte []ptext=cp.doFinal(ctext);
分析:该步骤和 2.3.1 的第 5 步类似,执行 Cipher 对象的 doFinal( )方法,该方法的参数中传入密文,从而按照前面几步设置的算法及各种模式对所传入的密文进行解密操作,该方法返回解密的结果。
//文件: SDec.java
import java.io.*;
import java.security.*;
import javax.crypto.*;
import javax.crypto.spec.*;
public class SDec
{//使用对称密钥进行解密
public static void main(String args[]) throws Exception
{
/*
byte [ ] keykb ={11,-105,-119,50,4,-105,16,38,-14,-111,21,-95,
70,-15,76,-74,67,-88,59,-71,55,-125,104,42};
byte ctext[ ]={-57,119,0,-45,-9,23,37,-56,-60,-34,-99,105,99,113,-17,76};
*/
FileInputStream f=new FileInputStream("SEnc.dat");//获取密文
int num=f.available();
byte[ ] ctext=new byte[num];//解密是针对字节数组进行操作的,因此要先将密文从文件中
//读入字节数组。
f.read(ctext);
//获取密钥
FileInputStream f2=new FileInputStream("keykb1.dat");
int num2=f2.available();
byte[ ] keykb=new byte[num2];
f2.read(keykb);
SecretKeySpec k=new SecretKeySpec(keykb,"DESede");
//创建密码器 (Cipher对象 )
Cipher cp=Cipher.getInstance("DESede");
cp.init(Cipher.DECRYPT_MODE, k);//初始化密码器
byte []ptext=cp.doFinal(ctext);//执行解密
String p=new String(ptext,"UTF8");//执行 Cipher对象的 doFinal()方法,该方法的参数中传入密文 ,
//从而按照前面几步设置的算法及各种模式对所传入的密文进行解密操作,该方法返回
//解密的结果,及返回明文。
System.out.println(p);//输出明文 ,将明文生成字符串加以显示。
}
}
程序中最后将明文生成字符串加以显示。
运行程序
当前目录下必须有 2.2.2 小节中生成的密钥文件 keykb1.dat ,以及 2.3.1 小节的密文文件 SEnc.dat 。输入 java SDec 运行程序,将输出明文字符串“ Hello World! ”。
2.4 基于口令的加密和解密
使用对称密钥加密时密钥都很长, 如 2.2.2 小节的密钥对应的字节序列为“ 11,-105,-119,50,4,-105,16,38,-14,-111,21,-95,70,-15,76,-74,67,-88,59,-71,55,-125,104,42 ”,很难记住。一种做法是像 2.2.2 小节那样把它保存在文件中,需要时读取文件,其缺点容易被窃取,携带也不方便;另一种做法是将其打印出来,需要时对照打印出的内容手工一个一个
输入,但由于密钥很长,输入很麻烦。在实际使用中,更常见的是基于口令的加密。加密时输入口令,口令可以由使用者自己确定一个容易记忆的。解密时只有输入同样的口令才能够得到明文。本节通过两个最简单的例子说明其基本用法。
2.4.1 基于口令的加密
实例说明
本实例通过使用口令加密的一段最简单的字符串 "Hello World!" ,加密后的信息保存在文件中。在此基础上读者可以举一反三加密各种信息
编程思路:
和 2.3 节一样,基于口令的加密也是使用 Java 的 Cipher 类,只是在加密算法中使用基于口令的加密算法。此外,加密时所用的密钥是根据给定的口令生成的。为了增加破解的难度, PBE 还使用一个随机数(称为盐)和口令组合起来加密文件。此外还进行重复计算(迭
代)。编程的主要步骤如下:
( 1 ) 读取口令
char[] passwd=args[0].toCharArray( );
PBEKeySpec pbks=new PBEKeySpec(passwd);
分析:本实例通过命令行参数读取口令。为了后面步骤可以由口令生成密钥,需要将口令保存在类 PBEKeySpec 中,类 PBEKeySpec 的构造器传入的参数是字符数组,所以使用了字符串的 toCharArray( )方法生成字符数组。
( 2 ) 由口令生成密钥
SecretKeyFactory kf=SecretKeyFactory.getInstance("PBEWithMD5AndDES");
SecretKey k=kf.generateSecret(pbks);
分析:生成密钥可通过 SecretKeyFactory 类的 generateSecret( )方法实现,只要将存有口令的 PBEKeySpec 对象作为参数传递给 generateSecret( )方法方法即可。 SecretKeyFactory 类是一个工厂类,通过预定义的一个静态方法 getInstance()获取
SecretKeyFactory 对象。 getInstance ( ) 方法的参数是一个字符串, 指定口令加密算法,如 PBEWithMD5AndDES , PBEWithHmacSHA1AndDESede 等。 JCE 中已经实现的是 PBEWithMD5AndDES。
( 3 ) 生成随机数(盐)
byte[] salt=new byte[8];
Random r=new Random( );
r.nextBytes(salt);
分析:对于 PBEWithMD5AndDES 算法,盐必须是 8 个元素的字节数组,因此创建数组 salt。 Java 中 Random 类可以生成随机数,执行其 nextBytes( )方法,方法的参数为 salt,即可生成的随机数并将随机数赋值给 salt。
( 4 ) 创建并初始化密码器
Cipher cp=Cipher.getInstance("PBEWithMD5AndDES");
PBEParameterSpec ps=new PBEParameterSpec(salt,1000);
cp.init(Cipher.ENCRYPT_MODE, k,ps);
分析:和以前一样通过 getIntance( )方法获得密码器,其中的参数使用基于口令的加密算法“ PBEWithMD5AndDES”。但在执行 init( )初始化密码器时,除了指定第 2步生成的口令密钥外,还需要指定基于口令加密的参数,这些参数包括为了提高破解难度而添加的随机数(盐),以及进行迭代计算次数。只要将盐和迭代次数都作为参数传
递给 PBEParameterSpec 类的构造器即可。
( 5 ) 获取明文,执行加密
byte ptext[]=s.getBytes("UTF8");
byte ctext[]=cp.doFinal(ptext);
分析:和以前一样将字符串转换为字节数组,并执行密码器的 doFinal( )方法进行加密。加密结果保存在字节数组 ctext 中。
//文件: PBEEnc.java
import java.io.*;
import java.util.*;
import java.security.*;
import javax.crypto.*;
import javax.crypto.spec.*;
/*
基于口令的加密和解密:之前使用对称密钥加密时密钥都很长,很难记住。一种做法是将密钥保存在文件中,需要时读取文件,其缺点容易被窃取,携带也不方便。另一种做法是将其打印出来,需要时对照
打印出的内容手工一个一个地输入,但由于密钥很长,输入很麻烦。在实际使用中,更常见的是基于口令的加密。加密时输入口令,口令可以由使用者自己确定一个容易记忆的。解密时只有输入同样的口令
才能够得到明文。基于口令的加密也是使用 Java的 Ciphe类!!!!!只是在加密算法中使用基于口令的加密算法。此外,加密时所用的密钥是根据给定的口令生成的。为了增加破解的难度, PBE还使用一个随机数 (称为盐 )和口令组合起来加密文件。
*/
public class PBEEnc
{//基于口令的加密的密文由两部分组成,一个是盐,一个是加密结果
public static void main(String args[]) throws Exception
{
String s="Hello World!";
char[] passwd=args[0].toCharArray( );//读取口令
//由口令生成密钥
PBEKeySpec pbks=new PBEKeySpec(passwd);
SecretKeyFactory kf=SecretKeyFactory.getInstance("PBEWithMD5AndDES");
SecretKey k=kf.generateSecret(pbks);
//生成随机数 (盐 )
byte[] salt=new byte[8];
Random r=new Random( );
r.nextBytes(salt);
//创建并初始化密码器
Cipher cp=Cipher.getInstance("PBEWithMD5AndDES");
PBEParameterSpec ps=new PBEParameterSpec(salt,1000);
cp.init(Cipher.ENCRYPT_MODE, k,ps);
//获取明文,执行加密
byte ptext[]=s.getBytes("UTF8");
byte ctext[]=cp.doFinal(ptext);
// 将盐和加密结果合并在一起保存为密文
FileOutputStream f=new FileOutputStream("PBEEnc.dat");
f.write(salt);
f.write(ctext);
// 打印盐的值
System.out.println("盐的值: ");
for(int i=0;i<salt.length;i++)
{
System.out.print(salt[i] +",");
}
System.out.println("");
// 打印加密结果
System.out.println("加密结果: ");
for(int i=0;i<ctext.length;i++)
{
System.out.print(ctext[i] +",");
}
}
}
//程序运行后,在当前目录下将创建一个文件 PBEEnc.dat,该文件中存放的是密文。
//其中前 8个字节是盐,剩余部分是加密结果。
基于口令的加密的密文由两部分组成,一个是盐,一个是加密结果,两个值简单地合并起来即可,本程序中将其一起写入密文文件 PBEEnc.dat 。程序最后将盐和加密结果打印出来。
运行程序
输入 java PBEEnc s7es1.886 来运行程序,其中命令行参数 s7es1.886 为用户选择的用于加密的口令。将输出:
76,26,126,-117,12,-98,-112,95,
113,-56,-69,66,-101,-1,-12,-109,90,-85,-99,66,-80,-10,-84,-77,
其中第一行 8 个数字对应的是盐的值,第二行为加密结果。由于程序每次运行时使用的盐的值不同,因此即使程序运行时每次使用的口令相同,加密后的结果也不一样。程序运行后当前目录下将创建一个文件 PBEEnc.dat ,该文件中存放的是密文。其中前 8 个字节是盐,剩余部分是加密结果。
2.4.2 基于口令的解密
实例说明
本实例的输入 2.4.1 小节的存放密文的文件 PBEEnc.dat ,以及该文件的密文所使用的口令“ s7es1.886 ”。本实例将演示如何使用该口令对密文解密。
编程思路:
和加密时一样,基于口令的解密也是使用 Java 的 Cipher 类,只是初始化时传入的参数使用 Cipher.DECRYPT_MODE 。此外,由于密文中既包含盐也包含加密结果,因此需要将这两部分分离出来。此外,加密时所用的密钥是根据给定的口令生成的。为了增加破解的难度, PBE 还使用一个随机数(称为盐)和口令组合起来加密文件。此外还进行重复计算(迭代)。编程的主要步骤如下:
( 1 ) 读取口令并生成密钥
char[] passwd=args[0].toCharArray( );
PBEKeySpec pbks=new PBEKeySpec(passwd);
SecretKeyFactory kf=
SecretKeyFactory.getInstance("PBEWithMD5AndDES");
SecretKey k=kf.generateSecret(pbks);
分析:该步骤和加密时完全相同。
( 2 ) 获取随机数(盐)
byte[] salt=new byte[8];
FileInputStream f=new FileInputStream("PBEEnc.dat");
f.read(salt);
分析:由于盐的长度固定,为 8 个字节,因此定义大小为 8 的字节数组,从文件 PBEEnc.dat 中读取盐,存放在数组 salt 中。
( 3 ) 获取加密结果
int num=f.available();
byte[ ] ctext=new byte[num];
f.read(ctext);
分析:由于 PBEEnc.dat 中剩余部分为加密结果,因此使用文件输入流的 available( )方法判断剩余字节的数量,并创建相应大小的字节数组,读入数据。
( 4 ) 创建密码器,执行解密
Cipher cp=Cipher.getInstance("PBEWithMD5AndDES");
PBEParameterSpec ps=new PBEParameterSpec(salt,1000);
cp.init(Cipher.DECRYPT_MODE, k,ps);
byte ptext[]=cp.doFinal(ctext);
分析:该步骤和加密时类似,只是初始化时使用的是 Cipher.DECRYPT_MODE,执行 doFinal( )时传入的是以前的加密结果,而返回的字节数组 ptext 即包含了解密后的文字。
//文件: PBEDec.java
import java.io.*;
import java.util.*;
import java.security.*;
import javax.crypto.*;
import javax.crypto.spec.*;
/*
输入是存放密文的文件 PBEEnc.dat,以及该文件的密文所使用的口令 "s7es1.886".
编程思路:和加密时一样,基于口令的解密也是使用 Java的 Cipher类,只是初始化时传入的参数使用
Cipher.DECRYPT_MODE。此外,由于密文中既包含盐也包含加密结果,
因此需要将这两部分分离出来。
*/
public class PBEDec
{
public static void main(String args[]) throws Exception
{
//读取口令并生成密钥,该步骤和加密时完全相同。
char[] passwd=args[0].toCharArray( );
PBEKeySpec pbks=new PBEKeySpec(passwd);
SecretKeyFactory kf=SecretKeyFactory.getInstance("PBEWithMD5AndDES");
SecretKey k=kf.generateSecret(pbks);
//获取随机数 (盐 )
byte[] salt=new byte[8];
FileInputStream f=new FileInputStream("PBEEnc.dat");
f.read(salt);
//获取加密结果
int num=f.available();
byte[ ] ctext=new byte[num];
f.read(ctext);
//创建密码器,执行解密
Cipher cp=Cipher.getInstance("PBEWithMD5AndDES");
PBEParameterSpec ps=new PBEParameterSpec(salt,1000);
cp.init(Cipher.DECRYPT_MODE, k,ps);
byte ptext[]=cp.doFinal(ctext);
// 显示解密结果
System.out.println("显示解密结果: ");
for(int i=0;i<ptext.length;i++){
System.out.print(ptext[i] +",");
}
System.out.println("");
System.out.println("以字符串格式显示解密结果: ");
// 以字符串格式显示解密结果
for(int i=0;i<ptext.length;i++){
System.out.print((char) ptext[i]);
}
}
}
程序最后将解密后的得到的字节数组 ptext 中的内容打印出来,为了使显示出的结果更
加直观,最后将字节数组 ptext 中的内容转换字符进行显示。
运行程序
输入 java PBEDec s7es1.886 来运行程序,其中命令行参数 s7es1.886 是解密所使用的口令,必须和加密时使用的口令一样。程序将输出:
72,101,108,108,111,32,87,111,114,108,100,33,Hello World!
如果使用的口令不对,将无法解密。如输入 java PBEDec s7es1.888 运行程序,将显示如下异常信息:
Exception in thread "main" javax.crypto.BadPaddingException: Given final block not properly padded
at com.sun.crypto.provider.DESCipher.engineDoFinal(DashoA6275)
at com.sun.crypto.provider.DESCipher.engineDoFinal(DashoA6275)
at com.sun.crypto.provider.PBEWithMD5AndDESCipher.engineDoFinal(DashoA6275)
at javax.crypto.Cipher.doFinal(DashoA6275)
at PBEDec.main(PBEDec.java:27)
<!-- /* Font Definitions */ @font-face {font-family:宋体; panose-1:2 1 6 0 3 1 1 1 1 1; mso-font-alt:SimSun; mso-font-charset:134; mso-generic-font-family:auto; mso-font-pitch:variable; mso-font-signature:3 680460288 22 0 262145 0;} @font-face {font-family:幼圆; panose-1:0 0 0 0 0 0 0 0 0 0; mso-font-alt:宋体; mso-font-charset:134; mso-generic-font-family:auto; mso-font-format:other; mso-font-pitch:auto; mso-font-signature:1 135135232 16 0 262144 0;} @font-face {font-family:"/@宋体"; panose-1:2 1 6 0 3 1 1 1 1 1; mso-font-charset:134; mso-generic-font-family:auto; mso-font-pitch:variable; mso-font-signature:3 680460288 22 0 262145 0;} @font-face {font-family:楷体_GB2312; panose-1:0 0 0 0 0 0 0 0 0 0; mso-font-alt:宋体; mso-font-charset:134; mso-generic-font-family:auto; mso-font-format:other; mso-font-pitch:auto; mso-font-signature:1 135135232 16 0 262144 0;} @font-face {font-family:"/@幼圆"; panose-1:0 0 0 0 0 0 0 0 0 0; mso-font-charset:134; mso-generic-font-family:auto; mso-font-format:other; mso-font-pitch:auto; mso-font-signature:1 135135232 16 0 262144 0;} @font-face {font-family:"/@楷体_GB2312"; panose-1:0 0 0 0 0 0 0 0 0 0; mso-font-charset:134; mso-generic-font-family:auto; mso-font-format:other; mso-font-pitch:auto; mso-font-signature:1 135135232 16 0 262144 0;} /* Style Definitions */ p.MsoNormal, li.MsoNormal, div.MsoNormal {mso-style-parent:""; margin:0cm; margin-bottom:.0001pt; mso-pagination:widow-orphan; font-size:12.0pt; font-family:"Times New Roman"; mso-fareast-font-family:宋体;} @page Section1 {size:612.0pt 792.0pt; margin:72.0pt 90.0pt 72.0pt 90.0pt; mso-header-margin:36.0pt; mso-footer-margin:36.0pt; mso-paper-source:0;} div.Section1 {page:Section1;} -->
2.5 针对流的加密和解密
2.2 和 2.3 节的加密和解密都是针对字节数组进行的,但实际编程中更常针对流进行加密,如对整个文件进行加密 /解密或对网络通信进行加密 /解密等。尽管我们可以先从流中读出字节然后进行加密 /解密,但使用 Java 中针对流提供的专门的类更加方便。本节介绍其基本编程方法。
2.5.1 针对输入流的解密和解密
实例说明
本实例以最简单的程序演示了针对输入流的加密和解密,将指定文件中的内容进行加密和解密。
编程思路:
Java 中 CipherInputStream 提供了针对输入流的加密和解密,执行加密和解密的算法仍旧由以前使用的 Cipher 类担当, CipherInputStream 类的构造器中可以指定标准的输入流(如文件输入流)和密码器( Cipher 对象),当使用 CipherInputStream 类的 read()方法从流中读取数据时,会自动将标准输入流中的内容使用密码器进行加密或解密再读出。其基本步骤如下:
( 1 ) 生成密钥
FileInputStream f=new FileInputStream("key1.dat");
ObjectInputStream ob=new ObjectInputStream(f);
Key k=(Key)ob.readObject( );
分析:这里和 2.3.1 小节一样从文件中读取以前保存的密钥,这样保证了本实例所
用的密钥和以前相同,以便于对比加密结果。如果不需要作对比,也可以使用 2.2.1小节的步骤生成新的密钥。
( 2 ) 创建并初始化密码器
Cipher cp=Cipher.getInstance("DESede");
cp.init(Cipher.ENCRYPT_MODE, k);
分析:该步骤和以前相同,如果准备进行解密,则应将 Cipher.ENCRYPT_MODE 改为
Cipher.DECRYPT_MODE。
( 3 ) 创建要加密或解密的输入流
FileInputStream in=new FileInputStream(args[0]);
分析:这里以加密文件为例,因此创建文件输入流,文件名由命令行参数传入。
( 4 ) 创建 CipherInputStream 对象
CipherInputStream cin=new CipherInputStream(in, cp);
分析: 将第 2 步创建的密码器和第 3 步创建的需要加密 /解密的流作为参数传递给
CipherInputStream 对象。
( 5 ) 读取输入流
while( (b=cin.read()) !=-1 ){
System.out.print((byte)b+",");
}
分析:像使用基本的输入流一样使用 read( )方法从 CipherInputStream 流中读取
数据,则在读取过程中会自动根据第 2 步密码器中的设置进行加密或解密。
//文件 :StreamIn.java
import java.io.*;
import java.security.*;
import javax.crypto.*;
public class StreamIn{
public static void main(String args[]) throws Exception{
FileInputStream f=new FileInputStream("key1.dat");
ObjectInputStream ob=new ObjectInputStream(f);
Key k=(Key)ob.readObject( );
Cipher cp=Cipher.getInstance("DESede");
cp.init(Cipher.DECRYPT_MODE, k);
// cp.init(Cipher.ENCRYPT_MODE, k);
FileInputStream in=new FileInputStream(args[0]);
CipherInputStream cin=new CipherInputStream(in, cp);
int b=0;
while( (b=cin.read()) !=-1 ){
System.out.print((byte)b+",");
// System.out.print((char)b);
}
}
}
运行程序
在当前目录下使用 Windows 中的记事本创建一个文本文件: StreamIn1.txt,在其中输入需要加密的字符串,可以输入多行。为了和以前的加密结果进行对比,不妨先只输入一行“ Hello World!”。
输入 java StreamIn StreamIn1.txt 来运行程序,程序将输出加密以后的内容:
-57,119,0,-45,-9,23,37,-56,-60,-34,-99,105,99,113,-17,76,该结果和 2.3.1 小节的运行结果相同。注意,本实例和 2.3.1 小节的运行结果相同的前提是使用的密钥相同(都从 key1.dat 文件中读取),算法相同(都是 DESede 算法及默认的填充和模式)以及相同的加密内容(都是“ Hello World!”)。如果在编辑 StreamIn1.txt 文件时在“ Hello World!”后面加了回车或使用其他的文本编辑器(如使用 DOS下的 edit 工具可能会在文件末尾自动加上一些隐藏字符),则结果可能会不同。
本实例将加密的结果打印了出来,也可以再创建一个文件输出流,将加密结果保存起来,其内容将和 2.3.1 小节的 SEnc.dat 相同。将该实例稍作修改就可以对以文件形式保存的密文进行解密。如果将程序中的
cp.init(Cipher.ENCRYPT_MODE, k);
改为
cp.init(Cipher.DECRYPT_MODE, k);
则可进行解密操作。此时可将 2.3.1 小节输出的加密文件 SEnc.dat 拷贝到当前目录,运行java
StreamIn SEnc.dat , 程序将输出解密结果:
72,101,108,108,111,32,87,111,114,108,100,33,
此即“ Hello World!”字节数组编码方式。若进一步将该实例中的
System.out.print((byte)b+",");
改为
System.out.print((char)b);
则进行解密时将直接输出“ Hello World!”。
2.5.2 针对输出流的解密和解密
实例说明
本实例演示了针对输出流的加密和解密,将指定文件中的内容进行加密和解密,并把
加密和解密的结果输入指定的另外一个文件。
编程思路:
和输入流类似, Java 中 CipherOutputStream 提供了针对输出流的加密和解密。
CipherOutputStream 类的构造器中可以指定标准的输出流(如文件输出流)和密码器( Cipher对象),当使用 CipherOutputStream 类的 write()方法进行输出时,会自动将 write()方法参数中的内容使用密码器进行加密或解密后再写入标准输出流。其基本步骤如下:
( 1 ) 生成密钥
FileInputStream f=new FileInputStream("key1.dat");
ObjectInputStream ob=new ObjectInputStream(f);
Key k=(Key)ob.readObject( );
分析: 该步骤和 2.5.1 小节的第 1 步一样。
( 2 ) 创建并初始化密码器
Cipher cp=Cipher.getInstance("DESede");
if(args[0].equals("dec"))
cp.init(Cipher.DECRYPT_MODE, k);
else cp.init(Cipher.ENCRYPT_MODE, k);
分析:该步骤和 2.5.1 小节的第 2 步一样,但为了使程序更具有通用性,这里不妨
通过命令行参数确定密码器是加密模式还是解密模式。当第一个命令行参数为 enc 时,使用加密模式,否则为解密模式。
( 3 ) 获取要加密或解密的内容
FileInputStream in=new FileInputStream(args[1]);
分析:要加密或解密的内容可以是各种形式,只要可以转换为整型或字节数组形式
即可。如可以是一个字符串。本实例以加密文件为例,因此创建文件输入流,文件名由命令行的第 2 个参数传入。
( 4 ) 获取加密或解密的输出以及 CipherOutputStream 对象
FileOutputStream out=new FileOutputStream(args[2]);
CipherOutputStream cout=new CipherOutputStream(out, cp);
分析:加密和解密的结果可以输出到各种输出流中,本实例将加密结果保存为文件,因此创建文件输出流。将其和第 3 步创建的密码器一起作为参数传递给
CipherOutputStream 对象。
( 5 ) 写输出流
while( (b=in.read())!=-1){
cout.write(b);
}
分析:像使用基本的输出流一样使用 write( )方法向 CipherOutputStream 流中写
数据(数据为需要加密的明文,本实例从文件中使用 read( )方法从文件中读取明文),则在写之前 CipherOutputStream 流会自动按照其参数中的密码器设置先进行加密或解密操作,然后再写入其参数中的输出流中。本实例
//文件: StreamOut.java
import java.io.*;
import java.security.*;
import javax.crypto.*;
public class StreamOut{
public static void main(String args[]) throws Exception{
FileInputStream f=new FileInputStream("key1.dat");
ObjectInputStream ob=new ObjectInputStream(f);
Key k=(Key)ob.readObject( );
Cipher cp=Cipher.getInstance("DESede");
if(args[0].equals("dec"))
cp.init(Cipher.DECRYPT_MODE, k);
else cp.init(Cipher.ENCRYPT_MODE, k);
FileInputStream in=new FileInputStream(args[1]);
FileOutputStream out=new FileOutputStream(args[2]);
CipherOutputStream cout=new CipherOutputStream(out, cp);
int b=0;
while( (b=in.read())!=-1){
cout.write(b);
}
cout.close();
out.close();
in.close();
}
}
运行程序
仍旧使用 2.5.1 小节的文本文件: StreamIn1.txt 进行试验,输入:
java StreamOut encStreamIn1.txt mytest.txt
来运行程序,则将把 StreamIn1.txt 中的内容加密成为文件 mytest.txt。
若进一步运行 :
java StreamOut decmytest.txt mytest2.txt
则将文件 mytest.txt 中的密文解密为文件 mytest2.txt。打开 mytest2.txt,可以看到解密后的明文“ Hello World!”。解密时必须有加密时所用的完全相同的密钥才能正常运行。和 2.5.1 小节一样,被加密的文件可以不止一行。 2.5.1 和 2.5.2 小节都使用了文件输入 /输出流,也可针对其他的流进行加密和解密。此外,密码器也使用基于口令的加密和解密。
2.6 加密方式的设定
2.3.1 小节的程序加密的字符串如果是“ Hello123Hello123Hello123Hello123” (每 8 个字符相同),则加密后的结果如下:
-46,-71,65,-43,48,105,-52,-13,
-46,-71,65,-43,48,105,-52,-13,
-46,-71,65,-43,48,105,-52,-13,
-46,-71,65,-43,48,105,-52,-13,
51,82,-102,-119,76,5,60,-114,
可以看出加密结果每 8 个字节出现相同,这是因为数据在进行加密时其实不是一个一个字节进行加密,也不是一次处理加密字节,而是每 8 个字节( 64 位)作为一组进行加密,有些算法一次处理 16 个字节或更多。默认情况下,每组之间独立进行加密,因此相同的明文分组得到的加密结果也相同。 2.5.1 和 2.5.2 的例子使用密钥进行加密时,当文件 StreamIn1.txt 的内容为“ Hello123Hello123Hello123Hello123”(每 8 个字符相同),也同样具有规律性。使用其他加密方式可以解决这一问题,本节将介绍 CBC 加密方式。
2.6.1 使用CBC 方式的加密
实例说明
本实例演示了使用 CBC 加密方式以及初始向量进行加密和解密编程步骤。
编程思路:
对明文分组的不同处理方式形成了不同的加密方式,本章前面各节的程序中没有指定加密方式,默认的加密方式是 ECB( Electronic Code Book),它对每个明文分组独立进行处理。所以明文若 8 个字节一组相同的话(如本节开头的“ Hello123Hello123Hello123Hello123”),加密出的结果也是 8 个字节一组相同的。另一种加密方式称为 CBC( Cipher Block Chaining),它先加密第一个分组,然后使用得到的密文加密第二个分组,加密第二个分组得到的密文再加密第三个分组,……。这样,即使两个分组相同,得到的密文也不同。剩下的问题是如果两个密文的开头 8 个字节相同,按照这种加密方式,只要使用的密钥相同,则每条密文的开头 8 个字节也将相同。为此, CBC 使用一个 8 个字节的随机数(称为初始向量, IV)来加密第一个分组,其作用类似于基于口令加密中的盐。
因此,使用 CBC 方式首先要生成初始向量,然后在获取密码器对象时通过 getInstance( )方法的参数设定加密方式,在密码器初始化时传入初始向量。具体步骤如下:
( 1 ) 生成密钥
FileInputStream f=new FileInputStream("key1.dat");
ObjectInputStream ob=new ObjectInputStream(f);
Key k=(Key)ob.readObject( );
分析: 该步骤和以前一样。
( 2 ) 生成初始向量
byte[] rand=new byte[8];
Random r=new Random( );
r.nextBytes(rand);
IvParameterSpec iv=new IvParameterSpec(rand);
分析:该步骤前三条语句和 2.4.1 小节的第 3 步一样,生成随机数,第 4 条语句则
使用该随机数得到代表初始向量的 IvParameterSpec 对象。
( 3 ) 获取密码器
Cipher cp=Cipher.getInstance("DESede/CBC/PKCS5Padding");
分析:在获取密码器时,通过 getInstance( )方法的参数指定加密方式,该参数
“ DESede/CBC/PKCS5Padding”由三部分组成。第一部分“ DESede”代表所用的加密算法。由于本实例仍旧使用了 2.2.1 小节生成
的密钥,因此这里必须仍旧使用 DEDede 算法。若 2.2.1 小节改为其他的算法,如“ DES”、“ Blowfish”等,则这里也必须相应改变。第二部分“ CBC”即加密模式,除了 CBC 外,还有 NONE、 ECB、 CFB、 OFB 和 PCBC等可以用。第三部分为填充模式,明文在被 64 位一组分成明文分组时,最后一个分组可能不足 64 位,因此加密算法一般使用一定规则对最后一个分组进行填充。对称加密常用的填充方式称为“ PKCS#5 padding”,其中的 PKCS 是 Public Key Cryptography Standard
的缩写。如果加密算法不进行填充(填充方式为 No padding),则要求明文长度必须是 64 的整数倍。在本章前面各节的程序中没有指定填充方式,默认的填充方式就是“ PKCS#5 padding ”, 因此以前的语句 Cipher.getInstance("DESede") 和
Cipher.getInstance("DESede/ECB/PKCS5Padding")是等价的。在本节的开头介绍加密字符串“ Hello123Hello123Hello123Hello123 ” 时, 输出结果最后多出的
“ 51,82,-102,-119,76,5,60,-114,”就是由于填充的结果(使用 PKCS#5 padding 时,即使明文长度是 8 字节的整数倍,也会再数据最后加上一个完整的填充块。
( 4 ) 初始化密码器,并执行加密
cp.init(Cipher.ENCRYPT_MODE, k, iv);
byte ptext[]=s.getBytes("UTF8");
byte ctext[]=cp.doFinal(ptext);
分析:和前面的程序相比,在其参数中增加了一项初始化向量,即第 2 步得到的
iv。执行加密时同样使用 doFinal( )方法对字节数组进行加密。
//文件: SEncCBC.java
import java.io.*;
import java.util.*;
import java.security.*;
import javax.crypto.*;
import javax.crypto.spec.*;
public class SEncCBC{
public static void main(String args[]) throws Exception{
String s="Hello123Hello123Hello123Hello123";
FileInputStream f1=new FileInputStream("key1.dat");
ObjectInputStream b=new ObjectInputStream(f1);
Key k=(Key)b.readObject( );
byte[] rand=new byte[8];
Random r=new Random( );
r.nextBytes(rand);
IvParameterSpec iv=new IvParameterSpec(rand);
Cipher cp=Cipher.getInstance("DESede/CBC/PKCS5Padding");
cp.init(Cipher.ENCRYPT_MODE, k, iv);
byte ptext[]=s.getBytes("UTF8");
byte ctext[]=cp.doFinal(ptext);
for(int i=0;i<ctext.length;i++){
System.out.print(ctext[i] +",");
}
FileOutputStream f2=new FileOutputStream("SEncCBC.dat");
f2.write(rand);
f2.write(ctext);
}
}
为了方便看到加密结果,程序中通过循环打印出字节数组的内容。为了以后进行解密,程序中通过文件将初始化向量和加密结果保存在一起。
运行程序
输入 java SEncCBC 运行程序,得到如下结果:
47,-79,65,-41,25,-70,-62,-55,3,10,-3,118,-12,100,-113,2,124,-66,-84,93,-74,8,17,64,-80,-82,29,126,-23,-102,6,-98,-85,-110,-64,10,-23,-82,-30,-80,
再运行一次,得到如下结果:
118,-63,110,81,21,-99,44,-17,29,59,-121,-27,80,40,-89,-37,74,-117,-110,52,33,54,85,85,94,1
21,-122,125,29,-39,11,-71,-80,-99,-50,0,22,-50,-72,-12,
可见明文有规律性时,密文并无规律性,而且相同的明文加密后的结果不同。
密文保存在文件“ SEncCBC.dat”中,其中前 8 个字节为该密文对应的初始化向量。
2.6.2 使用CBC 方式的解密
实例说明
本实例演示了如何对 2.6.1 小节的密文进行解密。
编程思路:
同样加密一样,先要获取加密时所用的初始向量。由于 2.6.1 小节将初始化向量保存在文件 SEncCBC.dat 的开头 8 个子节中,因此可直接使用文件输入流读取。进而读取密文和密钥,最后在获取密码器对象时通过 getInstance( )方法的参数设定加密方式,在密码器初始化时传入初始向量。具体步骤如下:
( 1 ) 获取初始向量
FileInputStream f=new FileInputStream("SEncCBC.dat");
byte[] rand=new byte[8];
f.read(rand);
IvParameterSpec iv=new IvParameterSpec(rand);
分析:使用文件输入流的 read( )方法从文件 SEncCBC.dat 中读取 8 个字节的对应
初始向量的随机数,并用其创建 IvParameterSpec 对象 。
( 2 ) 获取密文和密钥
int num=f.available();
byte[ ] ctext=new byte[num];
f.read(ctext);
FileInputStream f2=new FileInputStream("key1.dat");
ObjectInputStream b=new ObjectInputStream(f2);
Key k=(Key)b.readObject( );
分析:由于 SEncCBC.dat 中剩余部分为加密结果,因此使用文件输入流的
available( )方法判断剩余字节的数量,并创建相应大小的字节数组,读入数据。密钥必须和 2.6.1 小节所用的密钥相同。
( 3 ) 获取并初始化密码器
Cipher cp=Cipher.getInstance("DESede/CBC/PKCS5Padding");
cp.init(Cipher.DECRYPT_MODE, k, iv);
byte []ptext=cp.doFinal(ctext);
分析: 该步骤和 2.6.1 小节相同, 只是在初始化密码器时使用
Cipher.DECRYPT_MODE,表明进行节密。
//文件: SDecCBC.java
import java.io.*;
import java.security.*;
import javax.crypto.*;
import javax.crypto.spec.*;
public class SDecCBC{
public static void main(String args[]) throws Exception{
FileInputStream f=new FileInputStream("SEncCBC.dat");
byte[] rand=new byte[8];
f.read(rand);
IvParameterSpec iv=new IvParameterSpec(rand);
int num=f.available();
byte[ ] ctext=new byte[num];
f.read(ctext);
FileInputStream f2=new FileInputStream("key1.dat");
ObjectInputStream b=new ObjectInputStream(f2);
Key k=(Key)b.readObject( );
Cipher cp=Cipher.getInstance("DESede/CBC/PKCS5Padding");
cp.init(Cipher.DECRYPT_MODE, k, iv);
byte []ptext=cp.doFinal(ctext);
String p=new String(ptext,"UTF8");
System.out.println(p);
}
}
程序中最后将明文生成字符串加以显示。
运行程序
输入 java SDecCBC 运行程序,得到如下结果:
Hello123Hello123Hello123Hello123
解密成功。同样,对 2.5 节中的例子也可以类似地使用 CBC 加密方式。
<!-- /* Font Definitions */ @font-face {font-family:宋体; panose-1:2 1 6 0 3 1 1 1 1 1; mso-font-alt:SimSun; mso-font-charset:134; mso-generic-font-family:auto; mso-font-pitch:variable; mso-font-signature:3 680460288 22 0 262145 0;} @font-face {font-family:幼圆; panose-1:0 0 0 0 0 0 0 0 0 0; mso-font-alt:宋体; mso-font-charset:134; mso-generic-font-family:auto; mso-font-format:other; mso-font-pitch:auto; mso-font-signature:1 135135232 16 0 262144 0;} @font-face {font-family:"/@宋体"; panose-1:2 1 6 0 3 1 1 1 1 1; mso-font-charset:134; mso-generic-font-family:auto; mso-font-pitch:variable; mso-font-signature:3 680460288 22 0 262145 0;} @font-face {font-family:楷体_GB2312; panose-1:0 0 0 0 0 0 0 0 0 0; mso-font-alt:宋体; mso-font-charset:134; mso-generic-font-family:auto; mso-font-format:other; mso-font-pitch:auto; mso-font-signature:1 135135232 16 0 262144 0;} @font-face {font-family:"/@幼圆"; panose-1:0 0 0 0 0 0 0 0 0 0; mso-font-charset:134; mso-generic-font-family:auto; mso-font-format:other; mso-font-pitch:auto; mso-font-signature:1 135135232 16 0 262144 0;} @font-face {font-family:"/@楷体_GB2312"; panose-1:0 0 0 0 0 0 0 0 0 0; mso-font-charset:134; mso-generic-font-family:auto; mso-font-format:other; mso-font-pitch:auto; mso-font-signature:1 135135232 16 0 262144 0;} /* Style Definitions */ p.MsoNormal, li.MsoNormal, div.MsoNormal {mso-style-parent:""; margin:0cm; margin-bottom:.0001pt; mso-pagination:widow-orphan; font-size:12.0pt; font-family:"Times New Roman"; mso-fareast-font-family:宋体;} @page Section1 {size:612.0pt 792.0pt; margin:72.0pt 90.0pt 72.0pt 90.0pt; mso-header-margin:36.0pt; mso-footer-margin:36.0pt; mso-paper-source:0;} div.Section1 {page:Section1;} -->
2.7 生成非对称加密的公钥和私钥
前面几节的程序中,加密和解密使用的是同一个密钥,这种方式称为对称加密。使用对称密钥时,若 A 想让 B 向其秘密传送信息, A 必须先将密钥提供给 B ,或者由 B 将密钥提供给 A 。如果在传递密钥过程中密钥被窃取,则 A 和 B 之间的通信就不再安全了。非对称加密解决了这一问题。它将加密的密钥和解密的密钥分开。 A 事先生成一对密钥,一个用于加密,称为公钥(公钥),一个用于解密,称为私钥。由于产生这一对密钥的一些数学特性,公钥加密的信息只能用私钥解密。这样, A 只要将公钥对外公开,不论谁就可以使用这个公钥给 A 发送秘密信息了。 A 接收到加密信息后可以用私钥打开。由于只需要传递公钥,而公钥只能加密不能解密,因此即使攻击者知道了公钥也无济于事。本节以 RSA 算法为例介绍 Java 中如何生成公钥和私钥。
实例说明
本实例演示了如何使用 Java 中定义好的类创建 RSA 公钥和私钥。
编程思路:
Java 的 KeyPairGenerator 类提供了一些方法来创建密钥对以便用于非对称加密,密钥对创建好后封装在 KeyPair 类型的对象中,在 KeyPair 类中提供了获取公钥和私钥的方法。具体步骤如下:
( 1 ) 创建密钥对生成器
KeyPairGenerator kpg=KeyPairGenerator.getInstance("RSA");
分析:密钥对生成器即 KeyPairGenerator 类型的对象,和 2.2.1 小节的第 1 步中介绍的 KeyGenerator 类一样, KeyPairGenerator 类是一个工厂类,它通过其中预定义
的一个静态方法 getInstance( )获取 KeyPairGenerator 类型的对象。 getInstance()方法的参数是一个字符串,指定非对称加密所使用的算法,常用的有 RSA, DSA等。
( 2 ) 初始化密钥生成器
kpg.initialize(1024);
分析:对于密钥长度。对于 RSA 算法,这里指定的其实是 RSA 算法中所用的模的位数。可以在 512 到 2048 之间。
( 3 ) 生成密钥对
KeyPair kp=kpg.genKeyPair( );
分析:使用 KeyPairGenerator 类的 genKeyPair( )方法生成密钥对,其中包含了一对公钥和私钥的信息。
( 4 ) 获取公钥和私钥
PublicKey pbkey=kp.getPublic( );
PrivateKey prkey=kp.getPrivate( );
分析:使用 KeyPair 类的 getPublic( )和 getPrivate( )方法获得公钥和私钥对象。
//文件: Skey_RSA.java
import java.io.*;
import java.security.*;
import javax.crypto.*;
import javax.crypto.spec.*;
public class Skey_RSA{
public static void main(String args[]) throws Exception{
KeyPairGenerator kpg=KeyPairGenerator.getInstance("RSA");
kpg.initialize(1024);
KeyPair kp=kpg.genKeyPair();
PublicKey pbkey=kp.getPublic();
PrivateKey prkey=kp.getPrivate();
FileOutputStream f1=new FileOutputStream("Skey_RSA_pub.dat");
ObjectOutputStream b1=new ObjectOutputStream(f1);
b1.writeObject(pbkey);
FileOutputStream f2=new FileOutputStream("Skey_RSA_priv.dat");
ObjectOutputStream b2=new ObjectOutputStream(f2);
b2.writeObject(prkey);
}
}
分析:本实例和 2.2.1 小节一样使用对象流将密钥保存在文件中,所不同的是加密所用
的公钥和解密所用的私钥分开保存。将公钥对外公布,供其他人加密使用,而把私钥秘密保
存,在需要解密时使用。
运行程序
输入 java Skey_RSA 运行程序,当前目录下将生成两个文件: Skey_RSA_pub.dat 和
Skey_RSA_priv.dat ,前者保存着公钥,后者保存着私钥。将文件 Skey_RSA_pub.dat 对外公
布(如放在 Web 服务器上给大家下载,或者直接拷贝给所有需要的人),而 Skey_RSA_priv.dat
秘密保存。
2.8 使用 RSA 算法进行加密和解密
2.7 节的程序创建了 RSA 的公钥和密钥,本节使用公钥进行加密,然后使用私钥对加密的信息进行解密。
2.8.1 使用RSA 公钥进行加密
实例说明
本实例以加密一串最简单的字符串“ Hello World! ”为例,演示了如何使用 2.7 节生成的 RSA 公钥文件 Skey_RSA_pub.dat 进行加密。
编程思路:
使用 RSA 公钥进行加密的代码和 2.3.1 小节使用 DESede 进行加密其实没什么大的区别,只是 Cipher 类的 getInstance( ) 方法的参数中应该指定使用 RSA 。但由于 J2SDK1.4 中只实现了 RSA 密钥的创建,没有实现 RSA 算法,因此需要安装其他加密提供者软件才能直接使用 Cipher 类执行加密解密。其实有了 RSA 公钥和私钥后,自己编写程序从底层实现 RSA 算法也并不复杂。本实例给出简单的例子实现了 RSA 加密,使读者只使用 J2SDK1.4 便能直观地了解非对称加密算法。 RSA 算法是使用整数进行加密运算的,在 RSA 公钥中包含了两个信息:公钥对应的整数 e 和用于取模的整数 n 。对于明文数字 m ,计算密文的公式是: me mod n 。因此,编程步骤如下:
( 1 ) 获取公钥
FileInputStream f=new FileInputStream("Skey_RSA_pub.dat");
ObjectInputStream b=new ObjectInputStream(f);
RSAPublicKey pbk=(RSAPublicKey)b.readObject( );
分析: 从 2.7 节生成的公钥文件 Skey_RSA_pub.dat 中读取公钥,由于 2.7 节使用的是 RSA 算法,因此从文件读取公钥对象后强制转换为 RSAPublicKey 类型,以便后面读取 RSA 算法所需要的参数 .
( 2 ) 获取公钥的参数 (e, n)
BigInteger e=pbk.getPublicExponent();
BigInteger n=pbk.getModulus();
分析:使用 RSAPublicKey 类的 getPublicExponent( )和 getModulus( )方法可以分别获得公始中 e 和 n 的值。由于密钥很长,因此对应的整数值非常大,无法使用一般的整型来存储, Java 中定义了 BigInteger 类来存储这类很大的整数并可进行各种运算。
( 3 ) 获取明文整数 (m)
String s="Hello World!";
byte ptext[]=s.getBytes("UTF8");
BigInteger m=new BigInteger(ptext);
分析:明文是一个字符串,为了用整数表达这个字符串,先使用字符串的 getBytes( ) 方法 将其转换为 byte 类型数组,它其实是字符串中各个字符的二进制表达方式,这一串二进制数转换为一个整数将非常大,因此仍旧使用 BigInteger 类将这个二进制串转换为整型。本实例中出于简化,将整个字符串转换为一个整数。实际使用中,应该对明文进行分组,因为 RSA 算法要求整型数 m 的值必须小于 n。
( 4 ) 执行计算
BigInteger c=m.modPow(e,n);
分析:计算前面的公式: me mod n。 BigInteger 类中已经提供了方法 modPow( )来执行这个计算。底数 m 执行这个方法,方法 modPow( )的第一个参数即指数 e,第二个参数即模 n。方法返回的结果即公式 me mod n 的计算结果,即密文。
//文件: Enc_RSA.java
import java.security.*;
import java.security.spec.*;
import javax.crypto.*;
import javax.crypto.spec.*;
import javax.crypto.interfaces.*;
import java.security.interfaces.*;
import java.math.*;
import java.io.*;
public class Enc_RSA{
public static void main(String args[]) throws Exception{
String s="Hello World!";
FileInputStream f=new FileInputStream("Skey_RSA_pub.dat");
ObjectInputStream b=new ObjectInputStream(f);
RSAPublicKey pbk=(RSAPublicKey)b.readObject( );
BigInteger e=pbk.getPublicExponent();
BigInteger n=pbk.getModulus();
System.out.println("e= "+e);
System.out.println("n= "+n);
byte ptext[]=s.getBytes("UTF8");
BigInteger m=new BigInteger(ptext);
BigInteger c=m.modPow(e,n);
System.out.println("c= "+c);
String cs=c.toString( );
BufferedWriter out=
new BufferedWriter(new OutputStreamWriter(
new FileOutputStream("Enc_RSA.dat")));
out.write(cs,0,cs.length( ));
out.close( );
}
}程序最后将密文 c 打印出来,并以字符串形式保存在文件中。
运行程序
输入 java Enc_RSA 运行程序
2.8.2 使用RSA 私钥进行解密
实例说明
本实例使用 2.7 节生成的私钥文件 Skey_RSA_priv.dat ,对 2.8.1 小节生成的密文文件 Enc_RSA.dat 进行解密。
编程思路:
和 2.8.1 小节类似,使用 RSA 私钥进行解密的代码也可以在 Cipher 类的 getInstance( ) 方法的参数中指定使用 RSA ,使用解密模式进行解密。但需要安装其他加密提供者软件才能直接使用 Cipher 类执行加密解密。本实例给出简单的例子从底层实现 RSA 解密,以便只使用 J2SDK1.4 便能直观地了解非对称加密算法。 RSA 算法的解密和加密类似,在 RSA 私钥中包含了两个信息:私钥对应的整数 d 和用于取模的整数 n 。其中的 n 和加密时的 n 完全相同。对于密文数字 c ,计算明文的公式是: cd mod
n ,之所以加密时由公式 me mod n 得到的密文 c 通过这个公式计算一下就可以反过来得到原来的明文 m ,有其本身的数学规律决定。从编程角度只需要知道这个结果就行了。编程步骤如下:
( 1 ) 读取密文
BufferedReader in=
new BufferedReader(new InputStreamReader(
new FileInputStream("Enc_RSA.dat")));
String ctext=in.readLine();
BigInteger c=new BigInteger(ctext);
分析: 从 2.8.1 小节生成的密文文件 Enc_RSA.dat 中读取密文,由于 2.8.1 小节保存的只是一行字符串,因此只要一条 readLine( )语句即可。由于这一行字符串表示的是一个很大的整型数,因此使用 BigInteger 类来表示这个整型数。
( 2 ) 获取私钥
FileInputStream f=new FileInputStream("Skey_RSA_priv.dat");
ObjectInputStream b=new ObjectInputStream(f);
RSAPrivateKey prk=(RSAPrivateKey)b.readObject( );
分析: 从 2.7 节生成的私钥文件 Skey_RSA_priv.dat 中读取公钥,由于 2.7 节使用的是 RSA 算法,因此从文件读取公钥对象后强制转换为 RSAPrivateKey 类型,以便后面读取 RSA 算法所需要的参数。
( 3 ) 获取私钥的参数 (d, n)
BigInteger d=prk.getPrivateExponent( );
BigInteger n=prk.getModulus( );
分析:使用 RSAPrivateKey 类的 getPrivateExponent( ) 和 getModulus( )方法可以分别获得公始中 d 和 n 的值。
( 4 ) 执行计算
BigInteger m=c.modPow(d,n);
分析:使用 BigInteger 的 modPow( )方法计算前面的公式: cd mod n。方法返回的结果即公式 cd mod n 的计算结果,即明文对应的整型数 m。
( 5 ) 计算明文整型数对应的字符串
byte[] mt=m.toByteArray();
for(int i=0;i<mt.length;i++){
System.out.print((char) mt[i]);
}
分析: RSA 算法解密的结果 m 是一个很大的整数,为了计算出其对应的字符串的值,先使用 BigInteger 类的 toByteArray( )方法得到代表该整型数的字节数组,然后将数组中每个元素转换为字符,组成字符串。
//文件: Dec_RSA.java
import java.security.*;
import java.security.spec.*;
import javax.crypto.*;
import javax.crypto.spec.*;
import javax.crypto.interfaces.*;
import java.security.interfaces.*;
import java.math.*;
import java.io.*;
public class Dec_RSA{
public static void main(String args[]) throws Exception{
BufferedReader in=
new BufferedReader(new InputStreamReader(new FileInputStream("Enc_RSA.dat")));
String ctext=in.readLine();
BigInteger c=new BigInteger(ctext);
FileInputStream f=new FileInputStream("Skey_RSA_priv.dat");
ObjectInputStream b=new ObjectInputStream(f);
RSAPrivateKey prk=(RSAPrivateKey)b.readObject( );
BigInteger d=prk.getPrivateExponent();
BigInteger n=prk.getModulus();
System.out.println("d= "+d);
System.out.println("n= "+n);
BigInteger m=c.modPow(d,n);
System.out.println("m= "+m);
byte[] mt=m.toByteArray();
System.out.println("PlainText is ");
for(int i=0;i<mt.length;i++){
System.out.print((char) mt[i]);
}
}
}
运行程序
输入 java Dec_RSA 运行程序
2.9 使用密钥协定创建共享密钥
非对称加密解决了密钥分发的难题,但其计算量比对称密钥大,因此一般并不使用非对称加密加密大量数据。常见的做法是:主要数据通过对称密钥加密,而使用非对称加密来分发对称密钥。将两者的优势结合了起来。例如若 A 和 B 之间想秘密传送大量数据,一方(如 A )先创建公钥和私钥对,公钥对外公布,另一方(如 B )创建对称密钥,然后使用公钥加密对称密钥,传递给 A , A 收到后用私钥解密,得到对称密钥,以后 A 和 B 之间就可以使用对称密钥加密通信了。除了这种方式以外,还可以使用密钥协定来交换对称密钥。执行密钥协定的标准算法是 DH 算法( Diffie-Hellman 算法),本节介绍在 Java 中如何使用 DH 算法来交换共享密钥。
2.9.1 创建DH 公钥和私钥
实例说明
DH 算法是建立在 DH 公钥和私钥的基础上的, A 需要和 B 共享密钥时, A 和 B 各自生成 DH 公钥和私钥,公钥对外公布而私钥各自秘密保存。本实例将介绍 Java 中如何创建并部署 DH 公钥和私钥,以便后面一小节利用它创建共享密钥。
编程思路:
在 2.7 节中使用了 KeyPairGenerator 类创建 RSA 公钥和私钥,本节也一样,只是其参数中指定“ DH ”,此外在初始化时需要为 DH 指定特定的参数。具体步骤如下:
( 1 ) 生成 DH 参数
DHParameterSpec DHP=
new DHParameterSpec(skip1024Modulus,skip1024Base);
分析:和 RSA 算法类似, DH 算法涉及到一些指数和取模运算, DH 参数指定 A、 B双方在创建 DH 密钥时所公用的基数和模, Java 中 DHParameterSpec 类可以定义 DH 参数,其构造器的第一个参数指定模,第二个参数指定基数。模和基数的取值在 Internet协议简单密钥管理( SKIP)标准中已经标准化,在安装 J2SDK1.4 后,计算机 C 盘中 C:/j2sdk-1_4_0-doc/docs/guide/security/jce/JCERefGuide.html 文件也包含了密钥长度为 1024 的 DH 密钥中模和基数的定义, 可以直接拷贝下来使用, 在 JCERefGuide.html 文件中查找“ 1024 bit Diffie-Hellman modulus”注释语句,将其下的 skip1024ModulusBytes[ ]数组以及 BigInteger 类型的 skip1024Modulus 和 skip1024Base 变量拷贝下来即可。在本小节的“代码与分析”中也给出了完整的代码。此外 DH 密钥长度也可以是 512 或 2048 位。
( 2 ) 创建密钥对生成器
KeyPairGenerator kpg= KeyPairGenerator.getInstance("DH");
分析:密钥对生成器即 KeyPairGenerator 类型的对象,和 2.7 节一样,通过其中预定义的一个静态方法 getInstance( )获取 KeyPairGenerator 类型的对象。 getInstance( )方法的参数指定为“ DH”。
( 3 ) 初始化密钥生成器
kpg.initialize(DHP);
分析:初始化时使用的参数即第 1 步中生成的参数。
( 4 ) 生成密钥对,获取公钥和私钥
KeyPair kp=kpg.genKeyPair();
PublicKey pbk=kp.getPublic( );
PrivateKey prk=kp.getPrivate( );
分析:和 2.7 节一样,使用使用 KeyPairGenerator 类的 genKeyPair( )方法生成密钥对,进而使用密钥对的 getPublic( ) 和 getPrivate( ) 获取公钥和私钥。
//文件: Key_DH.java
import java.io.*;
import java.math.*;
import java.security.*;
import java.security.spec.*;
import javax.crypto.*;
import javax.crypto.spec.*;
import javax.crypto.interfaces.*;
public class Key_DH{
// The 1024 bit Diffie-Hellman modulus values used by SKIP
private static final byte skip1024ModulusBytes[] = {
(byte)0xF4, (byte)0x88, (byte)0xFD, (byte)0x58,
(byte)0x4E, (byte)0x49, (byte)0xDB, (byte)0xCD,
(byte)0x20, (byte)0xB4, (byte)0x9D, (byte)0xE4,
(byte)0x91, (byte)0x07, (byte)0x36, (byte)0x6B,
(byte)0x33, (byte)0x6C, (byte)0x38, (byte)0x0D,
(byte)0x45, (byte)0x1D, (byte)0x0F, (byte)0x7C,
(byte)0x88, (byte)0xB3, (byte)0x1C, (byte)0x7C,
(byte)0x5B, (byte)0x2D, (byte)0x8E, (byte)0xF6,
(byte)0xF3, (byte)0xC9, (byte)0x23, (byte)0xC0,
(byte)0x43, (byte)0xF0, (byte)0xA5, (byte)0x5B,
(byte)0x18, (byte)0x8D, (byte)0x8E, (byte)0xBB,
(byte)0x55, (byte)0x8C, (byte)0xB8, (byte)0x5D,
(byte)0x38, (byte)0xD3, (byte)0x34, (byte)0xFD,
(byte)0x7C, (byte)0x17, (byte)0x57, (byte)0x43,
(byte)0xA3, (byte)0x1D, (byte)0x18, (byte)0x6C,
(byte)0xDE, (byte)0x33, (byte)0x21, (byte)0x2C,
(byte)0xB5, (byte)0x2A, (byte)0xFF, (byte)0x3C,
(byte)0xE1, (byte)0xB1, (byte)0x29, (byte)0x40,
(byte)0x18, (byte)0x11, (byte)0x8D, (byte)0x7C,
(byte)0x84, (byte)0xA7, (byte)0x0A, (byte)0x72,
(byte)0xD6, (byte)0x86, (byte)0xC4, (byte)0x03,
(byte)0x19, (byte)0xC8, (byte)0x07, (byte)0x29,
(byte)0x7A, (byte)0xCA, (byte)0x95, (byte)0x0C,
(byte)0xD9, (byte)0x96, (byte)0x9F, (byte)0xAB,
(byte)0xD0, (byte)0x0A, (byte)0x50, (byte)0x9B,
(byte)0x02, (byte)0x46, (byte)0xD3, (byte)0x08,
(byte)0x3D, (byte)0x66, (byte)0xA4, (byte)0x5D,
(byte)0x41, (byte)0x9F, (byte)0x9C, (byte)0x7C,
(byte)0xBD, (byte)0x89, (byte)0x4B, (byte)0x22,
(byte)0x19, (byte)0x26, (byte)0xBA, (byte)0xAB,
(byte)0xA2, (byte)0x5E, (byte)0xC3, (byte)0x55,
(byte)0xE9, (byte)0x2F, (byte)0x78, (byte)0xC7
};
// The SKIP 1024 bit modulus
private static final BigInteger skip1024Modulus
= new BigInteger(1, skip1024ModulusBytes);
// The base used with the SKIP 1024 bit modulus
private static final BigInteger skip1024Base = BigInteger.valueOf(2);
public static void main(String args[ ]) throws Exception{
DHParameterSpec DHP=new DHParameterSpec(skip1024Modulus,skip1024Base);
KeyPairGenerator kpg= KeyPairGenerator.getInstance("DH");
kpg.initialize(DHP);
KeyPair kp=kpg.genKeyPair();
PublicKey pbk=kp.getPublic();
PrivateKey prk=kp.getPrivate();
FileOutputStream f1=new FileOutputStream(args[0]);
ObjectOutputStream b1=new ObjectOutputStream(f1);
b1.writeObject(pbk);
FileOutputStream f2=new FileOutputStream(args[1]);
ObjectOutputStream b2=new ObjectOutputStream(f2);
b2.writeObject(prk);
}
}
程序最后将公钥和私钥以对象流的形式保存在文件中,文件名通过命令行参数指定,第一个命令行参数对应的文件保存公钥,第二个命令行参数对应的文件保存私钥。
运行程序
建立两个目录 A 和 B ,模拟需要秘密通信的 A 、 B 双方,由于 DH 算法需要 A 和 B 各自生成 DH 公钥和私钥,因此在这两个目录下都拷贝编译后文件 Key_DH 。首先由 A 创建自己的公钥和私钥,即在 A 目录下输入“ java Key_DH Apub.dat Apri.dat ”运行程序,这时在目录 A 下将产生文件 Apub.dat 和 Apri.dat ,前者保存着 A 的公钥,后者保存着 A 的私钥。然后由 B 创建自己的公钥和私钥,即在 B 目录下输入“ java Key_DH Bpub.dat Bpri.dat ”运行程序,这时在目录 B 下将产生文件 Bpub.dat 和 Bpri.dat ,前者保存着 B 的公钥,后者保存着 B 的私钥。最后发布公钥, A 将 Apub.dat 拷贝到 B 目录, B 将 Bpub.dat 拷贝到 A 的目录。这样, A 、 B 双方的 DH 公钥和私钥已经创建并部署完毕。
2.9.2 创建共享密钥
实例说明
DH 算法中, A 可以用自己的密钥和 B 的公钥按照一定方法生成一个密钥, B 也可以用自己的密钥和 A 的公钥按照一定方法生成一个密钥,由于一些数学规律,这两个密钥完全相同。这样, A 和 B 间就有了一个共同的密钥可以用于各种加密。本实例介绍 Java 中在上一小节的基础上如何利用 DH 公钥和私钥各自创建共享密钥。
编程思路:
Java 中 KeyAgreement 类实现了密钥协定,它使用 init( ) 方法传入自己的私钥,使用 doPhase
( )方法传入对方的公钥,进而可以使用 generateSecret( ) 方法生成共享的信息具体步骤如下:
( 1 ) 读取自己的 DH 私钥和对方的DH 公钥
FileInputStream f1=new FileInputStream(args[0]);
ObjectInputStream b1=new ObjectInputStream(f1);
PublicKey pbk=(PublicKey)b1.readObject( );
FileInputStream f2=new FileInputStream(args[1]);
ObjectInputStream b2=new ObjectInputStream(f2);
PrivateKey prk=(PrivateKey)b2.readObject( );
分析:和 2.3.1 小节类似,从文件中获取密钥。只是分为公钥和私钥两个文件,通过命令行参数传入公钥和私钥文件名,第一个命令行参数为对方的公钥文件名,第二个命令行参数为自己的私钥文件名。
( 2 ) 创建密钥协定对象
KeyAgreement ka=KeyAgreement.getInstance("DH");
分析:密钥协定对象即 KeyAgreement 类型的对象,和 KeyPairGenerator 类类似, KeyAgreement 类是一个工厂类,通过其中预定义的一个静态方法 getInstance( )获取 KeyAgreement 类型的对象。 getInstance( )方法的参数指定为“ DH”。
( 3 ) 初始化密钥协定对象
ka.init(prk);
分析:执行密钥协定对象的 init( )方法,传入第 1 步获得的自己的私钥,它在第 1 步中通过第 2 个命令行参数提供。
( 4 ) 执行密钥协定
ka.doPhase(pbk,true);
分析:执行密钥协定对象的 doPhase( )方法,其第一个参数中传入对方的公钥。在本实例中,只有 A、 B 两方需要共享密钥,因此对方只有一个,因此第二个参数设置为 true。如果有 A、 B、 C 三方需要共享密钥,则对方有两个, doPhase()方法要写两次,每次在第 1 个参数中传入一个公钥,第 2 个参数最初设置为 false,最后一次设置为 true。例如 C 方应该执行 ka.doPhase(pbk_of_A,false); ka.doPhase(pbk_of_B,true); 。一次类推,可以用密钥协定实现多方共享一个密钥。
( 5 ) 生成共享信息
byte[ ] sb=ka.generateSecret();
分析:执行密钥协定对象的 generateSecret( ) 方法,返回字节类型的数组。 A、 B双方得到的该数组的内容完全相同,用它创建密钥也各方完全相同。如可使用 SecretKeySpec k=new SecretKeySpec(sb,"DESede"); 创建密钥。
//文件: KeyAgree.java
import java.io.*;
import java.math.*;
import java.security.*;
import java.security.spec.*;
import javax.crypto.*;
import javax.crypto.spec.*;
import javax.crypto.interfaces.*;
public class KeyAgree{
public static void main(String args[ ]) throws Exception{
FileInputStream f1=new FileInputStream(args[0]);
ObjectInputStream b1=new ObjectInputStream(f1);
PublicKey pbk=(PublicKey)b1.readObject( );
FileInputStream f2=new FileInputStream(args[1]);
ObjectInputStream b2=new ObjectInputStream(f2);
PrivateKey prk=(PrivateKey)b2.readObject( );
KeyAgreement ka=KeyAgreement.getInstance("DH");
ka.init(prk);
ka.doPhase(pbk,true);
byte[ ] sb=ka.generateSecret();
for(int i=0;i<sb.length;i++){
System.out.print(sb[i]+",");
}
SecretKeySpec k=new SecretKeySpec(sb,"DESede");
}
}
程序最后将共享信息打印了出来,以便直观地对比 A 和 B 得到的信息是否相同。
运行程序
将程序 KeyAgree 编译后分别拷贝在 A 和 B 两个目录,首先在 A 目录输入“ java KeyAgree
Bpub.dat Apri.dat ”运行程序,它使用文件 Bpub.dat 中对方的公钥和文件 Apri.dat 中自己的
私钥创建了一段共享的字节数组。然后在 B 目录输入“ java KeyAgree Apub.dat Bpri.dat ”运行程序,它使用文件 Apub.dat 中对方的公钥和文件 Bpri.dat 中自己的私钥创建了一段共享的字节数组。
可以看到 A 和 B 运行后得到的字节数组内容完全相同,因此使用它创建的密钥也将相同。由于 DH 算法内在的数学规律, A 和 B 在运行时只使用了对方可以公开的信息(公钥),而各自独立地得到了相同的密钥,完成了密钥分发工作。该部分介绍了对称加密和非对称加密的基本用法,并演示了使用密钥协定进行密钥分发。读者在掌握了其原理后,可以举一反三,互相组合,满足不同应用。出于实例的简洁,该部分的例子都使用文件来交换信息,读者也可以使用其它方式,如在密钥协定中可以使用 Socket 等多种方式在程序间传递信息。
BASE64、MD5、SHA、HMAC几种加密算法
本篇内容简要介绍BASE64 、MD5 、SHA 、HMAC 几种加密算法。
BASE64 编码算法不算是真正的加密算法。
MD5 、SHA 、HMAC 这三种加密算法,可谓是非可逆加密,就是不可解密的加密方法,我们称之为单向加密算法。我们通常只把他们作为加密的基础。单纯的以上三种的加密并不可靠。
BASE64
按照RFC2045的定义,Base64被定义为:Base64内容传送编码被设计用来把任意序列的8位字节描述为一种不易被人直接识别的形式。(The Base64 Content-Transfer-Encoding is designed to represent arbitrary sequences of octets in a form that need not be humanly readable.)
常见于邮件、http加密,截取http信息,你就会发现登录操作的用户名、密码字段通过BASE64加密的。
通过java代码实现如下:
- /**
- * BASE64解密
- *
- * @param key
- * @return
- * @throws Exception
- */
- public static byte[] decryptBASE64(String key) throws Exception {
- return ( new BASE64Decoder()).decodeBuffer(key);
- }
- /**
- * BASE64加密
- *
- * @param key
- * @return
- * @throws Exception
- */
- public static String encryptBASE64( byte[] key) throws Exception {
- return ( new BASE64Encoder()).encodeBuffer(key);
- }
主要就是BASE64Encoder、BASE64Decoder两个类,我们只需要知道使用对应的方法即可。另,BASE加密后产生的字节位数是8的倍数,如果不够位数以= 符号填充。
MD5
MD5 -- message-digest algorithm 5 (信息-摘要算法)缩写,广泛用于加密和解密技术,常用于文件校验。校验?不管文件多大,经过MD5后都能生成唯一的MD5值。好比现在的ISO校验,都是MD5校验。怎么用?当然是把ISO经过MD5后产生MD5的值。一般下载linux-ISO的朋友都见过下载链接旁边放着MD5的串。就是用来验证文件是否一致的。
通过java代码实现如下:
- /**
- * MD5加密
- *
- * @param data
- * @return
- * @throws Exception
- */
- public static byte[] encryptMD5( byte[] data) throws Exception {
- MessageDigest md5 = MessageDigest.getInstance(KEY_MD5);
- md5.update(data);
- return md5.digest();
- }
通常我们不直接使用上述MD5加密。通常将MD5产生的字节数组交给BASE64再加密一把,得到相应的字符串。
SHA
SHA(Secure Hash Algorithm,安全散列算法),数字签名等密码学应用中重要的工具,被广泛地应用于电子商务等信息安全领域。虽然,SHA与MD5通过碰撞法都被破解了, 但是SHA仍然是公认的安全加密算法,较之MD5更为安全。
通过java代码实现如下:
- /**
- * SHA加密
- *
- * @param data
- * @return
- * @throws Exception
- */
- public static byte[] encryptSHA( byte[] data) throws Exception {
- MessageDigest sha = MessageDigest.getInstance(KEY_SHA);
- sha.update(data);
- return sha.digest();
- }
- }
HMAC
HMAC(Hash Message Authentication Code,散列消息鉴别码,基于密钥的Hash算法的认证协议。消息鉴别码实现鉴别的原理是,用公开函数和密钥产生一个固定长度的值作为认证标识,用这个标识鉴别消息的完整性。使用一个密钥生成一个固定大小的小数据块,即MAC,并将其加入到消息中,然后传输。接收方利用与发送方共享的密钥进行鉴别认证等。
通过java代码实现如下:
- /**
- * 初始化HMAC密钥
- *
- * @return
- * @throws Exception
- */
- public static String initMacKey() throws Exception {
- KeyGenerator keyGenerator = KeyGenerator.getInstance(KEY_MAC);
- SecretKey secretKey = keyGenerator.generateKey();
- return encryptBASE64(secretKey.getEncoded());
- }
- /**
- * HMAC加密
- *
- * @param data
- * @param key
- * @return
- * @throws Exception
- */
- public static byte[] encryptHMAC( byte[] data, String key) throws Exception {
- SecretKey secretKey = new SecretKeySpec(decryptBASE64(key), KEY_MAC);
- Mac mac = Mac.getInstance(secretKey.getAlgorithm());
- mac.init(secretKey);
- return mac.doFinal(data);
- }
给出一个完整类,如下:
- import java.security.MessageDigest;
- import javax.crypto.KeyGenerator;
- import javax.crypto.Mac;
- import javax.crypto.SecretKey;
- import sun.misc.BASE64Decoder;
- import sun.misc.BASE64Encoder;
- /**
- * 基础加密组件
- *
- * @author 梁栋
- * @version 1.0
- * @since 1.0
- */
- public abstract class Coder {
- public static final String KEY_SHA = "SHA";
- public static final String KEY_MD5 = "MD5";
- /**
- * MAC算法可选以下多种算法
- *
- * <pre>
- * HmacMD5
- * HmacSHA1
- * HmacSHA256
- * HmacSHA384
- * HmacSHA512
- * </pre>
- */
- public static final String KEY_MAC = "HmacMD5";
- /**
- * BASE64解密
- *
- * @param key
- * @return
- * @throws Exception
- */
- public static byte[] decryptBASE64(String key) throws Exception {
- return ( new BASE64Decoder()).decodeBuffer(key);
- }
- /**
- * BASE64加密
- *
- * @param key
- * @return
- * @throws Exception
- */
- public static String encryptBASE64( byte[] key) throws Exception {
- return ( new BASE64Encoder()).encodeBuffer(key);
- }
- /**
- * MD5加密
- *
- * @param data
- * @return
- * @throws Exception
- */
- public static byte[] encryptMD5( byte[] data) throws Exception {
- MessageDigest md5 = MessageDigest.getInstance(KEY_MD5);
- md5.update(data);
- return md5.digest();
- }
- /**
- * SHA加密
- *
- * @param data
- * @return
- * @throws Exception
- */
- public static byte[] encryptSHA( byte[] data) throws Exception {
- MessageDigest sha = MessageDigest.getInstance(KEY_SHA);
- sha.update(data);
- return sha.digest();
- }
- /**
- * 初始化HMAC密钥
- *
- * @return
- * @throws Exception
- */
- public static String initMacKey() throws Exception {
- KeyGenerator keyGenerator = KeyGenerator.getInstance(KEY_MAC);
- SecretKey secretKey = keyGenerator.generateKey();
- return encryptBASE64(secretKey.getEncoded());
- }
- /**
- * HMAC加密
- *
- * @param data
- * @param key
- * @return
- * @throws Exception
- */
- public static byte[] encryptHMAC( byte[] data, String key) throws Exception {
- SecretKey secretKey = new SecretKeySpec(decryptBASE64(key), KEY_MAC);
- Mac mac = Mac.getInstance(secretKey.getAlgorithm());
- mac.init(secretKey);
- return mac.doFinal(data);
- }
- }
再给出一个测试类:
- import static org.junit.Assert.*;
- import org.junit.Test;
- /**
- *
- * @author 梁栋
- * @version 1.0
- * @since 1.0
- */
- public class CoderTest {
- @Test
- public void test() throws Exception {
- String inputStr = "简单加密";
- System.err.println("原文:/n" + inputStr);
- byte[] inputData = inputStr.getBytes();
- String code = Coder.encryptBASE64(inputData);
- System.err.println("BASE64加密后:/n" + code);
- byte[] output = Coder.decryptBASE64(code);
- String outputStr = new String(output);
- System.err.println("BASE64解密后:/n" + outputStr);
- // 验证BASE64加密解密一致性
- assertEquals(inputStr, outputStr);
- // 验证MD5对于同一内容加密是否一致
- assertArrayEquals(Coder.encryptMD5(inputData), Coder
- .encryptMD5(inputData));
- // 验证SHA对于同一内容加密是否一致
- assertArrayEquals(Coder.encryptSHA(inputData), Coder
- .encryptSHA(inputData));
- String key = Coder.initMacKey();
- System.err.println("Mac密钥:/n" + key);
- // 验证HMAC对于同一内容,同一密钥加密是否一致
- assertArrayEquals(Coder.encryptHMAC(inputData, key), Coder.encryptHMAC(
- inputData, key));
- BigInteger md5 = new BigInteger(Coder.encryptMD5(inputData));
- System.err.println("MD5:/n" + md5.toString( 16));
- BigInteger sha = new BigInteger(Coder.encryptSHA(inputData));
- System.err.println("SHA:/n" + sha.toString( 32));
- BigInteger mac = new BigInteger(Coder.encryptHMAC(inputData, inputStr));
- System.err.println("HMAC:/n" + mac.toString( 16));
- }
- }
控制台输出:
- 原文:
- 简单加密
- BASE64加密后:
- 566A5Y2V5Yqg5a+G
- BASE64解密后:
- 简单加密
- Mac密钥:
- uGxdHC+6ylRDaik++leFtGwiMbuYUJ6mqHWyhSgF4trVkVBBSQvY/a22xU8XT1RUemdCWW155Bke
- pBIpkd7QHg==
- MD5:
- -550b4d90349ad4629462113e7934de56
- SHA:
- 91k9vo7p400cjkgfhjh0ia9qthsjagfn
- HMAC:
- 2287d192387e95694bdbba2fa941009a
BASE64的加密解密是双向的,可以求反解。
MD5、SHA以及HMAC是单向加密,任何数据加密后只会产生唯一的一个加密串,通常用来校验数据在传输过程中是否被修改。其中HMAC算法有一个密钥,增强了数据传输过程中的安全性,强化了算法外的不可控因素。
单向加密的用途主要是为了校验数据在传输过程中是否被修改。
JAVA实现AES加密
1. 因子
上次介绍了《JAVA实现AES加密》,中间提到近些年DES使用越来越少,原因就在于其使用56位密钥,比较容易被破解,近些年来逐渐被AES替代,AES已经变成目前对称加密中最流行算法之一;AES可以使用128、192、和256位密钥,并且用128位分组加密和解密数据。本文就简单介绍如何通过JAVA实现AES加密。
2. JAVA实现
闲话少许,掠过AES加密原理及算法,关于这些直接搜索专业网站吧,我们直接看JAVA的具体实现。
2.1 加密
代码有详细解释,不多废话。
view plaincopy to clipboardprint?
/**
* 加密
*
* @param content 需要加密的内容
* @param password 加密密码
* @return
*/
public static byte[] encrypt(String content, String password) {
try {
KeyGenerator kgen = KeyGenerator.getInstance("AES");
kgen.init(128, new SecureRandom(password.getBytes()));
SecretKey secretKey = kgen.generateKey();
byte[] enCodeFormat = secretKey.getEncoded();
SecretKeySpec key = new SecretKeySpec(enCodeFormat, "AES");
Cipher cipher = Cipher.getInstance("AES");// 创建密码器
byte[] byteContent = content.getBytes("utf-8");
cipher.init(Cipher.ENCRYPT_MODE, key);// 初始化
byte[] result = cipher.doFinal(byteContent);
return result; // 加密
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} catch (NoSuchPaddingException e) {
e.printStackTrace();
} catch (InvalidKeyException e) {
e.printStackTrace();
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
} catch (IllegalBlockSizeException e) {
e.printStackTrace();
} catch (BadPaddingException e) {
e.printStackTrace();
}
return null;
}
/**
* 加密
*
* @param content 需要加密的内容
* @param password 加密密码
* @return
*/
public static byte[] encrypt(String content, String password) {
try {
KeyGenerator kgen = KeyGenerator.getInstance("AES");
kgen.init(128, new SecureRandom(password.getBytes()));
SecretKey secretKey = kgen.generateKey();
byte[] enCodeFormat = secretKey.getEncoded();
SecretKeySpec key = new SecretKeySpec(enCodeFormat, "AES");
Cipher cipher = Cipher.getInstance("AES");// 创建密码器
byte[] byteContent = content.getBytes("utf-8");
cipher.init(Cipher.ENCRYPT_MODE, key);// 初始化
byte[] result = cipher.doFinal(byteContent);
return result; // 加密
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} catch (NoSuchPaddingException e) {
e.printStackTrace();
} catch (InvalidKeyException e) {
e.printStackTrace();
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
} catch (IllegalBlockSizeException e) {
e.printStackTrace();
} catch (BadPaddingException e) {
e.printStackTrace();
}
return null;
}
2.2 解密
代码有详细注释,不多废话
注意:解密的时候要传入byte数组
view plaincopy to clipboardprint?
/**解密
* @param content 待解密内容
* @param password 解密密钥
* @return
*/
public static byte[] decrypt(byte[] content, String password) {
try {
KeyGenerator kgen = KeyGenerator.getInstance("AES");
kgen.init(128, new SecureRandom(password.getBytes()));
SecretKey secretKey = kgen.generateKey();
byte[] enCodeFormat = secretKey.getEncoded();
SecretKeySpec key = new SecretKeySpec(enCodeFormat, "AES");
Cipher cipher = Cipher.getInstance("AES");// 创建密码器
cipher.init(Cipher.DECRYPT_MODE, key);// 初始化
byte[] result = cipher.doFinal(content);
return result; // 加密
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} catch (NoSuchPaddingException e) {
e.printStackTrace();
} catch (InvalidKeyException e) {
e.printStackTrace();
} catch (IllegalBlockSizeException e) {
e.printStackTrace();
} catch (BadPaddingException e) {
e.printStackTrace();
}
return null;
}
/**解密
* @param content 待解密内容
* @param password 解密密钥
* @return
*/
public static byte[] decrypt(byte[] content, String password) {
try {
KeyGenerator kgen = KeyGenerator.getInstance("AES");
kgen.init(128, new SecureRandom(password.getBytes()));
SecretKey secretKey = kgen.generateKey();
byte[] enCodeFormat = secretKey.getEncoded();
SecretKeySpec key = new SecretKeySpec(enCodeFormat, "AES");
Cipher cipher = Cipher.getInstance("AES");// 创建密码器
cipher.init(Cipher.DECRYPT_MODE, key);// 初始化
byte[] result = cipher.doFinal(content);
return result; // 加密
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} catch (NoSuchPaddingException e) {
e.printStackTrace();
} catch (InvalidKeyException e) {
e.printStackTrace();
} catch (IllegalBlockSizeException e) {
e.printStackTrace();
} catch (BadPaddingException e) {
e.printStackTrace();
}
return null;
}
2.3 测试代码
view plaincopy to clipboardprint?
String content = "test";
String password = "12345678";
//加密
System.out.println("加密前:" + content);
byte[] encryptResult = encrypt(content, password);
//解密
byte[] decryptResult = decrypt(encryptResult,password);
System.out.println("解密后:" + new String(decryptResult));
String content = "test";
String password = "12345678";
//加密
System.out.println("加密前:" + content);
byte[] encryptResult = encrypt(content, password);
//解密
byte[] decryptResult = decrypt(encryptResult,password);
System.out.println("解密后:" + new String(decryptResult));
输出结果如下:
加密前:test
解密后:test
2.4 容易出错的地方
但是如果我们将测试代码修改一下,如下:
view plaincopy to clipboardprint?
String content = "test";
String password = "12345678";
//加密
System.out.println("加密前:" + content);
byte[] encryptResult = encrypt(content, password);
try {
String encryptResultStr = new String(encryptResult,"utf-8");
//解密
byte[] decryptResult = decrypt(encryptResultStr.getBytes("utf-8"),password);
System.out.println("解密后:" + new String(decryptResult));
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
String content = "test";
String password = "12345678";
//加密
System.out.println("加密前:" + content);
byte[] encryptResult = encrypt(content, password);
try {
String encryptResultStr = new String(encryptResult,"utf-8");
//解密
byte[] decryptResult = decrypt(encryptResultStr.getBytes("utf-8"),password);
System.out.println("解密后:" + new String(decryptResult));
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
则,系统会报出如下异常:
javax.crypto.IllegalBlockSizeException: Input length must be multiple of 16 when decrypting with padded cipher
at com.sun.crypto.provider.SunJCE_f.b(DashoA13*..)
at com.sun.crypto.provider.SunJCE_f.b(DashoA13*..)
at com.sun.crypto.provider.AESCipher.engineDoFinal(DashoA13*..)
at javax.crypto.Cipher.doFinal(DashoA13*..)
这主要是因为加密后的byte数组是不能强制转换成字符串的,换言之:字符串和byte数组在这种情况下不是互逆的;要避免这种情况,我们需要做一些修订,可以考虑将二进制数据转换成十六进制表示,主要有如下两个方法:
2.4.1将二进制转换成16进制
view plaincopy to clipboardprint?
/**将二进制转换成16进制
* @param buf
* @return
*/
public static String parseByte2HexStr(byte buf[]) {
StringBuffer sb = new StringBuffer();
for (int i = 0; i < buf.length; i++) {
String hex = Integer.toHexString(buf[i] & 0xFF);
if (hex.length() == 1) {
hex = '0' + hex;
}
sb.append(hex.toUpperCase());
}
return sb.toString();
}
/**将二进制转换成16进制
* @param buf
* @return
*/
public static String parseByte2HexStr(byte buf[]) {
StringBuffer sb = new StringBuffer();
for (int i = 0; i < buf.length; i++) {
String hex = Integer.toHexString(buf[i] & 0xFF);
if (hex.length() == 1) {
hex = '0' + hex;
}
sb.append(hex.toUpperCase());
}
return sb.toString();
}
2.4.2 将16进制转换为二进制
view plaincopy to clipboardprint?
/**将16进制转换为二进制
* @param hexStr
* @return
*/
public static byte[] parseHexStr2Byte(String hexStr) {
if (hexStr.length() < 1)
return null;
byte[] result = new byte[hexStr.length()/2];
for (int i = 0;i< hexStr.length()/2; i++) {
int high = Integer.parseInt(hexStr.substring(i*2, i*2+1), 16);
int low = Integer.parseInt(hexStr.substring(i*2+1, i*2+2), 16);
result[i] = (byte) (high * 16 + low);
}
return result;
}
/**将16进制转换为二进制
* @param hexStr
* @return
*/
public static byte[] parseHexStr2Byte(String hexStr) {
if (hexStr.length() < 1)
return null;
byte[] result = new byte[hexStr.length()/2];
for (int i = 0;i< hexStr.length()/2; i++) {
int high = Integer.parseInt(hexStr.substring(i*2, i*2+1), 16);
int low = Integer.parseInt(hexStr.substring(i*2+1, i*2+2), 16);
result[i] = (byte) (high * 16 + low);
}
return result;
}
然后,我们再修订以上测试代码,如下:
view plaincopy to clipboardprint?
String content = "test";
String password = "12345678";
//加密
System.out.println("加密前:" + content);
byte[] encryptResult = encrypt(content, password);
String encryptResultStr = parseByte2HexStr(encryptResult);
System.out.println("加密后:" + encryptResultStr);
//解密
byte[] decryptFrom = parseHexStr2Byte(encryptResultStr);
byte[] decryptResult = decrypt(decryptFrom,password);
System.out.println("解密后:" + new String(decryptResult));
String content = "test";
String password = "12345678";
//加密
System.out.println("加密前:" + content);
byte[] encryptResult = encrypt(content, password);
String encryptResultStr = parseByte2HexStr(encryptResult);
System.out.println("加密后:" + encryptResultStr);
//解密
byte[] decryptFrom = parseHexStr2Byte(encryptResultStr);
byte[] decryptResult = decrypt(decryptFrom,password);
System.out.println("解密后:" + new String(decryptResult));
测试结果如下:
加密前:test
加密后:73C58BAFE578C59366D8C995CD0B9D6D
解密后:test
2.5 另外一种加密方式
还有一种加密方式,大家可以参考如下:
view plaincopy to clipboardprint?
/**
* 加密
*
* @param content 需要加密的内容
* @param password 加密密码
* @return
*/
public static byte[] encrypt2(String content, String password) {
try {
SecretKeySpec key = new SecretKeySpec(password.getBytes(), "AES");
Cipher cipher = Cipher.getInstance("AES/ECB/NoPadding");
byte[] byteContent = content.getBytes("utf-8");
cipher.init(Cipher.ENCRYPT_MODE, key);// 初始化
byte[] result = cipher.doFinal(byteContent);
return result; // 加密
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} catch (NoSuchPaddingException e) {
e.printStackTrace();
} catch (InvalidKeyException e) {
e.printStackTrace();
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
} catch (IllegalBlockSizeException e) {
e.printStackTrace();
} catch (BadPaddingException e) {
e.printStackTrace();
}
return null;
}
/**
* 加密
*
* @param content 需要加密的内容
* @param password 加密密码
* @return
*/
public static byte[] encrypt2(String content, String password) {
try {
SecretKeySpec key = new SecretKeySpec(password.getBytes(), "AES");
Cipher cipher = Cipher.getInstance("AES/ECB/NoPadding");
byte[] byteContent = content.getBytes("utf-8");
cipher.init(Cipher.ENCRYPT_MODE, key);// 初始化
byte[] result = cipher.doFinal(byteContent);
return result; // 加密
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} catch (NoSuchPaddingException e) {
e.printStackTrace();
} catch (InvalidKeyException e) {
e.printStackTrace();
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
} catch (IllegalBlockSizeException e) {
e.printStackTrace();
} catch (BadPaddingException e) {
e.printStackTrace();
}
return null;
}
这种加密方式有两种限制
密钥必须是16位的
待加密内容的长度必须是16的倍数,如果不是16的倍数,就会出如下异常:
javax.crypto.IllegalBlockSizeException: Input length not multiple of 16 bytes
at com.sun.crypto.provider.SunJCE_f.a(DashoA13*..)
at com.sun.crypto.provider.SunJCE_f.b(DashoA13*..)
at com.sun.crypto.provider.SunJCE_f.b(DashoA13*..)
at com.sun.crypto.provider.AESCipher.engineDoFinal(DashoA13*..)
at javax.crypto.Cipher.doFinal(DashoA13*..)
要解决如上异常,可以通过补全传入加密内容等方式进行避免。
相关文章
- 暂无相关文章
用户点评