13.4.2 加密与解密
加密的目的是保证数据被窃取之后,数据中所包含的信息仍然不会泄露。加密的过程是把原始数据(明文)转换成第三方无法识别的内容(密文),解密的过程则是把密文重新转换成明文。在这个过程中必不可少的关键部分是密钥。密钥是加密和解密算法实现的基础。加密算法一般分为对称和非对称两种。对称加密算法使用同一个密钥进行加密和解密,而非对称加密算法使用一对密钥分别进行加密和解密,两个密钥分别称为公钥和私钥。使用其中一个密钥进行加密的数据,需要使用另外一个密钥来解密。非对称算法的公钥是公开的,而私钥则由程序妥善保存。需要与程序进行通信的其他代码使用公钥对数据进行加密,程序使用保存的私钥对数据进行解密。
1.密钥
密钥在Java密码框架中有两种表示方式,一种是基于java.security.Key接口的不透明表示方式,另一种是基于java.security.spec.KeySpec接口的透明表示方式。在程序中使用密钥时,可以直接从相关工厂方法中得到所需的Key接口的实现对象。Key接口所提供的操作比较有限,不能通过该接口获取密钥具体实现的内部细节。通过Key接口的getAlgorithm方法可以得到该密钥所使用的算法。当密钥在不同组件之间传输时,需要使用一些标准的格式进行编码。使用getFormat方法可以得到编码格式的名称,而使用getEncoded方法可以获得编码之后的字节数组。对于对称算法来说,javax.crypto.SecretKey接口表示唯一的私钥;对于非对称算法来说,java.security.PublicKey接口和java.security.PrivateKey接口分别表示公钥和私钥。KeySpec只是一个标记接口,并没有具体的方法。从KeySpec接口的具体实现类中可以获取密钥底层实现的具体细节。通过java.security.KeyFactory和javax.crypto.SecretKeyFactory类中的方法可以在Key接口和KeySpec接口的对象之间互相转换,前者和后者分别适合非对称算法和对称算法使用的密钥。
密钥的获取通过标准的服务提供者来完成,对称算法使用javax.crypto.KeyGenerator类,而非对称算法使用java.security.KeyPairGenerator类。通过静态方法getInstance获取服务提供者的具体实现对象。在使用这两个类的对象之前,需要调用init方法进行初始化。如果没有调用init方法进行初始化,那么两个类使用默认设置来生成密钥。
2.加密与解密的过程
加密和解密功能由服务javax.crypto.Cipher类提供。创建Cipher类的实现对象也是通过getInstance方法来完成。在调用getInstance方法时需要指定加密时的转换方式。转换方式包括必要的算法名称及可选的反馈模式和填充方式。接着调用init方法通过合适的密钥把Cipher类的对象初始化成所需的加密或解密模式。如果需要加密或解密的数据过多,那么可以分成多次来进行处理。在每次处理中调用update方法来提供数据。在最后一次处理中调用doFinal方法来完成。如果数据较少,可以直接使用doFinal方法来提供数据并同时完成处理。调用doFinal方法的返回值是加密或解密之后的结果数据。
代码清单13-16给出了对称加密和解密操作的一个示例。在加密方法encrypt中,先使用KeyGenerator类的对象来生成所需的密钥,并把密钥编码之后的格式保存下来。在得到Cipher类的对象并完成初始化之后,对一段文本进行加密处理,把加密之后的结果数据保存下来。使用对称加密方式的双方需要通过可靠的方式来传递密钥。在示例中是通过文件的方式来传递的。在解密方法decrypt中,先读取保存的密钥数据,从中创建出SecretKeySpec类的对象,再使用SecretKeySpec类的对象把Cipher类的对象初始化为解密模式,最后对加密的数据进行处理即可得到原始的明文数据。
代码清单13-16 对称加密和解密操作的示例
public class SymmetricEncryption{
public void encrypt()throws Exception{
KeyGenerator generator=KeyGenerator.getInstance("DES");
SecretKey key=generator.generateKey();
Files.write(Paths.get("key.data"),key.getEncoded());
Cipher cipher=Cipher.getInstance("DES");
cipher.init(Cipher.ENCRYPT_MODE, key);
String text="Hello World";
byte[]encrypted=cipher.doFinal(text.getBytes());
Files.write(Paths.get("encrypted.bin"),encrypted);
}
public void decrypt()throws Exception{
byte[]keyData=Files.readAllBytes(Paths.get("key.data"));
SecretKeySpec keySpec=new SecretKeySpec(keyData,"DES");
Cipher cipher=Cipher.getInstance("DES");
cipher.init(Cipher.DECRYPT_MODE, keySpec);
byte[]data=Files.readAllBytes(Paths.get("encrypted.bin"));
byte[]result=cipher.doFinal(data);
System.out.println(new String(result));
}
}
非对称加密的实现方式类似于对称加密的方式,只不过在加密和解密时使用的密钥不同。
要对一个数据流进行加密和解密操作,可以使用javax.crypto.CipherInputStream类和javax.crypto.CipherOutputStream类这两个过滤流实现。在创建这两个流的对象时都需要使用一个Cipher类的对象。在CipherOutputStream类的对象写入数据时,会先通过Cipher类的对象对数据进行加密处理,再把加密的结果写入所包装的底层输出流中;CipherInputStream类的对象进行数据读取时,会先对从底层输入流中读取的数据进行解密处理,再把解密的结果返回给调用者。代码清单13-17给出了使用加密方式把对象序列化之后的内容安全保存在磁盘上的示例。
代码清单13-17 使用加密方式保存对象序列化之后的内容
public void storeSafely(Serializable obj, Cipher cipher, Path path)throws IOException{
try(ObjectOutputStream oos=new ObjectOutputStream(new CipherOutputStream(Files.newOutputStream(path),cipher))){
oos.writeObject(obj);
}
}
Cipher类、CipherInputStream类和CipherOutputStream类所处理的对象都是字节流。如果需要加密Java对象,可以使用javax.crypto.SealedObject类。SealedObject类可以用来封装一个实现了Serializable接口的对象。在创建SealedObject类的对象时需要提供进行加密操作的Cipher类的对象。使用者通过getObject方法来获取所封装的对象,不过在调用getObject方法时需要提供进行解密操作的Cipher类的对象或密钥。