【问题标题】:How to destroy SecretKey in Java 14?如何在 Java 14 中销毁 SecretKey?
【发布时间】:2021-02-20 01:23:48
【问题描述】:

我试图在解密后清除我的Secretkey。 根据我的阅读,SecretKeys 自 Java 8 以来可以通过 destroy 方法销毁。 我使用的是 Java 14,所以应该可以。

但是,每当我在键上使用 destroy 方法时,都会抛出 DestroyFailedException。 我还看到人们在他们的代码中忽略了该异常,但是,如果我这样做,我可以在调用 destroy 方法后打印密钥。

这里是我的解密方法:

private byte[] decrypt(byte[] encryptedText, char[] password) throws InvalidKeyException,
        InvalidAlgorithmParameterException, NoSuchAlgorithmException, NoSuchPaddingException,
        InvalidKeySpecException, IllegalBlockSizeException, BadPaddingException, DestroyFailedException {

    ByteBuffer bb = ByteBuffer.wrap(encryptedText);

    byte[] iv = new byte[ivLengthByte];
    bb.get(iv);

    byte[] salt = new byte[saltLengthByte];
    bb.get(salt);

    byte[] cipherText = new byte[bb.remaining()];
    bb.get(cipherText);

    SecretKey key;
    key = crypto.getAESKeyFromPassword(password, salt);

    Cipher cipher;
    cipher = Cipher.getInstance(algorithm);

    cipher.init(Cipher.DECRYPT_MODE, key, new GCMParameterSpec(tagLengthBit, iv));

    byte[] plainText = cipher.doFinal(cipherText);

    Main.clearArray(password, null);
    Main.clearArray(null, iv);
    Main.clearArray(null, salt);
    Main.clearArray(null, cipherText);

    key.destroy();

    cipher = null;

    return plainText;

}

在调用destroy方法后,如前所述,我(假设我忽略异常)能够通过String encodedKey = Base64.getEncoder().encodeToString(key.getEncoded()); 打印密钥;

编辑: 在数组上使用我的 Clear 方法后,我仍然可以打印它:

byte[] temp = key.getEncoded();
        Main.clearArray(null, temp);

清空数组:

protected static void clearArray(char[] chars, byte[] bytes) {
    if (chars != null) {
        for (int i = 0; i < chars.length; i++) {
            chars[i] = '\0';
        }

    }
    if (bytes != null) {
        for (int i = 0; i < bytes.length; i++) {
            bytes[i] = 0;
        }

    }

}

获取AESKey:

protected SecretKey getAESKeyFromPassword(char[] password, byte[] salt)
        throws NoSuchAlgorithmException, InvalidKeySpecException {

    SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA512");

    KeySpec spec = new PBEKeySpec(password, salt, 65536, 256);
    SecretKey secret = new SecretKeySpec(factory.generateSecret(spec).getEncoded(), "AES");

    return secret;

}

最终编辑:

最好的解决方案是将 frim PBKDF2 切换到 argon2。 https://github.com/kosprov/jargon2-api Argon2 允许使用原始哈希,然后您可以将该字节数组存储在如上所述的 SecureKeySpec 中,因为它允许销毁 Spec,并清除原始哈希数组。

【问题讨论】:

  • 我们需要粘贴您的加密类 - 特别是 getAESKeyFromPassword 方法。
  • 此外,这是在毫无意义的安全剧院中进行的练习。只有两个选项:[1] 您信任您的进程内存,在这种情况下这无关紧要,或者 [2] 您不信任,在这种情况下,任何时候的任何解密都是安全漏洞,您应该确保您的进程内存是安全的。如果这不是一个选项,请使用安全飞地(这在 Java 应用程序中很难做到)。这可以防止“我的进程内存现在是安全的,但以后可能不会”。给我写一个詹姆斯邦德式的情节,你就是这样被黑的,我敢打赌这完全不现实。
  • 据我所知key.getEncoded 返回一个新数组,清除它毫无意义。可能需要清除密码数组和getAESKeyFromPassword方法创建的数组
  • 关键是它仍然按原样存储字节,在该步骤中没有混淆。您可以尽快清理它,但任何内存转储确实会清楚地显示您的密钥。默认情况下,它也将保留在内存中,直到下一次 GC。不可能从内存转储中隐藏它,并且密钥很容易找到,只需搜索那些方法名称。在这一点上,你可能会通过混淆代码获得更快的解决方法,而不是尝试实现不可能的事情。
  • 我添加了getAESKeyFromPassword,它不返回一个数组,而是一个SecretKey

标签: java encryption destroy secret-key aes-gcm


【解决方案1】:

实际上,没有简单的解决方案。问题是destroy 方法是一种“可选”方法。并非SecretKey 的所有实现都实现了它。如果你使用的 SecretKey 类型没有实现该方法,你会得到这个异常并且没有简单的解决方案。

不幸的是,您不能只自己实现该方法,因为(通常)它所属的类是由 Java SE 库提供的。

即使你知道如何销毁密钥,也存在String包含密码1的问题。 (而且这个问题更多的是安全风险,因为搜索包含密码的String 可能比搜索未知字节序列更容易。)

