为什么char[]比String更适合密码?

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

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

489710 次浏览

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

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

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

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

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

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

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

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

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

更新

感谢这些评论,我必须更新我的答案。显然,在两种情况下,这可以增加一个(非常)小的安全改进,因为它减少了密码登陆硬盘的时间。尽管如此,我认为这对大多数用例来说是矫枉过正的。

  • 您的目标系统可能配置得很糟糕,或者您必须假设它是,并且您必须对核心转储感到偏执(如果系统不是由管理员管理,则可能是有效的)。
  • 您的软件必须过于偏执,以防止攻击者访问硬件时发生数据泄露-使用TrueCrypt(已停产)、VeraCryptCipherShed之类的东西。

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

虽然这里的其他建议似乎是有效的,但还有另一个很好的理由。使用普通的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: PasswordArray: [C@5829428e

引用官方文件,Java密码学架构指南说这是关于char[]String密码(关于基于密码的加密,但这当然更普遍地是关于密码):

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

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

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

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

本指南还对实现和使用没有数据语义知识的低级库他们正在处理。例如,低级字符串解析库可以记录它所处理的文本。应用程序可以解析SSN这就造成了一种情况,即SSN可供管理员访问日志文件。

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

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

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

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

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

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

1.由于String在Java中是不可变的,如果您将密码以纯文本形式存储,它将在内存中可用,直到垃圾收集器将其清除,并且由于String在String池中用于可重用性,因此它很可能会在内存中保留很长时间,这会造成安全威胁。

由于任何有权访问内存转储的人都可以以明文形式找到密码,这是您应该始终使用加密密码而不是纯文本的另一个原因。由于Strings是不可变的,因此无法更改Strings的内容,因为任何更改都会生成一个新的String,而如果您使用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: UnknownCharacter password: [C@110b053

引用自这个博客。希望对你有帮助

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

看看广泛使用 Spring Security库这里,问问自己-Spring Security的人是无能还是char[]密码没有多大意义。当一些讨厌的黑客抓取您RAM的内存转储时,请确保他/她会得到所有密码,即使您使用复杂的方法来隐藏它们。

然而,Java一直在变化,一些可怕的功能,如Java8的字符串重复数据删除功能可能会在你不知情的情况下实习String对象。

编辑:经过一年的安全研究,回到这个答案,我意识到这是一个相当不幸的暗示,你永远不会真正比较明文密码。请不要。使用安全的单向哈希,带有盐和合理的迭代次数.考虑使用库:这东西很难弄对!

原答复: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 passwordhello worldpassword: '***********'

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

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

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

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

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

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

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

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

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,而现有的String仍然不受影响。这可以被视为将密码存储为String的一个优势,但即使在使用后,对象也会保留在内存中。因此,如果有人以某种方式获得了对象的内存位置,那个人可以轻松跟踪存储在该位置的密码。

char[]是可变的,但它的优点是在使用后程序员可以显式清理数组或覆盖值。所以当它被使用后,它会被清理,没有人会知道你存储的信息。

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

案例字符串:

    String password = "ill stay in StringPool after Death !!!";// some long code goes// ...Now I want to remove traces of passwordpassword = 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 passwordfor (int i=0; i<passArray.length;i++){passArray[i] = 'x';}// Now you ACTUALLY DESTROYED traces of password form memory

上面有很多很好的答案。还有一点我假设(如果我错了,请纠正我)。默认情况下Java使用UTF-16来存储字符串。使用字符数组char[]数组有助于使用Unicode、区域字符等。这种技术允许所有字符集在存储密码时得到平等尊重,从此不会因为字符集混淆而引发某些加密问题。最后使用char数组,我们可以将密码数组转换为所需的字符集字符串。