<!-- /* 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 enc StreamIn1.txt mytest.txt
来运行程序,则将把 StreamIn1.txt 中的内容加密成为文件 mytest.txt。
若进一步运行 :
java StreamOut dec mytest.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 加密方式。