Java平台强调安全性,包括语言安全,密码学,公钥基础设施,认证,安全通信和访问控制。
JCA是平台的一个主要部分,包含一个“Provider”体系结构和一组用于数字签名,消息摘要(哈希),证书和证书验证,加密(对称/非对称块/流密码),密钥生成 管理和安全随机数生成等等。 这些API允许开发人员将安全性轻松集成到应用程序代码中。 这个架构是围绕以下原则设计的:
JDK中提供的其他密码通信库使用JCA提供程序体系结构,但在别处进行了介绍。 Java安全套接字扩展(JSSE)提供对安全套接字层(SSL)和传输层安全性(TLS)实现的访问。 Java通用安全服务(JGSS)(通过Kerberos)API以及简单身份验证和安全层(SASL)也可用于在通信应用程序之间安全地交换消息。
无论何时提及特定的JCA提供者,都将由提供者的名称明确提及。
JCA是围绕这些原则设计的:
实现独立性和算法独立性是互补的; 您可以使用加密服务(如数字签名和消息摘要),而无需担心实现细节,甚至是构成这些概念基础的算法。 尽管完全的算法独立性是不可能的,但JCA提供了标准化的,算法特定的API。 当实现独立性不可取时,JCA可以让开发人员指出具体的实现。
通过定义密码“engines”(服务)的类型,并定义提供这些密码引擎的功能的类来实现算法独立性。 这些类被称为引擎类,例如MessageDigest,Signature,KeyFactory,KeyPairGenerator和Cipher类。
实现独立性是使用基于“Provider”的体系结构实现的。 术语 Cryptographic Service Provider (CSP)(在本文档中与“Provider”可互换使用)是指实现一个或多个密码服务(如数字签名算法,消息摘要算法和密钥转换服务)的包或一组包。 程序可以简单地请求实现特定服务(例如DSA签名算法)的特定类型的对象(例如签名对象),并从一个安装的提供者获得实现。 如果需要的话,程序可以改为请求来自特定提供者的实现。 提供商可能会更新透明的应用程序,例如,当一个更快或更安全的版本可用的时候。
实现互操作性意味着各种实现可以相互协作,使用彼此的密钥,或者验证彼此的签名。 这就意味着,对于相同的算法,由一个提供者生成的密钥可以被另一个提供者使用,并且由一个提供者生成的签名可以被另一个提供者验证。
算法可扩展性意味着可以容易地添加适合于所支持的引擎类之一的新算法。
java.security.Provider是所有安全提供程序的基类。 每个CSP都包含这个类的一个实例,它包含了提供者的名字,并列出了它实现的所有安全服务/算法。 当需要特定算法的实例时,JCA框架会咨询提供者的数据库,如果找到合适的匹配项,则创建该实例。
Provider包含一个包(或一组包),为声明的加密算法提供具体的实现。 每个JDK安装都默认安装并配置了一个或多个提供程序。 其他提供者可以静态或动态添加(参见Provider和Security类)。 客户端可以配置其运行时环境来指定提供程序的首选顺序。 首选顺序是在没有请求特定提供者时提供者搜索请求的服务的顺序。
要使用JCA,应用程序只需要请求特定类型的对象(如MessageDigest)和特定的算法或服务(如“SHA-256”算法),并从一个已安装的提供者获取实现。 或者,程序可以请求来自特定提供者的对象。 每个提供者都有一个名字来引用它。
md = MessageDigest.getInstance("SHA-256"); md = MessageDigest.getInstance("SHA-256", "ProviderC");
下图说明了请求“SHA-256”消息摘要实现。 这些图显示了实现各种消息摘要算法(“SHA-256”,“SHA-384”和“SHA-512”)的三个不同的提供者。 供应商按照优先顺序从左至右排列(1-3)。 在第一个示例中,应用程序请求SHA-256算法实现而不指定提供者名称。 提供程序按优先顺序搜索,并返回提供该特定算法ProviderB的第一个提供程序的实现。 在第二个图中,应用程序请求来自特定提供者ProviderC的SHA-256算法实现。 这次ProviderC的实现被返回,即使具有更高优先级的提供者ProviderB也提供SHA-256实现。
图1 自动搜索Provider
图2 指定特定的Provider
JDK中的加密实现主要是出于历史原因通过几个不同的提供者(Sun,SunJSSE,SunJCE,SunRsaSign)分发的,但在较小的程度上由它们提供的功能和算法的类型来分发。 其他Java运行时环境可能不一定包含这些Sun提供程序,因此除非知道特定的提供程序可用,否则应用程序不应请求提供程序特定的实现。
JCA提供了一组API,允许用户查询安装哪些提供程序以及支持哪些服务。
此架构还使最终用户可以轻松添加其他提供商。 许多第三方提供者实现已经可用。 有关如何编写,安装和注册提供程序的更多信息,请参阅Provider类。
如前所述,通过定义所有应用程序用于访问服务类型的通用高级应用程序编程接口(API)来实现算法独立性。 实现独立性是通过让所有提供者实现符合定义良好的接口来实现的。 引擎类的实例因此被具有相同方法签名的实现类“支持”。 应用程序调用通过引擎类路由,并传递到底层的后台实现。 该实现处理请求并返回正确的结果。
每个引擎类中的应用程序API方法都通过实现相应服务提供程序接口(SPI)的类路由到提供程序的实现。 也就是说,对于每个引擎类,都有一个对应的抽象SPI类,它定义了每个加密服务提供者算法必须实现的方法。 每个SPI类的名称与相应的引擎类相同,接着是Spi。 例如,Signature类提供对数字签名算法的功能的访问。 实际的提供者实现是在SignatureSpi的子类中提供的。 应用程序调用引擎类的API方法,在实际的实现中又调用SPI方法。
每个SPI类都是抽象的。 为了为特定算法提供特定类型的服务,提供者必须继承相应的SPI类,并提供所有抽象方法的实现。
对于API中的每个引擎类,通过调用引擎类中的getInstance()工厂方法来请求和实例化实现实例。 工厂方法是一个返回一个类的实例的静态方法。 引擎类使用上述框架Provider选择机制来获取实际的后台实现(SPI),然后创建实际的引擎对象。 引擎类的每个实例都封装(作为专用字段)相应SPI类的实例,称为SPI对象。 API对象的所有API方法都声明为final,并且它们的实现调用封装SPI对象的相应SPI方法。
为了使这个更清楚,请查看下面的代码和插图:
import javax.crypto.*; Cipher c = Cipher.getInstance("AES"); c.init(ENCRYPT_MODE, key);
这里的应用程序需要一个“AES”算法的javax.crypto.Cipher实例,并不关心使用哪个提供者。应用程序调用Cipher引擎类的getInstance()工厂方法,然后请求JCA框架查找支持“AES”的第一个提供程序实例。该框架会咨询每个已安装的提供者,并获取提供者类的提供者实例。 (回想一下,Provider类是可用算法的数据库。)框架搜索每个提供者,最终在CSP3中找到合适的条目。这个数据库入口指向扩展CipherSpi的实现类com.foo.AESCipher,因此适用于Cipher引擎类。创建一个com.foo.AESCipher的实例,并将其封装在一个新创建的javax.crypto.Cipher实例中,该实例返回给应用程序。当应用程序现在对Cipher实例执行init()操作时,Cipher引擎类将请求路由到com.foo.AESCipher类中相应的engineInit()支持方法中。
附录A列出了为Java环境定义的标准名称。 其他第三方提供商可以定义他们自己的这些服务的实现,甚至是额外的服务。
称为“KeyStore”的数据库可用于管理密钥和证书的存储库。 密钥库可用于需要用于身份验证,加密或签名的数据的应用程序。
应用程序可以通过java.security包中的KeyStore类的实现来访问密钥库。 建议的密钥库类型(格式)是“pkcs12”,它基于RSA PKCS12个人信息交换语法标准。 默认的密钥库类型是“jks”,这是一种专有格式。 其他密钥库格式也是可用的,例如作为替代专有密钥库格式的“jceks”,以及基于RSA PKCS11标准的“pkcs11”,并且支持对硬件安全模块和智能卡等加密令牌的访问。
应用程序可以使用上述相同的提供程序机制,从不同的提供程序中选择不同的密钥库实现。
请参阅密钥管理部分了解更多信息。
本节介绍主要的JCA API。
引擎类为特定类型的密码服务提供接口,而不依赖于特定的密码算法或提供者。 引擎需要提供:
以下引擎类是可用的:
注意:生成器可以创建具有全新内容的对象,而工厂只能从现有材料(例如编码)中创建对象。
本节讨论JCA中提供的核心类和接口:
注:有关CertPathBuilder,CertPathValidator和CertStore引擎类的更多信息,请参阅“Java PKI编程指南”。
本指南将首先介绍最有用的高级类(Provider,Security,SecureRandom,MessageDigest,Signature,Cipher和Mac),然后研究各种支持类。 现在,简单地说,密钥(公钥,私钥和加密)由各种JCA类生成和表示,并被高级类用作其操作的一部分。
本部分显示每个类和接口中主要方法的签名。 其中一些类(MessageDigest,Signature,KeyPairGenerator,SecureRandom,KeyFactory和密钥规范类)的示例在相应的示例部分提供。
相关安全API软件包的完整参考文档可以在软件包摘要中找到:
术语“加密服务提供者”(在本文档中与“提供者”可互换使用)是指提供JDK安全API加密特征子集的具体实现的一个或一组包。 Provider类是这种包或一组包的接口。 它具有访问提供程序名称,版本号和其他信息的方法。 请注意,除了注册加密服务的实现外,Provider类还可以用于注册可能被定义为JDK安全API或其扩展之一的其他安全服务的实现。
为了提供密码服务的实现,一个实体(例如开发组)编写实现代码并创建Provider类的子类。 Provider子类的构造函数设置各种属性的值; JDK安全API使用这些值来查找提供者实现的服务。 换句话说,子类指定实现服务的类的名称。
这里有几种类型的服务可以通过提供者包实现; 欲了解更多信息,请参阅Engine Classes and Algorithms
不同的实现可能具有不同的特性。 有些可能是基于软件的,有些可能是基于硬件的。 有些可能是平台无关的,有些可能是平台特定的。 一些供应商的源代码可能可用于审查和评估,而有些则可能不可用。 JCA让最终用户和开发者决定他们的需求。
在本节中,我们解释最终用户如何安装符合他们需求的加密实现,以及开发人员如何请求适合他们的实现。
注意:有关实现Provider的更多信息,请参阅指南How To Implement a Provider for the Java Cryptography Architecture
对于API中的每个引擎类,通过调用引擎类的getInstance方法之一来请求和实例化实现实例,指定想要实现的所需算法的名称以及可选的提供者(或提供者类)的名称。
static EngineClassName getInstance(String algorithm) throws NoSuchAlgorithmException static EngineClassName getInstance(String algorithm, String provider) throws NoSuchAlgorithmException, NoSuchProviderException static EngineClassName getInstance(String algorithm, Provider provider) throws NoSuchAlgorithmException
其中EngineClassName是所需的引擎类型(MessageDigest / Cipher / etc)。 例如:
MessageDigest md = MessageDigest.getInstance("SHA-256"); KeyAgreement ka = KeyAgreement.getInstance("DH", "SunJCE");
分别返回“SHA-256”MessageDigest和“DH”KeyAgreement对象的实例。
附录A包含已经标准化的用于Java环境的名称列表。 有些提供商可能会选择也包含也指向相同算法的别名。 例如,“SHA256”算法可能被称为“SHA-256”。 应用程序应该使用标准名称而不是别名,因为不是所有的提供者都可以用同样的方法来命名算法名称。
注意:算法名称不区分大小写。 例如,以下所有调用都是等同的:
MessageDigest.getInstance("SHA256") MessageDigest.getInstance("sha256") MessageDigest.getInstance("sHa256")
如果没有指定提供程序,getInstance将搜索已注册的提供程序,以获得与指定算法关联的请求的加密服务的实现。 在任何给定的Java虚拟机(JVM)中,提供程序都是以给定的优先顺序安装的,如果没有请求特定的提供程序,搜索提供程序列表的顺序就会被搜索到。 例如,假设在JVM,PROVIDER_1和PROVIDER_2中安装了两个提供程序。 假设:
现在让我们看看三种情况:
包含提供者参数的getInstance方法适用于想要指定哪个提供者需要算法的开发人员。 例如,联邦机构将希望使用已获得联邦认证的提供商实现方案。 假设来自PROVIDER_1的SHA256withDSA实施尚未获得此认证,而PROVIDER_2的DSA实现已获得此认证。
然后联邦机构计划将会有以下调用,并指定PROVIDER_2,因为它具有经过认证的实施:
Signature dsa = Signature.getInstance("SHA256withDSA", "PROVIDER_2");
在这种情况下,如果PROVIDER_2未安装,即使另一个已安装的提供程序实现了所请求的算法,也会引发NoSuchProviderException。
程序还可以选择获取所有已安装提供程序的列表(使用Security类中的getProviders方法),并从列表中选择一个。
注:通用应用程序不应该请求来自特定提供商的加密服务。 否则,应用程序被绑定到在其他Java实现上可能不可用的特定提供程序。 他们也可能无法利用可用的优化提供程序(例如,通过PKCS11的硬件加速器或Microsoft的MSCAPI等本机操作系统实现),这些提供程序的优先顺序高于特定的请求提供程序。
为了使用Provider,必须首先安装加密提供程序,然后静态或动态注册。 Sun发行版提供了多种Sun提供程序(SUN,SunJCE,SunJSSE,SunRsaSign等),已经安装并注册。 以下各节介绍如何安装和注册其他提供程序。
有两种可能的方法来安装Provider类:
1.Solaris, Linux, or Mac OS X: JAVA_HOME/lib/ext
2.Windows:JAVA_HOME/lib/ext
这里JAVA_HOME是指安装运行时软件的目录,它是Java运行时环境(JRE)的顶层目录或Java JDK软件中的jre目录。 例如,如果在Solaris上将JDK 6安装在名为/home/user1/JDK1.6.0的目录中或在Microsoft Windows上的名为C:/ Java / JDK1.6.0的目录中,则需要将JAR文件安装到 以下目录:
同样,如果在Solaris上将JRE 6安装在名为/home/user1/jre1.6.0的目录中或在Microsoft Windows上的名为C:/ jre1.6.0的目录中,则需要将JAR文件安装在以下目录中:
有关如何部署扩展的更多信息,请参阅如何扩展部署
下一步是将Provider添加到注册提供者列表中。 通过在运行Java应用程序之前编辑安全属性配置文件,或通过在运行时调用方法来动态地注册提供程序,可以静态注册提供程序。 为了防止将恶意提供程序的安装添加到运行时环境中,试图动态注册提供程序的应用程序必须拥有适当的运行时权限。
配置文件位于以下位置:
对于每个注册的提供者,这个文件应该有以下形式的表单:
security.provider.n=masterClassName
这声明了一个提供者,并且指定了它的首选顺序n。 首选顺序是提供者搜索请求的算法的顺序(当没有请求特定的提供者时)。 顺序以1为基础:1是最优先的,然后是2,依此类推。
masterClassName必须指定提供者的主类的完全限定名称。 提供者的文档将指定它的主类。 这个类总是Provider类的一个子类。 子类构造函数设置Java加密API所需的各种属性的值,以查找提供者实现的算法或其他工具。
JDK标配自动安装和配置的提供程序,如“SUN”和“SunJCE”。 “SUN”提供者的主类是sun.security.provider包中的SUN类,相应的java.security文件条目如下:
security.provider.5=sun.security.provider.Sun
要利用另一个JCA提供者,请添加一条引用备用提供者的行,指定首选顺序(如果需要,对其他提供者的订单进行相应的调整)。
假设CompanyX的提供者的主类是com.companyx.provider.ProviderX,并且你想把这个提供者配置成第八个最优先的。 为此,您可以将以下行添加到java.security文件中:
security.provider.8=com.companyx.provider.ProviderX
要动态注册提供者,应用程序调用Security类中的addProvider或insertProviderAt方法。 这种类型的注册在VM实例中不是永久性的,只能通过具有适当权限的“可信任”程序完成。 参见安全性。
每当使用加密提供者(即提供Cipher,KeyAgreement,KeyGenerator,Mac或SecretKeyFactory的实现的提供者),并且提供者不是已安装的扩展时可能需要授予使用JCA的applet或应用程序时的权限 安装了一个安全管理器。 每当一个applet运行的时候,通常都会安装一个安全管理器,并且可以通过应用程序本身的代码或通过命令行参数为应用程序安装安全管理器。 由于默认系统策略配置文件授予安装的扩展的所有权限(即安装在扩展目录中),因此不需要授予安装的扩展的权限。
您将要使用的每个提供商的供应商的文档应该包括所需的权限以及如何授予这些权限的信息。 例如,如果提供程序不是已安装的扩展程序并安装了安全管理器,则可能需要以下权限:
例如,下面给出了一个样例语句,该语句授予名称为“MyJCE”且代码位于myjce_provider.jar中的提供程序的权限。 这样的陈述可以出现在政策文件中。 在这个例子中,假设myjce_provider.jar文件位于/ localWork目录中。
grant codeBase "file:/localWork/myjce_provider.jar" { permission java.lang.RuntimePermission "getProtectionDomain"; permission java.security.SecurityPermission "putProviderProperty.MyJCE"; };
每个Provider类实例都有一个(当前区分大小写的)名称,一个版本号以及提供者及其服务的字符串描述。 您可以通过调用以下方法来查询提供程序实例以获取此信息:
public String getName() public double getVersion() public String getInfo()
Security类管理已安装的提供程序和安全性属性。 它只包含静态方法,永远不会实例化。 用于添加或删除提供者以及设置安全属性的方法只能由受信任的程序执行。 目前,“可信程序”也是
如果确定代码被信任以执行尝试的操作(例如添加提供者),则要求该小应用程序被授予适当的特定操作权限。 JDK安装的策略配置文件指定来自指定代码源的代码允许哪些权限(哪些类型的系统资源访问)被允许。 (有关详细信息,请参阅下面的“默认策略实施和策略文件语法”和“https://docs.oracle.com/javase/8/docs/technotes/guides/security/spec/security-spec.doc.html”文件。)
正在执行的代码总是被认为来自特定的“代码源”。 代码源不仅包括源代码的位置(URL),还包括对可能用于签名代码的私钥对应的任何公钥的引用。 代码源中的公钥由用户密钥库中的(符号)别名引用。
在策略配置文件中,代码源由两个组件表示:代码库(URL)和别名(以signedBy开头),其中别名名称标识包含必须用于验证的公钥的密钥库条目 代码的签名。
以下是一个示例策略配置文件:
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文件的签名。)
代码源(或两者)的组件可能会省略。 下面是一个代码库被省略的配置文件的例子:
grant signedBy "sysadmin" { permission java.security.SecurityPermission "insertProvider.*"; permission java.security.SecurityPermission "removeProvider.*"; };
如果此策略有效,那么由sysadmin签名的JAR文件中的代码可以添加/删除提供程序,而不管JAR文件的来源在哪里。
这是一个没有签名者的例子:
grant codeBase "file:/home/sysadmin/" { permission java.security.SecurityPermission "insertProvider.*"; permission java.security.SecurityPermission "removeProvider.*"; };
在这种情况下,来自本地文件系统/ home / sysadmin /目录中任何地方的代码都可以添加/删除提供程序。 代码不需要签名。
不包括codeBase和signedBy的例子是:
grant { permission java.security.SecurityPermission "insertProvider.*"; permission java.security.SecurityPermission "removeProvider.*"; };
在这里,在缺少两个代码源组件的情况下,任何代码(无论它来自何处,是否签名,谁签名)都可以添加/删除提供程序。 显然,这是绝对不推荐,因为这可能会打开一个安全漏洞。 不受信任的代码可能会安装提供程序,从而影响后来取决于正常运行的实现的代码。 (例如,流氓Cipher对象可能会捕获并存储它收到的敏感信息。)
下表总结了Security类中的方法,您可以使用它来查询安装了哪些提供程序,以及在运行时安装或删除提供程序。
查询Provider
方法 | 描述 |
---|---|
static Provider[] getProviders() | 返回一个包含所有已安装提供程序的数组(技术上,每个包提供程序的提供程序子类)。 数组中提供者的顺序是他们的偏好顺序。 |
static Provider getProvider(String providerName) | 返回提供程序名providerName。 如果找不到Provider,它将返回null。 |
增加Provider
方法 | 描述 |
---|---|
static int addProvider(Provider provider) | 将提供程序添加到已安装提供程序列表的末尾。 它将返回提供者添加的偏好位置,如果提供者因为已经安装而未被添加,则返回-1。 |
static int insertProviderAt (Provider provider, int position) | 在指定的位置添加一个新的提供程序。 如果给定的供应商安装在要求的位置,则以前在该位置的供应商和位置大于位置的所有供应商向上移动一个位置(靠近清单的末尾)。 此方法返回提供程序添加的优先级位置,如果提供程序因为已经安装而未添加,则返回-1。 |
删除Provider
方法 | 描述 |
---|---|
static void removeProvider(String name) | 删除具有指定名称的提供程序。 如果没有安装提供程序,它将静默地返回。 当指定的提供者被移除时,位于比指定提供者所在位置更大的位置的所有提供者被向下移动一个位置(朝向已安装提供者列表的头部)。 |
注意:如果要更改提供者的偏好位置,则必须先将其删除,然后将其重新插入到新的偏好位置。
Security类维护一个系统范围的安全属性的列表。 这些属性与系统属性类似,但与安全性相关。 这些属性可以静态或动态设置。 我们已经看到了一个静态安全属性的例子(即,通过“security.provider.i”安全属性静态注册一个提供者)。 如果要动态设置属性,可信程序可以使用以下方法:
static String getProperty(String key) static void setProperty(String key, String datum)
注意:安全提供程序的列表是在VM启动过程中建立的,因此必须使用上述方法来更改提供程序列表。
提醒一下,配置文件位于以下位置:
SecureRandom类是提供随机数生成器(RNG)功能的引擎类。 它不同于java.lang.Random类,因为它产生密码强的随机数。 如果生成器中的随机性不足,则会使保护机制变得更加容易。 在密码学中使用随机数字,例如生成加密密钥,算法参数等等。
所有Java SE实现都必须指出它们在java.security.Security类的securerandom.strongAlgorithms属性中提供的最强(最随机)的SecureRandom实现。 当需要特别强的随机值时,可以使用此实现。
有几种方法可以获得SecureRandom的实例:
SecureRandom实现尝试完全随机化发生器本身的内部状态,除非调用者通过调用其中一个setSeed方法来调用getInstance方法:
synchronized public void setSeed(byte[] seed) public void setSeed(long seed)
一旦SecureRandom对象被设置了种子,它将产生与原始种子一样随机的比特。
在任何时候,SecureRandom对象都可以使用setSeed方法之一重新设置种子。 此时会对原来的种子进行补充,而不是取代现有的种子; 因此,保证重复的调用永远不会降低随机性。
要获得随机字节,调用者只需传递任意长度的数组,然后用随机字节填充:
synchronized public void nextBytes(byte[] bytes)
如果需要,可以调用generateSeed方法来生成给定数量的种子字节(例如,为其他随机数生成器生成种子):
byte[] generateSeed(int numBytes)
MessageDigest类是一个引擎类,用于提供密码安全的消息摘要(如SHA-256或SHA-512)的功能。 加密安全的消息摘要采用任意大小的输入(一个字节数组),并生成一个固定大小的输出,称为摘要或散列。
例如,SHA-256算法产生一个32字节的摘要,而SHA-512的是64字节。
摘要有两个属性:
消息摘要用于生成唯一且可靠的数据标识符。 它们有时被称为“校验和”或数据的“数字指纹”。 只改变消息的一位应该产生不同的摘要值。
消息摘要有许多用途,可以确定数据何时被修改,有意或无意。 最近,已经有相当大的努力来确定流行算法是否存在任何缺陷,结果不尽相同。 在选择一个摘要算法时,应该总是查阅最近的参考文献,以确定其当前任务的状态和适当性。
计算摘要的第一步是创建一个消息摘要实例。 MessageDigest对象是通过使用MessageDigest类中的getInstance()静态工厂方法之一获得的。 工厂方法返回一个初始化的消息摘要对象。 因此不需要进一步的初始化。
计算某些数据的摘要的下一步是将数据提供给初始化的消息摘要对象。 它可以一次或一块地提供。 可以通过调用其中一种更新方法将消息提供给消息摘要:
void update(byte input) void update(byte[] input) void update(byte[] input, int offset, int len)
在通过调用更新数据块之后,使用对其中一个摘要方法的调用来计算摘要:
byte[] digest() byte[] digest(byte[] input) int digest(byte[] buf, int offset, int len)
第一种方法返回计算的摘要。 第二种方法在调用digest()之前用输入字节数组进行最终更新(输入),digest()返回摘要字节数组。 最后一个方法将计算的摘要存储在提供的缓冲区buf中,从偏移量开始。 len是分配给摘要的buf中的字节数,该方法返回实际存储在buf中的字节数。 如果缓冲区中没有足够的空间,该方法将抛出异常。
请参阅代码示例部分中的计算MessageDigest示例以获取更多详细信息。
Signature类是一个引擎类,旨在提供加密数字签名算法(如DSA或RSAwithMD5)的功能。 密码安全签名算法采用任意大小的输入和私钥,并生成一个相对较短(通常是固定大小)的字节串,称为签名,具有以下属性:
它也可以用来验证所谓的签名是否实际上是与其相关的数据的真实签名。
一个签名对象被初始化为用一个私钥进行签名,并被赋予待签名的数据。 结果签名字节通常与签名数据保持一致。 当需要验证时,另一个签名对象被创建和初始化以进行验证并给出相应的公钥。 数据和签名字节被馈送到签名对象,并且如果数据和签名匹配,则签名对象报告成功。
尽管签名看起来与消息摘要相似,但它们在提供的保护类型上却有着非常不同的目的。 事实上,诸如“SHA256withRSA”的算法使用消息摘要“SHA256”将大数据集初始“压缩”为更易于管理的形式,然后用“RSA”算法对得到的32字节消息摘要进行签名。
请参阅示例部分以获取签名和验证数据的示例。
签名对象是模态对象。 这意味着一个Signature对象总是处于一个给定的状态,它只能执行一种操作。 状态被表示为在其各自的类中定义的最终整数常量。
签名对象可能具有的三种状态是:
第一次创建时,Signature对象处于UNINITIALIZED状态。 Signature类定义了两个初始化方法initSign和initVerify,它们分别将状态更改为SIGN和VERIFY。
签名或验证签名的第一步是创建一个签名实例。 签名对象是通过使用Signature getInstance()静态工厂方法之一获得的。
Signature对象在使用之前必须被初始化。 初始化方法取决于对象是要用于签名还是验证。
如果要用于签名,则必须首先使用要生成签名的实体的私钥来初始化该对象。 这个初始化通过调用方法完成:
final void initSign(PrivateKey privateKey)
此方法将Signature对象置于SIGN状态。
如果相反,Signature对象将用于验证,则必须首先使用将要验证签名的实体的公钥来初始化。 这个初始化是通过调用以下任一方法来完成的:
final void initVerify(PublicKey publicKey) final void initVerify(Certificate certificate)
此方法将Signature对象置于VERIFY状态。
如果签名对象已经被初始化为签名(如果它处于SIGN状态),则待签名的数据可以被提供给该对象。 这是通过对其中一个更新方法进行一个或多个调用完成的:
final void update(byte b) final void update(byte[] data) final void update(byte[] data, int off, int len)
应该调用update方法,直到所有要签名的数据都被提供给签名对象。
要生成签名,只需调用其中一个签名方法:
final byte[] sign() final int sign(byte[] outbuf, int offset, int len)
第一个方法以字节数组的形式返回签名结果。 第二个将签名结果存储在提供的缓冲区outbuf中,从offset开始。 len是分配给签名的outbuf中的字节数。 该方法返回实际存储的字节数。
签名编码是特定于算法的。 有关在Java加密体系结构中使用ASN.1编码的更多信息,请参阅标准名称文档。
对sign方法的调用会将签名对象重置为之前通过调用initSign进行初始化以进行签名的状态。 也就是说,如果需要,该对象将被重置并可用于生成具有相同私钥的另一个签名,通过新的调用来更新和签名。或者,可以对指定不同私钥的initSign或initVerify(初始化Signature对象以验证签名)进行新的调用。
如果签名对象已被初始化以进行验证(如果它处于VERIFY状态),则它可以验证所谓的签名实际上是与其相关联的数据的真实签名。 为了开始这个过程,要被验证的数据(而不是签名本身)被提供给对象。 通过调用其中一种更新方法将数据传递给对象:
final void update(byte b) final void update(byte[] data) final void update(byte[] data, int off, int len)
应该调用更新方法,直到所有要验证的数据都被提供给Signature对象。 现在可以通过调用其中一种验证方法来验证签名:
final boolean verify(byte[] signature) final boolean verify(byte[] signature, int offset, int length)
参数必须是包含签名的字节数组。 这个字节数组将保存以前调用返回的签名字节。
verify方法返回一个布尔值,指示编码签名是否是提供给更新方法的数据的真实签名。
对verify方法的调用通过调用initVerify将初始化后的签名对象重置为其状态。 也就是说,该对象被重置并可用于验证在initVerify调用中指定了公钥的身份的另一个签名。
或者,可以对initVerify进行一次新的调用,指定一个不同的公钥(初始化Signature对象以验证来自不同实体的签名)或initSign(初始化Signature对象以生成签名)。
Cipher类提供用于加密和解密的加密密码的功能。 加密是取数据(称为明文)和密钥的过程,并且产生对不知道密钥的第三方毫无意义的数据(密文)。 解密是相反的过程:取密文和密钥并产生明文。
有两种主要类型的加密:对称(也称为密钥)和非对称(或公钥密码)。 在对称密码学中,同一个密钥既能加密也能解密数据。 保持密钥私密对保持数据保密至关重要。 另一方面,非对称加密使用公钥/私钥对来加密数据。 用一个密钥加密的数据与另一个密钥解密。 用户首先生成公钥/私钥对,然后将公钥发布在任何人都可以访问的可信数据库中。 希望与该用户安全通信的用户使用检索到的公钥来加密数据。 只有私钥的持有者才能解密。 保密私钥对此方案至关重要。
不对称算法(如RSA)通常比对称算法慢得多。 这些算法不能有效地保护大量的数据。 实际上,不对称算法被用来交换较小的秘密密钥,用来初始化对称算法。
密码有两种主要类型:块和流。 分组密码一次处理整个块,通常是多个字节的长度。 如果没有足够的数据来创建完整的输入块,则必须填充数据:也就是说,在加密之前,必须添加虚拟字节以使密码块大小成倍数。 这些字节在解密阶段被剥离。 填充既可以由应用程序完成,也可以通过初始化密码来使用填充类型,例如“PKCS5PADDING”。 相比之下,流密码一次只能处理一个小单元(通常是一个字节,甚至一点点)的传入数据。 这允许密码处理任意数量的数据而不用填充。
当使用简单的分组密码进行加密时,两个相同的明文块将总是产生相同的密文块。如果他们注意到重复文本块,那么试图破解密文的密码分析者将会有更容易的工作。为了增加文本的复杂性,反馈模式使用前面的输出块在应用加密算法之前改变输入块。第一个块需要一个初始值,这个值被称为初始化向量(IV)。由于IV在加密之前只是简单地改变数据,所以IV应该是随机的,但不一定需要保密。有多种模式,例如CBC(密码块链接),CFB(密码反馈模式)和OFB(输出反馈模式)。 ECB(电子码本模式)是一种不受块位置或其他密文块影响的模式。因为如果ECB密文使用相同的明文/密钥,ECB密文是相同的,这种模式通常不适合加密应用,不应该使用。
一些算法如AES和RSA允许不同长度的密钥,但其他算法则是固定的,如3DES。 使用更长的密钥进行加密通常意味着对消息恢复的更强的抵抗力。 像往常一样,安全和时间之间有一个折衷,所以选择适当的密钥长度。
大多数算法使用二进制密钥。 即使以十六进制表示,大多数人类也无法记忆长序列的二进制数字。 字符密码更容易记忆。 由于字符密码通常是从少量字符中选择的(例如[a-zA-Z0-9]),因此已经定义了诸如“基于密码的加密”(PBE)等协议,这些协议使用字符密码并生成强二进制密钥。 为了使攻击者从口令到密钥的任务非常耗时(通过所谓的“字典式攻击”,其中常用字典字 – 值映射是预先计算的),大多数PBE实现将以随机数混合, 被称为盐,以增加关键的随机性。
更新的密码模式,例如带有关联数据的认证加密(AEAD)(例如,伽罗瓦/计数器模式(GCM)),可以对数据进行加密并同时验证结果信息。 在计算生成的AEAD标记(Mac)期间可以使用附加关联数据(AAD),但是这个AAD数据不会以密文的形式输出。 (例如,某些数据可能不需要保密,但应该计算标签计算以检测修改。)Cipher.updateAAD()方法可用于在标签计算中包含AAD。
使用GCM的AES密码是一种AEAD密码,与非AEAD密码具有不同的使用模式。 除了常规数据外,还需要AAD,这是可选的加密/解密,但AAD必须在数据加密/解密之前提供。 另外,为了安全地使用GCM,调用者不应该重复使用密钥和IV组合来进行加密。 这意味着每次加密操作时,密码对象应该用不同的一组参数显式地重新初始化。
SecretKey myKey = ... ; byte[] myAAD = ... ; byte[] plainText = ... ; int myTLen = ... ; byte[] myIv = ... ; GCMParameterSpec myParams = new GCMParameterSpec(myTLen, myIv); Cipher c = Cipher.getInstance("AES/GCM/NoPadding"); c.init(Cipher.ENCRYPT_MODE, myKey, myParams); // AAD is optional, if present, it must be supplied before any update/doFinal calls. c.updateAAD(myAAD); // if AAD is non-null byte[] cipherText = new byte[c.getOutputSize(plainText.length)]; c.doFinal(plainText, 0, plainText.length, cipherText); // conclusion of encryption operation // To decrypt, same AAD and GCM parameters must be supplied c.init(Cipher.DECRYPT_MODE, myKey, myParams); c.updateAAD(myAAD); byte[] recoveredText = c.doFinal(cipherText); // MUST CHANGE IV VALUE if the same key were to be used again for encryption byte[] newIv = ...; myParams = new GCMParameterSpec(myTLen, newIv);
密码对象是通过使用密码getInstance()静态工厂方法之一获得的。 在这里,算法名称与其他引擎类稍有不同,因为它不仅指定算法名称,而且指定“变换”。 转换是一个字符串,它描述了在给定输入上执行的操作(或操作集)以产生一些输出。 变换总是包括密码算法的名称(例如,AES),并且可以跟随模式和填充方案。
转型的形式是:
例如,以下是有效的转换:
"AES/CBC/PKCS5Padding" "AES"
如果只指定了一个转换名称,系统将确定在环境中是否有所需的转换实现,如果有多个转换名称,则返回一个首选项。
如果同时指定了转换名称和包提供者,系统将确定所请求的包中是否存在所请求转换的实现,如果没有,则抛出异常。
建议使用完全指定算法,模式和填充的转换。 如果不这样做,提供者将使用默认值。 例如,SunJCE和SunPKCS11提供程序将ECB用作默认模式,将PKCS5Padding用作许多对称密码的默认填充。
这意味着在SunJCE提供商的情况下:
Cipher c1 = Cipher.getInstance("AES/ECB/PKCS5Padding");
和
Cipher c1 = Cipher.getInstance("AES");
是等同的语句。
**注意:**ECB模式是最简单的块密码模式,并且是JDK / JRE中的默认模式。 ECB适用于单个数据块,但绝对不应该用于多个数据块。
使用CFB和OFB等模式,分组密码可以以小于密码实际块大小的单位加密数据。 在请求这种模式时,可以通过在“AES / CFB8 / NoPadding”和“AES / OFB32 / PKCS5Padding”转换中将模式名称附加到模式名称来一次指定要处理的位数。 如果没有指定这样的号码,则使用提供者特定的默认值。 (例如,SunJCE提供程序使用默认值为128位的AES)。因此,可以使用8位模式(如CFB8或OFB8)将块密码转换为面向字节的流密码。
本文档的附录A包含一个标准名称列表,可用于指定转换的算法名称,模式和填充方案组件。
工厂方法返回的对象是未初始化的,必须在它们变为可用之前进行初始化。
通过getInstance获得的Cipher对象必须初始化为四种模式之一,在Cipher类中定义为最终整数常量。 这些模式可以通过它们的符号名称来引用,这些符号名称将在下面显示,同时还会描述每种模式的用途:
每个密码初始化方法都采用操作模式参数(opmode),并初始化该模式的密码对象。 其他参数包括包含密钥(certificate)的密钥(key)或证书,算法参数(params)以及随机源(random)。
要初始化Cipher对象,请调用以下init方法之一:
public void init(int opmode, Key key); public void init(int opmode, Certificate certificate); public void init(int opmode, Key key, SecureRandom random); public void init(int opmode, Certificate certificate, SecureRandom random); public void init(int opmode, Key key, AlgorithmParameterSpec params); public void init(int opmode, Key key, AlgorithmParameterSpec params, SecureRandom random); public void init(int opmode, Key key, AlgorithmParameters params); public void init(int opmode, Key key, AlgorithmParameters params, SecureRandom random);
如果需要参数的密码对象(例如,初始化向量)被初始化为加密,并且没有参数提供给初始化方法,则底层密码实现本身应该提供所需的参数,或者通过生成随机参数或者通过使用 默认的,特定于提供者的参数集合。
但是,如果需要参数的Cipher对象被初始化为解密,并且没有参数提供给init方法,则将引发InvalidKeyException或InvalidAlgorithmParameterException异常,具体取决于所使用的init方法。
有关更多详细信息,请参阅关于管理算法参数一节.
必须使用与加密相同的参数进行解密。
请注意,当一个Cipher对象被初始化时,它将失去所有先前获得的状态。 换句话说,初始化一个Cipher就相当于创建一个新的Cipher实例,并初始化它。 例如,如果密码首先被初始化为用给定密钥进行解密,然后初始化用于加密,则在解密模式下将失去获得的任何状态。
数据可以在一个步骤(单部分操作)或多个步骤(多部分操作)中加密或解密。 如果事先不知道数据将要运行多长时间,或者数据太长而无法一次存储在内存中,则多部分操作非常有用。
要在一个步骤中加密或解密数据,请调用其中一个doFinal方法:
public byte[] doFinal(byte[] input); public byte[] doFinal(byte[] input, int inputOffset, int inputLen); public int doFinal(byte[] input, int inputOffset, int inputLen, byte[] output); public int doFinal(byte[] input, int inputOffset, int inputLen, byte[] output, int outputOffset)
要以多个步骤加密或解密数据,请调用其中一种更新方法:
public byte[] update(byte[] input); public byte[] update(byte[] input, int inputOffset, int inputLen); public int update(byte[] input, int inputOffset, int inputLen, byte[] output); public int update(byte[] input, int inputOffset, int inputLen, byte[] output, int outputOffset)
多部分操作必须由上述doFinal方法之一终止(如果最后一步仍有一些输入数据),或者通过以下doFinal方法之一(如果没有输入数据留给最后一步):
public byte[] doFinal(); public int doFinal(byte[] output, int outputOffset);
如果填充(或非填充)已被请求作为指定转换的一部分,则所有doFinal方法都将处理任何必要的填充(或非填充)。
调用doFinal会将Cipher对象重置为通过调用init初始化时的状态。 也就是说,Cipher对象被重置并且可用于加密或解密(取决于在对init的调用中指定的操作模式)更多的数据。
包装钥匙可以将钥匙从一个地方安全地转移到另一个地方。
wrap / unwrap API使得编写代码更方便,因为它直接处理关键对象。 这些方法还可以安全地传输基于硬件的密钥。
要包装一个Key,首先要为WRAP_MODE初始化Cipher对象,然后调用以下内容:
public final byte[] wrap(Key key);
如果您要将打包的密钥字节(调用wrap的结果)提供给打开它们的其他人,请务必发送收件人需要的附加信息,以便进行解包:
密钥算法名称可以通过从Key接口调用getAlgorithm方法来确定:
public String getAlgorithm();
要打开先前调用的包装返回的字节,请首先初始化UNWRAP_MODE的Cipher对象,然后调用以下内容:
public final Key unwrap(byte[] wrappedKey, String wrappedKeyAlgorithm, int wrappedKeyType));
这里,wrappedKey是从前一个包装调用返回的字节,wrappedKeyAlgorithm是与包装的关键字相关的算法,wrappedKeyType是包装的关键字的类型。 这必须是Cipher.SECRET_KEY,Cipher.PRIVATE_KEY或Cipher.PUBLIC_KEY之一。
底层Cipher实现使用的参数(通过应用程序显式传递给init方法或由底层实现本身生成)可以通过调用其getParameters方法从Cipher对象中检索,该方法将参数作为java返回 .security.AlgorithmParameters对象(如果没有使用参数,则返回null)。 如果参数是初始化向量(IV),则也可以通过调用getIV方法来检索。
在以下示例中,实现基于密码的加密(PBE)的Cipher对象仅使用一个键而没有参数进行初始化。 但是,所选择的基于密码的加密算法需要两个参数 – 一个salt和一个迭代计数。 这些将由底层算法实现本身生成。 应用程序可以从Cipher对象中检索生成的参数,如下所示:
import javax.crypto.*; import java.security.AlgorithmParameters; // get cipher object for password-based encryption Cipher c = Cipher.getInstance("PBEWithHmacSHA256AndAES_256"); // initialize cipher for encryption, without supplying // any parameters. Here, "myKey" is assumed to refer // to an already-generated key. c.init(Cipher.ENCRYPT_MODE, myKey); // encrypt some data and store away ciphertext // for later decryption byte[] cipherText = c.doFinal("This is just an example".getBytes()); // retrieve parameters generated by underlying cipher // implementation AlgorithmParameters algParams = c.getParameters(); // get parameter encoding and store it away byte[] encodedAlgParams = algParams.getEncoded();
必须使用与加密相同的参数进行解密。 它们可以从它们的编码实例化,并用于初始化相应的Cipher对象进行解密,如下所示:
import javax.crypto.*; import java.security.AlgorithmParameters; // get parameter object for password-based encryption AlgorithmParameters algParams; algParams = AlgorithmParameters.getInstance("PBEWithHmacSHA256AndAES_256"); // initialize with parameter encoding from above algParams.init(encodedAlgParams); // get cipher object for password-based encryption Cipher c = Cipher.getInstance("PBEWithHmacSHA256AndAES_256"); // initialize cipher for decryption, using one of the // init() methods that takes an AlgorithmParameters // object, and pass it the algParams object from above c.init(Cipher.DECRYPT_MODE, myKey, algParams);
如果在初始化Cipher对象时没有指定任何参数,并且您不确定底层实现是否使用任何参数,则可以通过简单地调用Cipher对象的getParameters方法并检查返回的值来找到。 返回值为null表示没有使用参数。
SunJCE提供者实现的以下密码算法使用参数:
请注意,如果使用SealedObject类,则不必担心存储或传输解密操作使用的任何算法参数。 该类将用于密封(加密)的参数附加到加密的对象内容,并使用相同的参数来解密(解密)。
一些Cipher的更新和doFinal方法允许调用者指定要将数据加密或解密的输出缓冲区。 在这些情况下,传递足够大的缓冲区来保存加密或解密操作的结果是很重要的。
密码中的以下方法可用于确定输出缓冲区的大小:
public int getOutputSize(int inputLen)
有一些帮助程序类在内部使用密码提供方便的访问常见的密码使用。
这个类是一个FilterInputStream,用于加密或解密通过它的数据。 它由一个InputStream或其一个子类和一个Cipher组成。 CipherInputStream表示一个安全的输入流,一个Cipher对象被插入到其中。 CipherInputStream的读取方法返回从底层InputStream中读取的数据,但是嵌入的Cipher对象已经处理了额外的数据。 Cipher对象在被CipherInputStream使用之前必须完全初始化。
例如,如果嵌入式密码已经被初始化为解密,则CipherInputStream会在将它们返回到应用程序之前尝试解密从底层InputStream中读取的数据。
该类严格遵守其祖先类java.io.FilterInputStream和java.io.InputStream的语义,特别是失败语义。 这个类具有在祖先类中指定的那些方法,并覆盖它们全部,以便数据由嵌入式密码进行额外处理。 此外,这个类捕获所有祖先类不抛出的异常。 特别是skip(long)方法只会跳过已经被Cipher处理过的数据。
使用这个类的程序员不要使用在这个类中没有定义或重写的方法(比如一个新的方法或稍后添加到其中一个超类的构造函数),这是非常重要的,因为这些方法的设计和实现 不太可能考虑对CipherInputStream的安全影响。
作为使用的一个例子,假设cipher1已经被初始化用于加密。 下面的代码演示了如何使用包含该密码和FileInputStream的CipherInputStream来加密输入流数据:
FileInputStream fis; FileOutputStream fos; CipherInputStream cis; fis = new FileInputStream("/tmp/a.txt"); cis = new CipherInputStream(fis, cipher1); fos = new FileOutputStream("/tmp/b.txt"); byte[] b = new byte[8]; int i = cis.read(b); while (i != -1) { fos.write(b, 0, i); i = cis.read(b); } fos.close();
上面的程序从文件/tmp/a.txt中读取和加密内容,然后将结果(加密字节)存储在/tmp/b.txt中。
以下示例演示如何轻松连接CipherInputStream和FileInputStream的多个实例。 在这个例子中,假设cipher1和cipher2已分别被加密和解密初始化(使用相应的密钥)。
FileInputStream fis; FileOutputStream fos; CipherInputStream cis1, cis2; fis = new FileInputStream("/tmp/a.txt"); cis1 = new CipherInputStream(fis, cipher1); cis2 = new CipherInputStream(cis1, cipher2); fos = new FileOutputStream("/tmp/b.txt"); byte[] b = new byte[8]; int i = cis2.read(b); while (i != -1) { fos.write(b, 0, i); i = cis2.read(b); } fos.close();
上面的程序将文件/tmp/a.txt中的内容复制到/tmp/b.txt中,除了内容首先被加密,然后在从/tmp/a.txt读取内容时解密。 当然,因为这个程序只是简单地加密文本并立即解密,实际上它并不是非常有用,除非作为一个简单的方式来说明CipherInputStreams的链接。
请注意,CipherInputStream的读取方法将阻塞,直到从底层密码返回数据。 如果使用分组密码,则必须从底层的InputStream获得完整的密文块。
这个类是FilterOutputStream,用于加密或解密通过它的数据。 它由一个OutputStream或其一个子类和一个Cipher组成。 CipherOutputStream表示一个安全的输出流,一个Cipher对象插入其中。 CipherOutputStream的写入方法首先使用嵌入的Cipher对象处理数据,然后将它们写出到底层的OutputStream中。 Cipher对象在被CipherOutputStream使用之前必须完全初始化。
例如,如果嵌入式密码已经被初始化为加密,那么CipherOutputStream会在将数据写出到底层输出流之前对其数据进行加密。
该类严格遵守其祖先类java.io.OutputStream和java.io.FilterOutputStream的语义,特别是失败语义。 这个类具有在其祖先类中指定的方法,并覆盖它们全部,以便所有数据都由嵌入式密码额外处理。 此外,这个类捕获所有祖先类不抛出的异常。
使用这个类的程序员不要使用在这个类中没有定义或重写的方法(比如一个新的方法或稍后添加到其中一个超类的构造函数),这是非常重要的,因为这些方法的设计和实现 不太可能考虑对CipherOutputStream的安全影响。
作为使用的一个例子,假设cipher1已经被初始化用于加密。 下面的代码演示了如何使用包含该密码和FileOutputStream的CipherOutputStream来加密要写入输出流的数据:
FileInputStream fis; FileOutputStream fos; CipherOutputStream cos; fis = new FileInputStream("/tmp/a.txt"); fos = new FileOutputStream("/tmp/b.txt"); cos = new CipherOutputStream(fos, cipher1); byte[] b = new byte[8]; int i = fis.read(b); while (i != -1) { cos.write(b, 0, i); i = fis.read(b); } cos.flush();
上面的程序从文件/tmp/a.txt中读取内容,然后将结果(加密字节)加密并存储在/tmp/b.txt中。
以下示例演示如何轻松连接CipherOutputStream和FileOutputStream的多个实例。 在这个例子中,假设cipher1和cipher2已分别被初始化用于解密和加密(使用相应的密钥):
FileInputStream fis; FileOutputStream fos; CipherOutputStream cos1, cos2; fis = new FileInputStream("/tmp/a.txt"); fos = new FileOutputStream("/tmp/b.txt"); cos1 = new CipherOutputStream(fos, cipher1); cos2 = new CipherOutputStream(cos1, cipher2); byte[] b = new byte[8]; int i = fis.read(b); while (i != -1) { cos2.write(b, 0, i); i = fis.read(b); } cos2.flush();
上述程序将文件/tmp/a.txt中的内容复制到/tmp/b.txt中,除了内容先被加密,然后在写入/tmp/b.txt之前将其解密。
使用分组密码算法时需要注意的一点是,在将数据加密并发送到底层输出流之前,必须给CipherOutputStream一个完整的明文数据块。
在这个类的flush和close方法之间还有一个其他的重要区别,如果被封装的密码对象实现了一个开启了padding的分组密码算法,它变得更加相关:
这个类使程序员能够用密码算法创建对象并保护其机密性。
给定任何实现了java.io.Serializable接口的对象,可以使用密码算法创建一个封装原始对象的SealedObject,以序列化格式(即“深层复制”)封装原始对象,并封装(加密)其序列化内容 如AES,以保护其机密性。 加密的内容稍后可以被解密(使用正确的解密密钥的相应算法)并且解序列化,产生原始对象。
下面的代码段说明了一个典型的用法:为了密封一个对象,您需要从要被密封的对象和一个完全初始化的Cipher对象中创建一个SealedObject,它将加密序列化的对象内容。 在这个例子中,字符串“这是一个秘密”是使用AES算法密封的。 请注意,密封操作中可能使用的任何算法参数都存储在SealedObject内部:
// create Cipher object // NOTE: sKey is assumed to refer to an already-generated // secret AES key. Cipher c = Cipher.getInstance("AES"); c.init(Cipher.ENCRYPT_MODE, sKey); // do the sealing SealedObject so = new SealedObject("This is a secret", c);
密封的原始对象可以通过两种不同的方法恢复:
c.init(Cipher.DECRYPT_MODE, sKey); try { String s = (String)so.getObject(c); } catch (Exception e) { // do something };
这种方法的优点是开封密封对象的一方不需要知道解密密钥。 例如,在一方用所需的解密密钥初始化密码对象之后,它可以将密码对象交给另一方,然后开封密封的对象。
try { String s = (String)so.getObject(sKey); } catch (Exception e) { // do something };
在这种方法中,getObject方法为相应的解密算法创建一个密码对象,并使用给定的解密密钥和存储在密封对象中的算法参数(如果有的话)对其初始化。 这种方法的优点是开封对象的一方不需要跟踪用于密封对象的参数(例如,IV)。
与消息摘要类似,消息认证码(MAC)提供了一种方法来检查通过或存储在不可靠介质中的信息的完整性,但在计算中包括秘密密钥。 只有具有正确密钥的人才能够验证收到的消息。 典型地,在共享密钥的双方之间使用消息认证码以验证在这些方之间传输的信息。
基于密码散列函数的MAC机制被称为HMAC。 HMAC可以与任何密码散列函数(例如SHA-256)结合秘密共享密钥一起使用。
Mac类提供消息认证码(MAC)的功能。 请参考代码示例。
Mac对象是通过使用Mac getInstance()静态工厂方法之一获得的。
一个Mac对象总是用一个(秘密)密钥初始化的,并且可以根据底层的MAC算法用一组参数初始化。
您可以使用实现javax.crypto.SecretKey接口的任何(秘密)密钥对象来初始化您的Mac对象。 这可以是javax.crypto.KeyGenerator.generateKey()返回的对象,也可以是javax.crypto.KeyAgreement.generateSecret()返回的密钥协议协议的结果,也可以是javax.crypto的实例。spec.SecretKeySpec。
使用一些MAC算法,与用于初始化Mac对象的(秘密)密钥对象关联的(秘密)密钥算法无关紧要(SunJCE提供者的HMAC-MD5和HMAC-SHA1实现就是这种情况)。 然而,对于其他人来说,(秘密)密钥算法确实很重要,如果使用具有不适当(秘密)密钥算法的(秘密)密钥对象,则会引发InvalidKeyException。
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);
到目前为止,我们已经将JCA的高层次使用集中在了Key以及如何生成/表示的细节上。 现在是把注意力转向Key的时候了。
java.security.Key接口是所有不透明密钥的顶层接口。 它定义了所有不透明密钥对象共享的功能。
一个不透明的密钥表示就是你不能直接访问构成密钥的密钥材料。 换句话说:“不透明”使您能够有限地访问密钥 – 只有Key接口定义的三个方法(参见下面):getAlgorithm,getFormat和getEncoded。
这与透明表示形式相反,在透明表示形式中,可以通过相应规范类中定义的某个get方法单独访问每个关键材料值。
所有不透明的密钥有三个特征:
String getAlgorithm()
byte[] getEncoded()
String getFormat()
密钥一般通过密钥生成器(如KeyGenerator和KeyPairGenerator),证书,密钥规范(使用KeyFactory)或访问用于管理密钥的密钥库数据库的KeyStore实现获得。 可以使用KeyFactory以算法相关的方式解析编码密钥。
也可以使用CertificateFactory解析证书。
以下是在java.security.interfaces和javax.crypto.interfaces包中扩展Key接口的接口列表:
PublicKey和PrivateKey接口(它们都扩展了Key接口)是无方法的接口,用于类型安全和类型标识。
KeyPair类是密钥对(公钥和私钥)的简单持有者。 它有两个公共方法,一个用于返回私钥,另一个用于返回公钥:
PrivateKey getPrivate() PublicKey getPublic()
密钥对象和密钥规范(KeySpecs)是密钥数据的两种不同表示。 密码使用密钥对象来初始化他们的加密算法,但密钥可能需要转换成更便携的格式进行传输或存储。
密钥的透明表示意味着您可以通过相应规范类中定义的某个get方法单独访问每个关键字值。 例如,DSAPrivateKeySpec定义了getX,getP,getQ和getG方法来访问私钥x以及用于计算密钥的DSA算法参数:素数p,次数q和基数g。 如果密钥存储在硬件设备上,其规格可能包含有助于识别设备密钥的信息。
这种表示形式与密钥接口中定义的不透明表示形式相反,在这种表示形式中,您无法直接访问密钥材料字段。 换句话说,“不透明”的表示形式使得您可以有限地访问键 – 只有Key接口定义的三种方法:getAlgorithm,getFormat和getEncoded。
密钥可以按照算法特定的方式或者以独立于算法的编码格式(例如ASN.1)来指定。 例如,DSA私钥可以由其组件x,p,q和g(请参阅DSAPrivateKeySpec)指定,也可以使用DER编码指定(请参阅PKCS8EncodedKeySpec)。
KeyFactory和SecretKeyFactory类可以用来在不透明和透明的键表示之间进行转换(也就是说,在Keys和KeySpecs之间,假设操作是可能的)(例如,智能卡上的私钥可能不能离开卡。 密钥不可转换。)
在下面的章节中,我们将讨论java.security.spec包中的密钥规范接口和类。
这个接口不包含方法或常量。 其唯一目的是为所有规范进行分组和提供类型安全。 所有密钥规范都必须实现这个接口。
就像Key接口一样,KeySpec接口也是类似的。
这个抽象类(实现KeySpec接口)表示编码格式的公钥或私钥。 它的getEncoded方法返回编码密钥:
abstract byte[] getEncoded();
它的getFormat方法返回编码格式的名称:
abstract String getFormat();
有关具体实现PKCS8EncodedKeySpec和X509EncodedKeySpec,请参阅下一节。
该类是EncodedKeySpec的一个子类,它表示根据PKCS8标准中指定的格式对私钥进行DER编码。 它的getEncoded方法返回按照PKCS8标准编码的密钥字节。 它的getFormat方法返回字符串“PKCS#8”。
这个类是EncodedKeySpec的一个子类,它表示公钥的DER编码,根据X.509标准中规定的格式。 它的getEncoded方法返回按照X.509标准编码的密钥字节。 它的getFormat方法返回字符串“X.509”。
Java的新手和特别是和JCA API相关的有时并没有把握区分Generator和Factory之间的区别。
生成器用于生成全新的对象。 发生器可以以算法相关或算法独立的方式进行初始化。 例如,要创建Diffie-Hellman(DH)密钥对,应用程序可以指定必要的P和G值,或者可以简单地使用适当的密钥长度对生成器进行初始化,生成器将选择适当的P和G值。 在这两种情况下,发生器将根据参数生成全新的密钥。
另一方面,工厂用于将数据从一个现有的对象类型转换为另一个。 例如,一个应用程序可能具有DH私钥的组件,并且可以将它们打包为KeySpec,但是需要将它们转换为KeyAgreement对象可以使用的PrivateKey对象,反之亦然。 或者他们可能拥有证书的字节数组,但需要使用CertificateFactory将其转换为X509Certificate对象。 应用程序使用工厂对象来进行转换。
KeyFactory类是一个引擎类,设计用于在不透明的加密密钥和密钥规范(底层密钥材料的透明表示)之间执行转换。
密钥工厂是双向的。 它们允许您从给定的密钥规范(密钥材料)构建不透明的密钥对象,或者以合适的格式检索密钥对象的基础密钥材料。
同一个密钥可以存在多个兼容的密钥规范。 例如,DSA公钥可以由其组件y,p,q和g(参见java.security.spec.DSAPublicKeySpec)指定,也可以根据X.509标准使用其DER编码来指定(参见X509EncodedKeySpec)。
一个密钥工厂可以用来在兼容的密钥规范之间进行转换。 可以通过兼容密钥规范之间的转换来实现密钥解析,例如,当从X509EncodedKeySpec转换为DSAPublicKeySpec时,基本上将编码密钥解析为其组件。 有关示例,请参阅使用密钥规范和KeyFactory部分生成/验证签名的章节。
KeyFactory对象是通过使用KeyFactory的getInstance()静态工厂方法之一获得的。
如果您有公钥的密钥规范,则可以使用generatePublic方法从规范中获取不透明的PublicKey对象:
PublicKey generatePublic(KeySpec keySpec)
同样,如果您拥有私钥的密钥规范,则可以使用generatePrivate方法从规范中获取不透明的PrivateKey对象:
PrivateKey generatePrivate(KeySpec keySpec)
如果您有Key对象,则可以通过调用getKeySpec方法来获取相应的密钥规范对象:
KeySpec getKeySpec(Key key, Class keySpec)
keySpec标识应该返回密钥材料的规范类。 例如,它可以是DSAPublicKeySpec.class,用于指示应该在DSAPublicKeySpec类的实例中返回密钥材料。
请参阅示例部分以获取更多详细信息。
这个类代表一个秘密密钥的工厂。 与KeyFactory不同的是,javax.crypto.SecretKeyFactory对象仅在秘密(对称)键上运行,而java.security.KeyFactory对象则处理密钥对的公钥和私钥组件。
密钥工厂用于将密钥(java.security.Key类型的不透明密钥)转换为密钥规范(以合适的格式对基础密钥材料进行透明表示),反之亦然。
java.security.Key类型的对象(其中java.security.PublicKey,java.security.PrivateKey和javax.crypto.SecretKey是子类)是不透明的密钥对象,因为您无法知道它们是如何实现的。 底层的实现依赖于提供者,可能是基于软件或硬件的。 密钥工厂允许供应商提供他们自己的加密密钥的实现。
例如,如果您具有Diffie Hellman公钥的密钥规范(由公有值y,主模数p和基数g组成),并将相同的规范从不同的提供程序提供给Diffie-Hellman密钥工厂,则 结果PublicKey对象将很可能有不同的底层实现。
提供者应记录其密钥工厂支持的密钥规范。 例如,SunJCE提供者提供的DES密钥的SecretKeyFactory支持DESKeySpec作为DES密钥的透明表示,DES-EDE密钥的SecretKeyFactory支持DESedeKeySpec作为DES-EDE密钥的透明表示,而PBE的SecretKeyFactory支持PBEKeySpec作为 底层密码的透明表示。
以下是如何使用SecretKeyFactory将密钥数据转换为可用于后续密码操作的SecretKey对象的示例:
// Note the following bytes are not realistic secret key data // bytes but are simply supplied as an illustration of using data // bytes (key material) you already have to build a DESedeKeySpec. byte[] desEdeKeyData = getKeyData(); DESedeKeySpec desEdeKeySpec = new DESedeKeySpec(desEdeKeyData); SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("DESede"); SecretKey secretKey = keyFactory.generateSecret(desEdeKeySpec);
在这种情况下,SecretKey的底层实现基于KeyFactory的提供者。
使用相同的密钥材料创建功能上等价的SecretKey对象的另一种独立于提供者的方法是使用实现javax.crypto.SecretKey接口的javax.crypto.spec.SecretKeySpec类:
byte[] aesKeyData = getKeyData(); SecretKeySpec secretKey = new SecretKeySpec(aesKeyData, "AES");
SecretKeyFactory对象是通过使用SecretKeyFactory getInstance()静态工厂方法之一获得的。
如果您拥有密钥的密钥规范,则可以使用generateSecret方法从规范中获取不透明的SecretKey对象:
SecretKey generateSecret(KeySpec keySpec)
keySpec标识应该返回密钥材料的规范类。 例如,它可以是DESKeySpec.class,以指示密钥材料应该在DESKeySpec类的实例中返回。
如果您有密钥对象,则可以通过调用getKeySpec方法来获取相应的密钥规范对象:
KeySpec getKeySpec(Key key, Class keySpec)
keySpec标识应该返回密钥材料的规范类。 例如,它可以是DESKeySpec.class,以指示密钥材料应该在DESKeySpec类的实例中返回。
KeyPairGenerator类是用于生成公钥和私钥对的引擎类。
有两种方法来生成密钥对:以独立于算法的方式,以特定于算法的方式。 两者之间的唯一区别是对象的初始化。
有关调用下面所述方法的示例,请参阅“示例”部分。
所有密钥对生成都以KeyPairGenerator开始。 KeyPairGenerator对象是通过使用KeyPairGenerator getInstance()静态工厂方法之一获得的。
用于特定算法的密钥对生成器创建可用于该算法的公钥/私钥对。 它还将算法特定的参数与每个生成的密钥相关联。
密钥对生成器在生成密钥之前需要进行初始化。 在大多数情况下,独立于算法的初始化就足够了。 但在其他情况下,可以使用算法特定的初始化。
所有的密钥对生成器都共享一个密钥的概念和一个随机的来源。 对于不同的算法,密钥大小的解释是不同的。 例如,在DSA算法的情况下,密钥大小对应于模数的长度。 (有关特定算法的密钥大小的信息,请参阅标准名称文档。)
初始化方法需要两个普遍共享的参数类型:
void initialize(int keysize, SecureRandom random)
另一个初始化方法只需要一个keysize参数; 它使用系统提供的随机来源:
void initialize(int keysize)
由于在您调用上述与算法无关的初始化方法时没有指定其他参数,因此提供程序应该如何处理与每个密钥关联的算法特定参数(如果有)。
如果算法是“DSA”算法,并且模数大小(密钥大小)是512,768或1024,那么SUN提供程序使用一组预先计算的p,q和g参数值。 如果模数大小不是以上值之一,则SUN提供程序会创建一组新的参数。 其他提供者可能具有预先计算的参数集,不仅仅是上面提到的三个模量大小。 还有一些可能根本没有预先计算的参数列表,而是总是创建新的参数集。
对于已经存在一组算法特定参数的情况(例如DSA中的“社区参数”),有两个具有AlgorithmParameterSpec参数的初始化方法。 一个也有一个SecureRandom参数,而随机性的来源是系统为另一个提供的:
void initialize(AlgorithmParameterSpec params, SecureRandom random) void initialize(AlgorithmParameterSpec params)
请参阅示例部分以获取更多详细信息
无论初始化(和算法)如何,生成密钥对的过程总是相同的。 您始终从KeyPairGenerator调用以下方法:
KeyPair generateKeyPair()
多次调用generateKeyPair将产生不同的密钥对。
密钥生成器用于为对称算法生成密钥。
KeyGenerator对象是通过使用KeyGenerator getInstance()静态工厂方法之一获得的。
用于特定对称密钥算法的密钥生成器创建可用于该算法的对称密钥。 它还将算法特定的参数(如果有的话)与生成的密钥相关联。
有两种方法来生成密钥:以独立于算法的方式,以特定于算法的方式。 两者之间唯一的区别是对象的初始化:
算法独立初始化
所有的密钥生成器都共享一个密钥的概念和一个随机的来源。 有一个init方法使用这两个普遍共享类型的参数。 还有一个只需要一个密钥大小的参数,并使用系统提供的随机源,以及只是一个随机源:
public void init(SecureRandom random); public void init(int keysize); public void init(int keysize, SecureRandom random);
特定于算法的初始化
对于已经存在一组特定于算法的参数的情况,有两个具有AlgorithmParameterSpec参数的init方法。 一个也有一个SecureRandom参数,而随机性的来源是系统为另一个提供的:
public void init(AlgorithmParameterSpec params); public void init(AlgorithmParameterSpec params, SecureRandom random);
如果客户端没有明确初始化KeyGenerator(通过调用init方法),则每个提供者都必须提供(并记录)一个默认的初始化。
以下方法生成一个密钥:
public SecretKey generateKey();
密钥协议是两方或多方可以建立相同密码密钥而不必交换任何秘密信息的协议。
各方用自己的私钥初始化密钥协商对象,然后输入参与通信的各方的公钥。 在大多数情况下,只有两方,但Diffie-Hellman等算法允许多方(3个或更多)参与。 当所有的公钥都被输入时,每个KeyAgreement对象都会生成(同意)相同的密钥。
KeyAgreement类提供了密钥协议协议的功能。 建立共享密钥所涉及的密钥由密钥生成器(KeyPairGenerator或KeyGenerator)之一,KeyFactory或者密钥协商协议的中间阶段的结果创建。
参与密钥协议的每一方都必须创建一个KeyAgreement对象。 KeyAgreement对象是通过使用KeyAgreement getInstance()静态工厂方法之一获得的。
您使用您的私人信息初始化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);
每个密钥协议协议都由许多阶段组成,这些阶段需要由密钥协议中涉及的每一方执行。
要执行密钥协议中的下一个阶段,请调用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。
在每一方执行了所有需要的密钥协商阶段之后,它可以通过调用一个generateSecret方法来计算共享密钥:
public byte[] generateSecret(); public int generateSecret(byte[] sharedSecret, int offset); public SecretKey generateSecret(String algorithm);
称为“密钥库”的数据库可用于管理密钥和证书的存储库。 (证书是来自一个实体的数字签名声明,表示某个其他实体的公钥具有特定的价值。)
用户密钥库默认存储在用户主目录中名为.keystore的文件中,由“user.home”系统属性确定。 在Solaris系统上,“user.home”默认为用户的主目录。 在Win32系统上,给定用户名uName,“user.home”默认为:
当然,密钥库文件可以根据需要进行定位。 在某些环境中,可能存在多个密钥库。 例如,在JSSE(SSL / TLS)中,一个密钥库可能包含用户的私钥,而另一个可能包含用于建立信任关系的证书。
除了用户的密钥仓库以外,JDK还维护一个系统范围的密钥仓库,用于存储来自各种证书颁发机构(CA)的可信证书。 这些CA证书可以用来帮助做出信任决定。 例如,在SSL / TLS中,当SunJSSE提供者被呈现来自远程对等体的证书时,缺省的信任管理者将查询:
文件来确定连接是否被信任。 应用程序可以设置和使用自己的密钥库,甚至可以使用上述的用户密钥库,而不是使用系统范围的cacerts密钥库。
KeyStore类提供了定义良好的接口来访问和修改密钥库中的信息。 有可能存在多个不同的具体实现,其中每个实现都是针对特定类型的密钥库的实现。
目前,有两个使用KeyStore:keytool和jarsigner的命令行工具,以及一个名为policytool的基于GUI的工具。 当Policy参考实现处理策略文件时,策略文件也会被使用,这些策略文件将指定权限(允许访问系统资源)授予各种来源的代码。 由于KeyStore是公开的,因此JDK用户可以编写使用它的其他安全性应用程序。
应用程序可以使用KeyStore类中的getInstance工厂方法,从不同的提供者中选择不同类型的密钥库实现。 密钥库类型定义密钥库信息的存储和数据格式,以及用于保护密钥库中的私钥和密钥库本身的完整性的算法。 不同类型的密钥库实现不兼容。
推荐的keystore实现是“pkcs12”。 这是基于RSA PKCS12个人信息交换语法标准的跨平台密钥库。 此标准主要用于存储或传输用户的私钥,证书和其他秘密。 任意属性可以与PKCS12密钥库中的单个条目相关联。
默认的keystore实现类型是“jks”,它在java.security文件中的以下行中指定:
keystore.type=jks
要使工具和其他应用程序使用不同的默认密钥库实现,可以更改该行以指定另一个默认类型。 例如,要使用“pkcs12”作为默认密钥库实现,请将该行更改为:
keystore.type=pkcs12
某些应用程序(如keytool)也可以覆盖默认的密钥库类型(通过-storetype命令行参数)。
注:密钥库类型标识不区分大小写。 例如,“jks”将被视为与“JKS”相同。
JDK实现有两个其他类型的密钥库。
“jceks”是替代的专有密钥库格式,用于使用基于密码的三重DES进行加密的“jks”。
Sun的“jceks”实现可以解析并将“jks”密钥库文件转换为“jceks”格式。 您可以通过更改密钥库中私钥条目的密码并指定-storetype jceks作为密钥库类型,将类型“jks”的密钥库升级到类型为“jceks”的密钥库。 要在默认密钥库中应用提供给名为“signkey”的私钥的加密强(er)密钥保护,请使用以下命令,该命令将提示您输入旧密钥和新密钥密码:
keytool -keypasswd -alias signkey -storetype jceks
有关keytool和有关密钥库及其管理方式的更多信息,请参阅[安全工具](https://docs.oracle.com/javase/8/docs/technotes/tools/index.html#security)。
– “dks”是一个域密钥库。 它是作为单个逻辑密钥库呈现的密钥库的集合。 组成给定域的密钥库由配置数据指定,其语法在DomainLoadStoreParameter中进行了描述。
密钥库实现是基于提供者的。 对编写他们自己的KeyStore实现感兴趣的开发人员应该参考如何为Java密码体系结构实现一个提供程序来获得关于这个主题的更多信息。
KeyStore类是一个引擎类,提供良好定义的接口来访问和修改密钥库中的信息。
这个类表示内存中的密钥和证书集合。 KeyStore管理两种类型的条目:
密钥库中的每个条目都由“别名”字符串标识。 在私钥及其相关证书链的情况下,这些字符串区分实体可以对其自身进行验证的不同方式。 例如,实体可以使用不同的证书颁发机构或使用不同的公钥算法来验证自己。
密钥库是否是持久的,以及密钥库使用的机制是否持久,这里没有指定。 这个约定允许使用各种技术来保护敏感(例如私人或秘密)密钥。 智能卡或其他集成密码引擎(SafeKeyper)是一种选择,并且也可以使用诸如文件的更简单的机制(以各种格式)。
主要的KeyStore方法如下所述。
KeyStore对象是通过使用KeyStore getInstance()静态工厂方法之一获得的。
在可以使用KeyStore对象之前,必须通过load方法将实际的密钥库数据加载到内存中:
final void load(InputStream stream, char[] password)
可选的password参数用于检查密钥库数据的完整性。 如果没有提供密码,则不执行完整性检查。
要创建一个空的密钥库,可以将null作为InputStream参数传递给load方法。
通过将DomainLoadStoreParameter传递给备用加载方法来加载DKS密钥库:
final void load(KeyStore.LoadStoreParameter param)
所有密钥库条目都通过唯一的别名来访问。 别名方法返回密钥库中别名的枚举:
final Enumeration aliases()
如KeyStore类所述,密钥库中有两种不同类型的条目。 以下方法分别确定由给定别名指定的条目是密钥/证书还是受信任的证书条目:
final boolean isKeyEntry(String alias) final boolean isCertificateEntry(String alias)
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)
在key作为字节数组的方法中,它是受保护格式的密钥的字节。 例如,在SUN提供程序提供的密钥库实现中,密钥字节数组需要包含一个受保护的私钥,该私钥被编码为PKCS8标准中定义的EncryptedPrivateKeyInfo。 在另一种方法中,密码是用于保护密钥的密码。
deleteEntry方法删除一个条目:
final void deleteEntry(String alias)
PKCS#12密钥库支持包含任意属性的条目。 使用java.security.PKCS12Attribute类来创建属性。 在创建新的密钥库条目时,使用接受属性的构造函数方法。 最后,使用以下方法将条目添加到密钥库:
final void setEntry(String alias, Entry entry, ProtectionParameter protParam)
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接口的方法来检查它们。
内存中的密钥库可以通过存储方法保存:
final void store(OutputStream stream, char[] password)
密码用于计算密钥库数据的完整性校验和,并将其附加到密钥库数据。
通过将DomainLoadStoreParameter传递给替代存储方法来存储DKS密钥库:
final void store(KeyStore.LoadStoreParameter param)
与Keys和Keyspec类似,算法的初始化参数由AlgorithmParameters或AlgorithmParameterSpecs表示。 根据使用情况,算法可以直接使用这些参数,或者可能需要将这些参数转换为更便携的格式以用于传输或存储。
一组参数的透明表示(通过AlgorithmParameterSpec)意味着您可以单独访问集合中的每个参数值。 您可以通过相应规范类中定义的某个get方法(例如,DSAParameterSpec定义getP,getQ和getG方法,分别访问p,q和g)来访问这些值。
相反,AlgorithmParameters类提供了一个不透明的表示形式,其中不能直接访问参数字段。 你只能得到与参数集相关的算法的名字(通过getAlgorithm)和参数集的某种编码(通过getEncoded)。
AlgorithmParameterSpec是加密参数的透明规范的接口。 这个接口不包含方法或常量。 其唯一的目的是为所有参数规格进行分组(并提供类型安全性)。 所有的参数规格必须实现这个接口。
JDK javadoc中描述了java.security.spec和javax.crypto.spec包中的算法参数说明接口和类:
以下算法参数规范专门用于数字签名,作为JSR 105的一部分。
AlgorithmParameters类是提供密码参数的不透明表示的引擎类。 您可以使用特定的AlgorithmParameterSpec对象初始化AlgorithmParameters类,或者通过以可识别的格式对参数进行编码。 您可以使用getParameterSpec方法检索结果规范(请参阅以下部分)。
AlgorithmParameters对象是通过使用AlgorithmParameters getInstance()静态工厂方法之一获得的。
一旦AlgorithmParameters对象被实例化,它必须通过调用init初始化,使用适当的参数规范或参数编码:
void init(AlgorithmParameterSpec paramSpec) void init(byte[] params) void init(byte[] params, String format)
在这些init方法中,params是包含编码参数的数组,format是解码格式的名称。 在带有params参数但不带格式参数的init方法中,使用参数的主要解码格式。 如果存在参数的ASN.1规范,则主要的解码格式是ASN.1。
注意:AlgorithmParameters对象只能被初始化一次。 它们不可重用。
AlgorithmParameters对象中表示的参数的字节编码可以通过调用getEncoded来获得:
byte[] getEncoded()
此方法返回主编码格式的参数。 如果存在这种类型参数的ASN.1规范,参数的主要编码格式是ASN.1。
如果您想要以指定的编码格式返回参数,请使用
byte[] getEncoded(String format)
如果格式为空,则使用参数的主要编码格式,就像其他getEncoded方法一样。
注:在由SUN提供程序提供的默认AlgorithmParameters实现中,format参数当前被忽略。
算法参数的透明参数规范可以通过调用getParameterSpec从AlgorithmParameters对象获得:
AlgorithmParameterSpec getParameterSpec(Class paramSpec)
paramSpec标识了参数应该返回的规范类。 例如,规范类可以是DSAParameterSpec.class,以指示参数应该在DSAParameterSpec类的实例中返回。 (这个类在java.security.spec包中。)
AlgorithmParameterGenerator类是一个引擎类,用于生成一组适用于特定算法的全新参数(该算法在创建AlgorithmParameterGenerator实例时指定)。 如果没有现有的一组算法参数,并且想从头开始生成,则使用此对象。
AlgorithmParameterGenerator对象通过使用AlgorithmParameterGenerator getInstance()静态工厂方法之一获得。
AlgorithmParameterGenerator对象可以用两种不同的方式进行初始化:独立于算法的方式或特定于算法的方式。
独立于算法的方法使用所有参数生成器共享“大小”和随机源的概念。 对于不同的算法来说,大小的度量是所有算法参数所共有的,尽管它们的解释是不同的。 例如,在用于DSA算法的参数的情况下,“大小”对应于质量模数的大小,以位为单位。 (有关特定算法大小的信息,请参阅“标准名称”文档。)使用此方法时,算法特定的参数生成值(如果有的话)默认为某些标准值。 一个采用这两个普遍共享类型的参数的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系统参数,例如,参数生成值通常由质数的大小和随机指数的大小组成,二者均以位数指定。
一旦创建并初始化AlgorithmParameterGenerator对象,就可以使用generateParameters方法生成算法参数:
AlgorithmParameters generateParameters()
CertificateFactory类是定义证书工厂功能的引擎类,用于从其编码生成证书和证书吊销列表(CRL)对象。
X.509的证书工厂必须返回作为java.security.cert.X509Certificate实例的证书和作为java.security.cert.X509CRL实例的CRL。
CertificateFactory对象是通过使用getInstance()静态工厂方法之一获得的。
要生成证书对象并使用从输入流读取的数据进行初始化,请使用generateCertificate方法:
final Certificate generateCertificate(InputStream inStream)
要返回从给定输入流中读取的证书(可能为空)的集合视图,请使用generateCertificates方法:
final Collection generateCertificates(InputStream inStream)
要生成证书吊销列表(CRL)对象并使用从输入流中读取的数据进行初始化,请使用generateCRL方法:
final CRL generateCRL(InputStream inStream)
要返回从给定输入流读取的CRL的(可能是空的)集合视图,请使用generateCRLs方法:
final Collection generateCRLs(InputStream inStream)
PKIX的证书路径生成器和验证器由Internet X.509公钥基础结构证书和CRL配置文件RFC 3280定义。
用于从Collection和LDAP目录中检索证书和CRL的证书存储实现(使用PKIX LDAP V2架构)也可以从IETF以RFC 2587形式获得。
要生成CertPath对象并使用从输入流中读取的数据对其进行初始化,请使用以下一种generateCertPath方法(带或不带指定要用于数据的编码):
final CertPath generateCertPath(InputStream inStream) final CertPath generateCertPath(InputStream inStream, String encoding)
要生成CertPath对象并使用证书列表对其进行初始化,请使用以下方法:
final CertPath generateCertPath(List certificates)
要检索此证书工厂支持的CertPath编码列表,可以调用getCertPathEncodings方法:
final Iterator getCertPathEncodings()
默认编码将首先列出。
了解JCA类后,可以考虑如何组合这些类来实现像SSL / TLS这样的高级网络协议。 “JSSE参考指南”中的“SSL / TLS概述”部分从高层次描述了协议的工作原理。 由于不对称(公钥)密码操作比对称操作(密钥)慢得多,所以使用公钥密码术来建立密钥,然后用它来保护实际的应用数据。 非常简单,SSL / TLS握手包括交换初始化数据,执行一些公钥操作以获得密钥,然后使用该密钥来加密进一步的通信量。
注意:这里提供的细节只是简单地展示了如何使用上面的一些类。 本部分不会提供足够的信息来构建SSL / TLS实施。 有关更多详细信息,请参阅“JSSE参考指南”和RFC 2246:“TLS协议”。
假设这个SSL / TLS实现将作为JSSE提供者提供。 首先编写Provider类的具体实现,最终将在Security类的提供者列表中注册。 这个提供者主要提供从算法名称到实际实现类的映射。 (即:“SSLContext.TLS” – >“com.foo.TLSImpl”)当应用程序请求一个“TLS”实例(通过SSLContext.getInstance(“TLS”))时,将根据请求的算法查询提供者的列表, 创建一个适当的实例。
在讨论实际握手的细节之前,需要快速回顾一些JSSE的体系结构。 JSSE体系结构的核心是SSLContext。 上下文最终创建实际实现SSL / TLS协议的结束对象(SSLSocket和SSLEngine)。 SSLContexts使用两个回调类KeyManager和TrustManager进行初始化,它们允许应用程序首先选择要发送的验证资料,然后再验证对等方发送的凭证。
JSSE KeyManager负责选择向对等体显示哪些凭据。 许多算法是可能的,但是常见的策略是在由磁盘文件支持的KeyStore中维护RSA或DSA公钥/私钥对以及X509Certificate。 当从文件初始化并加载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对象。 我们将在下面的例子中使用服务器唯一身份验证。
在第一个示例中,服务器尝试使用基于RSA的密码组,例如TLS_RSA_WITH_AES_128_CBC_SHA。 查询服务器的KeyManager,并返回相应的RSA条目。 服务器的凭证(即:证书/公钥)将在服务器的证书消息中发送。 客户端的TrustManager验证服务器的证书,如果接受,客户端使用SecureRandom对象生成一些随机字节。 然后使用已使用在服务器证书中找到的PublicKey初始化的加密非对称RSA密码对象对其进行加密。 此加密数据在客户端密钥交换消息中发送。 服务器将使用其相应的PrivateKey在解密模式下使用类似的密码恢复字节。 这些字节然后用于建立实际的加密密钥。
一旦建立了真实的加密密钥,秘密密钥就被用来初始化一个对称的密码对象,并且这个密码被用来保护所有传输中的数据。 为了帮助确定数据是否已被修改,创建MessageDigest并接收发往网络的数据的副本。 当数据包完成时,摘要(哈希)被附加到数据,并且整个数据包被密码加密。 如果使用诸如AES的分组密码,则必须填充数据以形成完整的块。 在另一端,这些步骤简单地颠倒过来。
再一次,这是非常简单的,但给了一个想法,这些类可能被结合起来创建一个更高层次的协议。
注1:大多数应用程序开发人员应该忽略此部分。 只有那些申请可能被出口到那些政府要求加密限制的少数几个国家的人,如果希望这样的申请比那些授权有更少的加密限制的话。
注2:在本节中,术语“应用程序”意味着包含应用程序和小程序。]
由于受到少数国家政府的进口管制限制,Java SE Development Kit 6附带的管辖权政策文件规定可以使用“强”但有限的密码术。 这些文件的“无限强度”版本对于那些生活在符合条件的国家(这是大多数国家)中的加密强度没有限制。 但只有“强”版本才可以进入政府限制的国家。 JCA框架将强制执行已安装辖区策略文件中指定的限制。
一些或所有这些国家的政府有可能允许某些应用程序免于某些或全部密码限制。 例如,他们可能将某些类型的申请视为“特殊”,从而免除。 或者,他们可以免除任何利用“免除机制”的申请,如关键的恢复。 被视为豁免的应用程序可以获得比这些国家允许的非豁免应用程序更强大的加密技术。
为了使应用程序在运行时被识别为“免除”,它必须满足以下条件:
以下是为使应用程序免于某些或所有加密限制所需的示例步骤。 这是一个基本概要,其中包含有关JCA为了将应用程序识别和处理为豁免所需的信息。 您需要知道您希望您的应用程序能够运行的特定国家或国家的豁免要求,但其政府需要加密限制。 您还需要了解具有处理免除应用程序的JCA框架供应商的要求。 请咨询这样的供应商获取更多信息。 (注意:SunJCE提供程序不提供ExemptionMechanismSpi类的实现。)
当应用程序具有与其关联的权限策略文件(在同一个JAR文件中),并且该权限策略文件指定了一个豁免机制时,则当调用Cipher getInstance方法来实例化一个Cipher时,JCA代码会搜索已安装的提供程序 实行指定的豁免机制。 如果找到这样的提供者,JCA实例化一个与提供者的实现相关联的ExemptionMechanism API对象,然后将ExemptionMechanism对象与getInstance返回的Cipher关联起来。
实例化密码之后,在初始化之前(通过调用Cipher init方法),代码必须调用以下Cipher方法:
public ExemptionMechanism getExemptionMechanism()
该调用返回与密码关联的ExemptionMechanism对象。 您必须通过在返回的ExemptionMechanism上调用以下方法来初始化豁免机制实现:
public final void init(Key key)
您提供的参数应该与随后提供给Cipher init方法的相同类型的参数相同。
一旦你初始化了ExemptionMechanism,你可以照常进行初始化和使用密码。
为了使应用程序在运行时被识别为免除某些或所有加密限制,它必须在JAR文件中绑定一个许可策略文件。 权限策略文件指定应用程序具有什么与密码相关的权限,以及在什么条件下(如果有的话)。
注意:与应用程序捆绑在一起的权限策略文件必须命名为cryptoPerms。
伴随免除申请的权限策略文件中的权限条目的格式与用JDK下载的权限策略文件的格式相同,即:
permission <crypto permission class name>[ <alg_name> [[, <exemption mechanism name>][, <maxKeySize> [, <AlgorithmParameterSpec class name>, <parameters for constructing an AlgorithmParameterSpec object> ]]]];
有关管辖权政策文件格式的更多信息,请参阅附录B.
有些应用程序可能被允许完全不受限制。 因此,这种应用程序所附带的许可策略文件通常只需要包含以下内容:
grant { // There are no restrictions to any algorithms. permission javax.crypto.CryptoAllPermission; };
如果一个应用程序只使用一个算法(或几个特定的算法),那么许可策略文件可以简单地提及该算法(或多个算法),而不是授予CryptoAllPermission。 例如,如果应用程序只使用Blowfish算法,则权限策略文件不必为所有算法授予CryptoAllPermission。 如果使用Blowfish算法,它可以指定没有密码限制。 为此,权限策略文件如下所示:
grant { permission javax.crypto.CryptoPermission "Blowfish"; };
如果一个申请被认为是“豁免”,如果豁免机制被强制执行,则申请附带的权限策略文件必须指定一个或多个免责机制。 在运行时,如果任何这些豁免机制被强制执行,应用程序将被视为免除。 每个豁免机制必须在如下所示的许可条目中指定:
// No algorithm restrictions if specified // exemption mechanism is enforced. permission javax.crypto.CryptoPermission *, "<ExemptionMechanismName>";
其中指定豁免机制的名称。 可能的豁免机制名称清单包括:
例如,假设您的应用程序在执行密钥恢复或密钥托管时是免除的。 那么你的权限策略文件应该包含以下内容:
grant { // No algorithm restrictions if KeyRecovery is enforced. permission javax.crypto.CryptoPermission *, "KeyRecovery"; // No algorithm restrictions if KeyEscrow is enforced. permission javax.crypto.CryptoPermission *, "KeyEscrow"; };
注:指定免除机制的权限条目不应指定最大密钥大小。 允许的密钥大小实际上是从安装的豁免管辖权策略文件中确定的,如下一节所述。
在运行时,当应用程序实例化密码(通过调用其getInstance方法)并且该应用程序具有关联的权限策略文件时,JCA将检查权限策略文件是否包含适用于getInstance调用中指定的算法的条目。如果是这样,并且条目授予CryptoAllPermission或不指定必须执行豁免机制,则意味着对于此特定算法没有密码限制。
如果权限策略文件具有适用于getInstance调用中指定的算法的条目,并且该条目指定必须执行豁免机制,则会检查豁免管辖权策略文件。如果豁免权限包括相关算法和豁免机制的条目,并且该条目被与该应用程序捆绑在一起的权限策略文件中的权限所暗示,并且如果存在从已注册的一个注册机构提供者,则密码的最大密钥大小和算法参数值由免除权限条目确定。
如果没有与应用程序绑定的许可策略文件中的相关条目暗含的免除许可条目,或者没有实施任何注册提供商提供的指定免除机制,则应用程序只允许标准默认加密权限。
示例代码都是一些代码例子,就不一一列举,感兴趣的可以去官网查看。
JDK安全API要求并使用一组用于算法,证书和密钥库类型的标准名称。 之前在附录A和其他安全规范(JSSE / CertPath / etc)中找到的规范名称已经在“标准名称”文档中进行了组合。 本文档还包含有关算法规范的更多信息。 Sun提供程序文档中可以找到具体的提供程序信息。
JDK中的加密实现主要是由于历史原因(Sun,SunJSSE,SunJCE,SunRsaSign)通过几个不同的提供者分发的。 请注意,这些提供程序可能不适用于所有JDK实现,因此,真正的可移植应用程序应该调用getInstance()而不指定特定的提供程序。 指定特定提供程序的应用程序可能无法利用为底层操作环境(如PKCS或Microsoft的CAPI)而调优的本机提供程序。
SunPKCS11提供程序本身不包含任何加密算法,而是将请求指向基础PKCS11实现。 应参考“PKCS11参考指南”和基础PKCS11实施,以确定是否可通过PKCS11提供商获得所需的算法。 同样,在Windows系统上,SunMSCAPI提供程序不提供任何加密功能,而是将请求路由到底层操作系统进行处理。
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>]]]];
包括将“Blowfish”算法限制为最大密钥大小为64位的示例权限策略文件是:
grant { permission javax.crypto.CryptoPermission "Blowfish", 64; // ... };
权限条目必须以单词权限开头。 上面模板中的实际上是一个特定的权限类名,例如javax.crypto.CryptoPermission。 加密权限类反映了应用程序/ applet在特定环境中使用特定密钥大小的某些算法的能力。 有两个加密权限类:CryptoPermission和CryptoAllPermission。 特殊的CryptoAllPermission类意味着所有与密码相关的权限,即它指定没有密码相关的限制。
使用时,是一个带引号的字符串,用于指定加密算法(如“AES”或“RSA”)的标准名称(请参阅附录A)。
指定时,<豁免机制名称>是带引号的字符串,表示免除机制,如果强制执行,则可以减少加密限制。 可以使用的豁免机制名称包括“KeyRecovery”,“KeyEscrow”和“KeyWeaking”。
是一个整数,指定指定算法允许的最大密钥大小(以位为单位)。
对于一些算法来说,仅仅根据密钥大小来指定算法强度可能是不够的。 例如,在“RC5”算法的情况下,还必须考虑回合的数量。 对于其强度需要表示为大于关键字大小的算法,权限条目还应指定AlgorithmParameterSpec类名(例如javax.crypto.spec.RC5ParameterSpec)以及用于构造指定AlgorithmParameterSpec对象的参数列表。
出现在权限条目中的项目必须以指定的顺序出现。 一个条目以分号结尾。
大小写对于标识符(grant,permission)来说并不重要,但对于或者作为值传入的任何字符串都是重要的。
注意:“ ”可以用作任何权限输入选项的通配符。 例如,对于选项,“ ”(不带引号)表示“所有算法”。
由于进口控制限制,Java SE开发工具包附带的权限策略文件允许使用“强大”但有限的加密技术。 有关更多信息,请参阅加密算法的进口限制。
附录D也是有关算法使用和操作方法的代码示例,不再一一列举,感兴趣可以去官网查看。
来源:https://blog.csdn.net/u012741741/article/details/79209984