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)