Java安全编程笔记【2】【续2】------数据内容的保护---加密和解密。

<!-- /* 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 加密方式。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值