提问



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


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

最佳参考


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


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


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


正如评论中所指出的那样,垃圾收集器移动的数组可能会将数据的杂散副本留在内存中。我相信这是特定于实现的 - 垃圾收集器可以清除所有内存事实上,为了避免这种情况。即使它确实存在,char[]仍然包含实际角色作为攻击窗口的时间。

其它参考1


虽然这里的其他建议似乎有效,但还有另外一个好理由。使用普通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

其它参考2


引用官方文档,Java密码体系结构指南说明了char[]String密码(关于基于密码的加密,但当然更常见的是关于密码):[49]



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



Java编程语言安全编码指南的准则2-2也说了类似的东西(虽然它最初是在日志记录的上下文中):[50]



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

  
  一些信息,例如社会安全号码(SSN)和
  密码,非常敏感。不应保留此信息
  超过必要的时间,甚至可以看到的地方
  管理员。例如,它不应该发送到日志文件和
  它的存在不应该通过搜索检测到。一些短暂的
  数据可以保存在可变数据结构中,例如char数组,和
  使用后立即清除。清算数据结构已减少
  移动对象时,典型Java运行时系统的有效性
  内存对程序员透明。

  
  该指南也对实施和使用有影响
  没有数据语义知识的低级库
  他们正在处理。例如,一个低级字符串解析
  库可能会记录它所使用的文本。应用程序可以解析SSN
  与图书馆。这会产生SSN的情况
  可供具有日志文件访问权限的管理员使用。


其它参考3


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

其它参考4


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


更新


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



  • 您的目标系统可能配置错误,或者您必须假设它并且您必须对核心转储感到偏执(如果系统不由管理员管理,则可能有效)。

  • 您的软件必须过于偏执,以防止数据泄漏,攻击者可以访问硬件 - 使用TrueCrypt(已停用),VeraCrypt或CipherShed等内容。



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

其它参考5



  1. 在Java中字符串是不可变的如果您将密码存储为纯文本,它将在内存中可用,直到垃圾收集器清除它并且因为字符串池中使用字符串以便重新使用,所以它非常高它会长时间留在记忆中的可能性,这会带来安全威胁。由于任何有权访问内存转储的人都可以以明文形式找到密码

  2. Java推荐使用JPasswordField的getPassword()方法,该方法返回一个char [[]]和不推荐的getText()方法,该方法以明文形式返回密码,说明安全原因。

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


    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 [[]]还不够,但您需要删除内容才能更安全。我还建议使用散列或加密密码而不是纯文本,并在验证完成后立即从内存中清除它。

其它参考6


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


我认为动机是要确保您可以在使用后及时清除内存中的所有密码。使用char[],您可以用空白或某些东西覆盖数组的每个元素。你不能那样编辑String的内部值。


但仅此一点并不是一个好的答案;为什么不确保对char[]String的引用没有逃脱?然后没有安全问题。但问题是String对象可以在理论上[[并intern()编辑并在常数池中保持活着。我想使用char[]禁止这种可能性。

其它参考7


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


我想到的是例如PrivateKey类。考虑一个场景,你将从PKCS#12文件加载一个私有RSA密钥,用它来执行一些操作。现在在这种情况下,单独嗅探密码对你没什么帮助只要对密钥文件的物理访问得到适当限制。作为攻击者,如果您直接获得密钥而不是密码,那将会好得多。所需的信息可以泄漏,核心转储,调试器会话或交换文件只是一些例子。[54]


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


这是一个糟糕的情况,因为本文描述了这种情况如何可能被利用。[55]


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

其它参考8


正如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循环的一部分,则前一个副本可能在内存中的某个位置。


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

其它参考9


 在经过一年的安全研究之后回到这个答案,我意识到它实际上比较明确地比较了明文密码。请不要。使用带有盐和合理迭代次数的安全单向散列。考虑使用库:这些东西很难正确!


原始答案: String.equals()使用短路评估,因此容易受到时序攻击的影响?它可能不太可能,但你可以理论上时间密码比较,以确定正确的字符序列。[57]


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;
}


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



  • 时间攻击课程

  • 有关信息安全堆栈交换的定时攻击的讨论

  • 当然还有Timing Attack Wikipedia页面


其它参考10


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


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


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


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


    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
希望这会有所帮助。[61]

其它参考11


没有任何char数组给你vs String,除非你在使用后手动清理它,我没有看到任何人真正这样做。所以对我来说char [[]] vs String的偏好有点夸张。


看一下广泛使用的 Spring Security库,并问问自己 - Spring Security人员不称职或char [[]]密码只是没有多大意义。当一些讨厌的黑客抓住你RAM的内存转储时即使你使用复杂的方法来隐藏它们,也要确保她会获得所有密码。[62]


但是,Java一直在变化,Java 8的字符串重复数据删除功能等一些可怕的功能可能会在您不知情的情况下实施String对象。但这是不同的对话。

其它参考12


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


另一方面,一旦认证完成,char [[]]就是可变的你可以用任何字符覆盖它们,比如所有M或反斜杠。现在即使有人进行堆转储,他也许无法获得密码。目前还没有使用。这样就可以让你在自己清除对象内容和等待GC执行操作时获得更多控制权。

其它参考13


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


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


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


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

其它参考14


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


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

其它参考15


java中的字符串是不可变的。因此,每当创建一个字符串时,它将保留在内存中,直到它被垃圾收集。所以任何有权访问内存的人都可以读取字符串的值。

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


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


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

其它参考16


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


char[]是一个在使用密码后应该覆盖的数组,这是应该如何完成的:


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 = null;
 }
}


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


这不一定是恶意的外部攻击者。这可以是支持用户,可以访问服务器以进行监视。他可以查看故障转储并找到密码。