本文是从英文的官网摘了翻译的,用作自己的整理和记录。水平有限,欢迎指正。版本是:22
原文地址:https://docs.oracle.com/en/java/javase/22/security/java-cryptography-architecture-jca-reference-guide.html#GUID-815542FE-CF3D-407A-9673-CAE9840F6231
Java Cryptography Architecture(JCA)是平台的重要组成部分,它包含了一个“provider”架构和一系列API,用于数字签名(digital signatures)、消息摘要(hashes)、证书(certificates )及其验证( certificate validation)、加密(对称/非对称块/流密码)encryption (symmetric/asymmetric block/stream ciphers)、密钥生成与管理(key generation and management)以及安全随机数生成(secure random number generation)等。
一、Java密码体系结构简介
Java平台非常强调安全性,包括语言安全、密码学、公钥基础设施、身份验证、安全通信和访问控制。
JCA是该平台的一个主要部分,包含一个“provider”架构和一组API,用于数字签名、消息摘要(哈希)、证书和证书验证、加密(对称/非对称块/流密码)、密钥生成和管理以及安全随机数生成。这些API允许开发人员轻松地将安全性集成到应用程序代码中。该体系结构的设计遵循以下原则:
- 实现独立性:应用程序不需要实现安全算法。相反,他们可以从Java平台请求安全服务。安全服务在提供程序中实现(请参阅加密服务提供程序),这些提供程序通过标准接口插入Java平台。一个应用程序可能依赖于多个独立的安全功能提供程序。
- 实现互操作性:提供者可以跨应用程序进行互操作。具体而言,应用程序不绑定到特定的提供程序,提供程序也不绑定到具体的应用程序。
- 算法可扩展性:Java平台包括许多内置的提供程序,这些提供程序实现了当今广泛使用的一组基本安全服务。然而,一些应用程序可能依赖于尚未实现的新兴标准,或者依赖于专有服务。Java平台支持安装实现此类服务的自定义提供程序。
JDK中提供的其他加密通信库使用JCA提供程序体系结构,但在其他地方进行了描述。JSSE组件提供对安全套接字层(SSL)、传输层安全性(TLS)和数据报传输层安全(DTLS)实现的访问;请参阅《Java安全套接字扩展(JSSE)参考指南》。您可以使用Java通用安全服务(JGSS)(通过Kerberos)API和简单身份验证和安全层(SASL)在通信应用程序之间安全地交换消息;请参阅JAAS和Java GSS-API教程简介以及Java SASL API编程和部署指南。
1.1 JCA设计原则 CA Design Principles
JCA是围绕以下原则设计的:
- 实施独立性和互操作性
- 算法独立性和可扩展性
实现独立性和算法独立性是相辅相成的;您可以使用加密服务,如数字签名和消息摘要,而不必担心实现细节,甚至不必担心构成这些概念基础的算法。虽然完全的算法独立性是不可能的,但JCA提供了标准化的、特定于算法的API。当不希望实现独立性时,JCA允许开发人员指示特定的实现。
算法独立性是通过定义加密“引擎engines”(服务)的类型,并定义提供这些加密引擎功能的类来实现的。这些类称为引擎类,例如MessageDigest、Signature、KeyFactory、KeyPairGenerator和Cipher类。
实现独立性是使用基于“provider提供者”的体系结构实现的。术语Cryptographic Service Provider加密服务提供商(CSP)与术语“provider提供商”可互换使用(请参阅加密服务提供商),是指实现一个或多个加密服务(如数字签名算法、消息摘要算法和密钥转换服务)的包或包集。程序可以简单地请求实现特定服务(例如DSA签名算法)的特定类型的对象(例如Signature对象),并从安装的提供者之一获得实现。如果需要,程序可以改为向特定提供者请求实现。提供者可以对应用程序透明地进行更新,例如当有更快或更安全的版本可用时。
实现互操作性意味着各种实现可以相互工作、使用彼此的密钥或验证彼此的签名。例如,这意味着,对于相同的算法,一个提供者生成的密钥将被另一个提供者使用,并且一个提供者所生成的签名将可被另一提供者验证。
实现独立性是通过采用基于“provider服务提供者”的架构来实现的。密码服务提供者(Cryptographic Service Provider,CSP)这个术语与“provider服务提供者”一词可以互换使用(见密码服务提供者),它指的是一个或一组包,这些包实现了一个或多个加密服务,如数字签名算法、消息摘要算法和密钥转换服务。程序可以简单地请求一种特定类型的对象(例如Signature对象),实现特定服务(例如DSA签名算法),并从已安装的服务提供者中获得实现。如果需要,程序也可以请求来自特定服务提供者的实现。服务提供者可以在应用程序不知情的情况下进行透明更新,例如当有更快或更安全的版本可用时。
实现互操作性意味着不同的实现可以相互协作,使用对方的密钥或验证对方的签名。这意味着,例如,对于相同的算法,一个服务提供者生成的密钥可以被另一个服务提供者使用,一个服务提供者生成的签名也可以被另一个服务提供者验证。
算法可扩展性意味着可以很容易地添加适合支持的引擎类之一的新算法。
1.2 加密服务提供者架构 Provider Architecture
服务提供者包含一个包(或一组包),这些包为所advertised 宣传的加密算法提供具体的实现。
1.2.1 加密服务提供商 Cryptographic Service Providers
java.security.Provider 是所有安全服务提供者的基础类。每个密码服务提供者(CSP)都包含这个类的实例,该实例包含提供者的名称,并列出它实现的所有安全服务/算法。当需要特定算法的实例时,JCA框架会查询提供者的数据库,如果找到了合适的匹配项,就会创建该实例。
msp; 服务提供者包含一个包(或一组包),这些包为所宣传的加密算法提供具体的实现。每个JDK安装都有一个或多个默认安装和配置的服务提供者。可以静态或动态地添加额外的服务提供者。客户端可以配置它们的运行时环境以指定服务提供者的偏好顺序。偏好顺序是在没有请求特定服务提供者时搜索请求服务的服务提供者顺序。
要使用JCA,应用程序只需请求一种特定类型的对象(如MessageDigest)以及特定算法或服务(如"SHA-256"算法),并从已安装的服务提供者中获得实现。例如,以下语句从已安装的服务提供者请求一个SHA-256消息摘要:
md = MessageDigest.getInstance("SHA-256");
msp; 或者,程序可以从特定服务提供者请求对象。每个服务提供者都有一个用于引用它的名称。例如,以下语句从名为ProviderC的服务提供者请求一个SHA-256消息摘要:
md = MessageDigest.getInstance("SHA-256", "ProviderC");
以下图表说明了请求SHA-256消息摘要实现的过程。它们展示了三个不同的服务提供者,这些服务提供者实现了各种消息摘要算法(SHA-256、SHA-384和SHA-512)。服务提供者按照从左到右的偏好顺序排列(1-3)。在图2-1中,一个应用程序请求SHA-256算法的实现,而没有指定服务提供者的名称。按照偏好顺序搜索服务提供者,由第一个提供特定算法的服务提供者ProviderB返回实现。在图2-2中,应用程序从特定服务提供者ProviderC请求SHA-256算法的实现。这一次,即使偏好顺序更高的服务提供者ProviderB也提供了MD5算法的实现,但还是从ProviderC返回了实现。
Figure 2-1 获取消息摘要算法实现没有指定服务提供者
Figure 2-2 获取消息摘要算法指定服务提供者 ProvideB
JDK中的加密实现通过几个不同的服务提供者(Sun、SunJSSE、SunJCE、SunRsaSign)进行分发,这主要是出于历史原因,但在较小程度上也是根据它们提供的功能性和算法类型。其他Java运行时环境不一定包含这些服务提供者,因此应用程序不应请求特定于服务提供者的实现,除非已知特定服务提供者将可用。
JCA提供了一组API,允许用户查询已安装的服务提供者以及它们支持的服务。
这种架构还方便了最终用户添加额外的服务提供者。已经有许多第三方服务提供者实现可用。有关如何编写、安装和注册服务提供者的更多信息,请参见“Provider Class 服务提供者类”。
1.2.2 加密服务提供者的实现 How Providers Are Actually Implemented
算法独立性是通过定义一个通用的高层应用程序接口(API)来实现的,所有应用程序都使用这个API来访问服务类型。实现独立性是通过让所有服务提供者的实现都符合定义良好的接口来实现的。引擎类的实例因此由具有相同方法签名的实现类“支持”。应用程序调用通过引擎类路由,并传递到底层的支持实现。实现处理请求并返回正确的结果。
每个引擎类中的应用程序API方法通过实现相应服务提供者接口(SPI)的类路由到服务提供者的实现。也就是说,对于每个引擎类,都有一个对应的抽象SPI类定义了每个加密服务提供者的算法必须实现的方法。每个SPI类的名称与其对应的引擎类名称相同,后面加上Spi。例如,Signature引擎类提供了访问数字签名算法的功能。实际的服务提供者实现是在SignatureSpi的子类中提供的。应用程序调用引擎类的API方法,这些方法反过来调用实际实现中的SPI方法。
每个SPI类都是抽象的。为了提供特定算法的特定类型服务的实现,服务提供者必须子类化相应的SPI类,并为所有抽象方法提供实现。
对于API中的每个引擎类,通过调用引擎类的getInstance()工厂方法来请求和实例化实现实例。工厂方法是一种静态方法,返回一个类的实例。引擎类使用前面描述的框架服务提供者选择机制来获取实际的支持实现(SPI),然后创建实际的引擎对象。每个引擎类的实例都封装了(作为一个私有字段)相应SPI类的实例,称为SPI对象。API对象的所有API方法都声明为final,它们的实现调用封装的SPI对象的相应SPI方法。
Example 2-1 样例代码获取引擎类的实例
import javax.crypto.*;Cipher c = Cipher.getInstance("AES");c.init(ENCRYPT_MODE, key);
Figure 2-3 应用程序检索“AES”密码实例
在这里,一个应用程序需要一个"AES" javax.crypto.Cipher 实例,并不在意使用哪个服务提供者。应用程序调用 Cipher 引擎类的 getInstance() 工厂方法,该方法反过来请求JCA框架找到第一个支持"AES"的服务提供者实例。框架查询每个已安装的服务提供者,并获取服务提供者的 Provider 类实例。(回想一下,Provider 类是一个可用算法的数据库。)框架搜索每个服务提供者,最终在CSP3中找到一个合适的条目。这个数据库条目指向实现类 com.foo.AESCipher,它扩展了 CipherSpi,因此适合由 Cipher 引擎类使用。创建了 com.foo.AESCipher 的一个实例,并封装在新创建的 javax.crypto.Cipher 实例中,然后返回给应用程序。当应用程序现在对 Cipher 实例执行 init() 操作时,Cipher 引擎类将请求路由到 com.foo.AESCipher 类中相应的 engineInit() 支持方法。
Java安全标准算法名称列出了为Java环境定义的标准名称。其他第三方服务提供者可能会定义这些服务的自己的实现,甚至额外的服务。
1.2.3 密钥库 Keystores
一个名为“密钥库”的数据库可以用来管理密钥和证书的存储库。密钥库对需要数据身份认证、加密或签名的应用程序是可用的。
应用程序可以通过实现java.security包中的KeyStore类来访问密钥库。截至JDK 9,默认且推荐的密钥库类型(格式)是"pkcs12",它基于RSA PKCS12个人信息交换语法标准。之前,默认的密钥库类型是"jks",这是一种专有格式。还有其他可用的密钥库格式,例如"jceks",这是另一种专有的密钥库格式,以及基于RSA PKCS11标准的"pkcs11",支持访问加密令牌,如硬件安全模块和智能卡。
应用程序可以通过之前描述的服务提供者机制,从不同的服务提供者中选择不同的密钥库实现。参见密钥管理 Key Management.
1.3 引擎类和算法 Engine Classes and Algorithms
引擎类为特定类型的加密服务提供接口,与特定的加密算法或提供程序无关。
引擎(engines provides)提供以下功能之一:
- 密码操作(加密、数字签名、消息摘要等encryption, digital signatures, message digests)
- 密码材料(密钥和算法参数keys and algorithm parameters)的生成器或转换器 generators or converters of cryptographic material
- 对象(密钥库或证书keystores or certificates),这些对象封装了加密数据,并且可以在更高的抽象层中使用。
有以下引擎类可以使用:
- SecureRandom:用于生成随机数或伪随机数。
- MessageDigest:用于计算指定数据的消息摘要(hash)。
- Signature签名 :用密钥初始化,用于对数据进行签名和验证数字签名。
- Cipher密码:用密钥初始化,这些密钥用于加密/解密数据。有各种类型的算法:对称批量加密(例如AES)、非对称加密(例如RSA)和基于密码的加密(例如PBE)。
- Mac:与MessageDigest一样,消息验证码(Mac)也会生成哈希值,但首先使用密钥进行初始化,以保护消息的完整性。
- KEM:双方用于从私钥/公钥对中派生共享密钥。
- KeyFactory:用于将Key类型的现有不透明加密密钥转换为密钥规范(底层密钥材料的透明表示),反之亦然。
- SecretKeyFactory:用于将SecretKey类型的现有不透明加密密钥转换为密钥规范(底层密钥材料的透明表示),反之亦然。SecretKeyFactorys是专门的KeyFactorys,只创建秘密(对称)密钥
- KeyPairGenerator:用于生成一对适合与指定算法一起使用的新公钥和私钥。
- KeyGenerator:用于生成与指定算法一起使用的新密钥。
- KeyAgreement 密钥协议:由两方或多方使用,以商定并建立用于特定加密操作的特定密钥。
- AlgorithmParameters:用于存储特定算法的参数,包括参数编码和解码。
- AlgorithmParameterGenerator:用于生成一组适合指定算法的AlgorithmParameters。
- KeyStore:用于创建和管理密钥库。密钥库是一个密钥数据库。密钥库中的私钥有一个与之相关联的证书链,用于验证相应的公钥。密钥库还包含来自受信任实体的证书。
- CertificateFactory:用于创建公钥证书和证书吊销列表(CRL)。
- CertPathBuilder:用于构建证书链(也称为证书路径)。
- CertPathValidator:用于验证证书链。
- CertStore:用于从存储库中检索证书和CRL。
二、核心类和接口 Core Classes and Interfaces
以下是JCA提供的核心类和接口。
- Provider 和 Security
- SecureRandom, MessageDigest, Signature, Cipher, Mac, KEM, KeyFactory, SecretKeyFactory, KeyPairGenerator, KeyGenerator, KeyAgreement, AlgorithmParameter, AlgorithmParameterGenerator, KeyStore, 和 CertificateFactory 引擎类
- 密钥接口和类,KeyPair
- AlgorithmParameterSpec 接口,AlgorithmParameters,AlgorithmParameterGenerator,以及在
java.security.spec
和javax.crypto.spec
包中的算法参数规范接口和类。 - KeySpec 接口,EncodedKeySpec,PKCS8EncodedKeySpec,和 X509EncodedKeySpec。
- SecretKeyFactory,KeyFactory,KeyPairGenerator,KeyGenerator,KeyAgreement,和 KeyStore。
注意:请参阅《Java PKI程序员指南》中的 CertPathBuilder、CertPathValidator 和 CertStore 引擎类。
本指南将首先介绍最有用的高级类(Provider、Security、SecureRandom、MessageDigest、Signature、Cipher、Mac 和 KEM),然后深入探讨各种支持类。目前,只需简单地说,密钥(公钥、私钥和秘密密钥)由各种JCA类生成和表示,并作为它们操作的一部分被高级类使用。
本节展示了每个类和接口的主要方法签名。一些这些类(MessageDigest、Signature、KeyPairGenerator、SecureRandom、KeyFactory 和密钥规范类)的示例将提供在相应的代码示例部分。
相关的安全API包的完整参考文档可以在包摘要中找到:
java.security
javax.crypto
java.security.cert
java.security.spec
javax.crypto.spec
java.security.interfaces
javax.crypto.interfaces
2.1 服务提供者 The Provider
“密码服务提供者Cryptographic Service Provider”(在本文档中与“服务提供者provider”一词交替使用)指的是一个包或一组包,这些包提供了JDK安全API加密特性的一个子集的具体实现。Provider类是这类包或包集的接口。它具有访问提供者名称、版本号和其他信息的方法。请注意,除了注册加密服务的实现之外,Provider类还可以用来注册可能作为JDK安全API或其扩展之一而定义的其他安全服务的实现。
为了提供加密服务的实现,一个实体(例如,一个开发小组)编写实现代码并创建Provider类的子类。Provider子类的构造函数设置了各种属性的值;JDK安全API使用这些值来查找提供者实现的服务。换句话说,子类指定了实现服务的类的名称。
Figure 2-4 Provider Class
提供程序包可以实现几种类型的服务;请参阅引擎类和算法。
不同的实现可能有不同的特性。有些可能是基于软件的,而其他一些可能是基于硬件的。有些可能是平台独立的,而其他一些可能是特定平台的。有些服务提供者的源代码可能可供审查和评估,而有些则可能不公开。JCA允许最终用户和开发者决定他们的需求是什么。
你可以找到有关最终用户如何安装符合他们需求的加密实现的信息,以及开发者如何请求符合他们需求的实现。
2.1.1 服务提供者如何获取 How Provider Implementations Are Requested and Supplied
对于API中的每个引擎类(参见引擎类和算法),通过调用引擎类上的一个getInstance方法来请求和实例化一个实现实例,指定所需算法的名称,以及可选地指定所希望的服务提供者的名称(或Provider类)。
static EngineClassName getInstance(String algorithm)throws NoSuchAlgorithmExceptionstatic EngineClassName getInstance(String algorithm, String provider)throws NoSuchAlgorithmException, NoSuchProviderExceptionstatic EngineClassName getInstance(String algorithm, Provider provider)throws NoSuchAlgorithmException
这里EngineClassName是所需的引擎类型(for example, Signature, MessageDigest, or Cipher). 例如:
Signature sig = Signature.getInstance("SHA256withRSA");KeyAgreement ka = KeyAgreement.getInstance("DH", "SunJCE");
分别返回“SHA256withRSA”签名和“DH”密钥协商对象的实例。
Java安全标准算法名称包含了已标准化用于Java环境的名称列表。一些服务提供者可能选择也包括指向同一算法的别名。例如,“SHA256”算法可能被称为“SHA-256”。应用程序应该使用标准名称而不是别名,因为并非所有服务提供者都以相同的方式为算法名称设置别名。
如果未指定服务提供者,getInstance
会在已注册的服务提供者中搜索与指定算法相关的请求的加密服务实现。在任何给定的Java虚拟机(JVM)中,服务提供者按照特定的偏好顺序安装,如果在调用时没有请求特定服务提供者,则按照这个顺序搜索服务提供者列表。(见“安装服务提供者”。)例如,假设在JVM中安装了两个服务提供者,PROVIDER_1 和 PROVIDER_2。假设:
- PROVIDER_1 实现了 SHA256withRSA 和 AES。PROVIDER_1 有最高的引用顺序1。
- PROVIDER_2 实现了 SHA256withRSA、SHA256withDSA 和 RC5。PROVIDER_2 有引用顺序2。
现在让我们看三个场景:
- 我们正在寻找 SHA256withRSA 实现:两个服务提供者都提供了这样的实现。由于 PROVIDER_1 有最高的优先级并且首先被搜索,所以返回 PROVIDER_1 的实现。
- 我们正在寻找 SHA256withDSA 实现:首先搜索 PROVIDER_1。没有找到实现,所以接着搜索 PROVIDER_2。由于找到了实现,所以返回它。
- 我们正在寻找 SHA256withECDSA 实现:因为没有已安装的服务提供者实现了它,所以抛出 NoSuchAlgorithmException。
包含服务提供者参数的 getInstance
方法是为那些想要指定他们希望从哪个服务提供者获取算法的开发者准备的。例如,一个联邦机构将希望使用一个已经获得联邦认证的服务提供者实现。假设 PROVIDER_1 没有获得这样的认证,而 PROVIDER_2 获得了。
那么一个联邦机构的程序会有如下的调用,指定 PROVIDER_2,因为它有经过认证的实现:
Signature s = Signature.getInstance("SHA256withRSA", "PROVIDER_2");
在这种情况下,如果没有安装PROVIDER_2,则会引发NoSuchProviderException,即使另一个已安装的提供程序实现了所请求的算法。
程序还可以选择获取所有已安装提供程序的列表(使用安全类中的getProviders方法),然后从列表中选择一个。
2.1.2 安装服务提供者 Installing Providers
为了使用,加密服务提供者首先必须被安装,然后无论是静态还是动态地进行注册。随此版本发布的Sun服务提供者有多种(SUN、SunJCE、SunJSSE、SunRsaSign等),它们已经安装并注册好了。以下部分描述了如何安装和注册额外的服务提供者。
所有JDK服务提供者已经安装并注册。然而,如果你需要任何第三方服务提供者,请参见“实施和整合服务提供者步骤”中的第8步:“准备测试”,以获取有关如何将服务提供者添加到类或模块路径、注册服务提供者(静态或动态),以及添加任何所需权限的信息。
2.1.3 服务提供者的方法 Provider Class Methods
每个Provider类实例都有一个(当前区分大小写)名称、一个版本号以及提供程序及其服务的字符串描述。
您可以通过调用以下方法查询提供程序实例以获取此信息:
public String getName()
public double getVersion()
public String getInfo()
2.2 安全类 The Security Class
Security 类管理已安装的服务提供者和全局安全属性。它只包含静态方法,并且永远不会被实例化。添加或移除服务提供者的方法,以及设置安全属性的方法,只能由一个可信程序执行。目前,“可信程序” 是以下之一:
- 一个没有在安全管理器下运行的本地应用程序
- 一个有权限执行指定方法的小应用程序或应用程序
要使代码被认为可信任以执行尝试的操作(例如添加服务提供者),需要为该特定操作授予小程序适当的权限。JDK安装的策略配置文件指定了来自指定代码源的代码允许的权限(即可访问的系统资源类型)。请参阅默认策略实现、策略文件语法和Java SE平台安全架构。
被执行的代码始终被认为是来自特定的“代码源”。代码源不仅包括代码原始位置(URL),还包括对可能用于签名代码的私钥对应的任何公钥的引用。代码源中的公钥通过用户别名(symbolic alias names)引用。
在策略配置文件中,代码源由两个组成部分表示:代码基(URL)和别名名称(前面加上signedBy),其中别名名称标识包含用于验证代码签名的公钥的密钥库条目。
此类文件中的每个“grant”语句为指定的代码源授予一组权限,指定允许哪些操作。
以下是一个策略配置文件的示例:
grant codeBase "file:/home/sysadmin/", signedBy "sysadmin" {permission java.security.SecurityPermission "insertProvider";permission java.security.SecurityPermission "removeProvider";permission java.security.SecurityPermission "putProviderProperty.*";
};
此配置文件指定从本地文件系统上/home/sysadmin/目录中的已签名JAR文件加载的代码可以添加或删除提供程序或设置提供程序属性。(请注意,可以使用用户密钥库中别名sysadmin引用的公钥来验证JAR文件的签名。)。
可能缺少代码源的任何一个组件(或两者都缺少)。以下是一个配置文件的示例,其中省略了codeBase:
grant signedBy "sysadmin" {permission java.security.SecurityPermission "insertProvider.*";permission java.security.SecurityPermission "removeProvider.*";
};
如果此策略有效,则本地文件系统上由/home/sysadmin/目录签名的JAR文件中的代码可以添加或删除提供程序。代码不需要签名。
一个既不包括codeBase也不包括signedBy的示例是:
grant {permission java.security.SecurityPermission "insertProvider.*";permission java.security.SecurityPermission "removeProvider.*";
};
在这里,由于缺少代码源的两个组件,任何代码(无论其来源、是否签名或由谁签名)都可以添加或移除服务提供者。显然,这绝对不是推荐的,因为这种授权可能会打开一个安全漏洞。不可信的代码可能会安装一个服务提供者,从而影响依赖于正常功能实现的后续代码。(例如,一个恶意的Cipher对象可能会捕获并存储它接收到的敏感信息。)
2.2.1 管理提供商 Managing Providers
2.2.1.1 查询服务提供者 Querying Providers
Method | Description |
---|---|
static Provider[] getProviders() | 返回一个数组,包含所有已安装的服务提供者(技术上,是每个包服务提供者的子类)。数组中服务提供者的顺序代表了它们的优先级顺序。. |
static Provider getProvider (String providerName) | 返回名为providerName的提供程序。如果找不到提供程序,则返回null。. |
2.2.1.2 增加服务提供者Adding Providers
Method | Description |
---|---|
static int addProvider(Provider provider) | 将一个服务提供者添加到已安装服务提供者列表的末尾。如果成功添加,它将返回服务提供者被添加时的优先级位置;如果服务提供者因为已经安装而没有被添加,则返回-1。. |
static int insertProviderAt (Provider provider, int position) | 在指定位置添加一个新的服务提供者。如果指定位置已经安装了服务提供者,那么原先在该位置的服务提供者以及所有位置大于该位置的服务提供者都会向上移动一个位置(即向列表末尾移动)。这个方法返回服务提供者被添加时的优先级位置,或者如果因为服务提供者已经安装而没有添加,则返回-1。. |
2.2.1.3 移除服务提供者 Removing Providers
Method | Description |
---|---|
static void removeProvider(String name) | 移除指定名称的服务提供者。如果服务提供者未安装,则默默返回。当指定的服务提供者被移除时,所有位于指定服务提供者之后位置的服务提供者都会向下移动一个位置(朝已安装服务提供者列表的头部方向)。. |
2.2.2 安全属性 Security Properties
Security 类维护着一个系统范围的安全属性列表。这些属性与系统属性类似,但是与安全相关。这些属性可以静态设置(通过 <java-home>/conf/security/java.security
文件),也可以动态设置(使用 API)。请参阅“实现和整合服务提供者步骤”中的第 8.1 步:使用 security.provider.n 安全属性静态配置服务提供者。如果你想动态设置属性,可信程序可以使用以下方法:
static String getProperty(String key)
static void setProperty(String key, String datum)
2.3 安全随机数 The SecureRandom Class
SecureRandom 类是一个引擎类(参见引擎类和算法),它提供密码学上强随机数,可以通过访问伪随机数生成器(PRNG),一个从初始种子值产生伪随机序列的确定性算法,或者通过读取本地随机源(例如 /dev/random 或真正的随机数生成器)。一个 PRNG 的例子是 NIST SP 800-90Ar1 指定的确定性随机位生成器(DRBG)。其他实现可能产生真正的随机数,还有一些可能同时使用这两种技术。密码学上的强随机数至少符合 FIPS 140-2,密码模块的安全要求,第 4.9.1 节中指定的统计随机数生成器测试。
所有 Java SE 实现都必须在 java.security.Security 类的 securerandom.strongAlgorithms 属性中指明它们提供的最强(最随机)的 SecureRandom 实现。当需要特别强的随机值时,可以使用这个实现。
securerandom.drbg.config 属性用于指定 SUN 服务提供者的 DRBG SecureRandom 配置和实现。securerandom.drbg.config 是 java.security.Security 类的一个属性。其他 DRBG 实现也可以使用 securerandom.drbg.config 属性。
2.3.1 创建 SecureRandom对象 Creating a SecureRandom Object
获取 SecureRandom 实例的几种方法
-
默认的 SecureRandom 实例:
所有 Java SE 实现都提供了一个默认的 SecureRandom,使用无参数构造函数:new SecureRandom()
。这个构造函数遍历注册的安全服务提供者列表,从最优先的服务提供者开始,然后返回第一个支持 SecureRandom 随机数生成器(RNG)算法的服务提供者的新 SecureRandom 对象。如果没有服务提供者支持 RNG 算法,则返回一个使用 SUN 服务提供者的 SHA1PRNG 的 SecureRandom 对象。 -
要获取 SecureRandom 的特定实现,请使用 “服务提供者实现的请求和供应方式 How Provider Implementations Are Requested and Supplied” 中的一种。
-
使用
getInstanceStrong()
方法来获取由 java.security.Security 类的 securerandom.strongAlgorithms 属性定义的强 SecureRandom 实现。这个属性列出了适合生成重要值的平台实现。
2.3.2 设定种子或重新设定种子 Seeding or Re-Seeding the SecureRandom Object
除非对getInstance()的调用后面跟着对以下setSeed方法之一的调用,否则SecureRandom对象将使用随机种子进行初始化。
void setSeed(byte[] seed)void setSeed(long seed)
在首次调用 nextBytes 方法之前,必须调用 setSeed 以防止任何环境随机性。
由 SecureRandom 对象产生的位的随机性取决于种子位的随机性。
SecureRandom 对象可以随时使用 setSeed 或 reseed 方法之一重新设置种子。setSeed 中给定的种子是对现有种子的补充,而不是替代;因此,重复调用保证不会减少随机性。
2.3.3 使用SecureRandom对象 Using a SecureRandom Object
要获得随机字节,调用者只需传递一个任意长度的数组,然后用随机字节填充:
void nextBytes(byte[] bytes)
2.3.4 正在生成种子字节 Generating Seed Bytes
如果需要,可以调用generateSeed方法来生成给定数量的种子字节(例如,为其他随机数生成器种子):
byte[] generateSeed(int numBytes)
2.4 消息摘要 The MessageDigest Class
MessageDigest 类是一个引擎类(参见引擎类和算法),旨在提供密码学上安全的散列消息摘要的功能,例如 SHA-256 或 SHA-512。密码学上安全的散列消息摘要接受任意大小的输入(一个字节数组),并生成一个固定大小的输出,称为摘要或哈希值。
例如,SHA-256 算法生成一个 32 字节的摘要,而 SHA-512 的摘要是 64 字节。
摘要具有两个属性:
- 计算上几乎不可能找到两个消息,它们散列到同一个值。
- 摘要不应当透露任何用于生成它的输入信息。
消息摘要用于生成数据的唯一且可靠的标识符。它们有时被称为“校验和”或数据的“数字指纹”。即使是消息中的一个比特位的更改也应该产生不同的摘要值。
消息摘要有多种用途,可以确定数据是否被修改,无论是有意还是无意。最近,人们已经付出相当的努力去确定流行的算法是否存在任何弱点,结果各异。在选择摘要算法时,人们应该总是咨询最新的参考资料,以确定其状态和适用性是否适合手头的任务。
2.4.1 创建摘要t对象 Creating a MessageDigest Object
创建 MessageDigest 对象的步骤。
- 要计算摘要,需要创建一个消息摘要实例。MessageDigest 对象是通过使用 MessageDigest 类中的一个
getInstance() 方法来获取的。请参阅“服务提供者实现的请求和供应方式”。 - 工厂方法返回一个已初始化的消息摘要对象。因此,它不需要进一步的初始化。
2.4.2 更新摘要Updating a Message Digest Object
更新消息摘要对象的过程。
要计算某些数据的摘要,必须将数据提供给初始化的消息摘要对象。它可以同时提供,也可以分块提供。可以通过调用以下更新方法之一将片段馈送到消息摘要:
void update(byte input)
void update(byte[] input)
void update(byte[] input, int offset, int len)
2.4.3 计算摘要 Computing the Digest
使用不同类型的digest()方法计算摘要的过程。
数据块必须由对update方法的调用提供。请参阅更新邮件摘要对象。
摘要是使用对其中一个摘要方法的调用来计算的
byte[] digest()
byte[] digest(byte[] input)
int digest(byte[] buf, int offset, int len)
- byte[] digest() 方法返回计算出的摘要。
- byte[] digest(byte[] input) 方法在调用 digest() 返回摘要字节数组之前,使用输入的字节数组进行最终的 update(input) 更新。
- intdigest(byte[] buf, int offset, int len) 方法将计算出的摘要存储在提供的缓冲区 buf
中,从 offset 开始。len 是在 buf 中为摘要分配的字节数,该方法返回实际存储在 buf
中的字节数。如果缓冲区中没有足够的空间,方法将抛出一个异常。
2.5 签名类 The Signature Class
Signature 类是一个引擎类(参见引擎类和算法),旨在提供密码学数字签名算法的功能,例如 SHA256withDSA 或 SHA512withRSA。密码学安全的签名算法接受任意大小的输入和私钥,并生成一个相对较短(通常是固定大小)的字节串,称为签名,具有以下属性:
- 只有私钥/公钥对的所有者才能创建签名。对于仅拥有公钥和一定数量签名的任何人来说,计算上几乎不可能恢复私钥。
- 给定用于生成签名的私钥对应的公钥,应该能够验证输入的真实性和完整性。
Figure 2-7 Signature Class
签名对象使用私钥进行初始化,用于签名,并提供待签名的数据。生成的签名字节通常与签名数据一起保存。当需要验证时,会创建并初始化另一个用于验证的签名对象,并提供相应的公钥。将数据和签名字节提供给签名对象,如果数据和签名匹配,签名对象会报告成功。
尽管签名看起来与消息摘要相似,但它们在提供的保护类型上有着非常不同的目的。实际上,像 “SHA256WithRSA” 这样的算法使用消息摘要 “SHA256” 最初将大型数据集 “压缩” 成更易管理的形式,然后使用 “RSA” 算法对生成的 32 字节消息摘要进行签名。
有关签名和验证数据的示例,请参见使用生成的密钥生成和验证签名。
2.5.1 签名状态 Signature Object States
这意味着签名对象始终处于给定状态,在该状态下,它只能执行一种类型的操作。
状态由其各自类中定义的最终整型常量表示。
签名对象可能有的三种状态是:
- UNINITIALIZED(未初始化)
- SIGN(签名)
- VERIFY(验证)
当它首次创建时,签名对象处于 UNINITIALIZED 状态。Signature 类定义了两个初始化方法,initSign
和 initVerify
,分别将状态更改为 SIGN 和 VERIFY。
2.5.2 创建签名对象 Creating a Signature Object
签名或验证签名的第一步是创建签名实例。
签名对象是通过使用Signature getInstance()静态工厂方法之一获得的。请参阅如何请求和提供提供程序实现。
2.5.3 初始化签名对象 Initializing a Signature Object
在使用签名对象之前,必须对其进行初始化。初始化方法取决于该对象是用于签名还是用于验证。
如果用于签名,对象首先必须使用将要生成签名的实体的私钥进行初始化。这个初始化通过调用以下方法完成:
final void initSign(PrivateKey privateKey)
这个方法将签名对象设置为 SIGN 状态。如果签名对象将用于验证,它首先必须使用将要验证签名的实体的公钥进行初始化。这个初始化可以通过调用以下任一方法完成:
final void initVerify(PublicKey publicKey)final void initVerify(Certificate certificate)
这个方法将签名对象设置为 VERIFY 状态。
2.5.4 使用签名对象签名 Signing with a Signature Object
如果签名对象已经初始化用于签名(即处于 SIGN 状态),则可以将待签名的数据提供给该对象。这可以通过调用 update 方法之一或多次来完成:
final void update(byte b)
final void update(byte[] data)
final void update(byte[] data, int off, int len)
应持续调用 update 方法,直到所有待签名的数据都已提供给签名对象。
要生成签名,只需调用 sign 方法之一:
final byte[] sign()
final int sign(byte[] outbuf, int offset, int len)
第一个方法以字节数组的形式返回签名结果。第二个方法将签名结果存储在提供的缓冲区 outbuf 中,从 offset 开始。len 是 outbuf 中为签名分配的字节数。该方法返回实际存储的字节数。
签名编码是特定于算法的。请参阅 Java 安全标准算法名称,了解更多关于 Java 加密体系结构中 ASN.1 编码的使用。
调用 sign 方法会将签名对象重置为通过调用 initSign 进行初始化时的状态。也就是说,如果需要,对象将被重置并可用于使用相同的私钥通过新的 update 和 sign 调用生成另一个签名。
或者,可以再次调用 initSign 指定不同的私钥,或者调用 initVerify(以初始化签名对象以验证签名)。
2.5.1 使用签名对象验证签名 Verifying with a Signature Object
如果签名对象已经初始化用于验证(即处于 VERIFY 状态),则可以验证所谓的签名是否确实是与之相关联的数据的真实签名。开始此过程时,将待验证的数据(与签名本身相对)提供给对象。通过调用 update 方法之一,将数据传递给对象:
final void update(byte b)
final void update(byte[] data)
final void update(byte[] data, int off, int len)
应持续调用 update 方法,直到所有待验证的数据都已提供给签名对象。现在,可以通过调用 verify 方法之一来验证签名:
final boolean verify(byte[] signature) final boolean verify(byte[] signature, int offset, int length)
参数必须是一个包含签名的字节数组。这个字节数组将保存由先前调用 sign 方法之一返回的签名字节。
verify 方法返回一个布尔值,指示编码的签名是否是提供给 update 方法的数据的真实签名。
调用 verify 方法会将签名对象重置为其通过调用 initVerify 进行初始化时的状态。也就是说,对象将被重置并可用于验证由指定公钥的身份的另一个签名。
或者,可以再次调用 initVerify 指定不同的公钥(以初始化签名对象以验证来自不同实体的签名),或者调用 initSign(以初始化签名对象以生成签名)。
2.6 密码 The Cipher Class
Cipher 类提供了密码学密码的功能,用于加密和解密。加密是将数据(称为明文)和密钥一起处理,生成对不知道密钥的第三方来说无意义的数据(密文)。解密是相反的过程:将密文和密钥一起处理,生成明文。
Figure 2-8 The Cipher Class
2.6.1 对称加密和非对称加密 Symmetric vs. Asymmetric Cryptography
有两种主要的加密类型:对称加密(也称为密钥加密)和非对称加密(或公钥密码学)。在对称加密中,相同的密钥用于加密和解密数据。保持密钥的私密性对于保持数据的机密性至关重要。另一方面**,非对称加密使用一对公钥/私钥来加密数据。用一个密钥加密的数据必须用另一个密钥来解密**。用户首先生成一对公钥/私钥,然后将公钥发布在一个可信赖的数据库中,任何人都可以访问。想要与该用户安全通信的用户使用检索到的公钥来加密数据。只有持有私钥的人才能解密。保持私钥的机密性对于这种方案至关重要。
非对称算法(如 RSA)通常比对称算法慢得多。这些算法并不设计用来高效地保护大量数据。在实践中,非对称算法用于交换较小的密钥,这些密钥用于初始化对称算法。
2.6.2 流密码与分组密码 Stream versus Block Ciphers
有两种主要类型的密码机:块密码和流密码。块密码机一次处理整个数据块,通常是许多字节的长度。如果数据不足以构成一个完整的输入块,就必须进行填充:也就是说,在加密之前,必须添加虚拟字节以构成密码块大小的倍数。然后在解密阶段去除这些字节。填充可以由应用程序完成,或者通过初始化密码机使用某种填充类型,如 “PKCS5PADDING” 来实现。
相比之下,流密码机一次处理一个很小的数据单元(通常是一个字节甚至一个比特)。这允许密码机在不需要填充的情况下处理任意数量的数据。
2.6.3 工作模式 Modes Of Operation
当使用简单的块密码进行加密时,两个相同的明文块将始终产生相同的密文块。密码分析者如果注意到重复的文本块,将更容易破解密文。密码的工作模式通过基于块位置或其他密文块的值来改变输出块,使密文不那么可预测。第一个块将需要一个初始值,这个值称为初始化向量(IV)。由于IV只是在加密之前改变数据,IV应该是随机的,但不一定需要保密。有多种模式,例如CBC(密码块链接)、CFB(密码反馈模式)和OFB(输出反馈模式)。ECB(电子密码本模式)是一种工作模式,其中没有块位置或其他密文块的影响。因为如果使用相同的明文/密钥,ECB密文是相同的,这种模式通常不适合密码学应用,因此不应使用。
一些算法,如AES和RSA,允许不同长度的密钥,但其他算法的密钥长度是固定的,如3DES。使用更长的密钥通常意味着更强的抵抗消息恢复的能力。像往常一样,在安全性和时间之间存在权衡,因此适当选择密钥长度。
大多数算法使用二进制密钥。大多数人类没有能力记住长序列的二进制数字,即使以十六进制表示。字符密码更容易回忆。因为字符密码通常从少数字符中选择(例如[a-zA-Z0-9]),所以已经定义了像“基于密码的加密”(PBE)这样的协议,它们采用字符密码并生成强大的二进制密钥。为了使攻击者从密码到密钥的任务变得非常耗时(通过所谓的“彩虹表攻击”或“预计算字典攻击”,其中预计算了常见的字典单词->值映射),大多数PBE实现会混合一个随机数,称为盐,以减少预计算表的有用性。
一些更新的密码模式,如带有关联数据的认证加密(AEAD)(例如,伽罗瓦/计数器模式(GCM))可以同时加密数据并认证生成的消息。可以在计算结果AEAD标签(MAC)期间使用额外的关联数据(AAD),但这个AAD数据不会作为密文输出。(例如,一些数据可能不需要保密,但应该计算标签以检测修改。)Cipher.updateAAD() 方法可以用来在标签计算中包含AAD。
2.6.4 Using an AES Cipher with GCM Mode
AES Cipher 使用 GCM(Galois/Counter Mode)是一种 AEAD(Authenticated Encryption with Associated Data,认证加密与关联数据)密码机,它的使用模式与非 AEAD 密码机不同。除了常规数据外,它还接受可选的 AAD(Additional Authenticated Data,附加认证数据),这些数据对于加密/解密是可选的,但是在加密/解密的数据之前必须提供 AAD。此外,为了安全地使用 GCM,调用者不应重复使用密钥和 IV(Initialization Vector,初始化向量)组合。这意味着每次加密操作时,都应该使用不同的参数集显式重新初始化密码对象。
以下是使用 AES Cipher 与 GCM 模式的示例代码:
SecretKey myKey = ...; // 密钥
byte[] myAAD = ...; // 附加认证数据,可选
byte[] plainText = ...; // 明文数据
int myTLen = ...; // 标签长度
byte[] myIv = ...; // 初始化向量GCMParameterSpec myParams = new GCMParameterSpec(myTLen, myIv); // GCM参数
Cipher c = Cipher.getInstance("AES/GCM/NoPadding"); // 获取密码机实例
c.init(Cipher.ENCRYPT_MODE, myKey, myParams); // 初始化为加密模式// AAD 是可选的,如果存在,必须在任何 update/doFinal 调用之前提供。
c.updateAAD(myAAD); // 如果 AAD 非空
byte[] cipherText = new byte[c.getOutputSize(plainText.length)]; // 密文数组
// 加密操作的结束
int actualOutputLen = c.doFinal(plainText, 0, plainText.length, cipherText);// 要解密,必须提供相同的 AAD 和 GCM 参数
c.init(Cipher.DECRYPT_MODE, myKey, myParams); // 初始化为解密模式
c.updateAAD(myAAD); // 再次提供 AAD
byte[] recoveredText = c.doFinal(cipherText, 0, actualOutputLen); // 密文解密// 如果要再次使用相同的密钥进行加密,必须更改 IV 值
byte[] newIv = ...;
myParams = new GCMParameterSpec(myTLen, newIv); // 更新 GCM 参数
在这段代码中,我们首先设置了密钥、附加认证数据、明文、标签长度和初始化向量。然后我们创建了一个 GCM 参数规范,并使用它来初始化密码机实例。在加密之前,如果存在附加认证数据,则使用 updateAAD
方法提供。接着使用 doFinal
方法完成加密操作。解密时,使用相同的附加认证数据和 GCM 参数重新初始化密码机,并再次使用 doFinal
方法来解密密文。如果再次使用相同的密钥进行加密,必须更改初始化向量的值。
2.6.5 创建Cipher 对象 Creating a Cipher Object
密码对象是通过使用 Cipher 类的静态工厂方法 getInstance()
来获取的。请参阅“如何请求和供应服务提供者实现How Provider Implementations Are Requested and Supplied”。在这里,算法名称与其它引擎类略有不同,因为它指定的不仅仅是一个算法名称,而是一个“转换”。转换是一个字符串,描述了要在给定输入上执行的操作(或操作集)以产生某些输出。转换始终包括一个密码算法的名称(例如,AES),并且可以后跟模式和填充方案。
转换的形式为:
- “algorithm/mode/padding”
- “algorithm”
例如,以下是有效的转换:
"AES/CBC/PKCS5Padding"
"AES"
如果只指定了转换名称,系统将确定环境中是否有请求转换的实现,如果有多个,则返回首选的实现。
如果同时指定了转换名称和包服务提供者,则系统将确定请求的包中是否有请求转换的实现,并在没有时抛出异常。
建议使用完整指定算法、模式和填充的转换。如果不这样做,服务提供者将使用默认值。例如,SunJCE 和 SunPKCS11 服务提供者将 ECB 作为默认模式,将 PKCS5Padding 作为许多对称密码的默认填充。
这意味着在 SunJCE 服务提供者的情况下:
Cipher c1 = Cipher.getInstance("AES/ECB/PKCS5Padding");
和
Cipher c1 = Cipher.getInstance("AES");
这两种方式是等效的,因为后者将使用默认的模式和填充方案,根据 SunJCE 服务提供者的默认设置,这将是 ECB 模式和 PKCS5Padding 填充。
使用诸如 CFB(Cipher Feedback Mode,密码反馈模式)和 OFB(Output Feedback Mode,输出反馈模式)这样的模式,块密码机可以以小于密码机实际块大小的单元加密数据。在请求这种模式时,您可以通过将此数字附加到模式名称后面来选择每次处理的位数,如 “AES/CFB8/NoPadding” 和 “AES/OFB32/PKCS5Padding” 转换中所示。如果没有指定这样的数字,将使用服务提供者特定的默认值。(例如,SunJCE 服务提供者对 AES 使用 128 位的默认值。)因此,通过使用 8 位模式如 CFB8 或 OFB8,可以将块密码机转换为面向字节的流密码机。
Java 安全标准算法名称包含了一个标准名称列表,这些名称可以用来指定转换的算法名称、模式和填充方案组件。
工厂方法返回的对象是未初始化的,在使用之前必须进行初始化。
2.6.6 初始化Cipher 对象 Initializing a Cipher Object
通过 getInstance
获取的 Cipher 对象必须为以下四种模式之一进行初始化,这些模式在 Cipher 类中被定义为最终整型常量。这 些模式可以通过它们的象征性名称引用:
ENCRYPT_MODE
:数据的加密。DECRYPT_MODE
:数据的解密。WRAP_MODE
:将java.security.Key
包装成字节,以便安全传输密钥。UNWRAP_MODE
:将之前包装的密钥解开,恢复为java.security.Key
对象。
每个 Cipher 初始化方法都需要一个操作模式参数(opmode
),并根据该模式初始化 Cipher 对象。其他参数包括密钥(key
)或包含密钥的证书(certificate
)、算法参数(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
方法提供参数,那么底层的密码实现应该自行提供所需的参数,要么是通过生成随机参数,要么是通过使用默认的、服务提供者特定的参数集。
然而,如果需要参数的 Cipher 对象在初始化为解密模式时,并且没有向 init
方法提供参数,将根据使用的 init
方法,抛出 InvalidKeyException
或 InvalidAlgorithmParameterException
异常。
请参阅“管理算法参数Managing Algorithm Parameters.”。
加密时使用的相同参数必须用于解密。
请注意,当 Cipher 对象被初始化时,它会丢失所有之前获得的状态。换句话说,初始化 Cipher 等同于创建该 Cipher 的一个新实例,并对其进行初始化。例如,如果一个 Cipher 首先使用给定的密钥初始化为解密模式,然后被重新初始化为加密模式,它将丢失在解密模式下获得的任何状态。
2.6.7 加密和解密数据 Encrypting and Decrypting Data
数据可以一步加密或解密(单部分操作),也可以通过多个步骤进行(多部分操作)。如果您事先不知道数据的长度,或者数据太长而无法一次性全部存储在内存中,多部分操作就非常有用。
要进行单步加密或解密,调用 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)
要进行多步加密或解密,调用 update
方法之一:
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
方法之一来终止(如果最后一步仍有一些输入数据剩余),或者通过以下 doFinal
方法之一来终止(如果最后一步没有输入数据剩余):
public byte[] doFinal();public int doFinal(byte[] output, int outputOffset);
所有的 doFinal
方法都会处理任何必要的填充(或去填充),如果作为指定转换的一部分请求了填充(或去填充)。
调用 doFinal
会将 Cipher 对象重置为其通过调用 init
方法初始化时的状态。也就是说,Cipher 对象被重置,并且可用于加密或解密(取决于在调用 init
时指定的操作模式)更多的数据。
2.6.8 包装和解包秘钥 Wrapping and Unwrapping Keys
包装密钥可以安全地将密钥从一个位置传输到另一个位置。
使用包装/解包 API 使得代码编写更加方便,因为它可以直接使用密钥对象。这些方法还支持硬件型密钥的安全传输。
要包装一个密钥,首先为 Cipher 对象初始化为 WRAP_MODE,然后调用以下方法:
public final byte[] wrap(Key key);
如果您要将包装后的密钥字节(调用 wrap 的结果)提供给其他人以进行解包,请确保同时发送接收者解包所需的额外信息:
- 密钥算法的名称。
- 包装密钥的类型(Cipher.SECRET_KEY、Cipher.PRIVATE_KEY 或 Cipher.PUBLIC_KEY 中的一个)。
可以通过调用 Key 接口的 getAlgorithm 方法确定密钥算法名称:
public String getAlgorithm();
要解包之前调用 wrap 返回的字节,首先为 Cipher 对象初始化为 UNWRAP_MODE,然后调用以下方法:
public final Key unwrap(byte[] wrappedKey,String wrappedKeyAlgorithm,int wrappedKeyType));
这里,wrappedKey 是之前调用 wrap 返回的字节,wrappedKeyAlgorithm 是与包装密钥关联的算法,wrappedKeyType 是包装密钥的类型。这必须是 Cipher.SECRET_KEY、Cipher.PRIVATE_KEY 或 Cipher.PUBLIC_KEY 中的一个。
2.6.9 管理算法参数 Managing Algorithm Parameters
底层 Cipher 实现使用的参数,无论是应用程序明确传递给 init
方法的,还是底层实现自身生成的,都可以通过调用 Cipher 对象的 getParameters
方法来检索,该方法返回一个 java.security.AlgorithmParameters
对象(如果没有使用参数则返回 null)。如果参数是初始化向量(IV),也可以通过调用 getIV
方法来检索。
在以下示例中,实现了基于密码的加密(PBE)的 Cipher 对象仅使用密钥而没有参数进行初始化。然而,选定的基于密码的加密算法需要两个参数 - 一个盐值和一个迭代计数。这些将由底层算法实现自身生成。应用程序可以从 Cipher 对象中检索生成的参数,见示例 2-3。
用于加密的相同参数必须用于解密。它们可以从它们的编码实例化,并用于初始化对应的 Cipher 对象进行解密,见示例 2-4。
如果在初始化 Cipher 对象时没有指定任何参数,并且您不确定底层实现是否使用任何参数,您可以通过简单地调用 Cipher 对象的 getParameters
方法并检查返回值来找出。返回值为 null 表示没有使用参数。
SunJCE 提供者实现的以下密码算法使用参数:
- 当在反馈模式下使用 AES、DES-EDE 和 Blowfish 时(即 CBC、CFB、OFB 或 PCBC),使用初始化向量 (IV)。
javax.crypto.spec.IvParameterSpec
类可以用来初始化 Cipher 对象的给定 IV。此外,CTR 和 GCM 模式需要 IV。 - PBE Cipher 算法使用一组参数,包括盐值和迭代计数。
javax.crypto.spec.PBEParameterSpec
类可以用来初始化实现 PBE 算法的 Cipher 对象(例如:PBEWithHmacSHA256AndAES_256)的给定盐值和迭代计数。
请注意,如果您使用 The SealedObject Class
类,您不必担心存储或传输任何算法参数以供解密操作使用。这个类将密封(加密)时使用的参数附加到加密对象的内容中,并使用相同的参数进行解封(解密)。
示例 2-3 从 Cipher 对象检索参数的示例代码
应用程序可以按照以下方式从 Cipher 对象检索加密生成的参数:
import javax.crypto.*;
import java.security.AlgorithmParameters;// 获取基于密码加密的 Cipher 对象
Cipher c = Cipher.getInstance("PBEWithHmacSHA256AndAES_256");// 初始化 Cipher 进行加密,不提供任何参数。
// 这里假设 "myKey" 是一个已经生成的密钥。
c.init(Cipher.ENCRYPT_MODE, myKey);// 加密一些数据并存储起来,以便稍后解密
byte[] cipherText = c.doFinal("This is just an example".getBytes());// 检索底层 Cipher 实现生成的参数
AlgorithmParameters algParams = c.getParameters();// 获取参数编码并存储起来
byte[] encodedAlgParams = algParams.getEncoded();
示例 2-4 用于解密的 Cipher 对象初始化的示例代码
用于加密的相同参数必须用于解密。它们可以从它们的编码实例化,并按照以下方式用于初始化对应的 Cipher 对象进行解密:
import javax.crypto.*;
import java.security.AlgorithmParameters;// 获取基于密码加密的参数对象
AlgorithmParameters algParams;
algParams = AlgorithmParameters.getInstance("PBEWithHmacSHA256AndAES_256");// 使用前一个示例中的参数编码进行初始化
algParams.init(encodedAlgParams);// 获取基于密码加密的 Cipher 对象
Cipher c = Cipher.getInstance("PBEWithHmacSHA256AndAES_256");// 使用接受 AlgorithmParameters 对象的 init() 方法之一进行解密初始化,
// 并传递前一个示例中的 algParams 对象
c.init(Cipher.DECRYPT_MODE, myKey, algParams);
2.6.10 注意事项 Cipher Output Considerations
在 Cipher 的一些 update 和 doFinal 方法中,允许调用者指定一个输出缓冲区,用于存放加密或解密后的数据。在这些情况下,传递一个足够大的缓冲区以容纳加密或解密操作的结果非常重要。
以下 Cipher 中的方法可以用来确定输出缓冲区应该有多大:
public int getOutputSize(int inputLen)
2.7 其他基于Cipher的类 Other Cipher-based Classes
有一些助手类在内部使用密码,以方便访问常见的密码用法。
2.7.1 Cipher字节输入流类 The CipherInputStream Class
这个类是一个 FilterInputStream
,它加密或解密通过它的数据。它由一个 InputStream
组成。CipherInputStream
表示一个安全的输入流,其中嵌入了一个 Cipher 对象。CipherInputStream
的读取方法返回的数据是从底层 InputStream
读取的,但还被嵌入的 Cipher 对象进一步处理过。在使用 CipherInputStream
之前,Cipher 对象必须完全初始化。
例如,如果嵌入的 Cipher 已初始化用于解密,CipherInputStream
将在将数据返回给应用程序之前尝试解密从底层 InputStream
读取的数据。
这个类严格遵守其祖先类 java.io.FilterInputStream
和 java.io.InputStream
的语义,特别是失败的语义。这个类具有其祖先类中指定的所有方法,并覆盖了它们,以便数据还被嵌入的密码进行额外的处理。此外,这个类捕获了其祖先类没有抛出的所有异常。特别是,skip(long)
方法只跳过已被 Cipher 处理过的数据。
对于使用这个类的程序员来说,关键不要使用在这类中没有定义或覆盖的方法(例如,后来添加到超类之一中的新方法或构造函数),因为这些方法的设计和实现不太可能考虑到了 CipherInputStream 在安全方面的影响。见示例 2-5 以了解其用法,假设 cipher1 已初始化用于加密。程序读取并加密来自文件 /tmp/a.txt 的内容,然后将结果(加密的字节)存储在 /tmp/b.txt 中。
示例 2-6 演示了如何轻松连接 CipherInputStream 和 FileInputStream 的几个实例。在这个示例中,假设 cipher1 和 cipher2 分别已使用相应的密钥初始化用于加密和解密。程序将文件 /tmp/a.txt 的内容复制到 /tmp/b.txt,只是内容首先被加密,然后在从 /tmp/a.txt 读取时被解密回原样。当然,由于这个程序只是简单地加密文本然后立即解密回来,除了作为演示 CipherInputStreams 串联的简单方法外,实际上并没有太大用处。
请注意,CipherInputStream
的读取方法将阻塞,直到从底层密码返回数据。如果使用块密码,将需要从底层 InputStream
获得一个完整的密码文本块。
示例 2-5 使用 CipherInputStream 和 FileInputStream 的示例代码
以下代码展示了如何使用包含 Cipher 的 CipherInputStream 和 FileInputStream 对输入流数据进行加密:
try (FileInputStream fis = new FileInputStream("/tmp/a.txt");CipherInputStream cis = new CipherInputStream(fis, cipher1);FileOutputStream 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);}
}
示例 2-6 连接 CipherInputStream 和 FileInputStream 的示例代码
以下示例演示了如何轻松连接 CipherInputStream 和 FileInputStream 的几个实例。在这个示例中,假设 cipher1 和 cipher2 分别已使用相应的密钥初始化用于加密和解密:
try (FileInputStream fis = new FileInputStream("/tmp/a.txt");CipherInputStream cis1 = new CipherInputStream(fis, cipher1);CipherInputStream cis2 = new CipherInputStream(cis1, cipher2);FileOutputStream 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);}
}
2.7.2 Cipher字节输出流类 The CipherOutputStream Class
这个类是一个 FilterOutputStream
,它加密或解密通过它的数据。它由一个 OutputStream
或其子类以及一个 Cipher 组成。CipherOutputStream
表示一个安全的输出流,其中嵌入了一个 Cipher 对象。CipherOutputStream
的写入方法首先使用嵌入的 Cipher 对象处理数据,然后才将它们写入底层的 OutputStream
。在使用 CipherOutputStream
之前,Cipher 对象必须完全初始化。
例如,如果嵌入的 Cipher 已初始化用于加密,CipherOutputStream
将在将数据写入底层输出流之前对其进行加密。
这个类严格遵守其祖先类 java.io.OutputStream
和 java.io.FilterOutputStream
的语义,特别是失败的语义。这个类具有其祖先类中指定的所有方法,并覆盖了它们全部,以便所有数据都被嵌入的密码进行额外的处理。此外,这个类捕获了其祖先类没有抛出的所有异常。
对于使用这个类的程序员来说,关键不要使用在这类中没有定义或覆盖的方法(例如,后来添加到超类之一中的新方法或构造函数),因为这些方法的设计和实现不太可能考虑到了 CipherOutputStream 在安全方面的影响。
见示例 2-7,说明其用法,假设 cipher1 已初始化用于加密。程序读取文件 /tmp/a.txt 的内容,然后加密并存储结果(加密的字节)在 /tmp/b.txt。
示例 2-7 演示了如何轻松连接 CipherOutputStream 和 FileOutputStream 的几个实例。在这个示例中,假设 cipher1 和 cipher2 分别已使用相应的密钥初始化用于解密和加密。程序将文件 /tmp/a.txt 的内容复制到 /tmp/b.txt,只是内容首先被加密,然后在写入 /tmp/b.txt 之前被解密回原样。
使用块密码算法时需要记住的一点是,在数据将被加密并发送到底层输出流之前,必须给 CipherOutputStream 提供一个完整的明文数据块。
这个类的 flush 和 close 方法还有一个重要区别,如果封装的 Cipher 对象实现了带有填充的块密码算法,这一点就更加重要了:
flush
通过强制写入已被封装的 Cipher 对象处理的所有缓冲输出字节来刷新底层OutputStream
。由封装的 Cipher 对象缓冲并等待其处理的任何字节将不会被写入。close
关闭底层OutputStream
并释放与其关联的任何系统资源。它调用封装的 Cipher 对象的doFinal
方法,导致任何由其缓冲的字节被处理并写入底层流(通过调用其 flush 方法)。
示例 2-7 使用 CipherOutputStream 和 FileOutputStream 的示例代码
以下代码展示了如何使用包含 Cipher 的 CipherOutputStream 和 FileOutputStream 以便将数据加密写入输出流:
try (FileInputStream fis = new FileInputStream("/tmp/a.txt");FileOutputStream fos = new FileOutputStream("/tmp/b.txt");CipherOutputStream 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();
}
示例 2-8 连接 CipherOutputStream 和 FileOutputStream 的示例代码
以下代码展示了如何轻松连接 CipherOutputStream 和 FileOutputStream 的几个实例。在这个示例中,假设 cipher1 和 cipher2 分别已使用相应的密钥初始化用于解密和加密:
try (FileInputStream fis = new FileInputStream("/tmp/a.txt");FileOutputStream fos = new FileOutputStream("/tmp/b.txt");CipherOutputStream cos1 = new CipherOutputStream(fos, cipher1);CipherOutputStream 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();
}
2.7.3 密封对象类 The SealedObject Class
这个类允许程序员创建一个对象,并使用密码算法保护其机密性。
任何实现了 java.io.Serializable
接口的对象,都可以通过创建一个 SealedObject
来封装原始对象,以序列化格式(即“深拷贝”),并使用密码算法(如 AES)对其进行加密(封印),以保护其机密性。加密的内容稍后可以被解密(使用正确的解密密钥和相应的算法)并反序列化,从而得到原始对象。
以下代码片段展示了典型用法:为了封印一个对象,你从一个要封印的对象和一个完全初始化的 Cipher 对象创建一个 SealedObject
,该 Cipher 对象将加密序列化的对象内容。在这个例子中,字符串 “This is a secret” 使用 AES 算法进行封印。注意,封印操作中可能使用的任何算法参数都存储在 SealedObject
中:
// 创建 Cipher 对象
// 注意:sKey 假定是指已经生成的 AES 密钥。
Cipher c = Cipher.getInstance("AES");
c.init(Cipher.ENCRYPT_MODE, sKey);// 执行封印
SealedObject so = new SealedObject("This is a secret", c);
原始被封印的对象可以通过以下两种不同方式恢复:
- 使用一个已经使用与封印对象时完全相同的算法、密钥、填充方案等初始化的 Cipher 对象:
c.init(Cipher.DECRYPT_MODE, sKey);
try {String s = (String)so.getObject(c);
} catch (Exception e) {// 采取一些措施
};
这种方法的优点是解开封印的一方不需要知道解密密钥。例如,一方可以使用所需的解密密钥初始化 Cipher 对象,然后将其交给另一方,后者随后解开封印的对象。
- 使用适当的解密密钥(由于 AES 是对称加密算法,我们使用相同的密钥进行封印和解开封印):
try {String s = (String)so.getObject(sKey);
} catch (Exception e) {// 采取一些措施
};
这种方法中,getObject
方法为适当的解密算法创建了一个 Cipher 对象,并使用给定的解密密钥和存储在封印对象中的算法参数(如果有的话)进行初始化。这种方法的优点是解开封印的一方不需要记住封印对象时使用的参数(例如,IV)。
2.8 消息认证码类 The Mac Class
类似于消息摘要,消息认证码(MAC)提供了一种检查在不可靠介质上传输或存储的信息完整性的方法,但在计算中包括了一个密钥。
只有拥有正确密钥的人才能够验证接收到的消息。通常,消息认证码用于两个共享密钥的方之间,以验证这些方之间传输的信息。
基于密码散列函数的MAC机制称为HMAC。HMAC可以与任何密码散列函数结合使用,例如SHA-256,以及一个秘密共享密钥。
Mac
类提供了消息认证码(MAC)的功能。请参阅 HMAC-SHA256 示例。
2.8.1 创建 Mac 对象 Creating a Mac Object
通过使用 Mac
类的静态工厂方法 getInstance()
来获取 Mac 对象。请参阅“如何请求和供应服务提供者实现”。
2.8.2 初始化 Mac 对象 Initializing a Mac Object
Mac 对象始终使用(秘密)密钥进行初始化,并且可以根据底层 MAC 算法的需要,选择性地使用一组参数进行初始化。
要初始化 Mac 对象,请调用其 init
方法之一:
public void init(Key key);public void init(Key key, AlgorithmParameterSpec params);
您可以使用实现 javax.crypto.SecretKey
接口的任何(秘密)密钥对象来初始化您的 Mac 对象。这可以是由 javax.crypto.KeyGenerator.generateKey()
返回的对象,或者是密钥协商协议的结果,如 javax.crypto.KeyAgreement.generateSecret()
返回的,或者是 javax.crypto.spec.SecretKeySpec
的一个实例。
对于一些 MAC 算法,用于初始化 Mac 对象的(秘密)密钥对象关联的(秘密)密钥算法并不重要(这是 SunJCE 提供者 HMAC-MD5 和 HMAC-SHA1 实现的情况)。然而,对于其他算法,(秘密)密钥算法确实很重要,如果使用具有不适当(秘密)密钥算法的(秘密)密钥对象,则会抛出 InvalidKeyException
。
2.8.3 计算 MAC Computing a MAC
可以通过一步(单部分操作)或多步(多部分操作)计算 MAC。如果您事先不知道数据的长度,或者数据太长而无法一次性全部存储在内存中,则多部分操作非常有用。
要一步计算某些数据的 MAC,请调用以下 doFinal
方法:
public byte[] doFinal(byte[] input);
要分多步计算某些数据的 MAC,请调用 update
方法之一:
public void update(byte input);public void update(byte[] input);public void update(byte[] input, int inputOffset, int inputLen);
多部分操作必须通过 doFinal
方法终止(如果最后一步仍有一些输入数据剩余),或者通过以下 doFinal
方法之一终止(如果没有输入数据剩余):
public byte[] doFinal();public void doFinal(byte[] output, int outOffset);
2.9 密钥封装机制类 The KEM Class
KEM 类是一个引擎类(参见引擎类和算法),它提供了密钥封装机制(Key Encapsulation Mechanism, KEM)的功能。
您可以使用 KEM 通过两个方之间的非对称或公钥密码学来保护对称密钥。发送方调用 encapsulate 方法生成一个秘密密钥和密钥封装消息,接收方调用 decapsulate 方法从密钥封装消息中恢复相同的密钥。
2.9 .1 准备 Preparation
接收方需要使用 KeyPairGenerator 创建一个密钥对。公钥被公布并提供给发送方,私钥则保密。
2.9 .2 创建 KEM 对象 Creating KEM Objects
每个方都需要创建一个 KEM 对象。KEM 对象是通过使用 KEM 类的静态工厂方法之一 getInstance() 来创建的。请参阅“如何请求和供应服务提供者实现”。
2.9 .3 创建封装器和解封装器 Creating an Encapsulator and a Decapsulator
在发送方,调用 KEM 对象的 newEncapsulator 方法之一来创建一个封装器。在此过程中使用接收方的公钥。在接收方,调用 KEM 对象的 newDecapsulator 方法之一来创建一个解封装器。在此过程中使用接收方的私钥。
2.9 .4 封装和解封装 Encapsulation and Decapsulation
发送方在其新创建的 KEM.Encapsulator 对象中调用 encapsulate 方法之一,该方法返回一个 KEM.Encapsulated 对象。KEM.Encapsulated 对象中的秘密密钥保密,其中的密钥封装消息被发送给接收方。
接收方将发送方的密钥封装消息传递给新创建的 KEM.Decapsulator 对象的 decapsulate 方法之一,该方法返回一个 SecretKey 对象。这个秘密密钥与发送方的秘密密钥相同。
发送方可以使用该密钥与接收方进行未来安全通信。
请参阅“封装和解封装密钥”的代码示例。
2.10 秘钥接口 Key Interfaces
java.security.Key
接口是所有不透明密钥的最高级别接口。它定义了所有不透明密钥对象共享的功能。
到目前为止,我们一直关注 JCA(Java 加密体系结构)的高级用途,而没有深入探讨密钥是什么以及它们是如何生成/表示的。现在是我们关注密钥的时候了。
不透明密钥表示是指您无法直接访问构成密钥的密钥材料。换句话说:“不透明”的只给您有限的密钥访问权限——只有 Key
接口定义的三个方法:getAlgorithm
、getFormat
和 getEncoded
。
这与透明表示形成对比,在透明表示中,您可以通过相应的 KeySpec
接口中定义的 get
方法逐个访问每个密钥材料值(见 “KeySpec 接口”)。
所有不透明密钥都有三个特征:
-
算法(Algorithm):该密钥的密钥算法。密钥算法通常是一个加密或非对称操作算法(如 AES、DSA 或 RSA),它将与这些算法以及相关算法(如 SHA256withRSA)一起工作。密钥的算法名称是使用以下方法获得的:
String getAlgorithm()
-
编码形式(Encoded Form):当需要在 Java 虚拟机外部的标准表示时使用的密钥的外部编码形式,例如在将密钥传输给其他方时。密钥根据标准格式(如 X.509 或 PKCS8)进行编码,并使用以下方法返回:
byte[] getEncoded()
-
格式(Format):编码密钥的格式名称。它由以下方法返回:
String getFormat()
通常,密钥是通过密钥生成器(如 KeyGenerator
类和 KeyPairGenerator
类)、证书、密钥规范(见 “KeySpec 接口”)使用 KeyFactory
,或者通过访问用于管理密钥的密钥库数据库的密钥库实现来获得的。使用 KeyFactory
,还可以以算法依赖的方式解析编码的密钥。
还可以使用 CertificateFactory
解析证书。
以下是 java.security.interfaces
和 javax.crypto.interfaces
包中扩展 Key
接口的接口列表:
SecretKey
PBEKey
PrivateKey
DHPrivateKey
DSAPrivateKey
ECPrivateKey
RSAMultiPrimePrivateCrtKey
RSAPrivateCrtKey
RSAPrivateKey
PublicKey
DHPublicKey
DSAPublicKey
ECPublicKey
RSAPublicKey
PublicKey
和 PrivateKey
接口
PublicKey
和 PrivateKey
接口(这两个接口都扩展了 Key
接口)是无方法的接口,用于类型安全性和类型识别。
2.11 密钥对类 The KeyPair Class
KeyPair类是密钥对(公钥和私钥)的简单持有者。
它有两种公共方法,一种用于返回私钥,另一种用于归还公钥:
PrivateKey getPrivate()
PublicKey getPublic()
2.12 秘钥接口规范和类 Key Specification Interfaces and Classes
密钥对象和密钥规范(KeySpecs)是密钥数据的两种不同表示形式。密码机使用密钥对象来初始化它们的加密算法,但密钥可能需要转换为更便携的格式以进行传输或存储。
透明表示的密钥意味着您可以通过相应的规范类中定义的某个 get 方法单独访问每个密钥材料值。例如,DSAPrivateKeySpec 定义了 getX、getP、getQ 和 getG 方法,用于访问私钥 x,以及用于计算密钥的 DSA 算法参数:素数 p、子素数 q 和基数 g。如果密钥存储在硬件设备上,其规范可能包含有助于在设备上识别密钥的信息。
这种表示与 Key Interfaces 接口定义的不透明表示形成对比,在不透明表示中,您无法直接访问密钥材料字段。换句话说,“不透明”表示只给您有限的密钥访问权限——只有 Key 接口定义的三个方法:getAlgorithm、getFormat 和 getEncoded。
密钥可以以算法特定的方式指定,或以算法独立的编码格式(如 ASN.1)指定。例如,DSA 私钥可以通过其组成部分 x、p、q 和 g 来指定(见 DSAPrivateKeySpec),或者也可以使用其 DER 编码来指定(见 PKCS8EncodedKeySpec)。
  KeyFactory
