字符串是不可变的。这意味着一旦您创建了 String
,如果另一个进程可以转储内存,那么(除了 reflection)您将无法在 garbage collection 启动之前删除数据。
使用数组,您可以在完成后显式擦除数据。你可以用你喜欢的任何东西覆盖数组,密码不会出现在系统的任何地方,甚至在垃圾收集之前。
所以是的,这是一个安全问题 - 但即使使用 char[]
也只会减少攻击者的机会之窗,而且它仅适用于这种特定类型的攻击。
如评论中所述,垃圾收集器移动的数组可能会将数据的杂散副本留在内存中。我相信这是特定于实现的 - 垃圾收集器可能清除所有内存,以避免这种事情。即使是这样,仍有一段时间 char[]
包含实际字符作为攻击窗口。
虽然这里的其他建议似乎有效,但还有一个很好的理由。使用普通的 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 Cryptography Architecture guide 是关于 char[]
与 String
密码(关于基于密码的加密,但这当然更普遍地与密码有关):
将密码收集并存储在 java.lang.String 类型的对象中似乎是合乎逻辑的。但是,需要注意的是:String 类型的对象是不可变的,即没有定义允许您在使用后更改(覆盖)或清零 String 内容的方法。此功能使 String 对象不适合存储用户密码等安全敏感信息。您应该始终将安全敏感信息收集并存储在 char 数组中。
Guideline 2-2 of the Secure Coding Guidelines for the Java Programming Language, Version 4.0 也说了类似的话(尽管它最初是在日志记录的上下文中):
准则 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 crawl。直到 1.7。
intern()
可能会导致在 permgen 空间中分配等效字符串。如果没有共享该对象的相同内容的文字字符串,则后者可能会被 GC 处理……
有些人认为,一旦不再需要密码,就必须覆盖用于存储密码的内存。这减少了攻击者必须从您的系统读取密码的时间窗口,并且完全忽略了攻击者已经需要足够的访问权限来劫持 JVM 内存来执行此操作的事实。具有如此多访问权限的攻击者可以捕获您的关键事件,从而使其完全无用(AFAIK,如果我错了,请纠正我)。
更新
感谢评论,我必须更新我的答案。显然,在两种情况下,这可以增加(非常)小的安全性改进,因为它减少了密码可能落在硬盘上的时间。我仍然认为对于大多数用例来说这太过分了。
您的目标系统可能配置错误,或者您必须假设它是,并且您必须对核心转储持偏执态度(如果系统不是由管理员管理,则可能有效)。
您的软件必须过于偏执,以防止攻击者获得对硬件的访问权限而导致数据泄露——使用诸如 TrueCrypt(已停产)、VeraCrypt 或 CipherShed 之类的东西。
如果可能,禁用核心转储和交换文件将解决这两个问题。但是,它们需要管理员权限,并且可能会减少功能(使用更少的内存)并且从正在运行的系统中提取 RAM 仍然是一个有效的问题。
我不认为这是一个有效的建议,但是,我至少可以猜到原因。
我认为动机是希望确保您可以在使用后立即并确定地清除内存中的所有密码痕迹。使用 char[]
,您可以肯定地用空白或其他内容覆盖数组的每个元素。您不能以这种方式编辑 String
的内部值。
但这并不是一个好的答案。为什么不只是确保对 char[]
或 String
的引用不会逃逸?那么就没有安全问题了。但问题是 String
对象在理论上可以intern()
编辑并在常量池中保持活动状态。我想使用 char[]
禁止这种可能性。
char[]
可以修改,然后它是否被收集无关紧要。而且由于需要为非文字显式地完成字符串实习,这就像告诉静态字段可以引用 char[]
一样。
答案已经给出,但我想分享一个我最近在 Java 标准库中发现的问题。虽然他们现在非常注意用 char[]
替换密码字符串(这当然是一件好事),但在从内存中清除其他安全关键数据时似乎被忽略了。
我正在考虑例如 PrivateKey 类。考虑一个场景,您将从 PKCS#12 文件加载私有 RSA 密钥,并使用它来执行某些操作。现在在这种情况下,只要适当限制对密钥文件的物理访问,单独嗅探密码对您没有多大帮助。作为攻击者,如果您直接获得密钥而不是密码,您的情况会好得多。所需的信息可以是多种泄漏、核心转储、调试器会话或交换文件只是一些示例。
事实证明,没有什么可以让您从内存中清除 PrivateKey
的私有信息,因为没有 API 可以让您擦除形成相应信息的字节。
这是一个糟糕的情况,因为此 paper 描述了如何可能利用这种情况。
例如,OpenSSL 库会在释放私钥之前覆盖关键内存部分。由于 Java 是垃圾收集的,因此我们需要明确的方法来擦除和无效 Java 密钥的私有信息,这些信息将在使用密钥后立即应用。
PrivateKey
的实现,它实际上不会将其私有内容加载到内存中:例如,通过 PKCS#11 硬件令牌。也许 PKCS#11 的软件实现可以手动清理内存。也许使用类似 NSS 存储(它与 Java 中的 PKCS11
存储类型共享其大部分实现)之类的东西会更好。 KeychainStore
(OSX 密钥库)将私钥的全部内容加载到其 PrivateKey
实例中,但它不需要这样做。 (不确定 WINDOWS-MY
KeyStore 在 Windows 上的作用。)
char[]
,因为我正在查看 JDK 9 中添加的这个不错的新 ConnectionBuilder
类,它仍然有 password(String)
并且没有通过 {1 的选项}。似乎使用 Java 有很多“做我说的,而不是我做的”。
正如 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 总是存在在日志文件或控制台中打印纯文本的风险,但是如果您使用数组,您将不会打印数组的内容,而是打印其内存位置。虽然不是真正的原因,但它仍然是有道理的。
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
引用自 this blog。我希望这有帮助。
编辑: 在经过一年的安全研究后回到这个答案,我意识到这意味着您实际上会比较明文密码,这是相当不幸的暗示。请不要。 Use a secure one-way hash with a salt and a reasonable number of iterations。考虑使用库:这些东西很难做对!
原始答案: String.equals() 使用 short-circuit evaluation 并因此容易受到计时攻击这一事实又如何呢?这可能不太可能,但您可以理论上对密码比较进行计时,以确定正确的字符序列。
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 页面
Arrays.equals
用于 char[]
的诱惑与 String.equals
一样高。如果有人关心的话,这里有一个专门的密钥类来封装实际密码并处理问题——哦等等,真正的安全包有专门的密钥类,这个问答只是关于一个习惯 在它们之外,例如 JPasswordField
,使用 char[]
而不是 String
(实际算法无论如何都使用 byte[]
)。
sleep(secureRandom.nextInt())
的操作,这不仅消除了定时攻击的可能性,还可以抵消暴力尝试。
除非您在使用后手动清理它,否则 char 数组不会为您提供与 String 相比的任何东西,而且我还没有看到有人真正这样做。所以对我来说 char[] vs String 的偏好有点夸张。
看看广泛使用 Spring Security 库here,问问自己 - Spring Security 的人是无能的还是 char[] 密码没有多大意义。当一些讨厌的黑客窃取您 RAM 的内存转储时,即使您使用复杂的方法来隐藏它们,请确保他/她会获得所有密码。
然而,Java 一直在变化,一些可怕的特性(如 String Deduplication feature of Java 8)可能会在您不知情的情况下实习 String 对象。但这是一个不同的对话。
字符串是不可变的,一旦创建就不能更改。将密码创建为字符串会在堆或字符串池上留下对密码的杂散引用。现在,如果有人对 Java 进程进行堆转储并仔细扫描,他也许能够猜出密码。当然,这些未使用的字符串将被垃圾收集,但这取决于 GC 何时启动。
另一方面,一旦身份验证完成,char[] 是可变的,您可以用任何字符(如所有 M 或反斜杠)覆盖它们。现在,即使有人进行堆转储,他也可能无法获取当前未使用的密码。从某种意义上说,这为您提供了更多控制权,例如自己清除 Object 内容而不是等待 GC 执行它。
strings
和 String
池(以前使用的字符串池)都存储在 1.7 之前的 Permgen 中。此外,请参阅第 5.1 节:docs.oracle.com/javase/specs/jvms/se6/html/… JVM 总是检查 Strings
以查看它们是否是相同的参考值,并会为您调用 String.intern()
。结果是每次 JVM 在 constant_pool
或堆中检测到相同的字符串时,都会将它们移动到 permgen。在 1.7 之前,我使用“creeping permgen”开发了几个应用程序。这是一个真正的问题。
constant_pool
中,然后如果一个字符串被多次使用,它将被拦截。
字符串是不可变的,它进入字符串池。一旦写入,就无法覆盖。
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
对象不是。
Java 中的 Strings
是不可变对象。这就是为什么它们一旦创建就不能被修改的原因,因此从内存中删除它们的内容的唯一方法是让它们被垃圾收集。只有当对象释放的内存可以被覆盖时,数据才会消失。
现在,Java 中的垃圾收集不会在任何保证的时间间隔内发生。因此,String
可以在内存中保留很长时间,如果在此期间进程崩溃,则字符串的内容可能最终会出现在内存转储或某些日志中。
使用字符数组,您可以读取密码,尽快完成操作,然后立即更改内容。
案例字符串:
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[] 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中的字符串是不可变的。因此,无论何时创建一个字符串,它都会保留在内存中,直到它被垃圾回收。所以任何有权访问内存的人都可以读取字符串的值。如果字符串的值被修改,那么它将最终创建一个新字符串。因此,原始值和修改后的值都保留在内存中,直到它被垃圾回收。使用字符数组,一旦达到密码的目的,就可以修改或删除数组的内容。数组的原始内容在修改后甚至在垃圾收集开始之前都不会在内存中找到。出于安全考虑,最好将密码存储为字符数组。
是否应该为此使用 String 或使用 Char[] 是有争议的,因为两者都有其优点和缺点。这取决于用户需要什么。
由于 Java 中的字符串是不可变的,因此每当有人尝试操作您的字符串时,它都会创建一个新对象,而现有的字符串不受影响。这可以被视为将密码存储为字符串的优势,但即使在使用后该对象仍保留在内存中。因此,如果有人以某种方式获得了对象的内存位置,则该人可以轻松追踪存储在该位置的密码。
Char[] 是可变的,但它的优点是在使用后程序员可以显式清理数组或覆盖值。因此,当它使用完毕时,它会被清理干净,没有人会知道您存储的信息。
根据以上情况,可以根据自己的需求判断是使用 String 还是使用 Char[] 。
上面有很多很棒的答案。我假设还有一点(如果我错了,请纠正我)。默认情况下,Java 使用 UTF-16 存储字符串。使用字符数组 char[]array 有助于使用 unicode、区域字符等。这种技术允许在存储密码时平等地尊重所有字符集,并且今后不会由于字符集混淆而引发某些加密问题。最后使用 char 数组,我们可以将密码数组转换为我们想要的字符集字符串。
char[]
位置的密码会切断该攻击线,而使用String
则无法做到这一点。