数据加密

在本节中,将介绍如何使用密码加密数据。 通常将数据加密视为保护通过不安全网络发送的数据的一种手段,尽管也可用于保护存储在文件,Java智能卡或许多其他应用程序中的数据。 基于密码的加密是JCE的一部分,包含执行加密的引擎(密码引擎)以及支持数据加密的几个类。

加密引擎介绍(Cryptographic Engines)

从本质上讲,JCE中所有加密操作的结构都与下图的表示类似。 核心是加密算法本身,称为引擎; 术语“算法”(algorithm)指对加密操作的特定实现。 引擎获取一些输入数据和密钥(密钥是可选的),并产生一组加密后的输出数据。 需要注意的是, 有些引擎不需要密钥作为输入的一部分。 此外,并非所有加密引擎都产生对称输出(Symmetric Output) ,也就是说,并不是总可以从输出的数据中重新还原原始文本。 此外,输出的大小通常与输入的大小不同。 在消息摘要和数字签名的情况下,输出大小是小的并且固定大小的字节数; 在加密引擎的情况下,输出大小通常略大于输入大小。

用于加密的加密引擎
密钥是使用大多数这些加密引擎的核心,在Java Security 包中,存在对密钥进行操作的类,包括可用于生成某些类型密钥的引擎。

密钥分类

密钥分为两种:对称密钥与非对称密钥

对称密钥加密,又称私钥加密或会话密钥加密算法,即信息的发送方和接收方使用同一个密钥去加密和解密数据。它的最大优势是加/解密速度快,适合于对大数据量进行加密,但密钥管理困难。

非对称密钥加密系统,又称公钥密钥加密。它需要使用不同的密钥来分别完成加密和解密操作,一个公开发布,即公开密钥,另一个由用户自己秘密保存,即私用密钥。信息发送者用公开密钥去加密,而信息接收者则用私用密钥去解密。公钥机制灵活,但加密和解密速度却比对称密钥加密慢得多。

所以在实际的应用中,人们通常将两者结合在一起使用,例如,对称密钥加密系统用于存储大量数据信息,而公开密钥加密系统则用于加密密钥。

密钥主要概念

1、密钥对,在非对称加密技术中,有两种密钥,分为私钥和公钥,私钥是密钥对所有者持有,不可公布,公钥是密钥对持有者公布给他人的。

2、公钥,公钥用来给数据加密,用公钥加密的数据只能使用私钥解密。

3、私钥,如上,用来解密公钥加密的数据,对数据进行签名

公钥和私钥是通过一种算法得到的一个密钥对(即一个公钥和一个私钥),将其中的一个向外界公开,称为公钥;另一个自己保留,称为私钥。通过这种算法得到的密钥对能保证在世界范围内是唯一的。

使用这个密钥对的时候,如果用其中一个密钥加密一段数据,必须用另一个密钥解密。比如用公钥加密数据就必须用私钥解密,如果用私钥加密也必须用公钥解密,否则解密将不会成功。

加密类型

加密大体上分为单向加密和双向加密,双向加密又可分为对称加密和非对称加密。

单向加密就是非可逆加密,就是不可解密的加密方法,由于其在加密后会生成唯一的加密串,故而经常用于检测数据传输过程中是否被修改。常见的单向加密有MD5、SHA、HMAC。

双向加密是可逆加密,采用秘钥进行加密和解密。包含对称加密和非对称加密。对称加密是使用同样的秘钥进行加密和解密,如对称加密常用的算法,DES、RC2、RC4、RC5、AES等。非对称加密一般用两个秘钥完成加密解密,这两个秘钥是公开秘钥(公钥)和私有秘钥(私钥),公钥加密,私钥解密;私钥加密,公钥解密,常见的非对称加密算法有:RSA、DSA。

Cipher类

Cipher类提供用于加密和解密的加密密码的功能。 加密是取数据(称为明文)和密钥的过程,并且产生对不知道密钥的第三方毫无意义的数据(密文)。 解密是相反的过程:取密文和密钥并产生明文。

对称与非对称密码学

有两种主要类型的加密:对称(也称为密钥)和非对称(或公钥密码)。 在对称密码学中,同一个密钥既能加密也能解密数据。 保持密钥私密对保持数据保密至关重要。 另一方面,非对称加密使用公钥/私钥对来加密数据。 用一个密钥加密的数据与另一个密钥解密。 用户首先生成公钥/私钥对,然后将公钥发布在任何人都可以访问的可信数据库中。 希望与该用户安全通信的用户使用检索到的公钥来加密数据。 只有私钥的持有者才能解密。 保密私钥对此方案至关重要。