类和 SecretKeyFactory
类可以用来在不透明和透明密钥表示之间进行转换(即,密钥和密钥规范之间,假设操作是可能的。例如,智能卡上的私钥可能无法离开卡片。这样的密钥是不可转换的)。
在以下部分中,我们将讨论 java.security.spec 包中的密钥规范接口和类。
2.12.1 密钥规范接口(KeySpec Interface)
这个接口不包含任何方法或常量。它的唯一目的是对所有密钥规范进行分组并提供类型安全性。所有密钥规范都必须实现这个接口。
2.12.2 密钥规范子接口 The KeySpec Subinterfaces
Like the Key
interface, there are a similar set of KeySpec
interfaces.
SecretKeySpec
EncodedKeySpec
PKCS8EncodedKeySpec
X509EncodedKeySpec
DESKeySpec
DESedeKeySpec
PBEKeySpec
DHPrivateKeySpec
DSAPrivateKeySpec
ECPrivateKeySpec
RSAPrivateKeySpec
RSAMultiPrimePrivateCrtKeySpec
RSAPrivateCrtKeySpec
DHPublicKeySpec
DSAPublicKeySpec
ECPublicKeySpec
RSAPublicKeySpec
2.12.3 编码密钥规范类(EncodedKeySpec Class)
这个抽象类(实现了 KeySpec 接口)表示编码格式的公钥或私钥。它的 getEncoded 方法返回编码的密钥:
abstract byte[] getEncoded();
它的 getFormat 方法返回编码格式的名称:
abstract String getFormat();
请参阅下一部分关于具体实现 PKCS8EncodedKeySpec 和 X509EncodedKeySpec 的内容。
2.12.4 PKCS8EncodedKeySpec 类
这个类是 EncodedKeySpec 的子类,表示根据 PKCS8 标准指定格式的私钥的 DER 编码。
它的 getEncoded 方法返回根据 PKCS8 标准编码的密钥字节。它的 getFormat 方法返回字符串 “PKCS#8”。
2.12.5 X509EncodedKeySpec 类
这个类是 EncodedKeySpec 的子类,表示根据 X.509 标准指定格式的公钥的 DER 编码。
它的 getEncoded 方法返回根据 X.509 标准编码的密钥字节。它的 getFormat 方法返回字符串 “X.509”。
2.13 Generators and Factories
Java和JCA API的新手有时可能不理解生成器(generators)和工厂(factories)之间的区别。
Figure 2-10 Generators and Factories
生成器用于生成全新的对象。生成器可以以算法依赖或算法独立的方式进行初始化。例如,要创建一个Diffie-Hellman(DH)密钥对,应用程序可以指定必要的P和G值,或者生成器可以仅用适当的密钥长度进行初始化,生成器将根据参数生成全新的密钥。
另一方面,工厂用于将一种现有对象类型的数据转换为另一种。例如,应用程序可能有DH私钥的组成部分,并且可以将其包装为密钥规范接口(The KeySpec Interface),但需要将其转换为PrivateKey对象,以便KeyAgreement对象使用,反之亦然。或者他们可能有证书的字节数组,但需要使用CertificateFactory将其转换为X509Certificate对象。应用程序使用工厂对象进行转换。
2.13.1 秘钥工厂 KeyFactory 类 The KeyFactory Class
KeyFactory类是设计用于执行不透明密码学密钥接口和密钥规范接口与类(密钥材料的透明表示)之间转换的引擎类。
Figure 2-11 KeyFactory Class
Key工厂是双向的。它们允许您根据给定的密钥规范(密钥材料)构建不透明的密钥对象,或者以适当的格式检索密钥对象的底层密钥材料。
对于相同的密钥,可以存在多个兼容的密钥规范。例如,DSA公钥可以通过其组成部分y、p、q和g指定(见java.security.spec.DSAPublicKeySpec),或者也可以使用X.509标准中的DER编码指定(见The X509EncodedKeySpec Class)。
密钥工厂可用于在兼容的密钥规范之间进行转换。密钥解析可以通过在兼容的密钥规范之间进行转换来实现,例如,当您从X509EncodedKeySpec转换到DSAPublicKeySpec时,您基本上是将编码的密钥解析为其组成部分。有关示例,请参见“使用密钥规范和KeyFactory生成/验证签名”部分的末尾。
2.13.1.1 创建 KeyFactory 对象 Creating a KeyFactory Object
KeyFactory对象是通过使用KeyFactorygetInstance()静态工厂方法之一获得的。见“如何请求和供应服务提供者实现”。
2.13.1.2 密钥规范与密钥对象之间的转换 Converting Between a Key Specification and a Key Object
如果您有公钥的密钥规范,可以使用generatePublic方法从规范获取不透明的PublicKey对象:
PublicKey generatePublic(KeySpec keySpec)
类似地,如果您有私钥的密钥规范,可以使用generatePrivate方法从规范获取不透明的PrivateKey对象:
PrivateKey generatePrivate(KeySpec keySpec)
2.13.1.3 密钥对象与密钥规范之间的转换 Converting Between a Key Object and a Key Specification
如果您有一个Key对象,可以通过调用getKeySpec方法获取相应的密钥规范对象:
KeySpec getKeySpec(Key key, Class keySpec)
keySpec标识应返回密钥材料的规范类。例如,它可以是DSAPublicKeySpec.class,表示密钥材料应以DSAPublicKeySpec类的实例返回。见“使用密钥规范和KeyFactory生成/验证签名”。
2.13.2 SecretKeyFactory 类 The SecretKeyFactory Class
SecretKeyFactory类表示秘密密钥的工厂。与KeyFactory类不同(见The KeyFactory Class),javax.crypto.SecretKeyFactory对象仅操作秘密(对称)密钥,而java.security.KeyFactory对象处理密钥对的公钥和私钥组件。
Figure 2-12 SecretKeyFactory Class
Key工厂用于将Key接口(java.security.Key的不透明密码学密钥)转换为Key规范接口和类(以适当格式的底层密钥材料的透明表示),反之亦然。
java.security.Key的类型的对象,其中java.security.PublicKey、java.security.PrivateKey和javax.crypto.SecretKey是子类,是不透明的密钥对象,因为您无法了解它们的实现方式。底层实现是提供者依赖的,可能是基于软件或硬件。密钥工厂允许提供者提供自己实现的密码学密钥。
例如,如果您有一个由公共值y、素数模数p和基数g组成的Diffie-Hellman公钥规范,并且您将相同的规范输入不同提供者的Diffie-Hellman密钥工厂,生成的PublicKey对象很可能具有不同的底层实现。
提供者应记录其秘密密钥工厂支持的密钥规范。例如,SunJCE提供者提供的DES密钥的SecretKeyFactory支持DESKeySpec作为DES密钥的透明表示,DES-EDE密钥的SecretKeyFactory支持DESedeKeySpec作为DES-EDE密钥的透明表示,PBE的SecretKeyFactory支持PBEKeySpec作为底层密码的透明表示。
以下是一个示例,演示如何使用SecretKeyFactory将秘密密钥数据转换为SecretKey对象,该对象可用作后续Cipher操作:
// 注意以下字节不是现实的秘密密钥数据
// 字节仅作为使用数据的示例
// 字节(密钥材料)您已经拥有,以构建DESedeKeySpec。byte[] desEdeKeyData = getKeyData();
DESedeKeySpec desEdeKeySpec = new DESedeKeySpec(desEdeKeyData);
SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("DESede");
SecretKey secretKey = keyFactory.generateSecret(desEdeKeySpec);
在这种情况下,SecretKey的底层实现基于KeyFactory的提供者。
从相同的密钥材料创建功能等效的SecretKey对象的另一种提供者独立的方法,是使用javax.crypto.spec.SecretKeySpec类,它实现了javax.crypto.SecretKey接口:
byte[] aesKeyData = getKeyData();
SecretKeySpec secretKey = new SecretKeySpec(aesKeyData, "AES");
2.13.2.1 创建 SecretKeyFactory 对象 Creating a SecretKeyFactory Object
SecretKeyFactory对象是通过使用SecretKeyFactory getInstance()静态工厂方法之一获得的。见“如何请求和供应服务提供者实现”。
2.13.2.2 密钥规范与Secret Key对象之间的转换 Converting Between a Key Specification and a Secret Key Object
如果您有秘密密钥的密钥规范,可以使用generateSecret方法从规范获取不透明SecretKey对象:
SecretKey generateSecret(KeySpec keySpec)
2.13.2.3 Secret Key对象与密钥规范之间的转换 Converting Between a Secret Key Object and a Key Specification
如果您有SecretKey对象,可以通过调用getKeySpec方法获取相应的密钥规范对象:
KeySpec getKeySpec(Key key, Class keySpec)
keySpec标识应返回密钥材料的规范类。例如,它可以是DESKeySpec.class,表示密钥材料应以DESKeySpec类的实例返回。
2.13.3 KeyPairGenerator 类 The KeyPairGenerator Class
KeyPairGenerator类是一个引擎类,用于生成公钥和私钥对。
Figure 2-13 KeyPairGenerator Class
有两种生成密钥对的方式:以算法独立的方式和以算法特定的方式。两者之间的唯一区别在于对象的初始化。
见“生成一对密钥”以获取调用KeyPairGenerator方法的示例。
2.13.3.1 创建 KeyPairGenerator Creating a KeyPairGenerator
所有密钥对生成都从KeyPairGenerator开始。KeyPairGenerator对象是通过使用KeyPairGenerator getInstance()静态工厂方法之一获得的。见“如何请求和供应服务提供者实现”。
2.13.3.2 初始化 KeyPairGenerator Initializing a KeyPairGenerator
特定算法的密钥对生成器创建可与该算法一起使用的公钥/私钥对。它还将算法特定的参数与生成的每个密钥关联。
在生成密钥之前,密钥对生成器需要被初始化。在大多数情况下,算法独立的初始化就足够了。但在其他情况下,可以使用算法特定的初始化。
2.13.3.3 算法独立初始化 Algorithm-Independent Initialization
所有密钥对生成器共享密钥大小和随机源的概念。对于不同的算法,密钥大小的解释不同。例如,在DSA算法中,密钥大小对应于模数的长度。
一个initialize方法接受两个普遍共享类型的参数:
void initialize(int keysize, SecureRandom random)
另一个initialize方法只接受密钥大小参数;它使用系统提供的随机源:
void initialize(int keysize)
由于在调用这些算法独立的initialize方法时没有指定其他参数,因此提供者将决定如何处理要与每个密钥关联的算法特定参数(如果有)。
如果算法是“DSA”算法,且模数大小(密钥大小)是512、768、1024、2048或3072,则SUN提供者使用一组预计算的p、q和g参数值。如果模数大小不是这些值之一,则SUN提供者会创建一组新的参数。其他提供者可能对不仅仅是前述三种模数大小有预计算的参数集。还有一些可能根本没有预计算的参数列表,而是总是创建新的参数集。
2.13.3.4 算法特定初始化 Algorithm-Specific Initialization
对于已经存在一组算法特定参数的情况(例如DSA中的“社区参数”),有两个initialize方法具有AlgorithmParameterSpec接口参数。一个还有SecureRandom参数,而另一个的随机源由系统提供:
void initialize(AlgorithmParameterSpec params, SecureRandom random)void initialize(AlgorithmParameterSpec params)
2.13.3.5 生成密钥对 Generating a Key Pair
生成密钥对的过程始终相同,无论初始化(和算法)如何。您总是从KeyPairGenerator调用以下方法:
KeyPair generateKeyPair()
对generateKeyPair的多次调用将产生不同的密钥对。
2.13.4 KeyGenerator 类 The KeyGenerator Class
密钥生成器用于为对称算法生成秘密密钥。
Figure 2-14 The KeyGenerator Class
2.13.4.1 创建KeyGenerator对象 Initializing a KeyGenerator Object
KeyGenerator对象是通过使用KeyGenerator getInstance()静态工厂方法之一获得的。见“如何请求和供应服务提供者实现”。
2.13.4.2 初始化 KeyGenerator 对象 Algorithm-Independent Initialization
特定对称密钥算法的密钥生成器创建可与该算法一起使用的对称密钥。它还将算法特定的参数(如果有)与生成的密钥关联。
有两种生成密钥的方式:以算法独立的方式和以算法特定的方式。两者之间的唯一区别在于对象的初始化:
2.13.4.3 生成秘钥 Creating a Key
以下方法生成密钥:
public SecretKey generateKey();
2.14 密钥协商 The KeyAgreement Class
密钥协商是一种协议,通过它两个或多个方可以在不交换任何秘密信息的情况下建立相同的加密密钥。
Figure 2-15 The KeyAgreement Class
每个参与方都用其私钥初始化他们的密钥协商对象,然后输入将参与通信的每个方的公钥。在大多数情况下,只有两方参与,但像 Diffie-Hellman 这样的算法允许多个方(3个或更多)参与。当所有公钥都输入完毕后,每个 KeyAgreement 对象将生成(协商)相同的密钥。
KeyAgreement 类提供了密钥协商协议的功能。用于建立共享秘密的密钥是由一个密钥生成器(KeyPairGenerator 或 KeyGenerator)、KeyFactory 或作为密钥协商协议中间阶段的结果创建的。
2.14.1 创建 KeyAgreement 对象 Creating a KeyAgreement Object
每个参与密钥协商的方都必须创建一个 KeyAgreement 对象。KeyAgreement 对象是通过使用 KeyAgreement 类的静态工厂方法之一 getInstance() 来获取的。见“如何请求和供应服务提供者实现”。
2.14.2 初始化 KeyAgreement 对象 Initializing a KeyAgreement Object
您用您的私有信息初始化一个 KeyAgreement 对象。在 Diffie-Hellman 的情况下,您用您的 Diffie-Hellman 私钥来初始化它。其他初始化信息可能包含一个随机源和/或一组算法参数。请注意,如果所请求的密钥协商算法需要指定算法参数,并且只提供了密钥,而没有提供参数来初始化 KeyAgreement 对象,则密钥必须包含所需的算法参数。(例如,Diffie-Hellman 算法使用素数模 p 和基生成元 g 作为其参数。)
要初始化 KeyAgreement 对象,请调用它的一个 init 方法:
public void init(Key key);
public void init(Key key, SecureRandom random);
public void init(Key key, AlgorithmParameterSpec params);
public void init(Key key, AlgorithmParameterSpec params, SecureRandom random);
2.14.3 执行密钥协商阶段 Executing a KeyAgreement Phase
每个密钥协商协议都由一系列需要参与的每个方执行的阶段组成。
要执行密钥协商的下一个阶段,请调用 doPhase 方法:
public Key doPhase(Key key, boolean lastPhase);
key 参数包含该阶段要处理的密钥。在大多数情况下,这是密钥协商中其他方的公钥,或是由前一阶段生成的中间密钥。doPhase 可能会返回一个中间密钥,您可能需要将其发送给密钥协商的其他方,以便他们在后续阶段进行处理。
lastPhase 参数指定要执行的阶段是否是密钥协商中的最后一个阶段:值为 FALSE 表示这不是密钥协商的最后一个阶段(还有更多阶段要跟随),值为 TRUE 表示这是密钥协商的最后一个阶段并且密钥协商已完成,即,下一步可以调用 generateSecret。
在两方之间的 Diffie-Hellman 密钥交换示例中,您调用一次 doPhase,并将 lastPhase 设置为 TRUE。在三方之间的 Diffie-Hellman 示例中,您调用 doPhase 两次:第一次将 lastPhase 设置为 FALSE,第二次将 lastPhase 设置为 TRUE。
2.14.4 生成共享密钥 Generating the Shared Secret
在每个参与方都执行了所有必需的密钥协商阶段之后,它可以通过调用 generateSecret 方法之一来计算共享密钥:
public byte[] generateSecret();
public int generateSecret(byte[] sharedSecret, int offset);
public SecretKey generateSecret(String algorithm);
2.15 秘钥管理 Key Management
数据库称为“密钥库”(keystore),可用于管理密钥和证书的存储库。(证书是一种由一个实体数字签名的声明,表明另一个实体的公钥具有特定的值。)
2.15.1 密钥库位置 Keystore Location
用户密钥库默认存储在用户主目录中的一个名为 .keystore
的文件中,主目录由 user.home
系统属性确定,其默认值取决于操作系统:
- Linux 和 macOS:
/home/username/
- Windows:
C:\Users\username\
当然,密钥库文件可以按需定位。在某些环境中,可能需要存在多个密钥库。例如,一个密钥库可能保存用户的私钥,另一个可能保存用于建立信任关系的证书。
除了用户的密钥库,JDK 还维护一个系统范围的密钥库,用于存储来自各种证书颁发机构(CA)的信任证书。这些 CA 证书可用于帮助做出信任决策。例如,在 SSL/TLS/DTLS 中,当 SunJSSE 提供者遇到来自远程对等方的证书时,默认的 trustmanager 将查阅以下文件之一,以确定连接是否可信:
- Linux 和 macOS:
<java-home>/lib/security/cacerts
- Windows:
<java-home>\lib\security\cacerts
应用程序可以设置和使用自己的密钥库,或者甚至使用前面描述的用户密钥库,而不是使用系统范围的 cacerts 密钥库。
2.15.2 密钥库实现 Keystore Implementation
   KeyStore
