Answers:
字符串是不可变的。这意味着一旦创建了String
,如果另一个进程可以转储内存,则除了反射之外,您将无法清除数据,然后再进行垃圾回收。
使用数组,您可以在使用完数据后显式擦除数据。您可以用任何喜欢的东西覆盖阵列,并且即使在垃圾回收之前,密码也不会出现在系统中的任何位置。
因此,是的,这是一个安全性问题-但是即使使用,也char[]
只会减少攻击者的机会窗口,并且仅用于这种特定类型的攻击。
如评论中所述,垃圾回收器移动的数组可能会将数据的零散副本保留在内存中。我相信这是特定于实现的-垃圾收集器可能会清除所有内存,以免发生这种情况。即使这样做,仍然有一段时间char[]
包含实际角色作为攻击窗口。
char[]
位置中的密码会切断该攻击路线,使用时是不可能的String
。
尽管这里的其他建议似乎是有效的,但还有另一个很好的理由。使用plain,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
toString
是classname@hashcode
。[C
代表char[]
,其余为十六进制哈希码。
Password
为此编写一个类类型。它不那么晦涩难懂,而且更难以意外通过某个地方。
为了引用正式文档,《Java密码学体系结构指南》说了关于密码char[]
与String
密码(关于基于密码的加密,但这当然更普遍地是关于密码):
收集密码并将其存储在type对象中似乎是合乎逻辑的
java.lang.String
。但是,需要注意的是:Object
类型sString
是不可变的,即,没有定义允许您更改(覆盖)或将String
使用后的内容归零的方法。此功能使String
对象不适合存储对安全敏感的信息,例如用户密码。您应该始终将安全敏感信息收集并存储在char
阵列中。
Java编程语言版本4.0的安全编码准则的准则2-2也表示类似的内容(尽管它最初是在日志记录的上下文中):
准则2-2:请勿记录高度敏感的信息
某些信息(例如,社会安全号码(SSN)和密码)非常敏感。该信息的保存时间不应超过必要的时间,也不能保存在任何地方,即使是管理员也是如此。例如,不应将其发送到日志文件,并且不应通过搜索来检测其存在。某些瞬态数据可以保存在可变数据结构中,例如char数组,并在使用后立即清除。清除数据结构在典型的Java运行时系统上降低了效率,因为对象在内存中对程序员透明地移动。
该指南还对不具有所处理数据语义知识的低级库的实现和使用产生影响。例如,低级字符串解析库可能会记录其工作的文本。应用程序可以使用库解析SSN。这会导致管理员可以访问日志文件使用SSN。
String
已经很好地理解了它以及它在JVM中的行为…… 以安全的方式处理密码时,有充分的理由使用char[]
它String
。
At which point
是特定于应用程序的,但是一般规则是,只要您掌握了应该是密码的内容(纯文本或其他方式),便立即这样做。例如,您从浏览器获取它作为HTTP请求的一部分。您无法控制交付,但是可以控制自己的存储,因此,一旦获取它,将其放入char []中,执行所需的操作,然后将其全部设置为'0'并让gc回收它。
char[]
使用后可以通过将每个字符设置为零而不能将字符串设置为零来清除字符数组()。如果有人能以某种方式看到内存映像,则在使用字符串的情况下,他们可以看到纯文本密码,但是如果char[]
使用字符串,则在清除0数据后,密码是安全的。
HttpServletRequest
以纯文本形式传递到对象中。如果JVM版本为1.6或更低版本,它将在permgen空间中。如果它在1.7中,则在被收集之前仍然可读。(无论何时)
intern()
任意字符串的原因。但是您是对的,因为String
实例首先存在(直到收集到),char[]
之后再将它们转换为数组不会改变它。
new String()
或者StringBuilder.toString()
我管理的应用程序具有很多字符串常量,结果导致了很多permgen蠕变。直到1.7。
intern()
任意字符串可能会导致在permgen空间中分配等效的字符串。如果不存在共享该对象的相同内容的文字字符串,则后者可以进行GC处理……
有些人认为,一旦不再需要密码,就必须覆盖用于存储密码的内存。这减少了攻击者从系统读取密码的时间窗口,并且完全忽略了攻击者已经需要足够的访问权来劫持JVM内存的事实。具有这么多访问权限的攻击者可以捕获您的关键事件,从而使这完全无用(AFAIK,如果我错了,请纠正我)。
更新资料
感谢评论,我必须更新我的答案。显然,在两种情况下,这可以增加(非常)小的安全性改进,因为它减少了密码在硬盘驱动器上的停留时间。我仍然认为,对于大多数用例而言,这是过大的选择。
如果可能,禁用核心转储和交换文件将解决这两个问题。但是,它们将需要管理员权限,并且可能会减少功能(减少使用的内存),并且从运行中的系统中提取RAM仍然是一个有效的问题。
我认为这不是一个有效的建议,但是,我至少可以猜测原因。
我认为这样做的动机是要确保您可以在使用密码后立即确定地擦除内存中的所有密码痕迹。使用a char[]
可以确保用空格或其他东西覆盖数组的每个元素。您无法编辑String
这种方式。
但这并不是一个好的答案。为什么不仅仅确保对char[]
或String
不逃避引用呢?这样就没有安全问题了。但事实是,String
对象可以intern()
从理论上进行编辑,并在常量池中保持活动。我想使用char[]
禁止这种可能性。
char[]
可以修改,因此是否收集字符串无关紧要。而且由于需要对非文字进行显式的字符串插入,因此就像告诉a char[]
可以由静态字段引用一样。
答案已经给出,但是我想分享一个我最近在Java标准库中发现的问题。尽管他们现在非常注意将密码字符串替换为char[]
任何地方都(这当然是一件好事),但是从内存中清除密码时,其他对安全性要求较高的数据似乎却被忽略了。
我在想例如私钥类。考虑一种方案,您将从PKCS#12文件中加载私有RSA密钥,并使用它执行一些操作。现在,在这种情况下,只要适当限制对密钥文件的物理访问,仅嗅探密码将无济于事。作为攻击者,如果直接获得密钥而不是密码,那么情况会更好。所需信息可能会泄漏,核心转储,调试器会话或交换文件只是一些示例。
事实证明,没有什么可以让您PrivateKey
从内存中清除a的私有信息,因为没有API可以让您擦除构成相应信息的字节。
这是一个糟糕的情况,因为本文描述了如何潜在地利用这种情况。
例如,OpenSSL库在释放私钥之前会覆盖关键内存部分。由于Java是垃圾回收的,因此我们需要明确的方法来擦除Java密钥的私有信息并使它们无效,这些信息将在使用密钥后立即应用。
PrivateKey
实际上不会将其私有内容加载到内存中的实现:例如,通过PKCS#11硬件令牌。也许PKCS#11的软件实现可以负责手动清理内存。也许使用类似NSS商店(与PKCS11
Java中的商店类型共享其大部分实现)的工具会更好。该KeychainStore
(OSX密钥库)负荷私钥到它的全部内容PrivateKey
的情况下,但它不应该需要。(不知道什么WINDOWS-MY
密钥库确实在Windows上)
正如乔恩·斯基特(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周期的一部分复制,则前一个副本有可能在内存中的某个位置。
这个旧副本不会出现在堆转储中,但是如果您可以直接访问该进程的原始内存,则可以看到它。通常,您应该避免任何人具有这种访问权限。
'***********'
。
Scanner
的内部缓冲区中,并且由于您未使用System.console().readPassword()
,所以仍在控制台窗口中以可读的形式存在。但是对于大多数实际用例,usePassword
执行的持续时间是实际的问题。例如,当连接到另一台机器时,这会花费大量时间,并告诉攻击者现在是在堆中搜索密码的正确时间。唯一的解决方案是防止攻击者读取堆内存…
这些都是原因,应该选择一个char []数组而不是String作为密码。
1. 由于字符串在Java中是不可变的,因此,如果您将密码存储为纯文本格式,则密码将在内存中可用,直到垃圾回收器将其清除为止,并且由于字符串在字符串池中用于可重用性,因此很有可能长时间保留在内存中,构成安全威胁。
由于有权访问内存转储的任何人都可以以明文形式找到密码,这是另一个原因,您应该始终使用加密密码而不是纯文本。由于字符串是不可变的,因此无法更改字符串的内容,因为任何更改都会产生一个新的字符串,而如果使用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: Unknown
Character password: [C@110b053
从此博客引用。我希望这有帮助。
编辑:经过一年的安全研究,回到这个答案,我意识到,这很不幸地暗示您实际上会比较纯文本密码。请不要。使用带有盐和合理迭代次数的安全单向哈希。考虑使用图书馆:这东西很难弄对!
原始答案: 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;
}
有关计时攻击的更多资源:
Arrays.equals
for 的诱惑与for char[]
一样高String.equals
。如果有人在乎,会有专用的密钥类来封装实际的密码并解决问题—等等,真正的安全软件包有专用的密钥类,此问与答只是一种习惯,例如JPasswordField
,使用char[]
而不是String
(实际算法仍在使用byte[]
)。
sleep(secureRandom.nextInt())
无论如何都要在拒绝登录尝试之前执行类似的操作,这不仅消除了定时攻击的可能性,而且还抵消了暴力尝试。
除非您在使用后手动清理它,否则char数组不会给您vs String带来什么好处,而且我还没有看到有人真正这样做过。所以对我来说,char [] vs String的偏爱有点夸张。
在这里看看广泛使用的 Spring Security库,问问自己-Spring Security的人是不称职的还是char []密码没有多大意义。当一些讨厌的黑客抓取您RAM的内存转储时,请确保他/她将获得所有密码,即使您使用复杂的方式将其隐藏。
但是,Java一直在变化,某些令人恐惧的功能(例如Java 8的String Deduplication功能)可能会在您不知情的情况下插入String对象。但这是另一回事。
字符串是不可变的,一旦创建就不能更改。将密码创建为字符串将在堆或字符串池上留下对该密码的杂散引用。现在,如果有人对Java进程进行了堆转储并仔细扫描了一下,他也许可以猜出密码。当然,这些未使用的字符串将被垃圾回收,但这取决于GC启动的时间。
另一方面,一旦完成身份验证,就可以更改char [],您可以使用任何字符(如所有M或反斜杠)覆盖它们。现在,即使有人进行堆转储,他也可能无法获得当前未使用的密码。从某种意义上来说,这给了您更多的控制权,例如您自己清除对象内容与等待GC完成操作。
strings
和String
池(先前使用的字符串池)之前1.7均存储在PermGen的。另外,请参见5.1节: docs.oracle.com/javase/specs/jvms/se6/html/…JVM 始终检查Strings
它们是否具有相同的参考值,并会调用String.intern()
FOR YOU。结果是,每次JVM在constant_pool
或堆中检测到相同的String时,它将把它们移入permgen。我使用“蠕变蠕变”直到1.7的多个应用程序。这是一个真正的问题。
constant_pool
位于permgen中的字符串中,然后,如果多次使用字符串,则会对其进行检查。
字符串是不可变的,它进入字符串池。一旦写入,就不能覆盖。
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崩溃并生成内存转储时-您将能够看到密码。
那不一定是恶意的外部攻击者。这可能是支持用户,可以访问服务器以进行监视。他可以窥视一个崩溃转储并找到密码。
request.getPassword()
创建字符串并将其添加到池中吗?
ch = '0'
改变局部变量ch
; 它对数组没有影响。而且您的示例毫无意义,您从调用的字符串实例开始toCharArray()
,创建了一个新数组,即使正确覆盖了新数组,它也不会更改字符串实例,因此与仅使用字符串相比,它没有任何优势实例。
Arrays.fill(pass, '0');
简短而直接的答案是,因为char[]
可变的而String
对象不是可变的。
Strings
Java中的对象是不可变的。这就是为什么一旦创建便无法对其进行修改,因此从内存中删除其内容的唯一方法是对其进行垃圾回收。只有当对象释放的内存可以被覆盖并且数据将消失时,才会这样。
现在,Java中的垃圾回收不会在任何保证的时间间隔内发生。因此,String
can可以在内存中保留很长时间,并且如果在这段时间内进程崩溃,则字符串的内容可能最终出现在内存转储或某些日志中。
使用字符数组,您可以阅读密码,尽快完成密码的使用,然后立即更改内容。
是否为此目的应使用String还是使用Char []尚有争议,因为两者都有其优点和缺点。这取决于用户的需求。
由于Java中的字符串是不可变的,因此每当有人尝试操纵您的字符串时,它都会创建一个新的Object,而现有的String不会受到影响。这可能被视为将密码存储为字符串的一个优点,但是即使使用该对象,该对象仍保留在内存中。因此,如果有人以某种方式获得了对象的存储位置,则该人可以轻松地跟踪存储在该位置的密码。
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 ARRAY:
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