不对称算法(如RSA)通常比对称算法慢得多。 这些算法不能有效地保护大量的数据。 实际上,不对称算法被用来交换较小的密钥,用来初始化对称算法。

流与分组密码

密码有两种主要类型:块和流。 分组密码一次处理整个块,通常是多个字节的长度。 如果没有足够的数据来创建完整的输入块,则必须填充数据:也就是说,在加密之前,必须添加虚拟字节以使密码块大小成倍数。 这些字节在解密阶段被剥离。 填充既可以由应用程序完成,也可以通过初始化来使用填充类型,例如“PKCS5PADDING”。 相比之下,流密码一次只能处理一个小单元(通常是一个字节,甚至一点点)的传入数据。 这允许密码处理任意数量的数据而不用填充。

操作模式

当使用简单的分组密码进行加密时,两个相同的明文块将总是产生相同的密文块。如果人们注意到重复文本块,那么试图破解密文的密码分析者将会有更容易的工作。为了增加文本的复杂性,反馈模式使用前面的输出块在应用加密算法之前改变输入块。第一个块需要一个初始值,这个值被称为初始化向量(IV)。由于IV在加密之前只是简单地改变数据,所以IV应该是随机的,但不一定需要保密。有多种模式,例如CBC(密码块链接),CFB(密码反馈模式)和OFB(输出反馈模式)。 ECB(电子码本模式)是一种不受块位置或其他密文块影响的模式。因为如果ECB密文使用相同的明文/密钥,ECB密文是相同的,这种模式通常不适合加密应用。

一些算法如AES和RSA允许不同长度的密钥,但其他算法则是固定的,如3DES。 使用更长的密钥进行加密通常意味着对消息恢复的更强的抵抗力。 像往常一样,安全和时间之间有一个折衷,所以选择适当的密钥长度。

创建Cipher对象

密码对象是通过使用密码getInstance()静态工厂方法之一获得的。 在这里,算法名称与其他引擎类稍有不同,因为它不仅指定算法名称,而且指定模式和填充方案。 它描述了在给定输入上执行的操作(或操作集)以产生一些输出。 输入参数总是包括密码算法的名称(例如,AES),并且可以加上模式和填充方案。

输入的形式是:

“algorithm/mode/padding” or
“algorithm”

例如,以下是有效的输入:

"AES/CBC/PKCS5Padding"

"AES"

如果只指定了一个加密算法名称,系统将确定在环境中是否有所需的模式实现,如果有多个模式和填充方案名称,则返回一个首选项。

建议使用完全指定算法,模式和填充的转换。 如果不这样做,提供者将使用默认值。 例如,SunJCE和SunPKCS11提供程序将ECB用作默认模式,将PKCS5Padding用作许多对称密码的默认填充。

这意味着在SunJCE提供商的情况下:

Cipher c1 = Cipher.getInstance(“AES/ECB/PKCS5Padding”);

Cipher c1 = Cipher.getInstance(“AES”);
是等同的语句。

ECB模式是最简单的块密码模式,并且是JDK / JRE中的默认模式。 ECB适用于单个数据块,但绝对不应该用于多个数据块。

初始化Cipher对象

通过getInstance获得的Cipher对象必须初始化为四种模式之一,在Cipher类中定义为最终整数常量。 这些模式可以通过它们的符号名称来引用,这些符号名称将在下面显示,同时还会描述每种模式的用途:

ENCRYPT_MODE
数据加密.
DECRYPT_MODE
数据解密.
WRAP_MODE
将java.security.Key包装为字节,以便可以安全地传输密钥.
UNWRAP_MODE
将之前包装的密钥解包到java.security.Key对象中.

每个密码初始化方法都采用操作模式参数(opmode),并初始化该模式的密码对象。 其他参数包括包含的密钥或证书,算法参数(params)以及随机源(random)。

要初始化Cipher对象,请调用以下init方法之一:

public void init(int opmode, Key key);

public void init(int opmode, Certificate certificate);

public void init(int opmode, Key key, SecureRandom random);

public void init(int opmode, Certificate certificate,
                 SecureRandom random);

public void init(int opmode, Key key,
                 AlgorithmParameterSpec params);

public void init(int opmode, Key key,
                 AlgorithmParameterSpec params, SecureRandom random);

public void init(int opmode, Key key,
                 AlgorithmParameters params);

public void init(int opmode, Key key,
                 AlgorithmParameters params, SecureRandom random);

如果需要参数的密码对象(例如,初始化向量)被初始化为加密,并且没有参数提供给初始化方法,则底层密码实现本身应该提供所需的参数,或者通过生成随机参数或者通过使用 默认的,特定于提供者的参数集合。

但是,如果需要参数的Cipher对象被初始化为解密,并且没有参数提供给init方法,则将引发InvalidKeyException或InvalidAlgorithmParameterException异常,具体取决于所使用的init方法。

必须使用与加密相同的参数进行解密。当一个Cipher对象被初始化时,它将失去所有先前获得的状态。 换句话说,初始化一个Cipher就相当于创建一个新的Cipher实例,并初始化它。

数据加解密

数据可以在一个步骤或多个步骤中加密或解密。 如果事先不知道数据将要运行多长时间,或者数据太长而无法一次存储在内存中,则多部分操作非常有用。
要在一个步骤中加密或解密数据,请调用其中一个doFinal方法:

public byte[] doFinal(byte[] input);

public byte[] doFinal(byte[] input, int inputOffset, int inputLen);

public int doFinal(byte[] input, int inputOffset,
                   int inputLen, byte[] output);

public int doFinal(byte[] input, int inputOffset,
                   int inputLen, byte[] output, int outputOffset)

要以多个步骤加密或解密数据,请调用其中一种更新方法:

public byte[] update(byte[] input);

public byte[] update(byte[] input, int inputOffset, int inputLen);

public int update(byte[] input, int inputOffset, int inputLen,
                  byte[] output);

public int update(byte[] input, int inputOffset, int inputLen,
                  byte[] output, int outputOffset)

多部分操作必须由上述doFinal方法之一终止:

public byte[] doFinal();

public int doFinal(byte[] output, int outputOffset);

如果填充(或非填充)已被请求作为指定参数的一部分,则所有doFinal方法都将处理任何必要的填充(或非填充)。

调用doFinal会将Cipher对象重置为通过调用init初始化时的状态。 也就是说,Cipher对象被重置并且可用于加密或解密(取决于在对init的调用中指定的操作模式)更多的数据。

封装和解包密钥

包装密钥可以将密钥从一个地方安全地转移到另一个地方。
Wrap / unwrap API使得编写代码更方便,因为它直接处理密钥对象。 该方法还可以安全地传输基于硬件的密钥。

要包装一个Key,首先要为WRAP_MODE初始化Cipher对象,然后调用以下内容:

public final byte[] wrap(Key key);
1
如果要将打包的密钥字节(调用wrap的结果)提供给其他人,请务必发送收件人需要的附加信息,以便进行解包:

密钥算法的名称
包装密钥的类型(Cipher.SECRET_KEY,Cipher.PRIVATE_KEY或Cipher.PUBLIC_KEY之一)

密钥算法名称可以通过从Key接口调用getAlgorithm方法来确定:

public String getAlgorithm();

要打开先前调用的包装返回的字节,请首先初始化UNWRAP_MODE的Cipher对象,然后调用以下内容:

public final Key unwrap(byte[] wrappedKey,
                        String wrappedKeyAlgorithm,
                        int wrappedKeyType));

这里,wrappedKey是从前一个包装调用返回的字节,wrappedKeyAlgorithm是与包装Key的相关的算法,wrappedKeyType是包装的Key的类型,必须是Cipher.SECRET_KEY,Cipher.PRIVATE_KEY或Cipher.PUBLIC_KEY之一。

管理算法参数

底层Cipher实现使用的参数(通过应用程序显式传递给init方法)可以通过调用其getParameters方法从Cipher对象中检索,该方法将参数作为java返回 给AlgorithmParameters对象(如果没有使用参数,则返回null)。 如果参数是初始化向量(IV),则也可以通过调用getIV方法来检索。

在以下示例中,实现基于密码的加密(PBE)的Cipher对象仅使用一个键而没有参数进行初始化。 但是,所选择的基于密码的加密算法需要两个参数 – 一个salt和一个迭代计数。 这些将由底层算法实现本身生成。 应用程序可以从Cipher对象中检索生成的参数,如下所示:

import javax.crypto.*;
import java.security.AlgorithmParameters;

// get cipher object for password-based encryption
Cipher c = Cipher.getInstance("PBEWithHmacSHA256AndAES_256");

// initialize cipher for encryption, without supplying
// any parameters. Here, "myKey" is assumed to refer
// to an already-generated key.
c.init(Cipher.ENCRYPT_MODE, myKey);

