为什么字符[]优先于字符串的密码?

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

为什么String在涉及密码时对安全构成威胁?使用感觉不方便char[]

 答案


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

使用数组,您可以在完成数据后明确清除数据。你可以用你喜欢的任何东西覆盖这个数组,甚至在垃圾收集之前密码也不会出现在系统的任何地方。

所以是的,这一个安全问题 – 但即使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。然而,这里需要注意的是:Objects的类型String是不可变的,也就是说,没有定义的方法可以让你改变(覆盖)或者String 使用后的内容为零。此功能使String对象不适合存储安全敏感信息(如用户密码)。您应该始终将安全敏感信息收集并存储在 char数组中。

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

准则2-2:不要记录高度敏感的信息

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

本指南还对实施和使用不具备所处理数据的语义知识的低级库有影响。作为一个例子,一个低级别的字符串解析库可能会记录它所处理的文本。应用程序可以解析一个SSN与库。这创造了SSN可供管理员访问日志文件的情况。


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


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

更新

感谢评论我必须更新我的答案。显然有两种情况可以增加(非常)小的安全性改进,因为它减少了密码可能落在硬盘上的时间。尽管如此,我认为这对大多数用例来说都是过度的。

  • 您的目标系统可能配置错误,或者您必须假定它是,并且您必须对核心转储产生偏执(如果系统不是由管理员管理,则可能是有效的)。
  • 您的软件必须过分偏执,以防止攻击者访问硬件时发生数据泄露 – 使用诸如TrueCrypt(已停用),VeraCryptCipherShed等软件

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


  1. 如果将密码存储为纯文本,那么字符串在Java中是不可变的,直到垃圾收集器清除它为止,并且由于字符串用于字符串池以提供可重用性,所以它很可能会长时间保留在内存中持续时间,这构成安全威胁。由于任何有权访问内存转储的人都可以以明文形式找到密码
  2. Java推荐使用getPassword()JPasswordField的方法返回一个char []和弃用的getText()方法,它以明文形式返回密码,说明安全原因。
  3. toString()在日志文件或控制台中始终存在打印纯文本的风险,但如果使用Array,则不会打印数组的内容,而是打印其内存位置。
    String strPwd = "passwd";
    char[] charPwd = new char[]{'p','a','s','s','w','d'};
    System.out.println("String password: " + strPwd );
    System.out.println("Character password: " + charPwd );

    字符串密码:passwd

    字符密码:[C @ 110b2345

最后的想法:尽管使用char []不够,你需要擦除内容更安全。我还建议使用散列或加密密码而不是纯文本,并在身份验证完成后立即从内存中清除它。


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

我认为这样做的动机是希望确保在使用后可以及时并确定地删除内存中的所有密码。用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 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

参考:http : //javarevisited.blogspot.com/2012/03/why-character-array-is-better-than.html 希望这会有所帮助。


编辑:经过一年的安全研究后,回到这个答案,我意识到它会让你相信比较明文密码的相当不幸的暗示。请不要。使用安全的单向散列和盐以及合理数量的迭代。考虑使用库:这种东西很难得到正确的!

原始答案: 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;
}

更多关于定时攻击的资源:


除非你在使用后手动清理它,否则没有什么是char数组给你的,并且我还没有看到有人真的这样做。所以对我来说,char [] vs String的偏好有点夸张。

看看这里广泛使用的 Spring Security库并问问自己 – Spring Security家伙是无能的还是char []密码没有多大意义。当一些讨厌的黑客抓住你的RAM的内存转储时,即使你使用复杂的方式来隐藏它们,也要确保她能够获得所有的密码。

但是,Java始终在变化,而Java 8的String Deduplication功能等一些可怕的功能可能会在您不知情的情况下嵌入String对象。但那是不同的谈话。


字符串是不可变的,一旦创建就无法更改。以字符串形式创建密码将导致对堆或String池上密码的散布引用。现在,如果有人需要Java进程的堆转储并仔细扫描,他可能会猜到密码。当然,这些未使用的字符串将被垃圾收集,但这取决于GC何时启动。

另一方面,只要身份验证完成,char []就可以被修改,您可以用任何字符(如所有M或反斜杠)覆盖它们。现在,即使有人需要堆转储,他也可能无法获取当前未使用的密码。这给你更多的控制权,像你自己清理对象内容,等待GC去做。


简短而直接的答案是因为char[]可变而String对象不可以。

Strings在Java中是不可变的对象。这就是为什么一旦创建它们就不能被修改的原因,因此将它们的内容从内存中移除的唯一方法是让它们被垃圾收集。只有当对象释放的内存可以被覆盖时,数据才会消失。

现在,Java中的垃圾收集不会在任何保证的时间间隔内发生。因此,String可以在内存中保存很长时间,并且如果在此期间进程崩溃,则字符串的内容可能会以内存转储或某些日志结束。

使用字符数组,您可以读取密码,尽快完成处理,然后立即更改内容。


1)由于Strings在Java中是不可变的,所以如果将密码存储为纯文本,它将在内存中可用,直到垃圾收集器清除它为止,并且由于字符串池用于重用,因此它很可能保留在内存中持续很长时间,这构成安全威胁。由于任何有权访问内存转储的人都可以以明文形式找到密码,这也是您应该始终使用加密密码而不是纯文本的另一个原因。由于字符串是不可变的,所以没有办法改变字符串的内容,因为任何改变都会产生新的字符串,而如果你使用char [],你仍然可以将其所有元素都设置为空或零。因此,在字符数组中存储密码可以降低窃取密码的安全风险。

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


Java中的字符串是不可变的。所以无论何时创建一个字符串,它都会保留在内存中,直到它被垃圾回收。所以任何有权访问内存的人都可以读取字符串的值。
如果字符串的值被修改,那么它将最终创建一个新的字符串。所以原始值和修改后的值都保留在内存中直到垃圾收集。

使用字符数组,可以在提供密码的目的后修改或删除数组的内容。数组的原始内容在修改之后甚至在垃圾回收开始之前不会在内存中找到。

由于安全问题,最好将密码存储为字符数组。

添加评论

友情链接:蝴蝶教程