为什么密码比字符串更喜欢 char []?

在 Swing 中,密码字段具有getPassword() (返回char[] )方法,而不是通常的getText() (返回String )方法。同样,我遇到了不使用String处理密码的建议。

为什么在密码方面String会对安全构成威胁?使用char[]感觉很不方便。

答案

字符串是不可变的 。这意味着一旦创建了String ,如果另一个进程可以转储内存,就无法(除了Reflection )在垃圾回收开始之前摆脱数据。

使用数组,您可以在使用完数据后显式擦除数据。您可以用任何您喜欢的东西覆盖阵列,并且即使在垃圾回收之前,密码也不会出现在系统中的任何位置。

因此,是的,这一个安全性问题 - 但是即使使用char[]只会减少攻击者的机会之窗,并且仅针对这种特定类型的攻击。

如评论中所述,垃圾回收器移动的数组可能会将数据的零散副本保留在内存中。我相信这是特定于实现的 - 垃圾收集器可能会清除所有内存,以免发生这种情况。即使这样做,在char[]仍然包含实际字符作为攻击窗口的时间内仍然存在。

尽管这里的其他建议似乎是有效的,但还有另一个很好的理由。使用纯String您更有可能不小心将密码打印到日志 ,监视器或其他不安全的地方。 char[]不太容易受到攻击。

考虑一下:

public static void main(String[] args) {
    Object pw = "Password";
    System.out.println("String: " + pw);

    pw = "Password".toCharArray();
    System.out.println("Array: " + pw);
}

印刷品:

