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

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


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

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

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

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


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


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

我认为动机是希望确保在密码被使用后,您可以及时且确定地删除记忆中的所有密码痕迹。使用char[],您可以用空白或确定的内容覆盖数组的每个元素。不能这样编辑字符串的内部值。

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


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

使现代化

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

您的目标系统可能配置不正确,或者您必须假设它是错误的,并且您必须对核心转储(如果系统不是由管理员管理的,则可能是有效的)心存疑虑。您的软件必须过于偏执,以防止攻击者使用TrueCrypt(已停产)、VeraCrypt或CipherShed等工具访问硬件时发生数据泄漏。

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


虽然这里的其他建议似乎有效,但还有一个很好的理由。使用纯字符串时,意外将密码打印到日志、监视器或其他不安全的地方的可能性要大得多。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 Cryptography Architecture指南介绍了char[]与String密码(关于基于密码的加密,但这更一般地是关于密码):

将密码收集并存储在对象中似乎是合乎逻辑的然而,这里有一个警告:类型String是不可变的,即没有定义允许您更改(覆盖)或清零字符串的内容使用后。此功能使字符串对象不适合存储诸如用户密码之类的安全敏感信息。你应始终在char数组。

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

准则2-2:不记录高度敏感信息一些信息,如社会安全号码(SSN)和密码是高度敏感的。此信息不应保留无论在什么地方,甚至在管理员。例如,不应将其发送到日志文件和它的存在不应该通过搜索来检测。一些瞬态数据可以保存在可变数据结构中,例如char数组,以及使用后立即清除。清除数据结构减少了当对象移入时,在典型的Java运行时系统上的有效性存储器对程序员来说是透明的。本指南还对实施和使用没有数据语义知识的低级库他们正在处理。例如,低级字符串解析库可以记录其工作的文本。应用程序可以解析SSN与图书馆。这会导致SSN管理员可以访问日志文件。


答案已经给出了,但我想与大家分享一下我最近在Java标准库中发现的一个问题。尽管他们现在非常小心地将密码字符串替换为随处可见的char[](这当然是一件好事),但在从内存中清除密码时,其他安全关键数据似乎被忽略了。

我正在考虑例如PrivateKey类。考虑一个场景,您将从PKCS#12文件加载一个专用RSA密钥,并使用它执行一些操作。现在,在这种情况下,只要对密钥文件的物理访问受到适当限制,单独嗅探密码对您没有多大帮助。作为一名攻击者,如果您直接获得密钥而不是密码,您的情况会更好。所需的信息可能是多方面的泄漏,核心转储、调试器会话或交换文件只是一些示例。

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

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

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


除非您在使用后手动清理,否则char数组不会为您提供字符串,我还没有看到有人真正这样做。所以对我来说,char[]和String的偏好有点夸张。

看看这里广泛使用的SpringSecurity库,问问你自己——SpringSecurity的家伙们是无能还是字符密码根本没有意义。当一些讨厌的黑客从你的RAM中获取内存转储时,即使你使用复杂的方法隐藏密码,也要确保他/她会得到所有密码。

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


编辑:经过一年的安全研究后,我回到了这个答案,我意识到这意味着你可能会比较明文密码。请不要。使用一个安全的单向散列和一个合理的迭代次数。考虑使用图书馆:这东西很难弄对!

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

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

定时攻击的教训关于信息安全堆栈交换上定时攻击的讨论当然,定时攻击维基百科页面


正如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: '***********'

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

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


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

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


简单明了的答案是因为char[]是可变的,而String对象不是可变的。

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

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

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


Java中的字符串是不可变的。因此,无论何时创建字符串,它都将保留在内存中,直到被垃圾收集。因此,任何访问内存的人都可以读取字符串的值。

如果字符串的值被修改,那么它将最终创建一个新字符串。因此,原始值和修改后的值都保留在内存中,直到被垃圾收集。

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

出于安全考虑,最好将密码存储为字符数组。


字符串是不可变的,它将进入字符串池。一旦写入,就无法覆盖。

char[]是一个数组,当您使用密码时,应该覆盖该数组,这是应该这样做的:

char[] passw = request.getPassword().toCharArray()
if (comparePasswords(dbPassword, passw) {
  allowUser = true;
  cleanPassword(passw);
  cleanPassword(dbPassword);
  passw = null;
}

private static void cleanPassword (char[] pass) {

  Arrays.fill(pass, '0');
}

攻击者可以使用它的一种情况是,当JVM崩溃并生成内存转储时,您将能够看到密码。

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


是否应该使用String或Char[]来实现这一目的是有争议的,因为两者都有各自的优点和缺点。这取决于用户需要什么。

由于Java中的字符串是不可变的,所以每当有人试图操纵字符串时,它会创建一个新的Object,而现有的字符串不会受到影响。这可以被视为将密码存储为字符串的一个优点,但即使在使用后,该对象仍保留在内存中。因此,如果有人以某种方式获得了对象的内存位置,那么此人可以很容易地跟踪存储在该位置的密码。

Char[]是可变的,但它的优点是在使用后,程序员可以显式地清理数组或重写值。因此,当它被使用后,它会被清理干净,没有人会知道你存储的信息。

基于以上情况,我们可以根据他们的需求来决定是使用String还是使用Char[]。


大小写字符串:

    String password = "ill stay in StringPool after Death !!!";
    // some long code goes
    // ...Now I want to remove traces of password
    password = null;
    password = "";
    // above attempts wil change value of password
    // but the actual password can be traced from String pool through memory dump, if not garbage collected

案例CHAR阵列:

    char[] passArray = {'p','a','s','s','w','o','r','d'};
    // some long code goes
    // ...Now I want to remove traces of password
    for (int i=0; i<passArray.length;i++){
        passArray[i] = 'x';
    }
    // Now you ACTUALLY DESTROYED traces of password form memory

前面的很多答案都很好。还有一点我在假设(如果我错了,请纠正我)。

默认情况下,Java使用UTF-16存储字符串。使用字符数组、char[]数组,有助于使用Unicode、区域字符等。该技术允许所有字符集在存储密码时受到同等尊重,因此不会因字符集混淆而引发某些加密问题。最后,使用字符数组,我们可以将密码数组转换为所需的字符集字符串。