jenkov.tutorials系列(Java加密与解密概览)
Java密码学扩展
Java加密API由正式称为Java加密扩展的Java提供。 Java密码学扩展有时也通过缩写JCE来引用。
Java密码体系结构
Java密码学体系结构(JCA)是Java密码学API内部设计的名称。
JCA是围绕一些中央通用类和接口构建的。 这些接口背后的实际功能由提供程序提供。 因此,您可以使用Cipher类来加密和解密某些数据,但是具体的密码实现(加密算法)取决于所使用的具体提供程序。
核心类以及接口
Java加密API分为以下Java软件包:
java.security
java.security.cert
- java.security.spec
- java.security.interfaces
- javax.crypto
- javax.crypto.spec
- javax.crypto.interfaces
这些软件包的核心类和接口是:
Provider
SecureRandom
Cipher
MessageDigest
Signature
Mac
AlgorithmParameters
AlgorithmParameterGenerator
KeyFactory
SecretKeyFactory
KeyPairGenerator
KeyGenerator
KeyAgreement
KeyStore
CertificateFactory
CertPathBuilder
CertPathValidator
CertStore
Provider
Provider(java.security.Provider)类是Java加密API中的中心类。 为了使用Java crypto API,您需要提供程序集。 Java SDK带有自己的加密提供程序。 如果您未设置显式密码提供程序,则使用Java SDK默认提供程序。 但是,此提供程序可能不支持您要使用的加密算法。 因此,您可能必须设置自己的加密提供程序。
Java加密API最受欢迎的加密提供程序之一称为Bouncy Castle。 这是设置BouncyCastleProvider的示例:
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import java.security.Security;
public class ProviderExample {
public static void main(String[] args) {
Security.addProvider(new BouncyCastleProvider());
}
}
Cipher
Cipher(javax.crypto.Cipher)类表示密码算法。 密码可用于加密和解密数据。 Java Cipher类的文本中对Cipher类进行了更详细的说明,但是在以下各节中,我将对Cipher类进行简要介绍。
以下是创建Java Cipher实例的方法:
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
本示例创建一个Cipher实例,该实例在内部使用AES加密算法。
Cipher.getInstance(...)方法采用String标识使用的加密算法以及该算法的其他一些配置。 在上面的示例中,CBC部分是AES算法可以使用的模式。PKCS5Padding部分是AES算法应如何处理数据的最后字节(如果数据与64位或128位不对齐) 块大小边界。 确切的说,这通常属于有关加密的教程,而不是有关Java加密API的教程。
初始化
必须先初始化Cipher实例,然后才能使用它。 您可以通过调用实例实例的init()方法来初始化实例实例。 init()方法采用两个参数:
- 加密/解密密码模式
- 键
第一个参数指定密码实例应加密还是解密数据。 第二个参数指定它们用于加密或解密数据的密钥。
byte[] keyBytes = new byte[]{0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15};
String algorithm = "RawBytes";
SecretKeySpec key = new SecretKeySpec(keyBytes, algorithm);
cipher.init(Cipher.ENCRYPT_MODE, key);
请注意,在此示例中创建密钥的方式并不安全,因此不应在实践中使用。 该Java密码学教程将在后面的部分中介绍如何更安全地创建密钥。
要初始化Cipher实例以解密数据,您必须使用Cipher.DECRYPT_MODE,如下所示
cipher.init(Cipher.DECRYPT_MODE, key);
加密 & 解密数据
正确初始化密码后,即可开始加密或解密数据。 您可以通过调用Cipher update()或doFinal()方法来实现。
如果要加密或解密较大数据块的一部分,则使用update()方法。 当您加密大块数据的最后一部分时,或者如果您传递给doFinal()的块代表要加密的完整数据块,则会调用doFinal()方法。
byte[] plainText = "abcdefghijklmnopqrstuvwxyz".getBytes("UTF-8");
byte[] cipherText = cipher.doFinal(plainText);
要解密数据,您需要将密文(加密的数据)传递到doFinal()或doUpdate()方法中。
Keys
要加密或解密数据,您需要一个密钥。 密钥有两种类型-取决于您使用哪种加密算法:
- 对称
- 非对称密钥
对称密钥用于对称加密算法。 对称加密算法使用相同的密钥进行加密和解密。
非对称密钥用于非对称加密算法。 非对称加密算法将一个密钥用于加密,将另一个密钥用于解密。 公钥-私钥加密算法是非对称加密算法的示例。
需要解密数据的一方以某种方式需要知道解密数据所需的密钥。 如果解密数据的一方与加密数据的一方不同,那么这两个当事方就需要以某种方式商定密钥或交换密钥。 这称为密钥交换
密钥安全
密钥应该很难猜到,因此攻击者无法轻易猜出加密密钥。 上一节中有关Cipher类的示例使用了一个非常简单的硬编码键。 实际上这不是一个好主意。 如果他们的密钥很容易猜到,那么攻击者很容易解密加密的数据并可能自己创建虚假消息。
重要的是,使钥匙难以猜测。 因此,密钥应由随机字节组成。 随机性越好,字节越多,则越难猜测,因为存在更多可能的组合
创建密钥
您可以使用Java KeyGenerator类来生成更多随机加密密钥。 关于Java KeyGenerator的文本中更详细地介绍了KeyGenerator,但在这里我将向您展示如何使用它的示例。
KeyGenerator keyGenerator = KeyGenerator.getInstance("AES");
SecureRandom secureRandom = new SecureRandom();
int keyBitSize = 256;
keyGenerator.init(keyBitSize, secureRandom);
SecretKey secretKey = keyGenerator.generateKey();
可以将生成的SecretKey实例传递给Cipher.init()方法,如下所示:
cipher.init(Cipher.ENCRYPT_MODE, secretKey);
创建密钥对
非对称加密算法使用由公共密钥和私有密钥组成的密钥对来加密和解密数据。 要生成非对称密钥对,可以使用KeyPairGenerator(java.security.KeyPairGenerator)。 Java KeyPairGenerator教程中详细介绍了KeyPairGenerator,但这是一个简单的Java KeyPairGenerator示例:
SecureRandom secureRandom = new SecureRandom();
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("DSA");
KeyPair keyPair = keyPairGenerator.generateKeyPair();
密钥存储
Java KeyStore是可以包含密钥的数据库。 Java KeyStore由KeyStore(java.security.KeyStore)类表示。 密钥库可以保存以下类型的密钥:
Private keys
Public keys + certificates
Secret keys
私钥和公钥用于非对称加密。 公钥可以具有关联的证书。 证书是一种文件,用于验证声称拥有公钥的个人,组织或设备的身份。 证书通常由验证方进行数字签名以作为证明。
秘密密钥用于对称加密
Keytool
Java Keytool是可以与Java KeyStore文件一起使用的命令行工具。 Keytool可以将密钥对生成到KeyStore文件中,从中导出证书,以及将证书导入KeyStore和其他几个功能。
MessageDigest
当您从其他人那里收到一些加密数据时,您怎么知道没有人在去您的途中修改过加密数据?
一种常见的解决方案是在对数据进行加密之前,先从数据中计算出消息摘要,然后对数据和消息摘要进行加密,然后通过网络进行发送。消息摘要是根据消息数据计算出的哈希值。如果更改了加密数据中的字节,则根据数据计算的消息摘要也将更改。
接收加密的数据时,可以对其解密并从中计算出消息摘要,然后将计算出的消息摘要与随加密数据一起发送的消息摘要进行比较。如果两个消息摘要相同,则很有可能未修改数据(但不是100%保证)。
您可以使用Java MessageDigest(java.security.MessageDigest)计算消息摘要。您调用MessageDigest.getInstance()方法来创建MessageDigest实例。有几种不同的消息摘要算法可用。在创建MessageDigest实例时,您需要确定要使用哪种算法。
MessageDigest messageDigest = MessageDigest.getInstance("SHA-256");
本示例创建MessageDigest实例,该实例在内部使用SHA-256密码哈希算法来计算消息摘要。
为了计算某些数据的消息摘要,请调用update()或digest()方法。
可以多次调用update()方法,并且消息摘要在内部进行更新。 传递完要包含在消息摘要中的所有数据后,可以调用digest()并获取结果消息摘要数据。 这是多次调用update()并随后进行digest()调用的示例:
MessageDigest messageDigest = MessageDigest.getInstance("SHA-256");
byte[] data1 = "0123456789".getBytes("UTF-8");
byte[] data2 = "abcdefghijklmnopqrstuvxyz".getBytes("UTF-8");
messageDigest.update(data1);
messageDigest.update(data2);
byte[] digest = messageDigest.digest();
您也可以一次调用digest(),传递所有数据以计算消息摘要。 看起来是这样的:
MessageDigest messageDigest = MessageDigest.getInstance("SHA-256");
byte[] data1 = "0123456789".getBytes("UTF-8");
byte[] digest = messageDigest.digest(data1);
Mac
Java Mac类用于从消息创建MAC。 术语MAC是消息认证代码的缩写。 MAC与消息摘要类似,但是使用其他密钥来加密消息摘要。 只有同时拥有原始数据和密钥,您才能验证MAC。 因此,与消息摘要相比,MAC是保护数据块免受修改的更安全的方法。 Mac类在Java Mac教程中有更详细的描述,但是下面是简短的介绍。
通过调用Mac.getInstance()方法并传递要使用的算法名称作为参数来创建Java Mac实例。 看起来是这样的:
Mac mac = Mac.getInstance("HmacSHA256");
必须先使用密钥初始化Mac实例,然后才能根据数据创建MAC。 这是使用密钥初始化Mac实例的示例:
byte[] keyBytes = new byte[]{0,1,2,3,4,5,6,7,8 ,9,10,11,12,13,14,15};
String algorithm = "RawBytes";
SecretKeySpec key = new SecretKeySpec(keyBytes, algorithm);
mac.init(key)
初始化Mac实例后,您可以通过调用update()和doFinal()方法从数据中计算MAC。 如果拥有用于计算MAC的所有数据,则可以立即调用doFinal()方法。 看起来是这样的:
byte[] data = "abcdefghijklmnopqrstuvxyz".getBytes("UTF-8");
byte[] macBytes = mac.doFinal(data);
如果您只能在单独的块中访问数据,请对数据多次调用update(),然后结束对doFinal()的调用。 看起来是这样的:
byte[] data = "abcdefghijklmnopqrstuvxyz".getBytes("UTF-8");
byte[] data2 = "0123456789".getBytes("UTF-8");
mac.update(data);
mac.update(data2);
byte[] macBytes = mac.doFinal();
签名
Signature(java.security.Signature)类用于数字签名数据。 对数据签名后,将从该数据创建数字签名。 因此,签名与数据是分开的。
通过从数据创建消息摘要(哈希),并使用要对数据签名的设备,个人或组织的私钥对该消息摘要进行加密,可以创建数字签名。 加密的消息摘要称为数字签名。
若要创建一个Signature实例,请调用Signature.getInstance(...)方法。 这是一个创建Signature实例的示例:
Signature signature = Signature.getInstance("SHA256WithDSA");
签名数据
要签名数据,必须以签名模式初始化Signature实例。 为此,您可以调用initSign(...)方法,并传递用于签名数据的私钥。 这是在签名模式下初始化Signature实例的方式:
signature.initSign(keyPair.getPrivate(), secureRandom);
一旦签名实例被初始化,它就可以用来签名数据。 您可以通过调用update()来传递数据,以将其作为参数签名。 您可以多次调用update()方法,并在创建签名时包含更多数据。 当所有数据都传递给update()方法后,您可以调用sign()方法来获取数字签名。 看起来是这样的:
byte[] data = "abcdefghijklmnopqrstuvxyz".getBytes("UTF-8");
signature.update(data);
byte[] digitalSignature = signature.sign();
验证签名
要验证签名,必须将签名实例初始化为验证模式。 这是通过调用initVerify(...)方法来完成的,该方法将用作验证签名的公钥作为参数传递。 现在正在将一个Signature实例初始化为验证模式:
Signature signature = Signature.getInstance("SHA256WithDSA");
signature.initVerify(keyPair.getPublic());
初始化为验证模式后,您可以使用签名正在签名的数据调用update()方法,并结束对verify()的调用,该调用根据是否可以验证签名而返回true或false。 这是验证签名的样子:
byte[] data2 = "abcdefghijklmnopqrstuvxyz".getBytes("UTF-8");
signature2.update(data2);
boolean verified = signature2.verify(digitalSignature);
完整签名和验证示例
SecureRandom secureRandom = new SecureRandom();
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("DSA");
KeyPair keyPair = keyPairGenerator.generateKeyPair();
Signature signature = Signature.getInstance("SHA256WithDSA");
signature.initSign(keyPair.getPrivate(), secureRandom);
byte[] data = "abcdefghijklmnopqrstuvxyz".getBytes("UTF-8");
signature.update(data);
byte[] digitalSignature = signature.sign();
Signature signature2 = Signature.getInstance("SHA256WithDSA");
signature2.initVerify(keyPair.getPublic());
signature2.update(data);
boolean verified = signature2.verify(digitalSignature);
System.out.println("verified = " + verified);