String: Password
Array: [C@5829428e

为了引用官方文档,《 Java 密码学体系结构指南》说这是关于char[]String密码(关于基于密码的加密,但这当然更普遍地是关于密码):

收集密码并将其存储在java.lang.String类型的对象中似乎合乎逻辑。但是,有一个警告: String类型的Object是不可变的,即,没有定义的方法允许您在使用后更改(覆盖)或将String的内容归零。此功能使String对象不适合存储对安全敏感的信息,例如用户密码。您应该始终将安全敏感信息收集并存储在char数组中。

Java 编程语言版本 4.0 的安全编码准则的准则 2-2也表示类似的内容(尽管它最初是在日志记录的上下文中):

准则 2-2:请勿记录高度敏感的信息

某些信息(例如,社会安全号码(SSN)和密码)非常敏感。该信息的保存时间不应超过必要的时间,也不能保存在任何地方,即使是管理员也是如此。例如,不应将其发送到日志文件,并且不应通过搜索来检测其存在。某些瞬态数据可以保存在可变数据结构中,例如 char 数组,并在使用后立即清除。清除数据结构在典型的 Java 运行时系统上降低了效率,因为对象在内存中对程序员透明地移动。

该指南还对不具有所处理数据语义知识的低层库的实现和使用产生影响。例如,低级字符串解析库可能会记录其工作的文本。应用程序可以使用库解析 SSN。这会导致管理员可以访问日志文件使用 SSN。

使用后,可以通过将每个字符设置为零而不能将字符串设置为零来清除字符数组( char[] )。如果有人能够以某种方式看到内存映像,则在使用字符串的情况下,他们可以看到纯文本密码,但是如果使用char[] ,则在将数据清除为 0 后,密码是安全的。

有人认为,一旦不再需要密码,您就必须覆盖用于存储密码的内存。这减少了攻击者从系统读取密码的时间窗口,并且完全忽略了攻击者已经需要足够的访问权来劫持 JVM 内存的事实。具有这么多访问权限的攻击者可以捕获您的关键事件,从而使这完全无用(AFAIK,如果我错了,请纠正我)。

更新资料

感谢评论,我必须更新我的答案。显然,在两种情况下,这可以增加(非常)小的安全性改进,因为它减少了密码在硬盘驱动器上的停留时间。我仍然认为,对于大多数用例而言,这是过大的选择。

  • 您的目标系统可能配置错误,或者您必须假设目标系统配置错误,并且必须对核心转储抱有幻想(如果系统不是由管理员管理的,则可能是有效的)。
  • 您的软件必须过于偏执,以防止攻击者获得对硬件的访问权限,从而防止数据泄露 - 使用TrueCrypt (已停产), VeraCryptCipherShed 之类的东西

如果可能,禁用核心转储和交换文件将解决这两个问题。但是,它们将需要管理员权限,并且可能会减少功能(减少使用的内存),并且从运行中的系统中提取 RAM 仍然是一个有效的问题。

我认为这不是一个有效的建议,但是,我至少可以猜测原因。

我认为这样做的动机是要确保您可以在使用后立即确定地擦除内存中所有的密码痕迹。使用char[]可以确定用空格或其他东西覆盖数组的每个元素。您不能以这种方式编辑String的内部值。

但这并不是一个好的答案。为什么不仅仅确保对char[]String的引用不会转义?这样就没有安全问题了。但事实是, String对象可以在理论上进行intern()编辑,并在常量池中保持活动状态。我想使用char[]禁止这种可能性。

答案已经给出,但我想分享一下我最近在 Java 标准库中发现的一个问题。尽管他们现在非常注意在所有地方都用char[]替换密码字符串(这当然是一件好事),但在从内存中清除它时,其他对安全性要求较高的数据却似乎被忽略了。

我在想例如PrivateKey类。考虑一种方案,您将从 PKCS#12 文件中加载私有 RSA 密钥,并使用它执行一些操作。现在,在这种情况下,只要适当限制对密钥文件的物理访问,仅嗅探密码将无济于事。作为攻击者,如果直接获得密钥而不是密码,那么情况会更好。所需信息可能会泄漏,核心转储,调试器会话或交换文件只是一些示例。

事实证明,没有什么可以让您从内存中清除PrivateKey的私有信息的,因为没有 API 可以让您擦除构成相应信息的字节。

这是一种糟糕的情况,因为本文描述了如何潜在地利用这种情况。

例如,OpenSSL 库在释放私钥之前会覆盖关键内存部分。由于 Java 是垃圾回收的,因此我们需要显式的方法来擦除 Java 密钥的私有信息并使这些信息无效,这些信息将在使用密钥后立即应用。

正如乔恩 · 斯基特(Jon Skeet)所言,除了使用反射之外,别无他法。

但是,如果您可以选择反射,则可以执行此操作。

public static void main(String[] args) {
    System.out.println("please enter a password");
    // don't actually do this, this is an example only.
    Scanner in = new Scanner(System.in);
    String password = in.nextLine();
    usePassword(password);

    clearString(password);

    System.out.println("password: '" + password + "'");
}

private static void usePassword(String password) {

}

private static void clearString(String password) {
    try {
        Field value = String.class.getDeclaredField("value");
        value.setAccessible(true);
        char[] chars = (char[]) value.get(password);
        Arrays.fill(chars, '*');
    } catch (Exception e) {
        throw new AssertionError(e);
    }
}

运行时

please enter a password
hello world
password: '***********'

注意:如果字符串的 char [] 已作为 GC 周期的一部分复制,则前一个副本有可能在内存中的某个位置。

这个旧副本不会出现在堆转储中,但是如果您可以直接访问该进程的原始内存,则可以看到它。通常,您应该避免任何人具有这种访问权限。

这些都是原因,应该选择一个char []数组而不是String作为密码。

1.由于字符串在 Java 中是不可变的,因此,如果您将密码存储为纯文本格式,则密码将在内存中可用,直到垃圾回收器将其清除为止,并且由于字符串在字符串池中用于可重用性,因此很有可能长时间保留在内存中,构成安全威胁。

由于有权访问内存转储的任何人都可以以明文形式找到密码,这是另一个原因,您应该始终使用加密密码而不是纯文本。由于字符串是不可变的,因此无法更改字符串的内容,因为任何更改都会产生一个新的字符串,而如果使用 char [],仍可以将所有元素设置为空白或零。因此,将密码存储在字符数组中可以明显减轻窃取密码的安全风险。

2. Java 本身建议使用 JPasswordField 的 getPassword()方法,该方法返回 char [],而不建议使用不赞成使用的 getText()方法,该方法以明文形式返回密码,以说明安全性原因。遵循 Java 团队的建议并遵守标准而不是违背标准是很好的。

3.使用 String 总是存在在日志文件或控制台中打印纯文本的风险,但是如果您使用 Array,则不会打印数组的内容,而是会打印其内存位置。尽管这不是真正的原因,但还是有道理的。

String strPassword="Unknown";
char[] charPassword= new char[]{'U','n','k','w','o','n'};
System.out.println("String password: " + strPassword);
System.out.println("Character password: " + charPassword);

String password: Unknown
Character password: [C@110b053

从此博客引用。我希望这有帮助。

编辑:经过一年的安全研究,回到这个答案,我意识到,这很不幸地暗示您实际上会比较纯文本密码。请不要使用带有盐和合理迭代次数的安全单向哈希 。考虑使用图书馆:这东西很难弄对!

原始答案: String.equals()使用短路评估 ,因此容易受到定时攻击的事实呢?可能不太可能,但是理论上您可以安排密码比较的时间,以便确定正确的字符顺序。

public boolean equals(Object anObject) {
    if (this == anObject) {
        return true;
    }
    if (anObject instanceof String) {
        String anotherString = (String)anObject;
        int n = value.length;
        // Quits here if Strings are different lengths.
        if (n == anotherString.value.length) {
            char v1[] = value;
            char v2[] = anotherString.value;
            int i = 0;
            // Quits here at first different character.
            while (n-- != 0) {
                if (v1[i] != v2[i])
                    return false;
                i++;
            }
            return true;
        }
    }
    return false;
}

有关计时攻击的更多资源: