java:为什么密码首选char[]而不是String ?

在Swing中,password字段有一个getPassword()(返回char[])方法,而不是通常的getText()(返回String)方法。类似地,我也遇到过不使用String来处理密码的建议。

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


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

使用数组,您可以在处理完数据之后显式地擦除数据。您可以使用任何您喜欢的方法覆盖数组,并且密码不会出现在系统中的任何位置,甚至在垃圾收集之前也不会出现。

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

正如注释中所指出的,垃圾收集器移动的数组可能会在内存中留下零散的数据副本。我相信这是特定于实现的——垃圾收集器可能会清除所有内存,以避免这类事情。即使是这样,在char[]包含实际字符作为攻击窗口的时间内仍然存在。


虽然这里的其他建议似乎是有效的,但还有一个很好的理由。使用普通的String,您很有可能不小心将密码打印到日志、监视器或其他不安全的地方。char[]不那么脆弱。

考虑一下:

1
2
3
4
5
6
7
public static void main(String[] args) {
    Object pw ="Password";
    System.out.println("String:" + pw);

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

打印:

1
2
String: Password
Array: [C@5829428e


引用一份官方文件,Java Cryptography Architecture guide说这是关于char[]String的密码(关于基于密码的加密,但这当然是关于密码的更普遍的情况):

It would seem logical to collect and store the password in an object
of type java.lang.String. However, here's the caveat: Objects of
type String are immutable, i.e., there are no methods defined that
allow you to change (overwrite) or zero out the contents of a String
after usage. This feature makes String objects unsuitable for
storing security sensitive information such as user passwords. You
should always collect and store security sensitive information in a
char array instead.

Java编程语言安全编码指南的指南2-2,4.0版本也说了类似的话(尽管它最初是在日志的上下文中):

Guideline 2-2: Do not log highly sensitive information

Some information, such as Social Security numbers (SSNs) and
passwords, is highly sensitive. This information should not be kept
for longer than necessary nor where it may be seen, even by
administrators. For instance, it should not be sent to log files and
its presence should not be detectable through searches. Some transient
data may be kept in mutable data structures, such as char arrays, and
cleared immediately after use. Clearing data structures has reduced
effectiveness on typical Java runtime systems as objects are moved in
memory transparently to the programmer.

This guideline also has implications for implementation and use of
lower-level libraries that do not have semantic knowledge of the data
they are dealing with. As an example, a low-level string parsing
library may log the text it works on. An application may parse an SSN
with the library. This creates a situation where the SSNs are
available to administrators with access to the log files.


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


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

更新

感谢这些评论,我不得不更新我的答案。显然,在两种情况下,这可以增加(非常)小的安全性改进,因为它减少了密码在硬盘上停留的时间。不过,我仍然认为对于大多数用例来说,这样做是多余的。

您的目标系统可能配置得很差,或者您必须假设配置得很差,并且您必须对核心转储非常担心(如果系统不是由管理员管理的,那么它可能是有效的)。您的软件必须过于偏执,以防止数据泄漏与攻击者获得访问硬件-使用诸如TrueCrypt(中断),VeraCrypt,或密码学。

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


用Java字符串是不可变的,如果你将密码存储为纯文本,可以在内存中,直到垃圾收集器清除它,因为字符串中使用字符串池可重用性有很高的机会,它会一直保存在内存中,很长一段时间,构成安全威胁。因为任何访问内存转储的人都可以用明文找到密码Java推荐使用JPasswordField的getPassword()方法,该方法返回char[];不推荐使用的getText()方法,该方法以明文形式返回密码,说明安全性原因。

toString()总是存在在日志文件或控制台中打印纯文本的风险,但是如果使用Array,则不会打印数组的内容,而是打印数组的内存位置。

1
2
3
4
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 );

String password: passwd

Character password: [C@110b2345

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


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

我认为这样做的动机是想确保在密码被使用后,你可以迅速而确定地在内存中删除密码的所有痕迹。使用char[],您可以使用空白或其他内容覆盖数组中的每个元素。您不能以这种方式编辑String的内部值。

但这并不是一个好的答案;为什么不确保对char[]String的引用不转义呢?那么就没有安全问题了。但问题是,String对象在理论上可以被intern()创建,并在常量池中保持活动状态。我认为使用char[]禁止这种可能性。


答案已经给出了,但是我想与您分享我最近在Java标准库中发现的一个问题。虽然他们现在非常小心地在任何地方使用char[]替换密码字符串(当然这是一件好事),但是当涉及到从内存中清除密码字符串时,其他安全关键数据似乎被忽略了。

我考虑的是像PrivateKey这样的课程。考虑这样一个场景:您将从PKCS#12文件加载一个私有RSA密钥,并使用它执行一些操作。在本例中,只要对密钥文件的物理访问受到适当限制,仅嗅探密码对您就没有多大帮助。作为攻击者,如果您直接获得密钥而不是密码,情况会好得多。所需的信息可能会泄漏,例如歧管、核心转储、调试器会话或交换文件。

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

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

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


正如Jon Skeet所说,除了使用反射,没有其他方法。

但是,如果反射是一种选择,您可以这样做。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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);
    }
}

