解决 java.security.UnrecoverableKeyException: Cannot Recover Key

1、简介

本文将带你了解如 java.security.UnrecoverableKeyException 异常出现的原因以及如何解决该异常。

2、背景

在 Java 中,有一个 Keystore 的概念。它本质上是一个包含一些 secret 的文件。它可以包含证书链以及与之对应的私钥。由于证书只是一个带有公钥的 包装器,我们可以简单地说 Keystore 包含了一对非对称密钥。

通常,用密码(“password ”通 常也称为 “passphrase”)保护私钥是一种很好的做法。这不仅是 Java Keystore 的良好做法,也是网络安全的一般做法。实现这种保护的方法通常是使用对称密钥加密算法(如各种 AES 实例)对私钥和密码进行加密。

在这里对我们来说需要注意的是,Keystore 中的私钥可以使用密码进行加密,如上所述。这个特性并不是所有类型的 Keystore 都支持,例如,JKS Keystore 支持私钥密码保护,但 PKCS12 Keystore 不支持。在我们的示例中,我们需要密码保护功能,因此我们使用 JKS Keystore。

3、UnrecoverableKeyException

java.security.UnrecoverableKeyException 通常发生在使用 KeyManagerFactory 时,特别是调用 init() 方法时。这是 JSSE 中的一个类,允许我们检索 KeyManager 实例。KeyManager 是一个接口,它代表了一个抽象概念,负责将我们作为客户端向对等方进行身份验证。

init() 方法需要两个参数 - 用于获取认证凭证的 Keystore 和用于私钥解密的密码。当 KeyManagerFactory 无法恢复证书链的私钥时,就会出现 java.security.UnrecoverableKeyException 异常。问题来了 - UnrecoverableKeyException 中的 “recover” 到底是什么意思?这意味着证书链的私钥无法用给定的密码解密。因此,java.security.UnrecoverableKeyException 最常见的原因是 Keystore 中的私钥密码错误。

总之,如果为 KeyManagerFactory 提供的私钥密码/口令不正确,那么 KeyManagerFactory 将无法解密密钥,因此会出现此异常。

4、模拟异常

来模拟一下这个异常。使用 keytool 来创建一个带有私钥和相应证书对的 JKS Keystore:

$ keytool -genkey -alias single-entry -storetype JKS -keyalg RSA -validity 365 -keystore single_entry_keystore.jks

运行该命令后,Keytool 会提示我们提供一些附加信息。在本例中,就是生成证书的一些附加信息(CN、有效期等),以及 Keystore 和私钥的密码。假设我们为 keystore 设置了 “admin123” 密码,为私钥设置了 “privateKeyPassword” 口令。

在 Java 中加载此 keystore:

public static X509ExtendedKeyManager initializeKeyManager() 
  throws NoSuchAlgorithmException, KeyStoreException, IOException, CertificateException, UnrecoverableKeyException, URISyntaxException {
    KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
    KeyStore instance = KeyStore.getInstance(KeyStore.getDefaultType());
    InputStream resourceAsStream = Files.newInputStream(Paths.get(ClassLoader.getSystemResource("single_entry_keystore.jks").toURI()));
    instance.load(resourceAsStream, "admin123".toCharArray());
    kmf.init(instance, "privateKeyPassword".toCharArray());
    return (X509ExtendedKeyManager) kmf.getKeyManagers()[0];
}

从刚创建的 Keysotre 中获取了 KeyManager 的实例。由于两个密码都是正确的,所以这段代码运行正常。如果我们更改了上例中私钥的密码,就会出现 java.security.UnrecoverableKeyException 异常。因此,只要使用正确的密码就能解决这个问题

5、UnrecoverableKeyException 的其他案例

有一些可能导致 java.security.UnrecoverableKeyException 的特殊情况,大多数人都没有注意到。

5.1、多个 Private Key 条目

举个例子,假设我们的 Keysotre 有多个私钥/证书链。

创建一个新的 Keysotre,其中包含两个密钥:

$ keytool -genkey -alias entry-1 -storetype JKS -keyalg RSA -validity 365 -keystore multi_entry_keystore.jks
$ keytool -genkey -alias entry-2 -storetype JKS -keyalg RSA -validity 365 -keystore multi_entry_keystore.jks

如上,在 Keysotre 中添加了两个带有证书条目的私钥。假设第一个私钥的密码为 “abc123”,第二个私钥的密码为 “bcd456”。这应该完全没问题,毕竟 Keysotre 可以有多个用不同密码加密的密钥,目前还没有问题。

现在,为给定 Keysotre 构建 KeyManager 的代码不变。唯一的问题是,KeyManagerFactory.init() 方法只接受一个用于私钥解密的密码

这看起来很奇怪 - 我们到底应该提供什么密码 - “abc123”还是 “bcd456”?原来,绝大多数 JDK 都使用了 Sun JSSEKeyManagerFactory 实现,默认情况下,Keystore 中的每个私钥都需要一个密码。没错,尽管在 Keystore 中使用不同密码加密的两个私钥在技术上并不是问题。从理论上讲,没有任何限制;但从 API 的角度看,却有限制。

我们不能为 Keystore 中的任何两个给定密钥设置不同的密码。如果违反了这一规则,KeyManagerFactory 的实现就会尝试用提供的密码解密所有密钥,这至少有一个密钥会解密失败。因此,KeyManagerFactory.init() 会抛出异常,因为它无法解密密钥,必须牢记这一点。

5.2、外部库的限制

我们通常不会直接与 JSSE 进行交互。框架通常通过为我们创建多层抽象来隐藏这一点。不过,我们仍然可以通过使用各种客户端(如 Apache HTTP 客户端或 Apache Tomcat)与 KeyManager 和其他 JSSE 类间接交互。

这些框架通常对它们期望的密码施加各种限制。例如,当前的 Apache Tomcat 实现 依赖于 Keystore 密码与 Keystore 中私钥的密码相等。这些限制可能因库而异,但是现在,我们已经理解了 java.security.UnrecoverableKeyException 的原因,出现这个异常后我们就可以知道这么去排查。

6、总结

在本文介绍了在 Java 中使用 Keystore 时 KeyManagerFactory 抛出 java.security.UnrecoverableKeyException 异常的原因以及解决办法。


Ref:https://www.baeldung.com/java-security-unrecoverablekeyexception-resolve