选项:

  1. 忘记问题。不要破坏他们在内存中的密钥/密码。 (请参阅下文,了解为什么这不像听起来那么糟糕。)

  2. 寻找替代的 JSSE 加密库,其中 AES 密钥的 SecretKey 实现确实实现了 destroy。我猜 Bouncy Castle 图书馆可能。 (如果他们不这样做,您总是可以选择下载源代码并修补它们。)

  3. 令人讨厌的反射。您可以找出哪个实际类实现了密钥,并查看其代码以了解它在内部如何表示密钥。然后您可以使用反射来打破抽象并访问其私有状态并...在密钥上写入零。


为什么不销毁密钥不是灾难?

所以一些安全专家可能不同意这一点,但我仍然认为这是一个有效的观点。

当您将内存中的密钥或密码归零时,您(表面上)可以防止以下类型的攻击:

  • 将 Java 调试器附加到 JVM 进程并使用它来定位和读取密钥。
  • 读取 JVM 进程内存。
  • 读取已写入磁盘的内存页。

这些攻击有多容易?前两个要求黑客已经进入主机并升级到(可能)root权限。在第三种情况下,您可以这样做,但黑客也可以窃取写入交换页面的硬盘。

在所有情况下,黑客都必须找到密钥。与(例如)C / C++ 程序不同,密钥不会存储在固定位置。相反,黑客必须通过模式匹配或查找参考链来找到它。 (如果密钥对象仍然可以访问,Java 调试器会更容易。)另一方面,一旦密钥被垃圾收集,内存中的副本将消失,而交换中的副本将在下一个操作系统写出关键对象曾经存在的(现在)脏页的时间。在那之后......出于所有实际目的,它已经“消失”了。

所以倒带一点。我说为了发动这种攻击,黑客已经需要root权限了。 (或者硬盘驱动器,这很可能相当于同一件事。)现在,如果他们拥有它,他们还有其他方法可以窃取密钥。例如:

  • 使用调试器在(比如)destroy 方法上设置断点,并在其被销毁之前获取密钥。

  • 在创建密钥之前使用调试器捕获密码。

  • 窃取服务器 SSL 证书(或其他)的私钥,以便他们可以从网络流量中获取密码。

  • 安装软件按键记录器。

  • 将您的应用程序代码替换为通过某个侧通道泄露密钥或密码的版本。

当然,他们可以安装后门等。简而言之,如果黑客已经将系统破坏到了对 JVM 发起“从内存中窃取内容”攻击所需的程度,那么这可能是你最不重要的担心。

现在,安全专家可能会说,对黑客进行分层防御是“最佳实践”。这是有道理的。但是,如果安全对您来说很重要,您应该进行适当的安全分析(而不仅仅是“打勾”审计)并找出真正的风险是什么。这将(可能2)告诉您,与其担心某人(具有 root 权限)是否可以从内存中窃取密钥,不如专注于使 系统 安全。


1 - 虽然不是你的情况,因为我看到你正在使用 char[] ... 可以清除。除了这仍然容易受到我所说的所有其他攻击。
2 - 或者它不会。但是你需要做分析!

【讨论】:

  • 反思...我喜欢它
  • 有道理。我在这里回复了我的问题,介意看看吗?否则我不知道是否还有其他方法可以散列密码并返回字节数组。
【解决方案2】:

你必须自己实现destroy方法。文档对此进行了解释。

https://docs.oracle.com/javase/8/docs/api/javax/crypto/SecretKey.html

【讨论】:

  • 我明白了,所以我会,例如将其转换为字节数组,然后清除该数组?
  • 嗯,我做到了,但我仍然能够打印密钥,然后我在主帖中添加了我所做的
【解决方案3】:

我可能已经找到了解决方案,我尝试使用这个:https://github.com/dbsystel/SecureSecretKeySpec

唯一的问题是键必须是字节数组,并且这样做:

protected SecureSecretKeySpec getAESKeyFromPassword(char[] password, byte[] salt)
        throws NoSuchAlgorithmException, InvalidKeySpecException {

    SecretKeyFactory factory= SecretKeyFactory.getInstance("PBKDF2WithHmacSHA512");

    KeySpec spec = new PBEKeySpec(password, salt, 65536, 256);

    byte[] temp = factory.generateSecret(spec).getEncoded();

    SecureSecretKeySpec sec= new SecureSecretKeySpec(temp, "AES");

    Main.clearArray(null, temp);

    return sec;

}

可能不好,因为SecretKey上调用了getEncoded,所以内存中可能有SecretKey?

【讨论】:

  • “可能不太好......内存中还有一个普通的密钥” - 是的。会有的。
  • 还有其他方法可以将哈希作为字节数组而不是 SecretKey 吗?
  • 您可以查看密钥工厂如何生成字节数组(参见源代码)并执行相同的操作。
  • 嗯,是的,我在哪里可以找到源代码?在 eclipse 中打开类只是给了我一个不是我想要的类文件
  • 我发现SecretKeyFactory调用了SecretKeyFactorySpi,它返回了所需Key Format的实例
猜你喜欢
  • 1970-01-01
  • 2012-07-09
  • 2014-10-01
  • 2011-07-11
  • 2012-12-23
  • 1970-01-01
  • 2018-02-22
  • 1970-01-01
  • 2017-11-20
相关资源
最近更新 更多