类提供了访问和修改密钥库信息的明确定义的接口。可能存在多个不同的具体实现,其中每个实现是针对特定类型的密钥库。
目前,有两个命令行工具使用 KeyStore
:keytool
和 jarsigner
。它还由策略引用实现使用,当它处理指定来自各种来源的代码的权限(允许访问系统资源)的策略文件时。由于 KeyStore
是公开可用的,JDK 用户可以编写使用它的其他安全应用程序。
应用程序可以从不同的提供者选择不同类型的密钥库实现,使用 KeyStore
类中的 getInstance
工厂方法。密钥库类型定义了密钥库信息的存储和数据格式,以及用于保护密钥库中的私钥和密钥库本身完整性的算法。不同类型的密钥库实现是不兼容的。
默认的密钥库实现是 “pkcs12”。这是一个基于 RSA PKCS12 个人信息交换语法标准的跨平台密钥库。此标准主要用于存储或传输用户的私钥、证书和各种秘密。PKCS12 密钥库中的个别条目可以关联任意属性。
keystore.type=pkcs12
要让工具和其他应用程序使用不同的默认密钥库实现,您可以更改该行以指定不同的类型。
一些应用程序(如 keytool
)还允许您覆盖默认的密钥库类型(通过命令行参数 -storetype
)。
注意:密钥库类型指定不区分大小写。例如,“jks” 被视为与 “JKS” 相同。
PKCS12 是默认且推荐的密钥库类型。然而,JDK 实现还包括其他三种类型的密钥库。
- “jceks” 是 “jks” 的另一种专有密钥库格式,使用带有三重 DES 的基于密码的加密。
- “jks” 实现将密钥库作为文件实现,使用专有的密钥库类型(格式)。它使用自己的单独密码保护每个私钥,并还使用一个(可能不同的)密码保护整个密钥库的完整性。
- “dks” 是一个域密钥库。它是一个密钥库的集合,表现为单个逻辑密钥库。由给定域组成的密钥库由配置数据指定,其语法在
DomainLoadStoreParameter
中描述。
密钥库实现是基于提供者的。如果你想编写自己的 KeyStore
实现,请参见 Java 加密体系结构中的 “如何实现提供者”。
2.15.3 秘钥存储类 The KeyStore Class
KeyStore
类提供了访问和修改密钥库信息的明确定义的接口。
KeyStore
类是一个引擎类和算法。
`Figure 2-16 KeyStore Class
这个类代表内存中的密钥和证书集合。KeyStore
管理两种类型的条目:
-
密钥条目:这种类型的密钥库条目保存非常敏感的加密密钥信息,必须防止未经授权的访问。通常,存储在这种类型的条目中的密钥是秘密密钥,或者是私钥以及认证相应公钥的证书链。
-
受信任的证书条目:这种类型的条目包含属于另一方的单个公钥证书。之所以称为受信任的证书,是因为密钥库所有者信任证书中的公钥确实属于证书的主题(所有者)所识别的身份。
`每种类型的条目都可以通过 “别名” 字符串在密钥库中标识。在私钥及其关联的证书链的情况下,这些字符串区分实体可能用于自我认证的不同方式。例如,实体可能使用不同的证书颁发机构或使用不同的公钥算法进行自我认证。
`密钥库是否持久化,以及如果密钥库持久化,密钥库使用的机制在这里没有指定。这种约定允许使用各种技术来保护敏感(例如,私钥或秘密密钥)。智能卡或其他集成的加密引擎(SafeKeyper)是一个选项,也可以使用更简单的机制,如文件(以各种格式)。
以下是主要的
KeyStore` 方法的描述。
2.15.3.1 创建 KeyStore 对象 Creating a KeyStore Object
KeyStore 对象是通过使用 KeyStore 的
getInstance()` 方法之一获得的。见“如何请求和供应服务提供者实现”。
2.15.3.2 加载特定的密钥库到内存 Loading a Particular Keystore into Memory
在 KeyStore 对象使用之前,必须通过
load` 方法将实际的密钥库数据加载到内存中:
final void load(InputStream stream, char[] password)
`可选的密码用于检查密钥库数据的完整性。如果没有提供密码,则不执行完整性检查。
要创建一个空的密钥库,将
null作为
InputStream参数传递给
load` 方法。
通过传递
DomainLoadStoreParameter到替代的
load` 方法来加载 DKS 密钥库:
final void load(KeyStore.LoadStoreParameter param)
2.15.3.3 获取密钥库别名列表 Getting a List of the Keystore Aliases
所有密钥库条目都通过唯一的别名访问。
aliases` 方法返回密钥库中别名名称的枚举:
final Enumeration<String> aliases()
2.15.3.4 确定密钥库条目类型 Determining Keystore Entry Types
`如 KeyStore 类所述,密钥库中有两种不同类型的条目。以下方法分别确定由给定别名指定的条目是密钥/证书还是受信任的证书条目:
final boolean isKeyEntry(String alias)
final boolean isCertificateEntry(String alias)
2.15.3.5 添加/设置/删除密钥库条目 Adding/Setting/Deleting Keystore Entries
setCertificateEntry` 方法将证书分配给指定的别名:
final void setCertificateEntry(String alias, Certificate cert)
如果别名不存在,将创建一个带有该别名的受信任证书条目。如果别名存在并标识一个受信任的证书条目,则与它关联的证书将被
cert` 替换。
  
setKeyEntry` 方法添加(如果别名尚未存在)或设置密钥条目:
final void setKeyEntry(String alias, Key key, char[] password, Certificate[] chain)
final void setKeyEntry(String alias, byte[] key, Certificate[] chain)
`在以字节数组形式的密钥方法中,它是受保护格式的密钥字节。例如,在 SUN 提供者提供的密钥库实现中,预期密钥字节数组包含一个受保护的私钥,按照 PKCS8 标准编码为 EncryptedPrivateKeyInfo。在另一种方法中,密码是用来保护密钥的密码。
``deleteEntry` 方法用于删除条目:
final void deleteEntry(String alias)
PKCS #12 密钥库支持包含任意属性的条目。使用
PKCS12Attribute` 类创建属性。创建新的密钥库条目时,使用接受属性的构造方法。最后,使用以下方法将条目添加到密钥库:
final void setEntry(String alias, Entry entry, ProtectionParameter protParam)
2.15.3.6 从密钥库获取信息 Getting Information from the Keystore
  
getKey` 方法返回与给定别名关联的密钥。使用给定的密码恢复密钥:
final Key getKey(String alias, char[] password)
`以下方法分别返回与给定别名关联的证书或证书链:
final Certificate getCertificate(String alias)
final Certificate[] getCertificateChain(String alias)
`您可以通过以下方式确定证书与给定证书匹配的第一个条目的名称(别名):
final String getCertificateAlias(Certificate cert)
`PKCS #12 密钥库支持包含任意属性的条目。使用以下方法检索可能包含属性的条目:
final Entry getEntry(String alias, ProtectionParameter protParam)
然后使用
KeyStore.Entry.getAttributes方法提取这些属性,并使用
KeyStore.Entry.Attribute` 接口的方法进行检查。
2.15.3.7 保存密钥库 Saving the KeyStore
可以通过
store` 方法保存内存中的密钥库:
final void store(OutputStream stream, char[] password)
密码用于计算密钥库数据的
final void store(KeyStore.LoadStoreParameter param)
2.16 算法参数类 Algorithm Parameters Classes
像密钥和密钥规范一样,算法的初始化参数由 AlgorithmParameters
或 AlgorithmParameterSpecs
表示。
根据使用情况,算法可以直接使用参数,或者可能需要将参数转换为更便携的格式以进行传输或存储。
通过 AlgorithmParameterSpec
对参数集的透明表示意味着您可以单独访问集合中的每个参数值。您可以通过相应规范类中定义的 get
方法来访问这些值(例如,DSAParameterSpec
定义了 getP
、getQ
和 getG
方法,分别用于访问 p、q 和 g)。
与此相反,AlgorithmParameters
类类提供不透明的表示,您无法直接访问参数字段。您只能获取与参数集相关联的算法名称(通过 getAlgorithm
)和参数集的某种编码(通过 getEncoded
)。
2.16.1 透明算法参数接口 The AlgorithmParameterSpec Interface
AlgorithmParameterSpec
是一个用于透明指定密码学参数的接口。这个接口不包含任何方法或常量。它的唯一目的是对所有参数规范进行分组(并提供类型安全性)。所有参数规范都必须实现此接口。
以下是 java.security.spec
和 javax.crypto.spec
包中的算法参数规范接口和类:
DHParameterSpec
DHGenParameterSpec
DSAParameterSpec
ECGenParameterSpec
ECParameterSpec
GCMParameterSpec
IvParameterSpec
MGF1ParameterSpec
OAEPParameterSpec
PBEParameterSpec
PSSParameterSpec
RC2ParameterSpec
RC5ParameterSpec
RSAKeyGenParameterSpec
以下算法参数规范用于 XML 数字签名:
Interface C14NMethodParameterSpec
Interface DigestMethodParameterSpec
Interface SignatureMethodParameterSpec
Interface TransformParameterSpec
Interface ExcC14NParameterSpec
Interface HMACParameterSpec
Interface XPathFilter2ParameterSpec
Interface XPathFilterParameterSpec
XSLTTransformParameterSpec
2.16.2 算法参数类 The AlgorithmParameters Class
  AlgorithmParameters
类提供密码学参数的不透明表示。
2.16.2.1 创建 AlgorithmParameters
对象 Creating an AlgorithmParameters Object
通过使用 AlgorithmParameters
的静态工厂方法之一 getInstance()
来获取 AlgorithmParameters
对象。更多信息,请参见“如何请求和供应服务提供者实现”。
2.16.2.2 初始化 AlgorithmParameters
对象 Initializing an AlgorithmParameters Object
实例化 AlgorithmParameters
对象后,必须通过调用 init
方法,使用适当的参数规范或参数编码来初始化:
void init(AlgorithmParameterSpec paramSpec)
void init(byte[] params)
void init(byte[] params, String format)
在这些 init
方法中,params
是包含编码参数的数组,format
是解码格式的名称。在只有 params
参数而没有 format
参数的 init
方法中,使用参数的主要解码格式。如果参数存在 ASN.1 规范,则主要解码格式为 ASN.1。
2.16.2.3 获取编码的参数 Obtaining the Encoded Parameters
可以通过调用 getEncoded
获取 AlgorithmParameters
对象中表示的参数的字节编码:
byte[] getEncoded()
此方法以主要编码格式返回参数。如果参数存在 ASN.1 规范,则参数的主要编码格式为 ASN.1。
如果需要以指定的编码格式返回参数,请使用:
byte[] getEncoded(String format)
如果 format
是 null
,则使用参数的主要编码格式,如另一个 getEncoded
方法。
2.16.2.4 将 AlgorithmParameters
对象转换为透明规范 Converting an AlgorithmParameters Object to a Transparent Specification
可以通过调用 getParameterSpec
从 AlgorithmParameters
对象获取算法参数的透明参数规范:
AlgorithmParameterSpec getParameterSpec(Class paramSpec)
paramSpec
标识应返回参数的规范类。规范类可以是 DSAParameterSpec.class
,表示参数应以 DSAParameterSpec
类的实例返回。(这个类在 java.security.spec
包中。)
2.16.3 AlgorithmParameterGenerator
类 The AlgorithmParameterGenerator Class
AlgorithmParameterGenerator
类是一个引擎类和算法,用于生成一组适用于特定算法的全新参数(在创建 AlgorithmParameterGenerator
实例时指定算法)。当您没有现有的算法参数集,并且希望从头开始生成一个时,使用此对象。
2.16.3.1 创建 AlgorithmParameterGenerator
对象 Creating an AlgorithmParameterGenerator Object
通过使用 AlgorithmParameterGenerator
的静态工厂方法之一 getInstance()
来获取 AlgorithmParameterGenerator
对象。见“如何请求和供应服务提供者实现”。
2.16.3.2 初始化 AlgorithmParameterGenerator
对象 Initializing an AlgorithmParameterGenerator Object
AlgorithmParameterGenerator
对象可以通过两种不同的方式进行初始化:算法独立方式或算法特定方式。
算法独立方法使用所有参数生成器共享的概念:“大小”和随机源。所有算法参数都普遍共享大小的概念,尽管它对不同算法的解释不同。例如,在 DSA 算法的参数中,“大小”对应于素数模数的大小,以位为单位。(请参阅 Java 安全标准算法名称,了解更多关于特定算法的大小。)使用这种方法时,如果有任何算法特定的参数生成值,则默认为某些标准值。一个 init
方法接受这两种普遍共享类型的参数:
void init(int size, SecureRandom random);
另一个 init
方法只接受大小参数,并使用系统提供的随机源:
void init(int size)
第三种方法使用算法特定的语义来初始化参数生成器对象,这些语义由 AlgorithmParameterSpec
对象中提供的一组算法特定的参数生成值表示:
void init(AlgorithmParameterSpec genParamSpec, SecureRandom random)void init(AlgorithmParameterSpec genParamSpec)
例如,生成 Diffie-Hellman 系统参数时,参数生成值通常包括素数模数的大小和随机指数的大小,两者都以位为单位指定。
2.16.3.3 生成算法参数 Generating Algorithm Parameters
创建并初始化 AlgorithmParameterGenerator
对象后,您可以使用 generateParameters
方法生成算法参数:
AlgorithmParameters generateParameters()
2.17 认证工厂类 The CertificateFactory Class
CertificateFactory
类定义了一个证书工厂的功能,该工厂用于从它们的编码生成证书和证书撤销列表(CRL)对象。
  CertificateFactory
类是一个引擎类和算法。
X.509 的证书工厂必须返回实例化为 java.security.cert.X509Certificate
的证书,以及实例化为 java.security.cert.X509CRL
的 CRL。
2.17.1 创建 CertificateFactory 对象 Creating a CertificateFactory Object
通过使用 getInstance()
静态工厂方法之一来获取 CertificateFactory
对象。更多信息,请参见“如何请求和供应服务提供者实现”。
2.17.2 生成证书对象 Generating Certificate Objects
要生成一个证书对象并用从输入流中读取的数据初始化它,请使用 generateCertificate
方法:
final Certificate generateCertificate(InputStream inStream)
要返回从给定输入流中读取的证书的(可能为空的)集合视图,请使用 generateCertificates
方法:
final Collection generateCertificates(InputStream inStream)
2.17.3 生成 CRL 对象 Generating CRL Objects
要生成一个证书撤销列表(CRL)对象并用从输入流中读取的数据初始化它,请使用 generateCRL
方法:
final CRL generateCRL(InputStream inStream)
要返回从给定输入流中读取的 CRL 的(可能为空的)集合视图,请使用 generateCRLs
方法:
final Collection generateCRLs(InputStream inStream)
2.17.4 生成 CertPath 对象 Generating CertPath Objects
PKIX 证书路径构建器和验证器由 Internet X.509 公钥基础设施证书和 CRL 配置文件定义,RFC 5280。
用于从集合和 LDAP 目录检索证书和 CRL 的证书存储实现,使用 PKIX LDAP V2 Schema,也可从 IETF 作为 RFC 2587 获取。
要生成一个 CertPath
对象并用从输入流中读取的数据初始化它,请使用以下 generateCertPath
方法之一(指定或不指定数据的编码):
final CertPath generateCertPath(InputStream inStream)final CertPath generateCertPath(InputStream inStream, String encoding)
要生成一个 CertPath
对象并用证书列表初始化它,请使用以下方法:
final CertPath generateCertPath(List<? extends Certificate> certificates)
要检索此证书工厂支持的 CertPath
编码列表,可以调用 getCertPathEncodings
方法:
final Iterator<String> getCertPathEncodings()
默认编码将首先列出。
三、如何在SSL/TLS实现中使用JCA How the JCA Might Be Used in a SSL/TLS Implementation
理解了 JCA 类之后,我们可以考虑如何将这些类结合起来实现像 SSL/TLS 这样的高级网络协议。
在 TLS 和 DTLS 协议的 “SSL/TLS 概述” 部分从高层次描述了这些协议的工作原理。由于非对称(公钥)密码操作比对称操作(密钥)慢得多,因此公钥密码学被用来建立密钥,然后这些密钥被用来保护实际的应用程序数据。简化地讲,SSL/TLS 握手涉及交换初始化数据,执行一些公钥操作以得到一个密钥,然后使用该密钥来加密进一步的通信。
注意:这里呈现的细节仅展示了如何使用这些类。本节不会提供足够的信息来构建 SSL/TLS 实现。有关更多信息,请参见 Java 安全套接字扩展(JSSE)参考指南和 RFC 5246:传输层安全(TLS)协议,版本 1.2。
假设这个 SSL/TLS 实现将作为 JSSE 提供者提供。首先编写一个具体的 Provider 类实现,最终将在 Security 类的提供者列表中注册。此提供者主要提供从算法名称到实际实现类的映射。(即:“SSLContext.TLS”->“com.foo.TLSImpl”)当应用程序请求一个 “TLS” 实例(通过 SSLContext.getInstance(“TLS”)),将查询提供者的列表以获取请求的算法,并创建一个适当的实例。
在讨论实际握手的细节之前,需要快速回顾一下 JSSE 的架构。JSSE 架构的核心是 SSLContext。上下文最终创建最终对象(SSLSocket 和 SSLEngine),这些对象实际实现了 SSL/TLS 协议。SSLContext 用两个回调类 KeyManager 和 TrustManager 初始化,这些类允许应用程序首先选择要发送的认证材料,其次验证对等方发送的凭据。
JSSE KeyManager 负责选择向对等方展示的凭据。有许多算法是可能的,但常见的策略是维护一个 RSA 或 DSA 公钥/私钥对以及一个 X509Certificate,它们由磁盘文件支持的 KeyStore。当 KeyStore 对象被初始化并从文件中加载时,文件的原始字节被使用 KeyFactory 转换为 PublicKey 和 PrivateKey 对象,证书链的字节使用 CertificateFactory 转换。当需要凭据时,KeyManager 只需查询此 KeyStore 对象并确定要展示哪些凭据。
KeyStore 的内容可能最初是使用像 keytool 这样的实用程序创建的。keytool 创建一个 RSA 或 DSA KeyPairGenerator 并用适当的密钥大小初始化它。然后,这个生成器被用来创建一个 KeyPair,keytool 将把其连同新创建的证书一起存储在 KeyStore 中,最终写入磁盘。
JSSE TrustManager 负责验证从对等方接收到的凭据。验证凭据有许多方法:其中之一是创建一个 CertPath 对象,并让 JDK 内置的公钥基础设施(PKI)框架处理验证。在内部,CertPath 实现可能会创建一个 Signature 对象,并使用它来验证证书链中的每个签名。
有了对架构的基本了解,我们可以看看 SSL/TLS 握手中的一些步骤。客户端首先向服务器发送一个 ClientHello 消息。服务器选择一个要使用的密码套件,并在 ServerHello 消息中发送回来,并开始基于套件选择创建 JCA 对象。我们将在以下示例中使用仅限服务器认证。
图 2-17 SSL/TLS 消息
图 2-17 的描述如下
“图 2-17 SSL/TLS 消息” 的描述
以下示例描述了仅限服务器认证。示例极大地简化了,但给出了如何将 JSSE 类结合起来创建更高级别协议的概念:
示例 2-9 SSL/TLS 服务器使用基于 RSA 的密码套件,如 TLS_RSA_WITH_AES_128_CBC_SHA
查询服务器的 KeyManager,并返回一个适当的 RSA 条目。服务器的凭据(即:证书/公钥)在服务器的 Certificate 消息中发送。客户端的 TrustManager 验证服务器的证书,如果接受,客户端使用 SecureRandom 对象生成一些随机字节。然后使用已使用服务器证书中找到的 PublicKey 初始化的加密非对称 RSA Cipher 对象进行加密。这个加密数据在 Client Key Exchange 消息中发送。服务器将使用相应的 PrivateKey 通过类似的解密模式 Cipher 恢复字节。然后这些字节被用来建立实际的加密密钥。
示例 2-10 选择一个临时 Diffie-Hellman 密钥协商算法和 DSA 签名算法,如 TLS_DHE_DSS_WITH_AES_128_CBC_SHA
双方必须各自使用 KeyPairGenerator 建立一个新的临时 DH 公钥/私钥对。每个生成器创建 DH 密钥,然后可以使用 KeyFactory 和 DHPublicKeySpec 类进一步转换为片段。然后每一方创建一个 KeyAgreement 对象,并用它们各自的 DH 私钥初始化它。服务器在 ServerKeyExchange 消息中发送其公钥片段(由 DSA 签名算法保护),客户端在 ClientKeyExchange 消息中发送其公钥。当公钥使用另一个 KeyFactory 重新组装时,它们被送入协议对象。然后 KeyAgreement 对象生成一致的字节,然后用来建立实际的加密密钥。
一旦确定了实际的加密密钥,就使用密钥初始化对称 Cipher 对象,该 Cipher 用来保护所有传输中的数据。为了帮助确定数据是否已被修改,创建了一个 MessageDigest 并接收了一份要发送到网络的数据的副本。当数据包完成时,将摘要(哈希)附加到数据上,整个数据包由 Cipher 加密。如果使用像 AES 这样的块密码,必须对数据进行填充以构成一个完整的块。在远程端,步骤只是简单地反向执行。
四、加密强度配置 Cryptographic Strength Configuration
您可以使用司法管辖区策略文件(见 Jurisdiction Policy File Format)和安全属性文件配置 Java 加密扩展(JCE)架构的加密强度。
在 Oracle Java JDK 9 之前,默认的加密强度由 Oracle 实现为“强但受限”(例如,AES 密钥限制为 128 位)。为了移除这个限制,管理员可以下载并安装一个单独的“无限强度”司法管辖区策略文件包。JDK 9 对司法管辖区策略文件机制进行了重新设计,现在它允许更灵活的配置。Oracle JDK 现在默认值为“无限”而不是“有限”。像往常一样,管理员和用户必须继续遵循他们地理位置的所有进出口指南。现在的激活加密强度是通过安全属性(通常设置在 java.security 属性文件中)与配置目录中找到的司法管辖区策略文件结合确定的。
提供无限加密强度或强但有限加密强度所需的所有 JCE 策略文件都随 JDK 捆绑提供。
4.1 加密强度配置 Cryptographic Strength Settings
每个 <java_home>/conf/security/policy
下的目录代表一组由它们包含的司法管辖区策略文件定义的策略配置。您可以通过设置 crypto.policy 安全属性(在文件 <java_home>/conf/security/java.security
中配置)指向该目录来激活由目录中的策略文件表示的特殊加密强度设置。
注意:java.security 文件中的属性通常只解析一次。如果您已修改此文件中的任何属性,请重新启动应用程序以确保更改得到正确反映。
JDK 附带了两个这样的目录:limited 和 unlimited,每个目录都包含多个策略文件。默认情况下,crypto.policy 安全属性设置为:
crypto.policy = unlimited
总体值是目录内文件的交集。这些策略文件设置是 VM 范围的,影响在此 VM 上运行的所有应用程序。如果您想覆盖应用程序级别的加密强度,请参见“如何使应用程序免受加密限制”。
4.2 无限目录内容 Unlimited Directory Contents
无限目录包含以下策略文件:
-
<java_home>/conf/security/unlimited/default_US_export.policy
// 默认美国出口政策文件。 grant { // 对任何算法没有限制。permission javax.crypto.CryptoAllPermission; };
注意:由于目前对从美国出口的加密没有限制,default_US_export.policy 文件没有设置限制。
-
<java_home>/conf/security/unlimited/default_local.policy
// 没有对加密强度限制的国家的特定国家政策文件。 grant { // 对任何算法没有限制。permission javax.crypto.CryptoAllPermission; };
注意:根据国家的不同,可能会有本地限制,但由于此策略文件位于无限目录中,这里没有列出任何限制。
要将这些两个文件中定义的无限加密强度设置为 crypto.policy = unlimited
,请在文件 <java_home>/conf/security/java.security
中设置。
4.3 有限目录内容 Limited Directory Contents
有限目录目前包含以下策略文件:
-
<java_home>/conf/security/limited/default_US_export.policy
// 默认美国出口政策文件。 grant { // 对任何算法没有限制。permission javax.crypto.CryptoAllPermission; };
注意:即使这在有限目录中,由于目前对从美国出口的加密没有限制,默认的
default_US_export.policy
文件也没有设置限制。 -
<java_home>/conf/security/limited/default_local.policy
// 一些国家对加密强度有进口限制。这个策略文件 // 是全球可进口的。 grant {permission javax.crypto.CryptoPermission "DES", 64;permission javax.crypto.CryptoPermission "DESede", *;permission javax.crypto.CryptoPermission "RC2", 128, "javax.crypto.spec.RC2ParameterSpec", 128;permission javax.crypto.CryptoPermission "RC4", 128;permission javax.crypto.CryptoPermission "RC5", 128, "javax.crypto.spec.RC5ParameterSpec", *, 12, *;permission javax.crypto.CryptoPermission "RSA", *;permission javax.crypto.CryptoPermission *, 128; };
注意:此本地策略文件显示了默认限制。它应该被任何国家允许,包括那些有进口限制的国家,但请获得法律指导。
-
<java_home>/conf/security/limited/exempt_local.policy
// 一些国家对加密强度有进口限制,但如果使用豁免机制, // 可能会允许这些豁免。grant {// 如果执行密钥恢复,则对任何算法没有限制。permission javax.crypto.CryptoPermission *, "KeyRecovery"; // 如果执行密钥托管,则对任何算法没有限制。permission javax.crypto.CryptoPermission *, "KeyEscrow"; // 如果执行密钥削弱,则对任何算法没有限制。permission javax.crypto.CryptoPermission *, "KeyWeakening"; };
注意:有进口限制的国家应该使用“limited”,但如果可以采用豁免机制,则这些限制可能会放宽。请参阅“如何使应用程序免受加密限制”。请为您的情况获取法律指导。
4.4 自定义加密强度设置 Custom Cryptographic Strength Settings
要设置不同于有限或无限目录中策略文件的加密强度限制,您可以创建一个新的目录,并与有限和无限的目录平行,将您的策略文件放在那里。例如,您可以创建一个名为 custom 的目录。在这个 custom 目录中,您包括文件 default_export.policy 和/或 exempt_local.policy。
要选定 custom 目录中文件定义的加密强度,请在文件 <java_home>/conf/security/java.security 中设置 crypto.policy = custom。
五、Jurisdiction策略文件格式Jurisdiction Policy File Format
JCA 将其司法管辖区策略文件表示为具有相应权限声明的 Java 风格策略文件。如加密强度配置所述,Java 策略文件指定了来自指定代码源的代码所允许的权限。权限表示对系统资源的访问。在 JCA 的情况下,“资源”是加密算法,并且不需要指定代码源,因为加密限制适用于所有代码。
司法管辖区策略文件由一个非常基本的“授权条目”组成,其中包含一个或多个“权限条目”。
grant {<permission entries>;
};
权限条目在司法管辖区策略文件中的格式是:
permission <crypto permission class name>[<alg_name>[[, <exemption mechanism name>][, <maxKeySize>[, <AlgorithmParameterSpec class name>,<parameters for constructing an AlgorithmParameterSpec object>]]]];
一个包含将 AES 算法限制为最大密钥大小为 128 位的示例司法管辖区策略文件是:
grant {permission javax.crypto.CryptoPermission "AES", 128;// ...
};
权限条目必须以“permission”一词开始。出现在权限条目中的项目必须按指定顺序出现。条目以分号结束。标识符的大小写(grant, permission)不重要,但 或作为值传递的任何字符串的大小写很重要。星号(*)可以用作任何权限条目选项的通配符。例如,<alg_name> 选项的星号意味着“所有算法”。
以下是权限条目选项的描述:
Table 2-1 Permission Entry Options
Option | Description |
---|---|
crypto permission class name | 特定的权限类名称,如 javax.crypto.CryptoPermission。必选。 加密权限类反映了应用程序在特定环境中使用具有特定密钥大小的特定算法的能力。有两个加密权限类:CryptoPermission和CryptoAllPermission。特殊的CryptoAllPermission类意味着所有与加密相关的权限,也就是说,它指定不存在与加密有关的限制。 |
alg_name | 指定加密算法的标准名称的引用字符串,如 “AES” 或 “RSA”。可选。 |
exemption mechanism name | 表示豁免机制的引用字符串,如果执行,可以减少加密限制。可选。豁免机制名称可以包括 “KeyRecovery”、“KeyEscrow” 和 “KeyWeakening”。 |
maxKeySize | 指定允许使用的最大密钥大小(以位为单位)的整数。可选。 |
AlgorithmParameterSpec class name | 指定算法强度的类名称。可选。对于一些算法,仅指定密钥大小可能不足以表示算法强度。例如,在 “RC5” 算法的情况下,还必须考虑轮数。对于需要以多于密钥大小的方式表示强度的算法,请使用此选项指定 AlgorithmParameterSpec 类名称(例如 “RC5” 算法的 javax.crypto.spec.RC5ParameterSpec)。 |
parameters for constructing an AlgorithmParameterSpec object | 构造指定的 AlgorithmParameterSpec 对象的参数列表。如果已指定 并且需要参数,则此项必选。 |
六、避免程序密码限制 How to Make Applications Exempt from Cryptographic Restrictions
注意:大部分应用开发者应忽略本节。它仅适用于那些应用程序可能被出口到少数几个政府强制要求加密限制的国家,并且如果希望这些应用程序拥有比强制要求的更少的加密限制。
默认情况下,应用程序可以使用任何强度的加密算法。然而,由于少数几个国家的政府进口控制限制,您可能需要限制这些算法的强度。JCA 框架包括一种能力,即在不同的司法管辖区(地点)对应用程序可用的加密算法的最大强度进行限制。您在司法管辖区策略文件中指定这些限制。有关司法管辖区策略文件以及如何创建和配置它们的更多信息,请参见加密强度配置。
一些或所有这样的国家政府可能允许某些应用程序免受一些或全部加密限制。例如,他们可能考虑某些类型的应用程序为“特殊”的,因此免受限制。或者,他们可能会豁免任何使用“豁免机制”的应用程序,例如密钥恢复。被认为免受限制的应用程序可以获得比这些国家中非豁免应用程序允许的更强的加密。
为了使应用程序在运行时被识别为“免受限制”,它必须满足以下条件:
- 它必须有一个与它一起打包在 JAR 文件中的权限策略文件。权限策略文件指定了应用程序拥有的与加密相关的权限,以及(如果有的话)在什么条件下。
- 包含应用程序和权限策略文件的 JAR 文件必须使用在应用程序被接受为免受限制后颁发的代码签名证书进行签名。
以下是使应用程序免受某些加密限制所需的示例步骤。这是一个基本概述,包括 JCA 识别和处理应用程序为免受限制所需的信息。您需要知道您希望应用程序能够运行的特定国家或国家的豁免要求,这些国家的政府要求加密限制。您还需要知道有处理免受限制应用程序流程的 JCA 框架供应商的要求。请咨询此类供应商以获取更多信息。
- Write and Compile Your Application Code
- Create a Permission Policy File Granting Appropriate Cryptographic Permissions
- Prepare for Testing
- Apply for Government Approval From the Government Mandating Restrictions.
- Get a Code-Signing Certificate
- Bundle the Application and Permission Policy File into a JAR file
- Step 7.1: Get a Code-Signing Certificate
- Set Up Your Environment Like That of a User in a Restricted Country
- (only for applications using exemption mechanisms) Install a Provider Implementing the Exemption Mechanism Specified by the entry in the Permission Policy File
- Test Your Application
- Apply for U.S. Government Export Approval If Required
- Deploy Your Application
6.1 使用豁免机制的应用程序的特殊代码要求 Special Code Requirements for Applications that Use Exemption Mechanisms
当应用程序与它有关联的权限策略文件(在同一 JAR 文件中)且该权限策略文件指定了豁免机制时,调用 Cipher 的 getInstance 方法实例化 Cipher 时,JCA 代码会搜索已安装的供应商,以找到一个实现指定豁免机制的供应商。如果找到这样的供应商,JCA 会实例化一个与供应商实现相关的 ExemptionMechanism API 对象,然后将 ExemptionMechanism 对象与 getInstance 返回的 Cipher 关联。
实例化 Cipher 后,在通过调用 Cipher 的 init 方法初始化它之前,您的代码必须调用以下 Cipher 方法:
public ExemptionMechanism getExemptionMechanism()
此调用返回与 Cipher 关联的 ExemptionMechanism 对象。然后,您必须通过调用返回的 ExemptionMechanism 上的以下方法来初始化豁免机制实现:
public final void init(Key key)
您提供的参数应该是与您随后将提供给 Cipher init 方法的相同类型的参数。
一旦初始化了 ExemptionMechanism,您可以像通常一样继续初始化和使用 Cipher。
6.2 权限策略文件 Permission Policy Files
为了使应用程序在运行时被识别为免受一些或全部加密限制,它必须有一个与它一起打包在 JAR 文件中的权限策略文件。权限策略文件指定了应用程序拥有的与加密相关的权限,以及(如果有的话)在什么条件下。
伴随免受限制应用程序的权限策略文件中的权限条目格式与 JDK 附带下载的司法管辖区策略文件的格式相同,即:
permission <crypto permission class name>[<alg_name>[[, <exemption mechanism name>][, <maxKeySize>[, <AlgorithmParameterSpec class name>,<parameters for constructing an AlgorithmParameterSpec object>]]]];
见司法管辖区策略文件格式。
6.3 免受限制应用程序的权限策略文件 Permission Policy Files for Exempt Applications
一些应用程序可能被允许完全没有限制。因此,伴随此类应用程序的权限策略文件通常只需要包含以下内容:
grant {// 对任何算法没有限制。permission javax.crypto.CryptoAllPermission;
};
如果应用程序只使用单一算法(或几种特定算法),那么权限策略文件可以只明确提及那个算法(或算法),而不是授予 CryptoAllPermission。
例如,如果应用程序只使用 Blowfish 算法,权限策略文件不必授予所有算法的 CryptoAllPermission。它可以直接指定如果使用 Blowfish 算法,则没有加密限制。为了做到这一点,权限策略文件将如下所示:
grant {permission javax.crypto.CryptoPermission "Blowfish";
};
6.4 由于豁免机制而免受限制的应用程序的权限策略文件 Permission Policy Files for Applications Exempt Due to Exemption Mechanisms
如果应用程序被认为是“免受限制”的,如果执行了豁免机制,那么伴随应用程序的权限策略文件必须指定一个或多个豁免机制。在运行时,如果执行了这些豁免机制中的任何一个,应用程序将被认为是免受限制的。每个豁免机制必须在以下格式的权限条目中指定:
// 如果执行了指定的豁免机制,则没有算法限制。
permission javax.crypto.CryptoPermission *,"<ExemptionMechanismName>";
其中 <ExemptionMechanismName>
指定了豁免机制的名称。可能的豁免机制名称列表包括:
- KeyRecovery
- KeyEscrow
- KeyWeakening
例如,假设您的应用程序如果执行了密钥恢复或密钥托管,则被认为是免受限制的。那么您的权限策略文件应该包含以下内容:
grant {// 如果执行了密钥恢复,则没有算法限制。permission javax.crypto.CryptoPermission *, "KeyRecovery";// 如果执行了密钥托管,则没有算法限制。permission javax.crypto.CryptoPermission *, "KeyEscrow";
};
注意:指定豁免机制的权限条目不应同时指定最大密钥大小。实际允许的密钥大小是从安装的免受限制的司法管辖区策略文件中确定的,如下一节所述。
6.5 打包的权限策略文件如何影响加密权限 How Bundled Permission Policy Files Affect Cryptographic Permissions
在运行时,当应用程序实例化 Cipher(通过调用其 getInstance 方法)并且该应用程序有一个关联的权限策略文件时,JCA 会检查权限策略文件是否有适用于 getInstance 调用中指定算法的条目。如果有,并且该条目授予了 CryptoAllPermission 或没有指定必须执行豁免机制,这意味着对这个特定算法没有加密限制。
如果权限策略文件有适用于 getInstance 调用中指定算法的条目,并且该条目确实指定必须执行豁免机制,那么将检查免受限制的司法管辖区策略文件。如果免受限制的权限包括相关算法和豁免机制的条目,并且该条目由与应用程序捆绑的权限策略文件中的权限所暗示,并且如果有注册供应商提供了指定豁免机制的实现,那么 Cipher 的最大密钥大小和算法参数值就从免受限制的权限条目中确定。
如果与应用程序捆绑的权限策略文件中没有由相关条目暗示的免受限制的权限条目,或者没有任何注册供应商提供了指定豁免机制的实现,那么应用程序只允许使用标准默认的加密权限。
七、标准名称 Standard Names
标准名称文档包含了有关算法规范的信息。
Java 安全标准算法名称描述了 JDK 安全 API 所要求和使用的算法、证书和密钥库类型的标准名称。它还包含了更多关于算法规范的信息。特定服务提供者的信息可以在 JDK 服务提供者文档中找到。
JDK 中的密码学实现通过几个不同的服务提供者进行分发,主要是出于历史原因(Sun、SunJSSE、SunJCE、SunRsaSign)。请注意,这些服务提供者可能不是在所有 JDK 实现中都可用,因此,真正可移植的应用程序应该在不指定特定服务提供者的情况下调用 getInstance()。指定特定服务提供者的应用程序可能无法利用为底层操作系统环境(如 PKCS 或 Microsoft 的 CAPI)优化的本地服务提供者。
SunPKCS11 服务提供者本身不包含任何密码学算法,而是将请求引导到底层的 PKCS11 实现。请查阅 PKCS#11 参考指南和底层的 PKCS11 实现,以确定所需的算法是否可以通过 PKCS11 服务提供者获得。同样,在 Windows 系统上,SunMSCAPI 服务提供者不提供任何密码学功能,而是将请求路由到底层操作系统进行处理。
八、打包你的应用 Packaging Your Application
模块化应用程序的三种类型
- 命名模块(Named or explicit module):出现在模块路径上的模块,在module-info.class文件中包含模块配置信息。
- 自动模块(Automatic module):出现在模块路径上的模块,不包含module-info.class文件中的模块配置信息(本质上是一个"常规"的JAR文件)。
- 未命名模块(Unnamed module):出现在类路径上的模块。 可能有或没有module-info.class文件;如果有,该文件将被忽略。
建议将应用程序打包为命名模块,因为它们提供了更好的性能、更强的封装性和更简单的配置。它们还提供了更大的灵活性;即使在非模块化的JDK中,或者通过在模块化JDK的类路径中指定它们,也可以作为未命名模块使用。
有关模块的更多信息:参阅 The State of the Module System and JEP 261: Module System
九、Additional JCA Code Samples
9.1 计算摘要加密对象 Computing a MessageDigest Object
这些步骤描述了计算 MessageDigest 对象的过程。
1 创建 MessageDigest 对象,如下例所示:
MessageDigest sha = MessageDigest.getInstance("SHA-256");
这个调用为 sha
变量分配一个正确初始化的摘要对象。该实现实现了安全哈希算法(SHA-256),如国家标准与技术研究院(NIST)的 FIPS 180-4 文档中定义。
2 假设我们有三个字节数组 i1
、i2
和 i3
,它们构成了我们想要计算其消息摘要的总输入。这个摘要(或“哈希”)可以通过以下调用计算:
sha.update(i1);
sha.update(i2);
sha.update(i3);
byte[] hash = sha.digest();
3 可选:一个等效的替代调用序列将是:
sha.update(i1);
sha.update(i2);
byte[] hash = sha.digest(i3);
计算消息摘要后,消息摘要对象会自动重置并准备好接收新数据并计算其摘要。所有之前的状态(即,提供给 update
调用的数据)都会丢失。
9.1.1 示例 2-11 通过克隆实现哈希 Example 2-11 Hash Implementations Through Cloning
一些哈希实现可能支持通过克隆进行中间哈希。假设我们想要分别计算以下内容的哈希值:
i1
i1
和i2
i1
、i2
和i3
以下是计算这些哈希的一种方法;但是,这段代码只有在 SHA-256 实现是可克隆的情况下才有效:
/* 计算 i1 的哈希 */
sha.update(i1);
byte[] i1Hash = sha.clone().digest();/* 计算 i1 和 i2 的哈希 */
sha.update(i2);
byte[] i12Hash = sha.clone().digest();/* 计算 i1、i2 和 i3 的哈希 */
sha.update(i3);
byte[] i123hash = sha.digest();
9.1.2 示例 2-12 确定哈希实现是否可克隆Example 2-12 Determine if the Hash Implementation is Cloneable or not Cloneable
一些消息摘要的实现是可克隆的,其他的则不是。要确定是否可能克隆,请尝试克隆 MessageDigest 对象并捕获潜在的异常,如下所示:
try {// 尝试克隆它/* 计算 i1 的哈希 */sha.update(i1);byte[] i1Hash = sha.clone().digest();// ...byte[] i123hash = sha.digest();
} catch (CloneNotSupportedException cnse) {// 做些其他事情,例如在“如果哈希实现不可克隆则计算中间摘要”部分的代码
}
9.1.3 示例 2-13 如果哈希实现不可克隆则计算中间摘要Example 2-13 Compute Intermediate Digests if the Hash Implementation is not Cloneable
如果消息摘要不可克隆,计算中间摘要的另一种不太优雅的方式是创建多个摘要对象。在这种情况下,必须提前知道要计算的中间摘要的数量:
MessageDigest md1 = MessageDigest.getInstance("SHA-256");
MessageDigest md2 = MessageDigest.getInstance("SHA-256");
MessageDigest md3 = MessageDigest.getInstance("SHA-256");byte[] i1Hash = md1.digest(i1);md2.update(i1);
byte[] i12Hash = md2.digest(i2);md3.update(i1);
md3.update(i2);
byte[] i123Hash = md3.digest(i3);
9.2 产生秘钥对 Generating a Pair of Keys
在这个例子中,我们将为名为 “DSA”(数字签名算法)的算法生成一对公私钥,并将这对密钥对用于后续的例子中。我们将生成一个 2048 位的模数的密钥。我们不关心哪个提供者提供了算法实现。
9.2.1 创建密钥对生成器 Creating the Key Pair Generator
第一步是获取一个用于生成 DSA 算法密钥的密钥对生成器对象:
KeyPairGenerator keyGen = KeyPairGenerator.getInstance("DSA");
9.2.2 初始化密钥对生成器 Initializing the Key Pair Generator
下一步是初始化密钥对生成器。在大多数情况下,算法独立的初始化就足够了,但在某些情况下,使用算法特定的初始化。
9.2.2.1 算法独立初始化 Algorithm-Independent Initialization
所有的密钥对生成器都共享密钥大小和随机源的概念。KeyPairGenerator
类的初始化方法至少需要一个密钥大小。如果未明确提供随机源,将使用最高优先级已安装提供者的 SecureRandom
实现。因此,要生成 2048 位密钥大小的密钥,只需调用:
keyGen.initialize(2048);
以下代码演示了如何使用特定的、另外种子化的 SecureRandom
对象:
SecureRandom random = SecureRandom.getInstance("DRBG", "SUN");
random.setSeed(userSeed);
keyGen.initialize(2048, random);
由于在调用这些算法独立初始化方法时没有指定其他参数,因此提供者将决定如何处理与每个密钥关联的算法特定参数(如果有)。提供者可能使用预计算的参数值,或者可能生成新值。
9.2.2.2 算法特定初始化 Algorithm-Specific Initialization
对于已经存在一组算法特定参数的情况(例如 DSA 中的“社区参数”),有两个初始化方法具有 AlgorithmParameterSpec
参数。假设您的密钥对生成器是针对 “DSA” 算法的,并且您有一组 DSA 特定参数 p、q 和 g,您希望使用这些参数来生成密钥对。您可以执行以下代码来初始化您的密钥对生成器(回想一下 DSAParameterSpec
是一个 AlgorithmParameterSpec
):
DSAParameterSpec dsaSpec = new DSAParameterSpec(p, q, g);
keyGen.initialize(dsaSpec);
9.2.3 生成密钥对 Generating the Pair of Keys
最后一步是实际生成密钥对。无论使用哪种类型的初始化(算法独立或算法特定),都使用相同的代码来生成 KeyPair
:
KeyPair pair = keyGen.generateKeyPair();
9.3 使用生成的密钥生成和验证签名 Generating and Verifying a Signature Using Generated Keys
使用生成的密钥生成和验证签名的例子。
以下签名生成和验证的例子使用了在生成密钥对中生成的 KeyPair
。
9.3.1 生成签名 Generating a Signature
我们首先创建一个 Signature
类对象:
Signature dsa = Signature.getInstance("SHA256withDSA");
接下来,使用在密钥对示例中生成的密钥对,我们使用私钥初始化该对象,然后对一个名为 data
的字节数组进行签名。
/* 使用私钥初始化对象 */
PrivateKey priv = pair.getPrivate();
dsa.initSign(priv);/* 更新并签名数据 */
dsa.update(data);
byte[] sig = dsa.sign();
9.3.2 验证签名 Verifying a Signature
验证签名很直接。(注意,在这里我们也使用在密钥对示例中生成的密钥对。)
/* 使用公钥初始化对象 */
PublicKey pub = pair.getPublic();
dsa.initVerify(pub);/* 更新并验证数据 */
dsa.update(data);
boolean verifies = dsa.verify(sig);
System.out.println("signature verifies: " + verifies);
9.4 使用密钥规范和KeyFactory生成/验证签名 Generating/Verifying Signatures Using Key Specifications and KeyFactory
假设您拥有 DSA 私钥的组成部分:x(私钥)、p(素数)、q(次素数)和 g(基),而不是拥有一对公私钥(如在生成密钥对部分生成的那样)。
此外,假设您想使用私钥对一些数据进行数字签名,这些数据存储在名为 someData
的字节数组中。您将执行以下步骤,这些步骤还说明了如何创建密钥规范,并使用密钥工厂从密钥规范获取 PrivateKey
(initSign
需要一个 PrivateKey
):
DSAPrivateKeySpec dsaPrivKeySpec = new DSAPrivateKeySpec(x, p, q, g);KeyFactory keyFactory = KeyFactory.getInstance("DSA");
PrivateKey privKey = keyFactory.generatePrivate(dsaPrivKeySpec);Signature sig = Signature.getInstance("SHA256withDSA");
sig.initSign(privKey);
sig.update(someData);
byte[] signature = sig.sign();
假设 Alice 想使用您签名的数据。为了让她能够这样做并验证您的签名,您需要给她发送三样东西:
- 数据
- 签名
- 您用来签名数据的私钥对应的公钥
您可以将 someData
字节存储在一个文件中,将签名字节存储在另一个文件中,并将这些文件发送给 Alice。
对于公钥,假设如前一个签名示例中一样,您拥有与用来签名数据的 DSA 私钥对应的 DSA 公钥的组成部分。然后您可以从这些组成部分创建 DSAPublicKeySpec
:
DSAPublicKeySpec dsaPubKeySpec = new DSAPublicKeySpec(y, p, q, g);
您仍然需要提取密钥字节,以便将它们放入文件中。为此,您可以首先调用前面示例中已经创建的 DSA 密钥工厂的 generatePublic
方法:
PublicKey pubKey = keyFactory.generatePublic(dsaPubKeySpec);
然后,您可以通过以下方式提取(编码的)密钥字节:
byte[] encKey = pubKey.getEncoded();
现在,您可以将这些字节存储在文件中,并与包含数据和签名的文件一起发送给 Alice。
现在,假设 Alice 已经收到了这些文件,并且她将数据文件中的数据字节复制到名为 data
的字节数组中,将签名文件中的签名字节复制到名为 signature
的字节数组中,将公钥文件中的编码公钥字节复制到名为 encodedPubKey
的字节数组中。
Alice 现在可以执行以下代码来验证签名。该代码还说明了如何使用密钥工厂从其编码实例化 DSA 公钥(initVerify
需要一个 PublicKey
)。
X509EncodedKeySpec pubKeySpec = new X509EncodedKeySpec(encodedPubKey);KeyFactory keyFactory = KeyFactory.getInstance("DSA");
PublicKey pubKey = keyFactory.generatePublic(pubKeySpec);Signature sig = Signature.getInstance("SHA256withDSA");
sig.initVerify(pubKey);
sig.update(data);
sig.verify(signature);
注意:在前一个示例中,Alice 需要从编码的密钥位生成 PublicKey
,因为 initVerify
需要一个 PublicKey
。一旦她有了 PublicKey
,她还可以使用 KeyFactory
的 getKeySpec
方法将其转换为 DSAPublicKeySpec
,以便在需要时访问组成部分,如下所示:
DSAPublicKeySpec dsaPubKeySpec =(DSAPublicKeySpec)keyFactory.getKeySpec(pubKey, DSAPublicKeySpec.class);
现在,她可以通过 DSAPublicKeySpec
类上的相应 “get” 方法(getY
、getP
、getQ
和 getG
)访问 DSA 公钥组成部分 y、p、q 和 g。
9.5 生成随机数 Generating Random Numbers
以下是使用 SecureRandom 类的 DRBG 实现,以不同安全强度配置生成随机数的示例代码:
SecureRandom drbg;
byte[] buffer = new byte[32];// 可以提供任何 DRBG
drbg = SecureRandom.getInstance("DRBG");
drbg.nextBytes(buffer);SecureRandomParameters params = drbg.getParameters();
if (params instanceof DrbgParameters.Instantiation) {DrbgParameters.Instantiation ins = (DrbgParameters.Instantiation) params;if (ins.getCapability().supportsReseeding()) {drbg.reseed();}
}// 以下调用请求一个弱 DRBG 实例。它只保证支持 112 位的安全强度。
drbg = SecureRandom.getInstance("DRBG",DrbgParameters.instantiation(112, NONE, null));// 接下来的两个调用很可能会失败,因为 drbg 可能是用没有预测抵抗支持的较小强度实例化的。
drbg.nextBytes(buffer,DrbgParameters.nextBytes(256, false, "more".getBytes()));
drbg.nextBytes(buffer,DrbgParameters.nextBytes(112, true, "more".getBytes()));// 以下调用请求一个强 DRBG 实例,带有一个个性化字符串。如果成功返回实例,
// 那么该实例保证支持 256 位的安全强度并提供预测抵抗。
drbg = SecureRandom.getInstance("DRBG", DrbgParameters.instantiation(256, PR_AND_RESEED, "hello".getBytes()));// 这个单独的调用没有请求预测抵抗,但是使用了额外的输入。
drbg.nextBytes(buffer,DrbgParameters.nextBytes(-1, false, "more".getBytes()));// 这个调用也是如此。
drbg.reseed(DrbgParameters.reseed(false, "extra".getBytes()));
9.6 确定两个密码是否相等 Determining If Two Keys Are Equal
确定两个密钥是否相等的示例代码。
在许多情况下,您可能想知道两个密钥是否相等;然而,默认的 java.lang.Object.equals
方法可能无法给出期望的结果。最不依赖于提供者的方法是比较编码后的密钥。如果这种比较不适当(例如,当比较 RSAPrivateKey
和 RSAPrivateCrtKey
),则应该比较每个组成部分。
以下代码演示了这个想法:
static boolean keysEqual(Key key1, Key key2) {if (key1.equals(key2)) {return true;}if (Arrays.equals(key1.getEncoded(), key2.getEncoded())) {return true;}// 更多关于不同类型密钥的代码在这里。// 例如,以下代码可以检查// RSAPrivateKey 和 RSAPrivateCrtKey 是否相等:// if ((key1 instanceof RSAPrivateKey) &&// (key2 instanceof RSAPrivateKey)) {// if ((key1.getModulus().equals(key2.getModulus())) &&// (key1.getPrivateExponent().equals(// key2.getPrivateExponent()))) {// return true;// }// }return false;
}
9.7 读取Base64编码的证书 Reading Base64-Encoded Certificates
以下示例读取包含 Base64 编码证书的文件,每个证书开头都以
-----BEGIN CERTIFICATE-----
结尾都以
-----END CERTIFICATE-----
我们把 FileInputStream
(它不支持标记和重置)转换为 ByteArrayInputStream
(它支持这些方法),这样每次调用 generateCertificate
只消耗一个证书,并且将输入流的读取位置定位到文件中的下一个证书:
try (FileInputStream fis = new FileInputStream(filename);BufferedInputStream bis = new BufferedInputStream(fis)) {CertificateFactory cf = CertificateFactory.getInstance("X.509");while (bis.available() > 0) {Certificate cert = cf.generateCertificate(bis); System.out.println(cert.toString());}
}
9.8 分析证书回应 Parsing a Certificate Reply
解析证书回复的示例。
以下示例解析存储在文件中的 PKCS7 格式的证书回复,并提取出所有的证书:
try (FileInputStream fis = new FileInputStream(filename)) {CertificateFactory cf = CertificateFactory.getInstance("X.509");Collection<? extends Certificate> c = cf.generateCertificates(fis);for (Certificate cert : c) {System.out.println(cert);}// 或者,使用这个聚合操作代替 for 循环:// c.stream().forEach(e -> System.out.println(e));
}
9.9 使用加密 Using Encryption
本节引导用户完成生成密钥、创建和初始化密码对象、加密文件,然后解密的过程。在整个示例中,我们使用高级加密标准(AES)。
9.9.1 生成密钥 Generating a Key
要创建 AES 密钥,我们必须实例化 AES 的 KeyGenerator
。我们没有指定提供者,因为我们不关心特定的 AES 密钥生成实现。由于我们没有初始化 KeyGenerator
,系统将使用提供的随机源和默认密钥大小来创建 AES 密钥:
KeyGenerator keygen = KeyGenerator.getInstance("AES");
keygen.init(128);
SecretKey aesKey = keygen.generateKey();
生成密钥后,可以重复使用相同的 KeyGenerator
对象来创建更多的密钥。
9.9.2生成密钥 创建密码对象 Creating a Cipher
下一步是创建 Cipher
实例。我们使用 Cipher 类的 getInstance
工厂方法之一来实现。我们必须指定请求转换的名称,该名称包括以下组成部分,用斜杠 (/) 分隔:
- 算法名称
- 模式(可选)
- 填充方案(可选)
在这个示例中,我们创建了一个使用密码块链接模式和 PKCS5 风格的填充的 AES 密码对象。我们没有指定提供者,因为我们不关心请求转换的特定实现。
AES 的标准算法名称是 “AES”,密码块链接模式的标准名称是 “CBC”,PKCS5 风格填充的标准名称是 “PKCS5Padding”:
Cipher aesCipher;// 创建密码对象
aesCipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
我们使用之前生成的 aesKey
来初始化密码对象进行加密:
// 初始化密码对象进行加密
aesCipher.init(Cipher.ENCRYPT_MODE, aesKey);// 我们的明文
byte[] cleartext = "This is just an example".getBytes();// 加密明文
byte[] ciphertext = aesCipher.doFinal(cleartext);// 检索加密期间使用的参数,以便正确地
// 初始化密码对象进行解密
AlgorithmParameters params = aesCipher.getParameters();// 用相同的密码对象进行解密
aesCipher.init(Cipher.DECRYPT_MODE, aesKey, params);// 解密密文
byte[] cleartext1 = aesCipher.doFinal(ciphertext);
cleartext
和 cleartext1
是相同的。
9.10 使用基于密码的加密 Using Password-Based Encryption
在这个示例中,我们提示用户输入一个密码,从中我们派生出一个加密密钥。
看起来,收集并存储密码在 java.lang.String
类型的对象中似乎是合乎逻辑的。然而,这里有一个问题:String 类型的对象是不可变的,即,没有定义的方法允许您在使用后更改(覆盖)或清零 String 的内容。这个特性使得 String 对象不适合存储诸如用户密码这样的安全敏感信息。您应该始终使用 char 数组来收集和存储安全敏感信息。出于这个原因,javax.crypto.spec.PBEKeySpec
类以 char 数组的形式获取(和返回)密码。
为了使用 PKCS5 中定义的基于密码的加密(PBE),我们必须指定一个盐值和一个迭代计数。用于加密的相同的盐值和迭代计数必须用于解密。较新的 PBE 算法至少使用 1000 的迭代计数。
PBEKeySpec pbeKeySpec;
PBEParameterSpec pbeParamSpec;
SecretKeyFactory keyFac;// 盐
byte[] salt = new SecureRandom().nextBytes(16); // 盐通常是16字节// 迭代计数
int count = 1000;// 创建 PBE 参数集
pbeParamSpec = new PBEParameterSpec(salt, count);// 提示用户输入加密密码。
// 将用户密码作为 char 数组收集,并转换
// 成 SecretKey 对象,使用 PBE 密钥
// 工厂。
char[] password = System.console().readPassword("Enter encryption password: ");
pbeKeySpec = new PBEKeySpec(password);
keyFac = SecretKeyFactory.getInstance("PBEWithHmacSHA256AndAES_256");
SecretKey pbeKey = keyFac.generateSecret(pbeKeySpec);// 创建 PBE 密码对象
Cipher pbeCipher = Cipher.getInstance("PBEWithHmacSHA256AndAES_256");// Initialize PBE Cipher with key and parameterspbeCipher.init(Cipher.ENCRYPT_MODE, pbeKey, pbeParamSpec);// Our cleartextbyte[] cleartext = "This is another example".getBytes();// Encrypt the cleartextbyte[] ciphertext = pbeCipher.doFinal(cleartext);
9.11 封装和解密密钥 Encapsulating and Decapsulating Keys
有关密钥封装和解封装的更多信息,请参阅KEM类。
// Receiver sidevar kpg = KeyPairGenerator.getInstance("X25519");var kp = kpg.generateKeyPair();// Sender sidevar kem1 = KEM.getInstance("DHKEM");var sender = kem1.newEncapsulator(kp.getPublic());var encapsulated = sender.encapsulate();var k1 = encapsulated.key();// Receiver sidevar kem2 = KEM.getInstance("DHKEM");var receiver = kem2.newDecapsulator(kp.getPrivate());var k2 = receiver.decapsulate(encapsulated.encapsulation());assert Arrays.equals(k1.getEncoded(), k2.getEncoded());
十、Sample Programs for Diffie-Hellman Key Exchange, AES/GCM, and HMAC-SHA256
10.1 Diffie-Hellman 两方密钥交换 Diffie-Hellman Key Exchange between Two Parties
该程序运行了 Diffie-Hellman 密钥协商协议,由两个参与者完成。
import java.io.*;
import java.math.BigInteger;
import java.security.*;
import java.security.spec.*;
import java.security.interfaces.*;
import javax.crypto.*;
import javax.crypto.spec.*;
import javax.crypto.interfaces.*;
// ...public class DHKeyAgreement2 {// ...public static void main(String[] args) throws Exception {// 爱丽丝创建她自己的 DH 密钥对,密钥长度为 2048 位System.out.println("ALICE: 生成 DH 密钥对 ...");KeyPairGenerator aliceKpairGen = KeyPairGenerator.getInstance("DH");aliceKpairGen.initialize(2048);KeyPair aliceKpair = aliceKpairGen.generateKeyPair();// 爱丽丝创建并初始化她的 DH 密钥协商对象System.out.println("ALICE: 初始化 ...");KeyAgreement aliceKeyAgree = KeyAgreement.getInstance("DH");aliceKeyAgree.init(aliceKpair.getPrivate());// 爱丽丝对她的公钥进行编码,并将其发送给鲍勃byte[] alicePubKeyEnc = aliceKpair.getPublic().getEncoded();// 转向鲍勃。鲍勃已经接收到爱丽丝的公钥// 以编码格式。// 他从编码的密钥材料实例化一个 DH 公钥。// ...}// ...
}
10.2 Diffie-Hellman 三方密钥交换 Diffie-Hellman Key Exchange between Three Parties
该程序运行了 Diffie-Hellman 密钥协商协议,由三个参与者完成:爱丽丝、鲍勃和卡罗尔。
import java.security.*;
import java.security.spec.*;
import javax.crypto.*;
import javax.crypto.spec.*;
import javax.crypto.interfaces.*;
// ...public class DHKeyAgreement3 {// ...public static void main(String[] args) throws Exception {// 爱丽丝创建她自己的 DH 密钥对,密钥长度为 2048 位System.out.println("ALICE: 生成 DH 密钥对 ...");KeyPairGenerator aliceKpairGen = KeyPairGenerator.getInstance("DH");aliceKpairGen.initialize(2048);KeyPair aliceKpair = aliceKpairGen.generateKeyPair();// 这些 DH 参数也可以通过创建一个// DHParameterSpec 对象来使用约定的值DHParameterSpec dhParamShared = ((DHPublicKey)aliceKpair.getPublic()).getParams();// 鲍勃使用相同的参数创建他自己的 DH 密钥对System.out.println("BOB: 生成 DH 密钥对 ...");KeyPairGenerator bobKpairGen = KeyPairGenerator.getInstance("DH");bobKpairGen.initialize(dhParamShared);KeyPair bobKpair = bobKpairGen.generateKeyPair();// 卡罗尔使用相同的参数创建她自己的 DH 密钥对System.out.println("CAROL: 生成 DH 密钥对 ...");KeyPairGenerator carolKpairGen = KeyPairGenerator.getInstance("DH");carolKpairGen.initialize(dhParamShared);KeyPair carolKpair = carolKpairGen.generateKeyPair();// 爱丽丝初始化System.out.println("ALICE: 初始化 ...");KeyAgreement aliceKeyAgree = KeyAgreement.getInstance("DH");aliceKeyAgree.init(aliceKpair.getPrivate());// 鲍勃初始化System.out.println("BOB: 初始化 ...");KeyAgreement bobKeyAgree = KeyAgreement.getInstance("DH");bobKeyAgree.init(bobKpair.getPrivate());// 卡罗尔初始化System.out.println("CAROL: 初始化 ...");KeyAgreement carolKeyAgree = KeyAgreement.getInstance("DH");carolKeyAgree.init(carolKpair.getPrivate());// 爱丽丝使用卡罗尔的公钥Key ac = aliceKeyAgree.doPhase(carolKpair.getPublic(), false);// 鲍勃使用爱丽丝的公钥Key ba = bobKeyAgree.doPhase(aliceKpair.getPublic(), false);// 卡罗尔使用鲍勃的公钥Key cb = carolKeyAgree.doPhase(bobKpair.getPublic(), false);// 爱丽丝使用卡罗尔的结果,cbaliceKeyAgree.doPhase(cb, true);// 鲍勃使用爱丽丝的结果,acbobKeyAgree.doPhase(ac, true);// 卡罗尔使用鲍勃的结果,bacarolKeyAgree.doPhase(ba, true);// 爱丽丝、鲍勃和卡罗尔计算他们的秘密byte[] aliceSharedSecret = aliceKeyAgree.generateSecret();System.out.println("爱丽丝的秘密: " + toHexString(aliceSharedSecret));byte[] bobSharedSecret = bobKeyAgree.generateSecret();System.out.println("鲍勃的秘密: " + toHexString(bobSharedSecret));byte[] carolSharedSecret = carolKeyAgree.generateSecret();System.out.println("卡罗尔的秘密: " + toHexString(carolSharedSecret));// 比较爱丽丝和鲍勃if (!java.util.Arrays.equals(aliceSharedSecret, bobSharedSecret))throw new Exception("爱丽丝和鲍勃不同");System.out.println("爱丽丝和鲍勃相同");// 比较鲍勃和卡罗尔if (!java.util.Arrays.equals(bobSharedSecret, carolSharedSecret))throw new Exception("鲍勃和卡罗尔不同");System.out.println("鲍勃和卡罗尔相同");}// ...
}
10.3 AES/GCM Example
以下是一个示例程序,演示了如何使用 AES/GCM 对数据进行加密/解密。
import java.security.AlgorithmParameters;
import java.util.Arrays;
import javax.crypto.*;public class AESGCMTest {public static void main(String[] args) throws Exception {// 略长于 1 个 AES 块(128 位)以展示 GCM "处理" PADDING。byte[] data = {// 数据字节};// 创建一个 128 位的 AES 密钥。KeyGenerator kg = KeyGenerator.getInstance("AES");kg.init(128);SecretKey key = kg.generateKey();// 获取一个用于加密的 AES/GCM 密码对象。必须获取并使用参数以成功解密。Cipher encCipher = Cipher.getInstance("AES/GCM/NOPADDING");encCipher.init(Cipher.ENCRYPT_MODE, key);byte[] enc = encCipher.doFinal(data);AlgorithmParameters ap = encCipher.getParameters();// 获取一个类似的密码对象,并使用参数。Cipher decCipher = Cipher.getInstance("AES/GCM/NOPADDING");decCipher.init(Cipher.DECRYPT_MODE, key, ap);byte[] dec = decCipher.doFinal(enc);if (Arrays.compare(data, dec) != 0) {throw new Exception("原始数据 != 解密后的数据");}}
}
10.4 HMAC-SHA256 Example
以下是一个示例程序,演示了如何为 HMAC-SHA256 生成一个密钥对象,并用它来初始化 HMAC-SHA256 对象。
import java.security.*;
import javax.crypto.*;public class initMac {public static void main(String[] args) throws Exception {// 为 HmacSHA256 生成密钥KeyGenerator kg = KeyGenerator.getInstance("HmacSHA256");SecretKey sk = kg.generateKey();// 获取实现 HmacSHA256 的 Mac 对象实例,并用密钥 sk 初始化它Mac mac = Mac.getInstance("HmacSHA256");mac.init(sk);byte[] result = mac.doFinal("Hi There".getBytes());}
}