在本章中,我们将探讨 Java 为保护应用程序之间的通信提供的支持。我们将研究几个主题,包括以下内容:
- 基本加密过程
- 使用密钥库存储密钥和证书
- 向简单服务器/客户端添加加密
- 使用 TLS\SSL 保护客户端/服务器通信
- 安全散列
有许多与安全相关的术语,其含义和目的在第一次遇到时可能会令人望而生畏。这些术语大多适用于网络应用。我们将从这些术语的简要概述开始。在本章后面的章节中,我们将详细介绍与我们讨论相关的内容。
大多数安全相关问题的核心是加密。这是使用一个密钥或一组密钥将需要保护的信息转换为加密形式的过程。加密信息的接收者可以使用一个密钥或一组密钥来解密信息并将其还原为原始形式。此技术将防止未经授权访问信息。
我们将演示对称和非对称加密技术的使用。对称加密使用单个密钥加密和解密消息。非对称加密使用一对密钥。这些密钥通常存储在名为密钥库的文件中,我们将对此进行演示。
对称加密通常更快,但要求加密数据的发送方和接收方以安全的方式共享密钥。对于远程分散的各方来说,这可能是一个问题。非对称加密速度较慢,但它使用公钥和私钥对,正如我们将看到的,这简化了密钥共享。非对称加密是一种支持数字证书的技术,它提供了一种验证文档真实性的方法。
安全商务非常普遍,对于全球每天发生的在线交易至关重要。传输层安全****TLS和安全套接字层****SSL是允许通过互联网进行安全可靠通信的协议。它是用于在互联网上进行大多数交易的超文本传输协议安全(HTTPS的基础。此协议支持以下功能:
- 服务器和客户端身份验证
- 数据加密
- 数据完整性
安全哈希是一种用于创建证书的技术。证书用于验证数据的真实性,并使用哈希值。Java 为这个过程提供了支持,我们将对此进行演示。
让我们先简要介绍一下常见的网络安全术语,以提供本章的高级视角。具体术语将在后续章节中进行更详细的探讨。
在处理安全通信时,会用到几个术语。这些措施包括:
- 认证:这是验证用户或系统的过程
- 授权:这是允许访问受保护资源的过程
- 加密:这是对信息进行编码和随后解码的过程,以保护信息免受未经授权的个人的攻击
- 散列算法:它们提供了一种方式为文档生成唯一值,并用于支持其他安全技术
- 数字签名:这些提供了一种对文档进行数字认证的方法
- 证书:这些证书通常被作为一条链使用,支持主体和其他参与者身份的确认
认证与授权相关。身份验证是确定一个人或系统是否是他们所声称的人的过程。这通常通过使用 ID 和密码来实现。然而,还有其他认证技术,如智能卡和生物特征签名,如指纹或虹膜扫描。
授权是确定个人或系统可以访问哪些资源的过程。验证一个人是他们所说的人是一回事。另一件事是确保用户只能访问授权的资源。
加密已经发展并将继续改进。Java 支持对称和非对称加密技术。该过程从生成密钥开始,密钥通常存储在密钥库中。需要加密或解密数据的应用程序将访问密钥库以检索适当的密钥。密钥库本身需要受到保护,以便它不会被篡改或以其他方式受损。
散列是获取数据并返回表示数据的数字的过程。哈希算法执行此操作,并且必须快速。然而,当仅给出散列值时,即使不是不可能,也极难导出原始数据。这称为单向散列函数。
这种技术的优点是可以将数据与哈希值一起发送到接收器。数据未加密,但哈希值使用一组非对称密钥加密。然后,接收器可以使用原始散列算法来计算所接收数据的散列值。如果这个新的散列值与发送的散列值相匹配,那么可以向接收方保证数据在传输中没有被修改或损坏。这提供了一种更可靠的传输数据的方法,该方法不需要加密,但可以保证数据未被修改。
证书是前一个过程的一部分,它使用哈希函数和非对称密钥。证书链提供了一种验证证书是否有效的方法,假设链的根可以信任。
在本节中,我们将研究 Java 如何支持对称和非对称加密。正如我们将看到的,有各种加密算法可用于这两种技术。
对称加密使用单个密钥对消息进行加密和解密。这种类型的加密分为流密码或分组密码。有关这些算法的更多详细信息,请访问https://en.wikipedia.org/wiki/Symmetric-key_algorithm 。提供者提供加密算法的实现,我们通常在两者之间进行选择。
Java 支持的对称算法包括以下算法,其中以位为单位的密钥大小用括号括起来:
- AES(128)
- DES(56)
- 德塞德(168)
- HmacSHA1
- HmacSHA256
可以对不同长度的数据进行加密。分组密码算法用于处理大数据块。下面列出了几种分组密码操作模式。我们在此不详细说明这些模式是如何工作的,但更多的信息可在中找到 https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation :
- 欧洲央行
- CBC
- 循环流化床
- OFB
- 多氯联苯
在加密或解密数据之前,我们需要一个密钥。
生成密钥的常用方法是使用KeyGenerator
类。该类没有公共构造函数,但重载的getInstance
方法将返回KeyGenerator
实例。以下示例将 AES 算法与默认提供程序一起使用。此方法的其他版本允许选择提供程序:
KeyGenerator keyGenerator = KeyGenerator.getInstance("AES");
generateKey
方法返回一个实现SecretKey
接口的对象实例,该接口如下所示。这是用于支持对称加密和解密的密钥:
SecretKey secretKey = keyGenerator.generateKey();
有了密钥,我们现在可以加密数据了。
我们将在后面的章节中使用以下encrypt
方法。此方法传递要加密的文本和密钥。术语纯文本通常用于指代未加密的数据。
Cipher
类提供了加密过程的框架。getInstance
方法返回使用 AES 算法的类的实例。使用Cipher.ENCRYPT_MODE
作为第一个参数,秘钥作为第二个参数,初始化Cipher
实例进行加密。doFinal
方法加密纯文本字节数组并返回加密的字节数组。Base64
类的getEncoder
返回对加密字节进行编码的编码器:
public static String encrypt(
String plainText, SecretKey secretKey) {
try {
Cipher cipher = Cipher.getInstance("AES");
byte[] plainTextBytes = plainText.getBytes();
cipher.init(Cipher.ENCRYPT_MODE, secretKey);
byte[] encryptedBytes =
cipher.doFinal(plainTextBytes);
Base64.Encoder encoder = Base64.getEncoder();
String encryptedText =
encoder.encodeToString(encryptedBytes);
return encryptedText;
} catch (NoSuchAlgorithmException|NoSuchPaddingException |
InvalidKeyException | IllegalBlockSizeException |
BadPaddingException ex) {
// Handle exceptions
}
return null;
}
编码加密字节数组用于将其转换为字符串,以便稍后使用。编码字符串可能是一种有用的安全技术,如中所述 http://javarevisited.blogspot.sg/2012/03/why-character-array-is-better-than.html 。
解密文本的过程在下面显示的解密方法中进行了说明。它使用一个反向过程,其中加密的字节被解码,Cipher
类的init
方法被初始化,以使用密钥解密字节:
public static String decrypt(String encryptedText,
SecretKey secretKey) {
try {
Cipher cipher = Cipher.getInstance("AES");
Base64.Decoder decoder = Base64.getDecoder();
byte[] encryptedBytes = decoder.decode(encryptedText);
cipher.init(Cipher.DECRYPT_MODE, secretKey);
byte[] decryptedBytes =
cipher.doFinal(encryptedBytes);
String decryptedText = new String(decryptedBytes);
return decryptedText;
} catch (NoSuchAlgorithmException|NoSuchPaddingException |
InvalidKeyException | IllegalBlockSizeException |
BadPaddingException ex) {
// Handle exceptions
}
return null;
}
我们将在对称加密客户端/服务器部分所示的 echo 客户端/服务器应用程序中使用这些方法。
非对称加密使用公钥和私钥。私钥由一个实体持有。每个人都可以使用公钥。可以使用以下任一密钥对数据进行加密:
- 如果使用私钥对数据进行加密,则可以使用公钥对其进行解密
- 如果使用公钥对数据进行加密,则可以使用私钥对其进行解密
如果私钥的所有者发送使用私钥加密的消息,则此消息的收件人可以使用公钥对其进行解密。他们都可以阅读消息,但他们知道只有私钥所有者才能发送此消息。
如果其他人使用公钥加密消息,则只有私钥所有者才能读取该消息。但是,所有者无法确定到底是谁发送了该消息。可能是个骗子。
但是,如果双方都有自己的一组公钥/私钥,我们可以保证只有发送方和接收方可以看到其内容。我们还可以保证发送者是他们所说的人。
假设苏想给鲍勃发个信。Sue 将使用她的私钥加密消息 M。让我们把这个消息称为 M1。然后,她将使用 Bob 的公钥对 M1 进行加密,给我们 M2。然后将消息 M2 发送给 Bob。现在,只有 Bob 可以使用他的私钥解密此消息。这将返回 M1。Bob 现在可以使用 Sue 的公钥解密 M1 以获得原始消息 M。他知道这是 Sue 的,因为只有 Sue 的公钥才能工作。
发送消息的过程要求两个参与者都拥有自己的公钥/私钥。除此之外,它的效率不如使用对称密钥。另一种方法是使用非对称密钥将密钥传输给参与者。然后可以将密钥用于实际的消息传输。这是与 SSL 一起使用的技术。
有几种不对称算法。Java 支持以下加密算法:
- RSA
- 密钥交换
- 数字减影
我们将使用下面声明的名为AsymmetricKeyUtility
的实用程序类演示非对称加密/解密。此类封装了创建、保存、加载和检索公钥和私钥的方法。我们将在此处解释这些方法的工作原理,并在稍后将其用于不对称 echo 客户端/服务器应用程序:
public class AsymmetricKeyUtility {
public static void savePrivateKey(PrivateKey privateKey) {
...
}
public static PrivateKey getPrivateKey() {
...
}
public static void savePublicKey(PublicKey publicKey) {
...
}
public static PublicKey getPublicKey() {
...
}
public static byte[] encrypt(PublicKey publicKey,
String message) {
...
}
public static String decrypt(PrivateKey privateKey,
byte[] encodedData) {
...
}
public static void main(String[] args) {
...
}
}
main
方法将创建密钥,保存密钥,然后测试它们是否正常工作。KeyPairGenerator
方法将生成密钥。为了使用非对称加密,我们使用 RSA 算法获得该类的一个实例。initialize
方法指定密钥使用 1024 位。generateKeyPair
方法生成密钥,getPrivate
和getPublic
方法分别返回私钥和公钥:
public static void main(String[] args) {
try {
KeyPairGenerator keyPairGenerator =
KeyPairGenerator.getInstance("RSA");
keyPairGenerator.initialize(1024);
KeyPair keyPair = keyPairGenerator.generateKeyPair();
PrivateKey privateKey = keyPair.getPrivate();
PublicKey publicKey = keyPair.getPublic();
...
} catch (NoSuchAlgorithmException ex) {
// Handle exceptions
}
我们将使用一组方法将这些键保存和检索到单独的文件。这种方法不是最安全的,但它将简化 echo 客户端/服务器的使用。下面的语句调用 save 方法:
savePrivateKey(privateKey);
savePublicKey(publicKey);
此处调用用于检索密钥的方法:
privateKey = getPrivateKey();
publicKey = getPublicKey();
下一个代码序列测试加密/解密过程。使用公钥创建一条消息并传递给encrypt
方法。调用decrypt
方法对消息进行解密。encodedData
变量引用加密数据:
String message = "The message";
System.out.println("Message: " + message);
byte[] encodedData = encrypt(publicKey,message);
System.out.println("Decrypted Message: " +
decrypt(privateKey,encodedData));
此示例的输出如下所示:
消息:消息
解密报文:报文
相反,我们可以使用私钥进行加密,使用公钥进行解密,以获得相同的结果。
现在,让我们检查一下encrypt
和decrypt
方法的细节。encrypt
方法使用getInstance
获取 RSA 算法的一个实例。init
方法指定Cipher
对象将使用公钥加密消息。doFinal
方法执行实际加密并返回包含加密消息的字节数组:
public static byte[] encrypt(PublicKey publicKey, String message) {
byte[] encodedData = null;
try {
Cipher cipher = Cipher.getInstance("RSA ");
cipher.init(Cipher.ENCRYPT_MODE, publicKey);
byte[] encryptedBytes =
cipher.doFinal(message.getBytes());
encodedData = Base64.getEncoder().withoutPadding()
.encode(encryptedBytes);
} catch (NoSuchAlgorithmException|NoSuchPaddingException |
InvalidKeyException | IllegalBlockSizeException |
BadPaddingException ex) {
// Handle exceptions
}
return encodedData;
}
下面描述decrypt
方法。它指定Cipher
实例将使用私钥解密消息。传递给它的加密消息必须在doFinal
方法解密之前进行解码。然后返回解密的字符串:
public static String decrypt(PrivateKey privateKey,
byte[] encodedData) {
String message = null;
try {
Cipher cipher = Cipher.getInstance("RSA ");
cipher.init(Cipher.DECRYPT_MODE, privateKey);
byte[] decodedData =
Base64.getDecoder().decode(encodedData);
byte[] decryptedBytes = cipher.doFinal(decodedData);
message = new String(decryptedBytes);
} catch (NoSuchAlgorithmException|NoSuchPaddingException |
InvalidKeyException | IllegalBlockSizeException |
BadPaddingException ex) {
// Handle exceptions
}
return message;
}
这两种方法都捕获加密/解密过程中可能发生的大量异常。我们在此不讨论这些例外情况。
接下来的两种方法演示了一种保存和检索私钥的技术。PKCS8EncodedKeySpec
类支持私钥的编码。编码后的密钥保存到private.key
文件中:
public static void savePrivateKey(PrivateKey privateKey) {
try {
PKCS8EncodedKeySpec pkcs8EncodedKeySpec =
new PKCS8EncodedKeySpec(privateKey.getEncoded());
FileOutputStream fos =
new FileOutputStream("private.key");
fos.write(pkcs8EncodedKeySpec.getEncoded());
fos.close();
} catch (FileNotFoundException ex) {
// Handle exceptions
} catch (IOException ex) {
// Handle exceptions
}
}
下面描述的getPrivateKey
方法从文件返回私钥。KeyFactory
类的generatePrivate
方法根据PKCS8EncodedKeySpec
规范创建密钥:
public static PrivateKey getPrivateKey() {
try {
File privateKeyFile = new File("private.key");
FileInputStream fis =
new FileInputStream("private.key");
byte[] encodedPrivateKey =
new byte[(int) privateKeyFile.length()];
fis.read(encodedPrivateKey);
fis.close();
PKCS8EncodedKeySpec privateKeySpec =
new PKCS8EncodedKeySpec(encodedPrivateKey);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
PrivateKey privateKey =
keyFactory.generatePrivate(privateKeySpec);
return privateKey;
} catch (FileNotFoundException ex) {
// Handle exceptions
} catch (IOException | NoSuchAlgorithmException |
InvalidKeySpecException ex) {
// Handle exceptions
}
return null;
}
下面将描述公钥的 save 和 get 方法。它们在所使用的文件以及X509EncodedKeySpec
类的使用上有所不同。此类表示公钥:
public static void savePublicKey(PublicKey publicKey) {
try {
X509EncodedKeySpec x509EncodedKeySpec =
new X509EncodedKeySpec(publicKey.getEncoded());
FileOutputStream fos =
new FileOutputStream("public.key");
fos.write(x509EncodedKeySpec.getEncoded());
fos.close();
} catch (FileNotFoundException ex) {
// Handle exceptions
} catch (IOException ex) {
// Handle exceptions
}
}
public static PublicKey getPublicKey() {
try {
File publicKeyFile = new File("public.key");
FileInputStream fis =
new FileInputStream("public.key");
byte[] encodedPublicKey =
new byte[(int) publicKeyFile.length()];
fis.read(encodedPublicKey);
fis.close();
X509EncodedKeySpec publicKeySpec =
new X509EncodedKeySpec(encodedPublicKey);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
PublicKey publicKey =
keyFactory.generatePublic(publicKeySpec);
return publicKey;
} catch (FileNotFoundException ex) {
// Handle exceptions
} catch (IOException | NoSuchAlgorithmException |
InvalidKeySpecException ex) {
// Handle exceptions
}
return null;
}
标准加密算法名称位于https://docs.oracle.com/javase/8/docs/technotes/guides/security/StandardNames.html 。对称算法的性能比较见http://www.javamex.com/tutorials/cryptography/ciphers.shtml 。
密钥库存储加密密钥和证书,并经常与服务器和客户端结合使用。密钥库通常是一个文件,但也可以是一个硬件设备。Java 支持以下类型的密钥库条目:
- 私钥:用于非对称加密
- 证书:包含公钥
- SecretKey:用于对称加密
Java 8 支持五种不同类型的密钥库:JKS、JCEKS、PKCS12、PKCS11 和 DKS:
- JKS:这是Java 密钥库(JKS,通常有
jks
的扩展名。 - JCEKS:这是Java 加密扩展密钥库(JCE)。它可以存储所有三种密钥库实体类型,为密钥提供更强的保护,并使用
jceks
扩展。 - PKCS12:与 JKS 和 JCEKS 相比这个密钥库可以与其他语言一起使用。它可以存储所有三种密钥库实体类型,并使用扩展名
p12
或pfx
。 - PKCS11:这是一个硬件密钥库类型。
- DKS:这是保存其他密钥库集合的域密钥库(DKS。
Java 中的默认密钥库类型是 JKS。可以使用keytool
命令行工具或 Java 代码创建和维护密钥库。我们将首先演示keytool
。
keytool 是一个用于创建密钥库的命令行程序。有关其使用的完整文档,请参见https://docs.oracle.com/javase/8/docs/technotes/tools/unix/keytool.html 。有几种 GUI 工具用于维护密钥库,它们比 keytool 更易于使用。其中一个是在发现的 IKEYMANhttp://www-01.ibm.com/software/webservers/httpservers/doc/v1312/ibm/9atikeyu.htm 。
要在命令提示下将 keytool 与 Windows 一起使用,需要配置 PATH 环境变量以定位其包含目录。使用与以下类似的命令:
C:\Some Directory>set path=C:\Program Files\Java\jdk1.8.0_25\bin;%path%
让我们使用 keytool 创建一个密钥库。在命令提示下,输入以下命令。这将启动在名为keystore.jks
的文件中创建密钥库的过程。别名是可用于引用密钥库的另一个名称:
C:\Some Directory>keytool -genkey -alias mykeystore -keystore keystore.jks
然后,系统会提示您输入以下几条信息。根据需要响应提示。您输入的密码将不会显示。对于本章中的示例,我们使用了密码password
:
Enter keystore password:
Re-enter new password:
What is your first and last name?
[Unknown]: some name
What is the name of your organizational unit?
[Unknown]: development
What is the name of your organization?
[Unknown]: mycom.com
What is the name of your City or Locality?
[Unknown]: some city
What is the name of your State or Province?
[Unknown]: some state
What is the two-letter country code for this unit?
[Unknown]: jv
然后将提示您确认输入,如下所示。如果数值正确,则响应yes
:
Is CN=some name, OU=development, O=mycom.com, L=some city, ST=some state, C=jv correct?
[no]: yes
您可以为密钥分配单独的密码,如下所示:
Enter key password for <mykeystore>
(RETURN if same as keystore password):
然后创建密钥库。可以使用–list
参数显示密钥库的内容,如下面的所示。–v
选项产生详细的输出:
keytool -list -v -keystore keystore.jks -alias mykeystore
这将显示以下输出。密钥库密码需要与别名一起输入:
Enter keystore password:
Alias name: mykeystore
Creation date: Oct 22, 2015
Entry type: PrivateKeyEntry
Certificate chain length: 1
Certificate[1]:
Owner: CN=some name, OU=development, O=mycom.com, L=some city, ST=some state, C=jv
Issuer: CN=some name, OU=development, O=mycom.com, L=some city, ST=some state, C=jv
Serial number: 39f2e11e
Valid from: Thu Oct 22 18:11:21 CDT 2015 until: Wed Jan 20 17:11:21 CST 2016
Certificate fingerprints:
MD5: 64:44:64:27:85:99:01:22:49:FC:41:DA:F7:A8:4C:35
SHA1: 48:57:3A:DB:1B:16:92:E6:CC:90:8B:D3:A7:A3:89:B3:9C:9B:7C:BB
SHA256: B6:B2:22:A0:64:61:DB:53:33:04:78:77:38:AF:D2:A0:60:37:A6:CB:3F:
3C:47:CC:30:5F:02:86:8F:68:84:7D
Signature algorithm name: SHA1withDSA
Version: 3
Extensions:
#1: ObjectId: 2.5.29.14 Criticality=false
SubjectKeyIdentifier [
KeyIdentifier [
0000: 07 D9 51 BE A7 48 23 34 5F 8E C6 F9 88 C0 36 CA ..Q..H#4_.....6.
0010: 27 8E 04 22 '.."
]
]
输入密钥库的信息可能会很乏味。简化此过程的一种方法是使用命令行参数。以下命令将创建上一个密钥库:
keytool -genkeypair -alias mykeystore -keystore keystore.jks -keypass password -storepass password -dname "cn=some name, ou=development, o=mycom.com, l=some city, st=some state c=jv
您将注意到,命令行末尾没有匹配的双引号。这是不必要的。命令行参数记录在前面列出的 keytool 网站上。
此工具可以创建对称和非对称密钥以及证书。以下一系列命令演示了其中几种类型的操作。我们将为一对非对称密钥创建密钥库。然后将导出一对证书,这些证书可与服务器和客户端应用程序一起使用。
此命令将使用 RSA 算法创建serverkeystore.jck
密钥库文件,密钥大小为 1024 位,过期日期为 365 天:
keytool -genkeypair -alias server -keyalg RSA -keysize 1024 -storetype jceks -validity 365 -keypass password -keystore serverkeystore.jck -storepass password -dname "cn=localhost, ou=Department, o=MyComp Inc, l=Some City, st=JV c=US
此命令生成供客户端应用程序使用的clientkeystore.jck
密钥库:
keytool -genkeypair -alias client -keyalg RSA -keysize 1024 -storetype jceks -validity 365 -keypass password -keystore clientkeystore.jck -storepass password -dname "cn=localhost, ou=Department, o=MyComp Inc, l=Some City, st=JV c=US
接下来将创建客户端的证书文件,并将其放置在client.crt
文件中:
keytool -export -alias client -storetype jceks -keystore clientkeystore.jck -storepass password -file client.crt
服务器的证书导出到此处:
keytool -export -alias server -storetype jceks -keystore serverkeystore.jck -storepass password -file server.crt
信任存储是用于验证凭据的文件,而密钥存储将生成凭据。凭证通常采用证书的形式。信任存储通常持有来自受信任第三方的证书,以形成证书链。
以下命令创建the clienttruststore.jck
文件,该文件是客户端的信任存储:
keytool -importcert -alias server -file server.crt -keystore clienttruststore.jck -keypass password -storepass storepassword
此命令生成以下输出:
Owner: CN=localhost, OU=Department, O=MyComp Inc, L=Some City, ST="JV c=US"
Issuer: CN=localhost, OU=Department, O=MyComp Inc, L=Some City, ST="JV c=US"
Serial number: 2d924315
Valid from: Tue Oct 20 19:26:00 CDT 2015 until: Wed Oct 19 19:26:00 CDT 2016
Certificate fingerprints:
MD5: 9E:3D:0E:D7:02:7A:F5:23:95:1E:24:B0:55:A9:F7:95
SHA1: 69:87:CE:EE:11:59:8F:40:A8:14:DA:D3:92:D0:3F:B6:A9:5A:7B:53
SHA256: BF:C1:7B:6D:D0:39:67:2D:1C:68:27:79:31:AA:B8:70:2B:FD:1C:85:18:
EC:5B:D7:4A:48:03:FA:F1:B8:CD:4E
Signature algorithm name: SHA256withRSA
Version: 3
Extensions:
#1: ObjectId: 2.5.29.14 Criticality=false
SubjectKeyIdentifier [
KeyIdentifier [
0000: D3 63 C9 60 6D 04 49 75 FB E8 F7 90 30 1D C6 C1 .c.`m.Iu....0...
0010: 10 DF 00 CF ....
]
]
Trust this certificate? [no]: yes
Certificate was added to keystore
使用以下命令创建服务器的信任存储:
keytool -importcert -alias client -file client.crt -keystore servertruststore.jck -keypass password -storepass password
其产出如下:
Owner: CN=localhost, OU=Department, O=MyComp Inc, L=Some City, ST="JV c=US"
Issuer: CN=localhost, OU=Department, O=MyComp Inc, L=Some City, ST="JV c=US"
Serial number: 5d5f3c40
Valid from: Tue Oct 20 19:27:31 CDT 2015 until: Wed Oct 19 19:27:31 CDT 2016
Certificate fingerprints:
MD5: 0E:FE:B3:EB:1B:D2:AD:32:9C:BC:FB:43:40:85:C1:A7
SHA1: 90:14:1E:17:DF:51:79:0B:1E:A3:EC:38:6B:BA:A6:F4:6F:BF:B6:D2
SHA256: 7B:3E:D8:2C:04:ED:E5:52:AE:B4:00:A8:63:A1:13:A7:E1:8E:59:63:E8:
86:38:D8:09:55:EA:3A:7C:F7:EC:4B
Signature algorithm name: SHA256withRSA
Version: 3
Extensions:
#1: ObjectId: 2.5.29.14 Criticality=false
SubjectKeyIdentifier [
KeyIdentifier [
0000: D9 53 34 3B C0 11 F8 75 0F 18 4E 18 23 A2 47 FE .S4;...u..N.#.G.
0010: E6 F5 C1 AF ....
]
]
Trust this certificate? [no]: yes
Certificate was added to keystore
现在我们将演示如何在 Java 中执行类似的操作。
可以使用 Java 代码直接创建密钥库、密钥和证书。在本节中,我们将演示如何创建包含密钥的密钥库。我们将在对称加密客户端/服务器部分中使用此类。
SymmetricKeyStoreCreation
类声明如下。SymmetricKeyStoreCreation
方法创建密钥库,main
方法生成并存储密钥:
public class SymmetricKeyStoreCreation {
private static KeyStore createKeyStore(String fileName,
String pw) {
...
}
public static void main(String[] args) {
...
}
}
下面描述createKeyStore
方法。它被传递密钥库的文件名和密码。创建了一个KeyStore
实例,它指定了一个 JCEKS 密钥库。如果密钥库已经存在,它将返回该密钥库:
private static KeyStore createKeyStore(String fileName,
String password) {
try {
File file = new File(fileName);
final KeyStore keyStore =
KeyStore.getInstance("JCEKS");
if (file.exists()) {
keyStore.load(new FileInputStream(file),
password.toCharArray());
} else {
keyStore.load(null, null);
keyStore.store(new FileOutputStream(fileName),
password.toCharArray());
}
return keyStore;
} catch (KeyStoreException | IOException |
NoSuchAlgorithmException |
CertificateException ex) {
// Handle exceptions
}
return null;
}
在main
方法中,使用 AES 算法创建KeyGenerator
实例。generateKey
方法将创建SecretKey
实例,如下图:
public static void main(String[] args) {
try {
final String keyStoreFile = "secretkeystore.jks";
KeyStore keyStore = createKeyStore(keyStoreFile,
"keystorepassword");
KeyGenerator keyGenerator =
KeyGenerator.getInstance("AES");
SecretKey secretKey = keyGenerator.generateKey();
...
} catch (Exception ex) {
// Handle exceptions
}
}
KeyStore.SecretKeyEntry
类表示密钥库中的一个条目。我们需要这个和一个KeyStore.PasswordProtection
类的实例代表密码,来存储密钥:
KeyStore.SecretKeyEntry keyStoreEntry
= new KeyStore.SecretKeyEntry(secretKey);
KeyStore.PasswordProtection keyPassword =
new KeyStore.PasswordProtection(
"keypassword".toCharArray());
setEntry
方法使用字符串别名、keystore 条目对象和密码来存储条目,如下所示:
keyStore.setEntry("secretKey", keyStoreEntry,
keyPassword);
然后将此条目写入密钥库:
keyStore.store(new FileOutputStream(keyStoreFile),
"keystorepassword".toCharArray());
其他密钥库操作可以使用 Java 实现。
本节演示如何在客户端/服务器应用程序中使用对称加密/解密。下面的示例实现了一个简单的 echo 客户端/服务器,使我们能够专注于基本流程,而不必脱离具体的客户端/服务器问题。服务器使用SymmetricEchoServer
类实现,客户端使用SymmetricEchoClient
类实现。
客户端将加密消息并将其发送到服务器。然后,服务器将解密消息并以纯文本形式将其发送回。如果需要,可以轻松加密响应。这种单向加密足以说明基本过程。
在 Windows 中运行本章讨论的应用程序时,您可能会遇到以下对话框。选择允许访问按钮以允许应用程序运行:
我们还将使用在对称加密技术中开发的SymmetricKeyStoreCreation
类。
下一步声明对称服务器。它拥有一个main
、decrypt
和getSecretKey
方法。decrypt
方法从客户端获取加密消息并解密。getSecretKey
方法将从对称加密技术中创建的密钥库中提取密钥。main
方法包含用于与客户端通信的基本套接字和流:
public class SymmetricEchoServer {
private static Cipher cipher;
public static String decrypt(String encryptedText,
SecretKey secretKey) {
...
}
private static SecretKey getSecretKey() {
...
}
public static void main(String[] args) {
...
}
}
decrypt
方法与对称加密技术中开发的方法相同,因此在此不再重复。下面描述getSecretKey
方法。使用对称加密技术创建的secretkeystore.jks
文件持有密钥。此方法使用了许多与SymmetricKeyStoreCreation
类的main
方法相同的类。KeyStore.PasswordProtection
类的一个实例用于从密钥库中提取密钥。密钥库密码keystorepassword
被硬编码到应用程序中。这不是最佳做法,但它简化了示例:
private static SecretKey getSecretKey() {
SecretKey keyFound = null;
try {
File file = new File("secretkeystore.jks");
final KeyStore keyStore =
KeyStore.getInstance("JCEKS");
keyStore.load(new FileInputStream(file),
"keystorepassword".toCharArray());
KeyStore.PasswordProtection keyPassword =
new KeyStore.PasswordProtection(
"keypassword".toCharArray());
KeyStore.Entry entry =
keyStore.getEntry("secretKey", keyPassword);
keyFound =
((KeyStore.SecretKeyEntry) entry).getSecretKey();
} catch (KeyStoreException | IOException |
NoSuchAlgorithmException |
CertificateException ex) {
// Handle exceptions
} catch (UnrecoverableEntryException ex) {
// Handle exceptions;
}
return keyFound;
}
main
方法与第一章网络编程入门中开发的服务器非常相似。主要区别在于 while 循环。来自客户端的输入与密钥一起传递到decrypt
方法,如下所示。然后显示解密的文本并将其返回给客户端:
String decryptedText = decrypt(inputLine,
getSecretKey());
main
方法如下:
public static void main(String[] args) {
System.out.println("Simple Echo Server");
try (ServerSocket serverSocket = new ServerSocket(6000)) {
System.out.println("Waiting for connection.....");
Socket clientSocket = serverSocket.accept();
System.out.println("Connected to client");
try (BufferedReader br = new BufferedReader(
new InputStreamReader(
clientSocket.getInputStream()));
PrintWriter out = new PrintWriter(
clientSocket.getOutputStream(), true)) {
String inputLine;
while ((inputLine = br.readLine()) != null) {
String decryptedText =
decrypt(inputLine, getSecretKey());
System.out.println("Client request: " +
decryptedText);
out.println(decryptedText;
}
} catch (IOException ex) {
// Handle exceptions
} catch (Exception ex) {
// Handle exceptions
}
} catch (IOException ex) {
// Handle exceptions
}
System.out.println("Simple Echo Server Terminating");
}
现在,让我们检查一下客户端应用程序。
下面将介绍客户端应用程序,它与第 1 章网络编程入门中开发的客户端应用程序非常相似。它使用与服务器中相同的getSecretKey
方法。对称加密技术中解释的encrypt
方法用于加密用户的消息。这两种方法在此不重复:
public class SymmetricEchoClient {
private static Cipher cipher;
public static String encrypt(String plainText,
SecretKey secretKey) {
...
}
...
}
public static void main(String args[]) {
...
}
}
main
方法与第一章网络编程入门中 while 循环中的版本不同。以下语句加密用户消息:
String encryptedText = encrypt(inputLine,
getSecretKey());
main
方法如下:
public static void main(String args[]) {
System.out.println("Simple Echo Client");
try (Socket clientSocket
= new Socket(InetAddress.getLocalHost(), 6000);
PrintWriter out = new PrintWriter(
clientSocket.getOutputStream(), true);
BufferedReader br = new BufferedReader(
new InputStreamReader(
clientSocket.getInputStream()))) {
System.out.println("Connected to server");
Scanner scanner = new Scanner(System.in);
while (true) {
System.out.print("Enter text: ");
String inputLine = scanner.nextLine();
if ("quit".equalsIgnoreCase(inputLine)) {
break;
}
String encryptedText =
encrypt(inputLine, getSecretKey());
System.out.println(
"Encrypted Text After Encryption: "
+ encryptedText);
out.println(encryptedText);
String response = br.readLine();
System.out.println(
"Server response: " + response);
}
} catch (IOException ex) {
// Handle exceptions
} catch (Exception ex) {
// Handle exceptions
}
}
我们现在准备好看客户端和服务器是如何交互的。
应用程序的行为方式与第 1 章网络编程入门中的行为方式相同。唯一的区别是发送到服务器的消息是加密的。除了在客户端显示加密文本外,此加密在应用程序的输出中不可见。一种可能的交互作用如下。首先显示服务器输出:
简单回音服务器
正在等待连接。。。。。
已连接到客户端
客户端请求:第一条消息
客户端请求:第二条消息
简单回显服务器终止
以下是客户端的应用程序输出:
简单回音客户端
已连接到服务器
输入文本:第一条消息
加密后的加密文本:DRKVP3BHNFMXRZLUFIQKB0RGJODQFIJMCO97YQGNUM=
服务器响应:DRKVP3BHNFMXRZLUFIQKB0RGJODQFIJMCO97YQGNUM=
输入文本:第二条消息
加密后的加密文本:fp9g+AqsVqZpxKMVNx8IkNdDcr9IGHb/qv0qrFinmYs=
服务器响应:fp9g+AqsVqZpxKMVNx8IkNdDcr9IGHb/qv0qrFinmYs=
输入文本:退出
我们现在将使用非对称密钥复制此功能。
在非对称加密技术中开发的AsymmetricKeyUtility
类用于支持客户端和服务器应用程序。我们将使用它的encrypt
和decrypt
方法。客户端和服务器应用程序的结构与前几节中使用的类似。客户端将向服务器发送一条加密消息,服务器将解密该消息,然后用纯文本响应。
下面声明的AsymmetricEchoServer
类用于服务器。main
方法是其唯一的方法。创建了一个服务器套接字,该套接字在accept
方法处阻塞,等待客户端请求:
public class AsymmetricEchoServer {
public static void main(String[] args) {
System.out.println("Simple Echo Server");
try (ServerSocket serverSocket = new ServerSocket(6000)) {
System.out.println("Waiting for connection.....");
Socket clientSocket = serverSocket.accept();
System.out.println("Connected to client");
...
} catch (IOException | NoSuchAlgorithmException |
NoSuchPaddingException ex) {
// Handle exceptions
}
System.out.println("Simple Echo Server Terminating");
}
}
接受客户端连接 IO 后,建立流,并实例化一个大小为171
的inputLine
字节数组。这是正在发送的消息的大小,使用此值将避免各种异常:
try (DataInputStream in = new DataInputStream(
clientSocket.getInputStream());
PrintWriter out = new PrintWriter(
clientSocket.getOutputStream(), true);) {
byte[] inputLine = new byte[171];
...
}
} catch (IOException ex) {
// Handle exceptions
} catch (Exception ex) {
// Handle exceptions
}
要执行解密,我们需要一个私钥。这是通过getPrivateKey
方法获得的:
PrivateKey privateKey =
AsymmetricKeyUtility.getPrivateKey();
while 循环将从客户端读入加密消息。使用消息和私钥调用decrypt
方法。然后显示解密的消息并将其发送回客户端。如果消息为quit
,则服务器终止:
while (true) {
int length = in.read(inputLine);
String buffer = AsymmetricKeyUtility.decrypt(
privateKey, inputLine);
System.out.println(
"Client request: " + buffer);
if ("quit".equalsIgnoreCase(buffer)) {
break;
}
out.println(buffer);
现在,让我们检查一下客户端应用程序。
客户端应用程序位于AsymmetricEchoClient
类中的,如下所示。它也只有一个main
方法。建立服务器连接后,将建立 IO 流:
public class AsymmetricEchoClient {
public static void main(String args[]) {
System.out.println("Simple Echo Client");
try (Socket clientSocket
= new Socket(InetAddress.getLocalHost(), 6000);
DataOutputStream out = new DataOutputStream(
clientSocket.getOutputStream());
BufferedReader br = new BufferedReader(
new InputStreamReader(
clientSocket.getInputStream()));
DataInputStream in = new DataInputStream(
clientSocket.getInputStream())) {
System.out.println("Connected to server");
...
}
} catch (IOException ex) {
// Handle exceptions
} catch (Exception ex) {
// Handle exceptions
}
}
}
Scanner
类用于获取用户输入。公钥用于加密用户消息,并通过AsymmetricKeyUtility
类的getPublicKey
方法获得:
Scanner scanner = new Scanner(System.in);
PublicKey publicKey =
AsymmetricKeyUtility.getPublicKey();
在下面的 while 循环中,提示用户输入一条消息,该消息使用encrypt
方法加密。然后将加密的消息发送到服务器。如果消息为quit
,则程序终止:
while (true) {
System.out.print("Enter text: ");
String inputLine = scanner.nextLine();
byte[] encodedData =
AsymmetricKeyUtility.encrypt(
publicKey, inputLine);
System.out.println(encodedData);
out.write(encodedData);
if ("quit".equalsIgnoreCase(inputLine)) {
break;
}
String message = br.readLine();
System.out.println("Server response: " + message);
现在,我们可以一起使用这些应用程序。
启动服务器,然后启动客户端。客户端将提示输入一系列消息。下面显示了一种可能的交换的输出。首先显示服务器端:
简单回音服务器
正在等待连接。。。。。
已连接到客户端
客户端请求:第一条消息
客户端请求:第二条消息
客户请求:退出
简单回显服务器终止
以下显示了客户端交互:
简单回音客户端
已连接到服务器
输入文本:第一条消息
[B@6bc168e5
服务器响应:第一条消息
输入文本:第二条消息
[B@7b3300e5
服务器响应:第二条消息
输入文本:退出
[B@2e5c649
TLS/SSL 是一组用于保护 Internet 上许多服务器安全的协议。SSL 是 TLS 的继承者。然而,它们并不总是可互换的。SSL 使用消息认证码(MAC算法,而 TLS 使用哈希对消息认证码(HMAC算法。
SSL 通常与许多其他协议一起使用,包括文件传输协议(FTP)、Telnet、网络新闻传输协议(NNTP)、轻量级目录访问协议(LDAP)和交互消息访问协议(IMAP。
TLS/SSL 在提供这些功能时确实会导致性能下降。然而,随着互联网速度的提高,影响通常并不显著。
当使用 HTTPS 协议时,用户会知道,因为该协议通常出现在浏览器的地址字段中。它甚至可以用在您可能不期望它出现的地方,例如在以下 Google URL 中:
我们将不深入探讨 SSL 协议如何工作的细节。然而,关于本协议的简要讨论可在中找到 http://www.javacodegeeks.com/2013/04/understanding-transport-layer-security-secure-socket-layer.html 。在本节中,我们将说明如何创建和使用 SSL 服务器以及用于支持此协议的 Java 类。
为了简化应用程序,客户端向服务器发送一条消息,然后服务器显示该消息。没有将响应发送回客户端。客户端使用 SSL 连接到服务器并与服务器通信。使用 SSL 将消息返回给客户端是留给读者的一个练习。
服务器在以下SSLServer
类中实现。所有代码都可以在main
方法中找到。我们将使用keystore.jks
密钥库来访问在对称加密技术中创建的密钥。为了提供对密钥库的访问,使用Provider
实例指定密钥库及其密码。在代码中硬编码密码不是一个好主意,但它用于简化此示例:
public class SSLServer {
public static void main(String[] args) throws Exception {
System.out.println("SSL Server Started");
Security.addProvider(new Provider());
System.setProperty("javax.net.ssl.keyStore",
"keystore.jks");
System.setProperty("javax.net.ssl.keyStorePassword",
"password");
...
}
}
SSLServerSocket
类的一个实例用于在客户端和服务器之间建立通信。此实例是使用SSLServerSocketFactory
类的getDefault
方法创建的。与以前的服务器套接字类似,accept
方法阻塞,直到建立客户端连接:
SSLServerSocketFactory sslServerSocketfactory =
(SSLServerSocketFactory)
SSLServerSocketFactory.getDefault();
SSLServerSocket sslServerSocket = (SSLServerSocket)
sslServerSocketfactory.createServerSocket(5000);
System.out.println("Waiting for a connection");
SSLSocket sslSocket =
(SSLSocket) sslServerSocket.accept();
System.out.println("Connection established");
然后从套接字的输出流创建一个BufferedReader
实例:
PrintWriter pw =
new PrintWriter(sslSocket.getOutputStream(), true);
BufferedReader br = new BufferedReader(
new InputStreamReader(sslSocket.getInputStream()));
下面的 while 循环读取并显示客户端请求。如果消息为quit
,则服务器终止:
String inputLine;
while ((inputLine = br.readLine()) != null) {
pw.println(inputLine);
if ("quit".equalsIgnoreCase(inputLine)) {
break;
}
System.out.println("Receiving: " + inputLine);
}
SSL 套接字自动处理加密和解密。
在 Mac 上,服务器在执行时可能会抛出异常。这可以通过创建 PKCS12 密钥库并使用-Djavax.net.ssl.keyStoreType=pkcs12 VM
选项来避免。
SSLClient
类实现客户端应用程序,如下所示。它使用与服务器基本相同的进程。while 循环处理用户输入的方式与在以前的客户端应用程序中执行的方式相同:
public class SSLClient {
public static void main(String[] args) throws Exception {
System.out.println("SSL Client Started");
Security.addProvider(new Provider());
System.setProperty("javax.net.ssl.trustStore",
"keystore.jks");
System.setProperty("javax.net.ssl.trustStorePassword",
"password");
SSLSocketFactory sslsocketfactory = (SSLSocketFactory)
SSLSocketFactory.getDefault();
SSLSocket sslSocket = (SSLSocket)
sslsocketfactory.createSocket("localhost", 5000);
System.out.println(
"Connection to SSL Server Established");
PrintWriter pw =
new PrintWriter(sslSocket.getOutputStream(), true);
BufferedReader in = new BufferedReader(
new InputStreamReader(sslSocket.getInputStream()));
Scanner scanner = new Scanner(System.in);
while (true) {
System.out.print("Enter a message: ");
String message = scanner.nextLine();
pw.println(message);
System.out.println("Sending: " + in.readLine());
if ("quit".equalsIgnoreCase(message)) {
break;
}
}
pw.close();
in.close();
sslSocket.close();
}
}
让我们看看它们是如何相互作用的。
启动服务器,然后启动客户端。在以下输出中,将向服务器发送三条消息,然后显示:
SSL 服务器启动
正在等待连接
已建立连接
接收:第一条消息
接收:第二条消息
客户端输入如下所示:
SSL 客户端启动
已建立与 SSL 服务器的连接
输入消息:第一条消息
发送:第一条消息
输入消息:第二条消息
发送:第二条消息
输入消息:退出
发送:退出
SSLServerSocket
类提供了一种实现启用 SSL 的服务器的简单方法。
当给定某种文档时,安全哈希函数将生成一个称为哈希值的大数字。此文档几乎可以是任何类型。我们将在示例中使用简单字符串。
该函数是单向散列函数,这意味着在给定散列值时实际上不可能重新创建文档。当与非对称密钥一起使用时,它允许传输文档,并保证文档未被更改。
文档的发送者将使用安全哈希函数为文档生成哈希值。发送方将使用其私钥加密此哈希值。然后将文档和密钥合并并发送给接收者。文档未加密。
接收到文档后,接收方将使用发送方的公钥解密散列值。然后,接收方将对文档使用相同的安全哈希函数来获得哈希值。如果此散列值与解密的散列值匹配,则接收方可以保证文档未被修改。
目的不是加密文档。虽然可能,但如果不需要向第三方隐藏文档,而只需保证文档未被修改,则此方法非常有用。
Java 支持以下哈希算法:
- MD5:默认大小为 64 字节
- SHA1:默认大小为 64 字节
我们将在示例中使用 SHA 哈希函数。这一系列功能是由国家安全局(NSA开发的。此哈希函数有三个版本:SHA-0、SHA-1 和 SHA-2。SHA-2 是更安全的算法,使用可变摘要大小:SHA-224、SHA-256、SHA-384 和 SHA-512。
MessageDigest
类处理任意大小的数据,产生固定大小的散列值。此类没有公共构造函数。当给定算法名称时,getInstance
方法返回类的实例。有效名称位于http://docs.oracle.com/javase/8/docs/technotes/guides/security/StandardNames.html#MessageDigest 。在本例中,我们使用SHA-256
:
MessageDigest messageDigest =
MessageDigest.getInstance("SHA-256");
messageDigest.update(message.getBytes());
完整示例,改编自http://www.mkyong.com/java/java-sha-hashing-example/ ,如下所示。displayHashValue
方法提取单个散列值字节并将其转换为可打印格式:
public class SHAHashingExample {
public static void main(String[] args) throws Exception {
SHAHashingExample example = new SHAHashingExample();
String message = "This is a simple text message";
byte hashValue[] = example.getHashValue(message);
example.displayHashValue(hashValue);
}
public void displayHashValue(byte hashValue[]) {
StringBuilder builder = new StringBuilder();
for (int i = 0; i < hashValue.length; i++) {
builder.append(Integer.toString((hashValue[i] & 0xff)
+ 0x100, 16).substring(1));
}
System.out.println("Hash Value: " + builder.toString());
}
public byte[] getHashValue(String message) {
try {
MessageDigest messageDigest =
MessageDigest.getInstance("SHA-256");
messageDigest.update(message.getBytes());
return messageDigest.digest();
} catch (NoSuchAlgorithmException ex) {
// Handle exceptions
}
return null;
}
}
执行程序。这将产生以下输出:
散列值:83c660972991049c25e6cad7a5600fc4d7c062c097b9a75c1c4f13238375c26c
关于用 Java 实现的安全散列函数的更详细检查,请参见http://howtodoinjava.com/2013/07/22/how-to-generate-secure-password-hash-md5-sha-pbkdf2-bcrypt-examples/ 。
在本章中,我们介绍了几种 Java 方法来保护应用程序之间的通信。我们首先简要介绍了与安全相关的术语,然后在介绍之后进行了更详细的讨论。
目前使用的加密/解密方法有两种。第一种是对称密钥加密,它使用在应用程序之间共享的单个密钥。这种方法要求以安全的方式在应用程序之间传输密钥。
第二种方法使用非对称加密。此技术使用私钥和公钥。用其中一个密钥加密的消息可以用另一个密钥解密。通常,使用来自可信来源的证书分发公钥。私钥的持有者需要保护私钥,以便其他人无法访问它。公钥可以与任何需要它的人自由共享。
加密密钥通常存储在密钥库中,该密钥库允许对密钥进行编程访问。密钥库是通过 keytool 应用程序创建和维护的。我们在几个应用程序中演示了密钥库的创建和使用。此外,我们使用对称密钥和非对称密钥对来支持 echo 客户端/服务器应用程序。
创建安全客户端和服务器的一种更常见的方法是使用SSLServerSocket
类。这将根据密钥库中找到的密钥对数据执行自动加密和解密。我们演示了如何在服务器和客户端应用程序中使用该类。
我们还研究了安全哈希函数的使用。这种技术允许传输未加密的数据,并保证数据未被修改。非对称密钥对用于加密哈希值。我们提供了这个过程的一个简单示例。
在下一章中,我们将研究影响分布式应用程序之间交互的各种因素。