运行时

1
2
3
please enter a password
hello world
password: '***********'

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

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


这些都是原因,应该选择char[]数组而不是字符串作为密码。

1. 因为在Java字符串是不可变的,如果你的密码存储为纯文本将在内存中,直到垃圾收集器清除它,因为字符串在字符串池中用于可重用性有很高的可能性,它会一直保存在内存中,很长一段时间,构成安全威胁。

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

2. Java本身推荐使用JPasswordField的getPassword()方法,该方法返回一个char[],而不建议使用getText()方法,该方法以明文返回说明安全原因的密码。听从Java团队的建议,坚持标准,而不是违背标准,这是很好的。

3.使用String总是有在日志文件或控制台中打印纯文本的风险,但是如果使用数组,则不会打印数组的内容,而是打印其内存位置。虽然这不是一个真正的原因,但仍然有意义。

1
2
3
4
5
6
7
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

引用自本博客。我希望这能有所帮助。


编辑:经过一年的安全研究之后,回到这个问题上,我意识到它带来了一个相当不幸的暗示,即你永远不会真正比较明文密码。请不要。使用带salt的安全单向散列和合理次数的迭代。考虑使用图书馆:这些东西很难正确使用!

原始答案:那么String.equals()使用短路求值,因此很容易受到定时攻击,这一事实又如何呢?这可能不太可能,但是理论上您可以对密码比较进行计时,以确定正确的字符序列。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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数组没有给你vs字符串任何东西,除非你在使用后手动清理它,我还没有看到任何人这样做。所以在我看来,char[]与String之间的偏爱有点夸张。

看看这里广泛使用的 Spring安全库,问问自己——Spring安全人员是否无能,或者char[]密码没有多大意义。当某个可恶的黑客窃取了你的内存转储文件时,即使你使用复杂的方法来隐藏它们,也要确保她会得到所有的密码。

然而,Java一直在变化,一些可怕的特性,比如Java 8的字符串重复数据删除特性,可能会在您不知情的情况下实习字符串对象。但那是不同的谈话。


字符串是不可变的,一旦创建就不能更改。将密码创建为字符串将在堆或字符串池中留下对密码的零星引用。现在,如果有人对Java进程进行堆转储并仔细扫描,他可能能够猜出密码。当然,这些未使用的字符串将被垃圾收集,但这取决于GC何时开始工作。

另一方面,一旦身份验证完成,char[]是可变的,您可以用任何字符(比如所有M或反斜杠)覆盖它们。现在,即使有人进行堆转储,他也可能无法获得当前未使用的密码。在某种意义上,这给了您更多的控制,比如自己清除对象内容,而不是等待GC执行此操作。


简单而直接的答案是,char[]是可变的,而String对象不是可变的。

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

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

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


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

2) Java本身推荐使用JPasswordField的getPassword()方法,该方法返回char[],不推荐使用getText()方法,该方法以明文形式返回密码,说明安全性原因。听从Java团队的建议,坚持标准,而不是违背标准,这是很好的。


String是不可变的,它进入字符串池。一旦写好了,就不能再写了。

char[]是一个数组,一旦你使用了密码,你就应该重写它。

1
2
3
4
5
6
7
8
9
10
11
12
13
char[] passw = request.getPassword().toCharArray()
if (comparePasswords(dbPassword, passw) {
 allowUser = true;
 cleanPassword(passw);
 cleanPassword(dbPassword);
 passw=null;
}

private static void cleanPassword (char[] pass) {
 for (char ch: pass) {
  ch = '0';
 }
}

攻击者可以使用它的一个场景是crashdump——当JVM崩溃并生成内存转储时——您将能够看到密码。

这并不一定是恶意的外部攻击者。这可能是一个支持用户,可以访问服务器进行监视。他可以偷看一个垃圾场,找到密码。


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

使用字符数组,一旦达到密码的目的,就可以修改或删除数组的内容。数组的原始内容在修改后甚至在垃圾收集开始之前都不会在内存中找到。出于安全考虑,最好将密码存储为字符数组。


将密码使用到String或Char[]中总是有争议的,因为这两种方法都有各自的实用价值和缺点。它取决于用户期望实现的需求。以下几行可能有助于更好地理解何时使用哪个容器:由于java中的String是不可变的,所以每当有人试图操纵您的String时,它就会创建一个新对象,而现有的对象则保持不变。从另一方面来说,这可以看作是将密码存储为字符串的优势。即使在使用之后,String对象仍然保存在内存中。因此,如果有人获得了内存位置,就可以很容易地跟踪存储在该位置的密码。虽然Char[]是可变的,但它的优势在于,在使用之后,程序员可以明确地清除数组或覆盖值。因此,在不使用之后,它就会被清除,没有人能够知道您存储的信息。

基于上面的情况,我们可以知道应该使用String还是Char[]来满足他们的需求。

谢谢。


简而言之,这是因为在您使用完存储的密码之后,它可能会发生什么变化。

一个字符串可以(潜在地)在您停止使用它之后在内存中保留很长时间,但是一个字符数组可以被清空,因此它不再包含数据。

这都与垃圾收集器和不可变对象的工作方式有关。