// encrypt some data and store away ciphertext
// for later decryption
byte[] cipherText = c.doFinal("This is just an example".getBytes());

// retrieve parameters generated by underlying cipher
// implementation
AlgorithmParameters algParams = c.getParameters();

// get parameter encoding and store it away
byte[] encodedAlgParams = algParams.getEncoded();

必须使用与加密相同的参数进行解密。 它们可以从它们的编码实例化,并用于初始化相应的Cipher对象进行解密,如下所示:

 import javax.crypto.*;
import java.security.AlgorithmParameters;

// get parameter object for password-based encryption
AlgorithmParameters algParams;
algParams = AlgorithmParameters.getInstance("PBEWithHmacSHA256AndAES_256");

// initialize with parameter encoding from above
algParams.init(encodedAlgParams);

// get cipher object for password-based encryption
Cipher c = Cipher.getInstance("PBEWithHmacSHA256AndAES_256");

// initialize cipher for decryption, using one of the
// init() methods that takes an AlgorithmParameters
// object, and pass it the algParams object from above
c.init(Cipher.DECRYPT_MODE, myKey, algParams);

如果在初始化Cipher对象时没有指定任何参数,并且77不确定底层实现是否使用任何参数,则可以通过简单地调用Cipher对象的getParameters方法并检查返回的值来找到。 返回值为null表示没有使用参数。

SunJCE提供者实现的以下密码算法使用参数:

AES,DES-EDE和Blowfish在使用反馈(即CBC,CFB,OFB或PCBC)模式时,使用初始化向量(IV)。 javax.crypto.spec.IvParameterSpec类可用于使用给定的IV初始化Cipher对象。
PBE密码算法使用一组参数,包括Salt和迭代计数。 javax.crypto.spec.PBEParameterSpec类可用于初始化实现PBE算法的Cipher对象(例如:PBEWithHmacSHA256AndAES_256),并使用给定的salt和迭代次数。

其他基于Cipher的类

CipherInputStream类

这个类是一个FilterInputStream,用于加密或解密通过它的数据。 它由一个InputStream或其一个子类和一个Cipher组成。 CipherInputStream表示一个安全的输入流,一个Cipher对象被插入到其中。 CipherInputStream的读取方法返回从底层InputStream中读取的数据,但是嵌入的Cipher对象已经处理了额外的数据。 Cipher对象在被CipherInputStream使用之前必须完全初始化。
假设cipher1已经被初始化用于加密。 下面的代码演示了如何使用包含该密码和FileInputStream的CipherInputStream来加密输入流数据:

FileInputStream fis;
FileOutputStream fos;
CipherInputStream cis;

fis = new FileInputStream("/tmp/a.txt");
cis = new CipherInputStream(fis, cipher1);
fos = new FileOutputStream("/tmp/b.txt");
byte[] b = new byte[8];
int i = cis.read(b);
while (i != -1) {
    fos.write(b, 0, i);
    i = cis.read(b);
}
fos.close();

上面的程序从文件/tmp/a.txt中读取和加密内容,然后将结果(加密字节)存储在/tmp/b.txt中。

以下示例演示如何轻松连接CipherInputStream和FileInputStream的多个实例。 在这个例子中,假设cipher1和cipher2已分别被加密和解密初始化(使用相应的密钥)。

FileInputStream fis;
FileOutputStream fos;
CipherInputStream cis1, cis2;

fis = new FileInputStream("/tmp/a.txt");
cis1 = new CipherInputStream(fis, cipher1);
cis2 = new CipherInputStream(cis1, cipher2);
fos = new FileOutputStream("/tmp/b.txt");
byte[] b = new byte[8];
int i = cis2.read(b);
while (i != -1) {
    fos.write(b, 0, i);
    i = cis2.read(b);
}
fos.close();

上面的程序将文件/tmp/a.txt中的内容复制到/tmp/b.txt中,除了内容首先被加密,然后在从/tmp/a.txt读取内容时解密。 当然,因为这个程序只是简单地加密文本并立即解密,实际上它并不是非常有用,除非作为一个简单的方式来说明CipherInputStreams的链接。

CipherOutputStream类

这个类是FilterOutputStream,用于加密或解密通过它的数据。 它由一个OutputStream或其一个子类和一个Cipher组成。 CipherOutputStream表示一个安全的输出流,一个Cipher对象插入其中。 CipherOutputStream的写入方法首先使用嵌入的Cipher对象处理数据,然后将它们写出到底层的OutputStream中。 Cipher对象在被CipherOutputStream使用之前必须完全初始化。

假设cipher1已经被初始化用于加密。 下面的代码演示了如何使用包含该密码和FileOutputStream的CipherOutputStream来加密要写入输出流的数据:

FileInputStream fis;
FileOutputStream fos;
CipherOutputStream cos;

fis = new FileInputStream("/tmp/a.txt");
fos = new FileOutputStream("/tmp/b.txt");
cos = new CipherOutputStream(fos, cipher1);
byte[] b = new byte[8];
int i = fis.read(b);
while (i != -1) {
    cos.write(b, 0, i);
    i = fis.read(b);
}
cos.flush();

上面的程序从文件/tmp/a.txt中读取内容,然后将结果(加密字节)加密并存储在/tmp/b.txt中。

以下示例演示如何轻松连接CipherOutputStream和FileOutputStream的多个实例。 在这个例子中,假设cipher1和cipher2已分别被初始化用于解密和加密(使用相应的密钥):

FileInputStream fis;
FileOutputStream fos;
CipherOutputStream cos1, cos2;

fis = new FileInputStream("/tmp/a.txt");
fos = new FileOutputStream("/tmp/b.txt");
cos1 = new CipherOutputStream(fos, cipher1);
cos2 = new CipherOutputStream(cos1, cipher2);
byte[] b = new byte[8];
int i = fis.read(b);
while (i != -1) {
    cos2.write(b, 0, i);
    i = fis.read(b);
}
cos2.flush();

上述程序将文件/tmp/a.txt中的内容复制到/tmp/b.txt中,除了内容先被加密,然后在写入/tmp/b.txt之前将其解密。

使用分组密码算法时需要注意的一点是,在将数据加密并发送到底层输出流之前,必须给CipherOutputStream一个完整的明文数据块。

DES对称加密算法示例演示

示例如下:

import java.security.; import javax.crypto.;
import javax.crypto.spec.*;
public class CipherTest {
public static void main(String args[]) {
try {
// First, we need a key; we’ll just generate one
// though we could look one up in the keystore or
// obtain one via a key agreement algorithm.
KeyGenerator kg = KeyGenerator.getInstance(“DES”);
Cipher c = Cipher.getInstance(“DES/CBC/PKCS5Padding”);
Key key = kg.generateKey( );
// Now we’ll do the encryption. We’ll also retrieve
// the initialization vector, which the engine will
// calculate for us.
c.init(Cipher.ENCRYPT_MODE, key);
byte input[] = “Stand and unfold yourself”.getBytes( );
byte encrypted[] = c.doFinal(input);
byte iv[] = c.getIV( );
// Now we’ll do the decryption. The initialization
// vector can be transmitted to the recipient with
// the ciphertext, but the key must be transmitted
// securely.
IvParameterSpec dps = new IvParameterSpec(iv);
c.init(Cipher.DECRYPT_MODE, key, dps);
byte output[] = c.doFinal(encrypted);
System.out.println(“The string was “);
System.out.println(new String(output));
} catch (Exception e) {
e.printStackTrace( );
} }
}

使用一个引擎对象来执行加密和解密。由于DES是对称加密算法,因此生成了一个用于两种操作的密钥。在try块中,第二个代码块执行加密:

1.初始化用于加密的密码引擎;
2.将要加密的字节传递给doFinal()方法。已经指定了填充方案,不必担心传递给doFinal()方法的数据大小;
3.最后,保存系统提供的初始化向量以执行加密。注意,ECB模式不需要此步骤,ECB模式不需要初始化向量;

执行解密类似:
1.首先,初始化密码引擎进行解密。但是,在这种情况下,必须提供一个初始化向量来初始化引擎以获得正确的结果(同样,这对于ECB模式来说也是不必要的);
2.接下来,将加密数据传递给doFinal()方法。同样,我们可能首先多次调用update()方法;
当然,在实际使用中,加密是在一个程序中完成的,而解密是在另一个程序中完成的。在上面的例子中,这将需要将初始化矢量和加密数据发送到接收器;可以通过套接字或文件或任何其他方便的手段来完成。传输初始化向量没有安全风险,因为它具有与其余加密数据相同的属性。所以可以简单地将字节数组写入数据接收器,然后是数据本身;接收器将读取字节数组,使用它来构造IvParameterSpec对象,然后解密数据。

数据加密

发表评论

电子邮件地址不会被公开。 必填项已用*标注

二十 九 − = 20